diff --git a/README.md b/README.md index 702eced9..646de177 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # Devfile Library + + ## About The Devfile Parser library is a Golang module that: @@ -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{ @@ -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) @@ -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() @@ -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 diff --git a/pkg/devfile/generator/generators.go b/pkg/devfile/generator/generators.go index c3d02d04..b0a16e36 100644 --- a/pkg/devfile/generator/generators.go +++ b/pkg/devfile/generator/generators.go @@ -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" @@ -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 ( @@ -27,6 +28,8 @@ const ( deploymentKind = "Deployment" deploymentAPIVersion = "apps/v1" + + containerNameMaxLen = 55 ) // GetTypeMeta gets a type meta of the specified kind and version @@ -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 +func GetInitContainers(devfileObj parser.DevfileObj) ([]corev1.Container, error) { + 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 { + 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- + // 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 diff --git a/pkg/devfile/generator/generators_test.go b/pkg/devfile/generator/generators_test.go index bbf0ef85..399aed38 100644 --- a/pkg/devfile/generator/generators_test.go +++ b/pkg/devfile/generator/generators_test.go @@ -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" @@ -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) + } + } + }) + } + +} diff --git a/pkg/devfile/parser/data/v2/common/command_helper.go b/pkg/devfile/parser/data/v2/common/command_helper.go index 6a93ac93..b3bf8a0a 100644 --- a/pkg/devfile/parser/data/v2/common/command_helper.go +++ b/pkg/devfile/parser/data/v2/common/command_helper.go @@ -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 { @@ -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 { + 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 +} diff --git a/pkg/devfile/parser/data/v2/common/command_helper_test.go b/pkg/devfile/parser/data/v2/common/command_helper_test.go index a4398f9d..62688929 100644 --- a/pkg/devfile/parser/data/v2/common/command_helper_test.go +++ b/pkg/devfile/parser/data/v2/common/command_helper_test.go @@ -331,3 +331,79 @@ func TestGetCommandType(t *testing.T) { } } + +func TestGetCommandsFromEvent(t *testing.T) { + + execCommands := []v1.Command{ + { + Id: "exec1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + { + Id: "exec2", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + { + Id: "exec3", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + } + + compCommands := []v1.Command{ + { + Id: "comp1", + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{ + Commands: []string{ + "exec1", + "exec3", + }, + }, + }, + }, + } + + commandsMap := map[string]v1.Command{ + compCommands[0].Id: compCommands[0], + execCommands[0].Id: execCommands[0], + execCommands[1].Id: execCommands[1], + execCommands[2].Id: execCommands[2], + } + + tests := []struct { + name string + eventName string + wantCommands []string + }{ + { + name: "composite event", + eventName: "comp1", + wantCommands: []string{ + "exec1", + "exec3", + }, + }, + { + name: "exec event", + eventName: "exec2", + wantCommands: []string{ + "exec2", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + commands := GetCommandsFromEvent(commandsMap, tt.eventName) + if !reflect.DeepEqual(tt.wantCommands, commands) { + t.Errorf("TestGetCommandsFromEvent error - got %v expected %v", commands, tt.wantCommands) + } + }) + } + +}