Skip to content

Instantly share code, notes, and snippets.

@shino
Created September 2, 2025 05:50
Show Gist options
  • Select an option

  • Save shino/7f72f40b341db9720d58f0d3540fc640 to your computer and use it in GitHub Desktop.

Select an option

Save shino/7f72f40b341db9720d58f0d3540fc640 to your computer and use it in GitHub Desktop.
dummy GHCR/OCI server
package ghcr
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"slices"
"strings"
"testing"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content/memory"
"github.com/MaineK00n/vuls-data-update/pkg/dotgit/ls/remote"
"github.com/google/uuid"
"github.com/opencontainers/go-digest"
)
type User struct {
Login string `json:"login"`
Type string `json:"type"`
}
type manifest struct {
id uint32
digest string
bin []byte
tags []string
}
type DummyServer struct {
owner string
pack string
users map[string]User
manifests map[string]manifest
counter uint32
}
func NewDummyServer(owner, pack string) *DummyServer {
return &DummyServer{
owner: owner,
pack: pack,
users: make(map[string]User),
manifests: make(map[string]manifest),
counter: 123,
}
}
func (d *DummyServer) PopulateUsers(us []User) {
for _, u := range us {
d.users[u.Login] = u
}
}
func (d *DummyServer) PopulateManifest(tags []string) error {
ctx := context.TODO()
store := memory.New() // Just to calculate digest, not used as content store
d.counter = d.counter + 1
desc, err := oras.PackManifest(ctx, store, oras.PackManifestVersion1_1, "application/vnd.example.artifact.v1", oras.PackManifestOptions{
ManifestAnnotations: map[string]string{"dummy-server-counter": fmt.Sprintf("%d", d.counter)}, // To make digests different
})
if err != nil {
return errors.Wrap(err, "pack manifest")
}
rc, err := store.Fetch(ctx, desc)
if err != nil {
return errors.Wrap(err, "fetch manifest")
}
defer rc.Close()
bin, err := io.ReadAll(rc)
if err != nil {
return errors.Wrap(err, "read manifest")
}
d.manifests[desc.Digest.String()] = manifest{
id: d.counter,
digest: desc.Digest.String(),
bin: bin,
tags: tags,
}
return nil
}
func (d *DummyServer) Start(t *testing.T) (string, func()) {
tlss := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Logf("%s: %s\n", r.Method, r.URL.Path)
switch {
case r.URL.Path == fmt.Sprintf("/users/%s", d.owner):
switch r.Method {
case http.MethodGet:
w.WriteHeader(http.StatusOK)
u, found := d.users[d.owner]
if !found {
t.Errorf("user not found: %s", d.owner)
http.Error(w, "User not found", http.StatusNotFound)
return
}
if err := json.NewEncoder(w).Encode(u); err != nil {
t.Errorf("write response: %v", err)
}
default:
t.Errorf("unexpected request received. method: %s, URL: %s", r.Method, r.URL.String())
http.Error(w, "Bad Request", http.StatusBadRequest)
}
case strings.HasPrefix(r.URL.Path, fmt.Sprintf("/orgs/%s/packages/container/%s/versions", d.owner, d.pack)):
switch r.Method {
case http.MethodGet:
vs := make([]remote.Version, 0, len(d.manifests))
for k, m := range d.manifests {
vs = append(vs, remote.Version{
ID: int(m.id),
Name: k,
URL: fmt.Sprintf("https://api.github.com/orgs/%s/packages/container/%s/versions/%d", d.owner, d.pack, m.id),
Metadata: &remote.Metadata{
PackageType: "container",
Container: &remote.Container{
Tags: m.tags,
},
},
})
}
w.WriteHeader(http.StatusOK)
err := json.NewEncoder(w).Encode(vs)
if err != nil {
t.Errorf("write response: %v", err)
}
case http.MethodDelete:
id := strings.TrimPrefix(r.URL.Path, fmt.Sprintf("/orgs/%s/packages/container/%s/versions/", d.owner, d.pack))
for digest, m := range d.manifests {
if fmt.Sprintf("%d", m.id) != id {
continue
}
delete(d.manifests, digest)
break
}
w.WriteHeader(http.StatusNoContent)
default:
t.Errorf("unexpected request received. method: %s, URL: %s", r.Method, r.URL.String())
http.Error(w, "Bad Request", http.StatusBadRequest)
}
case strings.HasPrefix(r.URL.Path, fmt.Sprintf("/v2/%s/%s/blobs/uploads/", d.owner, d.pack)):
switch r.Method {
case http.MethodPost:
sessionID := uuid.NewString()
w.Header().Set("Location", fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", d.owner, d.pack, sessionID))
w.Header().Set("Docker-Upload-UUID", sessionID)
w.WriteHeader(http.StatusAccepted)
case http.MethodPut:
queryDigest := r.URL.Query().Get("digest")
if queryDigest == "" {
t.Errorf("digest query parameter is required. actual: %q", r.URL.RawQuery)
http.Error(w, "digest query parameter is required", http.StatusBadRequest)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
t.Errorf("read request body: %v", err)
http.Error(w, "failed to read body", http.StatusInternalServerError)
return
}
calculatedDigest := digest.FromBytes(bodyBytes)
if calculatedDigest.String() != queryDigest {
t.Errorf("digest mismatch: query=%s, calculated=%s", queryDigest, calculatedDigest)
http.Error(w, "digest mismatch", http.StatusBadRequest)
return
}
w.Header().Set("Location", fmt.Sprintf("/v2/%s/%s/blobs/%s", d.owner, d.pack, calculatedDigest))
w.Header().Set("Docker-Content-Digest", calculatedDigest.String())
w.WriteHeader(http.StatusCreated)
default:
t.Errorf("unexpected request received. method: %s, URL: %s", r.Method, r.URL.String())
http.Error(w, "Bad Request", http.StatusBadRequest)
}
case strings.HasPrefix(r.URL.Path, fmt.Sprintf("/v2/%s/%s/blobs/", d.owner, d.pack)):
switch r.Method {
case http.MethodHead:
w.WriteHeader(http.StatusNotFound)
default:
t.Errorf("unexpected request received. method: %s, URL: %s", r.Method, r.URL.String())
http.Error(w, "Bad Request", http.StatusBadRequest)
}
case strings.HasPrefix(r.URL.Path, fmt.Sprintf("/v2/%s/%s/manifests/", d.owner, d.pack)):
key := strings.TrimPrefix(r.URL.Path, fmt.Sprintf("/v2/%s/%s/manifests/", d.owner, d.pack))
switch r.Method {
case http.MethodPut:
bin, err := io.ReadAll(r.Body)
if err != nil {
t.Errorf("read request body: %v", err)
}
if strings.HasPrefix(key, "sha256:") {
d.counter = d.counter + 1
d.manifests[key] = manifest{
id: d.counter,
digest: key,
bin: bin,
}
} else {
calculatedDigest := digest.FromBytes(bin)
m, ok := d.manifests[calculatedDigest.String()]
if !ok {
t.Errorf("manifest not found: %s", calculatedDigest.String())
http.Error(w, "manifest not found", http.StatusBadRequest)
return
}
m.tags = append(m.tags, key)
d.manifests[calculatedDigest.String()] = m
}
w.Header().Set("Location", r.URL.Path)
w.WriteHeader(http.StatusCreated)
case http.MethodGet:
var m *manifest
for k, v := range d.manifests {
if k == key {
m = &v
break
}
if slices.Contains(v.tags, key) {
m = &v
break
}
}
if m == nil {
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Set("Content-Type", ocispec.MediaTypeImageManifest)
w.Header().Set("Docker-Content-Digest", m.digest)
w.WriteHeader(http.StatusOK)
_, err := w.Write(m.bin)
if err != nil {
t.Errorf("write response: %v", err)
}
default:
t.Errorf("unexpected request received. method: %s, URL: %s", r.Method, r.URL.String())
http.Error(w, "Bad Request", http.StatusBadRequest)
}
}
}))
originalTransport := http.DefaultTransport
http.DefaultTransport = tlss.Client().Transport
return tlss.URL, func() {
tlss.Close()
http.DefaultTransport = originalTransport
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment