feat: support direct network exit for runtimes
This commit is contained in:
@@ -124,7 +124,7 @@ func decodeRestrictedBrowserRequest(body []byte, target any) error {
|
||||
|
||||
func validDouyinGeneration(input douyinGenerationRequest) bool {
|
||||
return input.BindingVersion > 0 && exitIDPattern.MatchString(input.RuntimeID) &&
|
||||
exitIDPattern.MatchString(input.NetworkID) && exitIDPattern.MatchString(input.NetworkExitID)
|
||||
exitIDPattern.MatchString(input.NetworkID) && (input.NetworkExitID == "" || exitIDPattern.MatchString(input.NetworkExitID))
|
||||
}
|
||||
|
||||
func (api gateway) requireDouyinGeneration(alias string, input douyinGenerationRequest) error {
|
||||
|
||||
+23
-15
@@ -353,6 +353,7 @@ func (api gateway) list(c fiber.Ctx) error {
|
||||
}
|
||||
bindingVersion, _ := strconv.ParseInt(container.Labels[bindingVersionLabel], 10, 64)
|
||||
proxyPort, _ := strconv.Atoi(container.Labels[proxyPortLabel])
|
||||
direct := container.Labels[networkExitLabel] == ""
|
||||
browsers = append(browsers, browser{
|
||||
ID: container.ID,
|
||||
Alias: alias,
|
||||
@@ -363,7 +364,7 @@ func (api gateway) list(c fiber.Ctx) error {
|
||||
BindingVersion: bindingVersion,
|
||||
NetworkExitID: container.Labels[networkExitLabel],
|
||||
NetworkID: container.Labels[networkIDLabel],
|
||||
ProxyReady: api.proxies.ready(alias, proxyPort, container.ID, container.Labels[networkIDLabel]),
|
||||
ProxyReady: direct || api.proxies.ready(alias, proxyPort, container.ID, container.Labels[networkIDLabel]),
|
||||
})
|
||||
}
|
||||
return writeJSON(c, http.StatusOK, browsers)
|
||||
@@ -397,6 +398,7 @@ func (api gateway) create(c fiber.Ctx) error {
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return writeError(c, statusFor(err), err)
|
||||
}
|
||||
direct := input.NetworkExitID == ""
|
||||
network, proxyServer, undoProxy := "none", "", func() {}
|
||||
var networkGeneration tenantNetworkGeneration
|
||||
keepNetwork := input.Stopped
|
||||
@@ -425,9 +427,11 @@ func (api gateway) create(c fiber.Ctx) error {
|
||||
return writeNetworkError(c, http.StatusBadGateway, errors.New("configure isolated browser network"), networkGeneration.ID)
|
||||
}
|
||||
network = networkGeneration.ID
|
||||
proxyServer, undoProxy, err = api.proxies.configure(input.Alias, input.BindingVersion, bindHost, 0, input.NetworkExit, networkGeneration.ID)
|
||||
if err != nil {
|
||||
return writeNetworkError(c, statusFor(err), errors.Join(errors.New("configure in-memory proxy"), err), networkGeneration.ID)
|
||||
if !direct {
|
||||
proxyServer, undoProxy, err = api.proxies.configure(input.Alias, input.BindingVersion, bindHost, 0, input.NetworkExit, networkGeneration.ID)
|
||||
if err != nil {
|
||||
return writeNetworkError(c, statusFor(err), errors.Join(errors.New("configure in-memory proxy"), err), networkGeneration.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
keepProxy := false
|
||||
@@ -439,7 +443,7 @@ func (api gateway) create(c fiber.Ctx) error {
|
||||
|
||||
pidsLimit := int64(512)
|
||||
cmd := append([]string{}, input.Cmd...)
|
||||
if !input.Stopped {
|
||||
if !input.Stopped && !direct {
|
||||
cmd = append(cmd[:len(cmd)-1], "--proxy-server="+proxyServer, "--disable-non-proxied-udp", cmd[len(cmd)-1])
|
||||
}
|
||||
payload := map[string]any{
|
||||
@@ -508,14 +512,16 @@ func (api gateway) create(c fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
if !input.Stopped {
|
||||
if !api.proxies.bind(input.Alias, input.BindingVersion, proxyServer, created.ID, networkGeneration.ID) {
|
||||
cleanupErr := api.docker.expect(http.MethodDelete, "/containers/"+url.PathEscape(created.ID)+"?force=1&v=0", nil, http.StatusNoContent)
|
||||
if cleanupErr != nil {
|
||||
return writeNetworkError(c, http.StatusBadGateway, fmt.Errorf("proxy generation changed and container cleanup failed: %w", cleanupErr), networkGeneration.ID)
|
||||
if !direct {
|
||||
if !api.proxies.bind(input.Alias, input.BindingVersion, proxyServer, created.ID, networkGeneration.ID) {
|
||||
cleanupErr := api.docker.expect(http.MethodDelete, "/containers/"+url.PathEscape(created.ID)+"?force=1&v=0", nil, http.StatusNoContent)
|
||||
if cleanupErr != nil {
|
||||
return writeNetworkError(c, http.StatusBadGateway, fmt.Errorf("proxy generation changed and container cleanup failed: %w", cleanupErr), networkGeneration.ID)
|
||||
}
|
||||
return writeNetworkError(c, http.StatusConflict, errGenerationConflict, networkGeneration.ID)
|
||||
}
|
||||
return writeNetworkError(c, http.StatusConflict, errGenerationConflict, networkGeneration.ID)
|
||||
undoProxy = func() { api.proxies.remove(input.Alias, input.BindingVersion, created.ID) }
|
||||
}
|
||||
undoProxy = func() { api.proxies.remove(input.Alias, input.BindingVersion, created.ID) }
|
||||
if err := api.docker.expect(http.MethodPost, "/containers/"+url.PathEscape(created.ID)+"/start", nil, http.StatusNoContent, http.StatusNotModified); err != nil {
|
||||
cleanupErr := api.docker.expect(http.MethodDelete, "/containers/"+url.PathEscape(created.ID)+"?force=1&v=0", nil, http.StatusNoContent)
|
||||
if cleanupErr != nil {
|
||||
@@ -524,7 +530,7 @@ func (api gateway) create(c fiber.Ctx) error {
|
||||
return writeNetworkError(c, http.StatusBadGateway, fmt.Errorf("container did not start and was removed while preserving its Profile volume: %w", err), networkGeneration.ID)
|
||||
}
|
||||
}
|
||||
keepProxy, keepNetwork = !input.Stopped, true
|
||||
keepProxy, keepNetwork = !input.Stopped && !direct, true
|
||||
return writeJSON(c, http.StatusCreated, map[string]string{"id": created.ID, "alias": input.Alias, "network_id": networkGeneration.ID})
|
||||
}
|
||||
|
||||
@@ -541,8 +547,10 @@ func validateCreate(input createRequest) error {
|
||||
if !volumePattern.MatchString(input.Volume) {
|
||||
return errors.New("volume must be a valid volume name")
|
||||
}
|
||||
if input.BindingVersion < 1 || (!input.Stopped && !exitIDPattern.MatchString(input.NetworkExitID)) ||
|
||||
(input.Stopped && (input.NetworkExitID != "" || input.NetworkExit != (gatewayProxyExit{}))) {
|
||||
direct := input.NetworkExitID == "" && input.NetworkExit == (gatewayProxyExit{})
|
||||
if input.BindingVersion < 1 || (input.Stopped && !direct) ||
|
||||
(!input.Stopped && !direct && !exitIDPattern.MatchString(input.NetworkExitID)) ||
|
||||
(input.NetworkExitID == "") != (input.NetworkExit == (gatewayProxyExit{})) {
|
||||
return errors.New("binding_version and network_exit_id must identify the current binding")
|
||||
}
|
||||
if len(input.Cmd) == 0 || len(input.Cmd) > 64 || input.Cmd[len(input.Cmd)-1] != "about:blank" {
|
||||
@@ -561,7 +569,7 @@ func validateCreate(input createRequest) error {
|
||||
if total > 4096 {
|
||||
return errors.New("cmd arguments exceed 4096 characters")
|
||||
}
|
||||
if input.Stopped {
|
||||
if input.Stopped || direct {
|
||||
return nil
|
||||
}
|
||||
proxy := input.NetworkExit
|
||||
|
||||
@@ -572,6 +572,50 @@ func TestGatewayCreatesConstrainedBrowserWithPlatformSpec(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayCreatesDirectBrowserWithoutProxyArguments(t *testing.T) {
|
||||
var created map[string]any
|
||||
docker, server := testDocker(func(response http.ResponseWriter, request *http.Request) {
|
||||
switch {
|
||||
case request.Method == http.MethodGet && strings.HasPrefix(request.URL.Path, "/images/"):
|
||||
_, _ = response.Write([]byte(`{}`))
|
||||
case request.Method == http.MethodGet && strings.HasPrefix(request.URL.Path, "/containers/"+namePrefix):
|
||||
response.WriteHeader(http.StatusNotFound)
|
||||
case request.Method == http.MethodPost && strings.HasSuffix(request.URL.Path, "/containers/create"):
|
||||
if err := json.NewDecoder(request.Body).Decode(&created); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
response.WriteHeader(http.StatusCreated)
|
||||
_, _ = response.Write([]byte(`{"Id":"container-id"}`))
|
||||
case request.Method == http.MethodPost && strings.HasSuffix(request.URL.Path, "/containers/container-id/start"):
|
||||
response.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
t.Fatalf("unexpected Docker request %s %s", request.Method, request.URL.String())
|
||||
}
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
handler := newGateway(docker, "creatorhub_browser", testToken)
|
||||
body := `{"alias":"account-a","name":"账号甲","image":"registry.example/browser:1.2.3",` +
|
||||
`"cmd":["--fingerprint=1000","about:blank"],"volume":"creatorhub-profile-account-a",` +
|
||||
`"binding_version":1,"network_exit_id":"","network_exit":{}}`
|
||||
response := httptest.NewRecorder()
|
||||
adaptor.FiberApp(handler).ServeHTTP(response, authed(http.MethodPost, "/v1/browsers", strings.NewReader(body)))
|
||||
|
||||
if response.Code != http.StatusCreated {
|
||||
t.Fatalf("expected 201, got %d: %s", response.Code, response.Body.String())
|
||||
}
|
||||
cmd := created["Cmd"].([]any)
|
||||
encoded, _ := json.Marshal(cmd)
|
||||
if len(cmd) != 2 || strings.Contains(string(encoded), "proxy") {
|
||||
t.Fatalf("direct runtime received proxy arguments: %#v", cmd)
|
||||
}
|
||||
host := created["HostConfig"].(map[string]any)
|
||||
labels := created["Labels"].(map[string]any)
|
||||
if host["NetworkMode"] != "network-account-a" || labels[networkExitLabel] != "" || labels[proxyPortLabel] != "0" {
|
||||
t.Fatalf("direct runtime metadata is invalid: host=%#v labels=%#v", host, labels)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayCreateUsesCapturedNetworkIDAcrossNameReplacement(t *testing.T) {
|
||||
networkID := ""
|
||||
members := map[string]string{}
|
||||
|
||||
Reference in New Issue
Block a user