Harden image admission, resource recovery and session renewal
Add exact-image compatibility policy, experimental admission, paginated build history and shared reauthentication checks. Preserve uncertain Docker creation records, bound frontend resource waits and renew runtime sessions proactively. Lazy-load and split Monaco, add regression and opt-in live Docker fault tests, and record full race and real-browser acceptance results.
This commit is contained in:
+109
-14
@@ -81,6 +81,7 @@ func (a *API) instances(w http.ResponseWriter, r *http.Request) {
|
||||
ImageID string `json:"image_id"`
|
||||
}
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if decoder.Decode(&in) != nil || decoder.Decode(new(any)) != io.EOF || strings.TrimSpace(in.Name) == "" || len(in.Name) > 200 || !runtimeImageID.MatchString(in.ImageID) {
|
||||
fail(w, 400, "invalid instance")
|
||||
return
|
||||
@@ -139,13 +140,25 @@ func (a *API) createRuntime(ctx context.Context, u store.User, i store.Instance,
|
||||
if _, err := a.s.GetInstance(i.ID); err != nil {
|
||||
return i, errors.New("instance reservation unavailable")
|
||||
}
|
||||
networkCreated, complete := false, false
|
||||
networkCreated, networkAttempted, containerAttempted, complete := false, false, false, false
|
||||
defer func() {
|
||||
if complete {
|
||||
return
|
||||
}
|
||||
cleanup, done := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer done()
|
||||
if networkAttempted && !networkCreated {
|
||||
if err := a.recoverRuntimeNetwork(cleanup, i.ID); err != nil {
|
||||
a.s.SetInstance(i.ID, "error")
|
||||
return
|
||||
}
|
||||
}
|
||||
if containerAttempted && i.ContainerID == "" {
|
||||
if err := a.recoverRuntimeContainer(cleanup, &i); err != nil {
|
||||
a.s.SetInstance(i.ID, "error")
|
||||
return
|
||||
}
|
||||
}
|
||||
if i.ContainerID != "" {
|
||||
if err := a.removeRuntimeContainer(cleanup, i.ContainerID); err != nil {
|
||||
a.s.SetInstance(i.ID, "error")
|
||||
@@ -159,7 +172,24 @@ func (a *API) createRuntime(ctx context.Context, u store.User, i store.Instance,
|
||||
}
|
||||
}
|
||||
a.s.DeleteInstance(i.ID)
|
||||
_, _ = a.s.DB.Exec("DELETE FROM runtime_networks WHERE instance_id=?", i.ID)
|
||||
_, _ = a.s.DB.Exec("DELETE FROM runtime_creates WHERE instance_id=?", i.ID)
|
||||
}()
|
||||
admissionMu.RLock()
|
||||
defer admissionMu.RUnlock()
|
||||
buildQueue.Lock()
|
||||
policyErr := a.ensureBuilds()
|
||||
buildQueue.Unlock()
|
||||
if policyErr != nil {
|
||||
return i, policyErr
|
||||
}
|
||||
policy, policyErr := a.evaluateImage(ctx, i.ImageID)
|
||||
if policyErr != nil {
|
||||
return i, policyErr
|
||||
}
|
||||
if !policy.Selectable || policy.Version != i.Version {
|
||||
return i, errors.New("image is not admitted for creation")
|
||||
}
|
||||
root, err := filepath.Abs(a.root)
|
||||
if err != nil {
|
||||
return i, errors.New("storage unavailable")
|
||||
@@ -186,10 +216,30 @@ func (a *API) createRuntime(ctx context.Context, u store.User, i store.Instance,
|
||||
return i, err
|
||||
}
|
||||
network := runtimeNetwork(i.ID)
|
||||
if err := a.docker.Do(ctx, "POST", "/networks/create", map[string]any{"Name": network, "Driver": "bridge", "CheckDuplicate": true, "Labels": map[string]string{"dsh-mgr.instance": i.ID}}, nil); err != nil {
|
||||
if err := a.setRuntimeCreatePending(i.ID, "network_pending", true); err != nil {
|
||||
return i, err
|
||||
}
|
||||
networkAttempted = true
|
||||
var createdNetwork struct {
|
||||
ID string `json:"Id"`
|
||||
}
|
||||
if err := a.docker.Do(ctx, "POST", "/networks/create", map[string]any{"Name": network, "Driver": "bridge", "CheckDuplicate": true, "Labels": map[string]string{"dsh-mgr.instance": i.ID}}, &createdNetwork); err != nil {
|
||||
if dockerCreateRejected(err) {
|
||||
if clearErr := a.setRuntimeCreatePending(i.ID, "network_pending", false); clearErr == nil {
|
||||
networkAttempted = false
|
||||
}
|
||||
}
|
||||
return i, errors.New("network creation failed")
|
||||
}
|
||||
networkCreated = true
|
||||
if createdNetwork.ID != "" {
|
||||
if _, err := a.s.DB.ExecContext(ctx, "INSERT INTO runtime_networks(instance_id,network_id) VALUES(?,?)", i.ID, createdNetwork.ID); err != nil {
|
||||
return i, err
|
||||
}
|
||||
}
|
||||
if err := a.setRuntimeCreatePending(i.ID, "network_pending", false); err != nil {
|
||||
return i, err
|
||||
}
|
||||
if panel := os.Getenv("PANEL_CONTAINER_ID"); panel != "" {
|
||||
if err := a.docker.Do(ctx, "POST", "/networks/"+network+"/connect", map[string]string{"Container": panel}, nil); err != nil {
|
||||
return i, errors.New("panel network connection failed")
|
||||
@@ -212,13 +262,33 @@ func (a *API) createRuntime(ctx context.Context, u store.User, i store.Instance,
|
||||
},
|
||||
},
|
||||
}
|
||||
if policy.RecipeRev != "7" {
|
||||
sharedSource, err := toHostPath(root, hostRoot, filepath.Join(root, suffix, "files"))
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
body["Env"] = []string{"DSH_HOME=/data/dsh-home", "HOME=/data/dsh-home", "XDG_DATA_HOME=/data/dsh-home/.local/share", "XDG_CONFIG_HOME=/data/dsh-home/.config"}
|
||||
body["HostConfig"].(map[string]any)["Mounts"] = []map[string]any{{"Type": "bind", "Source": homeSource, "Target": "/data/dsh-home"}, {"Type": "bind", "Source": sharedSource, "Target": "/workspace"}}
|
||||
}
|
||||
var created struct {
|
||||
ID string `json:"Id"`
|
||||
}
|
||||
if err := a.setRuntimeCreatePending(i.ID, "container_pending", true); err != nil {
|
||||
return i, err
|
||||
}
|
||||
containerAttempted = true
|
||||
if err := a.docker.Do(ctx, "POST", "/containers/create?name="+network, body, &created); err != nil || created.ID == "" {
|
||||
if dockerCreateRejected(err) {
|
||||
if clearErr := a.setRuntimeCreatePending(i.ID, "container_pending", false); clearErr == nil {
|
||||
containerAttempted = false
|
||||
}
|
||||
}
|
||||
return i, errors.New("container creation failed")
|
||||
}
|
||||
i.ContainerID = created.ID
|
||||
if err := a.setRuntimeCreatePending(i.ID, "container_pending", false); err != nil {
|
||||
return i, err
|
||||
}
|
||||
if _, err := a.s.DB.ExecContext(ctx, "UPDATE instances SET container_id=? WHERE id=?", i.ContainerID, i.ID); err != nil {
|
||||
return i, errors.New("database error")
|
||||
}
|
||||
@@ -230,6 +300,7 @@ func (a *API) createRuntime(ctx context.Context, u store.User, i store.Instance,
|
||||
return i, errors.New("database error")
|
||||
}
|
||||
complete = true
|
||||
_, _ = a.s.DB.Exec("DELETE FROM runtime_creates WHERE instance_id=? AND container_pending=0 AND network_pending=0", i.ID)
|
||||
a.s.AddAudit(u.ID, "instance.create", i.ID, i.ImageID)
|
||||
return i, nil
|
||||
}
|
||||
@@ -238,6 +309,10 @@ func dockerNotFound(err error) bool {
|
||||
var e *docker.APIError
|
||||
return errors.As(err, &e) && e.StatusCode == 404
|
||||
}
|
||||
func dockerCreateRejected(err error) bool {
|
||||
var e *docker.APIError
|
||||
return errors.As(err, &e) && e.StatusCode >= 400 && e.StatusCode < 500 && e.StatusCode != 408 && e.StatusCode != 409
|
||||
}
|
||||
func (a *API) removeRuntimeContainer(ctx context.Context, id string) error {
|
||||
err := a.docker.Do(ctx, "DELETE", "/containers/"+url.PathEscape(id)+"?force=1", nil, nil)
|
||||
if dockerNotFound(err) {
|
||||
@@ -246,18 +321,7 @@ func (a *API) removeRuntimeContainer(ctx context.Context, id string) error {
|
||||
return err
|
||||
}
|
||||
func (a *API) removeRuntimeNetwork(ctx context.Context, id string) error {
|
||||
path := "/networks/" + runtimeNetwork(id)
|
||||
if panel := os.Getenv("PANEL_CONTAINER_ID"); panel != "" {
|
||||
err := a.docker.Do(ctx, "POST", path+"/disconnect", map[string]any{"Container": panel, "Force": true}, nil)
|
||||
if err != nil && !dockerNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err := a.docker.Do(ctx, "DELETE", path, nil, nil)
|
||||
if dockerNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
return a.recoverRuntimeNetwork(ctx, id)
|
||||
}
|
||||
|
||||
func (a *API) instance(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -339,6 +403,12 @@ func (a *API) instance(w http.ResponseWriter, r *http.Request) {
|
||||
fail(w, 409, "instance files busy; retry later")
|
||||
return
|
||||
}
|
||||
if i.ContainerID == "" {
|
||||
if err := a.recoverRuntimeContainer(ctx, &i); err != nil {
|
||||
fail(w, 502, "container recovery failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
if i.ContainerID != "" {
|
||||
if err := a.removeRuntimeContainer(ctx, i.ContainerID); err != nil {
|
||||
fail(w, 502, "container deletion failed")
|
||||
@@ -353,7 +423,11 @@ func (a *API) instance(w http.ResponseWriter, r *http.Request) {
|
||||
fail(w, 500, "database error")
|
||||
return
|
||||
}
|
||||
_, _ = a.s.DB.Exec("DELETE FROM runtime_networks WHERE instance_id=?", i.ID)
|
||||
a.s.AddAudit(u.ID, "instance.delete", i.ID, "data retained")
|
||||
// The instance row is gone and its resources were just cleaned, so
|
||||
// any pending flags cannot resolve later: drop the row unconditionally.
|
||||
_, _ = a.s.DB.Exec("DELETE FROM runtime_creates WHERE instance_id=?", i.ID)
|
||||
runtimeAuthCache.Delete(runtimeAuthKey{a, i.ID})
|
||||
writeJSON(w, map[string]bool{"ok": true})
|
||||
return
|
||||
@@ -384,6 +458,27 @@ func (a *API) instance(w http.ResponseWriter, r *http.Request) {
|
||||
fail(w, 409, "instance files busy; retry later")
|
||||
return
|
||||
}
|
||||
admissionMu.RLock()
|
||||
defer admissionMu.RUnlock()
|
||||
if action != "stop" {
|
||||
buildQueue.Lock()
|
||||
e := a.ensureBuilds()
|
||||
buildQueue.Unlock()
|
||||
if e != nil {
|
||||
fail(w, 500, "database error")
|
||||
return
|
||||
}
|
||||
var blocked int
|
||||
e = a.s.DB.QueryRowContext(ctx, "SELECT count(*) FROM image_admissions WHERE image_id=? AND state='blocked'", i.ImageID).Scan(&blocked)
|
||||
if e != nil {
|
||||
fail(w, 500, "database error")
|
||||
return
|
||||
}
|
||||
if blocked > 0 {
|
||||
fail(w, 409, "image is blocked; start/restart denied")
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := a.docker.Do(ctx, "POST", "/containers/"+url.PathEscape(i.ContainerID)+"/"+action+"?t=10", nil, nil); err != nil {
|
||||
fail(w, 502, "container action failed")
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user