From 66299f093fb3ae55bb489eb05739628bcd0ff251 Mon Sep 17 00:00:00 2001 From: Helvio Pedreschi Date: Wed, 22 Nov 2023 11:30:46 -0500 Subject: [PATCH] Docker enable (#49) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Accept ENV Vars (Docker compatilibity) * Initial Docker README * Fix Traefik inside Details * Add all settings from ENV except Titles * remove SafeWriteConfig * Updated compose example * Config File wouldn't make sense on Docker. Renamed * A few tweaks on defaults and docs before merge * Moved docker below `Dev or build from source` * Update README.md Co-authored-by: Rémy Boulanouar * Update README.md Co-authored-by: Rémy Boulanouar * Add "See Docker" to "🎮 Use" --------- Co-authored-by: Rémy Boulanouar --- README.md | 79 +++++++++++++++++++++++++++- config/config.go | 104 +++++++++++++++++++++++-------------- config/config_test.go | 52 +++++++++---------- docker-compose.example.yml | 15 +++++- 4 files changed, 182 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 22230e2..82b5c24 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ To proper use this software, here is the checklist: - [ ] Comment/Uncomment parts in the config according to your needs - [ ] Games should have in their name `[ID][v0]` to be recognized - [ ] Games extension should be `nsp` or `nsz` -- [ ] Retrieve binary from [latest release](https://github.com/DblK/tinshop/releases) or [container](https://github.com/DblK/tinshop/pkgs/container/tinshop) or build from source (See [`Dev`](https://github.com/DblK/tinshop/tree/master#-dev-or-build-from-source) section below) +- [ ] Retrieve binary from [latest release](https://github.com/DblK/tinshop/releases) or [container](https://github.com/DblK/tinshop/pkgs/container/tinshop) (See [`Docker`](https://github.com/DblK/tinshop/tree/master#-docker) section below) or build from source (See [`Dev`](https://github.com/DblK/tinshop/tree/master#-dev-or-build-from-source) section below) Now simply run it and add a shop inside tinfoil with the address setup in `config` (or `http://localIp:3000` if not specified). @@ -67,6 +67,52 @@ If you want to build `TinShop` from source, please run `go build`. And then, simply run `./tinshop`. +# 🐋 Docker + +To run with [Docker](https://docs.docker.com/engine/install/), you can use this as a starting `cli` example: + +`docker run -d --restart=always -e TINSHOP_SOURCES_DIRECTORIES=/games -e TINSHOP_WELCOMEMESSAGE="Welcome to my Tinshop!" -v /local/game/backups:/games -p 3000:3000 ghcr.io/dblk/tinshop:latest` + +This will run Tinshop on `http://localhost:3000` and persist across reboots! + +If `docker compose` is your thing, then start with this example: + +```yaml +version: '3.9' +services: + tinshop: + container_name: tinshop + image: ghcr.io/dblk/tinshop:latest + restart: always + ports: + - 3000:3000 + environment: + - TINSHOP_SOURCES_DIRECTORIES=/games + - TINSHOP_WELCOMEMESSAGE=Welcome to my Tinshop! + volumes: + - /media/switch:/games +``` +All of the settings in the `config.yaml` file are valid Environment Variables. They must be `UPPERCASE` and prefixed by `TINSHOP_`. Nested properties should be prefixed by `_`. Here are a few examples: + +| ENV_VAR | `config.yaml` entry | Default Value | Example Value | +|------------------------------|---------------------|--------------------------------|-----------------------------------| +| TINSHOP_HOST | host | `` | `tinshop.example.com` | +| TINSHOP_PROTOCOL | protocol | `http` | `https` | +| TINSHOP_NAME | name | `TinShop` | `MyShop` | +| TINSHOP_REVERSEPROXY | reverseProxy | `false` | `true` | +| TINSHOP_WELCOMEMESSAGE | welcomeMessage | `Welcome to your own TinShop!` | `Welcome to my shop!` | +| TINSHOP_NOWELCOMEMESSAGE | noWelcomeMessage | `false` | `true` | +| TINSHOP_DEBUG_NFS | debug.nfs | `false` | `true` | +| TINSHOP_DEBUG_NOSECURITY | debug.nosecurity | `false` | `true` | +| TINSHOP_DEBUG_TICKET | debug.ticket | `false` | `true` | +| TINSHOP_NSP_CHECKVERIFIED | nsp.checkVerified | `false` | `true` | +| TINSHOP_SOURCES_DIRECTORIES | sources.directories | `./games` | `/games /path/two /path/three` | +| TINSHOP_SOURCES_NSF | sources.nfs | `null` | `192.168.1.100:/path/to/games` | +| TINSHOP_SECURITY_BANNEDTHEME | sources.bannedTheme | `null` | `THEME1 THEME2 THEME3` | +| TINSHOP_SECURITY_WHITELIST | sources.whitelist | `null` | `NSWID1 NSWID2 NSWID3` | +| TINSHOP_SECURITY_BLACKLIST | sources.blacklist | `null` | `NSWID4 NSWID5 NSWID6` | +| TINSHOP_SECURITY_FORWARDAUTH | sources.forwardAuth | `null` | `https://auth.tinshop.com/switch` | + ## 🥍 Want to do cross-build generation? Wanting to generate all possible os binaries (macOS, linux, windows) with all architectures (arm, amd64)? @@ -148,6 +194,37 @@ reverseProxy: true ``` If you want to have HTTPS, ensure `caddy` handle it (it will with Let's Encrypt) and change `https` in the config and remove `:80` in the `Caddyfile` example. + +### Example for traefik + +To work with [`traefik`](https://traefik.io/), you need to put in your Dynamic Configuration something similar to this: + +```yaml +http: + routers: + service: tinshop + rule: Host(`tinshop.example.com`) + entryPoints: websecure # Could be web if not using https + + services: + tinshop: + loadBalancer: + servers: + - url: http://192.168.1.2:3000 +``` + +and your `config.yaml` as follow: + +```yaml +host: tinshop.example.com +protocol: http +port: 3000 +reverseProxy: true +``` + +If you want to have HTTPS, ensure `traefik` can handle it (it will with Let's Encrypt) and use protocol `https` in the config. + +For more details on Traefik + Let's Encrypt, [click here](https://doc.traefik.io/traefik/https/acme/). ## How can I add a `basic auth` to protect my shop? diff --git a/config/config.go b/config/config.go index 46fb8d2..ac9c279 100644 --- a/config/config.go +++ b/config/config.go @@ -11,6 +11,7 @@ import ( "net" "os" "strconv" + "strings" "github.com/DblK/tinshop/repository" "github.com/DblK/tinshop/utils" @@ -35,8 +36,8 @@ type nsp struct { CheckVerified bool `mapstructure:"checkVerified"` } -// File holds all config information -type File struct { +// Configuration holds all config information +type Configuration struct { rootShop string ShopHost string `mapstructure:"host"` ShopProtocol string `mapstructure:"protocol"` @@ -58,18 +59,41 @@ type File struct { // New returns a new configuration func New() repository.Config { - return &File{} + return &Configuration{} } // LoadConfig handles viper under the hood -func (cfg *File) LoadConfig() { - viper.SetConfigName("config") // name of config file (without extension) - viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name - viper.AddConfigPath(".") // optionally look for config in the working directory - viper.SetDefault("sources.directories", "./games") +func (cfg *Configuration) LoadConfig() { + viper.SetConfigName("config") // name of config file (without extension) + viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name + viper.AddConfigPath(".") // optionally look for config in the working directory + viper.SetTypeByDefaultValue(true) // Allows []string to be parsed from Env Vars + + viper.SetDefault("host", "") + viper.SetDefault("protocol", "http") + viper.SetDefault("name", "TinShop") + viper.SetDefault("reverseProxy", false) viper.SetDefault("welcomeMessage", "Welcome to your own TinShop!") viper.SetDefault("noWelcomeMessage", false) + viper.SetDefault("debug.nfs", false) + viper.SetDefault("debug.noSecurity", false) + viper.SetDefault("debug.ticket", false) + + viper.SetDefault("nsp.checkVerified", false) + + viper.SetDefault("sources.directories", []string{"./games"}) + viper.SetDefault("sources.nfs", []string{}) + + viper.SetDefault("security.bannedTheme", []string{}) + viper.SetDefault("security.whitelist", []string{}) + viper.SetDefault("security.blacklist", []string{}) + viper.SetDefault("security.forwardAuth", "") + + viper.SetEnvPrefix("TINSHOP") + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + viper.AutomaticEnv() + if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { // Config file not found; ignore error if desired @@ -89,7 +113,7 @@ func (cfg *File) LoadConfig() { cfg.configChange() } -func (cfg *File) configChange() { +func (cfg *Configuration) configChange() { // Call all before hooks for _, hook := range cfg.beforeAllHooks { hook(cfg) @@ -121,8 +145,8 @@ func (cfg *File) configChange() { } } -func loadAndCompute() *File { - var loadedConfig = &File{} +func loadAndCompute() *Configuration { + var loadedConfig = &Configuration{} err := viper.Unmarshal(&loadedConfig) if err != nil { @@ -169,7 +193,7 @@ func ComputeDefaultValues(config repository.Config) repository.Config { rootShop += ":" + strconv.Itoa(config.Port()) } } - log.Println((rootShop)) + log.Println(rootShop) config.SetRootShop(rootShop) config.SetShopTemplateData(repository.ShopTemplate{ @@ -180,117 +204,117 @@ func ComputeDefaultValues(config repository.Config) repository.Config { } // AddHook Add hook function on change config -func (cfg *File) AddHook(f func(repository.Config)) { +func (cfg *Configuration) AddHook(f func(repository.Config)) { cfg.allHooks = append(cfg.allHooks, f) } // AddBeforeHook Add hook function before on change config -func (cfg *File) AddBeforeHook(f func(repository.Config)) { +func (cfg *Configuration) AddBeforeHook(f func(repository.Config)) { cfg.beforeAllHooks = append(cfg.beforeAllHooks, f) } // SetRootShop allow to change the root url of the shop -func (cfg *File) SetRootShop(root string) { +func (cfg *Configuration) SetRootShop(root string) { cfg.rootShop = root } // RootShop returns the RootShop url -func (cfg *File) RootShop() string { +func (cfg *Configuration) RootShop() string { return cfg.rootShop } // ReverseProxy returns the ReverseProxy setting -func (cfg *File) ReverseProxy() bool { +func (cfg *Configuration) ReverseProxy() bool { return cfg.Proxy } // WelcomeMessage returns the WelcomeMessage -func (cfg *File) WelcomeMessage() string { +func (cfg *Configuration) WelcomeMessage() string { return cfg.ShopWelcomeMessage } // NoWelcomeMessage returns the NoWelcomeMessage -func (cfg *File) NoWelcomeMessage() bool { +func (cfg *Configuration) NoWelcomeMessage() bool { return cfg.ShopNoWelcomeMessage } // Protocol returns the protocol scheme (http or https) -func (cfg *File) Protocol() string { +func (cfg *Configuration) Protocol() string { return cfg.ShopProtocol } // Host returns the host of the shop -func (cfg *File) Host() string { +func (cfg *Configuration) Host() string { return cfg.ShopHost } // Port returns the port number for outside access -func (cfg *File) Port() int { +func (cfg *Configuration) Port() int { return cfg.ShopPort } // DebugTicket tells if we should display additional log for ticket verification -func (cfg *File) DebugTicket() bool { +func (cfg *Configuration) DebugTicket() bool { return cfg.Debug.Ticket } // DebugNfs tells if we should display additional log for nfs -func (cfg *File) DebugNfs() bool { +func (cfg *Configuration) DebugNfs() bool { return cfg.Debug.Nfs } // DebugNoSecurity returns if we should disable security or not -func (cfg *File) DebugNoSecurity() bool { +func (cfg *Configuration) DebugNoSecurity() bool { return cfg.Debug.NoSecurity } // Directories returns the list of directories sources -func (cfg *File) Directories() []string { +func (cfg *Configuration) Directories() []string { return cfg.AllSources.Directories } // CustomDB returns the list of custom title db -func (cfg *File) CustomDB() map[string]repository.TitleDBEntry { +func (cfg *Configuration) CustomDB() map[string]repository.TitleDBEntry { return cfg.CustomTitleDB } // NfsShares returns the list of nfs sources -func (cfg *File) NfsShares() []string { +func (cfg *Configuration) NfsShares() []string { return cfg.AllSources.Nfs } // Sources returns all available sources -func (cfg *File) Sources() repository.ConfigSources { +func (cfg *Configuration) Sources() repository.ConfigSources { return cfg.AllSources } // ShopTemplateData returns the data needed to render template -func (cfg *File) ShopTemplateData() repository.ShopTemplate { +func (cfg *Configuration) ShopTemplateData() repository.ShopTemplate { return cfg.shopTemplateData } // SetShopTemplateData sets the data for template -func (cfg *File) SetShopTemplateData(data repository.ShopTemplate) { +func (cfg *Configuration) SetShopTemplateData(data repository.ShopTemplate) { cfg.shopTemplateData = data } // ShopTitle returns the name of the shop -func (cfg *File) ShopTitle() string { +func (cfg *Configuration) ShopTitle() string { return cfg.Name } // VerifyNSP tells if we need to verify NSP -func (cfg *File) VerifyNSP() bool { +func (cfg *Configuration) VerifyNSP() bool { return cfg.NSP.CheckVerified } // ForwardAuthURL returns the url of the forward auth -func (cfg *File) ForwardAuthURL() string { +func (cfg *Configuration) ForwardAuthURL() string { return cfg.Security.ForwardAuth } // IsBlacklisted tells if the uid is blacklisted or not -func (cfg *File) IsBlacklisted(uid string) bool { +func (cfg *Configuration) IsBlacklisted(uid string) bool { if len(cfg.Security.Whitelist) != 0 { return !cfg.isInWhiteList(uid) } @@ -298,20 +322,20 @@ func (cfg *File) IsBlacklisted(uid string) bool { } // IsWhitelisted tells if the uid is whitelisted or not -func (cfg *File) IsWhitelisted(uid string) bool { +func (cfg *Configuration) IsWhitelisted(uid string) bool { if len(cfg.Security.Whitelist) == 0 { return !cfg.isInBlackList(uid) } return cfg.isInWhiteList(uid) } -func (cfg *File) isInBlackList(uid string) bool { +func (cfg *Configuration) isInBlackList(uid string) bool { idxBlackList := utils.Search(len(cfg.Security.Blacklist), func(index int) bool { return cfg.Security.Blacklist[index] == uid }) return idxBlackList != -1 } -func (cfg *File) isInWhiteList(uid string) bool { +func (cfg *Configuration) isInWhiteList(uid string) bool { idxWhiteList := utils.Search(len(cfg.Security.Whitelist), func(index int) bool { return cfg.Security.Whitelist[index] == uid }) @@ -319,7 +343,7 @@ func (cfg *File) isInWhiteList(uid string) bool { } // IsBannedTheme tells if the theme is banned or not -func (cfg *File) IsBannedTheme(theme string) bool { +func (cfg *Configuration) IsBannedTheme(theme string) bool { idxBannedTheme := utils.Search(len(cfg.Security.BannedTheme), func(index int) bool { return cfg.Security.BannedTheme[index] == theme }) @@ -327,6 +351,6 @@ func (cfg *File) IsBannedTheme(theme string) bool { } // BannedTheme returns all banned theme -func (cfg *File) BannedTheme() []string { +func (cfg *Configuration) BannedTheme() []string { return cfg.Security.BannedTheme } diff --git a/config/config_test.go b/config/config_test.go index 86a10de..c529e1b 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -175,10 +175,10 @@ var _ = Describe("Config", func() { }) }) Context("Security for Blacklist/Whitelist tests", func() { - var myConfig config.File + var myConfig config.Configuration BeforeEach(func() { - myConfig = config.File{} + myConfig = config.Configuration{} }) Describe("Blacklist tests", func() { //nolint:dupl @@ -251,10 +251,10 @@ var _ = Describe("Config", func() { }) }) Context("Security for theme", func() { - var myConfig config.File + var myConfig config.Configuration BeforeEach(func() { - myConfig = config.File{} + myConfig = config.Configuration{} }) Describe("IsBannedTheme", func() { @@ -276,10 +276,10 @@ var _ = Describe("Config", func() { }) }) Describe("Protocol", func() { - var myConfig config.File + var myConfig config.Configuration BeforeEach(func() { - myConfig = config.File{} + myConfig = config.Configuration{} }) It("Test with empty object", func() { @@ -291,10 +291,10 @@ var _ = Describe("Config", func() { }) }) Describe("Host", func() { - var myConfig config.File + var myConfig config.Configuration BeforeEach(func() { - myConfig = config.File{} + myConfig = config.Configuration{} }) It("Test with empty object", func() { @@ -306,10 +306,10 @@ var _ = Describe("Config", func() { }) }) Describe("WelcomeMessage", func() { - var myConfig config.File + var myConfig config.Configuration BeforeEach(func() { - myConfig = config.File{} + myConfig = config.Configuration{} }) It("Test with empty object", func() { @@ -325,10 +325,10 @@ var _ = Describe("Config", func() { }) }) Describe("Port", func() { - var myConfig config.File + var myConfig config.Configuration BeforeEach(func() { - myConfig = config.File{} + myConfig = config.Configuration{} }) It("Test with empty object", func() { @@ -340,10 +340,10 @@ var _ = Describe("Config", func() { }) }) Describe("ReverseProxy", func() { - var myConfig config.File + var myConfig config.Configuration BeforeEach(func() { - myConfig = config.File{} + myConfig = config.Configuration{} }) It("Test with empty object", func() { @@ -355,10 +355,10 @@ var _ = Describe("Config", func() { }) }) Describe("ShopTitle", func() { - var myConfig config.File + var myConfig config.Configuration BeforeEach(func() { - myConfig = config.File{} + myConfig = config.Configuration{} }) It("Test with empty object", func() { @@ -370,10 +370,10 @@ var _ = Describe("Config", func() { }) }) Describe("DebugNfs", func() { - var myConfig config.File + var myConfig config.Configuration BeforeEach(func() { - myConfig = config.File{} + myConfig = config.Configuration{} }) It("Test with empty object", func() { @@ -385,10 +385,10 @@ var _ = Describe("Config", func() { }) }) Describe("VerifyNSP", func() { - var myConfig config.File + var myConfig config.Configuration BeforeEach(func() { - myConfig = config.File{} + myConfig = config.Configuration{} }) It("Test with empty object", func() { @@ -400,10 +400,10 @@ var _ = Describe("Config", func() { }) }) Describe("DebugNoSecurity", func() { - var myConfig config.File + var myConfig config.Configuration BeforeEach(func() { - myConfig = config.File{} + myConfig = config.Configuration{} }) It("Test with empty object", func() { @@ -415,10 +415,10 @@ var _ = Describe("Config", func() { }) }) Describe("DebugTicket", func() { - var myConfig config.File + var myConfig config.Configuration BeforeEach(func() { - myConfig = config.File{} + myConfig = config.Configuration{} }) It("Test with empty object", func() { @@ -430,10 +430,10 @@ var _ = Describe("Config", func() { }) }) Describe("BannedTheme", func() { - var myConfig config.File + var myConfig config.Configuration BeforeEach(func() { - myConfig = config.File{} + myConfig = config.Configuration{} }) It("Test with empty object", func() { diff --git a/docker-compose.example.yml b/docker-compose.example.yml index d092e55..f289953 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -5,10 +5,23 @@ services: build: . container_name: app restart: unless-stopped + environment: + - TINSHOP_HOST=tinshop.example.com + - TINSHOP_PROTOCOL=https + - TINSHOP_NAME=TinShop + - TINSHOP_REVERSEPROXY=true + - TINSHOP_WELCOMEMESSAGE=Welcome to TinShop! + - TINSHOP_NOWELCOMEMESSAGE=false + - TINSHOP_DEBUG_NFS=false + - TINSHOP_DEBUG_NOSECURITY=false + - TINSHOP_DEBUG_TICKET=false + - TINSHOP_NSP_CHECKVERIFIED=true + - TINSHOP_SOURCES_DIRECTORIES=/games + - TINSHOP_SECURITY_WHITELIST=0000000000000000000000000000000000000000000000000000000000000000 1111111111111111111111111111111111111111111111111111111111111111 ports: - 3000:3000 volumes: - - ./config.example.yaml:/config.yaml + - /media/switch:/games nginx: