github.com/openshift/installer@v1.4.17/pkg/clusterapi/internal/process/addr/addr.go (about) 1 package addr 2 3 import ( 4 "errors" 5 "fmt" 6 "io/fs" 7 "net" 8 "os" 9 "path/filepath" 10 "strings" 11 "time" 12 13 "github.com/openshift/installer/pkg/clusterapi/internal/process/flock" 14 ) 15 16 const ( 17 portReserveTime = 2 * time.Minute 18 portConflictRetry = 100 19 portFilePrefix = "port-" 20 ) 21 22 var ( 23 cacheDir string 24 ) 25 26 func init() { 27 baseDir, err := os.UserCacheDir() 28 if err == nil { 29 cacheDir = filepath.Join(baseDir, "kubebuilder-envtest") 30 err = os.MkdirAll(cacheDir, 0o750) 31 } 32 if err != nil { 33 // Either we didn't get a cache directory, or we can't use it 34 baseDir = os.TempDir() 35 cacheDir = filepath.Join(baseDir, "kubebuilder-envtest") 36 err = os.MkdirAll(cacheDir, 0o750) 37 } 38 if err != nil { 39 panic(err) 40 } 41 } 42 43 type portCache struct{} 44 45 func (c *portCache) add(port int) (bool, error) { 46 // Remove outdated ports. 47 if err := fs.WalkDir(os.DirFS(cacheDir), ".", func(path string, d fs.DirEntry, err error) error { 48 if err != nil { 49 return err 50 } 51 if d.IsDir() || !d.Type().IsRegular() || !strings.HasPrefix(path, portFilePrefix) { 52 return nil 53 } 54 info, err := d.Info() 55 if err != nil { 56 // No-op if file no longer exists; may have been deleted by another 57 // process/thread trying to allocate ports. 58 if errors.Is(err, fs.ErrNotExist) { 59 return nil 60 } 61 return err 62 } 63 if time.Since(info.ModTime()) > portReserveTime { 64 if err := os.Remove(filepath.Join(cacheDir, path)); err != nil { 65 // No-op if file no longer exists; may have been deleted by another 66 // process/thread trying to allocate ports. 67 if os.IsNotExist(err) { 68 return nil 69 } 70 return err 71 } 72 } 73 return nil 74 }); err != nil { 75 return false, err 76 } 77 // Try allocating new port, by acquiring a file. 78 path := fmt.Sprintf("%s/%s%d", cacheDir, portFilePrefix, port) 79 if err := flock.Acquire(path); errors.Is(err, flock.ErrAlreadyLocked) { 80 return false, nil 81 } else if err != nil { 82 return false, err 83 } 84 return true, nil 85 } 86 87 var cache = &portCache{} 88 89 func suggest(listenHost string) (*net.TCPListener, int, string, error) { 90 if listenHost == "" { 91 listenHost = "localhost" 92 } 93 addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(listenHost, "0")) 94 if err != nil { 95 return nil, -1, "", err 96 } 97 l, err := net.ListenTCP("tcp", addr) 98 if err != nil { 99 return nil, -1, "", err 100 } 101 return l, l.Addr().(*net.TCPAddr).Port, 102 addr.IP.String(), 103 nil 104 } 105 106 // Suggest suggests an address a process can listen on. It returns 107 // a tuple consisting of a free port and the hostname resolved to its IP. 108 // It makes sure that new port allocated does not conflict with old ports 109 // allocated within 1 minute. 110 func Suggest(listenHost string) (int, string, error) { 111 for i := 0; i < portConflictRetry; i++ { 112 listener, port, resolvedHost, err := suggest(listenHost) 113 if err != nil { 114 return -1, "", err 115 } 116 defer listener.Close() 117 if ok, err := cache.add(port); ok { 118 return port, resolvedHost, nil 119 } else if err != nil { 120 return -1, "", err 121 } 122 } 123 return -1, "", fmt.Errorf("no free ports found after %d retries", portConflictRetry) 124 }