From f7a4bf46417f0c7fc2e08eeb4f25c27235778a08 Mon Sep 17 00:00:00 2001 From: Toma Puljak Date: Tue, 11 Jun 2024 10:56:45 +0000 Subject: [PATCH] feat: server IP configuration If the server is running in a network with a static IP it can be directly accessed from the CLI and workspaces Signed-off-by: Toma Puljak --- .../apiclient/conversion/server_config.go | 44 +++++++++++++++++++ pkg/api/controllers/binary/get_daytona.go | 11 ++--- pkg/api/docs/docs.go | 3 ++ pkg/api/docs/swagger.json | 3 ++ pkg/api/docs/swagger.yaml | 2 + pkg/apiclient/api/openapi.yaml | 3 ++ pkg/apiclient/docs/ServerConfig.md | 26 +++++++++++ pkg/apiclient/model_server_config.go | 36 +++++++++++++++ pkg/cmd/apikey/generate.go | 4 +- pkg/cmd/server/config.go | 4 +- pkg/cmd/server/serve.go | 30 ++++++++----- pkg/server/headscale/config.go | 4 +- pkg/server/headscale/connect.go | 2 +- pkg/server/headscale/server.go | 21 ++++----- pkg/server/types.go | 14 ++++++ pkg/views/server/config.go | 7 ++- pkg/views/server/configure.go | 38 ++++++++++++---- 17 files changed, 204 insertions(+), 48 deletions(-) create mode 100644 internal/util/apiclient/conversion/server_config.go diff --git a/internal/util/apiclient/conversion/server_config.go b/internal/util/apiclient/conversion/server_config.go new file mode 100644 index 0000000000..cf2b7549c9 --- /dev/null +++ b/internal/util/apiclient/conversion/server_config.go @@ -0,0 +1,44 @@ +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package conversion + +import ( + "github.com/daytonaio/daytona/pkg/apiclient" + "github.com/daytonaio/daytona/pkg/server" +) + +func ToServerConfig(serverConfigDto *apiclient.ServerConfig) *server.Config { + if serverConfigDto == nil { + return nil + } + + config := &server.Config{ + Id: *serverConfigDto.Id, + ProvidersDir: *serverConfigDto.ProvidersDir, + RegistryUrl: *serverConfigDto.RegistryUrl, + ServerDownloadUrl: *serverConfigDto.ServerDownloadUrl, + IpWithProtocol: serverConfigDto.IpWithProtocol, + ApiPort: uint32(*serverConfigDto.ApiPort), + LocalBuilderRegistryPort: uint32(*serverConfigDto.LocalBuilderRegistryPort), + BuilderRegistryServer: *serverConfigDto.BuilderRegistryServer, + BuildImageNamespace: *serverConfigDto.BuildImageNamespace, + HeadscalePort: uint32(*serverConfigDto.HeadscalePort), + BinariesPath: *serverConfigDto.BinariesPath, + LogFilePath: *serverConfigDto.LogFilePath, + DefaultProjectImage: *serverConfigDto.DefaultProjectImage, + DefaultProjectUser: *serverConfigDto.DefaultProjectUser, + DefaultProjectPostStartCommands: serverConfigDto.DefaultProjectPostStartCommands, + BuilderImage: *serverConfigDto.BuilderImage, + } + + if serverConfigDto.Frps != nil { + config.Frps = &server.FRPSConfig{ + Domain: *serverConfigDto.Frps.Domain, + Port: uint32(*serverConfigDto.Frps.Port), + Protocol: *serverConfigDto.Frps.Protocol, + } + } + + return config +} diff --git a/pkg/api/controllers/binary/get_daytona.go b/pkg/api/controllers/binary/get_daytona.go index 3c254482c3..3492588aaf 100644 --- a/pkg/api/controllers/binary/get_daytona.go +++ b/pkg/api/controllers/binary/get_daytona.go @@ -4,22 +4,23 @@ package binary import ( - "fmt" "net/http" "net/url" "github.com/daytonaio/daytona/internal/constants" + "github.com/daytonaio/daytona/pkg/server" "github.com/gin-gonic/gin" ) // Used in projects to download the Daytona binary func GetDaytonaScript(ctx *gin.Context) { - scheme := "http" - if ctx.Request.TLS != nil || ctx.GetHeader("X-Forwarded-Proto") == "https" { - scheme = "https" + c, err := server.GetConfig() + if err != nil { + ctx.String(http.StatusInternalServerError, err.Error()) + return } - downloadUrl, _ := url.JoinPath(fmt.Sprintf("%s://%s", scheme, ctx.Request.Host), "binary") + downloadUrl, _ := url.JoinPath(c.GetApiUrl(), "binary") getServerScript := constants.GetDaytonaScript(downloadUrl) ctx.String(http.StatusOK, getServerScript) diff --git a/pkg/api/docs/docs.go b/pkg/api/docs/docs.go index e07a6f7e5c..3e993aa220 100644 --- a/pkg/api/docs/docs.go +++ b/pkg/api/docs/docs.go @@ -1569,6 +1569,9 @@ const docTemplate = `{ "id": { "type": "string" }, + "ipWithProtocol": { + "type": "string" + }, "localBuilderRegistryPort": { "type": "integer" }, diff --git a/pkg/api/docs/swagger.json b/pkg/api/docs/swagger.json index 5b28da6409..588c09e559 100644 --- a/pkg/api/docs/swagger.json +++ b/pkg/api/docs/swagger.json @@ -1566,6 +1566,9 @@ "id": { "type": "string" }, + "ipWithProtocol": { + "type": "string" + }, "localBuilderRegistryPort": { "type": "integer" }, diff --git a/pkg/api/docs/swagger.yaml b/pkg/api/docs/swagger.yaml index a8a4bfaa0a..6aa2d357c1 100644 --- a/pkg/api/docs/swagger.yaml +++ b/pkg/api/docs/swagger.yaml @@ -292,6 +292,8 @@ definitions: type: integer id: type: string + ipWithProtocol: + type: string localBuilderRegistryPort: type: integer logFilePath: diff --git a/pkg/apiclient/api/openapi.yaml b/pkg/apiclient/api/openapi.yaml index 9a752d6d55..657e4243ff 100644 --- a/pkg/apiclient/api/openapi.yaml +++ b/pkg/apiclient/api/openapi.yaml @@ -1306,6 +1306,7 @@ components: apiPort: 0 headscalePort: 1 buildImageNamespace: buildImageNamespace + ipWithProtocol: ipWithProtocol serverDownloadUrl: serverDownloadUrl binariesPath: binariesPath logFilePath: logFilePath @@ -1341,6 +1342,8 @@ components: type: integer id: type: string + ipWithProtocol: + type: string localBuilderRegistryPort: type: integer logFilePath: diff --git a/pkg/apiclient/docs/ServerConfig.md b/pkg/apiclient/docs/ServerConfig.md index 7cc1353afe..352080033b 100644 --- a/pkg/apiclient/docs/ServerConfig.md +++ b/pkg/apiclient/docs/ServerConfig.md @@ -15,6 +15,7 @@ Name | Type | Description | Notes **Frps** | Pointer to [**FRPSConfig**](FRPSConfig.md) | | [optional] **HeadscalePort** | Pointer to **int32** | | [optional] **Id** | Pointer to **string** | | [optional] +**IpWithProtocol** | Pointer to **string** | | [optional] **LocalBuilderRegistryPort** | Pointer to **int32** | | [optional] **LogFilePath** | Pointer to **string** | | [optional] **ProvidersDir** | Pointer to **string** | | [optional] @@ -315,6 +316,31 @@ SetId sets Id field to given value. HasId returns a boolean if a field has been set. +### GetIpWithProtocol + +`func (o *ServerConfig) GetIpWithProtocol() string` + +GetIpWithProtocol returns the IpWithProtocol field if non-nil, zero value otherwise. + +### GetIpWithProtocolOk + +`func (o *ServerConfig) GetIpWithProtocolOk() (*string, bool)` + +GetIpWithProtocolOk returns a tuple with the IpWithProtocol field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetIpWithProtocol + +`func (o *ServerConfig) SetIpWithProtocol(v string)` + +SetIpWithProtocol sets IpWithProtocol field to given value. + +### HasIpWithProtocol + +`func (o *ServerConfig) HasIpWithProtocol() bool` + +HasIpWithProtocol returns a boolean if a field has been set. + ### GetLocalBuilderRegistryPort `func (o *ServerConfig) GetLocalBuilderRegistryPort() int32` diff --git a/pkg/apiclient/model_server_config.go b/pkg/apiclient/model_server_config.go index a3b6943694..5997bc9e0e 100644 --- a/pkg/apiclient/model_server_config.go +++ b/pkg/apiclient/model_server_config.go @@ -30,6 +30,7 @@ type ServerConfig struct { Frps *FRPSConfig `json:"frps,omitempty"` HeadscalePort *int32 `json:"headscalePort,omitempty"` Id *string `json:"id,omitempty"` + IpWithProtocol *string `json:"ipWithProtocol,omitempty"` LocalBuilderRegistryPort *int32 `json:"localBuilderRegistryPort,omitempty"` LogFilePath *string `json:"logFilePath,omitempty"` ProvidersDir *string `json:"providersDir,omitempty"` @@ -406,6 +407,38 @@ func (o *ServerConfig) SetId(v string) { o.Id = &v } +// GetIpWithProtocol returns the IpWithProtocol field value if set, zero value otherwise. +func (o *ServerConfig) GetIpWithProtocol() string { + if o == nil || IsNil(o.IpWithProtocol) { + var ret string + return ret + } + return *o.IpWithProtocol +} + +// GetIpWithProtocolOk returns a tuple with the IpWithProtocol field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ServerConfig) GetIpWithProtocolOk() (*string, bool) { + if o == nil || IsNil(o.IpWithProtocol) { + return nil, false + } + return o.IpWithProtocol, true +} + +// HasIpWithProtocol returns a boolean if a field has been set. +func (o *ServerConfig) HasIpWithProtocol() bool { + if o != nil && !IsNil(o.IpWithProtocol) { + return true + } + + return false +} + +// SetIpWithProtocol gets a reference to the given string and assigns it to the IpWithProtocol field. +func (o *ServerConfig) SetIpWithProtocol(v string) { + o.IpWithProtocol = &v +} + // GetLocalBuilderRegistryPort returns the LocalBuilderRegistryPort field value if set, zero value otherwise. func (o *ServerConfig) GetLocalBuilderRegistryPort() int32 { if o == nil || IsNil(o.LocalBuilderRegistryPort) { @@ -609,6 +642,9 @@ func (o ServerConfig) ToMap() (map[string]interface{}, error) { if !IsNil(o.Id) { toSerialize["id"] = o.Id } + if !IsNil(o.IpWithProtocol) { + toSerialize["ipWithProtocol"] = o.IpWithProtocol + } if !IsNil(o.LocalBuilderRegistryPort) { toSerialize["localBuilderRegistryPort"] = o.LocalBuilderRegistryPort } diff --git a/pkg/cmd/apikey/generate.go b/pkg/cmd/apikey/generate.go index fcc6816da6..b5b11faf7a 100644 --- a/pkg/cmd/apikey/generate.go +++ b/pkg/cmd/apikey/generate.go @@ -11,8 +11,8 @@ import ( "github.com/spf13/cobra" "github.com/daytonaio/daytona/cmd/daytona/config" - "github.com/daytonaio/daytona/internal/util" apiclient_util "github.com/daytonaio/daytona/internal/util/apiclient" + "github.com/daytonaio/daytona/internal/util/apiclient/conversion" "github.com/daytonaio/daytona/pkg/apiclient" "github.com/daytonaio/daytona/pkg/views" "github.com/daytonaio/daytona/pkg/views/server/apikey" @@ -71,7 +71,7 @@ var GenerateCmd = &cobra.Command{ log.Fatal(err) } - apiUrl := util.GetFrpcApiUrl(*serverConfig.Frps.Protocol, *serverConfig.Id, *serverConfig.Frps.Domain) + apiUrl := conversion.ToServerConfig(serverConfig).GetApiUrl() view.Render(key, apiUrl) }, diff --git a/pkg/cmd/server/config.go b/pkg/cmd/server/config.go index 96e09fe730..fc351d48f8 100644 --- a/pkg/cmd/server/config.go +++ b/pkg/cmd/server/config.go @@ -8,7 +8,6 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/daytonaio/daytona/internal/util" "github.com/daytonaio/daytona/pkg/cmd/output" "github.com/daytonaio/daytona/pkg/server" ) @@ -22,8 +21,7 @@ var configCmd = &cobra.Command{ log.Fatal(err) } - apiUrl := util.GetFrpcApiUrl(config.Frps.Protocol, config.Id, config.Frps.Domain) - output.Output = apiUrl + output.Output = config.GetApiUrl() view.RenderConfig(config) }, diff --git a/pkg/cmd/server/serve.go b/pkg/cmd/server/serve.go index 603797b95a..2791faba64 100644 --- a/pkg/cmd/server/serve.go +++ b/pkg/cmd/server/serve.go @@ -96,11 +96,19 @@ var ServeCmd = &cobra.Command{ log.Fatal(err) } + var headscaleServerUrl string + if c.IpWithProtocol != nil { + headscaleServerUrl = fmt.Sprintf("%s:%d", *c.IpWithProtocol, c.HeadscalePort) + // FIXME + headscaleServerUrl = strings.Replace(headscaleServerUrl, "http://", "https://", 1) + } else if c.Frps != nil { + headscaleServerUrl = util.GetFrpcHeadscaleUrl(c.Frps.Protocol, c.Id, c.Frps.Domain) + } + headscaleServer := headscale.NewHeadscaleServer(&headscale.HeadscaleServerConfig{ - ServerId: c.Id, - FrpsDomain: c.Frps.Domain, - FrpsProtocol: c.Frps.Protocol, - HeadscalePort: c.HeadscalePort, + ServerId: c.Id, + ServerUrl: headscaleServerUrl, + Port: c.HeadscalePort, }) err = headscaleServer.Init() if err != nil { @@ -137,14 +145,12 @@ var ServeCmd = &cobra.Command{ ApiKeyStore: apiKeyStore, }) - headscaleUrl := util.GetFrpcHeadscaleUrl(c.Frps.Protocol, c.Id, c.Frps.Domain) - providerManager := manager.NewProviderManager(manager.ProviderManagerConfig{ LogsDir: logsDir, ProviderTargetService: providerTargetService, - ApiUrl: util.GetFrpcApiUrl(c.Frps.Protocol, c.Id, c.Frps.Domain), + ServerUrl: headscaleServerUrl, + ApiUrl: c.GetApiUrl(), DaytonaDownloadUrl: getDaytonaScriptUrl(c), - ServerUrl: headscaleUrl, RegistryUrl: c.RegistryUrl, BaseDir: c.ProvidersDir, CreateProviderNetworkKey: func(providerName string) (string, error) { @@ -185,8 +191,8 @@ var ServeCmd = &cobra.Command{ ApiKeyService: apiKeyService, GitProviderService: gitProviderService, ContainerRegistryService: containerRegistryService, - ServerApiUrl: util.GetFrpcApiUrl(c.Frps.Protocol, c.Id, c.Frps.Domain), - ServerUrl: headscaleUrl, + ServerApiUrl: c.GetApiUrl(), + ServerUrl: headscaleServerUrl, DefaultProjectImage: c.DefaultProjectImage, DefaultProjectUser: c.DefaultProjectUser, DefaultProjectPostStartCommands: c.DefaultProjectPostStartCommands, @@ -268,12 +274,12 @@ func waitForServerToStart(apiServer *api.ApiServer) error { } func getDaytonaScriptUrl(config *server.Config) string { - url, _ := url.JoinPath(util.GetFrpcApiUrl(config.Frps.Protocol, config.Id, config.Frps.Domain), "binary", "script") + url, _ := url.JoinPath(config.GetApiUrl(), "binary", "script") return url } func printServerStartedMessage(c *server.Config, runAsDaemon bool) { - started_view.Render(c.ApiPort, util.GetFrpcApiUrl(c.Frps.Protocol, c.Id, c.Frps.Domain), runAsDaemon) + started_view.Render(c.ApiPort, c.GetApiUrl(), runAsDaemon) } func getDbPath() (string, error) { diff --git a/pkg/server/headscale/config.go b/pkg/server/headscale/config.go index 1c72b7bbca..3e366983b5 100644 --- a/pkg/server/headscale/config.go +++ b/pkg/server/headscale/config.go @@ -27,8 +27,8 @@ func (s *HeadscaleServer) getHeadscaleConfig() (*hstypes.Config, error) { cfg := &hstypes.Config{ DBtype: "sqlite3", - ServerURL: fmt.Sprintf("https://%s.%s", s.serverId, s.frpsDomain), - Addr: fmt.Sprintf("0.0.0.0:%d", s.headscalePort), + ServerURL: s.serverUrl, + Addr: fmt.Sprintf("0.0.0.0:%d", s.port), EphemeralNodeInactivityTimeout: 5 * time.Minute, NodeUpdateCheckInterval: 10 * time.Second, BaseDomain: "daytona.local", diff --git a/pkg/server/headscale/connect.go b/pkg/server/headscale/connect.go index fe6e82174d..139d60ae53 100644 --- a/pkg/server/headscale/connect.go +++ b/pkg/server/headscale/connect.go @@ -27,7 +27,7 @@ func (s *HeadscaleServer) Connect() error { log.Fatal(err) } - tsNetServer.ControlURL = fmt.Sprintf("http://localhost:%d", s.headscalePort) + tsNetServer.ControlURL = s.serverUrl tsNetServer.AuthKey = authKey defer tsNetServer.Close() diff --git a/pkg/server/headscale/server.go b/pkg/server/headscale/server.go index 3832db89af..67e152b4f7 100644 --- a/pkg/server/headscale/server.go +++ b/pkg/server/headscale/server.go @@ -11,26 +11,23 @@ import ( ) type HeadscaleServerConfig struct { - ServerId string - FrpsDomain string - FrpsProtocol string - HeadscalePort uint32 + ServerId string + Port uint32 + ServerUrl string } func NewHeadscaleServer(config *HeadscaleServerConfig) *HeadscaleServer { return &HeadscaleServer{ - serverId: config.ServerId, - frpsDomain: config.FrpsDomain, - frpsProtocol: config.FrpsProtocol, - headscalePort: config.HeadscalePort, + serverId: config.ServerId, + port: config.Port, + serverUrl: config.ServerUrl, } } type HeadscaleServer struct { - serverId string - frpsDomain string - frpsProtocol string - headscalePort uint32 + serverId string + port uint32 + serverUrl string } func (s *HeadscaleServer) Init() error { diff --git a/pkg/server/types.go b/pkg/server/types.go index 4d94ff052a..360f893590 100644 --- a/pkg/server/types.go +++ b/pkg/server/types.go @@ -4,7 +4,10 @@ package server import ( + "fmt" "net/http" + + "github.com/daytonaio/daytona/internal/util" ) type TailscaleServer interface { @@ -35,6 +38,7 @@ type Config struct { Id string `json:"id"` ServerDownloadUrl string `json:"serverDownloadUrl"` Frps *FRPSConfig `json:"frps,omitempty"` + IpWithProtocol *string `json:"ipWithProtocol,omitempty"` ApiPort uint32 `json:"apiPort"` HeadscalePort uint32 `json:"headscalePort"` BinariesPath string `json:"binariesPath"` @@ -47,3 +51,13 @@ type Config struct { BuilderRegistryServer string `json:"builderRegistryServer"` BuildImageNamespace string `json:"buildImageNamespace"` } // @name ServerConfig + +func (config *Config) GetApiUrl() string { + apiUrl := util.GetFrpcApiUrl(config.Frps.Protocol, config.Id, config.Frps.Domain) + + if config.IpWithProtocol != nil { + apiUrl = fmt.Sprintf("%s:%d", *config.IpWithProtocol, config.ApiPort) + } + + return apiUrl +} diff --git a/pkg/views/server/config.go b/pkg/views/server/config.go index be62be159b..5e49ffc8c7 100644 --- a/pkg/views/server/config.go +++ b/pkg/views/server/config.go @@ -7,18 +7,21 @@ import ( "fmt" "github.com/charmbracelet/lipgloss" - "github.com/daytonaio/daytona/internal/util" "github.com/daytonaio/daytona/pkg/server" "github.com/daytonaio/daytona/pkg/views" ) func RenderConfig(config *server.Config) { - apiUrl := util.GetFrpcApiUrl(config.Frps.Protocol, config.Id, config.Frps.Domain) + apiUrl := config.GetApiUrl() output := views.GetStyledMainTitle("Daytona Server Config") + "\n\n" output += fmt.Sprintf("%s %s", views.GetPropertyKey("Server ID: "), config.Id) + "\n\n" + if config.IpWithProtocol != nil { + output += fmt.Sprintf("%s %s", views.GetPropertyKey("Server IP: "), *config.IpWithProtocol) + "\n\n" + } + output += fmt.Sprintf("%s %s", views.GetPropertyKey("API URL: "), apiUrl) + "\n\n" output += fmt.Sprintf("%s %d", views.GetPropertyKey("API Port: "), config.ApiPort) + "\n\n" diff --git a/pkg/views/server/configure.go b/pkg/views/server/configure.go index 763d2aafb8..d6ce0c8ff2 100644 --- a/pkg/views/server/configure.go +++ b/pkg/views/server/configure.go @@ -26,6 +26,10 @@ func ConfigurationForm(config *apiclient.ServerConfig, containerRegistries []api frpsPortView := strconv.Itoa(int(config.Frps.GetPort())) localBuilderRegistryPort := strconv.Itoa(int(config.GetLocalBuilderRegistryPort())) + if config.IpWithProtocol == nil { + config.IpWithProtocol = new(string) + } + builderContainerRegistryOptions := []huh.Option[string]{{ Key: "Local registry managed by Daytona", Value: "local", @@ -57,6 +61,30 @@ func ConfigurationForm(config *apiclient.ServerConfig, containerRegistries []api Value(config.DefaultProjectUser), GetPostStartCommandsInput(&config.DefaultProjectPostStartCommands, "Default Project Post Start Commands"), ), + huh.NewGroup( + huh.NewInput(). + Title("Server IP with Protocol"). + // TODO: Add docs entry link + Description("If the server has a static IP address, you can set it here to avoid using our reverse proxy\nNOTE: Due to current limitations of our Headscale server, the IP must be accessible over https"). + Value(config.IpWithProtocol). + Validate(func(s string) error { + if s == "" { + return nil + } + if !strings.HasPrefix(s, "http://") && !strings.HasPrefix(s, "https://") { + return errors.New("invalid protocol") + } + return nil + }), + huh.NewInput(). + Title("API Port"). + Value(&apiPortView). + Validate(createPortValidator(config, &apiPortView, config.ApiPort)), + huh.NewInput(). + Title("Headscale Port"). + Value(&headscalePortView). + Validate(createPortValidator(config, &headscalePortView, config.HeadscalePort)), + ), huh.NewGroup( huh.NewInput(). Title("Builder Image"). @@ -83,14 +111,6 @@ func ConfigurationForm(config *apiclient.ServerConfig, containerRegistries []api return config.BuilderRegistryServer == nil || *config.BuilderRegistryServer != "local" }), huh.NewGroup( - huh.NewInput(). - Title("API Port"). - Value(&apiPortView). - Validate(createPortValidator(config, &apiPortView, config.ApiPort)), - huh.NewInput(). - Title("Headscale Port"). - Value(&headscalePortView). - Validate(createPortValidator(config, &headscalePortView, config.HeadscalePort)), huh.NewInput(). Title("Binaries Path"). Description("Directory will be created if it does not exist"). @@ -120,7 +140,7 @@ func ConfigurationForm(config *apiclient.ServerConfig, containerRegistries []api huh.NewInput(). Title("Frps Protocol"). Value(config.Frps.Protocol), - ), + ).Description("Frps is a reverse proxy server that is used for connecting workspaces to the server and public port forwarding"), ).WithTheme(views.GetCustomTheme()) err := form.Run()