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
480 lines
16 KiB
Go
480 lines
16 KiB
Go
// Copyright 2026 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 e2e
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/alibaba/OpenSandbox/sdks/sandbox/go"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const credentialVaultDefaultTargetHost = "credential-vault-e2e.opensandbox.test"
|
|
|
|
var credentialVaultSecrets = map[string]string{
|
|
"bearer-token": "vault-bearer-token",
|
|
"basic-token": "dXNlcjpwYXNz",
|
|
"api-key-token": "vault-api-key-token",
|
|
"client-id": "vault-client-id",
|
|
"client-secret": "vault-client-secret",
|
|
"query-secret": "vault-query-secret",
|
|
"path-secret": "vault-path-secret",
|
|
"body-secret": "vault-body-secret",
|
|
"runtime-token": "vault-runtime-token",
|
|
"runtime-token-replaced": "vault-runtime-token-replaced",
|
|
}
|
|
|
|
func TestCredentialVaultInjectsAllAuthTypes(t *testing.T) {
|
|
targetIP := credentialVaultTargetIP(t)
|
|
ctx, sb := createCredentialVaultSandbox(t, targetIP)
|
|
|
|
state, err := sb.CreateCredentialVault(ctx, opensandbox.CredentialVaultCreateRequest{
|
|
Credentials: credentialVaultCredentials(
|
|
"bearer-token",
|
|
"basic-token",
|
|
"api-key-token",
|
|
"client-id",
|
|
"client-secret",
|
|
"runtime-token",
|
|
"runtime-token-replaced",
|
|
),
|
|
Bindings: []opensandbox.CredentialBinding{
|
|
credentialVaultBinding("bearer", "/bearer", opensandbox.CredentialAuth{
|
|
Type: opensandbox.CredentialAuthBearer,
|
|
Credential: "bearer-token",
|
|
}),
|
|
credentialVaultBinding("basic", "/basic", opensandbox.CredentialAuth{
|
|
Type: opensandbox.CredentialAuthBasic,
|
|
Credential: "basic-token",
|
|
}),
|
|
credentialVaultBinding("api-key", "/api-key", opensandbox.CredentialAuth{
|
|
Type: opensandbox.CredentialAuthAPIKey,
|
|
Name: "X-Api-Key",
|
|
Credential: "api-key-token",
|
|
}),
|
|
credentialVaultBinding("custom-headers", "/custom-headers", opensandbox.CredentialAuth{
|
|
Type: opensandbox.CredentialAuthCustomHeaders,
|
|
Headers: []opensandbox.CustomHeaderEntry{
|
|
{Name: "X-Client-Id", Credential: "client-id"},
|
|
{Name: "X-Client-Secret", Credential: "client-secret"},
|
|
},
|
|
}),
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
statePayload, err := json.Marshal(state)
|
|
require.NoError(t, err)
|
|
for _, secret := range credentialVaultSecrets {
|
|
require.NotContains(t, string(statePayload), secret)
|
|
}
|
|
|
|
gotAuthTypes := map[string]bool{}
|
|
for _, binding := range state.Bindings {
|
|
if binding.Auth != nil {
|
|
gotAuthTypes[binding.Auth.Type] = true
|
|
}
|
|
}
|
|
require.Equal(t, map[string]bool{
|
|
"bearer": true,
|
|
"basic": true,
|
|
"apiKey": true,
|
|
"customHeaders": true,
|
|
}, gotAuthTypes)
|
|
|
|
for _, path := range []string{"/bearer", "/basic", "/api-key", "/custom-headers"} {
|
|
response := credentialVaultCurlJSON(t, ctx, sb, targetIP, path, true)
|
|
require.Equal(t, true, response["ok"])
|
|
require.Equal(t, path[1:], response["case"])
|
|
require.Empty(t, stringSliceFromJSON(t, response["missingOrInvalid"]))
|
|
}
|
|
}
|
|
|
|
func TestCredentialVaultSubstitutesPlaceholdersInQueryPathAndBody(t *testing.T) {
|
|
targetIP := credentialVaultTargetIP(t)
|
|
ctx, sb := createCredentialVaultSandbox(t, targetIP)
|
|
|
|
state, err := sb.CreateCredentialVault(ctx, opensandbox.CredentialVaultCreateRequest{
|
|
Credentials: credentialVaultCredentials("query-secret", "path-secret", "body-secret"),
|
|
Bindings: []opensandbox.CredentialBinding{
|
|
credentialVaultBinding("query-substitution", "/query-substitution", opensandbox.CredentialAuth{
|
|
Type: opensandbox.CredentialAuthPassthrough,
|
|
Substitutions: []opensandbox.CredentialSubstitution{
|
|
{
|
|
Credential: "query-secret",
|
|
Placeholder: "__query_secret__",
|
|
In: []opensandbox.CredentialSubstitutionSurface{opensandbox.CredentialSubstitutionQuery},
|
|
},
|
|
},
|
|
}),
|
|
credentialVaultBinding("path-substitution", "/tenant/*", opensandbox.CredentialAuth{
|
|
Type: opensandbox.CredentialAuthPassthrough,
|
|
Substitutions: []opensandbox.CredentialSubstitution{
|
|
{
|
|
Credential: "path-secret",
|
|
Placeholder: "__path_secret__",
|
|
In: []opensandbox.CredentialSubstitutionSurface{opensandbox.CredentialSubstitutionPath},
|
|
},
|
|
},
|
|
}),
|
|
credentialVaultBindingWithMethods("body-substitution", "/body-substitution", opensandbox.CredentialAuth{
|
|
Type: opensandbox.CredentialAuthPassthrough,
|
|
Substitutions: []opensandbox.CredentialSubstitution{
|
|
{
|
|
Credential: "body-secret",
|
|
Placeholder: "__body_secret__",
|
|
In: []opensandbox.CredentialSubstitutionSurface{opensandbox.CredentialSubstitutionBody},
|
|
},
|
|
},
|
|
}, []string{"POST"}),
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
statePayload, err := json.Marshal(state)
|
|
require.NoError(t, err)
|
|
for _, secret := range credentialVaultSecrets {
|
|
require.NotContains(t, string(statePayload), secret)
|
|
}
|
|
for _, placeholder := range []string{"__query_secret__", "__path_secret__", "__body_secret__"} {
|
|
require.NotContains(t, string(statePayload), placeholder)
|
|
}
|
|
|
|
response := credentialVaultCurlJSON(t, ctx, sb, targetIP, "/query-substitution?api_key=__query_secret__", true)
|
|
require.Equal(t, true, response["ok"])
|
|
require.Equal(t, "query-substitution", response["case"])
|
|
require.Empty(t, stringSliceFromJSON(t, response["missingOrInvalid"]))
|
|
|
|
response = credentialVaultCurlJSON(t, ctx, sb, targetIP, "/tenant/__path_secret__/resource?tenant=__path_secret__", true)
|
|
require.Equal(t, true, response["ok"])
|
|
require.Equal(t, "path-substitution", response["case"])
|
|
require.Equal(t, true, response["queryStillPlaceholder"])
|
|
|
|
response = credentialVaultCurlJSONWithOptions(t, ctx, sb, targetIP, "/body-substitution", credentialVaultCurlOptions{
|
|
failOnHTTPError: true,
|
|
method: "POST",
|
|
headers: []string{"content-type: application/json"},
|
|
data: `{"client_secret":"__body_secret__"}`,
|
|
})
|
|
require.Equal(t, true, response["ok"])
|
|
require.Equal(t, "body-substitution", response["case"])
|
|
require.Empty(t, stringSliceFromJSON(t, response["missingOrInvalid"]))
|
|
}
|
|
|
|
func TestCredentialVaultRuntimeMutationAddsReplacesAndDeletesBinding(t *testing.T) {
|
|
targetIP := credentialVaultTargetIP(t)
|
|
ctx, sb := createCredentialVaultSandbox(t, targetIP)
|
|
|
|
state, err := sb.CreateCredentialVault(ctx, opensandbox.CredentialVaultCreateRequest{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, state.Revision)
|
|
require.Empty(t, state.Credentials)
|
|
require.Empty(t, state.Bindings)
|
|
|
|
state, err = sb.PatchCredentialVault(ctx, opensandbox.CredentialVaultPatchRequest{
|
|
ExpectedRevision: intPtr(state.Revision),
|
|
Credentials: &opensandbox.CredentialMutationSet{
|
|
Add: credentialVaultCredentials("runtime-token"),
|
|
},
|
|
Bindings: &opensandbox.CredentialBindingMutationSet{
|
|
Add: []opensandbox.CredentialBinding{
|
|
credentialVaultBinding("runtime-added", "/runtime-added", opensandbox.CredentialAuth{
|
|
Type: opensandbox.CredentialAuthAPIKey,
|
|
Name: "X-Runtime-Token",
|
|
Credential: "runtime-token",
|
|
}),
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, state.Revision)
|
|
require.Len(t, state.Credentials, 1)
|
|
require.Equal(t, "runtime-token", state.Credentials[0].Name)
|
|
require.Len(t, state.Bindings, 1)
|
|
require.Equal(t, "runtime-added", state.Bindings[0].Name)
|
|
|
|
response := credentialVaultCurlJSON(t, ctx, sb, targetIP, "/runtime-added", true)
|
|
require.Equal(t, true, response["ok"])
|
|
require.Equal(t, "runtime-added", response["case"])
|
|
require.Empty(t, stringSliceFromJSON(t, response["missingOrInvalid"]))
|
|
|
|
state, err = sb.PatchCredentialVault(ctx, opensandbox.CredentialVaultPatchRequest{
|
|
ExpectedRevision: intPtr(state.Revision),
|
|
Bindings: &opensandbox.CredentialBindingMutationSet{Delete: []string{"runtime-added"}},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, 3, state.Revision)
|
|
require.Empty(t, state.Bindings)
|
|
|
|
state, err = sb.PatchCredentialVault(ctx, opensandbox.CredentialVaultPatchRequest{
|
|
ExpectedRevision: intPtr(state.Revision),
|
|
Credentials: &opensandbox.CredentialMutationSet{
|
|
Replace: []opensandbox.Credential{
|
|
credentialVaultCredential("runtime-token", "runtime-token-replaced"),
|
|
},
|
|
},
|
|
Bindings: &opensandbox.CredentialBindingMutationSet{
|
|
Add: []opensandbox.CredentialBinding{
|
|
credentialVaultBinding("runtime-replaced", "/runtime-replaced", opensandbox.CredentialAuth{
|
|
Type: opensandbox.CredentialAuthAPIKey,
|
|
Name: "X-Runtime-Token",
|
|
Credential: "runtime-token",
|
|
}),
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, 4, state.Revision)
|
|
require.Len(t, state.Credentials, 1)
|
|
require.Equal(t, "runtime-token", state.Credentials[0].Name)
|
|
require.Len(t, state.Bindings, 1)
|
|
require.Equal(t, "runtime-replaced", state.Bindings[0].Name)
|
|
|
|
statePayload, err := json.Marshal(state)
|
|
require.NoError(t, err)
|
|
require.NotContains(t, string(statePayload), credentialVaultSecrets["runtime-token"])
|
|
require.NotContains(t, string(statePayload), credentialVaultSecrets["runtime-token-replaced"])
|
|
|
|
response = credentialVaultCurlJSON(t, ctx, sb, targetIP, "/runtime-replaced", true)
|
|
require.Equal(t, true, response["ok"])
|
|
require.Equal(t, "runtime-replaced", response["case"])
|
|
require.Empty(t, stringSliceFromJSON(t, response["missingOrInvalid"]))
|
|
|
|
response = credentialVaultCurlJSON(t, ctx, sb, targetIP, "/runtime-added", false)
|
|
require.Equal(t, false, response["ok"])
|
|
require.Equal(t, "runtime-added", response["case"])
|
|
require.Equal(t, []string{"x-runtime-token"}, stringSliceFromJSON(t, response["missingOrInvalid"]))
|
|
|
|
state, err = sb.PatchCredentialVault(ctx, opensandbox.CredentialVaultPatchRequest{
|
|
ExpectedRevision: intPtr(state.Revision),
|
|
Bindings: &opensandbox.CredentialBindingMutationSet{Delete: []string{"runtime-replaced"}},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, 5, state.Revision)
|
|
require.Empty(t, state.Bindings)
|
|
|
|
state, err = sb.PatchCredentialVault(ctx, opensandbox.CredentialVaultPatchRequest{
|
|
ExpectedRevision: intPtr(state.Revision),
|
|
Credentials: &opensandbox.CredentialMutationSet{Delete: []string{"runtime-token"}},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, 6, state.Revision)
|
|
require.Empty(t, state.Credentials)
|
|
}
|
|
|
|
func credentialVaultTargetHost() string {
|
|
if host := os.Getenv("OPENSANDBOX_CREDENTIAL_VAULT_E2E_TARGET_HOST"); host != "" {
|
|
return host
|
|
}
|
|
return credentialVaultDefaultTargetHost
|
|
}
|
|
|
|
func credentialVaultTargetIP(t *testing.T) string {
|
|
t.Helper()
|
|
targetIP := os.Getenv("OPENSANDBOX_CREDENTIAL_VAULT_E2E_TARGET_IP")
|
|
if targetIP == "" {
|
|
t.Skip("set OPENSANDBOX_CREDENTIAL_VAULT_E2E_TARGET_IP to run Credential Vault E2E")
|
|
}
|
|
return targetIP
|
|
}
|
|
|
|
func createCredentialVaultSandbox(t *testing.T, targetIP string) (context.Context, *opensandbox.Sandbox) {
|
|
t.Helper()
|
|
|
|
image := os.Getenv("OPENSANDBOX_CREDENTIAL_VAULT_E2E_SANDBOX_IMAGE")
|
|
if image == "" {
|
|
image = getSandboxImage()
|
|
}
|
|
|
|
config := connectionConfigForStreaming(t)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
t.Cleanup(cancel)
|
|
|
|
sb, err := opensandbox.CreateSandbox(ctx, config, opensandbox.SandboxCreateOptions{
|
|
Image: image,
|
|
ResourceLimits: credentialVaultSandboxResource(),
|
|
ReadyTimeout: 90 * time.Second,
|
|
NetworkPolicy: &opensandbox.NetworkPolicy{
|
|
DefaultAction: "deny",
|
|
Egress: []opensandbox.NetworkRule{
|
|
{Action: "allow", Target: credentialVaultTargetHost()},
|
|
{Action: "allow", Target: targetIP},
|
|
},
|
|
},
|
|
CredentialProxy: &opensandbox.CredentialProxyConfig{Enabled: true},
|
|
Metadata: map[string]string{
|
|
credentialVaultLabelKey(): credentialVaultLabelValue(),
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = sb.Kill(context.Background()) })
|
|
return ctx, sb
|
|
}
|
|
|
|
func credentialVaultSandboxResource() opensandbox.ResourceLimits {
|
|
cpu := os.Getenv("OPENSANDBOX_E2E_SANDBOX_CPU")
|
|
if cpu == "" {
|
|
cpu = "1"
|
|
}
|
|
memory := os.Getenv("OPENSANDBOX_E2E_SANDBOX_MEMORY")
|
|
if memory == "" {
|
|
memory = "2Gi"
|
|
}
|
|
return opensandbox.ResourceLimits{"cpu": cpu, "memory": memory}
|
|
}
|
|
|
|
func credentialVaultCredentials(names ...string) []opensandbox.Credential {
|
|
credentials := make([]opensandbox.Credential, 0, len(names))
|
|
for _, name := range names {
|
|
credentials = append(credentials, credentialVaultCredential(name, name))
|
|
}
|
|
return credentials
|
|
}
|
|
|
|
func credentialVaultCredential(name, valueName string) opensandbox.Credential {
|
|
return opensandbox.Credential{
|
|
Name: name,
|
|
Source: opensandbox.InlineCredentialSource{
|
|
Type: opensandbox.CredentialSourceInline,
|
|
Value: credentialVaultSecrets[valueName],
|
|
},
|
|
}
|
|
}
|
|
|
|
func credentialVaultBinding(name, path string, auth opensandbox.CredentialAuth) opensandbox.CredentialBinding {
|
|
return credentialVaultBindingWithMethods(name, path, auth, []string{"GET"})
|
|
}
|
|
|
|
func credentialVaultBindingWithMethods(name, path string, auth opensandbox.CredentialAuth, methods []string) opensandbox.CredentialBinding {
|
|
return opensandbox.CredentialBinding{
|
|
Name: name,
|
|
Match: opensandbox.CredentialMatch{
|
|
Schemes: []opensandbox.CredentialScheme{opensandbox.CredentialSchemeHTTP},
|
|
Hosts: []string{credentialVaultTargetHost()},
|
|
Methods: methods,
|
|
Paths: []string{path},
|
|
},
|
|
Auth: auth,
|
|
}
|
|
}
|
|
|
|
type credentialVaultCurlOptions struct {
|
|
failOnHTTPError bool
|
|
method string
|
|
headers []string
|
|
data string
|
|
}
|
|
|
|
func credentialVaultCurlJSON(
|
|
t *testing.T,
|
|
ctx context.Context,
|
|
sb *opensandbox.Sandbox,
|
|
targetIP string,
|
|
path string,
|
|
failOnHTTPError bool,
|
|
) map[string]any {
|
|
t.Helper()
|
|
return credentialVaultCurlJSONWithOptions(t, ctx, sb, targetIP, path, credentialVaultCurlOptions{
|
|
failOnHTTPError: failOnHTTPError,
|
|
})
|
|
}
|
|
|
|
func credentialVaultCurlJSONWithOptions(
|
|
t *testing.T,
|
|
ctx context.Context,
|
|
sb *opensandbox.Sandbox,
|
|
targetIP string,
|
|
path string,
|
|
options credentialVaultCurlOptions,
|
|
) map[string]any {
|
|
t.Helper()
|
|
args := []string{"curl"}
|
|
if options.failOnHTTPError {
|
|
args = append(args, "--fail")
|
|
}
|
|
args = append(args, "--silent", "--show-error", "--connect-timeout", "5", "--max-time", "20")
|
|
if options.method != "" {
|
|
args = append(args, "--request", options.method)
|
|
}
|
|
for _, header := range options.headers {
|
|
args = append(args, "--header", shellQuote(header))
|
|
}
|
|
if options.data != "" {
|
|
args = append(args, "--data", shellQuote(options.data))
|
|
}
|
|
args = append(
|
|
args,
|
|
"--resolve", credentialVaultTargetHost()+":80:"+targetIP,
|
|
"http://"+credentialVaultTargetHost()+path,
|
|
)
|
|
command := strings.Join(args, " ")
|
|
for _, secret := range credentialVaultSecrets {
|
|
require.NotContains(t, command, secret)
|
|
}
|
|
|
|
exec, err := sb.RunCommand(ctx, command, nil)
|
|
require.NoError(t, err)
|
|
require.Nil(t, exec.Error)
|
|
require.NotNil(t, exec.ExitCode)
|
|
require.Equal(t, 0, *exec.ExitCode)
|
|
|
|
stdout := exec.Text()
|
|
require.NotEmpty(t, stdout)
|
|
|
|
var response map[string]any
|
|
require.NoError(t, json.Unmarshal([]byte(stdout), &response))
|
|
return response
|
|
}
|
|
|
|
func shellQuote(value string) string {
|
|
return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'"
|
|
}
|
|
|
|
func stringSliceFromJSON(t *testing.T, value any) []string {
|
|
t.Helper()
|
|
values, ok := value.([]any)
|
|
require.Truef(t, ok, "expected []any, got %T", value)
|
|
result := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
text, ok := value.(string)
|
|
require.Truef(t, ok, "expected string, got %T", value)
|
|
result = append(result, text)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func credentialVaultLabelKey() string {
|
|
if key := os.Getenv("OPENSANDBOX_CREDENTIAL_VAULT_E2E_LABEL_KEY"); key != "" {
|
|
return key
|
|
}
|
|
return "opensandbox.e2e"
|
|
}
|
|
|
|
func credentialVaultLabelValue() string {
|
|
if value := os.Getenv("OPENSANDBOX_CREDENTIAL_VAULT_E2E_LABEL_VALUE"); value != "" {
|
|
return value
|
|
}
|
|
return "credential-vault"
|
|
}
|
|
|
|
func intPtr(v int) *int {
|
|
return &v
|
|
}
|