mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
441 lines
12 KiB
Go
441 lines
12 KiB
Go
package system
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/containerd/log"
|
|
"github.com/golang/gddo/httputil"
|
|
"github.com/moby/moby/api/pkg/authconfig"
|
|
"github.com/moby/moby/api/types"
|
|
"github.com/moby/moby/api/types/events"
|
|
"github.com/moby/moby/api/types/registry"
|
|
"github.com/moby/moby/api/types/swarm"
|
|
"github.com/moby/moby/api/types/system"
|
|
"github.com/moby/moby/v2/daemon/internal/compat"
|
|
"github.com/moby/moby/v2/daemon/internal/filters"
|
|
"github.com/moby/moby/v2/daemon/internal/timestamp"
|
|
"github.com/moby/moby/v2/daemon/internal/versions"
|
|
"github.com/moby/moby/v2/daemon/server/backend"
|
|
"github.com/moby/moby/v2/daemon/server/buildbackend"
|
|
"github.com/moby/moby/v2/daemon/server/httputils"
|
|
"github.com/moby/moby/v2/daemon/server/router/build"
|
|
"github.com/moby/moby/v2/pkg/ioutils"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
func optionsHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
w.WriteHeader(http.StatusOK)
|
|
return nil
|
|
}
|
|
|
|
func (s *systemRouter) pingHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
w.Header().Add("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
w.Header().Add("Pragma", "no-cache")
|
|
|
|
builderVersion := build.BuilderVersion(s.features())
|
|
if bv := builderVersion; bv != "" {
|
|
w.Header().Set("Builder-Version", string(bv))
|
|
}
|
|
|
|
w.Header().Set("Swarm", s.swarmStatus())
|
|
|
|
if r.Method == http.MethodHead {
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
w.Header().Set("Content-Length", "0")
|
|
return nil
|
|
}
|
|
_, err := w.Write([]byte{'O', 'K'})
|
|
return err
|
|
}
|
|
|
|
func (s *systemRouter) swarmStatus() string {
|
|
if s.cluster != nil {
|
|
if p, ok := s.cluster.(StatusProvider); ok {
|
|
return p.Status()
|
|
}
|
|
}
|
|
return string(swarm.LocalNodeStateInactive)
|
|
}
|
|
|
|
func (s *systemRouter) getInfo(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
version := httputils.VersionFromContext(ctx)
|
|
info, _, _ := s.collectSystemInfo.Do(ctx, version, func(ctx context.Context) (*compat.Wrapper, error) {
|
|
info, err := s.backend.SystemInfo(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if s.cluster != nil {
|
|
info.Swarm = s.cluster.Info(ctx)
|
|
info.Warnings = append(info.Warnings, info.Swarm.Warnings...)
|
|
}
|
|
|
|
var legacyOptions []compat.Option
|
|
if versions.LessThan(version, "1.44") {
|
|
for k, rt := range info.Runtimes {
|
|
// Status field introduced in API v1.44.
|
|
info.Runtimes[k] = system.RuntimeWithStatus{Runtime: rt.Runtime}
|
|
}
|
|
}
|
|
if versions.LessThan(version, "1.46") {
|
|
// Containerd field introduced in API v1.46.
|
|
info.Containerd = nil
|
|
}
|
|
if versions.LessThan(version, "1.47") {
|
|
// Field is omitted in API 1.48 and up, but should still be included
|
|
// in older versions, even if no values are set.
|
|
legacyOptions = append(legacyOptions, compat.WithExtraFields(map[string]any{
|
|
"RegistryConfig": map[string]any{
|
|
"AllowNondistributableArtifactsCIDRs": json.RawMessage(nil),
|
|
"AllowNondistributableArtifactsHostnames": json.RawMessage(nil),
|
|
},
|
|
}))
|
|
}
|
|
if versions.LessThan(version, "1.49") {
|
|
// FirewallBackend field introduced in API v1.49.
|
|
info.FirewallBackend = nil
|
|
|
|
// Expected commits are omitted in API 1.49, but should still be
|
|
// included in older versions.
|
|
legacyOptions = append(legacyOptions, compat.WithExtraFields(map[string]any{
|
|
"ContainerdCommit": map[string]any{"Expected": info.ContainerdCommit.ID},
|
|
"RuncCommit": map[string]any{"Expected": info.RuncCommit.ID},
|
|
"InitCommit": map[string]any{"Expected": info.InitCommit.ID},
|
|
}))
|
|
}
|
|
if versions.LessThan(version, "1.50") {
|
|
info.DiscoveredDevices = nil
|
|
|
|
// These fields are omitted in > API 1.49, and always false
|
|
// older API versions.
|
|
legacyOptions = append(legacyOptions, compat.WithExtraFields(map[string]any{
|
|
"BridgeNfIptables": json.RawMessage("false"),
|
|
"BridgeNfIp6tables": json.RawMessage("false"),
|
|
}))
|
|
}
|
|
if versions.LessThan(version, "1.53") {
|
|
// Field introduced in API v1.53.
|
|
info.NRI = nil
|
|
}
|
|
return compat.Wrap(info, legacyOptions...), nil
|
|
})
|
|
|
|
return httputils.WriteJSON(w, http.StatusOK, info)
|
|
}
|
|
|
|
func (s *systemRouter) getVersion(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
info, err := s.backend.SystemVersion(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return httputils.WriteJSON(w, http.StatusOK, info)
|
|
}
|
|
|
|
func (s *systemRouter) getDiskUsage(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
version := httputils.VersionFromContext(ctx)
|
|
|
|
var getContainers, getImages, getVolumes, getBuildCache bool
|
|
typeStrs, ok := r.Form["type"]
|
|
if versions.LessThan(version, "1.42") || !ok {
|
|
getContainers, getImages, getVolumes, getBuildCache = true, true, true, s.builder != nil
|
|
} else {
|
|
for _, typ := range typeStrs {
|
|
switch system.DiskUsageObject(typ) {
|
|
case system.ContainerObject:
|
|
getContainers = true
|
|
case system.ImageObject:
|
|
getImages = true
|
|
case system.VolumeObject:
|
|
getVolumes = true
|
|
case system.BuildCacheObject:
|
|
getBuildCache = true
|
|
default:
|
|
return invalidRequestError{Err: fmt.Errorf("unknown object type: %s", typ)}
|
|
}
|
|
}
|
|
}
|
|
|
|
var verbose, legacyFields bool
|
|
if versions.LessThan(version, "1.52") {
|
|
legacyFields = true
|
|
} else {
|
|
verbose = httputils.BoolValue(r, "verbose")
|
|
|
|
// For API 1.52, we include both legacy and current fields, as some
|
|
// integrations (such as "docker-py") currently use "latest", non-versioned
|
|
// API version.
|
|
//
|
|
// However, if the "verbose" query parameter is set, we can assume
|
|
// the client is "API 1.52 aware", and we'll omit the legacy fields.
|
|
//
|
|
// FIXME(thaJeztah): remove legacy fields entirely for API 1.53
|
|
legacyFields = !verbose
|
|
}
|
|
|
|
eg, ctx := errgroup.WithContext(ctx)
|
|
|
|
diskUsage := &backend.DiskUsage{}
|
|
if getContainers || getImages || getVolumes {
|
|
eg.Go(func() error {
|
|
du, err := s.backend.SystemDiskUsage(ctx, backend.DiskUsageOptions{
|
|
Containers: getContainers,
|
|
Images: getImages,
|
|
Volumes: getVolumes,
|
|
Verbose: verbose || legacyFields,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
diskUsage = du
|
|
return nil
|
|
})
|
|
}
|
|
|
|
var buildCacheUsage *buildbackend.DiskUsage
|
|
if getBuildCache {
|
|
eg.Go(func() error {
|
|
var err error
|
|
buildCacheUsage, err = s.builder.DiskUsage(ctx, buildbackend.DiskUsageOptions{
|
|
Verbose: verbose || legacyFields,
|
|
})
|
|
if err != nil {
|
|
return errors.Wrap(err, "error getting build cache usage")
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
if err := eg.Wait(); err != nil {
|
|
return err
|
|
}
|
|
diskUsage.BuildCache = buildCacheUsage
|
|
|
|
var legacy legacyDiskUsage
|
|
if legacyFields {
|
|
if diskUsage.Images != nil {
|
|
legacy.LayersSize = diskUsage.Images.TotalSize
|
|
legacy.Images = nonNilSlice(diskUsage.Images.Items)
|
|
}
|
|
if diskUsage.Containers != nil {
|
|
legacy.Containers = nonNilSlice(diskUsage.Containers.Items)
|
|
}
|
|
if diskUsage.Volumes != nil {
|
|
legacy.Volumes = nonNilSlice(diskUsage.Volumes.Items)
|
|
}
|
|
if diskUsage.BuildCache != nil {
|
|
legacy.BuildCache = nonNilSlice(diskUsage.BuildCache.Items)
|
|
}
|
|
}
|
|
if versions.LessThan(version, "1.52") {
|
|
return httputils.WriteJSON(w, http.StatusOK, &legacy)
|
|
}
|
|
|
|
return httputils.WriteJSON(w, http.StatusOK, &diskUsageCompat{
|
|
legacyDiskUsage: &legacy,
|
|
DiskUsage: &system.DiskUsage{
|
|
ImageUsage: diskUsage.Images,
|
|
ContainerUsage: diskUsage.Containers,
|
|
VolumeUsage: diskUsage.Volumes,
|
|
BuildCacheUsage: diskUsage.BuildCache,
|
|
},
|
|
})
|
|
}
|
|
|
|
// nonNilSlice is used for the legacy fields, which are either omitted
|
|
// entirely, or (if set), must return an empty slice in the response.
|
|
func nonNilSlice[T any](s []T) []T {
|
|
if s == nil {
|
|
return []T{}
|
|
}
|
|
return s
|
|
}
|
|
|
|
type invalidRequestError struct {
|
|
Err error
|
|
}
|
|
|
|
func (e invalidRequestError) Error() string {
|
|
return e.Err.Error()
|
|
}
|
|
|
|
func (e invalidRequestError) InvalidParameter() {}
|
|
|
|
func (s *systemRouter) getEvents(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
since, err := eventTime(r.Form.Get("since"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
until, err := eventTime(r.Form.Get("until"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var (
|
|
timeout <-chan time.Time
|
|
onlyPastEvents bool
|
|
)
|
|
if !until.IsZero() {
|
|
if until.Before(since) {
|
|
return invalidRequestError{fmt.Errorf("`since` time (%s) cannot be after `until` time (%s)", r.Form.Get("since"), r.Form.Get("until"))}
|
|
}
|
|
|
|
now := time.Now()
|
|
|
|
onlyPastEvents = until.Before(now)
|
|
|
|
if !onlyPastEvents {
|
|
dur := until.Sub(now)
|
|
timer := time.NewTimer(dur)
|
|
defer timer.Stop()
|
|
timeout = timer.C
|
|
}
|
|
}
|
|
|
|
ef, err := filters.FromJSON(r.Form.Get("filters"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
contentType := httputil.NegotiateContentType(r, []string{
|
|
types.MediaTypeNDJSON,
|
|
types.MediaTypeJSONSequence,
|
|
}, types.MediaTypeJSON) // output isn't actually JSON but API used to this content-type
|
|
w.Header().Set("Content-Type", contentType)
|
|
w.WriteHeader(http.StatusOK)
|
|
output := ioutils.NewWriteFlusher(w)
|
|
defer output.Close()
|
|
output.Flush()
|
|
|
|
encode := httputils.NewJSONStreamEncoder(output, contentType)
|
|
|
|
buffered, l := s.backend.SubscribeToEvents(since, until, ef)
|
|
defer s.backend.UnsubscribeFromEvents(l)
|
|
|
|
shouldSkip := func(ev events.Message) bool { return false }
|
|
if versions.LessThan(httputils.VersionFromContext(ctx), "1.46") {
|
|
// Image create events were added in API 1.46
|
|
shouldSkip = func(ev events.Message) bool {
|
|
return ev.Type == events.ImageEventType && ev.Action == events.ActionCreate
|
|
}
|
|
}
|
|
|
|
var includeLegacyFields bool
|
|
if versions.LessThan(httputils.VersionFromContext(ctx), "1.52") {
|
|
includeLegacyFields = true
|
|
}
|
|
|
|
for _, ev := range buffered {
|
|
if shouldSkip(ev) {
|
|
continue
|
|
}
|
|
if includeLegacyFields {
|
|
if err := encode(backFillLegacy(&ev)); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
if err := encode(ev); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if onlyPastEvents {
|
|
return nil
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case ev, ok := <-l:
|
|
if !ok {
|
|
log.G(ctx).Debug("event channel closed")
|
|
return nil
|
|
}
|
|
|
|
jev, ok := ev.(events.Message)
|
|
if !ok {
|
|
log.G(ctx).Warnf("unexpected event message: %q", ev)
|
|
continue
|
|
}
|
|
if shouldSkip(jev) {
|
|
continue
|
|
}
|
|
if includeLegacyFields {
|
|
if err := encode(backFillLegacy(&jev)); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
if err := encode(jev); err != nil {
|
|
return err
|
|
}
|
|
case <-timeout:
|
|
return nil
|
|
case <-ctx.Done():
|
|
log.G(ctx).Debug("Client context cancelled, stop sending events")
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *systemRouter) postAuth(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
config, err := authconfig.DecodeRequestBody(r.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
token, err := s.backend.AuthenticateToRegistry(ctx, config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return httputils.WriteJSON(w, http.StatusOK, ®istry.AuthResponse{
|
|
Status: "Login Succeeded",
|
|
IdentityToken: token,
|
|
})
|
|
}
|
|
|
|
func eventTime(formTime string) (time.Time, error) {
|
|
t, tNano, err := timestamp.ParseTimestamps(formTime, -1)
|
|
if err != nil {
|
|
return time.Time{}, err
|
|
}
|
|
if t == -1 {
|
|
return time.Time{}, nil
|
|
}
|
|
return time.Unix(t, tNano), nil
|
|
}
|
|
|
|
// These fields were deprecated in docker v1.10, API v1.22, but not removed
|
|
// from the API responses. Unfortunately, the Docker CLI (and compose indirectly),
|
|
// continued using these fields up until v25.0.0, and panic if the fields are
|
|
// omitted, or left empty (due to a bug), see: https://github.com/moby/moby/pull/50832#issuecomment-3276600925
|
|
func backFillLegacy(ev *events.Message) any {
|
|
switch ev.Type {
|
|
case events.ContainerEventType:
|
|
return compat.Wrap(ev, compat.WithExtraFields(map[string]any{
|
|
"id": ev.Actor.ID,
|
|
"status": ev.Action,
|
|
"from": ev.Actor.Attributes["image"],
|
|
}))
|
|
case events.ImageEventType:
|
|
return compat.Wrap(ev, compat.WithExtraFields(map[string]any{
|
|
"id": ev.Actor.ID,
|
|
"status": ev.Action,
|
|
}))
|
|
default:
|
|
return &ev
|
|
}
|
|
}
|