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  }