diff --git a/internal/app/app.go b/internal/app/app.go index 7a7a3b1..5a1092f 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -340,7 +340,7 @@ const ( DOCKERFILE = "Dockerfile" ) -func (a *App) loadContainerManager(dryRun DryRun) error { +func (a *App) loadContainerManager(stripAppPath bool) error { containerConfig, err := a.appDef.Attr("container") if err != nil || containerConfig == starlark.None { // Plugin not authorized, skip any container files @@ -447,7 +447,7 @@ func (a *App) loadContainerManager(dryRun DryRun) error { } a.containerManager, err = NewContainerManager(a.Logger, a, - fileName, a.systemConfig, port, lifetime, scheme, health, buildDir, a.sourceFS, a.paramMap, a.appConfig.Container) + fileName, a.systemConfig, port, lifetime, scheme, health, buildDir, a.sourceFS, a.paramMap, a.appConfig.Container, stripAppPath) if err != nil { return fmt.Errorf("error creating container manager: %w", err) } diff --git a/internal/app/container_manager.go b/internal/app/container_manager.go index 688c3b9..a9d7f5c 100644 --- a/internal/app/container_manager.go +++ b/internal/app/container_manager.go @@ -61,11 +61,12 @@ type ContainerManager struct { // Health check related fields healthCheckTicker *time.Ticker + stripAppPath bool } func NewContainerManager(logger *types.Logger, app *App, containerFile string, systemConfig *types.SystemConfig, configPort int64, lifetime, scheme, health, buildDir string, sourceFS appfs.ReadableFS, - paramMap map[string]string, containerConfig types.Container) (*ContainerManager, error) { + paramMap map[string]string, containerConfig types.Container, stripAppPath bool) (*ContainerManager, error) { image := "" volumes := []string{} @@ -140,6 +141,7 @@ func NewContainerManager(logger *types.Logger, app *App, containerFile string, containerConfig: containerConfig, stateLock: sync.RWMutex{}, currentState: ContainerStateUnknown, + stripAppPath: stripAppPath, } if containerConfig.IdleShutdownSecs > 0 && (!app.IsDev || containerConfig.IdleShutdownDevApps) { @@ -444,17 +446,29 @@ func (m *ContainerManager) WaitForHealth(attempts int) error { var err error var resp *http.Response + url := m.GetProxyUrl() + if !m.stripAppPath { + // Apps like Streamlit require the app path to be present + url = url + m.app.Path + } + + url += m.health + for attempt := 1; attempt <= attempts; attempt++ { - resp, err = client.Get(m.GetProxyUrl() + m.health) - if err == nil && resp.StatusCode == http.StatusOK { - return nil + resp, err = client.Get(url) + statusCode := "N/A" + if err == nil { + if resp.StatusCode == http.StatusOK { + return nil + } + statusCode = strconv.Itoa(resp.StatusCode) } if resp != nil { resp.Body.Close() } - m.Debug().Msgf("Attempt %d failed: %s", attempt, err) + m.Debug().Msgf("Attempt %d failed on %s : status %s err %s", attempt, url, statusCode, err) time.Sleep(1 * time.Second) } return err diff --git a/internal/app/setup.go b/internal/app/setup.go index 706fb53..60bbb1d 100644 --- a/internal/app/setup.go +++ b/internal/app/setup.go @@ -110,8 +110,13 @@ func (a *App) loadStarlarkConfig(dryRun DryRun) error { return err } + var stripAppPath bool + if stripAppPath, err = a.checkAppPathStripping(); err != nil { + return err + } + // Load container config. The proxy config in routes depends on this being loaded first - if err = a.loadContainerManager(dryRun); err != nil { + if err = a.loadContainerManager(stripAppPath); err != nil { return err } @@ -318,6 +323,63 @@ func verifyConfig(globals starlark.StringDict) (*starlarkstruct.Struct, error) { return appDef, nil } +// checkAppPathStripping checks if the app path should be stripped from the request path for container proxying +// This is required for container health checks. +func (a *App) checkAppPathStripping() (bool, error) { + appPathStripping := true + // Iterate through all the routes + routes, err := a.appDef.Attr("routes") + if err != nil { + return false, err + } + + var ok bool + var routeList *starlark.List + if routeList, ok = routes.(*starlark.List); !ok { + return false, fmt.Errorf("routes is not a list") + } + + iter := routeList.Iterate() + var val starlark.Value + var count int + + for iter.Next(&val) { + count += 1 + var pageDef *starlarkstruct.Struct + if pageDef, ok = val.(*starlarkstruct.Struct); !ok { + return false, fmt.Errorf("routes entry %d is not a struct", count) + } + + _, err := pageDef.Attr("config") + if err == nil { + // "config" is defined, this must be a proxy config instead of a page definition + var configAttr starlark.HasAttrs + if configAttr, err = getProxyConfig(count, pageDef); err != nil { + return false, err + } + + var urlValue starlark.Value + if urlValue, err = configAttr.Attr("Url"); err != nil { + return false, err + } + + if urlValue.(starlark.String).GoString() != apptype.CONTAINER_URL { + // Not proxying to container url, ignore + continue + } + + var stripAppValue starlark.Value + if stripAppValue, err = configAttr.Attr("StripApp"); err != nil { + return false, err + } + + return bool(stripAppValue.(starlark.Bool)), nil + } + } + + return appPathStripping, nil +} + func (a *App) initRouter() error { var defaultHandler starlark.Callable if a.globals.Has(apptype.DEFAULT_HANDLER) { @@ -488,57 +550,74 @@ func (a *App) addRoute(count int, router *chi.Mux, routeVal starlark.Value, defa return rootWildcard, nil } -func (a *App) addProxyConfig(count int, router *chi.Mux, proxyDef *starlarkstruct.Struct) (bool, error) { +// getProxyConfig extracts the proxy config from the proxy definition +func getProxyConfig(count int, proxyDef *starlarkstruct.Struct) (starlark.HasAttrs, error) { var err error var pathStr string - rootWildcard := false if pathStr, err = apptype.GetStringAttr(proxyDef, "path"); err != nil { - return rootWildcard, err - } - - if pathStr == "/" { - rootWildcard = true // Root wildcard path, static files are not served + return nil, err } var ok bool var responseAttr starlark.HasAttrs pluginResponse, err := proxyDef.Attr("config") if err != nil { - return rootWildcard, err + return nil, err } if responseAttr, ok = pluginResponse.(starlark.HasAttrs); !ok { - return rootWildcard, fmt.Errorf("proxy entry %d:%s is not a proxy response", count, pathStr) + return nil, fmt.Errorf("proxy entry %d:%s is not a proxy response", count, pathStr) } errorValue, err := responseAttr.Attr("error") if err != nil { - return rootWildcard, fmt.Errorf("error in proxy config: %w", err) + return nil, fmt.Errorf("error in proxy config: %w", err) } if errorValue != nil && errorValue != starlark.None { var errorString starlark.String if errorString, ok = errorValue.(starlark.String); !ok { - return rootWildcard, fmt.Errorf("error in proxy config: %w", err) + return nil, fmt.Errorf("error in proxy config: %w", err) } if errorString.GoString() != "" { - return rootWildcard, fmt.Errorf("error in proxy config: %s", errorString.GoString()) + return nil, fmt.Errorf("error in proxy config: %s", errorString.GoString()) } } config, err := responseAttr.Attr("value") if err != nil { - return rootWildcard, err + return nil, err } if config.Type() != "ProxyConfig" { - return rootWildcard, fmt.Errorf("proxy entry %d:%s is not a proxy config", count, pathStr) + return nil, fmt.Errorf("proxy entry %d:%s is not a proxy config", count, pathStr) } var configAttr starlark.HasAttrs if configAttr, ok = config.(starlark.HasAttrs); !ok { - return rootWildcard, fmt.Errorf("proxy entry %d:%s is not a proxy config attr", count, pathStr) + return nil, fmt.Errorf("proxy entry %d:%s is not a proxy config attr", count, pathStr) + } + + return configAttr, nil +} + +func (a *App) addProxyConfig(count int, router *chi.Mux, proxyDef *starlarkstruct.Struct) (bool, error) { + var err error + var pathStr string + rootWildcard := false + + if pathStr, err = apptype.GetStringAttr(proxyDef, "path"); err != nil { + return rootWildcard, err + } + + if pathStr == "/" { + rootWildcard = true // Root wildcard path, static files are not served + } + + var configAttr starlark.HasAttrs + if configAttr, err = getProxyConfig(count, proxyDef); err != nil { + return rootWildcard, err } var urlValue, stripPathValue starlark.Value diff --git a/internal/types/types.go b/internal/types/types.go index 4bfaa02..190b8b6 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -262,8 +262,8 @@ type VersionMetadata struct { type AppEntry struct { Id AppId `json:"id"` Path string `json:"path"` - MainApp AppId `json:"main_app"` // the id of the app that this app is linked to Domain string `json:"domain"` + MainApp AppId `json:"main_app"` // the id of the app that this app is linked to SourceUrl string `json:"source_url"` IsDev bool `json:"is_dev"` UserID string `json:"user_id"`