e0e362d700
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
SDK Tests / SDK CI (push) Has been cancelled
SDK Tests / CLI Tests (push) Has been cancelled
SDK Tests / Python SDK Quality (code-interpreter) (push) Has been cancelled
SDK Tests / Python SDK Quality (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (sandbox) (push) Has been cancelled
SDK Tests / CLI Quality (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Go SDK Quality And Tests (push) Has been cancelled
230 lines
7.1 KiB
Go
230 lines
7.1 KiB
Go
// Copyright 2025 Alibaba Group Holding Ltd.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package proxy
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/alibaba/opensandbox/ingress/pkg/sandbox"
|
|
slogger "github.com/alibaba/opensandbox/internal/logger"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// mockProvider implements sandbox.Provider interface for testing
|
|
type mockProvider struct {
|
|
endpoints map[string]string // sandboxName -> IP
|
|
notReady map[string]bool // sandboxName -> notReady flag
|
|
accessToken map[string]string // sandboxName -> opensandbox.io/secure-access-token value (non-empty => verification required)
|
|
}
|
|
|
|
func (m *mockProvider) sandboxExists(sandboxId string) bool {
|
|
if m.notReady != nil && m.notReady[sandboxId] {
|
|
return true
|
|
}
|
|
_, ok := m.endpoints[sandboxId]
|
|
return ok
|
|
}
|
|
|
|
func (m *mockProvider) GetEndpoint(sandboxId string) (*sandbox.EndpointInfo, error) {
|
|
if m.notReady != nil && m.notReady[sandboxId] {
|
|
return nil, fmt.Errorf("%w: %s", sandbox.ErrSandboxNotReady, sandboxId)
|
|
}
|
|
if !m.sandboxExists(sandboxId) {
|
|
return nil, fmt.Errorf("%w: %s", sandbox.ErrSandboxNotFound, sandboxId)
|
|
}
|
|
ip := m.endpoints[sandboxId]
|
|
token := ""
|
|
if m.accessToken != nil {
|
|
token = strings.TrimSpace(m.accessToken[sandboxId])
|
|
}
|
|
if ip == "" {
|
|
return nil, fmt.Errorf("%w: %s", sandbox.ErrSandboxNotFound, sandboxId)
|
|
}
|
|
return &sandbox.EndpointInfo{
|
|
Endpoint: ip,
|
|
SecureAccessToken: token,
|
|
}, nil
|
|
}
|
|
|
|
func (m *mockProvider) Start(_ context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
func Test_HTTPProxy(t *testing.T) {
|
|
t.Run("with header mode", func(t *testing.T) {
|
|
httpProxyWithHeaderMode(t)
|
|
})
|
|
|
|
t.Run("with uri mode", func(t *testing.T) {
|
|
httpProxyWithURIMode(t)
|
|
})
|
|
}
|
|
|
|
func httpProxyWithHeaderMode(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(realBackendHTTPHandler))
|
|
defer server.Close()
|
|
serverPort := server.URL[len("http://127.0.0.1:"):]
|
|
|
|
// Create mock provider with sandbox endpoint
|
|
provider := &mockProvider{
|
|
endpoints: map[string]string{
|
|
"test-sandbox": "127.0.0.1",
|
|
},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
Logger = slogger.MustNew(slogger.Config{Level: "debug"})
|
|
proxy := NewProxy(ctx, provider, ModeHeader, nil, nil)
|
|
|
|
mux := http.NewServeMux()
|
|
mux.Handle("/", proxy)
|
|
port, err := findAvailablePort()
|
|
assert.Nil(t, err)
|
|
|
|
go func() {
|
|
assert.NoError(t, http.ListenAndServe(":"+strconv.Itoa(port), mux))
|
|
}()
|
|
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// no header
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://127.0.0.1:%v/hello", port), nil)
|
|
assert.Nil(t, err)
|
|
response, err := http.DefaultClient.Do(request)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, http.StatusBadRequest, response.StatusCode)
|
|
bytes, _ := io.ReadAll(response.Body)
|
|
t.Log(string(bytes))
|
|
|
|
// no sandbox backend
|
|
request, err = http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://127.0.0.1:%v/hello", port), nil)
|
|
request.Header.Set(SandboxIngress, fmt.Sprintf("non-existent-%v", port))
|
|
response, err = http.DefaultClient.Do(request)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, http.StatusNotFound, response.StatusCode) // ErrSandboxNotFound -> 404
|
|
bytes, _ = io.ReadAll(response.Body)
|
|
t.Log(string(bytes))
|
|
|
|
// valid sandbox request
|
|
request, err = http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://127.0.0.1:%v/hello?a=1&b=2", port), nil)
|
|
assert.Nil(t, err)
|
|
|
|
request.Header.Set(SandboxIngress, fmt.Sprintf("test-sandbox-%v", serverPort))
|
|
response, err = http.DefaultClient.Do(request)
|
|
assert.Nil(t, err)
|
|
if response.StatusCode != http.StatusOK {
|
|
bytes, err := io.ReadAll(response.Body)
|
|
assert.Nil(t, err)
|
|
t.Log(string(bytes))
|
|
}
|
|
assert.Equal(t, http.StatusOK, response.StatusCode)
|
|
|
|
// Compatible Host parsing for reverse proxy mode
|
|
request, err = http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://127.0.0.1:%v/hello?a=1&b=2", port), nil)
|
|
assert.Nil(t, err)
|
|
|
|
request.Host = fmt.Sprintf("test-sandbox-%v.sandbox.alibaba-inc.com", serverPort)
|
|
response, err = http.DefaultClient.Do(request)
|
|
assert.Nil(t, err)
|
|
if response.StatusCode != http.StatusOK {
|
|
bytes, err := io.ReadAll(response.Body)
|
|
assert.Nil(t, err)
|
|
t.Log(string(bytes))
|
|
}
|
|
assert.Equal(t, http.StatusOK, response.StatusCode)
|
|
}
|
|
|
|
func httpProxyWithURIMode(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(realBackendHTTPHandler))
|
|
defer server.Close()
|
|
serverPort := server.URL[len("http://127.0.0.1:"):]
|
|
|
|
// Create mock provider with sandbox endpoint
|
|
provider := &mockProvider{
|
|
endpoints: map[string]string{
|
|
"test-sandbox": "127.0.0.1",
|
|
},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
Logger = slogger.MustNew(slogger.Config{Level: "debug"})
|
|
proxy := NewProxy(ctx, provider, ModeURI, nil, nil)
|
|
|
|
mux := http.NewServeMux()
|
|
mux.Handle("/", proxy)
|
|
port, err := findAvailablePort()
|
|
assert.Nil(t, err)
|
|
|
|
go func() {
|
|
assert.NoError(t, http.ListenAndServe(":"+strconv.Itoa(port), mux))
|
|
}()
|
|
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// uri is empty
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://127.0.0.1:%v", port), nil)
|
|
assert.Nil(t, err)
|
|
response, err := http.DefaultClient.Do(request)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, http.StatusBadRequest, response.StatusCode)
|
|
bytes, _ := io.ReadAll(response.Body)
|
|
t.Log(string(bytes))
|
|
|
|
// no sandbox backend
|
|
request, err = http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://127.0.0.1:%v/non-existent-xxx/80/hello", port), nil)
|
|
response, err = http.DefaultClient.Do(request)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, http.StatusNotFound, response.StatusCode) // ErrSandboxNotFound -> 404
|
|
bytes, _ = io.ReadAll(response.Body)
|
|
t.Log(string(bytes))
|
|
|
|
// valid sandbox request
|
|
request, err = http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://127.0.0.1:%v/test-sandbox/%v/hello?a=1&b=2", port, serverPort), nil)
|
|
assert.Nil(t, err)
|
|
response, err = http.DefaultClient.Do(request)
|
|
assert.Nil(t, err)
|
|
if response.StatusCode != http.StatusOK {
|
|
bytes, err := io.ReadAll(response.Body)
|
|
assert.Nil(t, err)
|
|
t.Log(string(bytes))
|
|
}
|
|
assert.Equal(t, http.StatusOK, response.StatusCode)
|
|
}
|
|
|
|
func realBackendHTTPHandler(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
if r.URL.Path != "/hello" {
|
|
http.Error(w, fmt.Sprintf("path is not /hello, but %s", r.URL.Path), http.StatusBadRequest)
|
|
}
|
|
if r.URL.RawQuery != "a=1&b=2" {
|
|
http.Error(w, fmt.Sprintf("query is not a=1&b=2, but %s", r.URL.RawQuery), http.StatusBadRequest)
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("hello world"))
|
|
}
|