1
16
17 package util
18
19 import (
20 "bufio"
21 "errors"
22 "fmt"
23 "io"
24 "os"
25 "os/signal"
26 "path/filepath"
27 "regexp"
28 "strings"
29 "syscall"
30
31 "github.com/blang/semver/v4"
32 "github.com/sirupsen/logrus"
33
34 "sigs.k8s.io/release-utils/command"
35 )
36
37 const (
38 TagPrefix = "v"
39 )
40
41 var (
42 regexpCRLF = regexp.MustCompile(`\015$`)
43 regexpCtrlChar = regexp.MustCompile(`\x1B[\[(](\d{1,2}(;\d{1,2})?)?[mKB]`)
44 regexpOauthToken = regexp.MustCompile(`[a-f0-9]{40}:x-oauth-basic`)
45 regexpGitToken = regexp.MustCompile(`git:[a-f0-9]{35,40}@github\.com`)
46 )
47
48
49 type UserInputError struct {
50 ErrorString string
51 isCtrlC bool
52 }
53
54
55 func (e UserInputError) Error() string {
56 return e.ErrorString
57 }
58
59
60 func (e UserInputError) IsCtrlC() bool {
61 return e.isCtrlC
62 }
63
64
65 func NewUserInputError(message string, ctrlC bool) UserInputError {
66 return UserInputError{
67 ErrorString: message,
68 isCtrlC: ctrlC,
69 }
70 }
71
72
73
74 func PackagesAvailable(packages ...string) (bool, error) {
75 type packageVerifier struct {
76 cmd string
77 args []string
78 }
79 type packageChecker struct {
80 manager string
81 verifier *packageVerifier
82 }
83 var checker *packageChecker
84
85 for _, x := range []struct {
86 possiblePackageManagers []string
87 verifierCmd string
88 verifierArgs []string
89 }{
90 {
91 []string{"apt"},
92 "dpkg",
93 []string{"-l"},
94 },
95 {
96 []string{"dnf", "yum", "zypper"},
97 "rpm",
98 []string{"--quiet", "-q"},
99 },
100 {
101 []string{"yay", "pacaur", "pacman"},
102 "pacman",
103 []string{"-Qs"},
104 },
105 } {
106
107 if !command.Available(x.verifierCmd) {
108 logrus.Debugf("Skipping not available package verifier %s",
109 x.verifierCmd)
110 continue
111 }
112
113
114 packageManager := ""
115 for _, mgr := range x.possiblePackageManagers {
116 if command.Available(mgr) {
117 packageManager = mgr
118 break
119 }
120 logrus.Debugf("Skipping not available package manager %s", mgr)
121 }
122 if packageManager == "" {
123 return false, fmt.Errorf(
124 "unable to find working package manager for verifier `%s`",
125 x.verifierCmd,
126 )
127 }
128
129 checker = &packageChecker{
130 manager: packageManager,
131 verifier: &packageVerifier{x.verifierCmd, x.verifierArgs},
132 }
133 break
134 }
135 if checker == nil {
136 return false, errors.New("unable to find working package manager")
137 }
138 logrus.Infof("Assuming %q as package manager", checker.manager)
139
140 missingPkgs := []string{}
141 for _, pkg := range packages {
142 logrus.Infof("Checking if %q has been installed", pkg)
143
144 args := checker.verifier.args
145 args = append(args, pkg)
146 if err := command.New(checker.verifier.cmd, args...).
147 RunSilentSuccess(); err != nil {
148 logrus.Infof("Adding %s to missing packages", pkg)
149 missingPkgs = append(missingPkgs, pkg)
150 }
151 }
152
153 if len(missingPkgs) > 0 {
154 logrus.Warnf("The following packages are not installed via %s: %s",
155 checker.manager, strings.Join(missingPkgs, ", "))
156
157
158
159 logrus.Infof("Install them with: sudo %s install %s",
160 checker.manager, strings.Join(missingPkgs, " "))
161 return false, nil
162 }
163
164 return true, nil
165 }
166
167
201
202
203
204
205
206
207
208
209
210
211
212 func readInput(question string) (string, error) {
213 fmt.Print(question)
214
215
216 inputChannel := make(chan string, 1)
217 signalChannel := make(chan os.Signal, 1)
218 signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM)
219 defer func() {
220 signal.Stop(signalChannel)
221 close(signalChannel)
222 }()
223 go func() {
224 scanner := bufio.NewScanner(os.Stdin)
225 scanner.Scan()
226 response := scanner.Text()
227 inputChannel <- response
228 close(inputChannel)
229 }()
230
231 select {
232 case <-signalChannel:
233 return "", NewUserInputError("Input canceled", true)
234 case response := <-inputChannel:
235 return response, nil
236 }
237 }
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260 func Ask(question, expectedResponse string, retries int) (answer string, success bool, err error) {
261 attempts := 1
262
263 if retries < 0 {
264 fmt.Printf("Retries was set to a number less than zero (%d). Please specify a positive number of retries or zero, if you want to ask unconditionally.\n", retries)
265 }
266
267 const (
268 partsSeparator string = "|"
269 optsSeparator string = ":"
270 )
271
272 successAnswers := make([]string, 0)
273 nonSuccessAnswers := make([]string, 0)
274 defaultAnswer := ""
275
276
277 if strings.Contains(expectedResponse, partsSeparator) {
278 parts := strings.Split(expectedResponse, partsSeparator)
279 if len(parts) > 3 {
280 return "", false, errors.New("answer spec malformed")
281 }
282
283 if strings.Contains(expectedResponse, parts[0]) {
284 successAnswers = strings.Split(parts[0], optsSeparator)
285 }
286
287 if len(parts) >= 2 {
288 if strings.Contains(parts[1], optsSeparator) {
289 nonSuccessAnswers = strings.Split(parts[1], optsSeparator)
290 } else {
291 nonSuccessAnswers = append(nonSuccessAnswers, parts[1])
292 }
293 }
294
295 if len(parts) == 3 {
296 defaultAnswer = parts[2]
297 }
298 }
299
300 for attempts <= retries {
301
302 answer, err = readInput(fmt.Sprintf("%s (%d/%d) \n", question, attempts, retries))
303 if err != nil {
304 return answer, false, err
305 }
306
307
308 if len(successAnswers) > 0 {
309
310 for _, testResponse := range successAnswers {
311 if answer == testResponse {
312 return answer, true, nil
313 }
314 }
315
316
317 for _, testResponse := range nonSuccessAnswers {
318 if answer == testResponse {
319 return answer, false, nil
320 }
321
322
323 if answer == "" && defaultAnswer == testResponse {
324 return defaultAnswer, false, nil
325 }
326 }
327 } else if answer == expectedResponse {
328 return answer, true, nil
329 }
330
331 if answer == "" && defaultAnswer != "" {
332 return defaultAnswer, true, nil
333 }
334
335 fmt.Printf("Expected '%s', but got '%s'\n", expectedResponse, answer)
336
337 attempts++
338 }
339
340 return answer, false, NewUserInputError("expected response was not input. Retries exceeded", false)
341 }
342
343
344
345
346 func MoreRecent(a, b string) (bool, error) {
347 fileA, errA := os.Stat(a)
348 if errA != nil && !os.IsNotExist(errA) {
349 return false, errA
350 }
351
352 fileB, errB := os.Stat(b)
353 if errB != nil && !os.IsNotExist(errB) {
354 return false, errB
355 }
356
357 switch {
358 case os.IsNotExist(errA) && os.IsNotExist(errB):
359 return false, errors.New("neither file exists")
360 case os.IsNotExist(errA):
361 return false, nil
362 case os.IsNotExist(errB):
363 return true, nil
364 }
365
366 return (fileA.ModTime().Unix() >= fileB.ModTime().Unix()), nil
367 }
368
369 func AddTagPrefix(tag string) string {
370 if strings.HasPrefix(tag, TagPrefix) {
371 return tag
372 }
373 return TagPrefix + tag
374 }
375
376 func TrimTagPrefix(tag string) string {
377 return strings.TrimPrefix(tag, TagPrefix)
378 }
379
380 func TagStringToSemver(tag string) (semver.Version, error) {
381 return semver.Make(TrimTagPrefix(tag))
382 }
383
384 func SemverToTagString(tag semver.Version) string {
385 return AddTagPrefix(tag.String())
386 }
387
388
389 func CopyFileLocal(src, dst string, required bool) error {
390 logrus.Infof("Trying to copy file %s to %s (required: %v)", src, dst, required)
391 srcStat, err := os.Stat(src)
392 if err != nil && required {
393 return fmt.Errorf("source %s is required but does not exist: %w", src, err)
394 }
395 if os.IsNotExist(err) && !required {
396 logrus.Infof(
397 "File %s does not exist but is also not required",
398 filepath.Base(src),
399 )
400 return nil
401 }
402
403 if !srcStat.Mode().IsRegular() {
404 return errors.New("cannot copy non-regular file: IsRegular reports " +
405 "whether m describes a regular file. That is, it tests that no " +
406 "mode type bits are set")
407 }
408
409 source, err := os.Open(src)
410 if err != nil {
411 return fmt.Errorf("open source file %s: %w", src, err)
412 }
413 defer source.Close()
414
415 destination, err := os.Create(dst)
416 if err != nil {
417 return fmt.Errorf("create destination file %s: %w", dst, err)
418 }
419 defer destination.Close()
420 if _, err := io.Copy(destination, source); err != nil {
421 return fmt.Errorf("copy source %s to destination %s: %w", src, dst, err)
422 }
423 logrus.Infof("Copied %s", filepath.Base(dst))
424 return nil
425 }
426
427
428
429 func CopyDirContentsLocal(src, dst string) error {
430 logrus.Infof("Trying to copy dir %s to %s", src, dst)
431
432 if _, err := os.Stat(dst); err != nil {
433 if err := os.MkdirAll(dst, os.FileMode(0o755)); err != nil {
434 return fmt.Errorf("create destination directory %s: %w", dst, err)
435 }
436 }
437 files, err := os.ReadDir(src)
438 if err != nil {
439 return fmt.Errorf("reading source dir %s: %w", src, err)
440 }
441 for _, file := range files {
442 srcPath := filepath.Join(src, file.Name())
443 dstPath := filepath.Join(dst, file.Name())
444
445 fileInfo, err := os.Stat(srcPath)
446 if err != nil {
447 return fmt.Errorf("stat source path %s: %w", srcPath, err)
448 }
449
450 switch fileInfo.Mode() & os.ModeType {
451 case os.ModeDir:
452 if !Exists(dstPath) {
453 if err := os.MkdirAll(dstPath, os.FileMode(0o755)); err != nil {
454 return fmt.Errorf("creating destination dir %s: %w", dstPath, err)
455 }
456 }
457 if err := CopyDirContentsLocal(srcPath, dstPath); err != nil {
458 return fmt.Errorf("copy %s to %s: %w", srcPath, dstPath, err)
459 }
460 default:
461 if err := CopyFileLocal(srcPath, dstPath, false); err != nil {
462 return fmt.Errorf("copy %s to %s: %w", srcPath, dstPath, err)
463 }
464 }
465 }
466 return nil
467 }
468
469
470 func RemoveAndReplaceDir(path string) error {
471 logrus.Infof("Removing %s", path)
472 if err := os.RemoveAll(path); err != nil {
473 return fmt.Errorf("remove %s: %w", path, err)
474 }
475 logrus.Infof("Creating %s", path)
476 if err := os.MkdirAll(path, os.FileMode(0o755)); err != nil {
477 return fmt.Errorf("create %s: %w", path, err)
478 }
479 return nil
480 }
481
482
483 func Exists(path string) bool {
484 if _, err := os.Stat(path); os.IsNotExist(err) {
485 return false
486 }
487
488 return true
489 }
490
491
492 func WrapText(originalText string, lineSize int) (wrappedText string) {
493 words := strings.Fields(strings.TrimSpace(originalText))
494 wrappedText = words[0]
495 spaceLeft := lineSize - len(wrappedText)
496 for _, word := range words[1:] {
497 if len(word)+1 > spaceLeft {
498 wrappedText += "\n" + word
499 spaceLeft = lineSize - len(word)
500 } else {
501 wrappedText += " " + word
502 spaceLeft -= 1 + len(word)
503 }
504 }
505
506 return wrappedText
507 }
508
509
510
511 func StripControlCharacters(logData []byte) []byte {
512 return regexpCRLF.ReplaceAllLiteral(
513 regexpCtrlChar.ReplaceAllLiteral(logData, []byte{}), []byte{},
514 )
515 }
516
517
518
519 func StripSensitiveData(logData []byte) []byte {
520
521 logData = regexpOauthToken.ReplaceAllLiteral(logData, []byte("__SANITIZED__:x-oauth-basic"))
522
523 logData = regexpGitToken.ReplaceAllLiteral(logData, []byte("//git:__SANITIZED__:@github.com"))
524 return logData
525 }
526
527
528 func CleanLogFile(logPath string) (err error) {
529 logrus.Debugf("Sanitizing logfile %s", logPath)
530
531
532 tempFile, err := os.CreateTemp("", "temp-release-log-")
533 if err != nil {
534 return fmt.Errorf("creating temp file for sanitizing log: %w", err)
535 }
536 defer func() {
537 err = tempFile.Close()
538 os.Remove(tempFile.Name())
539 }()
540
541
542 logFile, err := os.Open(logPath)
543 if err != nil {
544 return fmt.Errorf("while opening %s : %w", logPath, err)
545 }
546
547 scanner := bufio.NewScanner(logFile)
548 for scanner.Scan() {
549 chunk := scanner.Bytes()
550 chunk = StripControlCharacters(
551 StripSensitiveData(chunk),
552 )
553 chunk = append(chunk, []byte{10}...)
554 _, err := tempFile.Write(chunk)
555 if err != nil {
556 return fmt.Errorf("while writing buffer to file: %w", err)
557 }
558 }
559 if err := logFile.Close(); err != nil {
560 return fmt.Errorf("closing log file: %w", err)
561 }
562
563 if err := CopyFileLocal(tempFile.Name(), logPath, true); err != nil {
564 return fmt.Errorf("writing clean logfile: %w", err)
565 }
566
567 return err
568 }
569
View as plain text