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
457 lines
14 KiB
Go
457 lines
14 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 sandbox
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
sandboxv1alpha1 "github.com/alibaba/OpenSandbox/sandbox-k8s/apis/sandbox/v1alpha1"
|
|
fakeclientset "github.com/alibaba/OpenSandbox/sandbox-k8s/pkg/client/clientset/versioned/fake"
|
|
informers "github.com/alibaba/OpenSandbox/sandbox-k8s/pkg/client/informers/externalversions"
|
|
"github.com/alibaba/OpenSandbox/sandbox-k8s/pkg/utils"
|
|
)
|
|
|
|
// Note: Integration tests with real informers are in e2e tests
|
|
// Unit tests here focus on provider behavior
|
|
|
|
// TestBatchSandboxProvider_WithFakeInformer tests the provider using fake clientset and informer
|
|
func TestBatchSandboxProvider_WithFakeInformer(t *testing.T) {
|
|
namespace := "test-namespace"
|
|
|
|
// Create a ready BatchSandbox with valid endpoints
|
|
readyBatchSandbox := &sandboxv1alpha1.BatchSandbox{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "ready-sandbox",
|
|
Namespace: namespace,
|
|
Annotations: map[string]string{
|
|
utils.AnnotationEndpoints: `["10.0.0.1", "10.0.0.2"]`,
|
|
},
|
|
},
|
|
Spec: sandboxv1alpha1.BatchSandboxSpec{
|
|
Replicas: ptr(int32(2)),
|
|
},
|
|
Status: sandboxv1alpha1.BatchSandboxStatus{
|
|
Replicas: 2,
|
|
Ready: 2,
|
|
},
|
|
}
|
|
|
|
// Create a not ready BatchSandbox
|
|
notReadyBatchSandbox := &sandboxv1alpha1.BatchSandbox{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "not-ready-sandbox",
|
|
Namespace: namespace,
|
|
},
|
|
Spec: sandboxv1alpha1.BatchSandboxSpec{
|
|
Replicas: ptr(int32(1)),
|
|
},
|
|
Status: sandboxv1alpha1.BatchSandboxStatus{
|
|
Replicas: 1,
|
|
Ready: 0,
|
|
},
|
|
}
|
|
|
|
// Create fake clientset with test objects
|
|
fakeClient := fakeclientset.NewSimpleClientset(readyBatchSandbox, notReadyBatchSandbox)
|
|
|
|
// Create informer factory
|
|
informerFactory := informers.NewSharedInformerFactoryWithOptions(
|
|
fakeClient,
|
|
time.Second*30,
|
|
informers.WithNamespace(namespace),
|
|
)
|
|
|
|
batchSandboxInformer := informerFactory.Sandbox().V1alpha1().BatchSandboxes()
|
|
|
|
// Create provider
|
|
provider := &BatchSandboxProvider{
|
|
informerFactory: informerFactory,
|
|
lister: batchSandboxInformer.Lister(),
|
|
informerSynced: batchSandboxInformer.Informer().HasSynced,
|
|
}
|
|
|
|
// Start informer and wait for cache sync
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
err := provider.Start(ctx)
|
|
assert.NoError(t, err, "Provider should start successfully")
|
|
|
|
// Manually add objects to informer cache (fake clientset doesn't auto-populate informer)
|
|
err = batchSandboxInformer.Informer().GetStore().Add(readyBatchSandbox)
|
|
assert.NoError(t, err)
|
|
err = batchSandboxInformer.Informer().GetStore().Add(notReadyBatchSandbox)
|
|
assert.NoError(t, err)
|
|
|
|
// Test 1: Get endpoint from ready sandbox
|
|
t.Run("GetEndpoint from ready sandbox", func(t *testing.T) {
|
|
endpoint, err := provider.GetEndpoint("ready-sandbox")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "10.0.0.1", endpoint.Endpoint, "Should return first endpoint IP")
|
|
assert.Equal(t, "", endpoint.SecureAccessToken)
|
|
})
|
|
|
|
// Test 2: Get endpoint from not ready sandbox
|
|
t.Run("GetEndpoint from not ready sandbox", func(t *testing.T) {
|
|
_, err := provider.GetEndpoint("not-ready-sandbox")
|
|
assert.Error(t, err)
|
|
assert.True(t, errors.Is(err, ErrSandboxNotReady), "Should return ErrSandboxNotReady")
|
|
assert.Contains(t, err.Error(), "not ready")
|
|
})
|
|
|
|
// Test 3: Get endpoint from non-existent sandbox
|
|
t.Run("GetEndpoint from non-existent sandbox", func(t *testing.T) {
|
|
_, err := provider.GetEndpoint("non-existent")
|
|
assert.Error(t, err)
|
|
assert.True(t, errors.Is(err, ErrSandboxNotFound), "Should return ErrSandboxNotFound")
|
|
assert.Contains(t, err.Error(), "not found")
|
|
})
|
|
|
|
t.Run("GetEndpoint with non-empty access-token", func(t *testing.T) {
|
|
secureSB := &sandboxv1alpha1.BatchSandbox{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "secure-sb",
|
|
Namespace: namespace,
|
|
Annotations: map[string]string{
|
|
AnnotationAccessToken: "opaque",
|
|
utils.AnnotationEndpoints: `["10.0.0.1"]`,
|
|
},
|
|
},
|
|
Spec: sandboxv1alpha1.BatchSandboxSpec{
|
|
Replicas: ptr(int32(1)),
|
|
},
|
|
Status: sandboxv1alpha1.BatchSandboxStatus{
|
|
Replicas: 1,
|
|
Ready: 1,
|
|
},
|
|
}
|
|
err := batchSandboxInformer.Informer().GetStore().Add(secureSB)
|
|
assert.NoError(t, err)
|
|
info, err := provider.GetEndpoint("secure-sb")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "10.0.0.1", info.Endpoint)
|
|
assert.Equal(t, "opaque", info.SecureAccessToken)
|
|
})
|
|
}
|
|
|
|
// TestBatchSandboxProvider_MissingAnnotation tests sandbox without endpoints annotation
|
|
func TestBatchSandboxProvider_MissingAnnotation(t *testing.T) {
|
|
namespace := "test-namespace"
|
|
|
|
// Create BatchSandbox without endpoints annotation
|
|
batchSandbox := &sandboxv1alpha1.BatchSandbox{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "no-annotation-sandbox",
|
|
Namespace: namespace,
|
|
},
|
|
Spec: sandboxv1alpha1.BatchSandboxSpec{
|
|
Replicas: ptr(int32(1)),
|
|
},
|
|
Status: sandboxv1alpha1.BatchSandboxStatus{
|
|
Replicas: 1,
|
|
Ready: 1,
|
|
},
|
|
}
|
|
|
|
fakeClient := fakeclientset.NewSimpleClientset(batchSandbox)
|
|
informerFactory := informers.NewSharedInformerFactoryWithOptions(
|
|
fakeClient,
|
|
time.Second*30,
|
|
informers.WithNamespace(namespace),
|
|
)
|
|
|
|
batchSandboxInformer := informerFactory.Sandbox().V1alpha1().BatchSandboxes()
|
|
|
|
provider := &BatchSandboxProvider{
|
|
informerFactory: informerFactory,
|
|
lister: batchSandboxInformer.Lister(),
|
|
informerSynced: batchSandboxInformer.Informer().HasSynced,
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
err := provider.Start(ctx)
|
|
assert.NoError(t, err)
|
|
|
|
// Manually add object to informer cache
|
|
err = batchSandboxInformer.Informer().GetStore().Add(batchSandbox)
|
|
assert.NoError(t, err)
|
|
|
|
_, err = provider.GetEndpoint("no-annotation-sandbox")
|
|
assert.Error(t, err)
|
|
assert.True(t, errors.Is(err, ErrSandboxNotReady), "Should return ErrSandboxNotReady")
|
|
assert.Contains(t, err.Error(), "has no annotations")
|
|
}
|
|
|
|
// TestBatchSandboxProvider_InvalidAnnotation tests sandbox with invalid annotation format
|
|
func TestBatchSandboxProvider_InvalidAnnotation(t *testing.T) {
|
|
namespace := "test-namespace"
|
|
|
|
batchSandbox := &sandboxv1alpha1.BatchSandbox{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "invalid-annotation-sandbox",
|
|
Namespace: namespace,
|
|
Annotations: map[string]string{
|
|
utils.AnnotationEndpoints: `invalid-json`,
|
|
},
|
|
},
|
|
Spec: sandboxv1alpha1.BatchSandboxSpec{
|
|
Replicas: ptr(int32(1)),
|
|
},
|
|
Status: sandboxv1alpha1.BatchSandboxStatus{
|
|
Replicas: 1,
|
|
Ready: 1,
|
|
},
|
|
}
|
|
|
|
fakeClient := fakeclientset.NewSimpleClientset(batchSandbox)
|
|
informerFactory := informers.NewSharedInformerFactoryWithOptions(
|
|
fakeClient,
|
|
time.Second*30,
|
|
informers.WithNamespace(namespace),
|
|
)
|
|
|
|
batchSandboxInformer := informerFactory.Sandbox().V1alpha1().BatchSandboxes()
|
|
|
|
provider := &BatchSandboxProvider{
|
|
informerFactory: informerFactory,
|
|
lister: batchSandboxInformer.Lister(),
|
|
informerSynced: batchSandboxInformer.Informer().HasSynced,
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
err := provider.Start(ctx)
|
|
assert.NoError(t, err)
|
|
|
|
// Manually add object to informer cache
|
|
err = batchSandboxInformer.Informer().GetStore().Add(batchSandbox)
|
|
assert.NoError(t, err)
|
|
|
|
_, err = provider.GetEndpoint("invalid-annotation-sandbox")
|
|
assert.Error(t, err)
|
|
assert.True(t, errors.Is(err, ErrSandboxNotReady), "Should return ErrSandboxNotReady")
|
|
assert.Contains(t, err.Error(), "failed to parse")
|
|
}
|
|
|
|
// TestBatchSandboxProvider_DynamicUpdate tests adding object after informer starts
|
|
func TestBatchSandboxProvider_DynamicUpdate(t *testing.T) {
|
|
namespace := "test-namespace"
|
|
|
|
fakeClient := fakeclientset.NewSimpleClientset()
|
|
informerFactory := informers.NewSharedInformerFactoryWithOptions(
|
|
fakeClient,
|
|
time.Second*30,
|
|
informers.WithNamespace(namespace),
|
|
)
|
|
|
|
batchSandboxInformer := informerFactory.Sandbox().V1alpha1().BatchSandboxes()
|
|
|
|
provider := &BatchSandboxProvider{
|
|
informerFactory: informerFactory,
|
|
lister: batchSandboxInformer.Lister(),
|
|
informerSynced: batchSandboxInformer.Informer().HasSynced,
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
err := provider.Start(ctx)
|
|
assert.NoError(t, err)
|
|
|
|
// Initially no sandbox exists
|
|
_, err = provider.GetEndpoint("dynamic-sandbox")
|
|
assert.Error(t, err)
|
|
assert.True(t, errors.Is(err, ErrSandboxNotFound), "Should return ErrSandboxNotFound")
|
|
assert.Contains(t, err.Error(), "not found")
|
|
|
|
// Add a new BatchSandbox
|
|
newBatchSandbox := &sandboxv1alpha1.BatchSandbox{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "dynamic-sandbox",
|
|
Namespace: namespace,
|
|
Annotations: map[string]string{
|
|
utils.AnnotationEndpoints: `["10.0.0.100"]`,
|
|
},
|
|
},
|
|
Spec: sandboxv1alpha1.BatchSandboxSpec{
|
|
Replicas: ptr(int32(1)),
|
|
},
|
|
Status: sandboxv1alpha1.BatchSandboxStatus{
|
|
Replicas: 1,
|
|
Ready: 1,
|
|
},
|
|
}
|
|
|
|
_, err = fakeClient.SandboxV1alpha1().BatchSandboxes(namespace).Create(
|
|
context.Background(), newBatchSandbox, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
// Wait for informer to pick up the change
|
|
assert.Eventually(t, func() bool {
|
|
endpoint, err := provider.GetEndpoint("dynamic-sandbox")
|
|
return err == nil && endpoint.Endpoint == "10.0.0.100"
|
|
}, 3*time.Second, 100*time.Millisecond, "Informer should eventually sync the new object")
|
|
}
|
|
|
|
// TestBatchSandboxProvider_StartCacheSyncFailure tests cache sync timeout
|
|
func TestBatchSandboxProvider_StartCacheSyncFailure(t *testing.T) {
|
|
namespace := "test-namespace"
|
|
|
|
fakeClient := fakeclientset.NewSimpleClientset()
|
|
informerFactory := informers.NewSharedInformerFactoryWithOptions(
|
|
fakeClient,
|
|
time.Second*30,
|
|
informers.WithNamespace(namespace),
|
|
)
|
|
|
|
batchSandboxInformer := informerFactory.Sandbox().V1alpha1().BatchSandboxes()
|
|
|
|
provider := &BatchSandboxProvider{
|
|
informerFactory: informerFactory,
|
|
lister: batchSandboxInformer.Lister(),
|
|
informerSynced: batchSandboxInformer.Informer().HasSynced,
|
|
}
|
|
|
|
// Create a context that expires immediately
|
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
|
|
defer cancel()
|
|
|
|
// Wait for context to expire
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
err := provider.Start(ctx)
|
|
assert.Error(t, err, "Should fail when cache sync times out")
|
|
assert.Contains(t, err.Error(), "failed to sync")
|
|
}
|
|
|
|
// TestBatchSandboxProvider_GetEndpointNonNotFoundError tests non-IsNotFound K8s errors
|
|
func TestBatchSandboxProvider_GetEndpointNonNotFoundError(t *testing.T) {
|
|
namespace := "test-namespace"
|
|
|
|
// Create a sandbox with Ready status but missing endpoint annotation
|
|
batchSandbox := &sandboxv1alpha1.BatchSandbox{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "missing-endpoint-sandbox",
|
|
Namespace: namespace,
|
|
Annotations: map[string]string{
|
|
utils.AnnotationEndpoints: `["10.0.0.1"]`,
|
|
},
|
|
},
|
|
Spec: sandboxv1alpha1.BatchSandboxSpec{
|
|
Replicas: ptr(int32(1)),
|
|
},
|
|
Status: sandboxv1alpha1.BatchSandboxStatus{
|
|
Replicas: 1,
|
|
Ready: 1,
|
|
},
|
|
}
|
|
|
|
fakeClient := fakeclientset.NewSimpleClientset(batchSandbox)
|
|
informerFactory := informers.NewSharedInformerFactoryWithOptions(
|
|
fakeClient,
|
|
time.Second*30,
|
|
informers.WithNamespace(namespace),
|
|
)
|
|
|
|
batchSandboxInformer := informerFactory.Sandbox().V1alpha1().BatchSandboxes()
|
|
|
|
provider := &BatchSandboxProvider{
|
|
informerFactory: informerFactory,
|
|
lister: batchSandboxInformer.Lister(),
|
|
informerSynced: batchSandboxInformer.Informer().HasSynced,
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
err := provider.Start(ctx)
|
|
assert.NoError(t, err)
|
|
|
|
// Manually add object to informer cache
|
|
err = batchSandboxInformer.Informer().GetStore().Add(batchSandbox)
|
|
assert.NoError(t, err)
|
|
|
|
// Should successfully get endpoint
|
|
endpoint, err := provider.GetEndpoint("missing-endpoint-sandbox")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "10.0.0.1", endpoint.Endpoint)
|
|
}
|
|
|
|
func TestBatchSandboxProvider_GetEndpoint_AmbiguousAcrossNamespaces(t *testing.T) {
|
|
namespaceA := "ns-a"
|
|
namespaceB := "ns-b"
|
|
sandboxName := "shared-id"
|
|
|
|
first := &sandboxv1alpha1.BatchSandbox{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: sandboxName,
|
|
Namespace: namespaceA,
|
|
Annotations: map[string]string{
|
|
utils.AnnotationEndpoints: `["10.0.0.1"]`,
|
|
},
|
|
},
|
|
Status: sandboxv1alpha1.BatchSandboxStatus{Replicas: 1, Ready: 1},
|
|
}
|
|
second := &sandboxv1alpha1.BatchSandbox{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: sandboxName,
|
|
Namespace: namespaceB,
|
|
Annotations: map[string]string{
|
|
utils.AnnotationEndpoints: `["10.0.0.2"]`,
|
|
},
|
|
},
|
|
Status: sandboxv1alpha1.BatchSandboxStatus{Replicas: 1, Ready: 1},
|
|
}
|
|
|
|
fakeClient := fakeclientset.NewSimpleClientset(first, second)
|
|
informerFactory := informers.NewSharedInformerFactoryWithOptions(
|
|
fakeClient,
|
|
time.Second*30,
|
|
informers.WithNamespace(metav1.NamespaceAll),
|
|
)
|
|
batchSandboxInformer := informerFactory.Sandbox().V1alpha1().BatchSandboxes()
|
|
provider := &BatchSandboxProvider{
|
|
informerFactory: informerFactory,
|
|
lister: batchSandboxInformer.Lister(),
|
|
informerSynced: batchSandboxInformer.Informer().HasSynced,
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
err := provider.Start(ctx)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, batchSandboxInformer.Informer().GetStore().Add(first))
|
|
assert.NoError(t, batchSandboxInformer.Informer().GetStore().Add(second))
|
|
|
|
_, err = provider.GetEndpoint(sandboxName)
|
|
assert.Error(t, err)
|
|
assert.True(t, strings.Contains(err.Error(), "ambiguous sandbox id"))
|
|
}
|
|
|
|
// ptr is a helper function to create int32 pointer
|
|
func ptr(i int32) *int32 {
|
|
return &i
|
|
}
|