daemon: agent-driven workspace images — repo→image registry (GET/PUT/DELETE /api/repos), spawn resolves custom image, self-healing lvmh-ops control container with docker.sock; 132 tests, e2e 87/87
This commit is contained in:
+102
-12
@@ -36,6 +36,7 @@ type recordedCreate struct {
|
||||
Env []string `json:"Env"`
|
||||
Labels map[string]string `json:"Labels"`
|
||||
Cmd []string `json:"Cmd"`
|
||||
WorkingDir string `json:"WorkingDir"`
|
||||
HostConfig struct {
|
||||
Binds []string `json:"Binds"`
|
||||
NetworkMode string `json:"NetworkMode"`
|
||||
@@ -46,12 +47,16 @@ type recordedCreate struct {
|
||||
type fakeDocker struct {
|
||||
mu sync.Mutex
|
||||
|
||||
calls []dockerCall
|
||||
images int // entries served by GET /images/json
|
||||
volume map[string]bool // existing volumes
|
||||
nextID int
|
||||
create []recordedCreate
|
||||
archives []int // sizes of accepted CopyToContainer tar streams
|
||||
calls []dockerCall
|
||||
images int // legacy: >0 serves the default worker image
|
||||
imageTags map[string]bool // when set, /images/json serves exactly these
|
||||
volume map[string]bool // existing volumes
|
||||
nextID int
|
||||
create []recordedCreate
|
||||
archives []int // sizes of accepted CopyToContainer tar streams
|
||||
|
||||
containers map[string]string // name-or-id → container id
|
||||
running map[string]bool // container id → running
|
||||
|
||||
failBuild bool
|
||||
failBuildHTTP bool
|
||||
@@ -70,7 +75,23 @@ type fakeDocker struct {
|
||||
}
|
||||
|
||||
func newFakeDocker() *fakeDocker {
|
||||
return &fakeDocker{images: 1, volume: map[string]bool{}}
|
||||
return &fakeDocker{images: 1, volume: map[string]bool{},
|
||||
containers: map[string]string{}, running: map[string]bool{}}
|
||||
}
|
||||
|
||||
// containerID resolves a name-or-id path token to the tracked container id.
|
||||
func (f *fakeDocker) containerID(token string) (string, bool) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
id, ok := f.containers[token]
|
||||
return id, ok
|
||||
}
|
||||
|
||||
// isRunning reports the running flag of a tracked container id.
|
||||
func (f *fakeDocker) isRunning(id string) bool {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return f.running[id]
|
||||
}
|
||||
|
||||
func (f *fakeDocker) server(t *testing.T) *httptest.Server {
|
||||
@@ -82,6 +103,43 @@ func (f *fakeDocker) server(t *testing.T) *httptest.Server {
|
||||
|
||||
// record appends a call; the /vX.Y version prefix negotiated by the SDK is
|
||||
// stripped so assertions are version-agnostic.
|
||||
// imageListJSON builds the GET /images/json entry list (joined with commas)
|
||||
// honoring the reference filter. Legacy mode (imageTags nil): images>0 means
|
||||
// exactly the default worker image is present.
|
||||
func (f *fakeDocker) imageListJSON(filtersJSON string) string {
|
||||
matches := func(ref string) bool {
|
||||
if f.imageTags == nil {
|
||||
return f.images > 0 && ref == imageRefWorker
|
||||
}
|
||||
return f.imageTags[ref]
|
||||
}
|
||||
var refs []string
|
||||
if filtersJSON != "" {
|
||||
var flt struct {
|
||||
Reference map[string]bool `json:"reference"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(filtersJSON), &flt); err != nil {
|
||||
return ""
|
||||
}
|
||||
for ref, want := range flt.Reference {
|
||||
if want {
|
||||
refs = append(refs, ref)
|
||||
}
|
||||
}
|
||||
} else if f.imageTags != nil {
|
||||
for ref := range f.imageTags {
|
||||
refs = append(refs, ref)
|
||||
}
|
||||
}
|
||||
entries := make([]string, 0, len(refs))
|
||||
for _, ref := range refs {
|
||||
if matches(ref) {
|
||||
entries = append(entries, fmt.Sprintf(`{"Id":"sha256:abc","RepoTags":[%q]}`, ref))
|
||||
}
|
||||
}
|
||||
return strings.Join(entries, ",")
|
||||
}
|
||||
|
||||
func (f *fakeDocker) record(r *http.Request, body string) dockerCall {
|
||||
path := r.URL.Path
|
||||
if strings.HasPrefix(path, "/v") {
|
||||
@@ -171,11 +229,7 @@ func (f *fakeDocker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Api-Version", "1.44")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
case call.Method == http.MethodGet && call.Path == "/images/json":
|
||||
list := "[]"
|
||||
if f.images > 0 {
|
||||
list = `[{"Id":"sha256:abc","RepoTags":["lvmh-worker:latest"]}]`
|
||||
}
|
||||
writeJSONNow(w, http.StatusOK, list)
|
||||
writeJSONNow(w, http.StatusOK, "["+f.imageListJSON(r.URL.Query().Get("filters"))+"]")
|
||||
case call.Method == http.MethodPost && call.Path == "/build":
|
||||
if f.failBuildHTTP {
|
||||
writeJSONNow(w, http.StatusInternalServerError, `{"message":"build endpoint broken"}`)
|
||||
@@ -238,19 +292,47 @@ func (f *fakeDocker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
f.nextID++
|
||||
id := fmt.Sprintf("cid-%d", f.nextID)
|
||||
f.create = append(f.create, rc)
|
||||
f.containers[id] = id
|
||||
if rc.Name != "" {
|
||||
f.containers[rc.Name] = id
|
||||
}
|
||||
f.running[id] = false
|
||||
f.mu.Unlock()
|
||||
writeJSONNow(w, http.StatusCreated, fmt.Sprintf(`{"Id":%q,"Warnings":null}`, id))
|
||||
case call.Method == http.MethodGet && strings.HasPrefix(call.Path, "/containers/") && strings.HasSuffix(call.Path, "/json"):
|
||||
token := strings.TrimSuffix(strings.TrimPrefix(call.Path, "/containers/"), "/json")
|
||||
f.mu.Lock()
|
||||
id, ok := f.containers[token]
|
||||
running := ok && f.running[id]
|
||||
f.mu.Unlock()
|
||||
if !ok {
|
||||
writeJSONNow(w, http.StatusNotFound, fmt.Sprintf(`{"message":"No such container: %s"}`, token))
|
||||
return
|
||||
}
|
||||
writeJSONNow(w, http.StatusOK, fmt.Sprintf(`{"Id":%q,"State":{"Running":%t}}`, id, running))
|
||||
case call.Method == http.MethodPost && strings.HasSuffix(call.Path, "/start"):
|
||||
if f.failStart {
|
||||
writeJSONNow(w, http.StatusInternalServerError, `{"message":"start failed"}`)
|
||||
return
|
||||
}
|
||||
token := strings.TrimSuffix(strings.TrimPrefix(call.Path, "/containers/"), "/start")
|
||||
f.mu.Lock()
|
||||
if id, ok := f.containers[token]; ok {
|
||||
f.running[id] = true
|
||||
}
|
||||
f.mu.Unlock()
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
case call.Method == http.MethodPost && strings.HasSuffix(call.Path, "/stop"):
|
||||
if f.failStop {
|
||||
writeJSONNow(w, http.StatusInternalServerError, `{"message":"stop failed"}`)
|
||||
return
|
||||
}
|
||||
token := strings.TrimSuffix(strings.TrimPrefix(call.Path, "/containers/"), "/stop")
|
||||
f.mu.Lock()
|
||||
if id, ok := f.containers[token]; ok {
|
||||
f.running[id] = false
|
||||
}
|
||||
f.mu.Unlock()
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
case call.Method == http.MethodPost && strings.HasSuffix(call.Path, "/wait"):
|
||||
if f.failWait {
|
||||
@@ -277,6 +359,14 @@ func (f *fakeDocker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
f.mu.Unlock()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
case call.Method == http.MethodDelete && strings.HasPrefix(call.Path, "/containers/"):
|
||||
token := strings.TrimPrefix(call.Path, "/containers/")
|
||||
f.mu.Lock()
|
||||
if id, ok := f.containers[token]; ok {
|
||||
delete(f.containers, token)
|
||||
delete(f.containers, id)
|
||||
delete(f.running, id)
|
||||
}
|
||||
f.mu.Unlock()
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
f.mu.Lock()
|
||||
|
||||
Reference in New Issue
Block a user