diff --git a/README.md b/README.md index 8f717367..edae7673 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Devfile Parser Library +# Devfile Library ## About @@ -8,25 +8,43 @@ The Devfile Parser library is a Golang module that: 3. generates Kubernetes objects for the various devfile resources. 4. defines util functions for the devfile. +## 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 - ``` - // Parses the devfile and validates the devfile data - devfile, err := devfilePkg.ParseAndValidate(devfileLocation) - - // To get all the components from the devfile - components, err := devfile.Data.GetComponents(DevfileOptions{}) - - // To get all the components from the devfile with attributes tagged - tool: console-import & import: {strategy: Dockerfile} - components, err := devfile.Data.GetComponents(DevfileOptions{ - Filter: map[string]interface{}{ - "tool": "console-import", - "import": map[string]interface{}{ - "strategy": "Dockerfile", - }, + ``` + // Parses the devfile and validates the devfile data + devfile, err := devfilePkg.ParseAndValidate(devfileLocation) + + // To get all the components from the devfile + components, err := devfile.Data.GetComponents(DevfileOptions{}) + + // To get all the components from the devfile with attributes tagged - tool: console-import + // & import: {strategy: Dockerfile} + components, err := devfile.Data.GetComponents(DevfileOptions{ + Filter: map[string]interface{}{ + "tool": "console-import", + "import": map[string]interface{}{ + "strategy": "Dockerfile", }, - }) - ``` + }, + }) + + // To get all the volume components + components, err := devfile.Data.GetComponents(DevfileOptions{ + ComponentOptions: ComponentOptions{ + ComponentType: v1.VolumeComponentType, + }, + }) + + // To get all the exec commands that belong to the build group + commands, err := devfile.Data.GetCommands(DevfileOptions{ + CommandOptions: CommandOptions{ + CommandType: v1.ExecCommandType, + CommandGroupKind: v1.BuildCommandGroupKind, + }, + }) + ``` 2. To get the Kubernetes objects from the devfile, visit pkg/devfile/generator/generators.go ``` // To get a slice of Kubernetes containers of type corev1.Container from the devfile component containers @@ -43,8 +61,9 @@ The function documentation can be accessed via [pkg.go.dev](https://pkg.go.dev/g } deployment := generator.GetDeployment(deployParams) ``` - -

