diff --git a/Dockerfile.controller b/Dockerfile.controller index 238418d64f..75b5f9cb98 100644 --- a/Dockerfile.controller +++ b/Dockerfile.controller @@ -34,6 +34,7 @@ EXPOSE 8892 ENV FTL_CONTROLLER_BIND="http://0.0.0.0:8899" ENV FTL_CONTROLLER_ADVERTISE="http://127.0.0.1:8899" +ENV FTL_CONTROLLER_ALLOW_ORIGIN="*" ENV FTL_CONTROLLER_DSN="postgres://host.docker.internal/ftl?sslmode=disable&user=postgres&password=secret" CMD ["/root/ftl-controller"] diff --git a/backend/controller/controller.go b/backend/controller/controller.go index 1b34df2134..79928c42d3 100644 --- a/backend/controller/controller.go +++ b/backend/controller/controller.go @@ -45,7 +45,8 @@ import ( type Config struct { Bind *url.URL `help:"Socket to bind to." default:"http://localhost:8892" env:"FTL_CONTROLLER_BIND"` - Advertise *url.URL `help:"Endpoint the Controller should advertise (use --bind if omitted)." default:"" env:"FTL_CONTROLLER_ADVERTISE"` + Advertise *url.URL `help:"Endpoint the Controller should advertise (must be unique across the cluster, defaults to --bind if omitted)." env:"FTL_CONTROLLER_ADVERTISE"` + AllowOrigin string `help:"Allow CORS requests from this origin." default:"*" env:"FTL_CONTROLLER_ALLOW_ORIGIN"` Key model.ControllerKey `help:"Controller key (auto)." placeholder:"C" default:"C00000000000000000000000000"` DSN string `help:"DAL DSN." default:"postgres://localhost/ftl?sslmode=disable&user=postgres&password=secret" env:"FTL_CONTROLLER_DSN"` RunnerTimeout time.Duration `help:"Runner heartbeat timeout." default:"10s"` @@ -53,12 +54,20 @@ type Config struct { ArtefactChunkSize int `help:"Size of each chunk streamed to the client." default:"1048576"` } +func (c *Config) SetDefaults() { + if c.Advertise == nil { + c.Advertise = c.Bind + } +} + // Start the Controller. Blocks until the context is cancelled. func Start(ctx context.Context, config Config) error { + config.SetDefaults() + logger := log.FromContext(ctx) logger.Infof("Starting FTL controller") - c, err := console.Server(ctx) + c, err := console.Server(ctx, config.AllowOrigin) if err != nil { return errors.WithStack(err) } @@ -116,6 +125,7 @@ func New(ctx context.Context, db *dal.DAL, config Config) (*Service, error) { if config.Key.ULID() == (ulid.ULID{}) { key = model.NewControllerKey() } + config.SetDefaults() svc := &Service{ dal: db, key: key, @@ -124,9 +134,6 @@ func New(ctx context.Context, db *dal.DAL, config Config) (*Service, error) { routes: map[string][]dal.Route{}, config: config, } - if config.Advertise.String() == "" { - config.Advertise = config.Bind - } go runWithRetries(ctx, time.Second*1, time.Second*2, svc.syncRoutes) go runWithRetries(ctx, time.Second*3, time.Second*5, svc.heartbeatController) diff --git a/cmd/ftl-controller/main.go b/cmd/ftl-controller/main.go index 54671977a5..ee0ba7f154 100644 --- a/cmd/ftl-controller/main.go +++ b/cmd/ftl-controller/main.go @@ -7,7 +7,7 @@ import ( "github.com/alecthomas/kong" _ "github.com/TBD54566975/ftl/backend/common/automaxprocs" // Set GOMAXPROCS to match Linux container CPU quota. - log2 "github.com/TBD54566975/ftl/backend/common/log" + log "github.com/TBD54566975/ftl/backend/common/log" "github.com/TBD54566975/ftl/backend/controller" ) @@ -15,7 +15,7 @@ var version = "dev" var cli struct { Version kong.VersionFlag `help:"Show version."` - LogConfig log2.Config `embed:"" prefix:"log-"` + LogConfig log.Config `embed:"" prefix:"log-"` ControllerConfig controller.Config `embed:""` } @@ -25,7 +25,7 @@ func main() { kong.UsageOnError(), kong.Vars{"version": version}, ) - ctx := log2.ContextWithLogger(context.Background(), log2.Configure(os.Stderr, cli.LogConfig)) + ctx := log.ContextWithLogger(context.Background(), log.Configure(os.Stderr, cli.LogConfig)) err := controller.Start(ctx, cli.ControllerConfig) kctx.FatalIfErrorf(err) } diff --git a/console/cors.go b/console/cors.go new file mode 100644 index 0000000000..84ad4881b2 --- /dev/null +++ b/console/cors.go @@ -0,0 +1,11 @@ +package console + +import ( + "net/http" +) + +func writeCORSHeaders(w http.ResponseWriter, allowOrigin string) { + w.Header().Set("Access-Control-Allow-Origin", allowOrigin) + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") + w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") +} diff --git a/console/local.go b/console/local.go index f19dc937a8..6af49fbf41 100644 --- a/console/local.go +++ b/console/local.go @@ -17,7 +17,7 @@ import ( var consoleURL, _ = url.Parse("http://localhost:5173") var proxy = httputil.NewSingleHostReverseProxy(consoleURL) -func Server(ctx context.Context) (http.Handler, error) { +func Server(ctx context.Context, allowOrigin string) (http.Handler, error) { logger := log.FromContext(ctx) logger.Infof("Building console...") @@ -32,17 +32,16 @@ func Server(ctx context.Context) (http.Handler, error) { } logger.Infof("Console started") - return http.HandlerFunc(handler), nil + return http.HandlerFunc(handler(allowOrigin)), nil } -func handler(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") - w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") - - if r.Method == http.MethodOptions { - w.WriteHeader(http.StatusOK) - return +func handler(allowOrigin string) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + writeCORSHeaders(w, allowOrigin) + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + return + } + proxy.ServeHTTP(w, r) } - proxy.ServeHTTP(w, r) } diff --git a/console/release.go b/console/release.go index 9f9530561d..16e1f67775 100644 --- a/console/release.go +++ b/console/release.go @@ -18,12 +18,13 @@ import ( //go:embed all:client/dist var build embed.FS -func Server(ctx context.Context) (http.Handler, error) { +func Server(ctx context.Context, allowOrigin string) (http.Handler, error) { dir, err := fs.Sub(build, "client/dist") if err != nil { return nil, errors.WithStack(err) } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + writeCORSHeaders(w, allowOrigin) var f fs.File var err error filePath := strings.TrimPrefix(r.URL.Path, "/")