volcano.sh/volcano@v1.9.0/pkg/webhooks/admission/jobs/validate/util.go (about) 1 /* 2 Copyright 2018 The Volcano Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package validate 18 19 import ( 20 "fmt" 21 22 "github.com/hashicorp/go-multierror" 23 "k8s.io/apimachinery/pkg/util/validation/field" 24 "k8s.io/kubernetes/pkg/apis/core/validation" 25 26 batchv1alpha1 "volcano.sh/apis/pkg/apis/batch/v1alpha1" 27 busv1alpha1 "volcano.sh/apis/pkg/apis/bus/v1alpha1" 28 ) 29 30 // policyEventMap defines all policy events and whether to allow external use. 31 var policyEventMap = map[busv1alpha1.Event]bool{ 32 busv1alpha1.AnyEvent: true, 33 busv1alpha1.PodFailedEvent: true, 34 busv1alpha1.PodEvictedEvent: true, 35 busv1alpha1.JobUnknownEvent: true, 36 busv1alpha1.TaskCompletedEvent: true, 37 busv1alpha1.TaskFailedEvent: true, 38 busv1alpha1.OutOfSyncEvent: false, 39 busv1alpha1.CommandIssuedEvent: false, 40 busv1alpha1.JobUpdatedEvent: true, 41 } 42 43 // policyActionMap defines all policy actions and whether to allow external use. 44 var policyActionMap = map[busv1alpha1.Action]bool{ 45 busv1alpha1.AbortJobAction: true, 46 busv1alpha1.RestartJobAction: true, 47 busv1alpha1.RestartTaskAction: true, 48 busv1alpha1.TerminateJobAction: true, 49 busv1alpha1.CompleteJobAction: true, 50 busv1alpha1.ResumeJobAction: true, 51 busv1alpha1.SyncJobAction: false, 52 busv1alpha1.EnqueueAction: false, 53 busv1alpha1.SyncQueueAction: false, 54 busv1alpha1.OpenQueueAction: false, 55 busv1alpha1.CloseQueueAction: false, 56 } 57 58 func validatePolicies(policies []batchv1alpha1.LifecyclePolicy, fldPath *field.Path) error { 59 var err error 60 policyEvents := map[busv1alpha1.Event]struct{}{} 61 exitCodes := map[int32]struct{}{} 62 63 for _, policy := range policies { 64 if (policy.Event != "" || len(policy.Events) != 0) && policy.ExitCode != nil { 65 err = multierror.Append(err, fmt.Errorf("must not specify event and exitCode simultaneously")) 66 break 67 } 68 69 if policy.Event == "" && len(policy.Events) == 0 && policy.ExitCode == nil { 70 err = multierror.Append(err, fmt.Errorf("either event and exitCode should be specified")) 71 break 72 } 73 74 if len(policy.Event) != 0 || len(policy.Events) != 0 { 75 bFlag := false 76 policyEventsList := getEventList(policy) 77 for _, event := range policyEventsList { 78 if allow, ok := policyEventMap[event]; !ok || !allow { 79 err = multierror.Append(err, field.Invalid(fldPath, event, "invalid policy event")) 80 bFlag = true 81 break 82 } 83 84 if allow, ok := policyActionMap[policy.Action]; !ok || !allow { 85 err = multierror.Append(err, field.Invalid(fldPath, policy.Action, "invalid policy action")) 86 bFlag = true 87 break 88 } 89 if _, found := policyEvents[event]; found { 90 err = multierror.Append(err, fmt.Errorf("duplicate event %v across different policy", event)) 91 bFlag = true 92 break 93 } else { 94 policyEvents[event] = struct{}{} 95 } 96 } 97 if bFlag { 98 break 99 } 100 } else { 101 if *policy.ExitCode == 0 { 102 err = multierror.Append(err, fmt.Errorf("0 is not a valid error code")) 103 break 104 } 105 if _, found := exitCodes[*policy.ExitCode]; found { 106 err = multierror.Append(err, fmt.Errorf("duplicate exitCode %v", *policy.ExitCode)) 107 break 108 } else { 109 exitCodes[*policy.ExitCode] = struct{}{} 110 } 111 } 112 } 113 114 if _, found := policyEvents[busv1alpha1.AnyEvent]; found && len(policyEvents) > 1 { 115 err = multierror.Append(err, fmt.Errorf("if there's * here, no other policy should be here")) 116 } 117 118 return err 119 } 120 121 func getEventList(policy batchv1alpha1.LifecyclePolicy) []busv1alpha1.Event { 122 policyEventsList := policy.Events 123 if len(policy.Event) > 0 { 124 policyEventsList = append(policyEventsList, policy.Event) 125 } 126 uniquePolicyEventlist := removeDuplicates(policyEventsList) 127 return uniquePolicyEventlist 128 } 129 130 func removeDuplicates(eventList []busv1alpha1.Event) []busv1alpha1.Event { 131 keys := make(map[busv1alpha1.Event]bool) 132 list := []busv1alpha1.Event{} 133 for _, val := range eventList { 134 if _, value := keys[val]; !value { 135 keys[val] = true 136 list = append(list, val) 137 } 138 } 139 return list 140 } 141 142 func getValidEvents() []busv1alpha1.Event { 143 var events []busv1alpha1.Event 144 for e, allow := range policyEventMap { 145 if allow { 146 events = append(events, e) 147 } 148 } 149 150 return events 151 } 152 153 func getValidActions() []busv1alpha1.Action { 154 var actions []busv1alpha1.Action 155 for a, allow := range policyActionMap { 156 if allow { 157 actions = append(actions, a) 158 } 159 } 160 161 return actions 162 } 163 164 // validateIO validates IO configuration. 165 func validateIO(volumes []batchv1alpha1.VolumeSpec) error { 166 volumeMap := map[string]bool{} 167 for _, volume := range volumes { 168 if len(volume.MountPath) == 0 { 169 return fmt.Errorf(" mountPath is required;") 170 } 171 if _, found := volumeMap[volume.MountPath]; found { 172 return fmt.Errorf(" duplicated mountPath: %s;", volume.MountPath) 173 } 174 if volume.VolumeClaim == nil && volume.VolumeClaimName == "" { 175 return fmt.Errorf(" either VolumeClaim or VolumeClaimName must be specified;") 176 } 177 if len(volume.VolumeClaimName) != 0 { 178 if volume.VolumeClaim != nil { 179 return fmt.Errorf("conflict: If you want to use an existing PVC, just specify VolumeClaimName." + 180 "If you want to create a new PVC, you do not need to specify VolumeClaimName") 181 } 182 if errMsgs := validation.ValidatePersistentVolumeName(volume.VolumeClaimName, false); len(errMsgs) > 0 { 183 return fmt.Errorf("invalid VolumeClaimName %s : %v", volume.VolumeClaimName, errMsgs) 184 } 185 } 186 187 volumeMap[volume.MountPath] = true 188 } 189 return nil 190 } 191 192 // topoSort uses topo sort to sort job tasks based on dependsOn field 193 // it will return an array contains all sorted task names and a bool which indicates whether it's a valid dag 194 func topoSort(job *batchv1alpha1.Job) ([]string, bool) { 195 graph, inDegree, taskList := makeGraph(job) 196 var taskStack []string 197 for task, degree := range inDegree { 198 if degree == 0 { 199 taskStack = append(taskStack, task) 200 } 201 } 202 203 sortedTasks := make([]string, 0) 204 for len(taskStack) > 0 { 205 length := len(taskStack) 206 var out string 207 out, taskStack = taskStack[length-1], taskStack[:length-1] 208 sortedTasks = append(sortedTasks, out) 209 for in, connected := range graph[out] { 210 if connected { 211 graph[out][in] = false 212 inDegree[in]-- 213 if inDegree[in] == 0 { 214 taskStack = append(taskStack, in) 215 } 216 } 217 } 218 } 219 220 isDag := len(sortedTasks) == len(taskList) 221 if !isDag { 222 return nil, false 223 } 224 225 return sortedTasks, isDag 226 } 227 228 func makeGraph(job *batchv1alpha1.Job) (map[string]map[string]bool, map[string]int, []string) { 229 graph := make(map[string]map[string]bool) 230 inDegree := make(map[string]int) 231 taskList := make([]string, 0) 232 233 for _, task := range job.Spec.Tasks { 234 taskList = append(taskList, task.Name) 235 inDegree[task.Name] = 0 236 if task.DependsOn != nil { 237 for _, dependOnTask := range task.DependsOn.Name { 238 if graph[dependOnTask] == nil { 239 graph[dependOnTask] = make(map[string]bool) 240 } 241 242 graph[dependOnTask][task.Name] = true 243 inDegree[task.Name]++ 244 } 245 } 246 } 247 248 return graph, inDegree, taskList 249 }