Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add GenerateInitContainers function to generator #89

Merged
merged 6 commits into from
Jun 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Devfile Library

<div id="header">

[![Apache2.0 License](https://img.shields.io/badge/license-Apache2.0-brightgreen.svg)](LICENSE)
</div>

## About

The Devfile Parser library is a Golang module that:
Expand All @@ -11,7 +16,7 @@ The Devfile Parser library is a Golang module that:
## Usage

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

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

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

In the future, [Workspace Operator](https://github.com/devfile/devworkspace-operator) will be the next consumer of devfile/library.
## Tests

To run unit tests and api tests. Visit [library tests](tests/README.md) to find out more information on tests
```
make test
```

## Issues

Expand Down
61 changes: 57 additions & 4 deletions pkg/devfile/generator/generators.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package generator

import (
"fmt"
v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/pkg/devfile/parser"
"github.com/devfile/library/pkg/devfile/parser/data/v2/common"
"github.com/devfile/library/pkg/util"
buildv1 "github.com/openshift/api/build/v1"
imagev1 "github.com/openshift/api/image/v1"
routev1 "github.com/openshift/api/route/v1"
Expand All @@ -9,10 +14,6 @@ import (
extensionsv1 "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/pkg/devfile/parser"
"github.com/devfile/library/pkg/devfile/parser/data/v2/common"
)

const (
Expand All @@ -27,6 +28,8 @@ const (

deploymentKind = "Deployment"
deploymentAPIVersion = "apps/v1"

containerNameMaxLen = 55
)

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

// GetInitContainers gets the init container for every preStart devfile event
maysunfaisal marked this conversation as resolved.
Show resolved Hide resolved
func GetInitContainers(devfileObj parser.DevfileObj) ([]corev1.Container, error) {
maysunfaisal marked this conversation as resolved.
Show resolved Hide resolved
containers, err := GetContainers(devfileObj, common.DevfileOptions{})
if err != nil {
return nil, err
}
preStartEvents := devfileObj.Data.GetEvents().PreStart
var initContainers []corev1.Container
if len(preStartEvents) > 0 {
var eventCommands []string
commands, err := devfileObj.Data.GetCommands(common.DevfileOptions{})
if err != nil {
return nil, err
}

commandsMap := common.GetCommandsMap(commands)

for _, event := range preStartEvents {
eventSubCommands := common.GetCommandsFromEvent(commandsMap, event)
eventCommands = append(eventCommands, eventSubCommands...)
}

for i, commandName := range eventCommands {
if command, ok := commandsMap[commandName]; ok {
maysunfaisal marked this conversation as resolved.
Show resolved Hide resolved
component := common.GetApplyComponent(command)

// Get the container info for the given component
for _, container := range containers {
if container.Name == component {
// Override the init container name since there cannot be two containers with the same
// name in a pod. This applies to pod containers and pod init containers. The convention
// for init container name here is, containername-eventname-<position of command in prestart events>
// If there are two events referencing the same devfile component, then we will have
// tools-event1-1 & tools-event2-3, for example. And if in the edge case, the same command is
// executed twice by preStart events, then we will have tools-event1-1 & tools-event1-2
initContainerName := fmt.Sprintf("%s-%s", container.Name, commandName)
initContainerName = util.TruncateString(initContainerName, containerNameMaxLen)
initContainerName = fmt.Sprintf("%s-%d", initContainerName, i+1)
container.Name = initContainerName

initContainers = append(initContainers, container)
}
}
}
}
}

return initContainers, nil
}

// DeploymentParams is a struct that contains the required data to create a deployment object
type DeploymentParams struct {
TypeMeta metav1.TypeMeta
Expand Down
186 changes: 186 additions & 0 deletions pkg/devfile/generator/generators_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package generator

import (
"github.com/devfile/library/pkg/devfile/parser/data"
"github.com/devfile/library/pkg/util"
"reflect"
"strings"
"testing"

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

}

func TestGetInitContainers(t *testing.T) {
shellExecutable := "/bin/sh"
containers := []v1.Component{
{
Name: "container1",
ComponentUnion: v1.ComponentUnion{
Container: &v1.ContainerComponent{
Container: v1.Container{
Image: "container1",
Command: []string{shellExecutable, "-c", "cd execworkdir1 && execcommand1"},
},
},
},
},
{
Name: "container2",
ComponentUnion: v1.ComponentUnion{
Container: &v1.ContainerComponent{
Container: v1.Container{
Image: "container2",
Command: []string{shellExecutable, "-c", "cd execworkdir3 && execcommand3"},
},
},
},
},
}

execCommands := []v1.Command{
{
Id: "apply1",
CommandUnion: v1.CommandUnion{
Apply: &v1.ApplyCommand{
Component: "container1",
},
},
},
{
Id: "apply2",
CommandUnion: v1.CommandUnion{
Apply: &v1.ApplyCommand{
Component: "container1",
},
},
},
{
Id: "apply3",
CommandUnion: v1.CommandUnion{
Apply: &v1.ApplyCommand{
Component: "container2",
},
},
},
}

compCommands := []v1.Command{
{
Id: "comp1",
CommandUnion: v1.CommandUnion{
Composite: &v1.CompositeCommand{
Commands: []string{
"apply1",
"apply3",
},
},
},
},
}

longContainerName := "thisisaverylongcontainerandkuberneteshasalimitforanamesize-exec2"
trimmedLongContainerName := util.TruncateString(longContainerName, containerNameMaxLen)

tests := []struct {
name string
eventCommands []string
wantInitContainer map[string]corev1.Container
longName bool
wantErr bool
}{
{
name: "Composite and Exec events",
eventCommands: []string{
"apply1",
"apply3",
"apply2",
},
wantInitContainer: map[string]corev1.Container{
"container1-apply1": {
Command: []string{shellExecutable, "-c", "cd execworkdir1 && execcommand1"},
},
"container1-apply2": {
Command: []string{shellExecutable, "-c", "cd execworkdir1 && execcommand1"},
},
"container2-apply3": {
Command: []string{shellExecutable, "-c", "cd execworkdir3 && execcommand3"},
},
},
},
{
name: "Long Container Name",
eventCommands: []string{
"apply2",
},
wantInitContainer: map[string]corev1.Container{
trimmedLongContainerName: {
Command: []string{shellExecutable, "-c", "cd execworkdir1 && execcommand1"},
},
},
longName: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

if tt.longName {
containers[0].Name = longContainerName
execCommands[1].Apply.Component = longContainerName
}

devObj := parser.DevfileObj{
Data: func() data.DevfileData {
devfileData, err := data.NewDevfileData(string(data.APISchemaVersion210))
if err != nil {
t.Error(err)
}
err = devfileData.AddComponents(containers)
if err != nil {
t.Error(err)
}
err = devfileData.AddCommands(execCommands)
if err != nil {
t.Error(err)
}
err = devfileData.AddCommands(compCommands)
if err != nil {
t.Error(err)
}
err = devfileData.AddEvents(v1.Events{
DevWorkspaceEvents: v1.DevWorkspaceEvents{
PreStart: tt.eventCommands,
},
})
if err != nil {
t.Error(err)
}
return devfileData
}(),
}

initContainers, err := GetInitContainers(devObj)
if (err != nil) != tt.wantErr {
t.Errorf("TestGetInitContainers() error = %v, wantErr %v", err, tt.wantErr)
}

if len(tt.wantInitContainer) != len(initContainers) {
t.Errorf("TestGetInitContainers() error: init container length mismatch, wanted %v got %v", len(tt.wantInitContainer), len(initContainers))
}

for _, initContainer := range initContainers {
nameMatched := false
commandMatched := false
for containerName, container := range tt.wantInitContainer {
if strings.Contains(initContainer.Name, containerName) {
nameMatched = true
}

if reflect.DeepEqual(initContainer.Command, container.Command) {
commandMatched = true
}
}

if !nameMatched {
t.Errorf("TestGetInitContainers() error: init container name mismatch, container name not present in %v", initContainer.Name)
}

if !commandMatched {
t.Errorf("TestGetInitContainers() error: init container command mismatch, command not found in %v", initContainer.Command)
}
}
})
}

}
37 changes: 37 additions & 0 deletions pkg/devfile/parser/data/v2/common/command_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ func GetExecWorkingDir(dc v1.Command) string {
return ""
}

// GetApplyComponent returns the component of the apply command
func GetApplyComponent(dc v1.Command) string {
if dc.Apply != nil {
return dc.Apply.Component
}

return ""
}

// GetCommandType returns the command type of a given command
func GetCommandType(command v1.Command) (v1.CommandType, error) {
switch {
Expand All @@ -66,3 +75,31 @@ func GetCommandType(command v1.Command) (v1.CommandType, error) {
return "", fmt.Errorf("unknown command type")
}
}

// GetCommandsMap returns a map of the command Id to the command
func GetCommandsMap(commands []v1.Command) map[string]v1.Command {
maysunfaisal marked this conversation as resolved.
Show resolved Hide resolved
commandMap := make(map[string]v1.Command, len(commands))
for _, command := range commands {
commandMap[command.Id] = command
}
return commandMap
}

// GetCommandsFromEvent returns the list of commands from the event name.
// If the event is a composite command, it returns the sub-commands from the tree
func GetCommandsFromEvent(commandsMap map[string]v1.Command, eventName string) []string {
var commands []string

if command, ok := commandsMap[eventName]; ok {
if command.Composite != nil {
for _, compositeSubCmd := range command.Composite.Commands {
subCommands := GetCommandsFromEvent(commandsMap, compositeSubCmd)
commands = append(commands, subCommands...)
}
} else {
commands = append(commands, command.Id)
}
}

return commands
}
Loading