mirror of
https://github.com/henrygd/beszel.git
synced 2026-09-21 00:47:47 +02:00
205 lines
7.1 KiB
Go
205 lines
7.1 KiB
Go
//go:build testing
|
|
|
|
package agent
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type registryTransportFunc func(*http.Request) (*http.Response, error)
|
|
|
|
func (fn registryTransportFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
return fn(req)
|
|
}
|
|
|
|
func registryResponse(status int, body string) *http.Response {
|
|
return &http.Response{
|
|
StatusCode: status,
|
|
Status: fmt.Sprintf("%d %s", status, http.StatusText(status)),
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(strings.NewReader(body)),
|
|
}
|
|
}
|
|
|
|
func registryDigest(fill byte) string {
|
|
return "sha256:" + strings.Repeat(string(fill), 64)
|
|
}
|
|
|
|
func newRegistryChecker(t *testing.T, inspectBody string, transport http.RoundTripper) *dockerManager {
|
|
t.Helper()
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasPrefix(r.URL.Path, "/images/") {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = io.WriteString(w, inspectBody)
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
|
|
return &dockerManager{
|
|
client: newDockerManagerForVersionTest(server).client,
|
|
registryClient: &http.Client{Transport: transport},
|
|
}
|
|
}
|
|
|
|
func TestCheckImageUpdateUsesInspectAndManifestDigests(t *testing.T) {
|
|
local := registryDigest('a')
|
|
remote := registryDigest('b')
|
|
var authCalls, manifestCalls atomic.Int32
|
|
dm := newRegistryChecker(t, fmt.Sprintf(`{"RepoDigests":["docker.io/library/alpine@%s"]}`, local), registryTransportFunc(func(req *http.Request) (*http.Response, error) {
|
|
switch {
|
|
case req.Method == http.MethodGet && req.URL.Host == "auth.docker.io":
|
|
authCalls.Add(1)
|
|
require.Equal(t, "/token", req.URL.Path)
|
|
return registryResponse(http.StatusOK, `{"token":"test-token"}`), nil
|
|
case req.Method == http.MethodHead && req.URL.Host == "registry-1.docker.io":
|
|
manifestCalls.Add(1)
|
|
require.Equal(t, "/v2/library/alpine/manifests/latest", req.URL.Path)
|
|
require.Equal(t, "Bearer test-token", req.Header.Get("Authorization"))
|
|
resp := registryResponse(http.StatusOK, "")
|
|
resp.Header.Set("Docker-Content-Digest", remote)
|
|
return resp, nil
|
|
default:
|
|
return registryResponse(http.StatusNotFound, ""), nil
|
|
}
|
|
}))
|
|
|
|
available, err := dm.checkImageUpdate("alpine")
|
|
require.NoError(t, err)
|
|
require.True(t, available)
|
|
require.EqualValues(t, 1, authCalls.Load())
|
|
require.EqualValues(t, 1, manifestCalls.Load())
|
|
}
|
|
|
|
func TestCheckImageUpdateReportsUnknownInspectState(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
body string
|
|
}{
|
|
{name: "missing field", body: `{}`},
|
|
{name: "empty field", body: `{"RepoDigests":[]}`},
|
|
{name: "malformed reference", body: `{"RepoDigests":["not-a-repo-digest"]}`},
|
|
{name: "wrong repository", body: `{"RepoDigests":["docker.io/library/busybox@` + registryDigest('a') + `"]}`},
|
|
{name: "malformed digest", body: `{"RepoDigests":["docker.io/library/alpine@sha256:not-a-digest"]}`},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
var registryCalls atomic.Int32
|
|
dm := newRegistryChecker(t, test.body, registryTransportFunc(func(req *http.Request) (*http.Response, error) {
|
|
registryCalls.Add(1)
|
|
return registryResponse(http.StatusOK, `{"token":"unexpected"}`), nil
|
|
}))
|
|
|
|
available, err := dm.checkImageUpdate("alpine")
|
|
require.Error(t, err)
|
|
require.False(t, available)
|
|
require.EqualValues(t, 0, registryCalls.Load(), "invalid local state must not query a registry")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckImageUpdateChecksInspectAuthAndManifestStatuses(t *testing.T) {
|
|
local := registryDigest('a')
|
|
validInspect := fmt.Sprintf(`{"RepoDigests":["docker.io/library/alpine@%s"]}`, local)
|
|
|
|
tests := []struct {
|
|
name string
|
|
inspectCode int
|
|
authCode int
|
|
manifestCode int
|
|
remote string
|
|
want string
|
|
}{
|
|
{name: "inspect status", inspectCode: http.StatusNotFound, want: "inspect image"},
|
|
{name: "auth status", inspectCode: http.StatusOK, authCode: http.StatusUnauthorized, want: "registry auth"},
|
|
{name: "manifest status", inspectCode: http.StatusOK, authCode: http.StatusOK, manifestCode: http.StatusNotFound, remote: local, want: "manifest request"},
|
|
{name: "missing digest", inspectCode: http.StatusOK, authCode: http.StatusOK, manifestCode: http.StatusOK, want: "invalid digest"},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if test.inspectCode != http.StatusOK && strings.HasPrefix(r.URL.Path, "/images/") {
|
|
w.WriteHeader(test.inspectCode)
|
|
return
|
|
}
|
|
_, _ = io.WriteString(w, validInspect)
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
|
|
calls := 0
|
|
dm := &dockerManager{client: newDockerManagerForVersionTest(server).client, registryClient: &http.Client{Transport: registryTransportFunc(func(req *http.Request) (*http.Response, error) {
|
|
calls++
|
|
if req.Method == http.MethodGet {
|
|
return registryResponse(test.authCode, `{"token":"test"}`), nil
|
|
}
|
|
response := registryResponse(test.manifestCode, "")
|
|
response.Header.Set("Docker-Content-Digest", test.remote)
|
|
return response, nil
|
|
})}}
|
|
|
|
_, err := dm.checkImageUpdate("alpine")
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), test.want)
|
|
if test.inspectCode != http.StatusOK {
|
|
require.Zero(t, calls)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckImageUpdateSupportsAnonymousAndLSCRRegistries(t *testing.T) {
|
|
t.Run("anonymous registry", func(t *testing.T) {
|
|
local := registryDigest('a')
|
|
var calls atomic.Int32
|
|
dm := newRegistryChecker(t, fmt.Sprintf(`{"RepoDigests":["example.com/app@%s"]}`, local), registryTransportFunc(func(req *http.Request) (*http.Response, error) {
|
|
calls.Add(1)
|
|
require.Equal(t, http.MethodHead, req.Method)
|
|
require.Equal(t, "example.com", req.URL.Host)
|
|
resp := registryResponse(http.StatusOK, "")
|
|
resp.Header.Set("Docker-Content-Digest", local)
|
|
return resp, nil
|
|
}))
|
|
available, err := dm.checkImageUpdate("example.com/app")
|
|
require.NoError(t, err)
|
|
require.False(t, available)
|
|
require.EqualValues(t, 1, calls.Load())
|
|
})
|
|
|
|
t.Run("lscr ghcr alias", func(t *testing.T) {
|
|
local := registryDigest('a')
|
|
var authCalls, manifestCalls atomic.Int32
|
|
dm := newRegistryChecker(t, fmt.Sprintf(`{"RepoDigests":["ghcr.io/linuxserver/app@%s"]}`, local), registryTransportFunc(func(req *http.Request) (*http.Response, error) {
|
|
if req.Method == http.MethodGet {
|
|
authCalls.Add(1)
|
|
return registryResponse(http.StatusOK, `{"token":"test"}`), nil
|
|
}
|
|
manifestCalls.Add(1)
|
|
require.Equal(t, "lscr.io", req.URL.Host)
|
|
resp := registryResponse(http.StatusOK, "")
|
|
resp.Header.Set("Docker-Content-Digest", local)
|
|
return resp, nil
|
|
}))
|
|
available, err := dm.checkImageUpdate("lscr.io/linuxserver/app")
|
|
require.NoError(t, err)
|
|
require.False(t, available)
|
|
require.EqualValues(t, 1, authCalls.Load())
|
|
require.EqualValues(t, 1, manifestCalls.Load())
|
|
})
|
|
}
|
|
|
|
func TestCheckImageUpdateSkipsPinnedDigest(t *testing.T) {
|
|
image := "docker.io/library/alpine@" + registryDigest('a')
|
|
dm := &dockerManager{}
|
|
available, err := dm.checkImageUpdate(image)
|
|
require.NoError(t, err)
|
|
require.False(t, available)
|
|
}
|