mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-05-08 23:18:55 +02:00
Refactor gobuild to include all assets (locales, static-files and template) into one single binary
This commit is contained in:
+1
-4
@@ -53,11 +53,8 @@ VOLUME ["/config"]
|
||||
|
||||
ENV CONTAINER=true
|
||||
|
||||
# Copy Fail2Ban-UI binary and templates from the build stage
|
||||
# Copy Fail2Ban-UI binary (templates, locales, and static assets are embedded at compile time)
|
||||
COPY --from=builder /app/fail2ban-ui /app/fail2ban-ui
|
||||
COPY --from=builder /app/pkg/web/templates /app/templates
|
||||
COPY --from=builder /app/internal/locales /app/locales
|
||||
COPY --from=builder /app/pkg/web/static /app/static
|
||||
RUN chown fail2ban:0 /app/fail2ban-ui && chmod +x /app/fail2ban-ui
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
@@ -191,7 +191,7 @@ Documentation and deployment guidance in security tooling is never "done", and e
|
||||
|
||||
If you see a clearer way to describe installation steps, safer container defaults, better reverse-proxy examples, SELinux improvements, or a more practical demo environment, please contribute. Small improvements (typos, wording, examples) are just as valuable as code changes.
|
||||
|
||||
Want to add a new UI language? Copy `internal/locales/en.json`, translate all values, save it as `internal/locales/<lang>.json`, and open a pull request.
|
||||
Want to add a new UI language? Copy `pkg/web/locales/en.json`, translate all values, save it as `pkg/web/locales/<lang>.json`, and open a pull request.
|
||||
Please use a proper lowercase locale short code for `<lang>` (for example `ch`, `ch_de`, `es`, or `pt_br`).
|
||||
|
||||
|
||||
|
||||
+2
-11
@@ -19,7 +19,6 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -84,16 +83,8 @@ func main() {
|
||||
bindAddress, _ := config.GetBindAddressFromEnv()
|
||||
serverAddr := net.JoinHostPort(bindAddress, serverPort)
|
||||
|
||||
// Load templates and static assets based on running in container or locally (compiled binary)
|
||||
_, container := os.LookupEnv("CONTAINER")
|
||||
if container {
|
||||
router.LoadHTMLGlob("/app/templates/*")
|
||||
router.Static("/locales", "/app/locales")
|
||||
router.Static("/static", "/app/static")
|
||||
} else {
|
||||
router.LoadHTMLGlob("pkg/web/templates/*")
|
||||
router.Static("/locales", "./internal/locales")
|
||||
router.Static("/static", "./pkg/web/static")
|
||||
if err := web.MountEmbeddedAssets(router); err != nil {
|
||||
log.Fatalf("failed to mount embedded web assets: %v", err)
|
||||
}
|
||||
|
||||
// Initialize WebSocket hub and console log capture
|
||||
|
||||
@@ -37,9 +37,12 @@ Clone the repository to `/opt/fail2ban-ui`:
|
||||
```bash
|
||||
sudo git clone https://github.com/swissmakers/fail2ban-ui.git /opt/fail2ban-ui
|
||||
cd /opt/fail2ban-ui
|
||||
./build-tailwind.sh
|
||||
sudo go build -o fail2ban-ui ./cmd/server/main.go
|
||||
```
|
||||
|
||||
Web UI templates, translation JSON files, and `pkg/web/static` are embedded into the binary at compile time, so you only need to ship the `fail2ban-ui` executable (plus a writable `WorkingDirectory` for the SQLite database).
|
||||
|
||||
### Create the fail2ban-ui.service
|
||||
Save this file as `/etc/systemd/system/fail2ban-ui.service`:
|
||||
For production deployments, please use a dedicated service account instead of root.
|
||||
|
||||
@@ -75,7 +75,7 @@ cd /opt/fail2ban-ui
|
||||
# Build static CSS assets
|
||||
./build-tailwind.sh
|
||||
|
||||
# Build the Go binary
|
||||
# Build the Go binary (embeds pkg/web/templates, pkg/web/locales, and pkg/web/static)
|
||||
go build -o fail2ban-ui ./cmd/server/main.go
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// Fail2ban UI - A Swiss made, management interface for Fail2ban.
|
||||
//
|
||||
// Copyright (C) 2025 Swissmakers GmbH (https://swissmakers.ch)
|
||||
//
|
||||
// Licensed under the GNU General Public License, Version 3 (GPL-3.0)
|
||||
// You may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
//go:embed templates/*
|
||||
var embeddedTemplates embed.FS
|
||||
|
||||
//go:embed all:static
|
||||
var embeddedStatic embed.FS
|
||||
|
||||
//go:embed locales/*.json
|
||||
var embeddedLocales embed.FS
|
||||
|
||||
// LocalesFS is the embedded UI translations
|
||||
var LocalesFS fs.FS
|
||||
|
||||
func init() {
|
||||
sub, err := fs.Sub(embeddedLocales, "locales")
|
||||
if err != nil {
|
||||
panic("web: locales embed: " + err.Error())
|
||||
}
|
||||
LocalesFS = sub
|
||||
}
|
||||
|
||||
// Registers HTML templates and /static and /locales handlers using data embedded at compile time.
|
||||
func MountEmbeddedAssets(r *gin.Engine) error {
|
||||
tmpl, err := template.ParseFS(embeddedTemplates, "templates/*.html")
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse HTML templates: %w", err)
|
||||
}
|
||||
r.SetHTMLTemplate(tmpl)
|
||||
|
||||
staticRoot, err := fs.Sub(embeddedStatic, "static")
|
||||
if err != nil {
|
||||
return fmt.Errorf("static subdirectory: %w", err)
|
||||
}
|
||||
r.StaticFS("/static", http.FS(staticRoot))
|
||||
r.StaticFS("/locales", http.FS(LocalesFS))
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Fail2ban UI - A Swiss made, management interface for Fail2ban.
|
||||
//
|
||||
// Copyright (C) 2025 Swissmakers GmbH (https://swissmakers.ch)
|
||||
//
|
||||
// Licensed under the GNU General Public License, Version 3 (GPL-3.0)
|
||||
// You may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package web
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEmbeddedTemplatesIndexName(t *testing.T) {
|
||||
tmpl, err := template.ParseFS(embeddedTemplates, "templates/*.html")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var names []string
|
||||
for _, tp := range tmpl.Templates() {
|
||||
names = append(names, tp.Name())
|
||||
}
|
||||
ok := false
|
||||
for _, n := range names {
|
||||
if n == "index.html" {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("template index.html not found; templates: %v", names)
|
||||
}
|
||||
}
|
||||
+10
-19
@@ -25,6 +25,7 @@ import (
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -1583,11 +1584,7 @@ func renderIndexPage(c *gin.Context) {
|
||||
}
|
||||
|
||||
func listLocaleOptions() []localeOption {
|
||||
localeDir := "./internal/locales"
|
||||
if _, container := os.LookupEnv("CONTAINER"); container {
|
||||
localeDir = "/app/locales"
|
||||
}
|
||||
entries, err := os.ReadDir(localeDir)
|
||||
entries, err := fs.ReadDir(LocalesFS, ".")
|
||||
if err != nil {
|
||||
return []localeOption{{Code: "en", Label: "en"}}
|
||||
}
|
||||
@@ -1604,7 +1601,11 @@ func listLocaleOptions() []localeOption {
|
||||
if code == "" {
|
||||
continue
|
||||
}
|
||||
label := localeLabelFromFile(filepath.Join(localeDir, name), code)
|
||||
data, readErr := fs.ReadFile(LocalesFS, name)
|
||||
label := code
|
||||
if readErr == nil {
|
||||
label = localeLabelFromJSON(data, code)
|
||||
}
|
||||
options = append(options, localeOption{
|
||||
Code: code,
|
||||
Label: label,
|
||||
@@ -1626,9 +1627,8 @@ func listLocaleOptions() []localeOption {
|
||||
return options
|
||||
}
|
||||
|
||||
func localeLabelFromFile(path, fallback string) string {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
func localeLabelFromJSON(data []byte, fallback string) string {
|
||||
if len(data) == 0 {
|
||||
return fallback
|
||||
}
|
||||
var translations map[string]string
|
||||
@@ -2900,16 +2900,7 @@ func loadLocale(lang string) (map[string]string, error) {
|
||||
}
|
||||
localeCacheLock.RUnlock()
|
||||
|
||||
// Determines the locale file path
|
||||
var localePath string
|
||||
_, container := os.LookupEnv("CONTAINER")
|
||||
if container {
|
||||
localePath = fmt.Sprintf("/app/locales/%s.json", lang)
|
||||
} else {
|
||||
localePath = fmt.Sprintf("./internal/locales/%s.json", lang)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(localePath)
|
||||
data, err := fs.ReadFile(LocalesFS, lang+".json")
|
||||
if err != nil {
|
||||
// Falls back to English if the locale file is not found
|
||||
if lang != "en" {
|
||||
|
||||
Reference in New Issue
Block a user