Skip to content

Commit 4693e1d

Browse files
authored
Merge pull request #89 from yangcao77/181-generate-initcontainers
add GenerateInitContainers function to generator
2 parents bd69960 + 8ac3368 commit 4693e1d

File tree

5 files changed

+370
-8
lines changed

5 files changed

+370
-8
lines changed

README.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Devfile Library
22

3+
<div id="header">
4+
5+
[![Apache2.0 License](https://img.shields.io/badge/license-Apache2.0-brightgreen.svg)](LICENSE)
6+
</div>
7+
38
## About
49

510
The Devfile Parser library is a Golang module that:
@@ -11,7 +16,7 @@ The Devfile Parser library is a Golang module that:
1116
## Usage
1217

1318
The function documentation can be accessed via [pkg.go.dev](https://pkg.go.dev/github.com/devfile/library).
14-
1. To parse a devfile, visit pkg/devfile/parse.go
19+
1. To parse a devfile, visit [parse.go source file](pkg/devfile/parse.go)
1520
```go
1621
// ParserArgs is the struct to pass into parser functions which contains required info for parsing devfile.
1722
parserArgs := parser.ParserArgs{
@@ -60,7 +65,7 @@ The function documentation can be accessed via [pkg.go.dev](https://pkg.go.dev/g
6065
})
6166
```
6267

63-
3. To get the Kubernetes objects from the devfile, visit pkg/devfile/generator/generators.go
68+
3. To get the Kubernetes objects from the devfile, visit [generators.go source file](pkg/devfile/generator/generators.go)
6469
```go
6570
// To get a slice of Kubernetes containers of type corev1.Container from the devfile component containers
6671
containers, err := generator.GetContainers(devfile)
@@ -109,7 +114,7 @@ The function documentation can be accessed via [pkg.go.dev](https://pkg.go.dev/g
109114
err := devfile.Data.DeleteComponent(componentName)
110115
```
111116

112-
5. To write to a devfile, visit pkg/devfile/parser/writer.go
117+
5. To write to a devfile, visit [writer.go source file](pkg/devfile/parser/writer.go)
113118
```go
114119
// If the devfile object has been created with devfile path already set, can simply call WriteYamlDevfile to writes the devfile
115120
err := devfile.WriteYamlDevfile()
@@ -159,7 +164,12 @@ The following projects are consuming this library as a Golang dependency
159164
* [odo](https://github.com/openshift/odo)
160165
* [OpenShift Console](https://github.com/openshift/console)
161166

162-
In the future, [Workspace Operator](https://github.com/devfile/devworkspace-operator) will be the next consumer of devfile/library.
167+
## Tests
168+
169+
To run unit tests and api tests. Visit [library tests](tests/README.md) to find out more information on tests
170+
```
171+
make test
172+
```
163173

164174
## Issues
165175

pkg/devfile/generator/generators.go

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package generator
22

33
import (
4+
"fmt"
5+
v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
6+
"github.com/devfile/library/pkg/devfile/parser"
7+
"github.com/devfile/library/pkg/devfile/parser/data/v2/common"
8+
"github.com/devfile/library/pkg/util"
49
buildv1 "github.com/openshift/api/build/v1"
510
imagev1 "github.com/openshift/api/image/v1"
611
routev1 "github.com/openshift/api/route/v1"
@@ -9,10 +14,6 @@ import (
914
extensionsv1 "k8s.io/api/extensions/v1beta1"
1015
"k8s.io/apimachinery/pkg/api/resource"
1116
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12-
13-
v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
14-
"github.com/devfile/library/pkg/devfile/parser"
15-
"github.com/devfile/library/pkg/devfile/parser/data/v2/common"
1617
)
1718

1819
const (
@@ -27,6 +28,8 @@ const (
2728

2829
deploymentKind = "Deployment"
2930
deploymentAPIVersion = "apps/v1"
31+
32+
containerNameMaxLen = 55
3033
)
3134

3235
// GetTypeMeta gets a type meta of the specified kind and version
@@ -91,6 +94,56 @@ func GetContainers(devfileObj parser.DevfileObj, options common.DevfileOptions)
9194
return containers, nil
9295
}
9396

97+
// GetInitContainers gets the init container for every preStart devfile event
98+
func GetInitContainers(devfileObj parser.DevfileObj) ([]corev1.Container, error) {
99+
containers, err := GetContainers(devfileObj, common.DevfileOptions{})
100+
if err != nil {
101+
return nil, err
102+
}
103+
preStartEvents := devfileObj.Data.GetEvents().PreStart
104+
var initContainers []corev1.Container
105+
if len(preStartEvents) > 0 {
106+
var eventCommands []string
107+
commands, err := devfileObj.Data.GetCommands(common.DevfileOptions{})
108+
if err != nil {
109+
return nil, err
110+
}
111+
112+
commandsMap := common.GetCommandsMap(commands)
113+
114+
for _, event := range preStartEvents {
115+
eventSubCommands := common.GetCommandsFromEvent(commandsMap, event)
116+
eventCommands = append(eventCommands, eventSubCommands...)
117+
}
118+
119+
for i, commandName := range eventCommands {
120+
if command, ok := commandsMap[commandName]; ok {
121+
component := common.GetApplyComponent(command)
122+
123+
// Get the container info for the given component
124+
for _, container := range containers {
125+
if container.Name == component {
126+
// Override the init container name since there cannot be two containers with the same
127+
// name in a pod. This applies to pod containers and pod init containers. The convention
128+
// for init container name here is, containername-eventname-<position of command in prestart events>
129+
// If there are two events referencing the same devfile component, then we will have
130+
// tools-event1-1 & tools-event2-3, for example. And if in the edge case, the same command is
131+
// executed twice by preStart events, then we will have tools-event1-1 & tools-event1-2
132+
initContainerName := fmt.Sprintf("%s-%s", container.Name, commandName)
133+
initContainerName = util.TruncateString(initContainerName, containerNameMaxLen)
134+
initContainerName = fmt.Sprintf("%s-%d", initContainerName, i+1)
135+
container.Name = initContainerName
136+
137+
initContainers = append(initContainers, container)
138+
}
139+
}
140+
}
141+
}
142+
}
143+
144+
return initContainers, nil
145+
}
146+
94147
// DeploymentParams is a struct that contains the required data to create a deployment object
95148
type DeploymentParams struct {
96149
TypeMeta metav1.TypeMeta

pkg/devfile/generator/generators_test.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package generator
22

33
import (
4+
"github.com/devfile/library/pkg/devfile/parser/data"
5+
"github.com/devfile/library/pkg/util"
46
"reflect"
7+
"strings"
58
"testing"
69

710
v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
@@ -487,3 +490,186 @@ func TestGetVolumeMountPath(t *testing.T) {
487490
}
488491

489492
}
493+
494+
func TestGetInitContainers(t *testing.T) {
495+
shellExecutable := "/bin/sh"
496+
containers := []v1.Component{
497+
{
498+
Name: "container1",
499+
ComponentUnion: v1.ComponentUnion{
500+
Container: &v1.ContainerComponent{
501+
Container: v1.Container{
502+
Image: "container1",
503+
Command: []string{shellExecutable, "-c", "cd execworkdir1 && execcommand1"},
504+
},
505+
},
506+
},
507+
},
508+
{
509+
Name: "container2",
510+
ComponentUnion: v1.ComponentUnion{
511+
Container: &v1.ContainerComponent{
512+
Container: v1.Container{
513+
Image: "container2",
514+
Command: []string{shellExecutable, "-c", "cd execworkdir3 && execcommand3"},
515+
},
516+
},
517+
},
518+
},
519+
}
520+
521+
execCommands := []v1.Command{
522+
{
523+
Id: "apply1",
524+
CommandUnion: v1.CommandUnion{
525+
Apply: &v1.ApplyCommand{
526+
Component: "container1",
527+
},
528+
},
529+
},
530+
{
531+
Id: "apply2",
532+
CommandUnion: v1.CommandUnion{
533+
Apply: &v1.ApplyCommand{
534+
Component: "container1",
535+
},
536+
},
537+
},
538+
{
539+
Id: "apply3",
540+
CommandUnion: v1.CommandUnion{
541+
Apply: &v1.ApplyCommand{
542+
Component: "container2",
543+
},
544+
},
545+
},
546+
}
547+
548+
compCommands := []v1.Command{
549+
{
550+
Id: "comp1",
551+
CommandUnion: v1.CommandUnion{
552+
Composite: &v1.CompositeCommand{
553+
Commands: []string{
554+
"apply1",
555+
"apply3",
556+
},
557+
},
558+
},
559+
},
560+
}
561+
562+
longContainerName := "thisisaverylongcontainerandkuberneteshasalimitforanamesize-exec2"
563+
trimmedLongContainerName := util.TruncateString(longContainerName, containerNameMaxLen)
564+
565+
tests := []struct {
566+
name string
567+
eventCommands []string
568+
wantInitContainer map[string]corev1.Container
569+
longName bool
570+
wantErr bool
571+
}{
572+
{
573+
name: "Composite and Exec events",
574+
eventCommands: []string{
575+
"apply1",
576+
"apply3",
577+
"apply2",
578+
},
579+
wantInitContainer: map[string]corev1.Container{
580+
"container1-apply1": {
581+
Command: []string{shellExecutable, "-c", "cd execworkdir1 && execcommand1"},
582+
},
583+
"container1-apply2": {
584+
Command: []string{shellExecutable, "-c", "cd execworkdir1 && execcommand1"},
585+
},
586+
"container2-apply3": {
587+
Command: []string{shellExecutable, "-c", "cd execworkdir3 && execcommand3"},
588+
},
589+
},
590+
},
591+
{
592+
name: "Long Container Name",
593+
eventCommands: []string{
594+
"apply2",
595+
},
596+
wantInitContainer: map[string]corev1.Container{
597+
trimmedLongContainerName: {
598+
Command: []string{shellExecutable, "-c", "cd execworkdir1 && execcommand1"},
599+
},
600+
},
601+
longName: true,
602+
},
603+
}
604+
for _, tt := range tests {
605+
t.Run(tt.name, func(t *testing.T) {
606+
607+
if tt.longName {
608+
containers[0].Name = longContainerName
609+
execCommands[1].Apply.Component = longContainerName
610+
}
611+
612+
devObj := parser.DevfileObj{
613+
Data: func() data.DevfileData {
614+
devfileData, err := data.NewDevfileData(string(data.APISchemaVersion210))
615+
if err != nil {
616+
t.Error(err)
617+
}
618+
err = devfileData.AddComponents(containers)
619+
if err != nil {
620+
t.Error(err)
621+
}
622+
err = devfileData.AddCommands(execCommands)
623+
if err != nil {
624+
t.Error(err)
625+
}
626+
err = devfileData.AddCommands(compCommands)
627+
if err != nil {
628+
t.Error(err)
629+
}
630+
err = devfileData.AddEvents(v1.Events{
631+
DevWorkspaceEvents: v1.DevWorkspaceEvents{
632+
PreStart: tt.eventCommands,
633+
},
634+
})
635+
if err != nil {
636+
t.Error(err)
637+
}
638+
return devfileData
639+
}(),
640+
}
641+
642+
initContainers, err := GetInitContainers(devObj)
643+
if (err != nil) != tt.wantErr {
644+
t.Errorf("TestGetInitContainers() error = %v, wantErr %v", err, tt.wantErr)
645+
}
646+
647+
if len(tt.wantInitContainer) != len(initContainers) {
648+
t.Errorf("TestGetInitContainers() error: init container length mismatch, wanted %v got %v", len(tt.wantInitContainer), len(initContainers))
649+
}
650+
651+
for _, initContainer := range initContainers {
652+
nameMatched := false
653+
commandMatched := false
654+
for containerName, container := range tt.wantInitContainer {
655+
if strings.Contains(initContainer.Name, containerName) {
656+
nameMatched = true
657+
}
658+
659+
if reflect.DeepEqual(initContainer.Command, container.Command) {
660+
commandMatched = true
661+
}
662+
}
663+
664+
if !nameMatched {
665+
t.Errorf("TestGetInitContainers() error: init container name mismatch, container name not present in %v", initContainer.Name)
666+
}
667+
668+
if !commandMatched {
669+
t.Errorf("TestGetInitContainers() error: init container command mismatch, command not found in %v", initContainer.Command)
670+
}
671+
}
672+
})
673+
}
674+
675+
}

pkg/devfile/parser/data/v2/common/command_helper.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ func GetExecWorkingDir(dc v1.Command) string {
5050
return ""
5151
}
5252

53+
// GetApplyComponent returns the component of the apply command
54+
func GetApplyComponent(dc v1.Command) string {
55+
if dc.Apply != nil {
56+
return dc.Apply.Component
57+
}
58+
59+
return ""
60+
}
61+
5362
// GetCommandType returns the command type of a given command
5463
func GetCommandType(command v1.Command) (v1.CommandType, error) {
5564
switch {
@@ -66,3 +75,31 @@ func GetCommandType(command v1.Command) (v1.CommandType, error) {
6675
return "", fmt.Errorf("unknown command type")
6776
}
6877
}
78+
79+
// GetCommandsMap returns a map of the command Id to the command
80+
func GetCommandsMap(commands []v1.Command) map[string]v1.Command {
81+
commandMap := make(map[string]v1.Command, len(commands))
82+
for _, command := range commands {
83+
commandMap[command.Id] = command
84+
}
85+
return commandMap
86+
}
87+
88+
// GetCommandsFromEvent returns the list of commands from the event name.
89+
// If the event is a composite command, it returns the sub-commands from the tree
90+
func GetCommandsFromEvent(commandsMap map[string]v1.Command, eventName string) []string {
91+
var commands []string
92+
93+
if command, ok := commandsMap[eventName]; ok {
94+
if command.Composite != nil {
95+
for _, compositeSubCmd := range command.Composite.Commands {
96+
subCommands := GetCommandsFromEvent(commandsMap, compositeSubCmd)
97+
commands = append(commands, subCommands...)
98+
}
99+
} else {
100+
commands = append(commands, command.Id)
101+
}
102+
}
103+
104+
return commands
105+
}

0 commit comments

Comments
 (0)