mirror of https://github.com/containrrr/watchtower
				
				
				
			
							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