Skip to content

Commit

Permalink
Add visualization tool to debug image sources
Browse files Browse the repository at this point in the history
  • Loading branch information
phillebaba committed May 19, 2024
1 parent 3d770a2 commit 4cdb986
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 8 deletions.
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/alexflint/go-arg v1.5.0
github.com/containerd/containerd v1.7.16
github.com/containerd/typeurl/v2 v2.1.1
github.com/emicklei/dot v1.6.2
github.com/go-logr/logr v1.4.1
github.com/ipfs/go-cid v0.4.1
github.com/libp2p/go-libp2p v0.33.2
Expand Down Expand Up @@ -57,7 +58,7 @@ require (
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/flynn/noise v1.1.0 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/fsnotify/fsnotify v1.7.1-0.20240403050945-7086bea086b7 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
Expand Down Expand Up @@ -175,11 +176,11 @@ require (
golang.org/x/mod v0.15.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/oauth2 v0.16.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.18.0 // indirect
gonum.org/v1/gonum v0.13.0 // indirect
gonum.org/v1/gonum v0.14.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
Expand Down
13 changes: 8 additions & 5 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,8 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn
github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
github.com/elastic/gosigar v0.14.2 h1:Dg80n8cr90OZ7x+bAax/QjoW/XqTI11RmA79ZwIm9/4=
github.com/elastic/gosigar v0.14.2/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
github.com/emicklei/go-restful/v3 v3.10.2 h1:hIovbnmBTLjHXkqEBUz3HGpXZdM7ZrE9fJIZIqlJLqE=
github.com/emicklei/go-restful/v3 v3.10.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
Expand Down Expand Up @@ -764,8 +766,9 @@ github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiD
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fsnotify/fsnotify v1.7.1-0.20240403050945-7086bea086b7 h1:5ZeiG5gIjLqPKLl+f5zv++9ZO2oxA6hmZ3e7G0mMW1M=
github.com/fsnotify/fsnotify v1.7.1-0.20240403050945-7086bea086b7/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
Expand Down Expand Up @@ -1771,8 +1774,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
Expand Down Expand Up @@ -1896,8 +1899,8 @@ gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJ
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA=
gonum.org/v1/gonum v0.13.0 h1:a0T3bh+7fhRyqeNbiC3qVHYmkiQgit3wnNan/2c0HMM=
gonum.org/v1/gonum v0.13.0/go.mod h1:/WPYRckkfWrhWefxyYTfrTtQR0KH4iyHNuzxqXAKyAU=
gonum.org/v1/gonum v0.14.0 h1:2NiG67LD1tEH0D7kM+ps2V+fXmsAnpUeec7n8tcr4S0=
gonum.org/v1/gonum v0.14.0/go.mod h1:AoWeoz0becf9QMWtE8iWXNXc27fK4fNeHNf/oMejGfU=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
Expand Down
42 changes: 42 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net"
"net/http"
"net/http/pprof"
"net/netip"
"net/url"
"os"
"os/signal"
Expand All @@ -29,6 +30,7 @@ import (
"github.com/spegel-org/spegel/pkg/routing"
"github.com/spegel-org/spegel/pkg/state"
"github.com/spegel-org/spegel/pkg/throttle"
"github.com/spegel-org/spegel/pkg/visualize"
)

type ConfigurationCmd struct {
Expand Down Expand Up @@ -65,7 +67,10 @@ type RegistryCmd struct {
ResolveLatestTag bool `arg:"--resolve-latest-tag,env:RESOLVE_LATEST_TAG" default:"true" help:"When true latest tags will be resolved to digests."`
}

type VisualizationCmd struct{}

type Arguments struct {
Visualization *VisualizationCmd `arg:"subcommand:visualization"`
Configuration *ConfigurationCmd `arg:"subcommand:configuration"`
Registry *RegistryCmd `arg:"subcommand:registry"`
LogLevel slog.Level `arg:"--log-level,env:LOG_LEVEL" default:"INFO" help:"Minimum log level to output. Value should be DEBUG, INFO, WARN, or ERROR."`
Expand Down Expand Up @@ -96,6 +101,8 @@ func run(ctx context.Context, args *Arguments) error {
ctx, cancel := signal.NotifyContext(ctx, syscall.SIGTERM)
defer cancel()
switch {
case args.Visualization != nil:
return visualizeCommand(ctx)
case args.Configuration != nil:
return configurationCommand(ctx, args.Configuration)
case args.Registry != nil:
Expand All @@ -105,6 +112,23 @@ func run(ctx context.Context, args *Arguments) error {
}
}

func visualizeCommand(_ context.Context) error {
eventStore := visualize.NewMemoryStore()
eventStore.Record("foobar", netip.MustParseAddrPort("10.0.0.0:20202"), visualize.EventKindSuccess)
eventStore.Record("test", netip.MustParseAddrPort("10.0.0.0:20202"), visualize.EventKindSuccess)
eventStore.Record("hello world", netip.MustParseAddrPort("10.0.0.0:20202"), visualize.EventKindSuccess)
eventStore.Record("hello world", netip.MustParseAddrPort("10.0.0.1:20202"), visualize.EventKindSuccess)
eventStore.Record("hello world", netip.MustParseAddrPort("10.0.0.2:20202"), visualize.EventKindSuccess)
eventStore.Record("hello world", netip.MustParseAddrPort("10.0.0.3:20202"), visualize.EventKindSuccess)
vSvr := visualize.NewServer(eventStore)
svr := vSvr.Server(":9091")
err := svr.ListenAndServe()
if err != nil {
return err
}
return nil
}

func configurationCommand(ctx context.Context, args *ConfigurationCmd) error {
fs := afero.NewOsFs()
err := oci.AddMirrorConfiguration(ctx, fs, args.ContainerdRegistryConfigPath, args.Registries, args.MirrorRegistries, args.ResolveTags, args.AppendMirrors)
Expand Down Expand Up @@ -188,13 +212,31 @@ func registryCommand(ctx context.Context, args *RegistryCmd) (err error) {
return nil
})

// Visualizer
eventStore := visualize.NewMemoryStore()
vis := visualize.NewServer(eventStore)
visSrv := vis.Server(":9091")
g.Go(func() error {
if err := visSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
})
g.Go(func() error {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
return visSrv.Shutdown(shutdownCtx)
})

// Registry
registryOpts := []registry.Option{
registry.WithResolveLatestTag(args.ResolveLatestTag),
registry.WithResolveRetries(args.MirrorResolveRetries),
registry.WithResolveTimeout(args.MirrorResolveTimeout),
registry.WithLocalAddress(args.LocalAddr),
registry.WithLogger(log),
registry.WithEventStore(eventStore),
}
if args.BlobSpeed != nil {
registryOpts = append(registryOpts, registry.WithBlobSpeed(*args.BlobSpeed))
Expand Down
22 changes: 22 additions & 0 deletions pkg/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/spegel-org/spegel/pkg/oci"
"github.com/spegel-org/spegel/pkg/routing"
"github.com/spegel-org/spegel/pkg/throttle"
"github.com/spegel-org/spegel/pkg/visualize"
)

const (
Expand All @@ -37,6 +38,7 @@ type Registry struct {
resolveRetries int
resolveTimeout time.Duration
resolveLatestTag bool
eventStore visualize.EventStore
}

type Option func(*Registry)
Expand Down Expand Up @@ -83,6 +85,12 @@ func WithLogger(log logr.Logger) Option {
}
}

func WithEventStore(eventStore visualize.EventStore) Option {
return func(r *Registry) {
r.eventStore = eventStore
}
}

func NewRegistry(ociClient oci.Client, router routing.Router, opts ...Option) *Registry {
r := &Registry{
ociClient: ociClient,
Expand Down Expand Up @@ -288,6 +296,20 @@ func (r *Registry) handleMirror(rw mux.ResponseWriter, req *http.Request, ref re
return nil
}
proxy.ServeHTTP(rw, req)

// Record proxy source for visualization
if r.eventStore != nil && req.Method == http.MethodGet {
id := ref.name
if id == "" {
id = fmt.Sprintf("%s:%s", ref.dgst.Algorithm(), ref.dgst.String()[:7])
}
eventKind := visualize.EventKindSuccess
if !succeeded {
eventKind = visualize.EventKindFail
}
r.eventStore.Record(id, ipAddr, eventKind)
}

if !succeeded {
break
}
Expand Down
58 changes: 58 additions & 0 deletions pkg/visualize/event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package visualize

import (
"net/netip"
"sync"

"github.com/opencontainers/go-digest"
)

type EventKind string

const (
EventKindSuccess EventKind = "success"
EventKindPending EventKind = "pending"
EventKindFail EventKind = "fail"
)

type Event struct {
ID string
Peer netip.AddrPort
Kind EventKind
Digest digest.Digest
}

type EventStore interface {
Record(id string, peer netip.AddrPort, kind EventKind)
Events() []Event
}

type MemoryStore struct {

Check failure on line 30 in pkg/visualize/event.go

View workflow job for this annotation

GitHub Actions / lint

fieldalignment: struct with 32 pointer bytes could be 8 (govet)
eventsMx sync.RWMutex
events []Event
}

func NewMemoryStore() *MemoryStore {
return &MemoryStore{
events: []Event{},
}
}

func (m *MemoryStore) Record(id string, peer netip.AddrPort, kind EventKind) {
m.eventsMx.Lock()
defer m.eventsMx.Unlock()

event := Event{
ID: id,
Peer: peer,
Kind: kind,
}
m.events = append(m.events, event)
}

func (m *MemoryStore) Events() []Event {
m.eventsMx.RLock()
defer m.eventsMx.RUnlock()

return m.events
}
114 changes: 114 additions & 0 deletions pkg/visualize/visualize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package visualize

import (
"net/http"

"github.com/emicklei/dot"

"github.com/spegel-org/spegel/internal/mux"
)

type Server struct {
eventStore EventStore
}

func NewServer(eventStore EventStore) *Server {
return &Server{
eventStore: eventStore,
}
}

func (s *Server) Server(addr string) *http.Server {
srv := &http.Server{
Addr: addr,
Handler: mux.NewServeMux(s.handle),
}
return srv
}

func (s *Server) handle(rw mux.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/":
s.indexHandler(rw, req)
case "/graph":
s.getGraph(rw, req)
default:
rw.WriteHeader(http.StatusNotFound)
}
}

func (s *Server) indexHandler(rw mux.ResponseWriter, _ *http.Request) {
index := `
<html>
<head>
<title>Spegel</title>
<style>
.content {
width: 1110px;
margin: auto;
}
#graph {
width: 1110px;
}
</style>
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
<script src="https://unpkg.com/@viz-js/viz@3.5.0/lib/viz-standalone.js"></script>
</head>
<body>
<div class="content">
<h1>Spegel</h1>
<div id="graph-container" hx-get="/graph" hx-trigger="load, every 5s" hx-swap="none"
hx-on::after-request="updateGraph(event.detail.xhr.responseText)">
</div>
<script>
function updateGraph(dot) {
Viz.instance().then(function (viz) {
let container = document.getElementById("graph-container");
let newNode = viz.renderSVGElement(dot);
newNode.id = "graph"
let oldNode = document.getElementById("graph");
if (oldNode == null) {
container.appendChild(newNode)
return
}
container.replaceChild(newNode, oldNode)
});
}
</script>
</div>
</div>
</body>
</html>
`
rw.Write([]byte(index))

Check failure on line 89 in pkg/visualize/visualize.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `rw.Write` is not checked (errcheck)
}

// NOTE: image could be discoverd by peeking at the manifest content?

Check failure on line 92 in pkg/visualize/visualize.go

View workflow job for this annotation

GitHub Actions / lint

`discoverd` is a misspelling of `discovered` (misspell)
// NOTE: In the future we could show all nodes in the cluster?
// NOTE: Should be able to both see incoming and outgoing requests
// NOTE: Hash result to avoid generating graph over again
func (s *Server) getGraph(rw mux.ResponseWriter, _ *http.Request) {
events := s.eventStore.Events()
g := dot.NewGraph(dot.Directed)
// g.Attr("layout", "circo")

Check failure on line 99 in pkg/visualize/visualize.go

View workflow job for this annotation

GitHub Actions / lint

commentedOutCode: may want to remove commented-out code (gocritic)
g.Attr("root", "host")
g.Attr("layout", "sfdp")
g.Attr("beautify", true)
host := g.Node("host").Attr("shape", "box")
for _, event := range events {
color := "green"
if event.Kind == EventKindFail {
color = "red"
}

n := g.Node(event.Peer.Addr().String()).Attr("shape", "circle")
g.Edge(host, n, event.ID).Attr("color", color)
}
rw.Write([]byte(g.String()))

Check failure on line 113 in pkg/visualize/visualize.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `rw.Write` is not checked (errcheck)
}
2 changes: 2 additions & 0 deletions test/e2e/e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ then
exit 1
fi

exit 0

# Remove all Spegel Pods and only restart one to verify that running a single instance works
kubectl --kubeconfig $KIND_KUBECONFIG label nodes kind-control-plane kind-worker kind-worker2 spegel-
kubectl --kubeconfig $KIND_KUBECONFIG --namespace spegel delete pods --all
Expand Down

0 comments on commit 4cdb986

Please sign in to comment.