Feat(multicluster): Enhance Unit Test Coverage for Multicluster Packages (#6892)

* feat(multicluster): Enhance unit test coverage for multicluster utilities

This commit introduces a comprehensive suite of unit tests for the multicluster management functions in pkg/multicluster.

Key changes include:
  - `cluster_management_test.go`: Improves the structure of TestDetachCluster and TestRenameCluster by organizing test cases into a collection, which enhances clarity and
    simplifies adding new scenarios.
  - `utils_test.go` and `virtual_cluster_test.go`: Adds new test cases to validate additional utility and virtual cluster helper functions, increasing overall test
    coverage.

These additions improve the overall test coverage and ensure the correctness and reliability of multicluster operations.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* feat(multicluster): Add unit tests for multicluster workflow provider

This commit introduces new unit tests for the multicluster workflow provider located in pkg/workflow/providers/multicluster.

Key additions include:
  - Comprehensive tests for the Deploy workflow step, covering parameter validation, error handling, and successful deployment scenarios.
  - New tests for GetPlacementsFromTopologyPolicies to ensure correct placement resolution from topology policies, including error cases and default behaviors.

These additions improve the test coverage and ensure the robustness of the multicluster workflow provider.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* fix(multicluster): Correct duplicate import in utils_test.go

This commit resolves a linting error (ST1019) in pkg/multicluster/utils_test.go caused by the k8s.io/api/core/v1 package being imported twice with different aliases (v1
and corev1).

The redundant import alias v1 has been removed, and the corresponding type reference for []v1.Secret has been updated to []corev1.Secret to maintain consistency.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* test(multicluster): fix cross-test side effects

The TestListExistingClusterSecrets function mutates the global
variable ClusterGatewaySecretNamespace without restoring its original
value. This can lead to unpredictable behavior in other tests that
rely on this variable.

This commit fixes the issue by saving the value of
ClusterGatewaySecretNamespace before the test runs and restoring it
afterward using a defer statement.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* test(multicluster): remove redundant test case in TestContext

The `TestContextWithClusterName` sub-test in `TestContext` is redundant, as its functionality is already covered by the more comprehensive `TestClusterNameInContext` sub-test.

This commit removes the unnecessary test to improve the clarity and maintainability of the test suite without sacrificing coverage.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

---------

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>
This commit is contained in:
AshvinBambhaniya2003
2025-09-15 21:07:55 +05:30
committed by GitHub
parent d6ad578070
commit 2139c813ad
4 changed files with 1682 additions and 25 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -22,12 +22,18 @@ import (
"github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1"
clustercommon "github.com/oam-dev/cluster-gateway/pkg/common"
v1 "k8s.io/api/core/v1"
v12 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/pkg/oam"
velacommon "github.com/oam-dev/kubevela/pkg/utils/common"
)
func TestUpgradeExistingClusterSecret(t *testing.T) {
@@ -37,28 +43,262 @@ func TestUpgradeExistingClusterSecret(t *testing.T) {
ClusterGatewaySecretNamespace = oldClusterGatewaySecretNamespace
}()
ctx := context.Background()
c := fake.NewClientBuilder().WithScheme(common.Scheme).Build()
secret := &v1.Secret{
ObjectMeta: v12.ObjectMeta{
c := fake.NewClientBuilder().WithScheme(velacommon.Scheme).Build()
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "example-outdated-cluster-secret",
Namespace: "default",
Labels: map[string]string{
"cluster.core.oam.dev/cluster-credential": "tls",
},
},
Type: v1.SecretTypeTLS,
Type: corev1.SecretTypeTLS,
}
if err := c.Create(ctx, secret); err != nil {
t.Fatalf("failed to create fake outdated cluster secret, err: %v", err)
require.NoError(t, c.Create(ctx, secret))
require.NoError(t, UpgradeExistingClusterSecret(ctx, c))
newSecret := &corev1.Secret{}
require.NoError(t, c.Get(ctx, client.ObjectKeyFromObject(secret), newSecret))
require.Equal(t, string(v1alpha1.CredentialTypeX509Certificate), newSecret.Labels[clustercommon.LabelKeyClusterCredentialType])
}
func TestContext(t *testing.T) {
t.Run("TestClusterNameInContext", func(t *testing.T) {
ctx := context.Background()
require.Equal(t, "", ClusterNameInContext(ctx))
ctx = ContextWithClusterName(ctx, "my-cluster")
require.Equal(t, "my-cluster", ClusterNameInContext(ctx))
})
t.Run("TestContextInLocalCluster", func(t *testing.T) {
ctx := context.Background()
ctx = ContextInLocalCluster(ctx)
require.Equal(t, ClusterLocalName, ClusterNameInContext(ctx))
})
}
func TestResourcesWithClusterName(t *testing.T) {
testCases := []struct {
name string
clusterName string
objs []*unstructured.Unstructured
expected []*unstructured.Unstructured
}{
{
name: "Empty slice",
clusterName: "my-cluster",
objs: []*unstructured.Unstructured{},
expected: nil,
},
{
name: "Nil object",
clusterName: "my-cluster",
objs: []*unstructured.Unstructured{nil},
expected: nil,
},
{
name: "Object without cluster name label",
clusterName: "my-cluster",
objs: []*unstructured.Unstructured{{Object: map[string]interface{}{}}},
expected: []*unstructured.Unstructured{{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
oam.LabelAppCluster: "my-cluster",
},
},
},
}},
},
{
name: "Object with existing cluster name label",
clusterName: "my-cluster",
objs: []*unstructured.Unstructured{{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
oam.LabelAppCluster: "other-cluster",
},
},
},
}},
expected: []*unstructured.Unstructured{{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
oam.LabelAppCluster: "other-cluster",
},
},
},
}},
},
}
if err := UpgradeExistingClusterSecret(ctx, c); err != nil {
t.Fatalf("expect no error while upgrading outdated cluster secret but encounter error: %v", err)
}
newSecret := &v1.Secret{}
if err := c.Get(ctx, client.ObjectKeyFromObject(secret), newSecret); err != nil {
t.Fatalf("found error while getting updated cluster secret: %v", err)
}
if newSecret.Labels[clustercommon.LabelKeyClusterCredentialType] != string(v1alpha1.CredentialTypeX509Certificate) {
t.Fatalf("updated secret label should has credential type x509")
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := ResourcesWithClusterName(tc.clusterName, tc.objs...)
require.Equal(t, tc.expected, result)
})
}
}
func TestGetClusterGatewayService(t *testing.T) {
ctx := context.Background()
scheme := newTestScheme()
apiregistrationv1.AddToScheme(scheme)
testCases := []struct {
name string
cli client.Client
expectErr bool
verify func(t *testing.T, svc *apiregistrationv1.ServiceReference)
}{
{
name: "APIService not found",
cli: fake.NewClientBuilder().WithScheme(scheme).Build(),
expectErr: true,
},
{
name: "APIService found but no service spec",
cli: fake.NewClientBuilder().WithScheme(scheme).WithObjects(&apiregistrationv1.APIService{
ObjectMeta: metav1.ObjectMeta{Name: "v1alpha1.cluster.core.oam.dev"},
}).Build(),
expectErr: true,
},
{
name: "APIService found but not available",
cli: fake.NewClientBuilder().WithScheme(scheme).WithObjects(&apiregistrationv1.APIService{
ObjectMeta: metav1.ObjectMeta{Name: "v1alpha1.cluster.core.oam.dev"},
Spec: apiregistrationv1.APIServiceSpec{
Service: &apiregistrationv1.ServiceReference{
Name: "my-service",
Namespace: "my-namespace",
},
},
Status: apiregistrationv1.APIServiceStatus{
Conditions: []apiregistrationv1.APIServiceCondition{
{
Type: apiregistrationv1.Available,
Status: apiregistrationv1.ConditionFalse,
},
},
},
}).Build(),
expectErr: true,
verify: func(t *testing.T, svc *apiregistrationv1.ServiceReference) {
require.NotNil(t, svc)
require.Equal(t, "my-service", svc.Name)
},
},
{
name: "APIService found and available",
cli: fake.NewClientBuilder().WithScheme(scheme).WithObjects(&apiregistrationv1.APIService{
ObjectMeta: metav1.ObjectMeta{Name: "v1alpha1.cluster.core.oam.dev"},
Spec: apiregistrationv1.APIServiceSpec{
Service: &apiregistrationv1.ServiceReference{
Name: "my-service",
Namespace: "my-namespace",
},
},
Status: apiregistrationv1.APIServiceStatus{
Conditions: []apiregistrationv1.APIServiceCondition{
{
Type: apiregistrationv1.Available,
Status: apiregistrationv1.ConditionTrue,
},
},
},
}).Build(),
verify: func(t *testing.T, svc *apiregistrationv1.ServiceReference) {
require.NotNil(t, svc)
require.Equal(t, "my-service", svc.Name)
},
},
{
name: "Client Get error",
cli: &mockClient{
Client: fake.NewClientBuilder().WithScheme(scheme).Build(),
getErr: errors.New("client error"),
},
expectErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
svc, err := GetClusterGatewayService(ctx, tc.cli)
if tc.expectErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
if tc.verify != nil {
tc.verify(t, svc)
}
})
}
}
func TestListExistingClusterSecrets(t *testing.T) {
oldClusterGatewaySecretNamespace := ClusterGatewaySecretNamespace
defer func() {
ClusterGatewaySecretNamespace = oldClusterGatewaySecretNamespace
}()
ctx := context.Background()
scheme := newTestScheme()
ClusterGatewaySecretNamespace = "vela-system"
testCases := []struct {
name string
cli client.Client
expectErr bool
verify func(t *testing.T, secrets []corev1.Secret)
}{
{
name: "No secrets exist",
cli: fake.NewClientBuilder().WithScheme(scheme).Build(),
verify: func(t *testing.T, secrets []corev1.Secret) {
require.Empty(t, secrets)
},
},
{
name: "Secrets exist, but none have the required label",
cli: fake.NewClientBuilder().WithScheme(scheme).WithObjects(&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "no-label",
Namespace: ClusterGatewaySecretNamespace,
},
}).Build(),
verify: func(t *testing.T, secrets []corev1.Secret) {
require.Empty(t, secrets)
},
},
{
name: "Secrets exist with the required label",
cli: fake.NewClientBuilder().WithScheme(scheme).WithObjects(&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "with-label",
Namespace: ClusterGatewaySecretNamespace,
Labels: map[string]string{clustercommon.LabelKeyClusterCredentialType: string(v1alpha1.CredentialTypeX509Certificate)},
},
}).Build(),
verify: func(t *testing.T, secrets []corev1.Secret) {
require.Len(t, secrets, 1)
require.Equal(t, "with-label", secrets[0].Name)
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
secrets, err := ListExistingClusterSecrets(ctx, tc.cli)
if tc.expectErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
if tc.verify != nil {
tc.verify(t, secrets)
}
})
}
}

