mirror of
				https://github.com/redhat-developer/odo.git
				synced 2025-10-19 03:06:19 +03:00 
			
		
		
		
	This ensures the same local address is used for listening and checking if a given port is free.
Otherwise, `net.listen("tcp", ":$port")` would listen on `0.0.0.0:$port`,
and, on some operating systems like Windows 11, `127.0.0.1:$port` is surprisingly considered as free
(see output below). This, as a consequence, made it impossible to run multiple Dev Sessions on Windows.
```
PS C:\Users\asoro> netstat -aon | grep 2000
  TCP    0.0.0.0:20000          0.0.0.0:0              LISTENING       11044
  TCP    127.0.0.1:20001        0.0.0.0:0              LISTENING       11044
  TCP    [::]:20000             [::]:0                 LISTENING       11044
  TCP    [::1]:20000            [::1]:53656            ESTABLISHED     11044
  TCP    [::1]:53656            [::1]:20000            ESTABLISHED     9984
```
Using the same local address for listening and checking if the port is free would be safer.
If we decide to support passing a custom address, we would use that address instead.
		
	
		
			
				
	
	
		
			162 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			162 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package apiserver_impl
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"embed"
 | 
						|
	"fmt"
 | 
						|
	"io/fs"
 | 
						|
	"net"
 | 
						|
	"net/http"
 | 
						|
 | 
						|
	"k8s.io/klog"
 | 
						|
 | 
						|
	openapi "github.com/redhat-developer/odo/pkg/apiserver-gen/go"
 | 
						|
	"github.com/redhat-developer/odo/pkg/apiserver-impl/sse"
 | 
						|
	"github.com/redhat-developer/odo/pkg/informer"
 | 
						|
	"github.com/redhat-developer/odo/pkg/kclient"
 | 
						|
	"github.com/redhat-developer/odo/pkg/log"
 | 
						|
	"github.com/redhat-developer/odo/pkg/odo/cli/feature"
 | 
						|
	"github.com/redhat-developer/odo/pkg/podman"
 | 
						|
	"github.com/redhat-developer/odo/pkg/preference"
 | 
						|
	"github.com/redhat-developer/odo/pkg/state"
 | 
						|
	"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
 | 
						|
	"github.com/redhat-developer/odo/pkg/util"
 | 
						|
)
 | 
						|
 | 
						|
//go:embed ui/*
 | 
						|
var staticFiles embed.FS
 | 
						|
 | 
						|
// swagger UI files are from https://github.com/swagger-api/swagger-ui/tree/master/dist
 | 
						|
 | 
						|
//go:embed swagger-ui/*
 | 
						|
var swaggerFiles embed.FS
 | 
						|
 | 
						|
type ApiServer struct {
 | 
						|
	PushWatcher <-chan struct{}
 | 
						|
}
 | 
						|
 | 
						|
