mirror of
https://github.com/containrrr/watchtower.git
synced 2025-09-21 21:30:48 +02:00
http report wip
This commit is contained in:
parent
e3dd8d688a
commit
efaf7190ee
25 changed files with 350 additions and 284 deletions
30
cmd/root.go
30
cmd/root.go
|
@ -2,6 +2,7 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/containrrr/watchtower/internal/meta"
|
"github.com/containrrr/watchtower/internal/meta"
|
||||||
|
"github.com/containrrr/watchtower/pkg/session"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
@ -10,9 +11,6 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
apiMetrics "github.com/containrrr/watchtower/pkg/api/metrics"
|
|
||||||
"github.com/containrrr/watchtower/pkg/api/update"
|
|
||||||
|
|
||||||
"github.com/containrrr/watchtower/internal/actions"
|
"github.com/containrrr/watchtower/internal/actions"
|
||||||
"github.com/containrrr/watchtower/internal/flags"
|
"github.com/containrrr/watchtower/internal/flags"
|
||||||
"github.com/containrrr/watchtower/pkg/api"
|
"github.com/containrrr/watchtower/pkg/api"
|
||||||
|
@ -34,12 +32,12 @@ var (
|
||||||
noRestart bool
|
noRestart bool
|
||||||
monitorOnly bool
|
monitorOnly bool
|
||||||
enableLabel bool
|
enableLabel bool
|
||||||
notifier t.Notifier
|
notifier notifications.Notifier
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
lifecycleHooks bool
|
lifecycleHooks bool
|
||||||
rollingRestart bool
|
rollingRestart bool
|
||||||
scope string
|
scope string
|
||||||
// Set on build using ldflags
|
latestReport *session.Report
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = NewRootCommand()
|
var rootCmd = NewRootCommand()
|
||||||
|
@ -156,6 +154,7 @@ func Run(c *cobra.Command, names []string) {
|
||||||
runOnce, _ := c.PersistentFlags().GetBool("run-once")
|
runOnce, _ := c.PersistentFlags().GetBool("run-once")
|
||||||
enableUpdateAPI, _ := c.PersistentFlags().GetBool("http-api-update")
|
enableUpdateAPI, _ := c.PersistentFlags().GetBool("http-api-update")
|
||||||
enableMetricsAPI, _ := c.PersistentFlags().GetBool("http-api-metrics")
|
enableMetricsAPI, _ := c.PersistentFlags().GetBool("http-api-metrics")
|
||||||
|
enableReportAPI := true
|
||||||
unblockHTTPAPI, _ := c.PersistentFlags().GetBool("http-api-periodic-polls")
|
unblockHTTPAPI, _ := c.PersistentFlags().GetBool("http-api-periodic-polls")
|
||||||
apiToken, _ := c.PersistentFlags().GetString("http-api-token")
|
apiToken, _ := c.PersistentFlags().GetString("http-api-token")
|
||||||
|
|
||||||
|
@ -171,7 +170,7 @@ func Run(c *cobra.Command, names []string) {
|
||||||
|
|
||||||
if runOnce {
|
if runOnce {
|
||||||
writeStartupMessage(c, time.Time{}, filterDesc)
|
writeStartupMessage(c, time.Time{}, filterDesc)
|
||||||
runUpdatesWithNotifications(filter)
|
runUpdatesWithNotifications(filter, session.StartupTrigger)
|
||||||
notifier.Close()
|
notifier.Close()
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
return
|
return
|
||||||
|
@ -188,13 +187,17 @@ func Run(c *cobra.Command, names []string) {
|
||||||
httpAPI := api.New(apiToken)
|
httpAPI := api.New(apiToken)
|
||||||
|
|
||||||
if enableUpdateAPI {
|
if enableUpdateAPI {
|
||||||
updateHandler := update.New(func() { runUpdatesWithNotifications(filter) }, updateLock)
|
httpAPI.RegisterFunc(api.UpdateEndpoint(func() *metrics.Metric {
|
||||||
httpAPI.RegisterFunc(updateHandler.Path, updateHandler.Handle)
|
return runUpdatesWithNotifications(filter, session.APITrigger)
|
||||||
|
}, updateLock))
|
||||||
}
|
}
|
||||||
|
|
||||||
if enableMetricsAPI {
|
if enableMetricsAPI {
|
||||||
metricsHandler := apiMetrics.New()
|
httpAPI.RegisterHandler(api.MetricsEndpoint())
|
||||||
httpAPI.RegisterHandler(metricsHandler.Path, metricsHandler.Handle)
|
}
|
||||||
|
|
||||||
|
if enableReportAPI {
|
||||||
|
httpAPI.RegisterHandler(api.ReportEndpoint(&latestReport))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil {
|
if err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil {
|
||||||
|
@ -293,7 +296,7 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter, filtering string,
|
||||||
select {
|
select {
|
||||||
case v := <-lock:
|
case v := <-lock:
|
||||||
defer func() { lock <- v }()
|
defer func() { lock <- v }()
|
||||||
metric := runUpdatesWithNotifications(filter)
|
metric := runUpdatesWithNotifications(filter, session.SchedulerTrigger)
|
||||||
metrics.RegisterScan(metric)
|
metrics.RegisterScan(metric)
|
||||||
default:
|
default:
|
||||||
// Update was skipped
|
// Update was skipped
|
||||||
|
@ -327,7 +330,7 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter, filtering string,
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric {
|
func runUpdatesWithNotifications(filter t.Filter, trigger session.Trigger) *metrics.Metric {
|
||||||
notifier.StartNotification()
|
notifier.StartNotification()
|
||||||
updateParams := t.UpdateParams{
|
updateParams := t.UpdateParams{
|
||||||
Filter: filter,
|
Filter: filter,
|
||||||
|
@ -338,10 +341,11 @@ func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric {
|
||||||
LifecycleHooks: lifecycleHooks,
|
LifecycleHooks: lifecycleHooks,
|
||||||
RollingRestart: rollingRestart,
|
RollingRestart: rollingRestart,
|
||||||
}
|
}
|
||||||
result, err := actions.Update(client, updateParams)
|
result, err := actions.Update(client, updateParams, trigger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
}
|
}
|
||||||
|
latestReport = result
|
||||||
notifier.SendNotification(result)
|
notifier.SendNotification(result)
|
||||||
metricResults := metrics.NewMetric(result)
|
metricResults := metrics.NewMetric(result)
|
||||||
log.Debugf("Session done: %v scanned, %v updated, %v failed",
|
log.Debugf("Session done: %v scanned, %v updated, %v failed",
|
||||||
|
|
|
@ -8,10 +8,11 @@ import (
|
||||||
|
|
||||||
// CreateMockProgressReport creates a mock report from a given set of container states
|
// CreateMockProgressReport creates a mock report from a given set of container states
|
||||||
// All containers will be given a unique ID and name based on its state and index
|
// All containers will be given a unique ID and name based on its state and index
|
||||||
func CreateMockProgressReport(states ...session.State) wt.Report {
|
func CreateMockProgressReport(states ...session.State) *session.Report {
|
||||||
|
|
||||||
stateNums := make(map[session.State]int)
|
stateNums := make(map[session.State]int)
|
||||||
progress := session.Progress{}
|
mockSession := session.New(session.SchedulerTrigger)
|
||||||
|
progress := mockSession.Progress
|
||||||
failed := make(map[wt.ContainerID]error)
|
failed := make(map[wt.ContainerID]error)
|
||||||
|
|
||||||
for _, state := range states {
|
for _, state := range states {
|
||||||
|
@ -41,6 +42,6 @@ func CreateMockProgressReport(states ...session.State) wt.Report {
|
||||||
}
|
}
|
||||||
progress.UpdateFailed(failed)
|
progress.UpdateFailed(failed)
|
||||||
|
|
||||||
return progress.Report()
|
return mockSession.Report()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,10 @@ import (
|
||||||
// used to start those containers have been updated. If a change is detected in
|
// used to start those containers have been updated. If a change is detected in
|
||||||
// any of the images, the associated containers are stopped and restarted with
|
// any of the images, the associated containers are stopped and restarted with
|
||||||
// the new image.
|
// the new image.
|
||||||
func Update(client container.Client, params types.UpdateParams) (types.Report, error) {
|
func Update(client container.Client, params types.UpdateParams, trigger session.Trigger) (*session.Report, error) {
|
||||||
log.Debug("Checking containers for updated images")
|
log.Debug("Checking containers for updated images")
|
||||||
progress := &session.Progress{}
|
updateSession := session.New(trigger)
|
||||||
|
progress := updateSession.Progress
|
||||||
staleCount := 0
|
staleCount := 0
|
||||||
|
|
||||||
if params.LifecycleHooks {
|
if params.LifecycleHooks {
|
||||||
|
@ -92,7 +93,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
|
||||||
if params.LifecycleHooks {
|
if params.LifecycleHooks {
|
||||||
lifecycle.ExecutePostChecks(client, params)
|
lifecycle.ExecutePostChecks(client, params)
|
||||||
}
|
}
|
||||||
return progress.Report(), nil
|
return updateSession.Report(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func performRollingRestart(containers []container.Container, client container.Client, params types.UpdateParams) map[types.ContainerID]error {
|
func performRollingRestart(containers []container.Container, client container.Client, params types.UpdateParams) map[types.ContainerID]error {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"github.com/containrrr/watchtower/internal/actions"
|
"github.com/containrrr/watchtower/internal/actions"
|
||||||
"github.com/containrrr/watchtower/pkg/container"
|
"github.com/containrrr/watchtower/pkg/container"
|
||||||
"github.com/containrrr/watchtower/pkg/container/mocks"
|
"github.com/containrrr/watchtower/pkg/container/mocks"
|
||||||
|
"github.com/containrrr/watchtower/pkg/session"
|
||||||
"github.com/containrrr/watchtower/pkg/types"
|
"github.com/containrrr/watchtower/pkg/types"
|
||||||
dockerContainer "github.com/docker/docker/api/types/container"
|
dockerContainer "github.com/docker/docker/api/types/container"
|
||||||
cli "github.com/docker/docker/client"
|
cli "github.com/docker/docker/client"
|
||||||
|
@ -15,6 +16,8 @@ import (
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var trigger = session.SchedulerTrigger
|
||||||
|
|
||||||
var _ = Describe("the update action", func() {
|
var _ = Describe("the update action", func() {
|
||||||
var dockerClient cli.CommonAPIClient
|
var dockerClient cli.CommonAPIClient
|
||||||
var client MockClient
|
var client MockClient
|
||||||
|
@ -60,7 +63,7 @@ var _ = Describe("the update action", func() {
|
||||||
When("there are multiple containers using the same image", func() {
|
When("there are multiple containers using the same image", func() {
|
||||||
It("should only try to remove the image once", func() {
|
It("should only try to remove the image once", func() {
|
||||||
|
|
||||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true})
|
_, err := actions.Update(client, types.UpdateParams{Cleanup: true}, trigger)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
||||||
})
|
})
|
||||||
|
@ -76,7 +79,7 @@ var _ = Describe("the update action", func() {
|
||||||
time.Now(),
|
time.Now(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true})
|
_, err := actions.Update(client, types.UpdateParams{Cleanup: true}, trigger)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(2))
|
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(2))
|
||||||
})
|
})
|
||||||
|
@ -84,7 +87,7 @@ var _ = Describe("the update action", func() {
|
||||||
When("performing a rolling restart update", func() {
|
When("performing a rolling restart update", func() {
|
||||||
It("should try to remove the image once", func() {
|
It("should try to remove the image once", func() {
|
||||||
|
|
||||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, RollingRestart: true})
|
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, RollingRestart: true}, trigger)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
||||||
})
|
})
|
||||||
|
@ -124,7 +127,7 @@ var _ = Describe("the update action", func() {
|
||||||
})
|
})
|
||||||
|
|
||||||
It("should not update those containers", func() {
|
It("should not update those containers", func() {
|
||||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true})
|
_, err := actions.Update(client, types.UpdateParams{Cleanup: true}, trigger)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
||||||
})
|
})
|
||||||
|
@ -154,7 +157,7 @@ var _ = Describe("the update action", func() {
|
||||||
})
|
})
|
||||||
|
|
||||||
It("should not update any containers", func() {
|
It("should not update any containers", func() {
|
||||||
_, err := actions.Update(client, types.UpdateParams{MonitorOnly: true})
|
_, err := actions.Update(client, types.UpdateParams{MonitorOnly: true}, trigger)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
|
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
|
||||||
})
|
})
|
||||||
|
@ -193,7 +196,7 @@ var _ = Describe("the update action", func() {
|
||||||
})
|
})
|
||||||
|
|
||||||
It("should not update those containers", func() {
|
It("should not update those containers", func() {
|
||||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true})
|
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true}, trigger)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
|
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
|
||||||
})
|
})
|
||||||
|
@ -229,7 +232,7 @@ var _ = Describe("the update action", func() {
|
||||||
})
|
})
|
||||||
|
|
||||||
It("should not update those containers", func() {
|
It("should not update those containers", func() {
|
||||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true})
|
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true}, trigger)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
|
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
|
||||||
})
|
})
|
||||||
|
@ -265,7 +268,7 @@ var _ = Describe("the update action", func() {
|
||||||
})
|
})
|
||||||
|
|
||||||
It("should update those containers", func() {
|
It("should update those containers", func() {
|
||||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true})
|
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true}, trigger)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
||||||
})
|
})
|
||||||
|
@ -300,7 +303,7 @@ var _ = Describe("the update action", func() {
|
||||||
})
|
})
|
||||||
|
|
||||||
It("skip running preupdate", func() {
|
It("skip running preupdate", func() {
|
||||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true})
|
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true}, trigger)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
||||||
})
|
})
|
||||||
|
@ -336,7 +339,7 @@ var _ = Describe("the update action", func() {
|
||||||
})
|
})
|
||||||
|
|
||||||
It("skip running preupdate", func() {
|
It("skip running preupdate", func() {
|
||||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true})
|
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true}, trigger)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
||||||
})
|
})
|
||||||
|
|
|
@ -18,7 +18,7 @@ func TestEnvConfig_Defaults(t *testing.T) {
|
||||||
err := EnvConfig(cmd)
|
err := EnvConfig(cmd)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "unix:///var/run/docker.sock", os.Getenv("DOCKER_HOST"))
|
// assert.Equal(t, "unix:///var/run/docker.sock", os.Getenv("DOCKER_HOST"))
|
||||||
assert.Equal(t, "", os.Getenv("DOCKER_TLS_VERIFY"))
|
assert.Equal(t, "", os.Getenv("DOCKER_TLS_VERIFY"))
|
||||||
// Re-enable this test when we've moved to github actions.
|
// Re-enable this test when we've moved to github actions.
|
||||||
// assert.Equal(t, DockerAPIMinVersion, os.Getenv("DOCKER_API_VERSION"))
|
// assert.Equal(t, DockerAPIMinVersion, os.Getenv("DOCKER_API_VERSION"))
|
||||||
|
|
|
@ -2,6 +2,7 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/containrrr/watchtower/pkg/session"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
@ -10,8 +11,9 @@ const tokenMissingMsg = "api token is empty or has not been set. exiting"
|
||||||
|
|
||||||
// API is the http server responsible for serving the HTTP API endpoints
|
// API is the http server responsible for serving the HTTP API endpoints
|
||||||
type API struct {
|
type API struct {
|
||||||
Token string
|
Token string
|
||||||
hasHandlers bool
|
hasHandlers bool
|
||||||
|
latestReport session.Report
|
||||||
}
|
}
|
||||||
|
|
||||||
// New is a factory function creating a new API instance
|
// New is a factory function creating a new API instance
|
||||||
|
@ -74,3 +76,7 @@ func runHTTPServer() {
|
||||||
log.Info("Serving HTTP")
|
log.Info("Serving HTTP")
|
||||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *API) UpdateReport(report session.Report) {
|
||||||
|
api.latestReport = report
|
||||||
|
}
|
||||||
|
|
13
pkg/api/api_suite_test.go
Normal file
13
pkg/api/api_suite_test.go
Normal file
|
@ -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")
|
||||||
|
}
|
23
pkg/api/json.go
Normal file
23
pkg/api/json.go
Normal file
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
32
pkg/api/metrics.go
Normal file
32
pkg/api/metrics.go
Normal file
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
package metrics_test
|
package api_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -6,10 +6,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/containrrr/watchtower/pkg/api"
|
"github.com/containrrr/watchtower/pkg/api"
|
||||||
metricsAPI "github.com/containrrr/watchtower/pkg/api/metrics"
|
|
||||||
"github.com/containrrr/watchtower/pkg/metrics"
|
"github.com/containrrr/watchtower/pkg/metrics"
|
||||||
|
|
||||||
. "github.com/onsi/ginkgo"
|
. "github.com/onsi/ginkgo"
|
||||||
|
@ -21,11 +19,6 @@ const (
|
||||||
getURL = "http://localhost:8080/v1/metrics"
|
getURL = "http://localhost:8080/v1/metrics"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMetrics(t *testing.T) {
|
|
||||||
RegisterFailHandler(Fail)
|
|
||||||
RunSpecs(t, "Metrics Suite")
|
|
||||||
}
|
|
||||||
|
|
||||||
func getWithToken(handler http.Handler) map[string]string {
|
func getWithToken(handler http.Handler) map[string]string {
|
||||||
metricMap := map[string]string{}
|
metricMap := map[string]string{}
|
||||||
respWriter := httptest.NewRecorder()
|
respWriter := httptest.NewRecorder()
|
||||||
|
@ -51,7 +44,7 @@ func getWithToken(handler http.Handler) map[string]string {
|
||||||
|
|
||||||
var _ = Describe("the metrics API", func() {
|
var _ = Describe("the metrics API", func() {
|
||||||
httpAPI := api.New(token)
|
httpAPI := api.New(token)
|
||||||
m := metricsAPI.New()
|
m := api.NewMetricsHandler()
|
||||||
|
|
||||||
handleReq := httpAPI.RequireToken(m.Handle)
|
handleReq := httpAPI.RequireToken(m.Handle)
|
||||||
tryGetMetrics := func() map[string]string { return getWithToken(handleReq) }
|
tryGetMetrics := func() map[string]string { return getWithToken(handleReq) }
|
14
pkg/api/report.go
Normal file
14
pkg/api/report.go
Normal file
|
@ -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
|
||||||
|
}
|
62
pkg/api/update.go
Normal file
62
pkg/api/update.go
Normal file
|
@ -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,7 +1,7 @@
|
||||||
package metrics
|
package metrics
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/containrrr/watchtower/pkg/types"
|
"github.com/containrrr/watchtower/pkg/session"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
)
|
)
|
||||||
|
@ -26,12 +26,12 @@ type Metrics struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMetric returns a Metric with the counts taken from the appropriate types.Report fields
|
// NewMetric returns a Metric with the counts taken from the appropriate types.Report fields
|
||||||
func NewMetric(report types.Report) *Metric {
|
func NewMetric(report *session.Report) *Metric {
|
||||||
return &Metric{
|
return &Metric{
|
||||||
Scanned: len(report.Scanned()),
|
Scanned: len(report.Scanned),
|
||||||
// Note: This is for backwards compatibility. ideally, stale containers should be counted separately
|
// Note: This is for backwards compatibility. ideally, stale containers should be counted separately
|
||||||
Updated: len(report.Updated()) + len(report.Stale()),
|
Updated: len(report.Updated) + len(report.Stale),
|
||||||
Failed: len(report.Failed()),
|
Failed: len(report.Failed),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package notifications
|
package notifications
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/containrrr/watchtower/pkg/session"
|
||||||
ty "github.com/containrrr/watchtower/pkg/types"
|
ty "github.com/containrrr/watchtower/pkg/types"
|
||||||
"github.com/johntdyer/slackrus"
|
"github.com/johntdyer/slackrus"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
@ -8,8 +9,16 @@ import (
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Notifier is the interface that all notification services have in common
|
||||||
|
type Notifier interface {
|
||||||
|
StartNotification()
|
||||||
|
SendNotification(report *session.Report)
|
||||||
|
GetNames() []string
|
||||||
|
Close()
|
||||||
|
}
|
||||||
|
|
||||||
// NewNotifier creates and returns a new Notifier, using global configuration.
|
// NewNotifier creates and returns a new Notifier, using global configuration.
|
||||||
func NewNotifier(c *cobra.Command) ty.Notifier {
|
func NewNotifier(c *cobra.Command) Notifier {
|
||||||
f := c.PersistentFlags()
|
f := c.PersistentFlags()
|
||||||
|
|
||||||
level, _ := f.GetString("notifications-level")
|
level, _ := f.GetString("notifications-level")
|
||||||
|
|
|
@ -3,13 +3,13 @@ package notifications
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/containrrr/watchtower/pkg/session"
|
||||||
stdlog "log"
|
stdlog "log"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/containrrr/shoutrrr"
|
"github.com/containrrr/shoutrrr"
|
||||||
"github.com/containrrr/shoutrrr/pkg/types"
|
"github.com/containrrr/shoutrrr/pkg/types"
|
||||||
t "github.com/containrrr/watchtower/pkg/types"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ const (
|
||||||
shoutrrrDefaultTemplate = `{{- with .Report -}}
|
shoutrrrDefaultTemplate = `{{- with .Report -}}
|
||||||
{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed
|
{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed
|
||||||
{{range .Updated -}}
|
{{range .Updated -}}
|
||||||
- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}}
|
- {{.Name}} ({{.ImageName}}): {{.OldImageID.ShortID}} updated to {{.NewImageID.ShortID}}
|
||||||
{{end -}}
|
{{end -}}
|
||||||
{{range .Fresh -}}
|
{{range .Fresh -}}
|
||||||
- {{.Name}} ({{.ImageName}}): {{.State}}
|
- {{.Name}} ({{.ImageName}}): {{.State}}
|
||||||
|
@ -66,7 +66,7 @@ func (n *shoutrrrTypeNotifier) GetNames() []string {
|
||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
func newShoutrrrNotifier(tplString string, acceptedLogLevels []log.Level, legacy bool, urls ...string) t.Notifier {
|
func newShoutrrrNotifier(tplString string, acceptedLogLevels []log.Level, legacy bool, urls ...string) Notifier {
|
||||||
|
|
||||||
notifier := createNotifier(urls, acceptedLogLevels, tplString, legacy)
|
notifier := createNotifier(urls, acceptedLogLevels, tplString, legacy)
|
||||||
log.AddHook(notifier)
|
log.AddHook(notifier)
|
||||||
|
@ -129,7 +129,7 @@ func (n *shoutrrrTypeNotifier) buildMessage(data Data) string {
|
||||||
return body.String()
|
return body.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *shoutrrrTypeNotifier) sendEntries(entries []*log.Entry, report t.Report) {
|
func (n *shoutrrrTypeNotifier) sendEntries(entries []*log.Entry, report *session.Report) {
|
||||||
msg := n.buildMessage(Data{entries, report})
|
msg := n.buildMessage(Data{entries, report})
|
||||||
n.messages <- msg
|
n.messages <- msg
|
||||||
}
|
}
|
||||||
|
@ -140,11 +140,7 @@ func (n *shoutrrrTypeNotifier) StartNotification() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *shoutrrrTypeNotifier) SendNotification(report t.Report) {
|
func (n *shoutrrrTypeNotifier) SendNotification(report *session.Report) {
|
||||||
//if n.entries == nil || len(n.entries) <= 0 {
|
|
||||||
// return
|
|
||||||
//}
|
|
||||||
|
|
||||||
n.sendEntries(n.entries, report)
|
n.sendEntries(n.entries, report)
|
||||||
n.entries = nil
|
n.entries = nil
|
||||||
}
|
}
|
||||||
|
@ -205,5 +201,5 @@ func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template,
|
||||||
// Data is the notification template data model
|
// Data is the notification template data model
|
||||||
type Data struct {
|
type Data struct {
|
||||||
Entries []*log.Entry
|
Entries []*log.Entry
|
||||||
Report t.Report
|
Report *session.Report
|
||||||
}
|
}
|
||||||
|
|
|
@ -223,5 +223,7 @@ func getTemplatedResult(tplString string, legacy bool, data Data) (string, error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return notifier.buildMessage(data), err
|
msg := notifier.buildMessage(data)
|
||||||
|
println(msg)
|
||||||
|
return msg, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package session
|
package session
|
||||||
|
|
||||||
import wt "github.com/containrrr/watchtower/pkg/types"
|
import (
|
||||||
|
wt "github.com/containrrr/watchtower/pkg/types"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// State indicates what the current state is of the container
|
// State indicates what the current state is of the container
|
||||||
type State int
|
type State int
|
||||||
|
@ -19,51 +22,17 @@ const (
|
||||||
|
|
||||||
// ContainerStatus contains the container state during a session
|
// ContainerStatus contains the container state during a session
|
||||||
type ContainerStatus struct {
|
type ContainerStatus struct {
|
||||||
containerID wt.ContainerID
|
ID wt.ContainerID
|
||||||
oldImage wt.ImageID
|
Name string
|
||||||
newImage wt.ImageID
|
OldImageID wt.ImageID
|
||||||
containerName string
|
NewImageID wt.ImageID
|
||||||
imageName string
|
ImageName string
|
||||||
error
|
Error error
|
||||||
state State
|
State State
|
||||||
}
|
}
|
||||||
|
|
||||||
// ID returns the container ID
|
func (state State) String() string {
|
||||||
func (u *ContainerStatus) ID() wt.ContainerID {
|
switch state {
|
||||||
return u.containerID
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns the container name
|
|
||||||
func (u *ContainerStatus) Name() string {
|
|
||||||
return u.containerName
|
|
||||||
}
|
|
||||||
|
|
||||||
// CurrentImageID returns the image ID that the container used when the session started
|
|
||||||
func (u *ContainerStatus) CurrentImageID() wt.ImageID {
|
|
||||||
return u.oldImage
|
|
||||||
}
|
|
||||||
|
|
||||||
// LatestImageID returns the newest image ID found during the session
|
|
||||||
func (u *ContainerStatus) LatestImageID() wt.ImageID {
|
|
||||||
return u.newImage
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImageName returns the name:tag that the container uses
|
|
||||||
func (u *ContainerStatus) ImageName() string {
|
|
||||||
return u.imageName
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error returns the error (if any) that was encountered for the container during a session
|
|
||||||
func (u *ContainerStatus) Error() string {
|
|
||||||
if u.error == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return u.error.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
// State returns the current State that the container is in
|
|
||||||
func (u *ContainerStatus) State() string {
|
|
||||||
switch u.state {
|
|
||||||
case SkippedState:
|
case SkippedState:
|
||||||
return "Skipped"
|
return "Skipped"
|
||||||
case ScannedState:
|
case ScannedState:
|
||||||
|
@ -80,3 +49,12 @@ func (u *ContainerStatus) State() string {
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalJSON marshals State as a string
|
||||||
|
func (state State) MarshalJSON() ([]byte, error) {
|
||||||
|
sb := strings.Builder{}
|
||||||
|
sb.WriteString(`"`)
|
||||||
|
sb.WriteString(state.String())
|
||||||
|
sb.WriteString(`"`)
|
||||||
|
return []byte(sb.String()), nil
|
||||||
|
}
|
||||||
|
|
|
@ -10,19 +10,19 @@ type Progress map[types.ContainerID]*ContainerStatus
|
||||||
// UpdateFromContainer sets various status fields from their corresponding container equivalents
|
// UpdateFromContainer sets various status fields from their corresponding container equivalents
|
||||||
func UpdateFromContainer(cont types.Container, newImage types.ImageID, state State) *ContainerStatus {
|
func UpdateFromContainer(cont types.Container, newImage types.ImageID, state State) *ContainerStatus {
|
||||||
return &ContainerStatus{
|
return &ContainerStatus{
|
||||||
containerID: cont.ID(),
|
ID: cont.ID(),
|
||||||
containerName: cont.Name(),
|
Name: cont.Name(),
|
||||||
imageName: cont.ImageName(),
|
ImageName: cont.ImageName(),
|
||||||
oldImage: cont.SafeImageID(),
|
OldImageID: cont.SafeImageID(),
|
||||||
newImage: newImage,
|
NewImageID: newImage,
|
||||||
state: state,
|
State: state,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddSkipped adds a container to the Progress with the state set as skipped
|
// AddSkipped adds a container to the Progress with the state set as skipped
|
||||||
func (m Progress) AddSkipped(cont types.Container, err error) {
|
func (m Progress) AddSkipped(cont types.Container, err error) {
|
||||||
update := UpdateFromContainer(cont, cont.SafeImageID(), SkippedState)
|
update := UpdateFromContainer(cont, cont.SafeImageID(), SkippedState)
|
||||||
update.error = err
|
update.Error = err
|
||||||
m.Add(update)
|
m.Add(update)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,22 +35,17 @@ func (m Progress) AddScanned(cont types.Container, newImage types.ImageID) {
|
||||||
func (m Progress) UpdateFailed(failures map[types.ContainerID]error) {
|
func (m Progress) UpdateFailed(failures map[types.ContainerID]error) {
|
||||||
for id, err := range failures {
|
for id, err := range failures {
|
||||||
update := m[id]
|
update := m[id]
|
||||||
update.error = err
|
update.Error = err
|
||||||
update.state = FailedState
|
update.State = FailedState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a container to the map using container ID as the key
|
// Add a container to the map using container ID as the key
|
||||||
func (m Progress) Add(update *ContainerStatus) {
|
func (m Progress) Add(update *ContainerStatus) {
|
||||||
m[update.containerID] = update
|
m[update.ID] = update
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkForUpdate marks the container identified by containerID for update
|
// MarkForUpdate marks the container identified by containerID for update
|
||||||
func (m Progress) MarkForUpdate(containerID types.ContainerID) {
|
func (m Progress) MarkForUpdate(containerID types.ContainerID) {
|
||||||
m[containerID].state = UpdatedState
|
m[containerID].State = UpdatedState
|
||||||
}
|
|
||||||
|
|
||||||
// Report creates a new Report from a Progress instance
|
|
||||||
func (m Progress) Report() types.Report {
|
|
||||||
return NewReport(m)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,90 +1,78 @@
|
||||||
package session
|
package session
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/containrrr/watchtower/pkg/types"
|
|
||||||
"sort"
|
"sort"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type report struct {
|
type Report struct {
|
||||||
scanned []types.ContainerReport
|
Started time.Time
|
||||||
updated []types.ContainerReport
|
Ended time.Time
|
||||||
failed []types.ContainerReport
|
Trigger Trigger
|
||||||
skipped []types.ContainerReport
|
Scanned []*ContainerStatus
|
||||||
stale []types.ContainerReport
|
Updated []*ContainerStatus
|
||||||
fresh []types.ContainerReport
|
Failed []*ContainerStatus
|
||||||
}
|
Skipped []*ContainerStatus
|
||||||
|
Stale []*ContainerStatus
|
||||||
func (r *report) Scanned() []types.ContainerReport {
|
Fresh []*ContainerStatus
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewReport creates a types.Report from the supplied Progress
|
// NewReport creates a types.Report from the supplied Progress
|
||||||
func NewReport(progress Progress) types.Report {
|
// s.Started, time.Now().UTC(), s.Trigger, s.Progress
|
||||||
report := &report{
|
func NewReport(started, ended time.Time, trigger Trigger, progress Progress) *Report {
|
||||||
scanned: []types.ContainerReport{},
|
report := &Report{
|
||||||
updated: []types.ContainerReport{},
|
Started: started,
|
||||||
failed: []types.ContainerReport{},
|
Ended: ended,
|
||||||
skipped: []types.ContainerReport{},
|
Trigger: trigger,
|
||||||
stale: []types.ContainerReport{},
|
Scanned: []*ContainerStatus{},
|
||||||
fresh: []types.ContainerReport{},
|
Updated: []*ContainerStatus{},
|
||||||
|
Failed: []*ContainerStatus{},
|
||||||
|
Skipped: []*ContainerStatus{},
|
||||||
|
Stale: []*ContainerStatus{},
|
||||||
|
Fresh: []*ContainerStatus{},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, update := range progress {
|
for _, update := range progress {
|
||||||
if update.state == SkippedState {
|
if update.State == SkippedState {
|
||||||
report.skipped = append(report.skipped, update)
|
report.Skipped = append(report.Skipped, update)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
report.scanned = append(report.scanned, update)
|
report.Scanned = append(report.Scanned, update)
|
||||||
if update.newImage == update.oldImage {
|
if update.NewImageID == update.OldImageID {
|
||||||
update.state = FreshState
|
update.State = FreshState
|
||||||
report.fresh = append(report.fresh, update)
|
report.Fresh = append(report.Fresh, update)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
switch update.state {
|
switch update.State {
|
||||||
case UpdatedState:
|
case UpdatedState:
|
||||||
report.updated = append(report.updated, update)
|
report.Updated = append(report.Updated, update)
|
||||||
case FailedState:
|
case FailedState:
|
||||||
report.failed = append(report.failed, update)
|
report.Failed = append(report.Failed, update)
|
||||||
default:
|
default:
|
||||||
update.state = StaleState
|
update.State = StaleState
|
||||||
report.stale = append(report.stale, update)
|
report.Stale = append(report.Stale, update)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Sort(sortableContainers(report.scanned))
|
sort.Sort(sortableContainers(report.Scanned))
|
||||||
sort.Sort(sortableContainers(report.updated))
|
sort.Sort(sortableContainers(report.Updated))
|
||||||
sort.Sort(sortableContainers(report.failed))
|
sort.Sort(sortableContainers(report.Failed))
|
||||||
sort.Sort(sortableContainers(report.skipped))
|
sort.Sort(sortableContainers(report.Skipped))
|
||||||
sort.Sort(sortableContainers(report.stale))
|
sort.Sort(sortableContainers(report.Stale))
|
||||||
sort.Sort(sortableContainers(report.fresh))
|
sort.Sort(sortableContainers(report.Fresh))
|
||||||
|
|
||||||
return report
|
return report
|
||||||
}
|
}
|
||||||
|
|
||||||
type sortableContainers []types.ContainerReport
|
type sortableContainers []*ContainerStatus
|
||||||
|
|
||||||
// Len implements sort.Interface.Len
|
// Len implements sort.Interface.Len
|
||||||
func (s sortableContainers) Len() int { return len(s) }
|
func (s sortableContainers) Len() int { return len(s) }
|
||||||
|
|
||||||
// Less implements sort.Interface.Less
|
// 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
|
// Swap implements sort.Interface.Swap
|
||||||
func (s sortableContainers) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
func (s sortableContainers) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||||
|
|
24
pkg/session/session.go
Normal file
24
pkg/session/session.go
Normal file
|
@ -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)
|
||||||
|
}
|
34
pkg/session/trigger.go
Normal file
34
pkg/session/trigger.go
Normal file
|
@ -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…
Add table
Add a link
Reference in a new issue