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