fuzzyworld

template / ci-testing / files / profile.go

Branch ci-testing · default branch · all branches

profile.go

9.4 KB · history · blame

1package main
2
3import (
4 "bytes"
5 "fmt"
6 "os"
7 "os/user"
8 "path/filepath"
9 "runtime"
10 "strings"
11 "time"
12
13 "golang.org/x/exp/slices"
14)
15
16type ProfileType string
17
18const (
19 FileType ProfileType = "file"
20 FolderType ProfileType = "folder"
21 ScriptType ProfileType = "script"
22)
23
24type OSType string
25
26const (
27 Windows OSType = "Windows"
28 Mac OSType = "Mac"
29 All OSType = "All"
30)
31
32type ParameterizedText string
33
34type File struct {
35 Template ParameterizedText `yaml:"template,omitempty"`
36}
37
38type FileInFolder struct {
39 File `yaml:"file,omitempty"`
40 Name ParameterizedText
41}
42
43type Profile struct {
44 Name string
45 Description string `yaml:"description,omitempty"`
46 IsDefault bool
47 IsDisabled bool
48 Type ProfileType
49 Path string `yaml:"path,omitempty"`
50 OS OSType `yaml:"os,omitempty"`
51 NamePatterns []ParameterizedText
52 Files []FileInFolder `yaml:"files,omitempty"`
53 RunPostscriptOnFailure bool `yaml:"runpostscriptonfailure,omitempty"`
54 PostScripts []ParameterizedText `yaml:"postscripts,omitempty"`
55 File `yaml:"file,omitempty"`
56}
57
58var forbiddenProfileNames = []string{
59 "list",
60 "get",
61}
62
63func processProfile(profile Profile, projectName string, extras map[string]string, usr user.User) []error {
64 if profile.IsDisabled {
65 return []error{fmt.Errorf("Profile is disabled, doing nothing")}
66 }
67
68 // decide if the profile should run on this OS
69 profileOs := profile.OS
70 isWindows := runtime.GOOS == "windows"
71 shouldRun := profileOs == "" ||
72 profileOs == "All" ||
73 (profileOs == "Windows" && isWindows) ||
74 (profileOs == "Mac" && !isWindows)
75 if !shouldRun {
76 return []error{
77 fmt.Errorf("This profile is not supported on this OS, doing nothing. Profile OS: %s, system OS: %s. Supported OS values: Windows, Mac, All", profileOs, runtime.GOOS),
78 }
79 }
80
81 cwd, err := monkey.OSGetwd()
82 if err != nil {
83 return []error{fmt.Errorf("unable to get current working directory: %v", err)}
84 }
85 if profile.Path != "" {
86 // change cwd to the profile's path, if one is specified
87 path := getFullPath(profile.Path, usr)
88 err = monkey.OSChdir(path)
89 if err != nil {
90 return []error{fmt.Errorf("unable to change directory (1) to %v: %v", path, err)}
91 }
92 cwd = path
93 }
94
95 // determine if we're dealing with files or directories
96 profileType := profile.Type
97
98 errors := []error{}
99 fullPath := ""
100 now := time.Now()
101 prePatternData := createPrePatternData(now, projectName, extras)
102 if profileType == FileType || profileType == FolderType {
103 // get all entities in current directory
104 files, err := monkey.OSReadDir(cwd)
105 if err != nil {
106 return []error{err}
107 }
108
109 // filter entities to just files or directories
110 var entities []string
111 for _, file := range files {
112 isDir := file.IsDir()
113 if isDir == (profileType == FolderType) {
114 entities = append(entities, file.Name())
115 }
116 }
117
118 // generate a new name based on time, profile, and existing entities
119 validator := func(newName string) bool { return !slices.Contains(entities, newName) }
120 newName, err := createNewNameForPatterns(now, prePatternData, projectName, profile.NamePatterns, extras, validator)
121 swallowPrePostScriptErrors := profile.RunPostscriptOnFailure && profile.PostScripts != nil && len(profile.PostScripts) > 0
122
123 // check newName is created
124 if newName == "" {
125 if swallowPrePostScriptErrors {
126 fmt.Println("No new name was created, but will attempt to run post scripts")
127 } else {
128 errors = append(errors, fmt.Errorf("couldn't find a good new name"))
129 return errors
130 }
131 }
132
133 if err == nil && newName != "" {
134 fullPath, err = createEntity(now, profile, cwd, newName, projectName, extras, prePatternData)
135 if err != nil {
136 if swallowPrePostScriptErrors {
137 fmt.Printf("Error creating entity, but will attempt to run post scripts: %v", err)
138 } else {
139 errors = append(errors, fmt.Errorf("unable to create entity: %v", err))
140 return errors
141 }
142 }
143 }
144 }
145
146 // execute PostScript, if present
147 if profile.PostScripts != nil && len(profile.PostScripts) > 0 {
148 postPatternData := createPostPatternData(fullPath, prePatternData)
149 postScripts, err := createPostScriptArrays(profile, postPatternData)
150 if err != nil {
151 errors = append(errors, fmt.Errorf("unable to create post scripts: %v", err))
152 return errors
153 }
154 for _, postScript := range postScripts {
155
156 shell, args := shellCommandAndArgs(postScript)
157 var out bytes.Buffer
158 fmt.Printf("Executing post script: %v %v\n", shell, strings.Join(args, " "))
159 cmd := monkey.ExecCommand(shell, args...)
160 cmd.Stdout = &out
161 err := cmd.Run()
162 output := out.String()
163 if output != "" {
164 fmt.Printf("Post script output: %v\n", output)
165 } else {
166 fmt.Printf("Post script had no output\n")
167 }
168 if err != nil {
169 // post scripts can fail (TODO: investigate why), so for now we'll just log the error
170
171 fmt.Printf("Error executing post script '%v': %v\n",
172 fmt.Sprintf("%v %v", shell, strings.Trim(fmt.Sprint(args), "[]")), err)
173 }
174 }
175 }
176
177 return nil
178}
179
180func shellCommandAndArgs(command string) (shell string, args []string) {
181 if runtime.GOOS == "windows" {
182 shell = "cmd"
183 args = []string{"/c", command}
184 return
185 } else {
186 shell = "sh"
187 args = []string{"-c", command}
188 return
189 }
190}
191
192func createPrePatternData(now time.Time, projectName string, extras map[string]string) PrePatternData {
193 prePatternData := PrePatternData{
194 Year: now.Format("2006"),
195 Month: now.Format("01"),
196 Day: now.Format("02"),
197 Hour: now.Format("15"),
198 Minute: now.Format("04"),
199 Second: now.Format("05"),
200 ProjectName: projectName,
201 Extras: extras,
202 }
203 return prePatternData
204}
205
206func createEntity(
207 now time.Time,
208 profile Profile,
209 cwd string,
210 newName string,
211 projectName string,
212 extras map[string]string,
213 prePatternData PrePatternData) (string, error) {
214
215 profileType := profile.Type
216 fullPath := filepath.Join(cwd, newName)
217 if profileType == FolderType {
218 fmt.Printf("Creating folder: %v\n", fullPath)
219 err := monkey.OSMkdir(fullPath, os.ModePerm)
220 if err != nil {
221 return "", fmt.Errorf("error creating directory: %v", err)
222 }
223 err = monkey.OSChdir(fullPath)
224 if err != nil {
225 return "", fmt.Errorf("unable to change directory (2) to %v: %v", fullPath, err)
226 }
227
228 // create files in the new folder
229 if len(profile.Files) > 0 {
230 fmt.Printf("Creating %v files in folder: %v\n", len(profile.Files), fullPath)
231 existingFiles := []string{}
232 for index, file := range profile.Files {
233 filePrePatternData := createPrePatternData(now, projectName, extras)
234 validator := func(newName string) bool { return !slices.Contains(existingFiles, newName) }
235 fileName, err := createNewNameForPatterns(now, filePrePatternData, projectName, []ParameterizedText{file.Name}, extras, validator)
236 if err != nil {
237 return "", fmt.Errorf("error creating file name for file index %v : %v", index, err)
238 }
239 existingFiles = append(existingFiles, fileName)
240 subFilePath := filepath.Join(fullPath, fileName)
241 subDirPath := filepath.Dir(subFilePath)
242
243 if _, err := os.Stat(subDirPath); os.IsNotExist(err) {
244 fmt.Printf("Creating subdirectory: %v\n", subDirPath)
245 err = monkey.OSMkdir(subDirPath, os.ModePerm)
246 if err != nil {
247 return "", fmt.Errorf("error creating sub directory for file index %v : %v", index, err)
248 }
249 }
250 fmt.Printf("Creating sub file: %v\n", subFilePath)
251 err = createFile(filePrePatternData, file.File, subFilePath)
252 if err != nil {
253 return "", fmt.Errorf("error creating file %v: %v", subFilePath, err)
254 }
255 }
256 }
257 } else if profileType == FileType {
258 fmt.Printf("Creating file: %v\n", fullPath)
259 err := createFile(prePatternData, profile.File, fullPath)
260 if err != nil {
261 return "", err
262 }
263 } else {
264 fmt.Printf("Profile type is script, doing nothing during main phase\n")
265 }
266 return fullPath, nil
267}
268
269func validateProfile(profile *Profile) error {
270 if profile.Name == "" {
271 return fmt.Errorf("Profile name is required")
272 }
273 if slices.Contains(forbiddenProfileNames, profile.Name) {
274 return fmt.Errorf("forbidden profile name '%s'", profile.Name)
275 }
276 if profile.NamePatterns == nil || len(profile.NamePatterns) == 0 {
277 return fmt.Errorf("namePatterns is required")
278 }
279 for index, namePattern := range profile.NamePatterns {
280 if namePattern == "" {
281 return fmt.Errorf("name pattern is required for pattern index %v of profile %v", index, profile.Name)
282 }
283 }
284 if profile.Type == "" || (profile.Type != FileType && profile.Type != FolderType && profile.Type != ScriptType) {
285 return fmt.Errorf("invalid profile type %s for profile %s", profile.Type, profile.Name)
286 }
287 if profile.Type != FileType && profile.Template != "" {
288 return fmt.Errorf("file template is not supported for profile %s, which has Type=%s", profile.Name, profile.Type)
289 }
290 if profile.Type == FileType && profile.Path == "" {
291 return fmt.Errorf("path is not specified for profile %s, which has Type=%s", profile.Name, profile.Type)
292 }
293 if profile.Type == FolderType && profile.Path == "" {
294 return fmt.Errorf("path is required for profile %s, which has Type=%s", profile.Name, profile.Type)
295 }
296 if profile.Type == ScriptType && profile.NamePatterns != nil && len(profile.NamePatterns) > 1 {
297 return fmt.Errorf("multiple name patterns are not supported for profile %s, which has Type=%s", profile.Name, profile.Type)
298 }
299 if profile.Files == nil {
300 profile.Files = []FileInFolder{}
301 }
302 return nil
303}