http report wip
parent
e3dd8d688a
commit
efaf7190ee
@ -0,0 +1,13 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestAPI(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "API Suite")
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// WriteJsonOrError writes the supplied response to the http.ResponseWriter, handling any errors by logging and
|
||||
// returning an Internal Server Error response (status 500)
|
||||
func WriteJsonOrError(writer http.ResponseWriter, response interface{}) {
|
||||
data, err := json.MarshalIndent(response, "", " ")
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("failed to create json payload")
|
||||
writer.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
_, err = writer.Write(data)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("failed to write response")
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/containrrr/watchtower/pkg/metrics"
|
||||
"net/http"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
// MetricsHandler is a HTTP handler for serving metric data
|
||||
type MetricsHandler struct {
|
||||
Path string
|
||||
Handle http.HandlerFunc
|
||||
Metrics *metrics.Metrics
|
||||
}
|
||||
|
||||
// NewMetricsHandler is a factory function creating a new Metrics instance
|
||||
func NewMetricsHandler() *MetricsHandler {
|
||||
m := metrics.Default()
|
||||
handler := promhttp.Handler()
|
||||
|
||||
return &MetricsHandler{
|
||||
Path: "/v1/metrics",
|
||||
Handle: handler.ServeHTTP,
|
||||
Metrics: m,
|
||||
}
|
||||
}
|
||||
|
||||
func MetricsEndpoint() (path string, handler http.HandlerFunc) {
|
||||
mh := NewMetricsHandler()
|
||||
return mh.Path, mh.Handle
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/containrrr/watchtower/pkg/metrics"
|
||||
"net/http"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
// Handler is an HTTP handle for serving metric data
|
||||
type Handler struct {
|
||||
Path string
|
||||
Handle http.HandlerFunc
|
||||
Metrics *metrics.Metrics
|
||||
}
|
||||
|
||||
// New is a factory function creating a new Metrics instance
|
||||
func New() *Handler {
|
||||
m := metrics.Default()
|
||||
handler := promhttp.Handler()
|
||||
|
||||
return &Handler{
|
||||
Path: "/v1/metrics",
|
||||
Handle: handler.ServeHTTP,
|
||||
Metrics: m,
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/containrrr/watchtower/pkg/session"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func ReportEndpoint(reportPtr **session.Report) (path string, handler http.HandlerFunc) {
|
||||
path = "/v1/report"
|
||||
handler = func(writer http.ResponseWriter, request *http.Request) {
|
||||
WriteJsonOrError(writer, *reportPtr)
|
||||
}
|
||||
return path, handler
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/containrrr/watchtower/pkg/metrics"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
lock chan bool
|
||||
)
|
||||
|
||||
// NewUpdateHandler is a factory function creating a new Handler instance
|
||||
func NewUpdateHandler(updateFn func() *metrics.Metric, updateLock chan bool) *UpdateHandler {
|
||||
if updateLock != nil {
|
||||
lock = updateLock
|
||||
} else {
|
||||
lock = make(chan bool, 1)
|
||||
lock <- true
|
||||
}
|
||||
|
||||
return &UpdateHandler{
|
||||
fn: updateFn,
|
||||
Path: "/v1/update",
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateEndpoint(updateFn func() *metrics.Metric, updateLock chan bool) (path string, handler http.HandlerFunc) {
|
||||
uh := NewUpdateHandler(updateFn, updateLock)
|
||||
return uh.Path, uh.Handle
|
||||
}
|
||||
|
||||
// UpdateHandler is an API handler used for triggering container update scans
|
||||
type UpdateHandler struct {
|
||||
fn func() *metrics.Metric
|
||||
Path string
|
||||
}
|
||||
|
||||
// Handle is the actual http.Handle function doing all the heavy lifting
|
||||
func (handler *UpdateHandler) Handle(w http.ResponseWriter, _ *http.Request) {
|
||||
log.Info("Updates triggered by HTTP API request.")
|
||||
|
||||
result := updateResult{}
|
||||
|
||||
select {
|
||||
case chanValue := <-lock:
|
||||
defer func() { lock <- chanValue }()
|
||||
metric := handler.fn()
|
||||
metrics.RegisterScan(metric)
|
||||
result.Result = metric
|
||||
result.Skipped = false
|
||||
default:
|
||||
log.Debug("Skipped. Another update already running.")
|
||||
result.Skipped = true
|
||||
}
|
||||
WriteJsonOrError(w, result)
|
||||
}
|
||||
|
||||
type updateResult struct {
|
||||
Skipped bool
|
||||
Result *metrics.Metric
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
lock chan bool
|
||||
)
|
||||
|
||||
// New is a factory function creating a new Handler instance
|
||||
func New(updateFn func(), updateLock chan bool) *Handler {
|
||||
if updateLock != nil {
|
||||
lock = updateLock
|
||||
} else {
|
||||
lock = make(chan bool, 1)
|
||||
lock <- true
|
||||
}
|
||||
|
||||
return &Handler{
|
||||
fn: updateFn,
|
||||
Path: "/v1/update",
|
||||
}
|
||||
}
|
||||
|
||||
// Handler is an API handler used for triggering container update scans
|
||||
type Handler struct {
|
||||
fn func()
|
||||
Path string
|
||||
}
|
||||
|
||||
// Handle is the actual http.Handle function doing all the heavy lifting
|
||||
func (handle *Handler) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
log.Info("Updates triggered by HTTP API request.")
|
||||
|
||||
_, err := io.Copy(os.Stdout, r.Body)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case chanValue := <-lock:
|
||||
defer func() { lock <- chanValue }()
|
||||
handle.fn()
|
||||
default:
|
||||
log.Debug("Skipped. Another update already running.")
|
||||
}
|
||||
|
||||
}
|
@ -1,90 +1,78 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
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
|
||||
type Report struct {
|
||||
Started time.Time
|
||||
Ended time.Time
|
||||
Trigger Trigger
|
||||
Scanned []*ContainerStatus
|
||||
Updated []*ContainerStatus
|
||||
Failed []*ContainerStatus
|
||||
Skipped []*ContainerStatus
|
||||
Stale []*ContainerStatus
|
||||
Fresh []*ContainerStatus
|
||||
}
|
||||
|
||||
// NewReport creates a types.Report from the supplied Progress
|
||||
func NewReport(progress Progress) types.Report {
|
||||
report := &report{
|
||||
scanned: []types.ContainerReport{},
|
||||
updated: []types.ContainerReport{},
|
||||
failed: []types.ContainerReport{},
|
||||
skipped: []types.ContainerReport{},
|
||||
stale: []types.ContainerReport{},
|
||||
fresh: []types.ContainerReport{},
|
||||
// s.Started, time.Now().UTC(), s.Trigger, s.Progress
|
||||
func NewReport(started, ended time.Time, trigger Trigger, progress Progress) *Report {
|
||||
report := &Report{
|
||||
Started: started,
|
||||
Ended: ended,
|
||||
Trigger: trigger,
|
||||
Scanned: []*ContainerStatus{},
|
||||
Updated: []*ContainerStatus{},
|
||||
Failed: []*ContainerStatus{},
|
||||
Skipped: []*ContainerStatus{},
|
||||
Stale: []*ContainerStatus{},
|
||||
Fresh: []*ContainerStatus{},
|
||||
}
|
||||
|
||||
for _, update := range progress {
|
||||
if update.state == SkippedState {
|
||||
report.skipped = append(report.skipped, update)
|
||||
if update.State == SkippedState {
|
||||
report.Skipped = append(report.Skipped, update)
|
||||
continue
|
||||
}
|
||||
|
||||
report.scanned = append(report.scanned, update)
|
||||
if update.newImage == update.oldImage {
|
||||
update.state = FreshState
|
||||
report.fresh = append(report.fresh, update)
|
||||
report.Scanned = append(report.Scanned, update)
|
||||
if update.NewImageID == update.OldImageID {
|
||||
update.State = FreshState
|
||||
report.Fresh = append(report.Fresh, update)
|
||||
continue
|
||||
}
|
||||
|
||||
switch update.state {
|
||||
switch update.State {
|
||||
case UpdatedState:
|
||||
report.updated = append(report.updated, update)
|
||||
report.Updated = append(report.Updated, update)
|
||||
case FailedState:
|
||||
report.failed = append(report.failed, update)
|
||||
report.Failed = append(report.Failed, update)
|
||||
default:
|
||||
update.state = StaleState
|
||||
report.stale = append(report.stale, update)
|
||||
update.State = StaleState
|
||||
report.Stale = append(report.Stale, update)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(sortableContainers(report.scanned))
|
||||
sort.Sort(sortableContainers(report.updated))
|
||||
sort.Sort(sortableContainers(report.failed))
|
||||
sort.Sort(sortableContainers(report.skipped))
|
||||
sort.Sort(sortableContainers(report.stale))
|
||||
sort.Sort(sortableContainers(report.fresh))
|
||||
sort.Sort(sortableContainers(report.Scanned))
|
||||
sort.Sort(sortableContainers(report.Updated))
|
||||
sort.Sort(sortableContainers(report.Failed))
|
||||
sort.Sort(sortableContainers(report.Skipped))
|
||||
sort.Sort(sortableContainers(report.Stale))
|
||||
sort.Sort(sortableContainers(report.Fresh))
|
||||
|
||||
return report
|
||||
}
|
||||
|
||||
type sortableContainers []types.ContainerReport
|
||||
type sortableContainers []*ContainerStatus
|
||||
|
||||
// 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() }
|
||||
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,24 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
Trigger Trigger
|
||||
Started time.Time
|
||||
Progress Progress
|
||||
}
|
||||
|
||||
func New(trigger Trigger) *Session {
|
||||
return &Session{
|
||||
Started: time.Now().UTC(),
|
||||
Trigger: trigger,
|
||||
Progress: Progress{},
|
||||
}
|
||||
}
|
||||
|
||||
// Report creates a new Report from a Session instance
|
||||
func (s Session) Report() *Report {
|
||||
return NewReport(s.Started, time.Now().UTC(), s.Trigger, s.Progress)
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package session
|
||||
|
||||
import "strings"
|
||||
|
||||
type Trigger int
|
||||
|
||||
const (
|
||||
SchedulerTrigger Trigger = iota
|
||||
APITrigger
|
||||
StartupTrigger
|
||||
)
|
||||
|
||||
// String returns a string representation of the Trigger
|
||||
func (trigger Trigger) String() string {
|
||||
switch trigger {
|
||||
case SchedulerTrigger:
|
||||
return "Scheduler"
|
||||
case APITrigger:
|
||||
return "API"
|
||||
case StartupTrigger:
|
||||
return "Startup"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalJSON marshals Trigger as a quoted string
|
||||
func (trigger Trigger) MarshalJSON() ([]byte, error) {
|
||||
sb := strings.Builder{}
|
||||
sb.WriteString(`"`)
|
||||
sb.WriteString(trigger.String())
|
||||
sb.WriteString(`"`)
|
||||
return []byte(sb.String()), nil
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package types
|
||||
|
||||
// Notifier is the interface that all notification services have in common
|
||||
type Notifier interface {
|
||||
StartNotification()
|
||||
SendNotification(Report)
|
||||
GetNames() []string
|
||||
Close()
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package types
|
||||
|
||||
// Report contains reports for all the containers processed during a session
|
||||
type Report interface {
|
||||
Scanned() []ContainerReport
|
||||
Updated() []ContainerReport
|
||||
Failed() []ContainerReport
|
||||
Skipped() []ContainerReport
|
||||
Stale() []ContainerReport
|
||||
Fresh() []ContainerReport
|
||||
}
|
||||
|
||||
// ContainerReport represents a container that was included in watchtower session
|
||||
type ContainerReport interface {
|
||||
ID() ContainerID
|
||||
Name() string
|
||||
CurrentImageID() ImageID
|
||||
LatestImageID() ImageID
|
||||
ImageName() string
|
||||
Error() string
|
||||
State() string
|
||||
}
|
Loading…
Reference in New Issue