+ +## Updating Library Schema + Run `updateApi.sh` can update to use latest `github.com/devfile/api` and update the schema saved under `pkg/devfile/parser/data` The script also accepts version number as an argument to update devfile schema for a specific devfile version. @@ -54,15 +73,19 @@ For example, run the following command will update devfile schema for 2.0.0 ``` Running the script with no arguments will default to update the latest devfile version +## Projects using devfile/library -## Usage - -In the future, the following projects will be consuming this library as a Golang dependency +The following projects are consuming this library as a Golang dependency -* [Workspace Operator](https://github.com/devfile/devworkspace-operator) * [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. + ## Issues Issues are tracked in the [devfile/api](https://github.com/devfile/api) repo with the label [area/library](https://github.com/devfile/api/issues?q=is%3Aopen+is%3Aissue+label%3Aarea%2Flibrary) + +## Releases + +For devfile/library releases, please check the release [page](https://github.com/devfile/library/releases). diff --git a/pkg/devfile/parser/data/interface.go b/pkg/devfile/parser/data/interface.go index 3086b78d..dce8e56c 100644 --- a/pkg/devfile/parser/data/interface.go +++ b/pkg/devfile/parser/data/interface.go @@ -55,7 +55,7 @@ type DevfileData interface { GetDevfileWorkspace() *v1.DevWorkspaceTemplateSpecContent SetDevfileWorkspace(content v1.DevWorkspaceTemplateSpecContent) - //utils + // utils GetDevfileContainerComponents(common.DevfileOptions) ([]v1.Component, error) GetDevfileVolumeComponents(common.DevfileOptions) ([]v1.Component, error) } diff --git a/pkg/devfile/parser/data/v2/commands.go b/pkg/devfile/parser/data/v2/commands.go index bcdb3f49..9ebb735f 100644 --- a/pkg/devfile/parser/data/v2/commands.go +++ b/pkg/devfile/parser/data/v2/commands.go @@ -1,6 +1,7 @@ package v2 import ( + "reflect" "strings" v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" @@ -9,21 +10,40 @@ import ( // GetCommands returns the slice of Command objects parsed from the Devfile func (d *DevfileV2) GetCommands(options common.DevfileOptions) ([]v1.Command, error) { - if len(options.Filter) == 0 { + + if reflect.DeepEqual(options, common.DevfileOptions{}) { return d.Commands, nil } var commands []v1.Command for _, command := range d.Commands { + // Filter Command Attributes filterIn, err := common.FilterDevfileObject(command.Attributes, options) if err != nil { return nil, err + } else if !filterIn { + continue } - if filterIn { - command.Id = strings.ToLower(command.Id) - commands = append(commands, command) + // Filter Command Type - Exec, Composite, etc. + commandType, err := common.GetCommandType(command) + if err != nil { + return nil, err + } + if options.CommandOptions.CommandType != "" && commandType != options.CommandOptions.CommandType { + continue } + + // Filter Command Group Kind - Run, Build, etc. + commandGroup := common.GetGroup(command) + // exclude conditions: + // 1. options group is present and command group is present but does not match + // 2. options group is present and command group is not present + if options.CommandOptions.CommandGroupKind != "" && ((commandGroup != nil && options.CommandOptions.CommandGroupKind != commandGroup.Kind) || commandGroup == nil) { + continue + } + + commands = append(commands, command) } return commands, nil diff --git a/pkg/devfile/parser/data/v2/commands_test.go b/pkg/devfile/parser/data/v2/commands_test.go index 29d6473d..b721ed30 100644 --- a/pkg/devfile/parser/data/v2/commands_test.go +++ b/pkg/devfile/parser/data/v2/commands_test.go @@ -12,9 +12,6 @@ import ( func TestDevfile200_GetCommands(t *testing.T) { - type args struct { - name string - } tests := []struct { name string currentCommands []v1.Command @@ -23,7 +20,7 @@ func TestDevfile200_GetCommands(t *testing.T) { wantErr bool }{ { - name: "case 1: get the necessary commands", + name: "Get all the commands", currentCommands: []v1.Command{ { Id: "command1", @@ -38,12 +35,11 @@ func TestDevfile200_GetCommands(t *testing.T) { }, }, }, - filterOptions: common.DevfileOptions{}, - wantCommands: []string{"command1", "command2"}, - wantErr: false, + wantCommands: []string{"command1", "command2"}, + wantErr: false, }, { - name: "case 2: get the filtered commands", + name: "Get the filtered commands", currentCommands: []v1.Command{ { Id: "command1", @@ -52,7 +48,15 @@ func TestDevfile200_GetCommands(t *testing.T) { "secondString": "secondStringValue", }), CommandUnion: v1.CommandUnion{ - Exec: &v1.ExecCommand{}, + Exec: &v1.ExecCommand{ + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + Kind: v1.BuildCommandGroupKind, + }, + }, + }, + }, }, }, { @@ -65,18 +69,74 @@ func TestDevfile200_GetCommands(t *testing.T) { Composite: &v1.CompositeCommand{}, }, }, + { + Id: "command3", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{ + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + Kind: v1.BuildCommandGroupKind, + }, + }, + }, + }, + }, + }, + { + Id: "command4", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "thirdString": "thirdStringValue", + }), + CommandUnion: v1.CommandUnion{ + Apply: &v1.ApplyCommand{ + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + Kind: v1.BuildCommandGroupKind, + }, + }, + }, + }, + }, + }, + { + Id: "command5", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{ + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + Kind: v1.RunCommandGroupKind, + }, + }, + }, + }, + }, + }, }, filterOptions: common.DevfileOptions{ Filter: map[string]interface{}{ - "firstString": "firstStringValue", - "secondString": "secondStringValue", + "firstString": "firstStringValue", + }, + CommandOptions: common.CommandOptions{ + CommandGroupKind: v1.BuildCommandGroupKind, + CommandType: v1.CompositeCommandType, }, }, - wantCommands: []string{"command1"}, + wantCommands: []string{"command3"}, wantErr: false, }, { - name: "case 3: get the wrong filtered commands", + name: "Wrong filter for commands", currentCommands: []v1.Command{ { Id: "command1", @@ -104,8 +164,25 @@ func TestDevfile200_GetCommands(t *testing.T) { "firstStringIsWrong": "firstStringValue", }, }, - wantCommands: []string{}, - wantErr: false, + wantErr: false, + }, + { + name: "Invalid command type", + currentCommands: []v1.Command{ + { + Id: "command1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + }), + CommandUnion: v1.CommandUnion{}, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstString": "firstStringValue", + }, + }, + wantErr: true, }, } for _, tt := range tests { @@ -121,26 +198,27 @@ func TestDevfile200_GetCommands(t *testing.T) { } commands, err := d.GetCommands(tt.filterOptions) - if !tt.wantErr && err != nil { - t.Errorf("TestDevfile200_GetCommands() unexpected error - %v", err) - return - } else if tt.wantErr && err == nil { - t.Errorf("TestDevfile200_GetCommands() expected an error but got nil %v", commands) - return - } else if tt.wantErr && err != nil { - return - } + if (err != nil) != tt.wantErr { + t.Errorf("TestDevfile200_GetCommands() error = %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + // confirm the length of actual vs expected + if len(commands) != len(tt.wantCommands) { + t.Errorf("TestDevfile200_GetCommands() error - length of expected commands is not the same as the length of actual commands") + return + } - for _, wantCommand := range tt.wantCommands { - matched := false - for _, devfileCommand := range commands { - if wantCommand == devfileCommand.Id { - matched = true + // compare the command slices for content + for _, wantCommand := range tt.wantCommands { + matched := false + for _, command := range commands { + if wantCommand == command.Id { + matched = true + } } - } - if !matched { - t.Errorf("TestDevfile200_GetCommands() error - command %s not found in the devfile", wantCommand) + if !matched { + t.Errorf("TestDevfile200_GetCommands() error - command %s not found in the devfile", wantCommand) + } } } }) diff --git a/pkg/devfile/parser/data/v2/common/command_helper.go b/pkg/devfile/parser/data/v2/common/command_helper.go index d3e44b49..6a93ac93 100644 --- a/pkg/devfile/parser/data/v2/common/command_helper.go +++ b/pkg/devfile/parser/data/v2/common/command_helper.go @@ -1,6 +1,8 @@ package common import ( + "fmt" + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" ) @@ -47,3 +49,20 @@ func GetExecWorkingDir(dc v1.Command) string { return "" } + +// GetCommandType returns the command type of a given command +func GetCommandType(command v1.Command) (v1.CommandType, error) { + switch { + case command.Apply != nil: + return v1.ApplyCommandType, nil + case command.Composite != nil: + return v1.CompositeCommandType, nil + case command.Exec != nil: + return v1.ExecCommandType, nil + case command.Custom != nil: + return v1.CustomCommandType, nil + + default: + return "", fmt.Errorf("unknown command type") + } +} 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 e3391db3..16fa14bd 100644 --- a/pkg/devfile/parser/data/v2/common/command_helper_test.go +++ b/pkg/devfile/parser/data/v2/common/command_helper_test.go @@ -256,3 +256,80 @@ func TestGetExecWorkingDir(t *testing.T) { } } + +func TestGetCommandType(t *testing.T) { + + tests := []struct { + name string + command v1.Command + wantErr bool + commandType v1.CommandType + }{ + { + name: "Exec command", + command: v1.Command{ + Id: "exec1", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{}, + }, + }, + commandType: v1.ExecCommandType, + wantErr: false, + }, + { + name: "Composite command", + command: v1.Command{ + Id: "comp1", + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{}, + }, + }, + commandType: v1.CompositeCommandType, + wantErr: false, + }, + { + name: "Apply command", + command: v1.Command{ + Id: "apply1", + CommandUnion: v1.CommandUnion{ + Apply: &v1.ApplyCommand{}, + }, + }, + commandType: v1.ApplyCommandType, + wantErr: false, + }, + { + name: "Custom command", + command: v1.Command{ + Id: "custom", + CommandUnion: v1.CommandUnion{ + Custom: &v1.CustomCommand{}, + }, + }, + commandType: v1.CustomCommandType, + wantErr: false, + }, + { + name: "Unknown command", + command: v1.Command{ + Id: "unknown", + CommandUnion: v1.CommandUnion{}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetCommandType(tt.command) + // Unexpected error + if (err != nil) != tt.wantErr { + t.Errorf("TestGetCommandType() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.commandType { + t.Errorf("TestGetCommandType error: command type mismatch, expected: %v got: %v", tt.commandType, got) + } + }) + } + +} diff --git a/pkg/devfile/parser/data/v2/common/component_helper_test.go b/pkg/devfile/parser/data/v2/common/component_helper_test.go index 6fb5896e..76004ced 100644 --- a/pkg/devfile/parser/data/v2/common/component_helper_test.go +++ b/pkg/devfile/parser/data/v2/common/component_helper_test.go @@ -91,13 +91,13 @@ func TestIsVolume(t *testing.T) { func TestGetComponentType(t *testing.T) { tests := []struct { - name string - component v1.Component - wantErr bool - componentype v1.ComponentType + name string + component v1.Component + wantErr bool + componentType v1.ComponentType }{ { - name: "Case 1: Volume component", + name: "Volume component", component: v1.Component{ Name: "name", ComponentUnion: v1.ComponentUnion{ @@ -106,66 +106,66 @@ func TestGetComponentType(t *testing.T) { }, }, }, - componentype: v1.VolumeComponentType, - wantErr: false, + componentType: v1.VolumeComponentType, + wantErr: false, }, { - name: "Case 2: Openshift component", + name: "Openshift component", component: v1.Component{ Name: "name", ComponentUnion: v1.ComponentUnion{ Openshift: &v1.OpenshiftComponent{}, }, }, - componentype: v1.OpenshiftComponentType, - wantErr: false, + componentType: v1.OpenshiftComponentType, + wantErr: false, }, { - name: "Case 3: Kubernetes component", + name: "Kubernetes component", component: v1.Component{ Name: "name", ComponentUnion: v1.ComponentUnion{ Kubernetes: &v1.KubernetesComponent{}, }, }, - componentype: v1.KubernetesComponentType, - wantErr: false, + componentType: v1.KubernetesComponentType, + wantErr: false, }, { - name: "Case 4: Container component", + name: "Container component", component: v1.Component{ Name: "name", ComponentUnion: v1.ComponentUnion{ Container: &v1.ContainerComponent{}, }, }, - componentype: v1.ContainerComponentType, - wantErr: false, + componentType: v1.ContainerComponentType, + wantErr: false, }, { - name: "Case 5: Plugin component", + name: "Plugin component", component: v1.Component{ Name: "name", ComponentUnion: v1.ComponentUnion{ Plugin: &v1.PluginComponent{}, }, }, - componentype: v1.PluginComponentType, - wantErr: false, + componentType: v1.PluginComponentType, + wantErr: false, }, { - name: "Case 6: Custom component", + name: "Custom component", component: v1.Component{ Name: "name", ComponentUnion: v1.ComponentUnion{ Custom: &v1.CustomComponent{}, }, }, - componentype: v1.CustomComponentType, - wantErr: false, + componentType: v1.CustomComponentType, + wantErr: false, }, { - name: "Case 6: unknown component", + name: "Unknown component", component: v1.Component{ Name: "name", ComponentUnion: v1.ComponentUnion{}, @@ -181,8 +181,8 @@ func TestGetComponentType(t *testing.T) { t.Errorf("TestGetComponentType() error = %v, wantErr %v", err, tt.wantErr) return } - if got != tt.componentype { - t.Errorf("TestGetComponentType error: component type mismatch, expected: %v got: %v", tt.componentype, got) + if got != tt.componentType { + t.Errorf("TestGetComponentType error: component type mismatch, expected: %v got: %v", tt.componentType, got) } }) } diff --git a/pkg/devfile/parser/data/v2/common/options.go b/pkg/devfile/parser/data/v2/common/options.go index 14d595d0..b1443151 100644 --- a/pkg/devfile/parser/data/v2/common/options.go +++ b/pkg/devfile/parser/data/v2/common/options.go @@ -3,13 +3,46 @@ package common import ( "reflect" + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" apiAttributes "github.com/devfile/api/v2/pkg/attributes" ) // DevfileOptions provides options for Devfile operations type DevfileOptions struct { - // Filter is a map that lets you filter devfile object against their attributes. Interface can be string, float, boolean or a map + // Filter is a map that lets filter devfile object against their attributes. Interface can be string, float, boolean or a map Filter map[string]interface{} + + // CommandOptions specifies the various options available to filter commands + CommandOptions CommandOptions + + // ComponentOptions specifies the various options available to filter components + ComponentOptions ComponentOptions + + // ProjectOptions specifies the various options available to filter projects/starterProjects + ProjectOptions ProjectOptions +} + +// CommandOptions specifies the various options available to filter commands +type CommandOptions struct { + // CommandGroupKind is an option that allows to filter command based on their kind + CommandGroupKind v1.CommandGroupKind + + // CommandType is an option that allows to filter command based on their type + CommandType v1.CommandType +} + +// ComponentOptions specifies the various options available to filter components +type ComponentOptions struct { + + // ComponentType is an option that allows to filter component based on their type + ComponentType v1.ComponentType +} + +// ProjectOptions specifies the various options available to filter projects/starterProjects +type ProjectOptions struct { + + // ProjectSourceType is an option that allows to filter project based on their source type + ProjectSourceType v1.ProjectSourceType } // FilterDevfileObject filters devfile attributes with the given options diff --git a/pkg/devfile/parser/data/v2/common/project_helper.go b/pkg/devfile/parser/data/v2/common/project_helper.go index 4028779b..fe2d0859 100644 --- a/pkg/devfile/parser/data/v2/common/project_helper.go +++ b/pkg/devfile/parser/data/v2/common/project_helper.go @@ -41,3 +41,20 @@ func GetDefaultSource(ps v1.GitLikeProjectSource) (remoteName string, remoteURL return remoteName, remoteURL, revision, err } + +// GetProjectSourceType returns the source type of a given project source +func GetProjectSourceType(projectSrc v1.ProjectSource) (v1.ProjectSourceType, error) { + switch { + case projectSrc.Git != nil: + return v1.GitProjectSourceType, nil + case projectSrc.Github != nil: + return v1.GitHubProjectSourceType, nil + case projectSrc.Zip != nil: + return v1.ZipProjectSourceType, nil + case projectSrc.Custom != nil: + return v1.CustomProjectSourceType, nil + + default: + return "", fmt.Errorf("unknown project source type") + } +} diff --git a/pkg/devfile/parser/data/v2/common/project_helper_test.go b/pkg/devfile/parser/data/v2/common/project_helper_test.go index 109b560e..397a3adb 100644 --- a/pkg/devfile/parser/data/v2/common/project_helper_test.go +++ b/pkg/devfile/parser/data/v2/common/project_helper_test.go @@ -117,3 +117,65 @@ func TestGitLikeProjectSource_GetDefaultSource(t *testing.T) { }) } } + +func TestGetProjectSrcType(t *testing.T) { + + tests := []struct { + name string + projectSrc v1.ProjectSource + wantErr bool + projectSrcType v1.ProjectSourceType + }{ + { + name: "Git project", + projectSrc: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + projectSrcType: v1.GitProjectSourceType, + wantErr: false, + }, + { + name: "Github project", + projectSrc: v1.ProjectSource{ + Github: &v1.GithubProjectSource{}, + }, + projectSrcType: v1.GitHubProjectSourceType, + wantErr: false, + }, + { + name: "Zip project", + projectSrc: v1.ProjectSource{ + Zip: &v1.ZipProjectSource{}, + }, + projectSrcType: v1.ZipProjectSourceType, + wantErr: false, + }, + { + name: "Custom project", + projectSrc: v1.ProjectSource{ + Custom: &v1.CustomProjectSource{}, + }, + projectSrcType: v1.CustomProjectSourceType, + wantErr: false, + }, + { + name: "Unknown project", + projectSrc: v1.ProjectSource{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetProjectSourceType(tt.projectSrc) + // Unexpected error + if (err != nil) != tt.wantErr { + t.Errorf("TestGetProjectSrcType() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.projectSrcType { + t.Errorf("TestGetProjectSrcType error: project src type mismatch, expected: %v got: %v", tt.projectSrcType, got) + } + }) + } + +} diff --git a/pkg/devfile/parser/data/v2/components.go b/pkg/devfile/parser/data/v2/components.go index 0bac4bfd..372ba80f 100644 --- a/pkg/devfile/parser/data/v2/components.go +++ b/pkg/devfile/parser/data/v2/components.go @@ -1,32 +1,46 @@ package v2 import ( + "reflect" + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "github.com/devfile/library/pkg/devfile/parser/data/v2/common" ) // GetComponents returns the slice of Component objects parsed from the Devfile func (d *DevfileV2) GetComponents(options common.DevfileOptions) ([]v1.Component, error) { - if len(options.Filter) == 0 { + + if reflect.DeepEqual(options, common.DevfileOptions{}) { return d.Components, nil } var components []v1.Component - for _, comp := range d.Components { - filterIn, err := common.FilterDevfileObject(comp.Attributes, options) + for _, component := range d.Components { + // Filter Component Attributes + filterIn, err := common.FilterDevfileObject(component.Attributes, options) if err != nil { return nil, err + } else if !filterIn { + continue } - if filterIn { - components = append(components, comp) + // Filter Component Type - Container, Volume, etc. + componentType, err := common.GetComponentType(component) + if err != nil { + return nil, err + } + if options.ComponentOptions.ComponentType != "" && componentType != options.ComponentOptions.ComponentType { + continue } + + components = append(components, component) } return components, nil } -// GetDevfileContainerComponents iterates through the components in the devfile and returns a list of devfile container components +// GetDevfileContainerComponents iterates through the components in the devfile and returns a list of devfile container components. +// Deprecated, use GetComponents() with the DevfileOptions. func (d *DevfileV2) GetDevfileContainerComponents(options common.DevfileOptions) ([]v1.Component, error) { var components []v1.Component devfileComponents, err := d.GetComponents(options) @@ -41,7 +55,8 @@ func (d *DevfileV2) GetDevfileContainerComponents(options common.DevfileOptions) return components, nil } -// GetDevfileVolumeComponents iterates through the components in the devfile and returns a list of devfile volume components +// GetDevfileVolumeComponents iterates through the components in the devfile and returns a list of devfile volume components. +// Deprecated, use GetComponents() with the DevfileOptions. func (d *DevfileV2) GetDevfileVolumeComponents(options common.DevfileOptions) ([]v1.Component, error) { var components []v1.Component devfileComponents, err := d.GetComponents(options) diff --git a/pkg/devfile/parser/data/v2/components_test.go b/pkg/devfile/parser/data/v2/components_test.go index 09db14fe..2d62bc3d 100644 --- a/pkg/devfile/parser/data/v2/components_test.go +++ b/pkg/devfile/parser/data/v2/components_test.go @@ -170,6 +170,196 @@ func TestDevfile200_UpdateComponent(t *testing.T) { } } +func TestGetDevfileComponents(t *testing.T) { + + tests := []struct { + name string + component []v1.Component + wantComponents []string + filterOptions common.DevfileOptions + wantErr bool + }{ + { + name: "Invalid devfile", + component: []v1.Component{}, + }, + { + name: "Get all the components", + component: []v1.Component{ + { + Name: "comp1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + { + Name: "comp2", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "fourthString": "fourthStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{}, + }, + }, + }, + wantComponents: []string{"comp1", "comp2"}, + }, + { + name: "Get component with the specified filter", + component: []v1.Component{ + { + Name: "comp1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + { + Name: "comp2", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + { + Name: "comp3", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "fourthString": "fourthStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{}, + }, + }, + { + Name: "comp4", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "fourthString": "fourthStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstString": "firstStringValue", + }, + CommandOptions: common.CommandOptions{ + CommandGroupKind: v1.BuildCommandGroupKind, + CommandType: v1.CompositeCommandType, + }, + ComponentOptions: common.ComponentOptions{ + ComponentType: v1.VolumeComponentType, + }, + }, + wantComponents: []string{"comp3"}, + }, + { + name: "Wrong filter for component", + component: []v1.Component{ + { + Name: "comp1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + { + Name: "comp2", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstStringIsWrong": "firstStringValue", + }, + ComponentOptions: common.ComponentOptions{ + ComponentType: v1.ContainerComponentType, + }, + }, + wantErr: false, + }, + { + name: "Invalid component type", + component: []v1.Component{ + { + Name: "comp1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + }), + ComponentUnion: v1.ComponentUnion{}, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstString": "firstStringValue", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: tt.component, + }, + }, + }, + } + + components, err := d.GetComponents(tt.filterOptions) + if (err != nil) != tt.wantErr { + t.Errorf("TestGetDevfileComponents() error = %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + // confirm the length of actual vs expected + if len(components) != len(tt.wantComponents) { + t.Errorf("TestGetDevfileComponents() error - length of expected components is not the same as the length of actual components") + return + } + + // compare the component slices for content + for _, wantComponent := range tt.wantComponents { + matched := false + for _, component := range components { + if wantComponent == component.Name { + matched = true + } + } + + if !matched { + t.Errorf("TestGetDevfileComponents() error - component %s not found in the devfile", wantComponent) + } + } + } + }) + } + +} + func TestGetDevfileContainerComponents(t *testing.T) { tests := []struct { diff --git a/pkg/devfile/parser/data/v2/projects.go b/pkg/devfile/parser/data/v2/projects.go index 62185bee..20871c98 100644 --- a/pkg/devfile/parser/data/v2/projects.go +++ b/pkg/devfile/parser/data/v2/projects.go @@ -1,6 +1,7 @@ package v2 import ( + "reflect" "strings" v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" @@ -9,20 +10,31 @@ import ( // GetProjects returns the Project Object parsed from devfile func (d *DevfileV2) GetProjects(options common.DevfileOptions) ([]v1.Project, error) { - if len(options.Filter) == 0 { + + if reflect.DeepEqual(options, common.DevfileOptions{}) { return d.Projects, nil } var projects []v1.Project - for _, proj := range d.Projects { - filterIn, err := common.FilterDevfileObject(proj.Attributes, options) + for _, project := range d.Projects { + // Filter Project Attributes + filterIn, err := common.FilterDevfileObject(project.Attributes, options) if err != nil { return nil, err + } else if !filterIn { + continue } - if filterIn { - projects = append(projects, proj) + // Filter Project Source Type - Git, Zip, etc. + projectSourceType, err := common.GetProjectSourceType(project.ProjectSource) + if err != nil { + return nil, err + } + if options.ProjectOptions.ProjectSourceType != "" && projectSourceType != options.ProjectOptions.ProjectSourceType { + continue } + + projects = append(projects, project) } return projects, nil @@ -73,20 +85,31 @@ func (d *DevfileV2) DeleteProject(name string) error { //GetStarterProjects returns the DevfileStarterProject parsed from devfile func (d *DevfileV2) GetStarterProjects(options common.DevfileOptions) ([]v1.StarterProject, error) { - if len(options.Filter) == 0 { + + if reflect.DeepEqual(options, common.DevfileOptions{}) { return d.StarterProjects, nil } var starterProjects []v1.StarterProject - for _, starterProj := range d.StarterProjects { - filterIn, err := common.FilterDevfileObject(starterProj.Attributes, options) + for _, starterProject := range d.StarterProjects { + // Filter Starter Project Attributes + filterIn, err := common.FilterDevfileObject(starterProject.Attributes, options) if err != nil { return nil, err + } else if !filterIn { + continue } - if filterIn { - starterProjects = append(starterProjects, starterProj) + // Filter Starter Project Source Type - Git, Zip, etc. + starterProjectSourceType, err := common.GetProjectSourceType(starterProject.ProjectSource) + if err != nil { + return nil, err } + if options.ProjectOptions.ProjectSourceType != "" && starterProjectSourceType != options.ProjectOptions.ProjectSourceType { + continue + } + + starterProjects = append(starterProjects, starterProject) } return starterProjects, nil diff --git a/pkg/devfile/parser/data/v2/projects_test.go b/pkg/devfile/parser/data/v2/projects_test.go index d3455f7e..6b3e9e2c 100644 --- a/pkg/devfile/parser/data/v2/projects_test.go +++ b/pkg/devfile/parser/data/v2/projects_test.go @@ -5,10 +5,170 @@ import ( "testing" v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/api/v2/pkg/attributes" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" "github.com/kylelemons/godebug/pretty" "github.com/stretchr/testify/assert" ) +func TestDevfile200_GetProjects(t *testing.T) { + + tests := []struct { + name string + currentProjects []v1.Project + filterOptions common.DevfileOptions + wantProjects []string + wantErr bool + }{ + { + name: "Get all the projects", + currentProjects: []v1.Project{ + { + Name: "project1", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + }, + { + Name: "project2", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + }, + }, + filterOptions: common.DevfileOptions{}, + wantProjects: []string{"project1", "project2"}, + wantErr: false, + }, + { + name: "Get the filtered projects", + currentProjects: []v1.Project{ + { + Name: "project1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + ClonePath: "/project", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + }, + { + Name: "project2", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + ClonePath: "/project", + ProjectSource: v1.ProjectSource{ + Zip: &v1.ZipProjectSource{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstString": "firstStringValue", + }, + ProjectOptions: common.ProjectOptions{ + ProjectSourceType: v1.GitProjectSourceType, + }, + }, + wantProjects: []string{"project1"}, + wantErr: false, + }, + { + name: "Wrong filter for projects", + currentProjects: []v1.Project{ + { + Name: "project1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + ClonePath: "/project", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + }, + { + Name: "project2", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + ClonePath: "/project", + ProjectSource: v1.ProjectSource{ + Zip: &v1.ZipProjectSource{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstStringIsWrong": "firstStringValue", + }, + }, + wantErr: false, + }, + { + name: "Invalid project src type", + currentProjects: []v1.Project{ + { + Name: "project1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + }), + ProjectSource: v1.ProjectSource{}, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstString": "firstStringValue", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Projects: tt.currentProjects, + }, + }, + }, + } + + projects, err := d.GetProjects(tt.filterOptions) + if (err != nil) != tt.wantErr { + t.Errorf("TestDevfile200_GetProjects() error = %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + // confirm the length of actual vs expected + if len(projects) != len(tt.wantProjects) { + t.Errorf("TestDevfile200_GetProjects() error - length of expected projects is not the same as the length of actual projects") + return + } + + // compare the project slices for content + for _, wantProject := range tt.wantProjects { + matched := false + for _, project := range projects { + if wantProject == project.Name { + matched = true + } + } + + if !matched { + t.Errorf("TestDevfile200_GetProjects() error - project %s not found in the devfile", wantProject) + } + } + } + }) + } +} + func TestDevfile200_AddProjects(t *testing.T) { currentProject := []v1.Project{ { @@ -246,6 +406,164 @@ func TestDevfile200_DeleteProject(t *testing.T) { } +func TestDevfile200_GetStarterProjects(t *testing.T) { + + tests := []struct { + name string + currentStarterProjects []v1.StarterProject + filterOptions common.DevfileOptions + wantStarterProjects []string + wantErr bool + }{ + { + name: "Get all the starter projects", + currentStarterProjects: []v1.StarterProject{ + { + Name: "project1", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + }, + { + Name: "project2", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + }, + }, + filterOptions: common.DevfileOptions{}, + wantStarterProjects: []string{"project1", "project2"}, + wantErr: false, + }, + { + name: "Get the filtered starter projects", + currentStarterProjects: []v1.StarterProject{ + { + Name: "project1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + }, + { + Name: "project2", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + ProjectSource: v1.ProjectSource{ + Zip: &v1.ZipProjectSource{}, + }, + }, + { + Name: "project3", + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + ProjectOptions: common.ProjectOptions{ + ProjectSourceType: v1.GitProjectSourceType, + }, + }, + wantStarterProjects: []string{"project1", "project3"}, + wantErr: false, + }, + { + name: "Wrong filter for starter projects", + currentStarterProjects: []v1.StarterProject{ + { + Name: "project1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "secondString": "secondStringValue", + }), + ProjectSource: v1.ProjectSource{ + Git: &v1.GitProjectSource{}, + }, + }, + { + Name: "project2", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + "thirdString": "thirdStringValue", + }), + ProjectSource: v1.ProjectSource{ + Zip: &v1.ZipProjectSource{}, + }, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstStringIsWrong": "firstStringValue", + }, + }, + wantErr: false, + }, + { + name: "Invalid starter project src type", + currentStarterProjects: []v1.StarterProject{ + { + Name: "project1", + Attributes: attributes.Attributes{}.FromStringMap(map[string]string{ + "firstString": "firstStringValue", + }), + ProjectSource: v1.ProjectSource{}, + }, + }, + filterOptions: common.DevfileOptions{ + Filter: map[string]interface{}{ + "firstString": "firstStringValue", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DevfileV2{ + v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + StarterProjects: tt.currentStarterProjects, + }, + }, + }, + } + + starterProjects, err := d.GetStarterProjects(tt.filterOptions) + if (err != nil) != tt.wantErr { + t.Errorf("TestDevfile200_GetStarterProjects() error = %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + // confirm the length of actual vs expected + if len(starterProjects) != len(tt.wantStarterProjects) { + t.Errorf("TestDevfile200_GetStarterProjects() error - length of expected starter projects is not the same as the length of actual starter projects") + return + } + + // compare the starter project slices for content + for _, wantProject := range tt.wantStarterProjects { + matched := false + + for _, starterProject := range starterProjects { + if wantProject == starterProject.Name { + matched = true + } + } + + if !matched { + t.Errorf("TestDevfile200_GetStarterProjects() error - starter project %s not found in the devfile", wantProject) + } + } + } + }) + } +} + func TestDevfile200_AddStarterProjects(t *testing.T) { currentProject := []v1.StarterProject{ {