Skip to content

Commit 6424f41

Browse files
Add devfile validation (#301)
* validation - odo parity Signed-off-by: Maysun J Faisal <[email protected]> * add two util functions Signed-off-by: Stephanie <[email protected]> * Add command validations Signed-off-by: Maysun J Faisal <[email protected]> * Update event validation Signed-off-by: Maysun J Faisal <[email protected]> * add some unittests Signed-off-by: Stephanie <[email protected]> * validation - odo parity Signed-off-by: Maysun J Faisal <[email protected]> * run go fmt Signed-off-by: Stephanie <[email protected]> * Add project validations Signed-off-by: Maysun J Faisal <[email protected]> * Clean up PR Signed-off-by: Maysun J Faisal <[email protected]> * Address PR reviews 1 Signed-off-by: Maysun J Faisal <[email protected]> * Address PR reviews 2 Signed-off-by: Maysun J Faisal <[email protected]> * Address PR review for tests Signed-off-by: Maysun J Faisal <[email protected]> * Update id and name validation Signed-off-by: Maysun J Faisal <[email protected]> * Address PR reviews 3 Signed-off-by: Maysun J Faisal <[email protected]> Co-authored-by: Stephanie <[email protected]>
1 parent 793ec14 commit 6424f41

15 files changed

+1890
-2
lines changed

pkg/apis/workspaces/v1alpha2/keyed_implementations.go

+21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package v1alpha2
22

33
import (
4+
"fmt"
45
"reflect"
56
)
67

@@ -18,3 +19,23 @@ func extractKeys(keyedList interface{}) []Keyed {
1819
}
1920
return keys
2021
}
22+
23+
// CheckDuplicateKeys checks if duplicate keys are present in the devfile objects
24+
func CheckDuplicateKeys(keyedList interface{}) error {
25+
seen := map[string]bool{}
26+
value := reflect.ValueOf(keyedList)
27+
for i := 0; i < value.Len(); i++ {
28+
elem := value.Index(i)
29+
if elem.CanInterface() {
30+
i := elem.Interface()
31+
if keyed, ok := i.(Keyed); ok {
32+
key := keyed.Key()
33+
if seen[key] {
34+
return fmt.Errorf("duplicate key: %s", key)
35+
}
36+
seen[key] = true
37+
}
38+
}
39+
}
40+
return nil
41+
}

pkg/validation/commands.go

