restructure and fix empty report

This commit is contained in:
nils måsén 2023-10-02 15:30:28 +02:00
parent 0776077ac5
commit 3bcc86cd8f
12 changed files with 359 additions and 343 deletions

View file

@ -89,10 +89,13 @@
console.log("Levels: %o", levels); console.log("Levels: %o", levels);
const output = WATCHTOWER.tplprev(input, states, levels); const output = WATCHTOWER.tplprev(input, states, levels);
console.log('Output: \n%o', output); console.log('Output: \n%o', output);
document.querySelector('#result').innerText = output; if (output.length) {
document.querySelector('#result').innerText = output;
} else {
document.querySelector('#result').innerHTML = '<i>empty (would not be sent as a notification)</i>';
}
} }
const formSubmitted = (e) => { const formSubmitted = (e) => {
console.log("Event: %o", e);
e.preventDefault(); e.preventDefault();
updatePreview(); updatePreview();
} }
@ -115,7 +118,7 @@
<form id="tplprev" onchange="inputUpdated()" onsubmit="formSubmitted(event)" style="margin: 0;display: flex; flex-direction: column; row-gap: 1rem; box-sizing: border-box; position: relative; margin-right: -13.3rem"> <form id="tplprev" onchange="updatePreview()" onsubmit="formSubmitted(event)" style="margin: 0;display: flex; flex-direction: column; row-gap: 1rem; box-sizing: border-box; position: relative; margin-right: -13.3rem">
<pre id="loading" style="position: absolute; inset: 0; display: flex; padding: 1rem; box-sizing: border-box; background: var(--md-code-bg-color); margin-top: 0">loading wasm...</pre> <pre id="loading" style="position: absolute; inset: 0; display: flex; padding: 1rem; box-sizing: border-box; background: var(--md-code-bg-color); margin-top: 0">loading wasm...</pre>

View file

@ -0,0 +1,143 @@
package data
import (
"encoding/hex"
"errors"
"math/rand"
"strconv"
"time"
"github.com/containrrr/watchtower/pkg/types"
)
type previewData struct {
rand *rand.Rand
lastTime time.Time
report *report
containerCount int
Entries []*logEntry
StaticData staticData
}
type staticData struct {
Title string
Host string
}
// New initializes a new preview data struct
func New() *previewData {
return &previewData{
rand: rand.New(rand.NewSource(1)),
lastTime: time.Now().Add(-30 * time.Minute),
report: nil,
containerCount: 0,
Entries: []*logEntry{},
StaticData: staticData{
Title: "Title",
Host: "Host",
},
}
}
// AddFromState adds a container status entry to the report with the given state
func (pb *previewData) AddFromState(state State) {
cid := types.ContainerID(pb.generateID())
old := types.ImageID(pb.generateID())
new := types.ImageID(pb.generateID())
name := pb.generateName()
image := pb.generateImageName(name)
var err error
if state == FailedState {
err = errors.New(pb.randomEntry(errorMessages))
} else if state == SkippedState {
err = errors.New(pb.randomEntry(skippedMessages))
}
pb.addContainer(containerStatus{
containerID: cid,
oldImage: old,
newImage: new,
containerName: name,
imageName: image,
error: err,
state: state,
})
}
func (pb *previewData) addContainer(c containerStatus) {
if pb.report == nil {
pb.report = &report{}
}
switch c.state {
case ScannedState:
pb.report.scanned = append(pb.report.scanned, &c)
case UpdatedState:
pb.report.updated = append(pb.report.updated, &c)
case FailedState:
pb.report.failed = append(pb.report.failed, &c)
case SkippedState:
pb.report.skipped = append(pb.report.skipped, &c)
case StaleState:
pb.report.stale = append(pb.report.stale, &c)
case FreshState:
pb.report.fresh = append(pb.report.fresh, &c)
default:
return
}
pb.containerCount += 1
}
// AddLogEntry adds a preview log entry of the given level
func (pd *previewData) AddLogEntry(level LogLevel) {
var msg string
switch level {
case FatalLevel:
fallthrough
case ErrorLevel:
fallthrough
case WarnLevel:
msg = pd.randomEntry(logErrors)
default:
msg = pd.randomEntry(logMessages)
}
pd.Entries = append(pd.Entries, &logEntry{
Message: msg,
Data: map[string]any{},
Time: pd.generateTime(),
Level: level,
})
}
// Report returns a preview report
func (pb *previewData) Report() types.Report {
return pb.report
}
func (pb *previewData) generateID() string {
buf := make([]byte, 32)
_, _ = pb.rand.Read(buf)
return hex.EncodeToString(buf)
}
func (pb *previewData) generateTime() time.Time {
pb.lastTime = pb.lastTime.Add(time.Duration(pb.rand.Intn(30)) * time.Second)
return pb.lastTime
}
func (pb *previewData) randomEntry(arr []string) string {
return arr[pb.rand.Intn(len(arr))]
}
func (pb *previewData) generateName() string {
index := pb.containerCount
if index <= len(containerNames) {
return "/" + containerNames[index]
}
suffix := index / len(containerNames)
index %= len(containerNames)
return "/" + containerNames[index] + strconv.FormatInt(int64(suffix), 10)
}
func (pb *previewData) generateImageName(name string) string {
index := pb.containerCount % len(organizationNames)
return organizationNames[index] + name + ":latest"
}

