Skip to content

Commit

Permalink
Add support for containerContributions
Browse files Browse the repository at this point in the history
Add support for attributes

  controller.devfile.io/container-contribution: true
  controller.devfile.io/merge-contribution: true

When a container-contribution component can be matched with a
merge-contribution component, the two are merged:

  * resource requirements (memory, cpu) are added together
  * the image from the contribution is ignored
  * remaining fields are merged using a strategic merge patch

This can be used to e.g. update an existing devworkspace component to
inject configuration or an editor into that container without having to
update the DevWorkspace

Signed-off-by: Angel Misevski <amisevsk@redhat.com>
  • Loading branch information
amisevsk committed May 31, 2022
1 parent 9b9d8fd commit 79bd392
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 0 deletions.
10 changes: 10 additions & 0 deletions pkg/constants/attributes.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,14 @@ const (
// EndpointURLAttribute is an attribute added to endpoints to denote the endpoint on the cluster that
// was created to route to this endpoint
EndpointURLAttribute = "controller.devfile.io/endpoint-url"

// ContainerContributionAttribute defines a container component as a container contribution that should be merged
// into an existing container in the devfile if possible. If no suitable container exists, this component
// is treated as a regular container component
ContainerContributionAttribute = "controller.devfile.io/container-contribution"

// MergeContributionAttribute defines a container component as a target for merging a container contribution. If
// present on a container component, any container contributions will be merged into that container. If multiple
// container components have the merge-contribution attribute, the first one will be used and all others ignored.
MergeContributionAttribute = "controller.devfile.io/merge-contribution"
)
6 changes: 6 additions & 0 deletions pkg/library/flatten/flatten.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ func ResolveDevWorkspace(workspace *dw.DevWorkspaceTemplateSpec, tooling Resolve
return resolvedDW, &warnings, nil
}

if needsContainerContributionMerge(resolvedDW) {
if err := mergeContainerContributions(resolvedDW); err != nil {
return nil, nil, err
}
}

return resolvedDW, nil, nil
}

Expand Down
100 changes: 100 additions & 0 deletions pkg/library/flatten/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import (
"fmt"
"reflect"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"

dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
)

Expand Down Expand Up @@ -67,3 +70,100 @@ func formatImportCycle(end *resolutionContextTree) string {
}
return cycle
}

func parseResourcesFromComponent(component *dw.Component) (*corev1.ResourceRequirements, error) {
if component.Container == nil {
return nil, fmt.Errorf("attemped to parse resource requirements from a non-container component")
}
memLimitStr := component.Container.MemoryLimit
if memLimitStr == "" {
memLimitStr = "0Mi"
}
memRequestStr := component.Container.MemoryRequest
if memRequestStr == "" {
memRequestStr = "0Mi"
}
cpuLimitStr := component.Container.CpuLimit
if cpuLimitStr == "" {
cpuLimitStr = "0m"
}
cpuRequestStr := component.Container.CpuRequest
if cpuRequestStr == "" {
cpuRequestStr = "0m"
}

memoryLimit, err := resource.ParseQuantity(memLimitStr)
if err != nil {
return nil, fmt.Errorf("failed to parse memory limit for container component %s: %w", component.Name, err)
}
memoryRequest, err := resource.ParseQuantity(memRequestStr)
if err != nil {
return nil, fmt.Errorf("failed to parse memory request for container component %s: %w", component.Name, err)
}
cpuLimit, err := resource.ParseQuantity(cpuLimitStr)
if err != nil {
return nil, fmt.Errorf("failed to parse CPU limit for container component %s: %w", component.Name, err)
}
cpuRequest, err := resource.ParseQuantity(cpuRequestStr)
if err != nil {
return nil, fmt.Errorf("failed to parse CPU request for container component %s: %w", component.Name, err)
}

return &corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceMemory: memoryLimit,
corev1.ResourceCPU: cpuLimit,
},
Requests: corev1.ResourceList{
corev1.ResourceMemory: memoryRequest,
corev1.ResourceCPU: cpuRequest,
},
}, nil
}

func addResourceRequirements(resources *corev1.ResourceRequirements, toAdd *dw.Component) error {
componentResources, err := parseResourcesFromComponent(toAdd)
if err != nil {
return err
}

memoryLimit := resources.Limits[corev1.ResourceMemory]
memoryLimit.Add(componentResources.Limits[corev1.ResourceMemory])
resources.Limits[corev1.ResourceMemory] = memoryLimit

cpuLimit := resources.Limits[corev1.ResourceCPU]
cpuLimit.Add(componentResources.Limits[corev1.ResourceCPU])
resources.Limits[corev1.ResourceCPU] = cpuLimit

memoryRequest := resources.Requests[corev1.ResourceMemory]
memoryRequest.Add(componentResources.Requests[corev1.ResourceMemory])
resources.Requests[corev1.ResourceMemory] = memoryRequest

cpuRequest := resources.Requests[corev1.ResourceCPU]
cpuRequest.Add(componentResources.Requests[corev1.ResourceCPU])
resources.Requests[corev1.ResourceCPU] = cpuRequest

return nil
}