+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package validation
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
8+
)
9+
10+
// ValidateCommands validates the devfile commands and checks:
11+
// 1. there are no duplicate command ids
12+
// 2. the command type is not invalid
13+
// 3. if a command is part of a command group, there is a single default command
14+
func ValidateCommands(commands []v1alpha2.Command, components []v1alpha2.Component) (err error) {
15+
groupKindCommandMap := make(map[v1alpha2.CommandGroupKind][]v1alpha2.Command)
16+
commandMap := getCommandsMap(commands)
17+
18+
err = v1alpha2.CheckDuplicateKeys(commands)
19+
if err != nil {
20+
return err
21+
}
22+
23+
for _, command := range commands {
24+
// parentCommands is a map to keep a track of all the parent commands when validating the composite command's subcommands recursively
25+
parentCommands := make(map[string]string)
26+
err = validateCommand(command, parentCommands, commandMap, components)
27+
if err != nil {
28+
return err
29+
}
30+
31+
commandGroup := getGroup(command)
32+
if commandGroup != nil {
33+
groupKindCommandMap[commandGroup.Kind] = append(groupKindCommandMap[commandGroup.Kind], command)
34+
}
35+
}
36+
37+
groupErrors := ""
38+
for groupKind, commands := range groupKindCommandMap {
39+
if err = validateGroup(commands); err != nil {
40+
groupErrors += fmt.Sprintf("\ncommand group %s error - %s", groupKind, err.Error())
41+
}
42+
}
43+
44+
if len(groupErrors) > 0 {
45+
err = fmt.Errorf("%s", groupErrors)
46+
}
47+
48+
return err
49+
}
50+
51+
// validateCommand validates a given devfile command where parentCommands is a map to track all the parent commands when validating
52+
// the composite command's subcommands recursively and devfileCommands is a map of command id to the devfile command
53+
func validateCommand(command v1alpha2.Command, parentCommands map[string]string, devfileCommands map[string]v1alpha2.Command, components []v1alpha2.Component) (err error) {
54+
55+
switch {
56+
case command.Composite != nil:
57+
return validateCompositeCommand(&command, parentCommands, devfileCommands, components)
58+
case command.Exec != nil || command.Apply != nil:
59+
return validateCommandComponent(command, components)
60+
case command.VscodeLaunch != nil:
61+
if command.VscodeLaunch.Uri != "" {
62+
return ValidateURI(command.VscodeLaunch.Uri)
63+
}
64+
case command.VscodeTask != nil:
65+
if command.VscodeTask.Uri != "" {
66+
return ValidateURI(command.VscodeTask.Uri)
67+
}
68+
default:
69+
err = fmt.Errorf("command %s type is invalid", command.Id)
70+
}
71+
72+
return err
73+
}
74+
75+
// validateGroup validates commands belonging to a specific group kind. If there are multiple commands belonging to the same group:
76+
// 1. without any default, err out
77+
// 2. with more than one default, err out
78+
func validateGroup(commands []v1alpha2.Command) error {
79+
defaultCommandCount := 0
80+
81+
if len(commands) > 1 {
82+
for _, command := range commands {
83+
if getGroup(command).IsDefault {
84+
defaultCommandCount++
85+
}
86+
}
87+
} else {
88+
return nil
89+
}
90+
91+
if defaultCommandCount == 0 {
92+
return fmt.Errorf("there should be exactly one default command, currently there is no default command")
93+
} else if defaultCommandCount > 1 {
94+
return fmt.Errorf("there should be exactly one default command, currently there is more than one default command")
95+
}
96+
97+
return nil
98+
}
99+
100+
// getGroup returns the group the command belongs to, or nil if the command does not belong to a group
101+
func getGroup(command v1alpha2.Command) *v1alpha2.CommandGroup {
102+
switch {
103+
case command.Composite != nil:
104+
return command.Composite.Group
105+
case command.Exec != nil:
106+
return command.Exec.Group
107+
case command.Apply != nil:
108+
return command.Apply.Group
109+
case command.VscodeLaunch != nil:
110+
return command.VscodeLaunch.Group
111+
case command.VscodeTask != nil:
112+
return command.VscodeTask.Group
113+
case command.Custom != nil:
114+
return command.Custom.Group
115+
116+
default:
117+
return nil
118+
}
119+
}
120+
121+
// validateCommandComponent validates the given exec or apply command, the command should map to a valid container component
122+
func validateCommandComponent(command v1alpha2.Command, components []v1alpha2.Component) error {
123+
124+
if command.Exec == nil && command.Apply == nil {
125+
return &InvalidCommandError{commandId: command.Id, reason: "should be of type exec or apply"}
126+
}
127+
128+
var commandComponent string
129+
if command.Exec != nil {
130+
commandComponent = command.Exec.Component
131+
} else if command.Apply != nil {
132+
commandComponent = command.Apply.Component
133+
}
134+
135+
// must map to a container component
136+
for _, component := range components {
137+
if component.Container != nil && commandComponent == component.Name {
138+
return nil
139+
}
140+
}
141+
return &InvalidCommandError{commandId: command.Id, reason: "command does not map to a container component"}
142+
}
143+
144+
// validateCompositeCommand checks that the specified composite command is valid. The command:
145+
// 1. should not reference itself via a subcommand
146+
// 2. should not indirectly reference itself via a subcommand which is a composite command
147+
// 3. should reference a valid devfile command
148+
// 4. should have a valid exec sub command
149+
// where parentCommands is a map to track all the parent commands when validating the composite command's subcommands recursilvely
150+
// and devfileCommands is a map of command id to the devfile command
151+
func validateCompositeCommand(command *v1alpha2.Command, parentCommands map[string]string, devfileCommands map[string]v1alpha2.Command, components []v1alpha2.Component) error {
152+
153+
// Store the command ID in a map of parent commands
154+
parentCommands[command.Id] = command.Id
155+
156+
if command.Composite == nil {
157+
return &InvalidCommandError{commandId: command.Id, reason: "should be of type composite"}
158+
}
159+
160+
// Loop over the commands and validate that each command points to a command that's in the devfile
161+
for _, cmd := range command.Composite.Commands {
162+
if strings.ToLower(cmd) == command.Id {
163+
return &InvalidCommandError{commandId: command.Id, reason: "composite command cannot reference itself"}
164+
}
165+
166+
// Don't allow commands to indirectly reference themselves, so check if the command equals any of the parent commands in the command tree
167+
_, ok := parentCommands[strings.ToLower(cmd)]
168+
if ok {
169+
return &InvalidCommandError{commandId: command.Id, reason: "composite command cannot indirectly reference itself"}
170+
}
171+
172+
subCommand, ok := devfileCommands[strings.ToLower(cmd)]
173+
if !ok {
174+
return &InvalidCommandError{commandId: command.Id, reason: fmt.Sprintf("the command %q mentioned in the composite command does not exist in the devfile", cmd)}
175+
}
176+
177+
err := validateCommand(subCommand, parentCommands, devfileCommands, components)
178+
if err != nil {
179+
return err
180+
}
181+
delete(parentCommands, cmd)
182+
}
183+
return nil
184+
}

0 commit comments

Comments
 (0)