diff --git a/cmd.go b/cmd.go new file mode 100644 index 0000000..5977f37 --- /dev/null +++ b/cmd.go @@ -0,0 +1,51 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + + "github.com/spf13/cobra" +) + +func init() { + // rootCmd + rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/.bottin.agendas.yaml)") + + if err := BindFlags(rootCmd.PersistentFlags()); err != nil { + log.Fatal(err) + } +} + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "agendas", + //Args: cobra.(0), + Short: "Gestion de distribution d'agendas", + Run: func(c *cobra.Command, args []string) { + log.Println("Starting bottin agendas microservice") + + ctx := context.TODO() + + cfg, err := ParseConfig() + if err != nil { + log.Fatal(err) + } + + if len(args) > 0 { + if args[0] == "debugConfig" { + log.Println("debug: Printing current config") + cfgAsBytes, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + log.Fatal("Cannot marshal config for printing:", err) + } + fmt.Println(string(cfgAsBytes)) + } + } + + if err := run(ctx, cfg); err != nil { + log.Fatal(err) + } + }, +} diff --git a/config.go b/config.go index 3bfde7a..665e1d1 100644 --- a/config.go +++ b/config.go @@ -1,7 +1,81 @@ package main -import "git.agecem.com/bottin/bottin/v10/pkg/bottin" +import ( + "fmt" + "os" + "strings" + + "git.agecem.com/bottin/bottin/v10/pkg/bottin" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) type Config struct { + // Bottin holds a client config to contact a bottin API server + // Cannot be set using environment variables Bottin bottin.APIClientConfig + + // TLS holds options for TLS / SSL / HTTPS + TLS struct { + // Cert holds the public certificate (or path to a file containing one) for user interface TLS + Cert string + + // Key holds the private key (or path to a file containing one) for user interface TLS + Key string + + // Enabled holds the TLS activation state + Enabled bool + } + + // Credentials holds username-password pairs for basic authentication + Credentials map[string]string + + // Port holds the port on which to expose the user interface + Port int +} + +func ParseConfig() (cfg Config, err error) { + return cfg, viper.Unmarshal(&cfg) +} + +var cfgFile string + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Find home directory. + home, err := os.UserHomeDir() + cobra.CheckErr(err) + + // Search config in home directory with name ".bottin.agendas" (without extension). + viper.AddConfigPath(home) + viper.SetConfigType("yaml") + viper.SetConfigName(".bottin.agendas") + } + + // Read in env, will find matching viper bindings in flag.go [BindFlags] + viper.SetEnvPrefix("AGENDAS") + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + viper.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) + } +} + +func init() { + cobra.OnInitialize(initConfig) +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } } diff --git a/flag.go b/flag.go new file mode 100644 index 0000000..9ff9c91 --- /dev/null +++ b/flag.go @@ -0,0 +1,51 @@ +package main + +import ( + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +const ( + configPort = "Port" + defaultPort = 3333 + flagPort = "port" + + configTLSEnabled = "TLS.Enabled" + defaultTLSEnabled = true + flagTLSEnabled = "tls-enabled" + + configTLSCert = "TLS.Cert" + defaultTLSCert = "" + flagTLSCert = "tls-cert" + + configTLSKey = "TLS.Key" + defaultTLSKey = "" + flagTLSKey = "tls-key" +) + +// BindClientFlags declares client-related flags and config options in the specified *pflag.FlagSet +func BindFlags(set *pflag.FlagSet) error { + // Credentials -> seulement par config + + set.Int(flagPort, defaultPort, "User interface port") + if err := viper.BindPFlag(configPort, set.Lookup(flagPort)); err != nil { + return err + } + + set.Bool(flagTLSEnabled, defaultTLSEnabled, "User interface TLS state") + if err := viper.BindPFlag(configTLSEnabled, set.Lookup(flagTLSEnabled)); err != nil { + return err + } + + set.String(flagTLSKey, defaultTLSKey, "User interface TLS private key (or path to file)") + if err := viper.BindPFlag(configTLSKey, set.Lookup(flagTLSKey)); err != nil { + return err + } + + set.String(flagTLSCert, defaultTLSCert, "User interface TLS certificate (or path to file)") + if err := viper.BindPFlag(configTLSCert, set.Lookup(flagTLSCert)); err != nil { + return err + } + + return nil +} diff --git a/go.mod b/go.mod index 4c65e6e..089ecbd 100644 --- a/go.mod +++ b/go.mod @@ -6,13 +6,16 @@ require ( codeberg.org/vlbeaudoin/voki/v3 v3.0.1 git.agecem.com/bottin/bottin/v10 v10.4.1 github.com/labstack/echo/v4 v4.13.3 - golang.org/x/term v0.27.0 + github.com/spf13/cobra v1.8.1 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.19.0 ) require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.1 // indirect @@ -28,8 +31,6 @@ require ( github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.19.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect diff --git a/go.sum b/go.sum index b0c9def..8f11c0b 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,7 @@ codeberg.org/vlbeaudoin/voki/v3 v3.0.1 h1:pFjd/ZKsu4eOzRJYViE9F1S3RglSkAuIiqCo9I codeberg.org/vlbeaudoin/voki/v3 v3.0.1/go.mod h1:+6LMXosAu2ijNKV04sMwkeujpH+cghZU1fydqj2y95g= git.agecem.com/bottin/bottin/v10 v10.4.1 h1:mRZfqnLhGN9Qb+iQvfOvZM5pxXqSlkiXqIMX59zPAS8= git.agecem.com/bottin/bottin/v10 v10.4.1/go.mod h1:KTwlqY5XdVi9F7cpwy3hxYN1DQm+74tBv7Wc9rfKXuM= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -16,6 +17,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -48,6 +51,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= @@ -58,6 +62,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= @@ -96,8 +102,6 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= diff --git a/main.go b/main.go index d55e62b..d8ea5fa 100644 --- a/main.go +++ b/main.go @@ -2,38 +2,16 @@ package main import ( "context" - "fmt" - "log" "net/http" "codeberg.org/vlbeaudoin/voki/v3" "git.agecem.com/bottin/bottin/v10/pkg/bottin" - "golang.org/x/term" ) // Entry - -func init() {} - func main() { - cfg := Config{} - cfg.Bottin.Host = "api.bottin.agecem.com" - cfg.Bottin.Port = 443 - cfg.Bottin.TLS.Enabled = true - - ctx := context.TODO() - - fmt.Println("bottin password (no echo): ") - password, err := term.ReadPassword(0) - if err != nil { - log.Fatal(err) - } - - cfg.Bottin.Key = string(password) - - if err := run(ctx, cfg); err != nil { - log.Fatal(err) - } + // Start commandline parsing then call `run` + Execute() } func run(ctx context.Context, cfg Config) error { @@ -41,23 +19,21 @@ func run(ctx context.Context, cfg Config) error { case <-ctx.Done(): return ctx.Err() default: - var protocol string - - if cfg.Bottin.TLS.Enabled { - protocol = "https" - } else { - protocol = "http" - } - bottinClient := bottin.APIClient{Caller: voki.New( http.DefaultClient, cfg.Bottin.Host, cfg.Bottin.Key, cfg.Bottin.Port, - protocol, + func() string { + if cfg.Bottin.TLS.Enabled { + return "https" + } else { + return "http" + } + }(), )} - if err := RunServer(ctx, &bottinClient); err != nil && err != http.ErrServerClosed { + if err := RunServer(ctx, cfg, &bottinClient); err != nil && err != http.ErrServerClosed { return err } diff --git a/server.go b/server.go index 93c2352..89fc41e 100644 --- a/server.go +++ b/server.go @@ -4,12 +4,13 @@ import ( "context" "fmt" + "git.agecem.com/bottin/agendas/ui" "git.agecem.com/bottin/bottin/v10/pkg/bottin" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) -func RunServer(ctx context.Context, bottinClient *bottin.APIClient) error { +func RunServer(ctx context.Context, cfg Config, bottinClient *bottin.APIClient) error { select { case <-ctx.Done(): return ctx.Err() @@ -20,14 +21,23 @@ func RunServer(ctx context.Context, bottinClient *bottin.APIClient) error { e := echo.New() - e.Renderer = NewRenderer() + e.Renderer = ui.NewRenderer() e.Pre(middleware.AddTrailingSlash()) + //TODO basic auth + //TODO log successful basic auths username + e.GET("/", UIIndex(ctx, bottinClient)) e.GET("/membre/", UIReadMembre(ctx, bottinClient)) - return e.Start(":3333") + address := fmt.Sprintf(":%d", cfg.Port) + + if cfg.TLS.Enabled { + return e.StartTLS(address, cfg.TLS.Cert, cfg.TLS.Key) + } else { + return e.Start(address) + } } } diff --git a/ui/embed.go b/ui/embed.go index c10c63b..f757d26 100644 --- a/ui/embed.go +++ b/ui/embed.go @@ -4,7 +4,3 @@ import "embed" //go:embed *.html var htmlFS embed.FS - -func HTMLFS() embed.FS { - return htmlFS -} diff --git a/render.go b/ui/render.go similarity index 73% rename from render.go rename to ui/render.go index a3ebbc6..f4c08c3 100644 --- a/render.go +++ b/ui/render.go @@ -1,10 +1,9 @@ -package main +package ui import ( "io" "text/template" - "git.agecem.com/bottin/agendas/ui" "github.com/labstack/echo/v4" ) @@ -18,6 +17,6 @@ func (t *Renderer) Render(w io.Writer, name string, data any, c echo.Context) er func NewRenderer() *Renderer { return &Renderer{ - templates: template.Must(template.ParseFS(ui.HTMLFS(), "*html")), + templates: template.Must(template.ParseFS(htmlFS, "*html")), } }