parent
9b28fbc24d
commit
9180e9558e
@ -0,0 +1,219 @@
|
|||||||
|
<style>
|
||||||
|
#tplprev {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
margin-right: -13.3rem
|
||||||
|
}
|
||||||
|
#tplprev textarea {
|
||||||
|
box-decoration-break: slice;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0.77em 1.18em;
|
||||||
|
scrollbar-color: var(--md-default-fg-color--lighter) transparent;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
touch-action: auto;
|
||||||
|
word-break: normal;
|
||||||
|
height: 420px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
#tplprev .controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
column-gap: 0.5rem
|
||||||
|
}
|
||||||
|
#tplprev textarea, #tplprev input {
|
||||||
|
background-color: var(--md-code-bg-color);
|
||||||
|
border-width: 0;
|
||||||
|
border-radius: 0.1rem;
|
||||||
|
color: var(--md-code-fg-color);
|
||||||
|
font-feature-settings: "kern";
|
||||||
|
font-family: var(--md-code-font-family);
|
||||||
|
}
|
||||||
|
.numfield {
|
||||||
|
font-size: .7rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
#tplprev button {
|
||||||
|
border-radius: 0.1rem;
|
||||||
|
color: var(--md-typeset-color);
|
||||||
|
background-color: var(--md-primary-fg-color);
|
||||||
|
flex:1;
|
||||||
|
min-width: 12ch;
|
||||||
|
padding: 0.5rem
|
||||||
|
}
|
||||||
|
#tplprev button:hover {
|
||||||
|
background-color: var(--md-accent-fg-color);
|
||||||
|
}
|
||||||
|
#tplprev input[type="number"] { width: 5ch; flex: 1; font-size: 1rem; }
|
||||||
|
#tplprev fieldset {
|
||||||
|
margin-top: -0.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
column-gap: 0.5rem;
|
||||||
|
}
|
||||||
|
#tplprev .template-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex:1;
|
||||||
|
column-gap: 1rem;
|
||||||
|
}
|
||||||
|
#tplprev .result-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
display: flex
|
||||||
|
}
|
||||||
|
#result {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
background-color: var(--md-code-bg-color);
|
||||||
|
scrollbar-color: var(--md-default-fg-color--lighter) transparent;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
touch-action: auto;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0.77em 1.18em;
|
||||||
|
margin:0;
|
||||||
|
height: 540px;
|
||||||
|
flex:1;
|
||||||
|
width:100%
|
||||||
|
}
|
||||||
|
#tplprev .loading {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
padding: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: var(--md-code-bg-color);
|
||||||
|
margin-top: 0
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="../assets/wasm_exec.js"></script>
|
||||||
|
<script>
|
||||||
|
const updatePreview = () => {
|
||||||
|
const form = document.querySelector('#tplprev');
|
||||||
|
const input = form.template.value;
|
||||||
|
console.log('Input: %o', input);
|
||||||
|
const arrFromCount = (key) => Array.from(Array(form[key]?.valueAsNumber ?? 0), () => key);
|
||||||
|
const states = form.enablereport.checked ? [
|
||||||
|
...arrFromCount("skipped"),
|
||||||
|
...arrFromCount("scanned"),
|
||||||
|
...arrFromCount("updated"),
|
||||||
|
...arrFromCount("failed" ),
|
||||||
|
...arrFromCount("fresh" ),
|
||||||
|
...arrFromCount("stale" ),
|
||||||
|
] : [];
|
||||||
|
console.log("States: %o", states);
|
||||||
|
const levels = form.enablelog.checked ? [
|
||||||
|
...arrFromCount("error"),
|
||||||
|
...arrFromCount("warning"),
|
||||||
|
...arrFromCount("info"),
|
||||||
|
...arrFromCount("debug"),
|
||||||
|
] : [];
|
||||||
|
console.log("Levels: %o", levels);
|
||||||
|
const output = WATCHTOWER.tplprev(input, states, levels);
|
||||||
|
console.log('Output: \n%o', 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) => {
|
||||||
|
e.preventDefault();
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
let debounce;
|
||||||
|
const inputUpdated = () => {
|
||||||
|
if(debounce) clearTimeout(debounce);
|
||||||
|
debounce = setTimeout(() => updatePreview(), 400);
|
||||||
|
}
|
||||||
|
const formChanged = (e) => {
|
||||||
|
console.log('form changed: %o', e);
|
||||||
|
}
|
||||||
|
const go = new Go();
|
||||||
|
WebAssembly.instantiateStreaming(fetch("../assets/tplprev.wasm"), go.importObject).then((result) => {
|
||||||
|
document.querySelector('#tplprev .loading').style.display = "none";
|
||||||
|
go.run(result.instance);
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<form id="tplprev" onchange="updatePreview()" onsubmit="formSubmitted(event)">
|
||||||
|
<pre class="loading">loading wasm...</pre>
|
||||||
|
<div class="template-wrapper">
|
||||||
|
<textarea name="template" type="text" style="flex: 1" onkeyup="inputUpdated()">{{- with .Report -}}
|
||||||
|
{{- if ( or .Updated .Failed ) -}}
|
||||||
|
{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed
|
||||||
|
{{- range .Updated}}
|
||||||
|
- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- range .Fresh}}
|
||||||
|
- {{.Name}} ({{.ImageName}}): {{.State}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- range .Skipped}}
|
||||||
|
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- range .Failed}}
|
||||||
|
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- if (and .Entries .Report) }}
|
||||||
|
|
||||||
|
Logs:
|
||||||
|
{{ end -}}
|
||||||
|
{{range .Entries -}}{{.Time.Format "2006-01-02T15:04:05Z07:00"}} [{{.Level}}] {{.Message}}{{"\n"}}{{- end -}}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<fieldset>
|
||||||
|
<legend><label><input type="checkbox" name="enablereport" checked /> Container report</label></legend>
|
||||||
|
<label class="numfield">
|
||||||
|
Skipped:
|
||||||
|
<input type="number" name="skipped" value="3" />
|
||||||
|
</label>
|
||||||
|
<label class="numfield">
|
||||||
|
Scanned:
|
||||||
|
<input type="number" name="scanned" value="3" />
|
||||||
|
</label>
|
||||||
|
<label class="numfield">
|
||||||
|
Updated:
|
||||||
|
<input type="number" name="updated" value="3" />
|
||||||
|
</label>
|
||||||
|
<label class="numfield">
|
||||||
|
Failed:
|
||||||
|
<input type="number" name="failed" value="3" />
|
||||||
|
</label>
|
||||||
|
<label class="numfield">
|
||||||
|
Fresh:
|
||||||
|
<input type="number" name="fresh" value="3" />
|
||||||
|
</label>
|
||||||
|
<label class="numfield">
|
||||||
|
Stale:
|
||||||
|
<input type="number" name="stale" value="3" />
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend><label><input type="checkbox" name="enablelog" checked /> Log entries</label></legend>
|
||||||
|
<label class="numfield">
|
||||||
|
Error:
|
||||||
|
<input type="number" name="error" value="1" />
|
||||||
|
</label>
|
||||||
|
<label class="numfield">
|
||||||
|
Warning:
|
||||||
|
<input type="number" name="warning" value="2" />
|
||||||
|
</label>
|
||||||
|
<label class="numfield">
|
||||||
|
Info:
|
||||||
|
<input type="number" name="info" value="3" />
|
||||||
|
</label>
|
||||||
|
<label class="numfield">
|
||||||
|
Debug:
|
||||||
|
<input type="number" name="debug" value="4" />
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<button type="submit">Update preview</button>
|
||||||
|
</div>
|
||||||
|
<div style="result-wrapper">
|
||||||
|
<pre id="result"></pre>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -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"
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type logEntry struct {
|
||||||
|
Message string
|
||||||
|
Data map[string]any
|
||||||
|
Time time.Time
|
||||||
|
Level LogLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogLevel is the analog of logrus.Level
|
||||||
|
type LogLevel string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TraceLevel LogLevel = "trace"
|
||||||
|
DebugLevel LogLevel = "debug"
|
||||||
|
InfoLevel LogLevel = "info"
|
||||||
|
WarnLevel LogLevel = "warning"
|
||||||
|
ErrorLevel LogLevel = "error"
|
||||||
|
FatalLevel LogLevel = "fatal"
|
||||||
|
PanicLevel LogLevel = "panic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LevelsFromString parses a string of level characters and returns a slice of the corresponding log levels
|
||||||
|
func LevelsFromString(str string) []LogLevel {
|
||||||
|
levels := make([]LogLevel, 0, len(str))
|
||||||
|
for _, c := range str {
|
||||||
|
switch c {
|
||||||
|
case 'p':
|
||||||
|
levels = append(levels, PanicLevel)
|
||||||
|
case 'f':
|
||||||
|
levels = append(levels, FatalLevel)
|
||||||
|
case 'e':
|
||||||
|
levels = append(levels, ErrorLevel)
|
||||||
|
case 'w':
|
||||||
|
levels = append(levels, WarnLevel)
|
||||||
|
case 'i':
|
||||||
|
levels = append(levels, InfoLevel)
|
||||||
|
case 'd':
|
||||||
|
levels = append(levels, DebugLevel)
|
||||||
|
case 't':
|
||||||
|
levels = append(levels, TraceLevel)
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return levels
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the log level as a string
|
||||||
|
func (level LogLevel) String() string {
|
||||||
|
return string(level)
|
||||||
|
}
|
@ -0,0 +1,178 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
var containerNames = []string{
|
||||||
|
"cyberscribe",
|
||||||
|
"datamatrix",
|
||||||
|
"nexasync",
|
||||||
|
"quantumquill",
|
||||||
|
"aerosphere",
|
||||||
|
"virtuos",
|
||||||
|
"fusionflow",
|
||||||
|
"neuralink",
|
||||||
|
"pixelpulse",
|
||||||
|
"synthwave",
|
||||||
|
"codecraft",
|
||||||
|
"zapzone",
|
||||||
|
"robologic",
|
||||||
|
"dreamstream",
|
||||||
|
"infinisync",
|
||||||
|
"megamesh",
|
||||||
|
"novalink",
|
||||||
|
"xenogenius",
|
||||||
|
"ecosim",
|
||||||
|
"innovault",
|
||||||
|
"techtracer",
|
||||||
|
"fusionforge",
|
||||||
|
"quantumquest",
|
||||||
|
"neuronest",
|
||||||
|
"codefusion",
|
||||||
|
"datadyno",
|
||||||
|
"pixelpioneer",
|
||||||
|
"vortexvision",
|
||||||
|
"cybercraft",
|
||||||
|
"synthsphere",
|
||||||
|
"infinitescript",
|
||||||
|
"roborhythm",
|
||||||
|
"dreamengine",
|
||||||
|
"aquasync",
|
||||||
|
"geniusgrid",
|
||||||
|
"megamind",
|
||||||
|
"novasync-pro",
|
||||||
|
"xenonwave",
|
||||||
|
"ecologic",
|
||||||
|
"innoscan",
|
||||||
|
}
|
||||||
|
|
||||||
|
var organizationNames = []string{
|
||||||
|
"techwave",
|
||||||
|
"codecrafters",
|
||||||
|
"innotechlabs",
|
||||||
|
"fusionsoft",
|
||||||
|
"cyberpulse",
|
||||||
|
"quantumscribe",
|
||||||
|
"datadynamo",
|
||||||
|
"neuralink",
|
||||||
|
"pixelpro",
|
||||||
|
"synthwizards",
|
||||||
|
"virtucorplabs",
|
||||||
|
"robologic",
|
||||||
|
"dreamstream",
|
||||||
|
"novanest",
|
||||||
|
"megamind",
|
||||||
|
"xenonwave",
|
||||||
|
"ecologic",
|
||||||
|
"innosync",
|
||||||
|
"techgenius",
|
||||||
|
"nexasoft",
|
||||||
|
"codewave",
|
||||||
|
"zapzone",
|
||||||
|
"techsphere",
|
||||||
|
"aquatech",
|
||||||
|
"quantumcraft",
|
||||||
|
"neuronest",
|
||||||
|
"datafusion",
|
||||||
|
"pixelpioneer",
|
||||||
|
"synthsphere",
|
||||||
|
"infinitescribe",
|
||||||
|
"roborhythm",
|
||||||
|
"dreamengine",
|
||||||
|
"vortexvision",
|
||||||
|
"geniusgrid",
|
||||||
|
"megamesh",
|
||||||
|
"novasync",
|
||||||
|
"xenogeniuslabs",
|
||||||
|
"ecosim",
|
||||||
|
"innovault",
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorMessages = []string{
|
||||||
|
"Error 404: Resource not found",
|
||||||
|
"Critical Error: System meltdown imminent",
|
||||||
|
"Error 500: Internal server error",
|
||||||
|
"Invalid input: Please check your data",
|
||||||
|
"Access denied: Unauthorized access detected",
|
||||||
|
"Network connection lost: Please check your connection",
|
||||||
|
"Error 403: Forbidden access",
|
||||||
|
"Fatal error: System crash imminent",
|
||||||
|
"File not found: Check the file path",
|
||||||
|
"Invalid credentials: Authentication failed",
|
||||||
|
"Error 502: Bad Gateway",
|
||||||
|
"Database connection failed: Please try again later",
|
||||||
|
"Security breach detected: Take immediate action",
|
||||||
|
"Error 400: Bad request",
|
||||||
|
"Out of memory: Close unnecessary applications",
|
||||||
|
"Invalid configuration: Check your settings",
|
||||||
|
"Error 503: Service unavailable",
|
||||||
|
"File is read-only: Cannot modify",
|
||||||
|
"Data corruption detected: Backup your data",
|
||||||
|
"Error 401: Unauthorized",
|
||||||
|
"Disk space full: Free up disk space",
|
||||||
|
"Connection timeout: Retry your request",
|
||||||
|
"Error 504: Gateway timeout",
|
||||||
|
"File access denied: Permission denied",
|
||||||
|
"Unexpected error: Please contact support",
|
||||||
|
"Error 429: Too many requests",
|
||||||
|
"Invalid URL: Check the URL format",
|
||||||
|
"Database query failed: Try again later",
|
||||||
|
"Error 408: Request timeout",
|
||||||
|
"File is in use: Close the file and try again",
|
||||||
|
"Invalid parameter: Check your input",
|
||||||
|
"Error 502: Proxy error",
|
||||||
|
"Database connection lost: Reconnect and try again",
|
||||||
|
"File size exceeds limit: Reduce the file size",
|
||||||
|
"Error 503: Overloaded server",
|
||||||
|
"Operation aborted: Try again",
|
||||||
|
"Invalid API key: Check your API key",
|
||||||
|
"Error 507: Insufficient storage",
|
||||||
|
"Database deadlock: Retry your transaction",
|
||||||
|
"Error 405: Method not allowed",
|
||||||
|
"File format not supported: Choose a different format",
|
||||||
|
"Unknown error: Contact system administrator",
|
||||||
|
}
|
||||||
|
|
||||||
|
var skippedMessages = []string{
|
||||||
|
"Fear of introducing new bugs",
|
||||||
|
"Don't have time for the update process",
|
||||||
|
"Current version works fine for my needs",
|
||||||
|
"Concerns about compatibility with other software",
|
||||||
|
"Limited bandwidth for downloading updates",
|
||||||
|
"Worries about losing custom settings or configurations",
|
||||||
|
"Lack of trust in the software developer's updates",
|
||||||
|
"Dislike changes to the user interface",
|
||||||
|
"Avoiding potential subscription fees",
|
||||||
|
"Suspicion of hidden data collection in updates",
|
||||||
|
"Apprehension about changes in privacy policies",
|
||||||
|
"Prefer the older version's features or design",
|
||||||
|
"Worry about software becoming more resource-intensive",
|
||||||
|
"Avoiding potential changes in licensing terms",
|
||||||
|
"Waiting for initial bugs to be resolved in the update",
|
||||||
|
"Concerns about update breaking third-party plugins or extensions",
|
||||||
|
"Belief that the software is already secure enough",
|
||||||
|
"Don't want to relearn how to use the software",
|
||||||
|
"Fear of losing access to older file formats",
|
||||||
|
"Avoiding the hassle of having to update multiple devices",
|
||||||
|
}
|
||||||
|
|
||||||
|
var logMessages = []string{
|
||||||
|
"Checking for available updates...",
|
||||||
|
"Downloading update package...",
|
||||||
|
"Verifying update integrity...",
|
||||||
|
"Preparing to install update...",
|
||||||
|
"Backing up existing configuration...",
|
||||||
|
"Installing update...",
|
||||||
|
"Update installation complete.",
|
||||||
|
"Applying configuration settings...",
|
||||||
|
"Cleaning up temporary files...",
|
||||||
|
"Update successful! Software is now up-to-date.",
|
||||||
|
"Restarting the application...",
|
||||||
|
"Restart complete. Enjoy the latest features!",
|
||||||
|
"Update rollback complete. Your software remains at the previous version.",
|
||||||
|
}
|
||||||
|
|
||||||
|
var logErrors = []string{
|
||||||
|
"Unable to check for updates. Please check your internet connection.",
|
||||||
|
"Update package download failed. Try again later.",
|
||||||
|
"Update verification failed. Please contact support.",
|
||||||
|
"Update installation failed. Rolling back to the previous version...",
|
||||||
|
"Your configuration settings may have been reset to defaults.",
|
||||||
|
}
|
@ -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] }
|
@ -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)
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"golang.org/x/text/cases"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Funcs = template.FuncMap{
|
||||||
|
"ToUpper": strings.ToUpper,
|
||||||
|
"ToLower": strings.ToLower,
|
||||||
|
"ToJSON": toJSON,
|
||||||
|
"Title": cases.Title(language.AmericanEnglish).String,
|
||||||
|
}
|
||||||
|
|
||||||
|
func toJSON(v interface{}) string {
|
||||||
|
var bytes []byte
|
||||||
|
var err error
|
||||||
|
if bytes, err = json.MarshalIndent(v, "", " "); err != nil {
|
||||||
|
return fmt.Sprintf("failed to marshal JSON in notification template: %v", err)
|
||||||
|
}
|
||||||
|
return string(bytes)
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cd $(git rev-parse --show-toplevel)
|
||||||
|
|
||||||
|
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" ./docs/assets/
|
||||||
|
|
||||||
|
GOARCH=wasm GOOS=js go build -o ./docs/assets/tplprev.wasm ./tplprev
|
@ -0,0 +1,49 @@
|
|||||||
|
//go:build !wasm
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/containrrr/watchtower/internal/meta"
|
||||||
|
"github.com/containrrr/watchtower/pkg/notifications/preview"
|
||||||
|
"github.com/containrrr/watchtower/pkg/notifications/preview/data"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Fprintf(os.Stderr, "watchtower/tplprev %v\n\n", meta.Version)
|
||||||
|
|
||||||
|
var states string
|
||||||
|
var entries string
|
||||||
|
|
||||||
|
flag.StringVar(&states, "states", "cccuuueeekkktttfff", "sCanned, Updated, failEd, sKipped, sTale, Fresh")
|
||||||
|
flag.StringVar(&entries, "entries", "ewwiiidddd", "Fatal,Error,Warn,Info,Debug,Trace")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if len(flag.Args()) < 1 {
|
||||||
|
fmt.Fprintln(os.Stderr, "Missing required argument TEMPLATE")
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
input, err := os.ReadFile(flag.Arg(0))
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to read template file %q: %v\n", flag.Arg(0), err)
|
||||||
|
os.Exit(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := preview.Render(string(input), data.StatesFromString(states), data.LevelsFromString(entries))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to read template file %q: %v\n", flag.Arg(0), err)
|
||||||
|
os.Exit(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(result)
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
//go:build wasm
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/containrrr/watchtower/internal/meta"
|
||||||
|
"github.com/containrrr/watchtower/pkg/notifications/preview"
|
||||||
|
"github.com/containrrr/watchtower/pkg/notifications/preview/data"
|
||||||
|
|
||||||
|
"syscall/js"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("watchtower/tplprev v" + meta.Version)
|
||||||
|
|
||||||
|
js.Global().Set("WATCHTOWER", js.ValueOf(map[string]any{
|
||||||
|
"tplprev": js.FuncOf(jsTplPrev),
|
||||||
|
}))
|
||||||
|
<-make(chan bool)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsTplPrev(this js.Value, args []js.Value) any {
|
||||||
|
|
||||||
|
if len(args) < 3 {
|
||||||
|
return "Requires 3 arguments passed"
|
||||||
|
}
|
||||||
|
|
||||||
|
input := args[0].String()
|
||||||
|
|
||||||
|
statesArg := args[1]
|
||||||
|
var states []data.State
|
||||||
|
|
||||||
|
if statesArg.Type() == js.TypeString {
|
||||||
|
states = data.StatesFromString(statesArg.String())
|
||||||
|
} else {
|
||||||
|
for i := 0; i < statesArg.Length(); i++ {
|
||||||
|
state := data.State(statesArg.Index(i).String())
|
||||||
|
states = append(states, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
levelsArg := args[2]
|
||||||
|
var levels []data.LogLevel
|
||||||
|
|
||||||
|
if levelsArg.Type() == js.TypeString {
|
||||||
|
levels = data.LevelsFromString(statesArg.String())
|
||||||
|
} else {
|
||||||
|
for i := 0; i < levelsArg.Length(); i++ {
|
||||||
|
level := data.LogLevel(levelsArg.Index(i).String())
|
||||||
|
levels = append(levels, level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := preview.Render(input, states, levels)
|
||||||
|
if err != nil {
|
||||||
|
return "Error: " + err.Error()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
Loading…
Reference in New Issue