diff --git a/.gitignore b/.gitignore index a8c4332..e2f7237 100644 --- a/.gitignore +++ b/.gitignore @@ -22,8 +22,6 @@ # Dependency directories (remove the comment below to include it) # vendor/ -# cert files -*.pem # env .env diff --git a/Dockerfile b/Dockerfile index d24bb50..6479581 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,19 @@ -FROM golang:1.23.0 as build +FROM golang:1.22.3 as build LABEL author="vlbeaudoin" WORKDIR /go/src/app -COPY go.mod go.sum LICENSE ./ +COPY go.mod go.sum client.go client_test.go cmd.go config.go db.go entity.go main.go request.go response.go routes.go template.go ./ -ADD cmd/ cmd/ -ADD pkg/ pkg/ -ADD queries/ queries/ +ADD sql/ sql/ ADD templates/ templates/ -RUN CGO_ENABLED=0 go build -a -o bottin ./cmd/bottin +RUN CGO_ENABLED=0 go build -a -o bottin . # Alpine -FROM alpine:3.20.2 +FROM alpine:3.20 WORKDIR /app diff --git a/Makefile b/Makefile index 827fc0c..31a6c23 100644 --- a/Makefile +++ b/Makefile @@ -14,15 +14,3 @@ help: ## Show this help .PHONY: test-integration test-integration: ## run integration tests through API client. Config is read from `~/.bottin.yaml`. WARNING: affects data in the database, do not run on production server docker-compose down && docker-compose up -d --build && sleep 2 && go test - -.PHONY: dev -dev: generate-self-signed-x509 compose-inject-x509 ## deploy development environment on docker-compose - docker-compose up -d - -.PHONY: generate-self-signed-x509 -generate-self-signed-x509: ## Générer une paire de clés x509 self-signed pour utilisation avec un serveur de développement - ./scripts/generate-self-signed-x509.sh - -.PHONY: compose-inject-x509 -compose-inject-x509: ## Copie la paire de clés x509 du current directory vers les containers orchestrés par docker-compose - ./scripts/compose-inject-x509.sh diff --git a/README.md b/README.md index bf96431..822c4f1 100644 --- a/README.md +++ b/README.md @@ -55,4 +55,3 @@ Pour modifier la configuration du client web Redémarrer les containers une fois la configuration modifiée `$ docker-compose down && docker-compose up -d` -v diff --git a/pkg/bottin/client.go b/client.go similarity index 99% rename from pkg/bottin/client.go rename to client.go index 529d9c8..e4958a6 100644 --- a/pkg/bottin/client.go +++ b/client.go @@ -1,4 +1,4 @@ -package bottin +package main import ( "fmt" diff --git a/cmd/bottin/main_test.go b/client_test.go similarity index 91% rename from cmd/bottin/main_test.go rename to client_test.go index f8b6685..261eb20 100644 --- a/cmd/bottin/main_test.go +++ b/client_test.go @@ -6,7 +6,6 @@ import ( "testing" "codeberg.org/vlbeaudoin/voki/v3" - "git.agecem.com/agecem/bottin/v9/pkg/bottin" "github.com/spf13/viper" ) @@ -15,7 +14,7 @@ func init() { } func TestAPI(t *testing.T) { - var cfg bottin.Config + var cfg Config if err := viper.Unmarshal(&cfg); err != nil { t.Error(err) return @@ -36,7 +35,7 @@ func TestAPI(t *testing.T) { defer httpClient.CloseIdleConnections() vokiClient := voki.New(&httpClient, "localhost", cfg.Client.API.Key, cfg.Client.API.Port, cfg.Client.API.Protocol) - apiClient := bottin.APIClient{Voki: vokiClient} + apiClient := APIClient{vokiClient} t.Run("get API health", func(t *testing.T) { health, err := apiClient.GetHealth() @@ -54,9 +53,9 @@ func TestAPI(t *testing.T) { t.Run("insert programmes", func(t *testing.T) { - programmes := []bottin.Programme{ - {ID: "404.42", Name: "Cool programme"}, - {ID: "200.10", Name: "Autre programme"}, + programmes := []Programme{ + {"404.42", "Cool programme"}, + {"200.10", "Autre programme"}, } t.Log("programmes:", programmes) _, err := apiClient.InsertProgrammes(programmes...) @@ -65,7 +64,7 @@ func TestAPI(t *testing.T) { } }) - testMembres := []bottin.Membre{ + testMembres := []Membre{ { ID: "0000000", FirstName: "Test", diff --git a/cmd.go b/cmd.go new file mode 100644 index 0000000..3fe7a06 --- /dev/null +++ b/cmd.go @@ -0,0 +1,257 @@ +package main + +import ( + "context" + "crypto/subtle" + "fmt" + "html/template" + "log" + "net/http" + "os" + + "codeberg.org/vlbeaudoin/voki/v3" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "bottin", + Short: "Bottin étudiant de l'AGECEM", +} + +// 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) + } +} + +var serverCmd = &cobra.Command{ + Use: "server", + Short: "Démarrer serveurs (API ou Web UI)", +} + +// apiCmd represents the api command +var apiCmd = &cobra.Command{ + Use: "api", + Short: "Démarrer le serveur API", + Args: cobra.ExactArgs(0), + Run: func(cmd *cobra.Command, args []string) { + var cfg Config + if err := viper.Unmarshal(&cfg); err != nil { + log.Fatal("parse config:", err) + } + + e := echo.New() + + // Middlewares + + e.Pre(middleware.AddTrailingSlash()) + + if cfg.Server.API.Key != "" { + e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) { + return subtle.ConstantTimeCompare([]byte(key), []byte(cfg.Server.API.Key)) == 1, nil + })) + } else { + log.Println("Server started but no API key (server.api.key) was provided, using empty key (NOT RECOMMENDED FOR PRODUCTION)") + } + + // DataClient + ctx := context.Background() + + //prep + pool, err := pgxpool.New( + ctx, + fmt.Sprintf( + "user=%s password=%s database=%s host=%s port=%d sslmode=%s ", + cfg.Server.API.DB.User, + cfg.Server.API.DB.Password, + cfg.Server.API.DB.Database, + cfg.Server.API.DB.Host, + cfg.Server.API.DB.Port, + cfg.Server.API.DB.SSLMode, + )) + if err != nil { + log.Fatal("init pgx pool:", err) + } + defer pool.Close() + + db := &PostgresClient{ + Ctx: ctx, + Pool: pool, + } + if err := db.Pool.Ping(ctx); err != nil { + log.Fatal("ping db:", err) + } + + if err := db.CreateOrReplaceSchema(); err != nil { + log.Fatal("create or replace schema:", err) + } + + if err := db.CreateOrReplaceViews(); err != nil { + log.Fatal("create or replace views:", err) + } + + // Routes + if err := addRoutes(e, db, cfg); err != nil { + log.Fatal("add routes:", err) + } + /* + h := handlers.New(client) + + e.GET("/v8/health/", h.GetHealth) + + e.POST("/v8/membres/", h.PostMembres) + + e.GET("/v8/membres/", h.ListMembres) + + e.GET("/v8/membres/:membre_id/", h.ReadMembre) + + e.PUT("/v8/membres/:membre_id/prefered_name/", h.PutMembrePreferedName) + + e.POST("/v8/programmes/", h.PostProgrammes) + + e.POST("/v8/seed/", h.PostSeed) + */ + + // Execution + switch cfg.Server.API.TLS.Enabled { + case false: + e.Logger.Fatal( + e.Start( + fmt.Sprintf(":%d", cfg.Server.API.Port), + ), + ) + case true: + if cfg.Server.API.TLS.Certfile == "" { + log.Fatal("TLS enabled for API but no certificate file provided") + } + + if cfg.Server.API.TLS.Keyfile == "" { + log.Fatal("TLS enabled for UI but no private key file provided") + } + + e.Logger.Fatal( + e.StartTLS( + fmt.Sprintf(":%d", cfg.Server.API.Port), + cfg.Server.API.TLS.Certfile, + cfg.Server.API.TLS.Keyfile, + ), + ) + } + }, +} + +// uiCmd represents the ui command +var uiCmd = &cobra.Command{ + Use: "ui", + Aliases: []string{"web", "interface"}, + Short: "Démarrer l'interface Web UI", + Args: cobra.ExactArgs(0), + Run: func(cmd *cobra.Command, args []string) { + // Parse config + var cfg Config + if err := viper.Unmarshal(&cfg); err != nil { + log.Fatal("init config:", err) + } + + e := echo.New() + + // Middlewares + + // Trailing slash + e.Pre(middleware.AddTrailingSlash()) + + // Auth + e.Use(middleware.BasicAuth(func(user, password string, c echo.Context) (bool, error) { + usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(cfg.Server.UI.User)) == 1 + passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(cfg.Server.UI.Password)) == 1 + return usersMatch && passwordsMatch, nil + })) + + // Templating + e.Renderer = &Template{ + templates: template.Must(template.ParseFS(templatesFS, "templates/*.html")), + } + + // API Client + apiClient := APIClient{voki.New( + http.DefaultClient, + cfg.Server.UI.API.Host, + cfg.Server.UI.API.Key, + cfg.Server.UI.API.Port, + cfg.Server.UI.API.Protocol, + )} + defer apiClient.Voki.CloseIdleConnections() + + // Routes + e.GET("/", func(c echo.Context) error { + pingResult, err := apiClient.GetHealth() + if err != nil { + return c.Render( + http.StatusOK, + "index-html", + voki.MessageResponse{Message: fmt.Sprintf("impossible d'accéder au serveur API: %s", err)}, + ) + } + + return c.Render( + http.StatusOK, + "index-html", + voki.MessageResponse{Message: pingResult}, + ) + }) + + e.GET("/membre/", func(c echo.Context) error { + membreID := c.QueryParam("membre_id") + switch { + case membreID == "": + return c.Render( + http.StatusOK, + "index-html", + voki.MessageResponse{Message: "❗Veuillez entrer un numéro étudiant à rechercher"}, + ) + case !IsMembreID(membreID): + return c.Render( + http.StatusOK, + "index-html", + voki.MessageResponse{Message: fmt.Sprintf("❗Numéro étudiant '%s' invalide", membreID)}, + ) + } + + membre, err := apiClient.GetMembreForDisplay(membreID) + if err != nil { + return c.Render( + http.StatusOK, + "index-html", + voki.MessageResponse{Message: fmt.Sprintf("❗erreur: %s", err)}, + ) + } + + return c.Render( + http.StatusOK, + "index-html", + voki.MessageResponse{Message: fmt.Sprintf(` +Numéro étudiant: %s +Nom d'usage: %s +Programme: [%s] %s +`, + membre.ID, + membre.Name, + membre.ProgrammeID, + membre.ProgrammeName, + )}, + ) + }) + + // Execution + e.Logger.Fatal(e.Start( + fmt.Sprintf(":%d", cfg.Server.UI.Port))) + }, +} diff --git a/cmd/bottin/main.go b/config.go similarity index 52% rename from cmd/bottin/main.go rename to config.go index 526a344..01eecd5 100644 --- a/cmd/bottin/main.go +++ b/config.go @@ -1,25 +1,63 @@ package main import ( - "context" - "crypto/subtle" - "crypto/tls" "fmt" "log" - "net/http" "os" "strings" - "codeberg.org/vlbeaudoin/voki/v3" - "git.agecem.com/agecem/bottin/v9/pkg/bottin" - "git.agecem.com/agecem/bottin/v9/templates" - "github.com/jackc/pgx/v5/pgxpool" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" "github.com/spf13/cobra" "github.com/spf13/viper" ) +type Config struct { + Client struct { + API struct { + Host string `yaml:"host"` + Key string `yaml:"key"` + Port int `yaml:"port"` + Protocol string `yaml:"protocol"` + } `yaml:"api"` + } `yaml:"client"` + + Server struct { + API struct { + DB struct { + Database string `yaml:"database"` + Host string `yaml:"host"` + Password string `yaml:"password"` + Port int `yaml:"port"` + SSLMode string `yaml:"sslmode"` + User string `yaml:"user"` + } `yaml:"db"` + Host string `yaml:"host"` + Key string `yaml:"key"` + Port int `yaml:"port"` + TLS struct { + Enabled bool `yaml:"enabled"` + Certfile string `yaml:"certfile"` + Keyfile string `yaml:"keyfile"` + } `yaml:"tls"` + } `yaml:"api"` + UI struct { + API struct { + Host string `yaml:"host"` + Key string `yaml:"key"` + Port int `yaml:"port"` + Protocol string `yaml:"protocol"` + } `yaml:"api"` + Password string `yaml:"password"` + Port int `yaml:"port"` + TLS struct { + Enabled bool `yaml:"enabled"` + Certfile string `yaml:"certfile"` + Keyfile string `yaml:"keyfile"` + } `yaml:"tls"` + User string `yaml:"user"` + } `yaml:"ui"` + } `yaml:"server"` +} + var cfgFile string // initConfig reads in config file and ENV variables if set. @@ -48,17 +86,6 @@ func initConfig() { } } -func main() { - /* TODO - if err := Run(context.Background(), Config{}, nil, os.Stdout); err != nil { - log.Fatal(err) - } - */ - - // Handle the command-line via cobra and viper - execute() -} - func init() { // rootCmd @@ -339,32 +366,6 @@ func init() { log.Fatal(err) } - // server.ui.api.tls.skipverify - uiCmd.PersistentFlags().Bool( - "server-ui-api-tls-skipverify", - false, - "Skip API server TLS certificate verification", - ) - if err := viper.BindPFlag( - "server.ui.api.tls.skipverify", - uiCmd.PersistentFlags().Lookup("server-ui-api-tls-skipverify"), - ); err != nil { - log.Fatal(err) - } - - // server.ui.host - uiCmd.PersistentFlags().String( - "server-ui-host", - "", - "Web UI host", - ) - if err := viper.BindPFlag( - "server.ui.host", - uiCmd.PersistentFlags().Lookup("server-ui-host"), - ); err != nil { - log.Fatal(err) - } - // server.ui.password uiCmd.PersistentFlags().String( "server-ui-password", @@ -444,274 +445,3 @@ func init() { log.Fatal(err) } } - -/* TODO -func Run(ctx context.Context, config Config, args []string, stdout io.Writer) error { - return fmt.Errorf("not implemented") -} -*/ - -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ - Use: "bottin", - Short: "Bottin étudiant de l'AGECEM", -} - -// 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) - } -} - -var serverCmd = &cobra.Command{ - Use: "server", - Short: "Démarrer serveurs (API ou Web UI)", -} - -// apiCmd represents the api command -var apiCmd = &cobra.Command{ - Use: "api", - Short: "Démarrer le serveur API", - Args: cobra.ExactArgs(0), - Run: func(cmd *cobra.Command, args []string) { - var cfg bottin.Config - if err := viper.Unmarshal(&cfg); err != nil { - log.Fatal("parse config:", err) - } - - e := echo.New() - - // Middlewares - - e.Pre(middleware.AddTrailingSlash()) - - if cfg.Server.API.Key != "" { - e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) { - return subtle.ConstantTimeCompare([]byte(key), []byte(cfg.Server.API.Key)) == 1, nil - })) - } else { - log.Println("Server started but no API key (server.api.key) was provided, using empty key (NOT RECOMMENDED FOR PRODUCTION)") - } - - // DataClient - ctx := context.Background() - - //prep - pool, err := pgxpool.New( - ctx, - fmt.Sprintf( - "user=%s password=%s database=%s host=%s port=%d sslmode=%s ", - cfg.Server.API.DB.User, - cfg.Server.API.DB.Password, - cfg.Server.API.DB.Database, - cfg.Server.API.DB.Host, - cfg.Server.API.DB.Port, - cfg.Server.API.DB.SSLMode, - )) - if err != nil { - log.Fatal("init pgx pool:", err) - } - defer pool.Close() - - db := &bottin.PostgresClient{ - Ctx: ctx, - Pool: pool, - } - if err := db.Pool.Ping(ctx); err != nil { - log.Fatal("ping db:", err) - } - - if err := db.CreateOrReplaceSchema(); err != nil { - log.Fatal("create or replace schema:", err) - } - - if err := db.CreateOrReplaceViews(); err != nil { - log.Fatal("create or replace views:", err) - } - - // Routes - if err := bottin.AddRoutes(e, db, cfg); err != nil { - log.Fatal("add routes:", err) - } - /* - h := handlers.New(client) - - e.GET("/v9/health/", h.GetHealth) - - e.POST("/v9/membres/", h.PostMembres) - - e.GET("/v9/membres/", h.ListMembres) - - e.GET("/v9/membres/:membre_id/", h.ReadMembre) - - e.PUT("/v9/membres/:membre_id/prefered_name/", h.PutMembrePreferedName) - - e.POST("/v9/programmes/", h.PostProgrammes) - - e.POST("/v9/seed/", h.PostSeed) - */ - - // Execution - switch cfg.Server.API.TLS.Enabled { - case false: - e.Logger.Fatal( - e.Start( - fmt.Sprintf("%s:%d", cfg.Server.API.Host, cfg.Server.API.Port), - ), - ) - case true: - if cfg.Server.API.TLS.Certfile == "" { - log.Fatal("TLS enabled for API but no certificate file provided") - } - - if cfg.Server.API.TLS.Keyfile == "" { - log.Fatal("TLS enabled for UI but no private key file provided") - } - - e.Logger.Fatal( - e.StartTLS( - fmt.Sprintf("%s:%d", cfg.Server.API.Host, cfg.Server.API.Port), - cfg.Server.API.TLS.Certfile, - cfg.Server.API.TLS.Keyfile, - ), - ) - } - }, -} - -// uiCmd represents the ui command -var uiCmd = &cobra.Command{ - Use: "ui", - Aliases: []string{"web", "interface"}, - Short: "Démarrer l'interface Web UI", - Args: cobra.ExactArgs(0), - Run: func(cmd *cobra.Command, args []string) { - // Parse config - var cfg bottin.Config - if err := viper.Unmarshal(&cfg); err != nil { - log.Fatal("init config:", err) - } - - e := echo.New() - - // Middlewares - - // Trailing slash - e.Pre(middleware.AddTrailingSlash()) - - // Auth - e.Use(middleware.BasicAuth(func(user, password string, c echo.Context) (bool, error) { - usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(cfg.Server.UI.User)) == 1 - passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(cfg.Server.UI.Password)) == 1 - return usersMatch && passwordsMatch, nil - })) - - // Templating - e.Renderer = templates.NewTemplate() - - // API Client - var httpClient = &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: cfg.Server.UI.API.TLS.SkipVerify, - }, - }, - } - apiClient := bottin.APIClient{ - Voki: voki.New( - httpClient, - cfg.Server.UI.API.Host, - cfg.Server.UI.API.Key, - cfg.Server.UI.API.Port, - cfg.Server.UI.API.Protocol, - )} - defer apiClient.Voki.CloseIdleConnections() - - // Routes - e.GET("/", func(c echo.Context) error { - pingResult, err := apiClient.GetHealth() - if err != nil { - return c.Render( - http.StatusOK, - "index-html", - voki.MessageResponse{Message: fmt.Sprintf("impossible d'accéder au serveur API: %s", err)}, - ) - } - - return c.Render( - http.StatusOK, - "index-html", - voki.MessageResponse{Message: pingResult}, - ) - }) - - e.GET("/membre/", func(c echo.Context) error { - membreID := c.QueryParam("membre_id") - switch { - case membreID == "": - return c.Render( - http.StatusOK, - "index-html", - voki.MessageResponse{Message: "❗Veuillez entrer un numéro étudiant à rechercher"}, - ) - case !bottin.IsMembreID(membreID): - return c.Render( - http.StatusOK, - "index-html", - voki.MessageResponse{Message: fmt.Sprintf("❗Numéro étudiant '%s' invalide", membreID)}, - ) - } - - membre, err := apiClient.GetMembreForDisplay(membreID) - if err != nil { - return c.Render( - http.StatusOK, - "index-html", - voki.MessageResponse{Message: fmt.Sprintf("❗erreur: %s", err)}, - ) - } - - return c.Render( - http.StatusOK, - "index-html", - voki.MessageResponse{Message: fmt.Sprintf(` -Numéro étudiant: %s -Nom d'usage: %s -Programme: [%s] %s -`, - membre.ID, - membre.Name, - membre.ProgrammeID, - membre.ProgrammeName, - )}, - ) - }) - - // Execution - switch cfg.Server.UI.TLS.Enabled { - case false: - e.Logger.Fatal(e.Start( - fmt.Sprintf("%s:%d", cfg.Server.UI.Host, cfg.Server.UI.Port))) - case true: - if cfg.Server.UI.TLS.Certfile == "" { - log.Fatal("TLS enabled for UI but no certificate file provided") - } - - if cfg.Server.UI.TLS.Keyfile == "" { - log.Fatal("TLS enabled for UI but no private key file provided") - } - - e.Logger.Fatal( - e.StartTLS( - fmt.Sprintf("%s:%d", cfg.Server.UI.Host, cfg.Server.UI.Port), - cfg.Server.UI.TLS.Certfile, - cfg.Server.UI.TLS.Keyfile, - ), - ) - } - - }, -} diff --git a/pkg/bottin/db.go b/db.go similarity index 97% rename from pkg/bottin/db.go rename to db.go index 0769fbc..f0b2f4e 100644 --- a/pkg/bottin/db.go +++ b/db.go @@ -1,15 +1,20 @@ -package bottin +package main import ( "context" _ "embed" "fmt" - "git.agecem.com/agecem/bottin/v9/queries" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) +//go:embed sql/schema.sql +var sqlSchema string + +//go:embed sql/views.sql +var sqlViews string + type PostgresClient struct { //TODO move context out of client Ctx context.Context @@ -17,12 +22,12 @@ type PostgresClient struct { } func (db *PostgresClient) CreateOrReplaceSchema() error { - _, err := db.Pool.Exec(db.Ctx, queries.SQLSchema()) + _, err := db.Pool.Exec(db.Ctx, sqlSchema) return err } func (db *PostgresClient) CreateOrReplaceViews() error { - _, err := db.Pool.Exec(db.Ctx, queries.SQLViews()) + _, err := db.Pool.Exec(db.Ctx, sqlViews) return err } diff --git a/deployments/kubernetes/bottin-pod.yaml b/deployments/kubernetes/bottin-pod.yaml deleted file mode 100644 index 72569e4..0000000 --- a/deployments/kubernetes/bottin-pod.yaml +++ /dev/null @@ -1,60 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: bottin-pod -spec: - initContainers: - - name: clone - image: alpine:3.20 - command: ['sh', '-c'] - args: - - apk add git && - git clone -- https://git.agecem.com/agecem/bottin /opt/bottin-src - volumeMounts: - - name: bottin-src - mountPath: /opt/bottin-src - - name: build - image: golang:1.23 - env: - - name: CGO_ENABLED - value: '0' - command: ['sh', '-c'] - args: - - cd /opt/bottin-src && - go build -a -o /opt/bottin-executable/bottin - volumeMounts: - - name: bottin-src - mountPath: /opt/bottin-src - - name: bottin-executable - mountPath: /opt/bottin-executable - containers: - - name: api - image: alpine:3.20 - command: ['sh', '-c'] - args: - - ln -s /opt/bottin-executable/bottin /usr/bin/bottin - volumeMounts: - - name: bottin-executable - mountPath: /opt/bottin-executable - - name: bottin-secret - readOnly: true - mountPath: '/etc/bottin' - - name: ui - image: alpine:3.20 - command: ['sh', '-c'] - args: - - bottin --config /etc/bottin/ui.yaml server ui - volumeMounts: - - name: bottin-executable - mountPath: /opt/bottin-executable - - name: bottin-secret - readOnly: true - mountPath: '/etc/bottin' - volumes: - - name: bottin-src - emptyDir: {} - - name: bottin-executable - emptyDir: {} - - name: bottin-secret - secret: - secretName: bottin-secret diff --git a/deployments/kubernetes/example-bottin-secret.yaml b/deployments/kubernetes/example-bottin-secret.yaml deleted file mode 100644 index 36bed97..0000000 --- a/deployments/kubernetes/example-bottin-secret.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: bottin-secret -stringData: - api.yaml: | - bottin: - server: - api: - db: - database: 'bottin' - host: 'db.example.com' - password: 'bottin' - sslmode: 'require' - user: 'bottin' - key: 'bottin' - ui.yaml: | - bottin: - server: - ui: - api: - tls: - skipverify: 'true' - key: 'bottin' - password: 'bottin' - user: 'bottin' diff --git a/compose.yaml b/docker-compose.yaml similarity index 79% rename from compose.yaml rename to docker-compose.yaml index 686312c..1156be7 100644 --- a/compose.yaml +++ b/docker-compose.yaml @@ -1,12 +1,11 @@ -name: 'bottin' services: db: image: 'docker.io/library/postgres:16' environment: - POSTGRES_DATABASE: "${BOTTIN_SERVER_API_DB_DATABASE:?}" - POSTGRES_PASSWORD: "${BOTTIN_SERVER_API_DB_PASSWORD:?}" - POSTGRES_USER: "${BOTTIN_SERVER_API_DB_USER:?}" + POSTGRES_DATABASE: "${BOTTIN_SERVER_API_DB_DATABASE}" + POSTGRES_PASSWORD: "${BOTTIN_SERVER_API_DB_PASSWORD}" + POSTGRES_USER: "${BOTTIN_SERVER_API_DB_USER}" volumes: - 'db-data:/var/lib/postgresql/data' restart: 'unless-stopped' @@ -14,7 +13,7 @@ services: api: depends_on: - db - build: ../.. + build: . image: 'git.agecem.com/agecem/bottin:latest' env_file: '.env' ports: @@ -27,7 +26,7 @@ services: ui: depends_on: - api - build: ../.. + build: . image: 'git.agecem.com/agecem/bottin:latest' env_file: '.env' ports: diff --git a/pkg/bottin/entity.go b/entity.go similarity index 98% rename from pkg/bottin/entity.go rename to entity.go index 8279b9e..672f54c 100644 --- a/pkg/bottin/entity.go +++ b/entity.go @@ -1,4 +1,4 @@ -package bottin +package main import "unicode" diff --git a/go.mod b/go.mod index e3da402..489b37d 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module git.agecem.com/agecem/bottin/v9 +module git.agecem.com/agecem/bottin/v8 go 1.22.0 diff --git a/main.go b/main.go new file mode 100644 index 0000000..6f81738 --- /dev/null +++ b/main.go @@ -0,0 +1,18 @@ +package main + +func main() { + /* TODO + if err := Run(context.Background(), Config{}, nil, os.Stdout); err != nil { + log.Fatal(err) + } + */ + + // Handle the command-line via cobra and viper + execute() +} + +/* TODO +func Run(ctx context.Context, config Config, args []string, stdout io.Writer) error { + return fmt.Errorf("not implemented") +} +*/ diff --git a/pkg/bottin/config.go b/pkg/bottin/config.go deleted file mode 100644 index b0c4650..0000000 --- a/pkg/bottin/config.go +++ /dev/null @@ -1,53 +0,0 @@ -package bottin - -type Config struct { - Client struct { - API struct { - Host string `yaml:"host"` - Key string `yaml:"key"` - Port int `yaml:"port"` - Protocol string `yaml:"protocol"` - } `yaml:"api"` - } `yaml:"client"` - - Server struct { - API struct { - DB struct { - Database string `yaml:"database"` - Host string `yaml:"host"` - Password string `yaml:"password"` - Port int `yaml:"port"` - SSLMode string `yaml:"sslmode"` - User string `yaml:"user"` - } `yaml:"db"` - Host string `yaml:"host"` - Key string `yaml:"key"` - Port int `yaml:"port"` - TLS struct { - Enabled bool `yaml:"enabled"` - Certfile string `yaml:"certfile"` - Keyfile string `yaml:"keyfile"` - } `yaml:"tls"` - } `yaml:"api"` - UI struct { - API struct { - Host string `yaml:"host"` - Key string `yaml:"key"` - Port int `yaml:"port"` - Protocol string `yaml:"protocol"` - TLS struct { - SkipVerify bool `yaml:"skipverify"` - } `yaml:"tls"` - } `yaml:"api"` - Host string `yaml:"host"` - Password string `yaml:"password"` - Port int `yaml:"port"` - TLS struct { - Enabled bool `yaml:"enabled"` - Certfile string `yaml:"certfile"` - Keyfile string `yaml:"keyfile"` - } `yaml:"tls"` - User string `yaml:"user"` - } `yaml:"ui"` - } `yaml:"server"` -} diff --git a/queries/queries.go b/queries/queries.go deleted file mode 100644 index 0fa44d8..0000000 --- a/queries/queries.go +++ /dev/null @@ -1,14 +0,0 @@ -package queries - -import ( - _ "embed" -) - -//go:embed schema.sql -var sqlSchema string - -//go:embed views.sql -var sqlViews string - -func SQLSchema() string { return sqlSchema } -func SQLViews() string { return sqlViews } diff --git a/pkg/bottin/request.go b/request.go similarity index 94% rename from pkg/bottin/request.go rename to request.go index 555ddab..f1a73b4 100644 --- a/pkg/bottin/request.go +++ b/request.go @@ -1,4 +1,4 @@ -package bottin +package main import ( "bytes" @@ -23,7 +23,7 @@ func (request HealthGETRequest) Request(v *voki.Voki) (response HealthGETRespons statusCode, body, err := v.CallAndParse( http.MethodGet, - "/api/v9/health/", + "/api/v8/health/", nil, true, ) @@ -64,7 +64,7 @@ func (request ProgrammesPOSTRequest) Request(v *voki.Voki) (response ProgrammesP statusCode, body, err := v.CallAndParse( http.MethodPost, - "/api/v9/programme/", + "/api/v8/programme/", &buf, true, ) @@ -105,7 +105,7 @@ func (request MembresPOSTRequest) Request(v *voki.Voki) (response MembresPOSTRes statusCode, body, err := v.CallAndParse( http.MethodPost, - "/api/v9/membre/", + "/api/v8/membre/", &buf, true, ) @@ -146,7 +146,7 @@ func (request MembreGETRequest) Request(v *voki.Voki) (response MembreGETRespons statusCode, body, err := v.CallAndParse( http.MethodGet, - fmt.Sprintf("/api/v9/membre/%s/", request.Param.MembreID), + fmt.Sprintf("/api/v8/membre/%s/", request.Param.MembreID), nil, true, ) @@ -180,7 +180,7 @@ func (request MembresGETRequest) Request(v *voki.Voki) (response MembresGETRespo statusCode, body, err := v.CallAndParse( http.MethodGet, - fmt.Sprintf("/api/v9/membre/?limit=%d", request.Query.Limit), + fmt.Sprintf("/api/v8/membre/?limit=%d", request.Query.Limit), nil, true, ) @@ -224,7 +224,7 @@ func (request MembrePreferedNamePUTRequest) Request(v *voki.Voki) (response Memb statusCode, body, err := v.CallAndParse( http.MethodPut, - fmt.Sprintf("/api/v9/membre/%s/prefered_name/", request.Param.MembreID), + fmt.Sprintf("/api/v8/membre/%s/prefered_name/", request.Param.MembreID), &buf, true, ) @@ -258,7 +258,7 @@ func (request ProgrammesGETRequest) Request(v *voki.Voki) (response ProgrammesGE statusCode, body, err := v.CallAndParse( http.MethodGet, - fmt.Sprintf("/api/v9/programme/?limit=%d", request.Query.Limit), + fmt.Sprintf("/api/v8/programme/?limit=%d", request.Query.Limit), nil, true, ) @@ -292,7 +292,7 @@ func (request MembresDisplayGETRequest) Request(v *voki.Voki) (response MembresD statusCode, body, err := v.CallAndParse( http.MethodGet, - fmt.Sprintf("/api/v9/membre/display/?limit=%d", request.Query.Limit), + fmt.Sprintf("/api/v8/membre/display/?limit=%d", request.Query.Limit), nil, true, ) @@ -333,7 +333,7 @@ func (request MembreDisplayGETRequest) Request(v *voki.Voki) (response MembreDis statusCode, body, err := v.CallAndParse( http.MethodGet, - fmt.Sprintf("/api/v9/membre/%s/display/", request.Param.MembreID), + fmt.Sprintf("/api/v8/membre/%s/display/", request.Param.MembreID), nil, true, ) diff --git a/pkg/bottin/response.go b/response.go similarity index 99% rename from pkg/bottin/response.go rename to response.go index cd034f1..19818df 100644 --- a/pkg/bottin/response.go +++ b/response.go @@ -1,4 +1,4 @@ -package bottin +package main import ( "fmt" diff --git a/pkg/bottin/routes.go b/routes.go similarity index 99% rename from pkg/bottin/routes.go rename to routes.go index dc97041..a631aba 100644 --- a/pkg/bottin/routes.go +++ b/routes.go @@ -1,4 +1,4 @@ -package bottin +package main import ( "encoding/csv" @@ -13,11 +13,11 @@ import ( "github.com/labstack/echo/v4" ) -func AddRoutes(e *echo.Echo, db *PostgresClient, cfg Config) error { +func addRoutes(e *echo.Echo, db *PostgresClient, cfg Config) error { _ = db _ = cfg - apiPath := "/api/v9" + apiPath := "/api/v8" apiGroup := e.Group(apiPath) p := pave.New() if err := pave.EchoRegister[HealthGETRequest]( diff --git a/scripts/compose-inject-x509.sh b/scripts/compose-inject-x509.sh deleted file mode 100755 index 42ed90c..0000000 --- a/scripts/compose-inject-x509.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -docker-compose cp cert.pem api:/etc/bottin/cert.pem -docker-compose cp key.pem api:/etc/bottin/key.pem -docker-compose cp cert.pem ui:/etc/bottin/cert.pem -docker-compose cp key.pem ui:/etc/bottin/key.pem diff --git a/scripts/generate-self-signed-x509.sh b/scripts/generate-self-signed-x509.sh deleted file mode 100755 index 2e1a5bc..0000000 --- a/scripts/generate-self-signed-x509.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes diff --git a/queries/schema.sql b/sql/schema.sql similarity index 100% rename from queries/schema.sql rename to sql/schema.sql diff --git a/queries/views.sql b/sql/views.sql similarity index 100% rename from queries/views.sql rename to sql/views.sql diff --git a/templates/templates.go b/template.go similarity index 55% rename from templates/templates.go rename to template.go index 2539cde..a5b4980 100644 --- a/templates/templates.go +++ b/template.go @@ -1,4 +1,4 @@ -package templates +package main import ( "embed" @@ -8,7 +8,7 @@ import ( "github.com/labstack/echo/v4" ) -//go:embed *.html +//go:embed templates/* var templatesFS embed.FS type Template struct { @@ -18,10 +18,3 @@ type Template struct { func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { return t.templates.ExecuteTemplate(w, name, data) } - -// NewTemplate returns a new Template instance with templates embedded from *.html -func NewTemplate() *Template { - return &Template{ - templates: template.Must(template.ParseFS(templatesFS, "*.html")), - } -}