Skip to content

Commit b133925

Browse files
committed
Add support for containerContributions
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 <[email protected]>
1 parent ecfb532 commit b133925

File tree

4 files changed

+233
-0
lines changed

4 files changed

+233
-0
lines changed

pkg/constants/attributes.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,14 @@ const (
7373
// EndpointURLAttribute is an attribute added to endpoints to denote the endpoint on the cluster that
7474
// was created to route to this endpoint
7575
EndpointURLAttribute = "controller.devfile.io/endpoint-url"
76+
77+
// ContainerContributionAttribute defines a container component as a container contribution that should be merged
78+
// into an existing container in the devfile if possible. If no suitable container exists, this component
79+
// is treated as a regular container component
80+
ContainerContributionAttribute = "controller.devfile.io/container-contribution"
81+
82+
// MergeContributionAttribute defines a container component as a target for merging a container contribution. If
83+
// present on a container component, any container contributions will be merged into that container. If multiple
84+
// container components have the merge-contribution attribute, the first one will be used and all others ignored.
85+
MergeContributionAttribute = "controller.devfile.io/merge-contribution"
7686
)

pkg/library/flatten/flatten.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ func ResolveDevWorkspace(workspace *dw.DevWorkspaceTemplateSpec, tooling Resolve
6464
return resolvedDW, &warnings, nil
6565
}
6666

67+
if needsContainerContributionMerge(resolvedDW) {
68+
if err := mergeContainerContributions(resolvedDW); err != nil {
69+
return nil, nil, err
70+
}
71+
}
72+
6773
return resolvedDW, nil, nil
6874
}
6975

pkg/library/flatten/helper.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import (
1919
"fmt"
2020
"reflect"
2121

22+
corev1 "k8s.io/api/core/v1"
23+
"k8s.io/apimachinery/pkg/api/resource"
24+
2225
dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
2326
)
2427

