package config import ( "errors" "fmt" "io/fs" "os" "path/filepath" "strings" ) const AppsDir = "apps.d" const maxAppsFile = 1 << 20 func readAppsDir(configPath string, cfg *Config) []Problem { dir := filepath.Join(filepath.Dir(configPath), AppsDir) entries, err := os.ReadDir(dir) if errors.Is(err, fs.ErrNotExist) { return nil } if err != nil { return []Problem{{File: dir, Reason: reasonUnreadable}} } used := map[string]bool{cfg.Server.DomainIDP: true, cfg.Server.DomainAccount: true, cfg.Server.DomainAdmin: true} for _, a := range cfg.Apps { used[a.Domain] = true } var problems []Problem for _, e := range entries { if e.IsDir() || !strings.HasSuffix(e.Name(), ".toml") { continue } file := filepath.Join(dir, e.Name()) if cfg.Source == sourceGit { problems = append(problems, Problem{File: file, Reason: reasonAppsUnderGit}) continue } data, reason := readRegular(file, maxAppsFile, reasonAppsTooLarge, nil) if reason != "" { problems = append(problems, Problem{File: file, Reason: reason}) continue } idx, keyProblems := indexKeys(file, data) problems = append(problems, keyProblems...) if idx == nil { continue } others := map[string]bool{} for path := range idx.present { if top, _, _ := strings.Cut(path, "."); !strings.HasPrefix(top, "app[") && top != "app" && !others[top] { problems = append(problems, Problem{File: file, Line: idx.line(top), Key: top, Reason: reasonAppsOnly}) others[top] = true } } raw, typed := decodeTyped(file, data, idx, keyProblems) problems = append(problems, typed...) if raw == nil || len(others) > 0 || len(keyProblems) > 0 { continue } v := &validator{file: file, idx: idx} for i, a := range raw.App { prefix := fmt.Sprintf("app[%d]", i) app := v.app(prefix, a, cfg.Session.TTL) if a.Domain != "" && used[a.Domain] { v.add(prefix+".domain", reasonDomainReused) } used[a.Domain] = true cfg.Apps = append(cfg.Apps, app) } problems = append(problems, v.problems...) } return problems }