func applyResourceRequirementsToComponent(container *dw.ContainerComponent, resources *corev1.ResourceRequirements) {
memLimit := resources.Limits[corev1.ResourceMemory]
if !memLimit.IsZero() {
container.MemoryLimit = memLimit.String()
}

cpuLimit := resources.Limits[corev1.ResourceCPU]
if !cpuLimit.IsZero() {
container.CpuLimit = cpuLimit.String()
}

memRequest := resources.Requests[corev1.ResourceMemory]
if !memRequest.IsZero() {
container.MemoryRequest = memRequest.String()
}

cpuRequest := resources.Requests[corev1.ResourceCPU]
if !cpuRequest.IsZero() {
container.CpuRequest = cpuRequest.String()
}
}
117 changes: 117 additions & 0 deletions pkg/library/flatten/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
package flatten

import (
"encoding/json"
"fmt"

dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/api/v2/pkg/utils/overriding"
"github.com/devfile/devworkspace-operator/pkg/constants"
"k8s.io/apimachinery/pkg/api/resource"
)

Expand Down Expand Up @@ -118,3 +120,118 @@ func mergeVolume(into, from *dw.VolumeComponent) error {
}
return nil
}

func needsContainerContributionMerge(flattenedSpec *dw.DevWorkspaceTemplateSpec) bool {
hasContribution, hasTarget := false, false
for _, component := range flattenedSpec.Components {
if component.Container == nil {
// Ignore attribute on non-container components as it's not clear what this would mean
continue
}
if component.Attributes.GetBoolean(constants.ContainerContributionAttribute, nil) == true {
hasContribution = true
}
if component.Attributes.GetBoolean(constants.MergeContributionAttribute, nil) == true {
hasTarget = true
}
}
return hasContribution && hasTarget
}

func mergeContainerContributions(flattenedSpec *dw.DevWorkspaceTemplateSpec) error {
var contributions []dw.Component
for _, component := range flattenedSpec.Components {
if component.Container != nil && component.Attributes.GetBoolean(constants.ContainerContributionAttribute, nil) == true {
contributions = append(contributions, component)
}
}

var newComponents []dw.Component
mergeDone := false
for _, component := range flattenedSpec.Components {
if component.Container == nil {
newComponents = append(newComponents, component)
continue
}
if component.Attributes.GetBoolean(constants.ContainerContributionAttribute, nil) == true {
// drop contributions from updated list as they will be merged
continue
} else if component.Attributes.GetBoolean(constants.MergeContributionAttribute, nil) == true && !mergeDone {
mergedComponent, err := mergeContributionsInto(&component, contributions)
if err != nil {
return fmt.Errorf("failed to merge container contributions: %w", err)
}
delete(mergedComponent.Attributes, constants.ContainerContributionAttribute)
newComponents = append(newComponents, *mergedComponent)
mergeDone = true
} else {
newComponents = append(newComponents, component)
}
}

if mergeDone {
flattenedSpec.Components = newComponents
}

return nil
}

func mergeContributionsInto(mergeInto *dw.Component, contributions []dw.Component) (*dw.Component, error) {
if mergeInto == nil || mergeInto.Container == nil {
return nil, fmt.Errorf("attempting to merge container contributions into a non-container component")
}
totalResources, err := parseResourcesFromComponent(mergeInto)
if err != nil {
return nil, err
}

// We don't want to reimplement the complexity of a strategic merge here, so we set up a fake plugin override
// and use devfile/api overriding functionality. For specific fields that have to be handled specifically (memory
// and cpu limits, we compute the value separately and set it at the end
var toMerge []dw.ComponentPluginOverride
for _, component := range contributions {
if component.Container == nil {
return nil, fmt.Errorf("attempting to merge container contribution from a non-container component")
}
// Set name to match target component so that devfile/api override functionality will apply it correctly
component.Name = mergeInto.Name
// Unset image to avoid overriding the default image
component.Container.Image = ""
if err := addResourceRequirements(totalResources, &component); err != nil {
return nil, err
}
component.Container.MemoryLimit = ""
component.Container.MemoryRequest = ""
component.Container.CpuLimit = ""
component.Container.CpuRequest = ""
// Workaround to convert dw.Component into dw.ComponentPluginOverride: marshal to json, and unmarshal to a different type
// This works since plugin overrides are generated from components, with the difference being that all fields are optional
componentPluginOverride := dw.ComponentPluginOverride{}
tempJSONBytes, err := json.Marshal(component)
if err != nil {
return nil, err
}
if err := json.Unmarshal(tempJSONBytes, &componentPluginOverride); err != nil {
return nil, err
}
toMerge = append(toMerge, componentPluginOverride)
}

tempSpecContent := &dw.DevWorkspaceTemplateSpecContent{
Components: []dw.Component{
*mergeInto,
},
}

mergedSpecContent, err := overriding.OverrideDevWorkspaceTemplateSpec(tempSpecContent, dw.PluginOverrides{
Components: toMerge,
})
if err != nil {
return nil, err
}

mergedComponent := mergedSpecContent.Components[0]
applyResourceRequirementsToComponent(mergedComponent.Container, totalResources)

return &mergedComponent, nil
}

0 comments on commit 79bd392

Please sign in to comment.