@@ -67,3 +70,100 @@ func formatImportCycle(end *resolutionContextTree) string {
6770
}
6871
return cycle
6972
}
73+
74+
func parseResourcesFromComponent(component *dw.Component) (*corev1.ResourceRequirements, error) {
75+
if component.Container == nil {
76+
return nil, fmt.Errorf("attemped to parse resource requirements from a non-container component")
77+
}
78+
memLimitStr := component.Container.MemoryLimit
79+
if memLimitStr == "" {
80+
memLimitStr = "0Mi"
81+
}
82+
memRequestStr := component.Container.MemoryRequest
83+
if memRequestStr == "" {
84+
memRequestStr = "0Mi"
85+
}
86+
cpuLimitStr := component.Container.CpuLimit
87+
if cpuLimitStr == "" {
88+
cpuLimitStr = "0m"
89+
}
90+
cpuRequestStr := component.Container.CpuRequest
91+
if cpuRequestStr == "" {
92+
cpuRequestStr = "0m"
93+
}
94+
95+
memoryLimit, err := resource.ParseQuantity(memLimitStr)
96+
if err != nil {
97+
return nil, fmt.Errorf("failed to parse memory limit for container component %s: %w", component.Name, err)
98+
}
99+
memoryRequest, err := resource.ParseQuantity(memRequestStr)
100+
if err != nil {
101+
return nil, fmt.Errorf("failed to parse memory request for container component %s: %w", component.Name, err)
102+
}
103+
cpuLimit, err := resource.ParseQuantity(cpuLimitStr)
104+
if err != nil {
105+
return nil, fmt.Errorf("failed to parse CPU limit for container component %s: %w", component.Name, err)
106+
}
107+
cpuRequest, err := resource.ParseQuantity(cpuRequestStr)
108+
if err != nil {
109+
return nil, fmt.Errorf("failed to parse CPU request for container component %s: %w", component.Name, err)
110+
}
111+
112+
return &corev1.ResourceRequirements{
113+
Limits: corev1.ResourceList{
114+
corev1.ResourceMemory: memoryLimit,
115+
corev1.ResourceCPU: cpuLimit,
116+
},
117+
Requests: corev1.ResourceList{
118+
corev1.ResourceMemory: memoryRequest,
119+
corev1.ResourceCPU: cpuRequest,
120+
},
121+
}, nil
122+
}
123+
124+
func addResourceRequirements(resources *corev1.ResourceRequirements, toAdd *dw.Component) error {
125+
componentResources, err := parseResourcesFromComponent(toAdd)
126+
if err != nil {
127+
return err
128+
}
129+
130+
memoryLimit := resources.Limits[corev1.ResourceMemory]
131+
memoryLimit.Add(componentResources.Limits[corev1.ResourceMemory])
132+
resources.Limits[corev1.ResourceMemory] = memoryLimit
133+
134+
cpuLimit := resources.Limits[corev1.ResourceCPU]
135+
cpuLimit.Add(componentResources.Limits[corev1.ResourceCPU])
136+
resources.Limits[corev1.ResourceCPU] = cpuLimit
137+
138+
memoryRequest := resources.Requests[corev1.ResourceMemory]
139+
memoryRequest.Add(componentResources.Requests[corev1.ResourceMemory])
140+
resources.Requests[corev1.ResourceMemory] = memoryRequest
141+
142+
cpuRequest := resources.Requests[corev1.ResourceCPU]
143+
cpuRequest.Add(componentResources.Requests[corev1.ResourceCPU])
144+
resources.Requests[corev1.ResourceCPU] = cpuRequest
145+
146+
return nil
147+
}
148+
149+
func applyResourceRequirementsToComponent(container *dw.ContainerComponent, resources *corev1.ResourceRequirements) {
150+
memLimit := resources.Limits[corev1.ResourceMemory]
151+
if !memLimit.IsZero() {
152+
container.MemoryLimit = memLimit.String()
153+
}
154+
155+
cpuLimit := resources.Limits[corev1.ResourceCPU]
156+
if !cpuLimit.IsZero() {
157+
container.CpuLimit = cpuLimit.String()
158+
}
159+
160+
memRequest := resources.Requests[corev1.ResourceMemory]
161+
if !memRequest.IsZero() {
162+
container.MemoryRequest = memRequest.String()
163+
}
164+
165+
cpuRequest := resources.Requests[corev1.ResourceCPU]
166+
if !cpuRequest.IsZero() {
167+
container.CpuRequest = cpuRequest.String()
168+
}
169+
}

pkg/library/flatten/merge.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
package flatten
1717

1818
import (
19+
"encoding/json"
1920
"fmt"
2021

2122
dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
2223
"github.com/devfile/api/v2/pkg/utils/overriding"
24+
"github.com/devfile/devworkspace-operator/pkg/constants"
2325
"k8s.io/apimachinery/pkg/api/resource"
2426
)
2527