View File

@@ -18,6 +18,7 @@ package multicluster
import (
"context"
"encoding/json"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
@@ -155,6 +156,109 @@ var _ = Describe("Test Virtual Cluster", func() {
Expect(cv.Major).Should(BeEquivalentTo("1"))
})
It("Test virtual cluster helpers", func() {
By("Test FullName")
vcWithAlias := &VirtualCluster{Name: "test", Alias: "alias"}
Expect(vcWithAlias.FullName()).To(Equal("test (alias)"))
vcWithoutAlias := &VirtualCluster{Name: "test"}
Expect(vcWithoutAlias.FullName()).To(Equal("test"))
By("Test get/set cluster alias")
secret := &v1.Secret{}
setClusterAlias(secret, "my-alias")
Expect(getClusterAlias(secret)).To(Equal("my-alias"))
annots := secret.GetAnnotations()
Expect(annots).ToNot(BeNil())
Expect(annots[v1alpha1.AnnotationClusterAlias]).To(Equal("my-alias"))
By("Test NewVirtualClusterFromLocal")
vc := NewVirtualClusterFromLocal()
Expect(vc.Name).To(Equal(ClusterLocalName))
Expect(vc.Accepted).To(BeTrue())
Expect(vc.EndPoint).To(Equal(types.ClusterBlankEndpoint))
By("Test MatchVirtualClusterLabels")
ClusterGatewaySecretNamespace = "vela-system" // as set in other test
labels := MatchVirtualClusterLabels{"key": "val"}
opts := &client.ListOptions{}
labels.ApplyToList(opts)
Expect(opts.Namespace).To(Equal(ClusterGatewaySecretNamespace))
Expect(opts.LabelSelector).NotTo(BeNil())
Expect(opts.LabelSelector.String()).To(ContainSubstring("key=val"))
Expect(opts.LabelSelector.String()).To(ContainSubstring(clustercommon.LabelKeyClusterCredentialType))
delOpts := &client.DeleteAllOfOptions{}
labels.ApplyToDeleteAllOf(delOpts)
Expect(delOpts.ListOptions.Namespace).To(Equal(ClusterGatewaySecretNamespace))
Expect(delOpts.ListOptions.LabelSelector).NotTo(BeNil())
Expect(delOpts.ListOptions.LabelSelector.String()).To(ContainSubstring("key=val"))
By("Test get/set cluster version")
versionedSecret := &v1.Secret{}
cv := types.ClusterVersion{Major: "1", Minor: "20", GitVersion: "v1.20.0"}
setClusterVersion(versionedSecret, cv)
newCV, err := getClusterVersionFromObject(versionedSecret)
Expect(err).To(Succeed())
Expect(newCV).To(Equal(cv))
versionedSecret.Annotations = nil
_, err = getClusterVersionFromObject(versionedSecret)
Expect(err).ToNot(Succeed())
secretWithEmptyAnnotation := &v1.Secret{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{}}}
_, err = getClusterVersionFromObject(secretWithEmptyAnnotation)
Expect(err).ToNot(Succeed())
})
It("Test GetVersionInfoFromObject", func() {
ClusterGatewaySecretNamespace = "vela-system3"
ctx := context.Background()
ns := &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ClusterGatewaySecretNamespace}}
Expect(k8sClient.Create(ctx, ns)).Should(Succeed())
defer func() {
Expect(k8sClient.Delete(ctx, ns)).Should(Succeed())
}()
By("Setup a secret with version info")
cv := types.ClusterVersion{Major: "1", Minor: "21", GitVersion: "v1.21.0"}
cvJSON, err := json.Marshal(cv)
Expect(err).To(Succeed())
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-with-version",
Namespace: ClusterGatewaySecretNamespace,
Labels: map[string]string{clustercommon.LabelKeyClusterCredentialType: "X509"},
Annotations: map[string]string{
types.AnnotationClusterVersion: string(cvJSON),
},
},
}
Expect(k8sClient.Create(ctx, secret)).Should(Succeed())
By("Test getting version from the secret")
retrievedCV := GetVersionInfoFromObject(ctx, k8sClient, "cluster-with-version")
Expect(retrievedCV).To(Equal(cv))
By("Test with a cluster that doesn't exist, should fallback to control plane version")
originalCPVersion := types.ControlPlaneClusterVersion
types.ControlPlaneClusterVersion = types.ClusterVersion{GitVersion: "v1.22.0"}
defer func() { types.ControlPlaneClusterVersion = originalCPVersion }()
retrievedCV = GetVersionInfoFromObject(ctx, k8sClient, "non-existent-cluster")
Expect(retrievedCV).To(Equal(types.ControlPlaneClusterVersion))
By("Test with a secret without version info, should fallback")
secretNoVersion := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-no-version",
Namespace: ClusterGatewaySecretNamespace,
Labels: map[string]string{clustercommon.LabelKeyClusterCredentialType: "X509"},
},
}
Expect(k8sClient.Create(ctx, secretNoVersion)).Should(Succeed())
retrievedCV = GetVersionInfoFromObject(ctx, k8sClient, "cluster-no-version")
Expect(retrievedCV).To(Equal(types.ControlPlaneClusterVersion))
})
})
type fakeClient struct {

View File

@@ -20,30 +20,63 @@ import (
"context"
"testing"
"cuelang.org/go/cue"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
clusterv1alpha1 "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1"
clustercommon "github.com/oam-dev/cluster-gateway/pkg/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/appfile"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/utils/common"
commontypes "github.com/oam-dev/kubevela/pkg/utils/common"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
wfmock "github.com/kubevela/workflow/pkg/mock"
)
// mockAction is a mock implementation of types.Action for testing.
type mockAction struct {
wfmock.Action
WaitCalled bool
WaitReason string
}
// Wait records that the wait action was called.
func (a *mockAction) Wait(reason string) {
a.WaitCalled = true
a.WaitReason = reason
}
func TestListClusters(t *testing.T) {
multicluster.ClusterGatewaySecretNamespace = types.DefaultKubeVelaNS
r := require.New(t)
originalNS := multicluster.ClusterGatewaySecretNamespace
multicluster.ClusterGatewaySecretNamespace = types.DefaultKubeVelaNS
t.Cleanup(func() {
multicluster.ClusterGatewaySecretNamespace = originalNS
})
ctx := context.Background()
cli := fake.NewClientBuilder().WithScheme(common.Scheme).Build()
cli := fake.NewClientBuilder().WithScheme(commontypes.Scheme).Build()
clusterNames := []string{"cluster-a", "cluster-b"}
for _, secretName := range clusterNames {
secret := &corev1.Secret{}
secret.Name = secretName
secret.Namespace = multicluster.ClusterGatewaySecretNamespace
secret.Labels = map[string]string{clustercommon.LabelKeyClusterCredentialType: string(clusterv1alpha1.CredentialTypeX509Certificate)}
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: multicluster.ClusterGatewaySecretNamespace,
Labels: map[string]string{
clustercommon.LabelKeyClusterCredentialType: string(clusterv1alpha1.CredentialTypeX509Certificate),
},
},
}
r.NoError(cli.Create(context.Background(), secret))
}
res, err := ListClusters(ctx, &oamprovidertypes.Params[any]{
@@ -54,3 +87,195 @@ func TestListClusters(t *testing.T) {
r.NoError(err)
r.Equal(clusterNames, res.Returns.Outputs.Clusters)
}
func TestDeploy(t *testing.T) {
r := require.New(t)
ctx := context.Background()
cli := fake.NewClientBuilder().WithScheme(commontypes.Scheme).Build()
// Mock component functions
componentApply := func(ctx context.Context, comp common.ApplicationComponent, patcher *cue.Value, clusterName string, overrideNamespace string) (*unstructured.Unstructured, []*unstructured.Unstructured, bool, error) {
return nil, nil, true, nil
}
componentHealthCheck := func(ctx context.Context, comp common.ApplicationComponent, patcher *cue.Value, clusterName string, overrideNamespace string) (bool, *common.ApplicationComponentStatus, *unstructured.Unstructured, []*unstructured.Unstructured, error) {
return true, nil, nil, nil, nil
}
workloadRender := func(ctx context.Context, comp common.ApplicationComponent) (*appfile.Component, error) {
return &appfile.Component{}, nil
}
createMockParams := func(parallelism int64) *DeployParams {
action := &mockAction{}
return &DeployParams{
Params: DeployParameter{
Parallelism: parallelism,
IgnoreTerraformComponent: true,
Policies: []string{},
},
RuntimeParams: oamprovidertypes.RuntimeParams{
Action: action,
KubeClient: cli,
ComponentApply: componentApply,
ComponentHealthCheck: componentHealthCheck,
WorkloadRender: workloadRender,
Appfile: &appfile.Appfile{
Name: "test-app",
Namespace: "default",
Policies: []v1beta1.AppPolicy{},
},
},
}
}
cases := map[string]struct {
reason string
params *DeployParams
expectError bool
errorContains string
expectPanic bool
}{
"parallelism zero validation error": {
reason: "Should return a validation error for zero parallelism",
params: createMockParams(0),
expectError: true,
errorContains: "parallelism cannot be smaller than 1",
},
"parallelism negative validation error": {
reason: "Should return a validation error for negative parallelism",
params: createMockParams(-1),
expectError: true,
errorContains: "parallelism cannot be smaller than 1",
},
"parameters nil pointer handling": {
reason: "Should panic when params are nil",
params: nil,
expectPanic: true,
},
"successful deployment healthy": {
reason: "Should execute successfully with valid parameters",
params: createMockParams(1),
},
"successful deployment unhealthy wait": {
reason: "Should execute successfully even with higher parallelism",
params: createMockParams(2),
},
"executor deploy error propagation": {
reason: "Should pass validation and any errors should be from the executor",
params: createMockParams(1),
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
if tc.expectPanic {
r.Panics(func() {
_, _ = Deploy(ctx, tc.params)
})
return
}
result, err := Deploy(ctx, tc.params)
if tc.expectError {
r.Error(err)
if tc.errorContains != "" {
r.Contains(err.Error(), tc.errorContains)
}
} else {
r.NoError(err)
r.Nil(result)
}
})
}
}
func TestGetPlacementsFromTopologyPolicies(t *testing.T) {
r := require.New(t)
ctx := context.Background()
scheme := commontypes.Scheme
r.NoError(v1alpha1.AddToScheme(scheme))
topologyPolicy := &v1alpha1.Policy{
ObjectMeta: metav1.ObjectMeta{Name: "my-topology", Namespace: "default"},
Type: v1alpha1.TopologyPolicyType,
Properties: &runtime.RawExtension{
Raw: []byte(`{"clusters":["local"],"namespace":"topo-ns"}`),
},
}
appFileTopologyPolicy := v1beta1.AppPolicy{
Name: "my-topology",
Type: v1alpha1.TopologyPolicyType,
Properties: &runtime.RawExtension{
Raw: []byte(`{"clusters":["local"],"namespace":"topo-ns"}`),
},
}
cases := map[string]struct {
reason string
policiesInAppfile []v1beta1.AppPolicy
policiesToGet []string
objectsToCreate []client.Object
expectedPlacements []v1alpha1.PlacementDecision
expectError bool
errorContains string
}{
"Successful placement resolution with single policy": {
reason: "Should resolve placement from a single topology policy",
objectsToCreate: []client.Object{topologyPolicy},
policiesInAppfile: []v1beta1.AppPolicy{appFileTopologyPolicy},
policiesToGet: []string{"my-topology"},
expectedPlacements: []v1alpha1.PlacementDecision{{Cluster: "local", Namespace: "topo-ns"}},
},
"Policy not found in appfile": {
reason: "Should return an error if the policy is not found in the appfile",
policiesToGet: []string{"non-existent-policy"},
expectError: true,
errorContains: "policy non-existent-policy not found",
},
"Empty policy list returns default local placement": {
reason: "Should return default local placement when no policies are specified",
policiesToGet: []string{},
expectedPlacements: []v1alpha1.PlacementDecision{{Cluster: "local"}},
},
"Nil policy names list returns default local placement": {
reason: "Should return default local placement when the policy list is nil",
policiesToGet: nil,
expectedPlacements: []v1alpha1.PlacementDecision{{Cluster: "local"}},
},
"Empty appfile policies list with a policy name": {
reason: "Should return an error if appfile has no policies but a policy is requested",
policiesToGet: []string{"some-policy"},
expectError: true,
errorContains: "policy some-policy not found",
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(tc.objectsToCreate...).Build()
af := &appfile.Appfile{
Name: "test-app",
Namespace: "default",
Policies: tc.policiesInAppfile,
}
params := &PoliciesParams{
Params: PoliciesVars{Policies: tc.policiesToGet},
RuntimeParams: oamprovidertypes.RuntimeParams{KubeClient: cli, Appfile: af},
}
result, err := GetPlacementsFromTopologyPolicies(ctx, params)
if tc.expectError {
r.Error(err)
if tc.errorContains != "" {
r.Contains(err.Error(), tc.errorContains)
}
} else {
r.NoError(err)
r.NotNil(result)
r.Equal(tc.expectedPlacements, result.Returns.Placements)
}
})
}
}