1 package util
2
3 import (
4 "archive/zip"
5 "bufio"
6 "bytes"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "io"
11 "io/fs"
12 "net"
13 "net/url"
14 "os"
15 "os/signal"
16 "path"
17 "path/filepath"
18 "regexp"
19 "runtime"
20 "sort"
21 "strings"
22 "syscall"
23 "time"
24
25 "github.com/fatih/color"
26 "github.com/go-git/go-git/v5"
27 gitignore "github.com/sabhiram/go-gitignore"
28
29 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
30 devfilefs "github.com/devfile/library/v2/pkg/testingutil/filesystem"
31 dfutil "github.com/devfile/library/v2/pkg/util"
32
33 "github.com/redhat-developer/odo/pkg/testingutil/filesystem"
34
35 "k8s.io/klog"
36 )
37
38 var httpCacheDir = filepath.Join(os.TempDir(), "odohttpcache")
39
40
41
42 var customHomeDir = os.Getenv("CUSTOM_HOMEDIR")
43
44
45
46
47
48
49
50 func ConvertLabelsToSelector(labels map[string]string) string {
51 var selector string
52 isFirst := true
53 keys := make([]string, 0, len(labels))
54 for k := range labels {
55 keys = append(keys, k)
56 }
57 sort.Strings(keys)
58 for _, k := range keys {
59 v := labels[k]
60 if isFirst {
61 isFirst = false
62 if v == "" {
63 selector = selector + fmt.Sprintf("%v", k)
64 } else {
65 if strings.HasPrefix(v, "!") {
66 v = strings.Replace(v, "!", "", -1)
67 selector = fmt.Sprintf("%v!=%v", k, v)
68 } else {
69 selector = fmt.Sprintf("%v=%v", k, v)
70 }
71 }
72 } else {
73 if v == "" {
74 selector = selector + fmt.Sprintf(",%v", k)
75 } else {
76 if strings.HasPrefix(v, "!") {
77 v = strings.Replace(v, "!", "", -1)
78 selector = selector + fmt.Sprintf(",%v!=%v", k, v)
79 } else {
80 selector = selector + fmt.Sprintf(",%v=%v", k, v)
81 }
82 }
83 }
84 }
85 return selector
86 }
87
88
89 func NamespaceKubernetesObject(componentName string, applicationName string) (string, error) {
90
91
92 if componentName == "" {
93 return "", errors.New("namespacing: component name cannot be blank")
94 }
95
96
97 if applicationName == "" {
98 return "", errors.New("namespacing: application name cannot be blank")
99 }
100
101
102 return fmt.Sprintf("%s-%s", strings.Replace(componentName, "/", "-", -1), applicationName), nil
103 }
104
105
106
107
108 func NamespaceKubernetesObjectWithTrim(componentName, applicationName string, maxLen int) (string, error) {
109 value, err := NamespaceKubernetesObject(componentName, applicationName)
110 if err != nil {
111 return "", err
112 }
113
114
115 if len(value) <= maxLen {
116 return value, nil
117 }
118
119
120 appNameLen := len(applicationName)
121 if appNameLen > maxLen/2 {
122 appNameLen = maxLen / 2
123 }
124 applicationName = applicationName[:appNameLen]
125
126
127 minComponentLen := len(componentName)
128 if minComponentLen > maxLen-appNameLen-1 {
129 minComponentLen = maxLen - appNameLen - 1
130 }
131 componentName = componentName[:minComponentLen]
132
133 value, err = NamespaceKubernetesObject(componentName, applicationName)
134 if err != nil {
135 return "", err
136 }
137 return value, nil
138 }
139
140
141
142
143
144
145 func TruncateString(str string, maxLen int, appendIfTrunicated ...string) string {
146 if maxLen == -1 {
147 return str
148 }
149 if len(str) > maxLen {
150 truncatedString := str[:maxLen]
151 for _, item := range appendIfTrunicated {
152 truncatedString = fmt.Sprintf("%s%s", truncatedString, item)
153 }
154 return truncatedString
155 }
156 return str
157 }
158
159
160
161 func GetDNS1123Name(str string) string {
162 nonAllowedCharsRegex := regexp.MustCompile(`[^a-zA-Z0-9_-]+`)
163 withReplacedChars := strings.Replace(
164 nonAllowedCharsRegex.ReplaceAllString(str, "-"),
165 "--", "-", -1)
166 name := strings.ToLower(removeNonAlphaSuffix(removeNonAlphaPrefix(withReplacedChars)))
167
168 if len(str) != 0 && len(name) == 0 {
169 name = strings.ToLower(removeNonAlphaSuffix(removeNonAlphaPrefix("x" + withReplacedChars)))
170 }
171 return name
172 }
173
174 func removeNonAlphaPrefix(input string) string {
175 regex := regexp.MustCompile("^[^a-zA-Z]+(.*)$")
176 return regex.ReplaceAllString(input, "$1")
177 }
178
179 func removeNonAlphaSuffix(input string) string {
180 suffixRegex := regexp.MustCompile("^(.*?)[^a-zA-Z0-9]+$")
181 matches := suffixRegex.FindStringSubmatch(input)
182 matchesLength := len(matches)
183 if matchesLength == 0 {
184
185 return input
186 }
187
188 return matches[matchesLength-1]
189 }
190
191
192
193 func CheckPathExists(fsys filesystem.Filesystem, path string) bool {
194 if _, err := fsys.Stat(path); !errors.Is(err, fs.ErrNotExist) {
195
196 return true
197 }
198 klog.V(4).Infof("path %s doesn't exist, skipping it", path)
199 return false
200 }
201
202
203
204
205 func IsValidProjectDir(path string, devfilePath string, fs filesystem.Filesystem) error {
206 entries, err := fs.ReadDir(path)
207 if err != nil {
208 return err
209 }
210 if len(entries) >= 1 {
211 for _, entry := range entries {
212 fileName := entry.Name()
213 devfilePath = strings.TrimPrefix(devfilePath, "./")
214 if !entry.IsDir() && fileName == devfilePath {
215 return nil
216 }
217 }
218 return fmt.Errorf("folder %s doesn't contain the devfile used", path)
219 }
220
221 return nil
222 }
223
224
225
226
227
228 func GetAndExtractZip(zipURL string, destination string, pathToUnzip string, starterToken string, fsys filesystem.Filesystem) error {
229 if zipURL == "" {
230 return fmt.Errorf("empty zip url: %s", zipURL)
231 }
232
233 var pathToZip string
234 if strings.HasPrefix(zipURL, "file://") {
235 pathToZip = strings.TrimPrefix(zipURL, "file:/")
236 if runtime.GOOS == "windows" {
237 pathToZip = strings.Replace(pathToZip, "\\", "/", -1)
238 }
239 } else if strings.HasPrefix(zipURL, "http://") || strings.HasPrefix(zipURL, "https://") {
240
241 time := time.Now().Format(time.RFC3339)
242 time = strings.Replace(time, ":", "-", -1)
243 tempPath, err := fsys.TempDir("", "")
244 if err != nil {
245 return err
246 }
247 pathToZip = path.Join(tempPath, "_"+time+".zip")
248
249 params := dfutil.DownloadParams{
250 Request: dfutil.HTTPRequestParams{
251 URL: zipURL,
252 Token: starterToken,
253 },
254 Filepath: pathToZip,
255 }
256 err = dfutil.DownloadFile(params)
257 if err != nil {
258 return err
259 }
260
261 defer func() {
262 if err = fsys.Remove(pathToZip); err != nil && !errors.Is(err, fs.ErrNotExist) {
263 klog.Errorf("Could not delete temporary directory for zip file. Error: %s", err)
264 }
265 }()
266 } else {
267 return fmt.Errorf("invalid Zip URL: %s . Should either be prefixed with file://, http:// or https://", zipURL)
268 }
269
270 filenames, err := Unzip(pathToZip, destination, pathToUnzip, fsys)
271 if err != nil {
272 return err
273 }
274
275 if len(filenames) == 0 {
276 return errors.New("no files were unzipped, ensure that the project repo is not empty or that subDir has a valid path")
277 }
278
279 return nil
280 }
281
282
283
284
285
286
287 func Unzip(src, dest, pathToUnzip string, fsys filesystem.Filesystem) ([]string, error) {
288 var filenames []string
289
290 r, err := zip.OpenReader(src)
291 if err != nil {
292 return filenames, err
293 }
294 defer r.Close()
295
296
297 pathToUnzip = filepath.FromSlash(pathToUnzip)
298
299
300 pathToUnzip = strings.TrimPrefix(pathToUnzip, string(os.PathSeparator))
301
302 for _, f := range r.File {
303
304 index := strings.Index(f.Name, "/")
305 filename := filepath.FromSlash(f.Name[index+1:])
306 if filename == "" {
307 continue
308 }
309
310
311 match, err := filepath.Match(pathToUnzip, filename)
312 if err != nil {
313 return filenames, err
314 }
315
316
317 fpath := filepath.Join(dest, filename)
318
319
320 fpathDir := filepath.Dir(fpath)
321
322
323 if strings.HasPrefix(filename, pathToUnzip) {
324 filename = strings.TrimPrefix(filename, pathToUnzip)
325 } else if !strings.HasPrefix(filename, pathToUnzip) && !match && !sliceContainsString(fpathDir, filenames) {
326 continue
327 }
328
329 if (len(filename) == 1 && os.IsPathSeparator(filename[0])) || filename == "" {
330 fpath = dest + string(os.PathSeparator)
331 } else {
332 fpath = filepath.Join(dest, filename)
333 }
334
335 if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
336 return filenames, fmt.Errorf("%s: illegal file path", fpath)
337 }
338
339 filenames = append(filenames, fpath)
340
341 if f.FileInfo().IsDir() {
342
343 if err = fsys.MkdirAll(fpath, os.ModePerm); err != nil {
344 return filenames, err
345 }
346 continue
347 }
348
349
350 if err = fsys.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
351 return filenames, err
352 }
353
354 outFile, err := fsys.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
355 if err != nil {
356 return filenames, err
357 }
358
359 rc, err := f.Open()
360 if err != nil {
361 return filenames, err
362 }
363
364
365
366
367 limited := io.LimitReader(rc, 100*1024*1024)
368
369 _, err = io.Copy(outFile, limited)
370
371
372 outFile.Close()
373 rc.Close()
374
375 if err != nil {
376 return filenames, err
377 }
378 }
379 return filenames, nil
380 }
381
382
383
384 func DownloadFileInMemory(params dfutil.HTTPRequestParams) ([]byte, error) {
385 data, err := dfutil.HTTPGetRequest(params, 0)
386
387 if err != nil {
388 return nil, err
389 }
390
391 return data, nil
392 }
393
394
395 func DownloadFileInMemoryWithCache(params dfutil.HTTPRequestParams, cacheFor int) ([]byte, error) {
396 data, err := dfutil.HTTPGetRequest(params, cacheFor)
397
398 if err != nil {
399 return nil, err
400 }
401
402 return data, nil
403 }
404
405
406
407 func ValidateURL(sourceURL string) error {
408
409
410
411 url, err := url.ParseRequestURI(sourceURL)
412 if err != nil {
413 return err
414 }
415 host := url.Host
416
417 re := regexp.MustCompile(`[\/\?#\[\]@]`)
418 if host == "" || re.MatchString(host) {
419 return errors.New("URL is invalid")
420 }
421
422 return nil
423 }
424
425
426
427 func GetDataFromURI(uri, componentContext string, fs devfilefs.Filesystem) (string, error) {
428 parsedURL, err := url.Parse(uri)
429 if err != nil {
430 return "", err
431 }
432 if len(parsedURL.Host) != 0 && len(parsedURL.Scheme) != 0 {
433 params := dfutil.HTTPRequestParams{
434 URL: uri,
435 }
436 dataBytes, err := DownloadFileInMemoryWithCache(params, 1)
437 if err != nil {
438 return "", err
439 }
440 return string(dataBytes), nil
441 } else {
442 dataBytes, err := fs.ReadFile(filepath.Join(componentContext, uri))
443 if err != nil {
444 return "", err
445 }
446 return string(dataBytes), nil
447 }
448 }
449
450
451 func sliceContainsString(str string, slice []string) bool {
452 for _, b := range slice {
453 if b == str {
454 return true
455 }
456 }
457 return false
458 }
459
460 func addFileToIgnoreFile(gitIgnoreFile, filename string, fs filesystem.Filesystem) error {
461 filename = filepath.ToSlash(filename)
462
463 readFile, err := fs.Open(gitIgnoreFile)
464 if err != nil {
465 return err
466 }
467 defer readFile.Close()
468 fileScanner := bufio.NewScanner(readFile)
469 fileScanner.Split(bufio.ScanLines)
470 var fileLines []string
471 for fileScanner.Scan() {
472 fileLines = append(fileLines, fileScanner.Text())
473 }
474
475
476 ignoreMatcher := gitignore.CompileIgnoreLines(fileLines...)
477 if !ignoreMatcher.MatchesPath(filename) {
478 file, err := fs.OpenFile(gitIgnoreFile, os.O_APPEND|os.O_RDWR, dfutil.ModeReadWriteFile)
479 if err != nil {
480 return fmt.Errorf("failed to open .gitignore file: %w", err)
481 }
482 defer file.Close()
483 if _, err := file.WriteString("\n" + filename); err != nil {
484 return fmt.Errorf("failed to add %v to %v file: %w", filepath.Base(filename), gitIgnoreFile, err)
485 }
486 }
487 return nil
488 }
489
490
491
492
493 func DisplayLog(followLog bool, rd io.ReadCloser, writer io.Writer, compName string, numberOfLastLines int) (err error) {
494
495 defer func() {
496 cErr := rd.Close()
497 if err == nil {
498 err = cErr
499 }
500 }()
501
502
503 color.Set(color.FgYellow)
504 defer color.Unset()
505
506
507
508 if followLog {
509
510 c := make(chan os.Signal, 1)
511 signal.Notify(c, os.Interrupt, syscall.SIGTERM)
512 go func() {
513 <-c
514 color.Unset()
515 os.Exit(1)
516 }()
517
518 if _, err = io.Copy(writer, rd); err != nil {
519 return fmt.Errorf("error followLoging logs for %s: %w", compName, err)
520 }
521
522 } else if numberOfLastLines == -1 {
523
524 buf := new(bytes.Buffer)
525 _, err = io.Copy(buf, rd)
526 if err != nil {
527 return fmt.Errorf("unable to copy followLog to buffer: %w", err)
528 }
529
530
531 if _, err = io.Copy(writer, buf); err != nil {
532 return fmt.Errorf("error copying logs to stdout: %w", err)
533 }
534 } else {
535 reader := bufio.NewReader(rd)
536 var lines []string
537 var line string
538 for {
539 line, err = reader.ReadString('\n')
540 if err != nil {
541 if err != io.EOF {
542 return err
543 }
544 err = nil
545 break
546 }
547
548 lines = append(lines, line)
549 }
550
551 index := len(lines) - numberOfLastLines
552 if index < 0 {
553 index = 0
554 }
555
556 for i := index; i < len(lines); i++ {
557 _, err = fmt.Fprintf(writer, lines[i])
558 if err != nil {
559 return err
560 }
561 }
562 }
563 return err
564
565 }
566
567
568 func copyFileWithFs(src, dst string, fs filesystem.Filesystem) error {
569 var err error
570 var srcinfo os.FileInfo
571
572 srcfd, err := fs.Open(src)
573 if err != nil {
574 return err
575 }
576 defer func() {
577 if e := srcfd.Close(); e != nil {
578 klog.V(4).Infof("err occurred while closing file: %v", e)
579 }
580 }()
581
582 dstfd, err := fs.Create(dst)
583 if err != nil {
584 return err
585 }
586 defer func() {
587 if e := dstfd.Close(); e != nil {
588 klog.V(4).Infof("err occurred while closing file: %v", e)
589 }
590 }()
591
592 if _, err = io.Copy(dstfd, srcfd); err != nil {
593 return err
594 }
595 if srcinfo, err = fs.Stat(src); err != nil {
596 return err
597 }
598 return fs.Chmod(dst, srcinfo.Mode())
599 }
600
601
602 func CopyDirWithFS(src string, dst string, fs filesystem.Filesystem) error {
603 var err error
604 var fds []os.FileInfo
605 var srcinfo os.FileInfo
606
607 if srcinfo, err = fs.Stat(src); err != nil {
608 return err
609 }
610
611 if err = fs.MkdirAll(dst, srcinfo.Mode()); err != nil {
612 return err
613 }
614
615 if fds, err = fs.ReadDir(src); err != nil {
616 return err
617 }
618 for _, fd := range fds {
619 srcfp := path.Join(src, fd.Name())
620 dstfp := path.Join(dst, fd.Name())
621
622 if fd.IsDir() {
623 if err = CopyDirWithFS(srcfp, dstfp, fs); err != nil {
624 return err
625 }
626 } else {
627 if err = copyFileWithFs(srcfp, dstfp, fs); err != nil {
628 return err
629 }
630 }
631 }
632 return nil
633 }
634
635
636 func StartSignalWatcher(watchSignals []os.Signal, handle func(receivedSignal os.Signal)) {
637 signals := make(chan os.Signal, 1)
638 signal.Notify(signals, watchSignals...)
639 defer signal.Stop(signals)
640
641 receivedSignal := <-signals
642 handle(receivedSignal)
643 }
644
645
646
647 func cleanDir(originalPath string, leaveBehindFiles map[string]bool, fs filesystem.Filesystem) error {
648
649 outputDirRead, err := fs.Open(originalPath)
650 if err != nil {
651 return err
652 }
653
654
655 outputDirFiles, err := outputDirRead.Readdir(0)
656 if err != nil {
657 return err
658 }
659
660
661 for _, file := range outputDirFiles {
662 if value, ok := leaveBehindFiles[file.Name()]; ok && value {
663 continue
664 }
665 err = fs.RemoveAll(filepath.Join(originalPath, file.Name()))
666 if err != nil {
667 return err
668 }
669 }
670 return err
671 }
672
673
674 func GitSubDir(srcPath, destinationPath, subDir string) error {
675 return gitSubDir(srcPath, destinationPath, subDir, filesystem.DefaultFs{})
676 }
677
678
679 func gitSubDir(srcPath, destinationPath, subDir string, fs filesystem.Filesystem) error {
680 go StartSignalWatcher([]os.Signal{syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, os.Interrupt}, func(_ os.Signal) {
681 err := cleanDir(destinationPath, map[string]bool{
682 "devfile.yaml": true,
683 }, fs)
684 if err != nil {
685 klog.V(4).Infof("error %v occurred while calling handleInterruptedSubDir", err)
686 }
687 err = fs.RemoveAll(srcPath)
688 if err != nil {
689 klog.V(4).Infof("error %v occurred during temp folder clean up", err)
690 }
691 })
692
693 err := func() error {
694
695 outputDirRead, err := fs.Open(filepath.Join(srcPath, subDir))
696 if err != nil {
697 return err
698 }
699 defer func() {
700 if err1 := outputDirRead.Close(); err1 != nil {
701 klog.V(4).Infof("err occurred while closing temp dir: %v", err1)
702
703 }
704 }()
705
706 outputDirFiles, err := outputDirRead.Readdir(0)
707 if err != nil {
708 return err
709 }
710
711
712 for outputIndex := range outputDirFiles {
713 outputFileHere := outputDirFiles[outputIndex]
714
715
716 fileName := outputFileHere.Name()
717
718 oldPath := filepath.Join(srcPath, subDir, fileName)
719
720 if outputFileHere.IsDir() {
721 err = CopyDirWithFS(oldPath, filepath.Join(destinationPath, fileName), fs)
722 } else {
723 err = copyFileWithFs(oldPath, filepath.Join(destinationPath, fileName), fs)
724 }
725
726 if err != nil {
727 return err
728 }
729 }
730 return nil
731 }()
732 if err != nil {
733 return err
734 }
735 return fs.RemoveAll(srcPath)
736 }
737
738
739 func GetCommandStringFromEnvs(envVars []v1alpha2.EnvVar) string {
740 var setEnvVariable string
741 for i, envVar := range envVars {
742 if i == 0 {
743 setEnvVariable = "export"
744 }
745 setEnvVariable = setEnvVariable + fmt.Sprintf(" %v=\"%v\"", envVar.Name, envVar.Value)
746 }
747 return setEnvVariable
748 }
749
750
751
752 func GetGitOriginPath(path string) string {
753 open, err := git.PlainOpen(path)
754 if err != nil {
755 return ""
756 }
757
758 remotes, err := open.Remotes()
759 if err != nil {
760 return ""
761 }
762
763 for _, remote := range remotes {
764 if remote.Config().Name == "origin" {
765 if len(remote.Config().URLs) > 0 {
766
767
768 return remote.Config().URLs[0]
769 }
770 }
771 }
772 return ""
773 }
774
775
776 func GetBool(b bool) *bool {
777 return &b
778 }
779
780
781 func SafeGetBool(b *bool) bool {
782 if b == nil {
783 return false
784 }
785 return *b
786 }
787
788
789 func IsPortFree(port int, localAddress string) bool {
790 if localAddress == "" {
791 localAddress = "127.0.0.1"
792 }
793 address := fmt.Sprintf("%s:%d", localAddress, port)
794 listener, err := net.Listen("tcp", address)
795 if err != nil {
796 return false
797 }
798 err = listener.Close()
799 return err == nil
800 }
801
802
803
804
805 func NextFreePort(start, end int, usedPorts []int, address string) (int, error) {
806
807 port := start
808 for {
809 for _, usedPort := range usedPorts {
810 if usedPort == port {
811 return port, nil
812 }
813 }
814 if IsPortFree(port, address) {
815 return port, nil
816 }
817 port++
818 if port > end {
819 return 0, fmt.Errorf("no free port in range [%d-%d]", start, end)
820 }
821 }
822 }
823
824
825 func WriteToJSONFile(c interface{}, filename string) error {
826 data, err := json.Marshal(c)
827 if err != nil {
828 return fmt.Errorf("unable to marshal data: %w", err)
829 }
830
831 if err = CreateIfNotExists(filename); err != nil {
832 return err
833 }
834 err = os.WriteFile(filename, data, 0600)
835 if err != nil {
836 return fmt.Errorf("unable to write data to file %v: %w", c, err)
837 }
838
839 return nil
840 }
841
View as plain text