func StartServer(
 | 
						|
	ctx context.Context,
 | 
						|
	cancelFunc context.CancelFunc,
 | 
						|
	randomPort bool,
 | 
						|
	port int,
 | 
						|
	devfilePath string,
 | 
						|
	devfileFiles []string,
 | 
						|
	fsys filesystem.Filesystem,
 | 
						|
	kubernetesClient kclient.ClientInterface,
 | 
						|
	podmanClient podman.Client,
 | 
						|
	stateClient state.Client,
 | 
						|
	preferenceClient preference.Client,
 | 
						|
	informerClient *informer.InformerClient,
 | 
						|
) (ApiServer, error) {
 | 
						|
	pushWatcher := make(chan struct{})
 | 
						|
	defaultApiService := NewDefaultApiService(
 | 
						|
		cancelFunc,
 | 
						|
		pushWatcher,
 | 
						|
		kubernetesClient,
 | 
						|
		podmanClient,
 | 
						|
		stateClient,
 | 
						|
		preferenceClient,
 | 
						|
	)
 | 
						|
	defaultApiController := openapi.NewDefaultApiController(defaultApiService)
 | 
						|
	devstateApiService := NewDevstateApiService(
 | 
						|
		cancelFunc,
 | 
						|
		pushWatcher,
 | 
						|
		kubernetesClient,
 | 
						|
		podmanClient,
 | 
						|
		stateClient,
 | 
						|
		preferenceClient,
 | 
						|
	)
 | 
						|
	devstateApiController := openapi.NewDevstateApiController(devstateApiService)
 | 
						|
 | 
						|
	sseNotifier, err := sse.NewNotifier(ctx, fsys, devfilePath, devfileFiles)
 | 
						|
	if err != nil {
 | 
						|
		return ApiServer{}, err
 | 
						|
	}
 | 
						|
 | 
						|
	router := openapi.NewRouter(sseNotifier, defaultApiController, devstateApiController)
 | 
						|
 | 
						|
	fSysSwagger, err := fs.Sub(swaggerFiles, "swagger-ui")
 | 
						|
	if err != nil {
 | 
						|
		// Assertion, error can only happen if the path "swagger-ui" is not valid
 | 
						|
		panic(err)
 | 
						|
	}
 | 
						|
	swaggerServer := http.FileServer(http.FS(fSysSwagger))
 | 
						|
	router.PathPrefix("/swagger-ui/").Handler(http.StripPrefix("/swagger-ui/", swaggerServer))
 | 
						|
 | 
						|
	if feature.IsEnabled(ctx, feature.UIServer) {
 | 
						|
		var fSys fs.FS
 | 
						|
		fSys, err = fs.Sub(staticFiles, "ui")
 | 
						|
		if err != nil {
 | 
						|
			// Assertion, error can only happen if the path "ui" is not valid
 | 
						|
			panic(err)
 | 
						|
		}
 | 
						|
 | 
						|
		staticServer := http.FileServer(http.FS(fSys))
 | 
						|
		router.PathPrefix("/").Handler(staticServer)
 | 
						|
	}
 | 
						|
 | 
						|
	addr := "127.0.0.1"
 | 
						|
	if port == 0 && !randomPort {
 | 
						|
		port, err = util.NextFreePort(20000, 30001, nil, addr)
 | 
						|
		if err != nil {
 | 
						|
			klog.V(0).Infof("Unable to start the API server; encountered error: %v", err)
 | 
						|
			cancelFunc()
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", addr, port))
 | 
						|
	if err != nil {
 | 
						|
		return ApiServer{}, fmt.Errorf("unable to start API Server listener on port %d: %w", port, err)
 | 
						|
	}
 | 
						|
 | 
						|
	server := &http.Server{
 | 
						|
		BaseContext: func(net.Listener) context.Context {
 | 
						|
			return ctx
 | 
						|
		},
 | 
						|
		Handler: router,
 | 
						|
	}
 | 
						|
	var errChan = make(chan error)
 | 
						|
	go func() {
 | 
						|
		errChan <- server.Serve(listener)
 | 
						|
	}()
 | 
						|
 | 
						|
	// Get the actual port value assigned by the operating system
 | 
						|
	listeningPort := listener.Addr().(*net.TCPAddr).Port
 | 
						|
	if port != 0 && port != listeningPort {
 | 
						|
		panic(fmt.Sprintf("requested port (%d) not the same as the actual port the API Server is bound to (%d)", port, listeningPort))
 | 
						|
	}
 | 
						|
 | 
						|
	err = stateClient.SetAPIServerPort(ctx, listeningPort)
 | 
						|
	if err != nil {
 | 
						|
		klog.V(0).Infof("Unable to start the API server; encountered error: %v", err)
 | 
						|
		cancelFunc()
 | 
						|
	}
 | 
						|
 | 
						|
	if feature.IsEnabled(ctx, feature.UIServer) {
 | 
						|
		info := fmt.Sprintf("Web console accessible at http://localhost:%d/", listeningPort)
 | 
						|
		log.Spinner(info).End(true)
 | 
						|
		informerClient.AppendInfo(info + "\n")
 | 
						|
	}
 | 
						|
	log.Spinner(fmt.Sprintf("API Server started at http://localhost:%d/api/v1", listeningPort)).End(true)
 | 
						|
	log.Spinner(fmt.Sprintf("API documentation accessible at http://localhost:%d/swagger-ui/", listeningPort)).End(true)
 | 
						|
 | 
						|
	go func() {
 | 
						|
		select {
 | 
						|
		case <-ctx.Done():
 | 
						|
			klog.V(0).Infof("Shutting down the API server: %v", ctx.Err())
 | 
						|
			err = server.Shutdown(ctx)
 | 
						|
			if err != nil {
 | 
						|
				klog.V(1).Infof("Error while shutting down the API server: %v", err)
 | 
						|
			}
 | 
						|
		case err = <-errChan:
 | 
						|
			klog.V(0).Infof("Stopping the API server; encountered error: %v", err)
 | 
						|
			cancelFunc()
 | 
						|
		}
 | 
						|
	}()
 | 
						|
 | 
						|
	return ApiServer{
 | 
						|
		PushWatcher: pushWatcher,
 | 
						|
	}, nil
 | 
						|
}
 |