@@ -118,3 +120,118 @@ func mergeVolume(into, from *dw.VolumeComponent) error {
118120
}
119121
return nil
120122
}
123+
124+
func needsContainerContributionMerge(flattenedSpec *dw.DevWorkspaceTemplateSpec) bool {
125+
hasContribution, hasTarget := false, false
126+
for _, component := range flattenedSpec.Components {
127+
if component.Container == nil {
128+
// Ignore attribute on non-container components as it's not clear what this would mean
129+
continue
130+
}
131+
if component.Attributes.GetBoolean(constants.ContainerContributionAttribute, nil) == true {
132+
hasContribution = true
133+
}
134+
if component.Attributes.GetBoolean(constants.MergeContributionAttribute, nil) == true {
135+
hasTarget = true
136+
}
137+
}
138+
return hasContribution && hasTarget
139+
}
140+
141+
func mergeContainerContributions(flattenedSpec *dw.DevWorkspaceTemplateSpec) error {
142+
var contributions []dw.Component
143+
for _, component := range flattenedSpec.Components {
144+
if component.Container != nil && component.Attributes.GetBoolean(constants.ContainerContributionAttribute, nil) == true {
145+
contributions = append(contributions, component)
146+
}
147+
}
148+
149+
var newComponents []dw.Component
150+
mergeDone := false
151+
for _, component := range flattenedSpec.Components {
152+
if component.Container == nil {
153+
newComponents = append(newComponents, component)
154+
continue
155+
}
156+
if component.Attributes.GetBoolean(constants.ContainerContributionAttribute, nil) == true {
157+
// drop contributions from updated list as they will be merged
158+
continue
159+
} else if component.Attributes.GetBoolean(constants.MergeContributionAttribute, nil) == true && !mergeDone {
160+
mergedComponent, err := mergeContributionsInto(&component, contributions)
161+
if err != nil {
162+
return fmt.Errorf("failed to merge container contributions: %w", err)
163+
}
164+
delete(mergedComponent.Attributes, constants.ContainerContributionAttribute)
165+
newComponents = append(newComponents, *mergedComponent)
166+
mergeDone = true
167+
} else {
168+
newComponents = append(newComponents, component)
169+
}
170+
}
171+
172+
if mergeDone {
173+
flattenedSpec.Components = newComponents
174+
}
175+
176+
return nil
177+
}
178+
179+
func mergeContributionsInto(mergeInto *dw.Component, contributions []dw.Component) (*dw.Component, error) {
180+
if mergeInto == nil || mergeInto.Container == nil {
181+
return nil, fmt.Errorf("attempting to merge container contributions into a non-container component")
182+
}
183+
totalResources, err := parseResourcesFromComponent(mergeInto)
184+
if err != nil {
185+
return nil, err
186+
}
187+
188+
// We don't want to reimplement the complexity of a strategic merge here, so we set up a fake plugin override
189+
// and use devfile/api overriding functionality. For specific fields that have to be handled specifically (memory
190+
// and cpu limits, we compute the value separately and set it at the end
191+
var toMerge []dw.ComponentPluginOverride
192+
for _, component := range contributions {
193+
if component.Container == nil {
194+
return nil, fmt.Errorf("attempting to merge container contribution from a non-container component")
195+
}
196+
// Set name to match target component so that devfile/api override functionality will apply it correctly
197+
component.Name = mergeInto.Name
198+
// Unset image to avoid overriding the default image
199+
component.Container.Image = ""
200+
if err := addResourceRequirements(totalResources, &component); err != nil {
201+
return nil, err
202+
}
203+
component.Container.MemoryLimit = ""
204+
component.Container.MemoryRequest = ""
205+
component.Container.CpuLimit = ""
206+
component.Container.CpuRequest = ""
207+
// Workaround to convert dw.Component into dw.ComponentPluginOverride: marshal to json, and unmarshal to a different type
208+
// This works since plugin overrides are generated from components, with the difference being that all fields are optional
209+
componentPluginOverride := dw.ComponentPluginOverride{}
210+
tempJSONBytes, err := json.Marshal(component)
211+
if err != nil {
212+
return nil, err
213+
}
214+
if err := json.Unmarshal(tempJSONBytes, &componentPluginOverride); err != nil {
215+
return nil, err
216+
}
217+
toMerge = append(toMerge, componentPluginOverride)
218+
}
219+
220+
tempSpecContent := &dw.DevWorkspaceTemplateSpecContent{
221+
Components: []dw.Component{
222+
*mergeInto,
223+
},
224+
}
225+
226+
mergedSpecContent, err := overriding.OverrideDevWorkspaceTemplateSpec(tempSpecContent, dw.PluginOverrides{
227+
Components: toMerge,
228+
})
229+
if err != nil {
230+
return nil, err
231+
}
232+
233+
mergedComponent := mergedSpecContent.Components[0]
234+
applyResourceRequirementsToComponent(mergedComponent.Container, totalResources)
235+
236+
return &mergedComponent, nil
237+
}

0 commit comments

Comments
 (0)