diff --git a/.gitignore b/.gitignore index 343b19865..3a6f322af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -cmd/soft/soft -cmd/soft/.ssh -cmd/soft/.repos +soft +.ssh +.repos diff --git a/README.md b/README.md index a75ed4560..45c0f8fc8 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ The Soft Serve command is called `soft`. You can build and install it with ```bash git clone ssh://beta.charm.sh/soft-serve -cd soft-serve/cmd/soft +cd soft-serve go install ``` diff --git a/cmd/soft/main.go b/cmd/soft/main.go deleted file mode 100644 index d56324950..000000000 --- a/cmd/soft/main.go +++ /dev/null @@ -1,17 +0,0 @@ -package main - -import ( - "log" - - "github.com/charmbracelet/soft" -) - -func main() { - cfg := soft.DefaultConfig() - s := soft.NewServer(cfg) - log.Printf("Starting SSH server on %s:%d\n", cfg.Host, cfg.Port) - err := s.ListenAndServe() - if err != nil { - log.Fatalln(err) - } -} diff --git a/config/config.go b/config/config.go new file mode 100644 index 000000000..160e71120 --- /dev/null +++ b/config/config.go @@ -0,0 +1,34 @@ +package config + +import ( + "log" + + "github.com/charmbracelet/soft/stats" + "github.com/meowgorithm/babyenv" +) + +// Config is the configuration for the soft-serve. +type Config struct { + Host string `env:"SOFT_SERVE_HOST" default:""` + Port int `env:"SOFT_SERVE_PORT" default:"23231"` + KeyPath string `env:"SOFT_SERVE_KEY_PATH" default:".ssh/soft_serve_server_ed25519"` + RepoPath string `env:"SOFT_SERVE_REPO_PATH" default:".repos"` + InitialAdminKey string `env:"SOFT_SERVE_INITIAL_ADMIN_KEY" default:""` + Stats stats.Stats +} + +// DefaultConfig returns a Config with the values populated with the defaults +// or specified environment variables. +func DefaultConfig() *Config { + var scfg Config + err := babyenv.Parse(&scfg) + if err != nil { + log.Fatalln(err) + } + return scfg.WithStats(stats.NewStats()) +} + +func (cfg *Config) WithStats(s stats.Stats) *Config { + cfg.Stats = s + return cfg +} diff --git a/internal/config/config.go b/internal/config/config.go index cd71ee9f3..9c7f1cfb8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -9,8 +9,8 @@ import ( "os" "path/filepath" + "github.com/charmbracelet/soft/config" "github.com/charmbracelet/soft/internal/git" - "github.com/charmbracelet/soft/stats" gg "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" ) @@ -24,7 +24,7 @@ type Config struct { Users []User `yaml:"users"` Repos []Repo `yaml:"repos"` Source *git.RepoSource - Stats stats.Stats + Cfg *config.Config } type User struct { @@ -41,19 +41,20 @@ type Repo struct { Private bool `yaml:"private"` } -func (cfg *Config) WithStats(s stats.Stats) *Config { - cfg.Stats = s - return cfg -} - -func NewConfig(host string, port int, pk string, rs *git.RepoSource) (*Config, error) { +func NewConfig(cfg *config.Config) (*Config, error) { var anonAccess string var yamlUsers string var displayHost string - cfg := &Config{} - cfg.Host = host - cfg.Port = port - cfg.Source = rs + host := cfg.Host + port := cfg.Port + pk := cfg.InitialAdminKey + rs := git.NewRepoSource(cfg.RepoPath) + c := &Config{ + Cfg: cfg, + } + c.Host = cfg.Host + c.Port = port + c.Source = rs if pk == "" { anonAccess = "read-write" } else { @@ -75,11 +76,11 @@ func NewConfig(host string, port int, pk string, rs *git.RepoSource) (*Config, e yamlUsers = defaultUserConfig } yaml := fmt.Sprintf("%s%s%s", yamlConfig, yamlUsers, exampleUserConfig) - err := cfg.createDefaultConfigRepo(yaml) + err := c.createDefaultConfigRepo(yaml) if err != nil { return nil, err } - return cfg, nil + return c, nil } func (cfg *Config) reload() error { diff --git a/internal/config/git.go b/internal/config/git.go index 40ded971a..845c2fb8a 100644 --- a/internal/config/git.go +++ b/internal/config/git.go @@ -9,21 +9,15 @@ import ( ) func (cfg *Config) Push(repo string, pk ssh.PublicKey) { - log.Printf("git push: %s", repo) err := cfg.reload() if err != nil { log.Printf("error reloading after push: %s", err) } - if cfg.Stats != nil { - cfg.Stats.Push() - } + cfg.Cfg.Stats.Push(repo) } func (cfg *Config) Fetch(repo string, pk ssh.PublicKey) { - log.Printf("git fetch: %s", repo) - if cfg.Stats != nil { - cfg.Stats.Fetch() - } + cfg.Cfg.Stats.Fetch(repo) } func (cfg *Config) AuthRepo(repo string, pk ssh.PublicKey) gm.AccessLevel { diff --git a/internal/tui/bubbles/commits/bubble.go b/internal/tui/bubbles/commits/bubble.go index 662304a11..205daa701 100644 --- a/internal/tui/bubbles/commits/bubble.go +++ b/internal/tui/bubbles/commits/bubble.go @@ -1,11 +1,11 @@ package commits import ( - "soft-serve/server/git" "strings" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/soft/internal/git" "github.com/dustin/go-humanize" ) diff --git a/internal/tui/session.go b/internal/tui/session.go index 8bdd5c5d5..9f3cb3951 100644 --- a/internal/tui/session.go +++ b/internal/tui/session.go @@ -27,9 +27,7 @@ func SessionHandler(cfg *config.Config) func(ssh.Session) (tea.Model, []tea.Prog } scfg.Width = pty.Window.Width scfg.Height = pty.Window.Height - if cfg.Stats != nil { - cfg.Stats.Tui() - } + cfg.Cfg.Stats.Tui("view") return NewBubble(cfg, scfg), []tea.ProgramOption{tea.WithAltScreen()} } } diff --git a/main.go b/main.go new file mode 100644 index 000000000..f6052c285 --- /dev/null +++ b/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "log" + + "github.com/charmbracelet/soft/config" + "github.com/charmbracelet/soft/server" +) + +func main() { + cfg := config.DefaultConfig() + s := server.NewServer(cfg) + log.Printf("Starting SSH server on %s:%d\n", cfg.Host, cfg.Port) + err := s.Start() + if err != nil { + log.Fatalln(err) + } +} diff --git a/server.go b/server.go deleted file mode 100644 index 51fcf2b9d..000000000 --- a/server.go +++ /dev/null @@ -1,73 +0,0 @@ -package soft - -import ( - "fmt" - "log" - - "github.com/charmbracelet/soft/internal/config" - "github.com/charmbracelet/soft/internal/git" - "github.com/charmbracelet/soft/internal/tui" - "github.com/charmbracelet/soft/stats" - "github.com/meowgorithm/babyenv" - - "github.com/charmbracelet/wish" - bm "github.com/charmbracelet/wish/bubbletea" - gm "github.com/charmbracelet/wish/git" - lm "github.com/charmbracelet/wish/logging" - "github.com/gliderlabs/ssh" -) - -// Config is the configuration for the soft-serve. -type Config struct { - Host string `env:"SOFT_SERVE_HOST" default:""` - Port int `env:"SOFT_SERVE_PORT" default:"23231"` - KeyPath string `env:"SOFT_SERVE_KEY_PATH" default:".ssh/soft_serve_server_ed25519"` - RepoPath string `env:"SOFT_SERVE_REPO_PATH" default:".repos"` - InitialAdminKey string `env:"SOFT_SERVE_INITIAL_ADMIN_KEY" default:""` - Stats stats.Stats - cfg *config.Config -} - -// DefaultConfig returns a Config with the values populated with the defaults -// or specified environment variables. -func DefaultConfig() *Config { - var scfg Config - err := babyenv.Parse(&scfg) - if err != nil { - log.Fatalln(err) - } - return &scfg -} - -// NewServer returns a new *ssh.Server configured to serve Soft Serve. The SSH -// server key-pair will be created if none exists. An initial admin SSH public -// key can be provided with authKey. If authKey is provided, access will be -// restricted to that key. If authKey is not provided, the server will be -// publicly writable until configured otherwise by cloning the `config` repo. -func NewServer(scfg *Config) *ssh.Server { - rs := git.NewRepoSource(scfg.RepoPath) - cfg, err := config.NewConfig(scfg.Host, scfg.Port, scfg.InitialAdminKey, rs) - if err != nil { - log.Fatalln(err) - } - if scfg.Stats != nil { - cfg = cfg.WithStats(scfg.Stats) - } - scfg.cfg = cfg - mw := []wish.Middleware{ - bm.Middleware(tui.SessionHandler(cfg)), - gm.Middleware(scfg.RepoPath, cfg), - lm.Middleware(), - } - s, err := wish.NewServer( - ssh.PublicKeyAuth(cfg.PublicKeyHandler), - ssh.PasswordAuth(cfg.PasswordHandler), - wish.WithAddress(fmt.Sprintf("%s:%d", scfg.Host, scfg.Port)), - wish.WithHostKeyPath(scfg.KeyPath), - wish.WithMiddleware(mw...), - ) - if err != nil { - log.Fatalln(err) - } - return s -} diff --git a/server/server.go b/server/server.go new file mode 100644 index 000000000..f767abc8d --- /dev/null +++ b/server/server.go @@ -0,0 +1,56 @@ +package server + +import ( + "fmt" + "log" + + "github.com/charmbracelet/soft/config" + appCfg "github.com/charmbracelet/soft/internal/config" + "github.com/charmbracelet/soft/internal/tui" + + "github.com/charmbracelet/wish" + bm "github.com/charmbracelet/wish/bubbletea" + gm "github.com/charmbracelet/wish/git" + lm "github.com/charmbracelet/wish/logging" + "github.com/gliderlabs/ssh" +) + +type Server struct { + SSHServer *ssh.Server + Config *config.Config +} + +// NewServer returns a new *ssh.Server configured to serve Soft Serve. The SSH +// server key-pair will be created if none exists. An initial admin SSH public +// key can be provided with authKey. If authKey is provided, access will be +// restricted to that key. If authKey is not provided, the server will be +// publicly writable until configured otherwise by cloning the `config` repo. +func NewServer(cfg *config.Config) *Server { + ac, err := appCfg.NewConfig(cfg) + if err != nil { + log.Fatal(err) + } + mw := []wish.Middleware{ + bm.Middleware(tui.SessionHandler(ac)), + gm.Middleware(cfg.RepoPath, ac), + lm.Middleware(), + } + s, err := wish.NewServer( + ssh.PublicKeyAuth(ac.PublicKeyHandler), + ssh.PasswordAuth(ac.PasswordHandler), + wish.WithAddress(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)), + wish.WithHostKeyPath(cfg.KeyPath), + wish.WithMiddleware(mw...), + ) + if err != nil { + log.Fatalln(err) + } + return &Server{ + SSHServer: s, + Config: cfg, + } +} + +func (srv *Server) Start() error { + return srv.SSHServer.ListenAndServe() +} diff --git a/stats/stats.go b/stats/stats.go index 847cce8bb..b9eb71736 100644 --- a/stats/stats.go +++ b/stats/stats.go @@ -1,8 +1,28 @@ package stats +import "log" + // Stats provides an interface that can be used to collect metrics about the server. type Stats interface { - Tui() - Push() - Fetch() + Tui(action string) + Push(repo string) + Fetch(repo string) +} + +type stats struct{} + +func (s *stats) Tui(action string) { + log.Printf("TUI: %s", action) +} + +func (s *stats) Push(repo string) { + log.Printf("git push: %s", repo) +} + +func (s *stats) Fetch(repo string) { + log.Printf("git fetch: %s", repo) +} + +func NewStats() Stats { + return &stats{} }