diff --git a/.github/workflows/build-release-binaries.yml b/.github/workflows/build-release-binaries.yml new file mode 100644 index 0000000..80c5f26 --- /dev/null +++ b/.github/workflows/build-release-binaries.yml @@ -0,0 +1,43 @@ +# src https://github.com/akrabat/rodeo/blob/main/.github/workflows/build-release-binaries.yml +name: Build Release Binaries + +on: + release: + types: + - created + +jobs: + build: + name: Build Release Assets + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v1 + with: + go-version: 1.22 + + - name: Display the version of go that we have installed + run: go version + + - name: Display the release tag + run: echo ${{ github.event.release.tag_name }} + + - name: "DEBUG: What's our directory & what's in it?" + run: pwd && ls + + - name: Build the Kompanion executables + run: ./build-executables.sh ${{ github.event.release.tag_name }} + + - name: List the Kompanion executables + run: ls -l ./release + + - name: Upload the Kompanion binaries + uses: actions/svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ github.ref }} + file: ./release/kompanion-* + file_glob: true diff --git a/.gitignore b/.gitignore index 2ff406a..95450a4 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ go.work # Data for local development data/ bin/ +release/ diff --git a/Dockerfile b/Dockerfile index b1fc0ee..94813f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,8 +22,6 @@ FROM golang:1.22.5-alpine ENV GIN_MODE=release WORKDIR / -COPY --from=builder /app/config /config -COPY --from=builder /app/migrations /migrations COPY --from=builder /app/web /web COPY --from=builder /bin/app /app COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ diff --git a/README.md b/README.md index f475c46..15c7d08 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,17 @@ Features, that can buy you in: 1. you need a postgresql instance 2. run `docker run -e KOMPANION_PG_URL=postgres://... -e KOMPANION_AUTH_PASSWORD=password -e KOMPANION_AUTH_USERNAME=username kompanion` , where you pass pg url and admin username and password to init +### Pre-compiled binary + +1. download archive with latest binary from [Releases page](https://github.com/vanadium23/kompanion/releases) +2. run `KOMPANION_PG_URL=postgres://... -e KOMPANION_AUTH_PASSWORD=password -e KOMPANION_AUTH_USERNAME=username ./kompanion`, it will start server with provided postgresql and admin credentials + ### Configuration - `KOMPANION_AUTH_USERNAME` - required for setup - `KOMPANION_AUTH_PASSWORD` - required for setup - `KOMPANION_AUTH_STORAGE` - postgres or memory (default: postgres) -- `KOMPANION_HTTP_PORT` - port for service (default: postgres) +- `KOMPANION_HTTP_PORT` - port for service (default: 8080) - `KOMPANION_LOG_LEVEL` - debug, info, error (default: info) - `KOMPANION_PG_POOL_MAX` - integer number for pooling connections (default: 2) - `KOMPANION_PG_URL` - postgresql link diff --git a/assets.go b/assets.go new file mode 100644 index 0000000..6884182 --- /dev/null +++ b/assets.go @@ -0,0 +1,11 @@ +package kompanion + +import ( + "embed" +) + +//go:embed migrations/*.sql +var Migrations embed.FS + +//go:embed web/* +var WebAssets embed.FS diff --git a/build_release.sh b/build_release.sh new file mode 100644 index 0000000..c1074a0 --- /dev/null +++ b/build_release.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +# src: https://github.com/akrabat/rodeo/blob/main/build-executables.sh + +version=$1 +if [[ -z "$version" ]]; then + echo "usage: $0 " + exit 1 +fi +package_name=kompanion + +# +# The full list of the platforms is at: https://golang.org/doc/install/source#environment +platforms=( +"darwin/amd64" +"darwin/arm64" +"linux/amd64" +"linux/arm" +"linux/arm64" +"windows/amd64" +) + +rm -rf release/ +mkdir -p release + +for platform in "${platforms[@]}" +do + platform_split=(${platform//\// }) + os=${platform_split[0]} + GOOS=${platform_split[0]} + GOARCH=${platform_split[1]} + + if [ $os = "darwin" ]; then + os="macOS" + fi + + output_name=$package_name'-'$version'-'$os'-'$GOARCH + zip_name=$output_name + if [ $os = "windows" ]; then + output_name+='.exe' + fi + + echo "Building release/$output_name..." + env GOOS=$GOOS GOARCH=$GOARCH go build \ + -ldflags "-X main.Version=$version" \ + -o release/$output_name + if [ $? -ne 0 ]; then + echo 'An error has occurred! Aborting the script execution...' + exit 1 + fi + + pushd release > /dev/null + if [ $os = "windows" ]; then + zip $zip_name.zip $output_name + rm $output_name + else + chmod a+x $output_name + gzip $output_name + fi + popd > /dev/null +done diff --git a/docs/adr/0010-provide-single-binary-setup.md b/docs/adr/0010-provide-single-binary-setup.md new file mode 100644 index 0000000..3703b5d --- /dev/null +++ b/docs/adr/0010-provide-single-binary-setup.md @@ -0,0 +1,29 @@ +# 10. Provide single binary setup + +Date: 2025-03-02 + +## Status + +Accepted + +## Context + +Not all software are good to be distributed via docker. Some users may prefer to just run a single binary, and have something like `curl && ./run`. But currently we need to be migrations and web folders to be placed with binaries. + +## Decision + +Embed static files inside binary and adjust code to it. Also, we need to adjust CI to place the binary in release. + +We have several options to implement embeded FS: +- `go:embed` +- `go-bindata` +- `go-rice` + +We use `go:embed`, because it will not introduce new dependencies. + + +## Consequences + +- (+) Single binary is enough, but still requires postgresql +- (-) Binary will be larger + diff --git a/internal/app/migrate.go b/internal/app/migrate.go index 842c218..04e99a7 100644 --- a/internal/app/migrate.go +++ b/internal/app/migrate.go @@ -9,9 +9,11 @@ import ( "time" "github.com/golang-migrate/migrate/v4" + "github.com/vanadium23/kompanion" + // migrate tools _ "github.com/golang-migrate/migrate/v4/database/postgres" - _ "github.com/golang-migrate/migrate/v4/source/file" + "github.com/golang-migrate/migrate/v4/source/iofs" ) const ( @@ -33,8 +35,13 @@ func init() { m *migrate.Migrate ) + d, err := iofs.New(kompanion.Migrations, "migrations") + if err != nil { + log.Fatal(err) + } + for attempts > 0 { - m, err = migrate.New("file://migrations", databaseURL) + m, err = migrate.NewWithSourceInstance("iofs", d, databaseURL) if err == nil { break } diff --git a/internal/controller/http/web/router.go b/internal/controller/http/web/router.go index 2871ae3..8a27ca2 100644 --- a/internal/controller/http/web/router.go +++ b/internal/controller/http/web/router.go @@ -4,11 +4,15 @@ import ( "encoding/json" "fmt" "html/template" + "io/fs" + "net/http" + "path/filepath" "time" "github.com/foolin/goview" "github.com/foolin/goview/supports/ginview" "github.com/gin-gonic/gin" + "github.com/vanadium23/kompanion" "github.com/vanadium23/kompanion/internal/auth" "github.com/vanadium23/kompanion/internal/library" "github.com/vanadium23/kompanion/internal/stats" @@ -16,20 +20,6 @@ import ( "github.com/vanadium23/kompanion/pkg/logger" ) -func formatDuration(seconds int) string { - duration := time.Duration(seconds) * time.Second - hours := int(duration.Hours()) - minutes := int(duration.Minutes()) % 60 - secs := int(duration.Seconds()) % 60 - - if hours > 0 { - return fmt.Sprintf("%dh %dm %ds", hours, minutes, secs) - } else if minutes > 0 { - return fmt.Sprintf("%dm %ds", minutes, secs) - } - return fmt.Sprintf("%ds", secs) -} - func NewRouter( handler *gin.Engine, l logger.Interface, @@ -46,7 +36,11 @@ func NewRouter( c.Set("startTime", time.Now()) }) // static files - handler.Static("/static", "web/static") + staticFs, err := fs.Sub(kompanion.WebAssets, "web/static") + if err != nil { + l.Error("Failed to get static files: %v", err) + } + handler.StaticFS("/static", http.FS(staticFs)) config := goview.DefaultConfig config.Root = "web/templates" @@ -71,7 +65,9 @@ func NewRouter( return template.HTMLEscapeString(version) }, } - handler.HTMLRender = ginview.New(config) + gv := ginview.New(config) + gv.SetFileHandler(embeddedFH) + handler.HTMLRender = gv // Home handler.GET("/", func(c *gin.Context) { @@ -103,3 +99,24 @@ func passStandartContext(c *gin.Context, data gin.H) gin.H { data["startTime"] = c.GetTime("startTime") return data } + +func formatDuration(seconds int) string { + duration := time.Duration(seconds) * time.Second + hours := int(duration.Hours()) + minutes := int(duration.Minutes()) % 60 + secs := int(duration.Seconds()) % 60 + + if hours > 0 { + return fmt.Sprintf("%dh %dm %ds", hours, minutes, secs) + } else if minutes > 0 { + return fmt.Sprintf("%dm %ds", minutes, secs) + } + return fmt.Sprintf("%ds", secs) +} + +// https://github.com/foolin/goview/issues/25#issuecomment-876889943 +func embeddedFH(config goview.Config, tmpl string) (string, error) { + path := filepath.Join(config.Root, tmpl) + bytes, err := kompanion.WebAssets.ReadFile(path + config.Extension) + return string(bytes), err +}