WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
// Package sandbox: per-config sandbox inventory.
|
||||
//
|
||||
// Identity changes (provider / endpoint / API key) and config deletion both
|
||||
// need one question answered authoritatively: does this config still own live
|
||||
// sandboxes on the provider?
|
||||
//
|
||||
// The binding store cannot answer it. Bindings go missing exactly when leaks
|
||||
// happen - a CAS rebind overwrites the old sandbox ID, Redis evicts, a process
|
||||
// restarts - so trusting them under-reports in the one direction that costs
|
||||
// money. The provider's own listing is the source of truth.
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
)
|
||||
|
||||
// ConfigSandboxLister is the read half of the provider client.
|
||||
type ConfigSandboxLister interface {
|
||||
List(ctx context.Context, filter RemoteListFilter) ([]RemoteSandboxSummary, error)
|
||||
}
|
||||
|
||||
// ConfigSandboxClient adds deletion, needed to release an inventory.
|
||||
type ConfigSandboxClient interface {
|
||||
ConfigSandboxLister
|
||||
Delete(ctx context.Context, sandboxID string) error
|
||||
}
|
||||
|
||||
// NormalizeConfigID maps the empty config ID onto the sentinel used in
|
||||
// metadata, so "the deployment default config" is addressable like any other.
|
||||
func NormalizeConfigID(configID string) string {
|
||||
if strings.TrimSpace(configID) == "" {
|
||||
return types.SandboxConfigIDGlobalDefault
|
||||
}
|
||||
return configID
|
||||
}
|
||||
|
||||
// MetadataSessionIDKey returns the provider metadata key used for session IDs.
|
||||
func MetadataSessionIDKey() string {
|
||||
return remoteMetadataSessionID
|
||||
}
|
||||
|
||||
// configSandboxFilter narrows a listing to one workspace's one config.
|
||||
func configSandboxFilter(tenantID uint64, configID string) RemoteListFilter {
|
||||
return RemoteListFilter{
|
||||
Metadata: map[string]string{
|
||||
remoteMetadataTenantID: strconv.FormatUint(tenantID, 10),
|
||||
remoteMetadataConfigID: NormalizeConfigID(configID),
|
||||
},
|
||||
// Paused sandboxes are not idle leftovers: they bill, and a session
|
||||
// expects to resume them. Omitting them would report "nothing here"
|
||||
// right before the credentials that could delete them are replaced.
|
||||
States: []RemoteSandboxState{RemoteStateRunning, RemoteStatePaused},
|
||||
}
|
||||
}
|
||||
|
||||
// ListConfigSandboxes returns the sandboxes a config currently owns.
|
||||
func ListConfigSandboxes(
|
||||
ctx context.Context,
|
||||
client ConfigSandboxLister,
|
||||
tenantID uint64,
|
||||
configID string,
|
||||
) ([]RemoteSandboxSummary, error) {
|
||||
if client == nil {
|
||||
return nil, fmt.Errorf("sandbox: listing requires a client")
|
||||
}
|
||||
summaries, err := client.List(ctx, configSandboxFilter(tenantID, configID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"sandbox: list workspace %d config %q sandboxes: %w",
|
||||
tenantID, NormalizeConfigID(configID), err)
|
||||
}
|
||||
return summaries, nil
|
||||
}
|
||||
|
||||
// ReleaseConfigSandboxes deletes every sandbox the config owns and reports how
|
||||
// many went away.
|
||||
//
|
||||
// This is deliberately NOT exposed as an admin action. Its one caller is the
|
||||
// sweep that runs immediately after a config's credentials are overwritten,
|
||||
// using the outgoing credentials still held in memory — the only remaining
|
||||
// moment those sandboxes can be reached at all. An admin blocked from editing a
|
||||
// config is instead told to end the owning sessions or create a second config.
|
||||
//
|
||||
// One undeletable sandbox does not abort the sweep - partial progress is
|
||||
// strictly better than none - but the failures ARE returned. Swallowing them
|
||||
// would leave a paused instance billing on credentials that no longer exist
|
||||
// anywhere, with no record of which sandbox it was.
|
||||
func ReleaseConfigSandboxes(
|
||||
ctx context.Context,
|
||||
client ConfigSandboxClient,
|
||||
tenantID uint64,
|
||||
configID string,
|
||||
) (int, error) {
|
||||
summaries, err := ListConfigSandboxes(ctx, client, tenantID, configID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
deleted := 0
|
||||
var failures []error
|
||||
for _, summary := range summaries {
|
||||
if err := client.Delete(ctx, summary.ID); err != nil {
|
||||
failures = append(failures, fmt.Errorf("delete sandbox %s: %w", summary.ID, err))
|
||||
continue
|
||||
}
|
||||
deleted++
|
||||
}
|
||||
return deleted, errors.Join(failures...)
|
||||
}
|
||||
Reference in New Issue
Block a user