Remove dependency to badge-gen instance

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2024-04-15 15:08:40 +02:00
parent 3073908498
commit 60d8172559
Signed by: luzifer
SSH key fingerprint: SHA256:/xtE5lCgiRDQr8SLxHMS92ZBlACmATUmF1crK16Ks4E
11 changed files with 225 additions and 20 deletions

31
api.go
View file

@ -5,7 +5,6 @@ import (
"fmt"
"net/http"
"net/url"
"path"
"sort"
"strconv"
"strings"
@ -14,8 +13,9 @@ import (
"github.com/gorilla/feeds"
"github.com/gorilla/mux"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/sirupsen/logrus"
"github.com/Luzifer/go-latestver/internal/badge"
"github.com/Luzifer/go-latestver/internal/config"
"github.com/Luzifer/go-latestver/internal/database"
"github.com/Luzifer/go-latestver/internal/fetcher"
@ -58,7 +58,7 @@ func catalogEntryToAPICatalogEntry(ce database.CatalogEntry) (apiCatalogEntry, e
return apiCatalogEntry{CatalogEntry: ce, CatalogMeta: *cm}, nil
}
func handleBadgeRedirect(w http.ResponseWriter, r *http.Request) {
func handleBadge(w http.ResponseWriter, r *http.Request) {
var (
compare = r.FormValue("compare")
vars = mux.Vars(r)
@ -82,14 +82,11 @@ func handleBadgeRedirect(w http.ResponseWriter, r *http.Request) {
color = "red"
}
target, err := url.Parse(cfg.BadgeGenInstance)
if err != nil {
http.Error(w, "Misconfigured BadgeGenInstance", http.StatusInternalServerError)
return
svg := badge.Create(ce.Key(), cm.CurrentVersion, color)
w.Header().Add("Content-Type", "image/svg+xml")
if _, err = w.Write(svg); err != nil {
logrus.WithError(err).Error("writing SVG response")
}
target.Path = path.Join(target.Path, "static", ce.Key(), cm.CurrentVersion, color)
http.Redirect(w, r, target.String(), http.StatusFound)
}
func handleCatalogGet(w http.ResponseWriter, r *http.Request) {
@ -106,14 +103,14 @@ func handleCatalogGet(w http.ResponseWriter, r *http.Request) {
ae, err := catalogEntryToAPICatalogEntry(ce)
if err != nil {
log.WithError(err).Error("Unable to fetch catalog data")
logrus.WithError(err).Error("Unable to fetch catalog data")
http.Error(w, "Unable to fetch catalog data", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if err = json.NewEncoder(w).Encode(ae); err != nil {
log.WithError(err).Error("Unable to encode catalog entry")
logrus.WithError(err).Error("Unable to encode catalog entry")
http.Error(w, "Unable to encode catalog meta", http.StatusInternalServerError)
return
}
@ -133,7 +130,7 @@ func handleCatalogGetVersion(w http.ResponseWriter, r *http.Request) {
cm, err := storage.Catalog.GetMeta(&ce)
if err != nil {
log.WithError(err).Error("Unable to fetch catalog meta")
logrus.WithError(err).Error("Unable to fetch catalog meta")
http.Error(w, "Unable to fetch catalog meta", http.StatusInternalServerError)
return
}
@ -150,7 +147,7 @@ func handleCatalogList(w http.ResponseWriter, _ *http.Request) {
ae, err := catalogEntryToAPICatalogEntry(ce)
if err != nil {
log.WithError(err).Error("Unable to fetch catalog data")
logrus.WithError(err).Error("Unable to fetch catalog data")
http.Error(w, "Unable to fetch catalog data", http.StatusInternalServerError)
return
}
@ -162,7 +159,7 @@ func handleCatalogList(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(out); err != nil {
log.WithError(err).Error("Unable to encode catalog entry list")
logrus.WithError(err).Error("Unable to encode catalog entry list")
http.Error(w, "Unable to encode catalog meta", http.StatusInternalServerError)
return
}
@ -184,7 +181,7 @@ func handleLog(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if err = json.NewEncoder(w).Encode(logs); err != nil {
log.WithError(err).Error("Unable to encode logs")
logrus.WithError(err).Error("Unable to encode logs")
http.Error(w, "Unable to encode logs", http.StatusInternalServerError)
return
}
@ -229,7 +226,7 @@ func handleLogFeed(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/rss+xml; charset=utf-8")
if err = feed.WriteRss(w); err != nil {
log.WithError(err).Error("Unable to render RSS")
logrus.WithError(err).Error("Unable to render RSS")
http.Error(w, "Unable to render RSS", http.StatusInternalServerError)
return
}

6
go.mod
View file

@ -10,11 +10,15 @@ require (
github.com/antchfx/xpath v1.2.5
github.com/blang/semver/v4 v4.0.0
github.com/go-git/go-git/v5 v5.11.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/gorilla/feeds v1.1.2
github.com/gorilla/mux v1.8.1
github.com/pkg/errors v0.9.1
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.9.0
github.com/tdewolff/minify/v2 v2.20.19
golang.org/x/image v0.15.0
golang.org/x/net v0.22.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.5.5
@ -96,6 +100,7 @@ require (
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.19.0 // indirect
github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.50.0 // indirect
@ -104,6 +109,7 @@ require (
github.com/skeema/knownhosts v1.2.2 // indirect
github.com/spf13/cobra v1.8.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tdewolff/parse/v2 v2.7.12 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect

14
go.sum
View file

@ -140,6 +140,8 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -303,8 +305,9 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@ -314,6 +317,13 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tdewolff/minify/v2 v2.20.19 h1:tX0SR0LUrIqGoLjXnkIzRSIbKJ7PaNnSENLD4CyH6Xo=
github.com/tdewolff/minify/v2 v2.20.19/go.mod h1:ulkFoeAVWMLEyjuDz1ZIWOA31g5aWOawCFRp9R/MudM=
github.com/tdewolff/parse/v2 v2.7.12 h1:tgavkHc2ZDEQVKy1oWxwIyh5bP4F5fEh/JmBwPP/3LQ=
github.com/tdewolff/parse/v2 v2.7.12/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
@ -351,6 +361,8 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=

Binary file not shown.

6
internal/badge/assets.go Normal file
View file

@ -0,0 +1,6 @@
package badge
import "embed"
//go:embed badge.svg.tpl DejaVuSans.ttf
var assets embed.FS

73
internal/badge/badge.go Normal file
View file

@ -0,0 +1,73 @@
// Package badge contains a SVG-badge generator creating a badge from
// title, text and color
package badge
import (
"bytes"
"html/template"
"github.com/tdewolff/minify/v2"
"github.com/tdewolff/minify/v2/svg"
)
const (
xSpacing = 8
)
const (
colorNameBlue = "blue"
colorNameBrightGreen = "brightgreen"
colorNameGray = "gray"
colorNameGreen = "green"
colorNameLightGray = "lightgray"
colorNameOrange = "orange"
colorNameRed = "red"
colorNameYellow = "yellow"
colorNameYellowGreen = "yellowgreen"
)
var colorList = map[string]string{
colorNameBlue: "007ec6",
colorNameBrightGreen: "4c1",
colorNameGray: "555",
colorNameGreen: "97CA00",
colorNameLightGray: "9f9f9f",
colorNameOrange: "fe7d37",
colorNameRed: "e05d44",
colorNameYellow: "dfb317",
colorNameYellowGreen: "a4a61d",
}
// Create renders the badge and returns the SVG in minified but
// uncompressed form
func Create(title, text, color string) []byte {
var buf bytes.Buffer
titleW, _ := calculateTextWidth(title)
textW, _ := calculateTextWidth(text)
width := titleW + textW + 4*xSpacing //nolint:gomnd
t, _ := assets.ReadFile("badge.svg.tpl")
tpl, _ := template.New("svg").Parse(string(t))
if c, ok := colorList[color]; ok {
color = c
}
_ = tpl.Execute(&buf, map[string]any{
"Width": width,
"TitleWidth": titleW + 2*xSpacing,
"Title": title,
"Text": text,
"TitleAnchor": titleW/2 + xSpacing,
"TextAnchor": titleW + textW/2 + 3*xSpacing,
"Color": color,
})
m := minify.New()
m.AddFunc("image/svg+xml", svg.Minify)
out, _ := m.Bytes("image/svg+xml", buf.Bytes())
return out
}

View file

@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" width="{{ .Width }}" height="20">
<linearGradient id="b" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1" />
<stop offset="1" stop-opacity=".1" />
</linearGradient>
<mask id="a">
<rect width="{{ .Width }}" height="20" rx="3" fill="#fff" />
</mask>
<g mask="url(#a)">
<path fill="#555" d="M0 0 h{{ .TitleWidth }} v20 H0 z" />
<path fill="#{{ .Color }}" d="M{{ .TitleWidth }} 0 H{{ .Width }} v20 H{{ .TitleWidth }} z" />
<path fill="url(#b)" d="M0 0 h{{ .Width }} v20 H0 z" />
</g>
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
<text x="{{ .TitleAnchor }}" y="15" fill="#010101" fill-opacity=".3">{{ .Title }}</text>
<text x="{{ .TitleAnchor }}" y="14" >{{ .Title }}</text>
<text x="{{ .TextAnchor }}" y="15" fill="#010101" fill-opacity=".3">{{ .Text }}</text>
<text x="{{ .TextAnchor }}" y="14" >{{ .Text }}</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,24 @@
package badge
import (
"html/template"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseBadgeTemplate(t *testing.T) {
raw, err := assets.ReadFile("badge.svg.tpl")
require.NoError(t, err)
_, err = template.New("svg").Parse(string(raw))
require.NoError(t, err)
}
func TestRenderBadge(t *testing.T) {
badge := Create("golang", "test", "green")
assert.Equal(t,
[]byte(`<svg xmlns="http://www.w3.org/2000/svg" width="90" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="90" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h53v20H0z"/><path fill="#97ca00" d="M53 0H90v20H53z"/><path fill="url(#b)" d="M0 0h90v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="26" y="15" fill="#010101" fill-opacity=".3">golang</text><text x="26" y="14">golang</text><text x="71" y="15" fill="#010101" fill-opacity=".3">test</text><text x="71" y="14">test</text></g></svg>`),
badge)
}

41
internal/badge/font.go Normal file
View file

@ -0,0 +1,41 @@
package badge
// Some of this code (namely the code for computing the
// width of a string in a given font) was copied from
// code.google.com/p/freetype-go/freetype/ which includes
// the following copyright notice:
// Copyright 2010 The Freetype-Go Authors. All rights reserved.
import (
"github.com/golang/freetype/truetype"
"github.com/pkg/errors"
"golang.org/x/image/math/fixed"
)
const (
fontSize = 11
)
func calculateTextWidth(text string) (int, error) {
binFont, _ := assets.ReadFile("DejaVuSans.ttf")
font, err := truetype.Parse(binFont)
if err != nil {
return 0, errors.Wrap(err, "parsing truetype font")
}
scale := fontSize / float64(font.FUnitsPerEm())
width := 0
prev, hasPrev := truetype.Index(0), false
for _, rune := range text {
fUnitsPerEm := fixed.Int26_6(font.FUnitsPerEm())
index := font.Index(rune)
if hasPrev {
width += int(font.Kern(fUnitsPerEm, prev, index))
}
width += int(font.HMetric(fUnitsPerEm, index).AdvanceWidth)
prev, hasPrev = index, true
}
return int(float64(width) * scale), nil
}

View file

@ -0,0 +1,27 @@
package badge
import (
"crypto/sha256"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEmbeddedFontHash(t *testing.T) {
// Check the embedded font did not change
font, err := assets.ReadFile("DejaVuSans.ttf")
require.NoError(t, err)
assert.Equal(t,
"3fdf69cabf06049ea70a00b5919340e2ce1e6d02b0cc3c4b44fb6801bd1e0d22",
fmt.Sprintf("%x", sha256.Sum256(font)))
}
func TestStringLength(t *testing.T) {
// As the font is embedded into the source the length calculation should not change
w, err := calculateTextWidth("Test 123 öäüß … !@#%&")
require.NoError(t, err)
assert.Equal(t, 138, w)
}

View file

@ -20,7 +20,6 @@ import (
var (
cfg = struct {
BadgeGenInstance string `flag:"badge-gen-instance" default:"https://badges.fyi/" description:"Where to find the badge-gen instance to use badges from"`
BaseURL string `flag:"base-url" default:"https://example.com/" description:"Base-URL the application is reachable at"`
Config string `flag:"config,c" default:"config.yaml" description:"Configuration file with catalog entries"`
Listen string `flag:"listen" default:":3000" description:"Port/IP to listen on"`
@ -108,7 +107,7 @@ func main() {
router.HandleFunc("/v1/catalog/{name}/{tag}/version", handleCatalogGetVersion).Methods(http.MethodGet)
router.HandleFunc("/v1/log", handleLog).Methods(http.MethodGet)
router.HandleFunc("/{name}/{tag}.svg", handleBadgeRedirect).Methods(http.MethodGet).Name("catalog-entry-badge")
router.HandleFunc("/{name}/{tag}.svg", handleBadge).Methods(http.MethodGet).Name("catalog-entry-badge")
router.HandleFunc("/{name}/{tag}/log.rss", handleLogFeed).Methods(http.MethodGet).Name("catalog-entry-rss")
router.HandleFunc("/log.rss", handleLogFeed).Methods(http.MethodGet).Name("log-rss")