1
16
17 package addr
18
19 import (
20 "errors"
21 "fmt"
22 "io/fs"
23 "net"
24 "os"
25 "path/filepath"
26 "strings"
27 "time"
28
29 "sigs.k8s.io/controller-runtime/pkg/internal/flock"
30 )
31
32
33
34 const (
35 portReserveTime = 2 * time.Minute
36 portConflictRetry = 100
37 portFilePrefix = "port-"
38 )
39
40 var (
41 cacheDir string
42 )
43
44 func init() {
45 baseDir, err := os.UserCacheDir()
46 if err == nil {
47 cacheDir = filepath.Join(baseDir, "kubebuilder-envtest")
48 err = os.MkdirAll(cacheDir, 0o750)
49 }
50 if err != nil {
51
52 baseDir = os.TempDir()
53 cacheDir = filepath.Join(baseDir, "kubebuilder-envtest")
54 err = os.MkdirAll(cacheDir, 0o750)
55 }
56 if err != nil {
57 panic(err)
58 }
59 }
60
61 type portCache struct{}
62
63 func (c *portCache) add(port int) (bool, error) {
64
65 if err := fs.WalkDir(os.DirFS(cacheDir), ".", func(path string, d fs.DirEntry, err error) error {
66 if err != nil {
67 return err
68 }
69 if d.IsDir() || !d.Type().IsRegular() || !strings.HasPrefix(path, portFilePrefix) {
70 return nil
71 }
72 info, err := d.Info()
73 if err != nil {
74
75
76 if errors.Is(err, fs.ErrNotExist) {
77 return nil
78 }
79 return err
80 }
81 if time.Since(info.ModTime()) > portReserveTime {
82 if err := os.Remove(filepath.Join(cacheDir, path)); err != nil {
83
84
85 if os.IsNotExist(err) {
86 return nil
87 }
88 return err
89 }
90 }
91 return nil
92 }); err != nil {
93 return false, err
94 }
95
96 path := fmt.Sprintf("%s/%s%d", cacheDir, portFilePrefix, port)
97 if err := flock.Acquire(path); errors.Is(err, flock.ErrAlreadyLocked) {
98 return false, nil
99 } else if err != nil {
100 return false, err
101 }
102 return true, nil
103 }
104
105 var cache = &portCache{}
106
107 func suggest(listenHost string) (*net.TCPListener, int, string, error) {
108 if listenHost == "" {
109 listenHost = "localhost"
110 }
111 addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(listenHost, "0"))
112 if err != nil {
113 return nil, -1, "", err
114 }
115 l, err := net.ListenTCP("tcp", addr)
116 if err != nil {
117 return nil, -1, "", err
118 }
119 return l, l.Addr().(*net.TCPAddr).Port,
120 addr.IP.String(),
121 nil
122 }
123
124
125
126
127
128 func Suggest(listenHost string) (int, string, error) {
129 for i := 0; i < portConflictRetry; i++ {
130 listener, port, resolvedHost, err := suggest(listenHost)
131 if err != nil {
132 return -1, "", err
133 }
134 defer listener.Close()
135 if ok, err := cache.add(port); ok {
136 return port, resolvedHost, nil
137 } else if err != nil {
138 return -1, "", err
139 }
140 }
141 return -1, "", fmt.Errorf("no free ports found after %d retries", portConflictRetry)
142 }
143
View as plain text