diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go index 368fd1dfc..b51fd8e3e 100644 --- a/pkg/cloud/cloud.go +++ b/pkg/cloud/cloud.go @@ -19,6 +19,7 @@ package cloud import ( "fmt" "io" + "strings" "sync" "google.golang.org/grpc" @@ -26,6 +27,7 @@ import ( "github.com/nitrictech/cli/pkg/cloud/apis" "github.com/nitrictech/cli/pkg/cloud/batch" + "github.com/nitrictech/cli/pkg/cloud/env" "github.com/nitrictech/cli/pkg/cloud/gateway" "github.com/nitrictech/cli/pkg/cloud/http" "github.com/nitrictech/cli/pkg/cloud/keyvalue" @@ -255,6 +257,18 @@ type LocalCloudOptions struct { } func New(projectName string, opts LocalCloudOptions) (*LocalCloud, error) { + hostname := env.NITRIC_HOSTNAME.String() + + // Reject hostnames containing characters that would produce malformed URLs + if strings.ContainsAny(hostname, "/?#\r\n \t") { + return nil, fmt.Errorf("NITRIC_HOSTNAME contains invalid characters: %q", hostname) + } + + // Bracket IPv6 literals for use in URLs (RFC 3986) + if strings.Contains(hostname, ":") && !strings.HasPrefix(hostname, "[") { + hostname = "[" + hostname + "]" + } + localTopics, err := topics.NewLocalTopicsService() if err != nil { return nil, err @@ -268,6 +282,7 @@ func New(projectName string, opts LocalCloudOptions) (*LocalCloud, error) { localStorage, err := storage.NewLocalStorageService(storage.StorageOptions{ AccessKey: "dummykey", SecretKey: "dummysecret", + Hostname: hostname, }) if err != nil { return nil, err @@ -283,6 +298,7 @@ func New(projectName string, opts LocalCloudOptions) (*LocalCloud, error) { LogWriter: opts.LogWriter, LocalConfig: opts.LocalConfig, BatchPlugin: localBatch, + Hostname: hostname, }) if err != nil { return nil, err @@ -309,6 +325,9 @@ func New(projectName string, opts LocalCloudOptions) (*LocalCloud, error) { return nil, err } + // connectionStringHost controls how application services reach the Postgres container, + // which is distinct from NITRIC_HOSTNAME (how callers reach the Nitric backend). + // Postgres is its own container with host-mapped ports, so it needs separate addressing. connectionStringHost := "localhost" // Use the host.docker.internal address for connection strings with local cloud run mode @@ -321,7 +340,7 @@ func New(projectName string, opts LocalCloudOptions) (*LocalCloud, error) { return nil, err } - localWebsites := websites.NewLocalWebsitesService(localGateway.GetApiAddress, localGateway.GetWebsocketAddress, opts.LocalCloudMode == StartMode) + localWebsites := websites.NewLocalWebsitesService(localGateway.GetApiAddress, localGateway.GetWebsocketAddress, opts.LocalCloudMode == StartMode, hostname) return &LocalCloud{ servers: make(map[string]*server.NitricServer), diff --git a/pkg/cloud/env/env.go b/pkg/cloud/env/env.go index e25013d2e..6eca232af 100644 --- a/pkg/cloud/env/env.go +++ b/pkg/cloud/env/env.go @@ -37,3 +37,7 @@ var ( ) var MAX_WORKERS = env.GetEnv("MAX_WORKERS", "300") + +// The hostname that the local Nitric backend is reachable at. +// Override when the backend runs in a container and callers address it by container name/IP. +var NITRIC_HOSTNAME = env.GetEnv("NITRIC_HOSTNAME", "localhost") diff --git a/pkg/cloud/gateway/gateway.go b/pkg/cloud/gateway/gateway.go index fd857609b..fcb7020ac 100644 --- a/pkg/cloud/gateway/gateway.go +++ b/pkg/cloud/gateway/gateway.go @@ -96,6 +96,7 @@ type LocalGatewayService struct { serviceListener net.Listener localConfig localconfig.LocalConfiguration + hostname string logWriter io.Writer @@ -114,7 +115,7 @@ var _ gateway.GatewayService = &LocalGatewayService{} // GetTriggerAddress - Returns the base address built-in nitric services, like schedules and topics, will be exposed on. func (s *LocalGatewayService) GetTriggerAddress() string { if s.serviceListener != nil { - return strings.Replace(s.serviceListener.Addr().String(), "[::]", "localhost", 1) + return strings.Replace(s.serviceListener.Addr().String(), "[::]", s.hostname, 1) } return "" @@ -134,7 +135,7 @@ func (s *LocalGatewayService) GetApiAddresses() map[string]string { protocol = "https" } - address := strings.Replace(srv.lis.Addr().String(), "[::]", "localhost", 1) + address := strings.Replace(srv.lis.Addr().String(), "[::]", s.hostname, 1) addresses[srv.name] = fmt.Sprintf("%s://%s", protocol, address) } @@ -182,7 +183,7 @@ func (s *LocalGatewayService) GetHttpWorkerAddresses() map[string]string { protocol = "https" } - address := strings.Replace(srv.lis.Addr().String(), "[::]", "localhost", 1) + address := strings.Replace(srv.lis.Addr().String(), "[::]", s.hostname, 1) addresses[srv.name] = fmt.Sprintf("%s://%s", protocol, address) } @@ -199,7 +200,7 @@ func (s *LocalGatewayService) GetWebsocketAddresses() map[string]string { for socket, srv := range s.socketServer { if srv.workerCount > 0 { - srvAddress := strings.Replace(srv.lis.Addr().String(), "[::]", "localhost", 1) + srvAddress := strings.Replace(srv.lis.Addr().String(), "[::]", s.hostname, 1) addresses[socket] = srvAddress } } @@ -932,16 +933,23 @@ type NewGatewayOpts struct { LogWriter io.Writer LocalConfig localconfig.LocalConfiguration BatchPlugin *batch.LocalBatchService + Hostname string } // Create new HTTP gateway // XXX: No External Args for function atm (currently the plugin loader does not pass any argument information) func NewGateway(opts NewGatewayOpts) (*LocalGatewayService, error) { + hostname := opts.Hostname + if hostname == "" { + hostname = "localhost" + } + return &LocalGatewayService{ ApiTlsCredentials: opts.TLSCredentials, bus: EventBus.New(), logWriter: opts.LogWriter, localConfig: opts.LocalConfig, batchPlugin: opts.BatchPlugin, + hostname: hostname, }, nil } diff --git a/pkg/cloud/storage/storage.go b/pkg/cloud/storage/storage.go index 5547b9c0f..92c1989da 100644 --- a/pkg/cloud/storage/storage.go +++ b/pkg/cloud/storage/storage.go @@ -85,6 +85,7 @@ type LocalStorageService struct { listeners State storageListener net.Listener + hostname string bus EventBus.Bus } @@ -472,11 +473,13 @@ func (r *LocalStorageService) PreSignUrl(ctx context.Context, req *storagepb.Sto // XXX: Do not URL encode keys (path needs to be preserved) // TODO: May need to re-write slashes to a non-escapable character format + port := r.storageListener.Addr().(*net.TCPAddr).Port + switch req.Operation { case storagepb.StoragePreSignUrlRequest_WRITE: - address = fmt.Sprintf("http://localhost:%d/write/%s", r.storageListener.Addr().(*net.TCPAddr).Port, tokenString) + address = fmt.Sprintf("http://%s:%d/write/%s", r.hostname, port, tokenString) case storagepb.StoragePreSignUrlRequest_READ: - address = fmt.Sprintf("http://localhost:%d/read/%s", r.storageListener.Addr().(*net.TCPAddr).Port, tokenString) + address = fmt.Sprintf("http://%s:%d/read/%s", r.hostname, port, tokenString) } if address == "" { @@ -491,6 +494,7 @@ func (r *LocalStorageService) PreSignUrl(ctx context.Context, req *storagepb.Sto type StorageOptions struct { AccessKey string SecretKey string + Hostname string } func corsMiddleware(next http.Handler) http.Handler { @@ -511,8 +515,14 @@ func corsMiddleware(next http.Handler) http.Handler { func NewLocalStorageService(opts StorageOptions) (*LocalStorageService, error) { var err error + hostname := opts.Hostname + if hostname == "" { + hostname = "localhost" + } + storageService := &LocalStorageService{ listeners: map[string]map[string]int{}, + hostname: hostname, bus: EventBus.New(), } diff --git a/pkg/cloud/websites/websites.go b/pkg/cloud/websites/websites.go index 26afb3bef..76d22aef1 100644 --- a/pkg/cloud/websites/websites.go +++ b/pkg/cloud/websites/websites.go @@ -62,6 +62,7 @@ type LocalWebsiteService struct { getApiAddress GetApiAddress getWebsocketAddress GetApiAddress isStartCmd bool + hostname string bus EventBus.Bus } @@ -99,7 +100,7 @@ func (l *LocalWebsiteService) register(website Website, port int) { defer l.websiteRegLock.Unlock() // Emulates the CDN URL used in a deployed environment - publicUrl := fmt.Sprintf("http://localhost:%d/%s", port, strings.TrimPrefix(website.BasePath, "/")) + publicUrl := fmt.Sprintf("http://%s:%d/%s", l.hostname, port, strings.TrimPrefix(website.BasePath, "/")) l.state[website.Name] = Website{ WebsitePb: website.WebsitePb, @@ -397,12 +398,17 @@ func (l *LocalWebsiteService) Start(websites []Website) error { return nil } -func NewLocalWebsitesService(getApiAddress GetApiAddress, getWebsocketAddress GetApiAddress, isStartCmd bool) *LocalWebsiteService { +func NewLocalWebsitesService(getApiAddress GetApiAddress, getWebsocketAddress GetApiAddress, isStartCmd bool, hostname string) *LocalWebsiteService { + if hostname == "" { + hostname = "localhost" + } + return &LocalWebsiteService{ state: State{}, bus: EventBus.New(), getApiAddress: getApiAddress, getWebsocketAddress: getWebsocketAddress, isStartCmd: isStartCmd, + hostname: hostname, } }