View file

@ -1,29 +1,17 @@
package main package data
import ( import (
"time" "time"
"github.com/containrrr/watchtower/pkg/types"
) )
type Data struct { type logEntry struct {
Entries []*LogEntry
StaticData StaticData
Report types.Report
}
type StaticData struct {
Title string
Host string
}
type LogEntry struct {
Message string Message string
Data map[string]any Data map[string]any
Time time.Time Time time.Time
Level LogLevel Level LogLevel
} }
// LogLevel is the analog of logrus.Level
type LogLevel string type LogLevel string
const ( const (
@ -36,6 +24,7 @@ const (
PanicLevel LogLevel = "panic" PanicLevel LogLevel = "panic"
) )
// LevelsFromString parses a string of level characters and returns a slice of the corresponding log levels
func LevelsFromString(str string) []LogLevel { func LevelsFromString(str string) []LogLevel {
levels := make([]LogLevel, 0, len(str)) levels := make([]LogLevel, 0, len(str))
for _, c := range str { for _, c := range str {
@ -61,6 +50,7 @@ func LevelsFromString(str string) []LogLevel {
return levels return levels
} }
// String returns the log level as a string
func (level LogLevel) String() string { func (level LogLevel) String() string {
return string(level) return string(level)
} }

View file

@ -1,4 +1,4 @@
package main package data
var containerNames = []string{ var containerNames = []string{
"cyberscribe", "cyberscribe",
@ -43,7 +43,7 @@ var containerNames = []string{
"innoscan", "innoscan",
} }
var companyNames = []string{ var organizationNames = []string{
"techwave", "techwave",
"codecrafters", "codecrafters",
"innotechlabs", "innotechlabs",

View file

@ -0,0 +1,110 @@
package data
import (
"sort"
"github.com/containrrr/watchtower/pkg/types"
)
// State is the outcome of a container in a session report
type State string
const (
ScannedState State = "scanned"
UpdatedState State = "updated"
FailedState State = "failed"
SkippedState State = "skipped"
StaleState State = "stale"
FreshState State = "fresh"
)
// StatesFromString parses a string of state characters and returns a slice of the corresponding report states
func StatesFromString(str string) []State {
states := make([]State, 0, len(str))
for _, c := range str {
switch c {
case 'c':
states = append(states, ScannedState)
case 'u':
states = append(states, UpdatedState)
case 'e':
states = append(states, FailedState)
case 'k':
states = append(states, SkippedState)
case 't':
states = append(states, StaleState)
case 'f':
states = append(states, FreshState)
default:
continue
}
}
return states
}
type report struct {
scanned []types.ContainerReport
updated []types.ContainerReport
failed []types.ContainerReport
skipped []types.ContainerReport
stale []types.ContainerReport
fresh []types.ContainerReport
}
func (r *report) Scanned() []types.ContainerReport {
return r.scanned
}
func (r *report) Updated() []types.ContainerReport {
return r.updated
}
func (r *report) Failed() []types.ContainerReport {
return r.failed
}
func (r *report) Skipped() []types.ContainerReport {
return r.skipped
}
func (r *report) Stale() []types.ContainerReport {
return r.stale
}
func (r *report) Fresh() []types.ContainerReport {
return r.fresh
}
func (r *report) All() []types.ContainerReport {
allLen := len(r.scanned) + len(r.updated) + len(r.failed) + len(r.skipped) + len(r.stale) + len(r.fresh)
all := make([]types.ContainerReport, 0, allLen)
presentIds := map[types.ContainerID][]string{}
appendUnique := func(reports []types.ContainerReport) {
for _, cr := range reports {
if _, found := presentIds[cr.ID()]; found {
continue
}
all = append(all, cr)
presentIds[cr.ID()] = nil
}
}
appendUnique(r.updated)
appendUnique(r.failed)
appendUnique(r.skipped)
appendUnique(r.stale)
appendUnique(r.fresh)
appendUnique(r.scanned)
sort.Sort(sortableContainers(all))
return all
}
type sortableContainers []types.ContainerReport
// Len implements sort.Interface.Len
func (s sortableContainers) Len() int { return len(s) }
// Less implements sort.Interface.Less
func (s sortableContainers) Less(i, j int) bool { return s[i].ID() < s[j].ID() }
// Swap implements sort.Interface.Swap
func (s sortableContainers) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

View file

@ -0,0 +1,44 @@
package data
import wt "github.com/containrrr/watchtower/pkg/types"
type containerStatus struct {
containerID wt.ContainerID
oldImage wt.ImageID
newImage wt.ImageID
containerName string
imageName string
error
state State
}
func (u *containerStatus) ID() wt.ContainerID {
return u.containerID
}
func (u *containerStatus) Name() string {
return u.containerName
}
func (u *containerStatus) CurrentImageID() wt.ImageID {
return u.oldImage
}
func (u *containerStatus) LatestImageID() wt.ImageID {
return u.newImage
}
func (u *containerStatus) ImageName() string {
return u.imageName
}
func (u *containerStatus) Error() string {
if u.error == nil {
return ""
}
return u.error.Error()
}
func (u *containerStatus) State() string {
return string(u.state)
}

View file

@ -0,0 +1,36 @@
package preview
import (
"fmt"
"strings"
"text/template"
"github.com/containrrr/watchtower/pkg/notifications/preview/data"
"github.com/containrrr/watchtower/pkg/notifications/templates"
)
func Render(input string, states []data.State, loglevels []data.LogLevel) (string, error) {
data := data.New()
tpl, err := template.New("").Funcs(templates.Funcs).Parse(input)
if err != nil {
return "", fmt.Errorf("failed to parse template: %e", err)
}
for _, state := range states {
data.AddFromState(state)
}
for _, level := range loglevels {
data.AddLogEntry(level)
}
var buf strings.Builder
err = tpl.Execute(&buf, data)
if err != nil {
return "", fmt.Errorf("failed to execute template: %e", err)
}
return buf.String(), nil
}

View file

@ -8,10 +8,12 @@ import (
"os" "os"
"github.com/containrrr/watchtower/internal/meta" "github.com/containrrr/watchtower/internal/meta"
"github.com/containrrr/watchtower/pkg/notifications/preview"
"github.com/containrrr/watchtower/pkg/notifications/preview/data"
) )
func main() { func main() {
fmt.Fprintf(os.Stderr, "watchtower/tplprev v%v\n\n", meta.Version) fmt.Fprintf(os.Stderr, "watchtower/tplprev %v\n\n", meta.Version)
var states string var states string
var entries string var entries string
@ -36,7 +38,7 @@ func main() {
return return
} }
result, err := TplPrev(string(input), StatesFromString(states), LevelsFromString(entries)) result, err := preview.Render(string(input), data.StatesFromString(states), data.LevelsFromString(entries))
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Failed to read template file %q: %v\n", flag.Arg(0), err) fmt.Fprintf(os.Stderr, "Failed to read template file %q: %v\n", flag.Arg(0), err)
os.Exit(1) os.Exit(1)

View file

@ -6,6 +6,8 @@ import (
"fmt" "fmt"
"github.com/containrrr/watchtower/internal/meta" "github.com/containrrr/watchtower/internal/meta"
"github.com/containrrr/watchtower/pkg/notifications/preview"
"github.com/containrrr/watchtower/pkg/notifications/preview/data"
"syscall/js" "syscall/js"
) )
@ -29,30 +31,30 @@ func jsTplPrev(this js.Value, args []js.Value) any {
input := args[0].String() input := args[0].String()
statesArg := args[1] statesArg := args[1]
var states []State var states []data.State
if statesArg.Type() == js.TypeString { if statesArg.Type() == js.TypeString {
states = StatesFromString(statesArg.String()) states = data.StatesFromString(statesArg.String())
} else { } else {
for i := 0; i < statesArg.Length(); i++ { for i := 0; i < statesArg.Length(); i++ {
state := State(statesArg.Index(i).String()) state := data.State(statesArg.Index(i).String())
states = append(states, state) states = append(states, state)
} }
} }
levelsArg := args[2] levelsArg := args[2]
var levels []LogLevel var levels []data.LogLevel
if levelsArg.Type() == js.TypeString { if levelsArg.Type() == js.TypeString {
levels = LevelsFromString(statesArg.String()) levels = data.LevelsFromString(statesArg.String())
} else { } else {
for i := 0; i < levelsArg.Length(); i++ { for i := 0; i < levelsArg.Length(); i++ {
level := LogLevel(levelsArg.Index(i).String()) level := data.LogLevel(levelsArg.Index(i).String())
levels = append(levels, level) levels = append(levels, level)
} }
} }
result, err := TplPrev(input, states, levels) result, err := preview.Render(input, states, levels)
if err != nil { if err != nil {
return "Error: " + err.Error() return "Error: " + err.Error()
} }

View file

@ -1,205 +0,0 @@
package main
import (
"encoding/hex"
"errors"
"math/rand"
"sort"
"strconv"
"strings"
"github.com/containrrr/watchtower/pkg/types"
)
type reportBuilder struct {
rand *rand.Rand
report Report
}
func ReportBuilder() *reportBuilder {
return &reportBuilder{
report: Report{},
rand: rand.New(rand.NewSource(1)),
}
}
type buildAction func(*reportBuilder)
func (rb *reportBuilder) Build() types.Report {
return &rb.report
}
func (rb *reportBuilder) AddFromState(state State) {
cid := types.ContainerID(rb.generateID())
old := types.ImageID(rb.generateID())
new := types.ImageID(rb.generateID())
name := rb.generateName()
image := rb.generateImageName(name)
var err error
if state == FailedState {
err = errors.New(rb.randomEntry(errorMessages))
} else if state == SkippedState {
err = errors.New(rb.randomEntry(skippedMessages))
}
rb.AddContainer(ContainerStatus{
containerID: cid,
oldImage: old,
newImage: new,
containerName: name,
imageName: image,
error: err,
state: state,
})
}
func (rb *reportBuilder) AddContainer(c ContainerStatus) {
switch c.state {
case ScannedState:
rb.report.scanned = append(rb.report.scanned, &c)
case UpdatedState:
rb.report.updated = append(rb.report.updated, &c)
case FailedState:
rb.report.failed = append(rb.report.failed, &c)
case SkippedState:
rb.report.skipped = append(rb.report.skipped, &c)
case StaleState:
rb.report.stale = append(rb.report.stale, &c)
case FreshState:
rb.report.fresh = append(rb.report.fresh, &c)
}
}
func (rb *reportBuilder) generateID() string {
buf := make([]byte, 32)
_, _ = rb.rand.Read(buf)
return hex.EncodeToString(buf)
}
func (rb *reportBuilder) randomEntry(arr []string) string {
return arr[rb.rand.Intn(len(arr))]
}
func (rb *reportBuilder) generateName() string {
index := rb.containerCount()
if index <= len(containerNames) {
return containerNames[index]
}
suffix := index / len(containerNames)
index %= len(containerNames)
return containerNames[index] + strconv.FormatInt(int64(suffix), 10)
}
func (rb *reportBuilder) generateImageName(name string) string {
index := rb.containerCount()
return companyNames[index%len(companyNames)] + "/" + strings.ToLower(name) + ":latest"
}
func (rb *reportBuilder) containerCount() int {
return len(rb.report.scanned) +
len(rb.report.updated) +
len(rb.report.failed) +
len(rb.report.skipped) +
len(rb.report.stale) +
len(rb.report.fresh)
}
type State string
const (
ScannedState State = "scanned"
UpdatedState State = "updated"
FailedState State = "failed"
SkippedState State = "skipped"
StaleState State = "stale"
FreshState State = "fresh"
)
type Report struct {
scanned []types.ContainerReport
updated []types.ContainerReport
failed []types.ContainerReport
skipped []types.ContainerReport
stale []types.ContainerReport
fresh []types.ContainerReport
}
func StatesFromString(str string) []State {
states := make([]State, 0, len(str))
for _, c := range str {
switch c {
case 'c':
states = append(states, ScannedState)
case 'u':
states = append(states, UpdatedState)
case 'e':
states = append(states, FailedState)
case 'k':
states = append(states, SkippedState)
case 't':
states = append(states, StaleState)
case 'f':
states = append(states, FreshState)
default:
continue
}
}
return states
}
func (r *Report) Scanned() []types.ContainerReport {
return r.scanned
}
func (r *Report) Updated() []types.ContainerReport {
return r.updated
}
func (r *Report) Failed() []types.ContainerReport {
return r.failed
}
func (r *Report) Skipped() []types.ContainerReport {
return r.skipped
}
func (r *Report) Stale() []types.ContainerReport {
return r.stale
}
func (r *Report) Fresh() []types.ContainerReport {
return r.fresh
}
func (r *Report) All() []types.ContainerReport {
allLen := len(r.scanned) + len(r.updated) + len(r.failed) + len(r.skipped) + len(r.stale) + len(r.fresh)
all := make([]types.ContainerReport, 0, allLen)
presentIds := map[types.ContainerID][]string{}
appendUnique := func(reports []types.ContainerReport) {
for _, cr := range reports {
if _, found := presentIds[cr.ID()]; found {
continue
}
all = append(all, cr)
presentIds[cr.ID()] = nil
}
}
appendUnique(r.updated)
appendUnique(r.failed)
appendUnique(r.skipped)
appendUnique(r.stale)
appendUnique(r.fresh)
appendUnique(r.scanned)
sort.Sort(sortableContainers(all))
return all
}
type sortableContainers []types.ContainerReport
// Len implements sort.Interface.Len
func (s sortableContainers) Len() int { return len(s) }
// Less implements sort.Interface.Less
func (s sortableContainers) Less(i, j int) bool { return s[i].ID() < s[j].ID() }
// Swap implements sort.Interface.Swap
func (s sortableContainers) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

View file

@ -1,51 +0,0 @@
package main
import wt "github.com/containrrr/watchtower/pkg/types"
// ContainerStatus contains the container state during a session
type ContainerStatus struct {
containerID wt.ContainerID
oldImage wt.ImageID
newImage wt.ImageID
containerName string
imageName string
error
state State
}
// ID returns the container ID
func (u *ContainerStatus) ID() wt.ContainerID {
return u.containerID
}
// Name returns the container name
func (u *ContainerStatus) Name() string {
return u.containerName
}
// CurrentImageID returns the image ID that the container used when the session started
func (u *ContainerStatus) CurrentImageID() wt.ImageID {
return u.oldImage
}
// LatestImageID returns the newest image ID found during the session
func (u *ContainerStatus) LatestImageID() wt.ImageID {
return u.newImage
}
// ImageName returns the name:tag that the container uses
func (u *ContainerStatus) ImageName() string {
return u.imageName
}
// Error returns the error (if any) that was encountered for the container during a session
func (u *ContainerStatus) Error() string {
if u.error == nil {
return ""
}
return u.error.Error()
}
func (u *ContainerStatus) State() string {
return string(u.state)
}

View file

@ -1,58 +0,0 @@
package main
import (
"fmt"
"strings"
"text/template"
"time"
"github.com/containrrr/watchtower/pkg/notifications/templates"
)
func TplPrev(input string, states []State, loglevels []LogLevel) (string, error) {
rb := ReportBuilder()
tpl, err := template.New("").Funcs(templates.Funcs).Parse(input)
if err != nil {
return "", fmt.Errorf("failed to parse template: %e", err)
}
for _, state := range states {
rb.AddFromState(state)
}
var entries []*LogEntry
for _, level := range loglevels {
var msg string
if level <= WarnLevel {
msg = rb.randomEntry(logErrors)
} else {
msg = rb.randomEntry(logMessages)
}
entries = append(entries, &LogEntry{
Message: msg,
Data: map[string]any{},
Time: time.Now(),
Level: level,
})
}
report := rb.Build()
data := Data{
Entries: entries,
StaticData: StaticData{
Title: "Title",
Host: "Host",
},
Report: report,
}
var buf strings.Builder
err = tpl.Execute(&buf, data)
if err != nil {
return "", fmt.Errorf("failed to execute template: %e", err)
}
return buf.String(), nil
}