Skip to content

Commit 1ff62b3

Browse files
ci: add unit-tests
1 parent 36d83cc commit 1ff62b3

File tree

3 files changed

+1952
-0
lines changed

3 files changed

+1952
-0
lines changed

pkg/controller/stackconfigpolicy/controller_test.go

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@ import (
2929
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/hash"
3030
commonlabels "github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/labels"
3131
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/license"
32+
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/operator"
3233
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/reconciler"
3334
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/watches"
3435
esclient "github.com/elastic/cloud-on-k8s/v3/pkg/controller/elasticsearch/client"
3536
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/elasticsearch/filesettings"
3637
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/elasticsearch/label"
38+
eslabel "github.com/elastic/cloud-on-k8s/v3/pkg/controller/elasticsearch/label"
3739
"github.com/elastic/cloud-on-k8s/v3/pkg/utils/k8s"
3840
"github.com/elastic/cloud-on-k8s/v3/pkg/utils/net"
3941
)
@@ -676,6 +678,279 @@ func TestReconcileStackConfigPolicy_Reconcile(t *testing.T) {
676678
}
677679
}
678680

681+
func TestReconcileStackConfigPolicy_MultipleStackConfigPolicies(t *testing.T) {
682+
// Setup: Create an Elasticsearch cluster and multiple StackConfigPolicies with different weights
683+
esFixture := esv1.Elasticsearch{
684+
ObjectMeta: metav1.ObjectMeta{
685+
Namespace: "ns",
686+
Name: "test-es",
687+
Labels: map[string]string{"env": "prod"},
688+
},
689+
Spec: esv1.ElasticsearchSpec{Version: "8.6.1"},
690+
}
691+
692+
// Policy with weight 10 (applied first)
693+
policy1 := policyv1alpha1.StackConfigPolicy{
694+
ObjectMeta: metav1.ObjectMeta{
695+
Namespace: "ns",
696+
Name: "policy-low",
697+
},
698+
Spec: policyv1alpha1.StackConfigPolicySpec{
699+
Weight: 10,
700+
ResourceSelector: metav1.LabelSelector{MatchLabels: map[string]string{"env": "prod"}},
701+
Elasticsearch: policyv1alpha1.ElasticsearchConfigPolicySpec{
702+
ClusterSettings: &commonv1.Config{Data: map[string]interface{}{
703+
"indices.recovery.max_bytes_per_sec": "40mb",
704+
}},
705+
Config: &commonv1.Config{Data: map[string]interface{}{
706+
"logger.org.elasticsearch.discovery": "INFO",
707+
}},
708+
},
709+
},
710+
}
711+
712+
// Policy with weight 20 (applied second, overrides policy1)
713+
policy2 := policyv1alpha1.StackConfigPolicy{
714+
ObjectMeta: metav1.ObjectMeta{
715+
Namespace: "ns",
716+
Name: "policy-high",
717+
},
718+
Spec: policyv1alpha1.StackConfigPolicySpec{
719+
Weight: 20,
720+
ResourceSelector: metav1.LabelSelector{MatchLabels: map[string]string{"env": "prod"}},
721+
Elasticsearch: policyv1alpha1.ElasticsearchConfigPolicySpec{
722+
ClusterSettings: &commonv1.Config{Data: map[string]interface{}{
723+
"indices.recovery.max_bytes_per_sec": "50mb", // Overrides policy1
724+
}},
725+
Config: &commonv1.Config{Data: map[string]interface{}{
726+
"logger.org.elasticsearch.gateway": "DEBUG", // Additional setting
727+
}},
728+
SecretMounts: []policyv1alpha1.SecretMount{
729+
{
730+
SecretName: "test-secret",
731+
MountPath: "/usr/test",
732+
},
733+
},
734+
},
735+
},
736+
}
737+
738+
// Policy with same weight as policy2 (should cause conflict)
739+
policy3Conflicting := policyv1alpha1.StackConfigPolicy{
740+
ObjectMeta: metav1.ObjectMeta{
741+
Namespace: "ns",
742+
Name: "policy-conflict",
743+
},
744+
Spec: policyv1alpha1.StackConfigPolicySpec{
745+
Weight: 20, // Same weight as policy2
746+
ResourceSelector: metav1.LabelSelector{MatchLabels: map[string]string{"env": "prod"}},
747+
Elasticsearch: policyv1alpha1.ElasticsearchConfigPolicySpec{
748+
ClusterSettings: &commonv1.Config{Data: map[string]interface{}{
749+
"indices.recovery.max_bytes_per_sec": "60mb",
750+
}},
751+
},
752+
},
753+
}
754+
755+
// Initial empty file settings secret (will be populated by controller)
756+
esFileSettingsSecret := corev1.Secret{
757+
ObjectMeta: metav1.ObjectMeta{
758+
Namespace: "ns",
759+
Name: "test-es-es-file-settings",
760+
Labels: map[string]string{
761+
commonv1.TypeLabelName: "elasticsearch",
762+
eslabel.ClusterNameLabelName: "test-es",
763+
commonlabels.StackConfigPolicyOnDeleteLabelName: commonlabels.OrphanSecretResetOnPolicyDelete,
764+
},
765+
},
766+
Data: map[string][]byte{"settings.json": []byte(`{"metadata":{"version":"1","compatibility":"8.4.0"},"state":{"cluster_settings":{},"snapshot_repositories":{},"slm":{},"role_mappings":{},"autoscaling":{},"ilm":{},"ingest_pipelines":{},"index_templates":{"component_templates":{},"composable_index_templates":{}}}}`)},
767+
}
768+
769+
esPod := getEsPod("ns", map[string]string{})
770+
771+
// Source secret that will be mounted (exists in policy namespace)
772+
sourceSecret := corev1.Secret{
773+
ObjectMeta: metav1.ObjectMeta{
774+
Name: "test-secret",
775+
Namespace: "ns",
776+
},
777+
Data: map[string][]byte{
778+
"key1": []byte("value1"),
779+
},
780+
}
781+
782+
tests := []struct {
783+
name string
784+
policies []policyv1alpha1.StackConfigPolicy
785+
reconcilePolicy string // Which policy to reconcile
786+
wantResources int
787+
wantReady int
788+
wantPhase policyv1alpha1.PolicyPhase
789+
wantErr bool
790+
wantRequeueAfter bool
791+
validateSettings func(t *testing.T, r ReconcileStackConfigPolicy)
792+
}{
793+
{
794+
name: "Multiple policies with different weights merge successfully",
795+
policies: []policyv1alpha1.StackConfigPolicy{policy1, policy2},
796+
reconcilePolicy: "policy-low",
797+
wantResources: 1,
798+
wantReady: 0,
799+
wantPhase: policyv1alpha1.ApplyingChangesPhase,
800+
wantErr: false,
801+
wantRequeueAfter: true,
802+
validateSettings: func(t *testing.T, r ReconcileStackConfigPolicy) {
803+
// Verify the file settings secret was updated
804+
esNsn := k8s.ExtractNamespacedName(&esFixture)
805+
806+
settingsSecretNsn := types.NamespacedName{
807+
Namespace: esNsn.Namespace,
808+
Name: esv1.FileSettingsSecretName(esNsn.Name),
809+
}
810+
811+
var secret corev1.Secret
812+
err := r.Client.Get(context.Background(), settingsSecretNsn, &secret)
813+
require.NoError(t, err)
814+
815+
// Verify the file settings secret has merged config
816+
settings := r.getSettings(t, settingsSecretNsn)
817+
818+
// Check if ClusterSettings was populated
819+
if settings.State.ClusterSettings == nil {
820+
t.Logf("Secret data: %s", string(secret.Data["settings.json"]))
821+
t.Fatal("ClusterSettings should not be nil after reconciliation")
822+
}
823+
require.NotNil(t, settings.State.ClusterSettings.Data, "ClusterSettings.Data should not be nil")
824+
825+
// Should have the value from policy2 (higher weight)
826+
assert.EqualValues(t, map[string]any{
827+
"indices": map[string]any{
828+
"recovery": map[string]any{
829+
"max_bytes_per_sec": "40mb",
830+
},
831+
},
832+
}, settings.State.ClusterSettings.Data, "ClusterSettings.Data")
833+
834+
owners, err := getSoftOwnerPolicies(&secret)
835+
assert.NoError(t, err)
836+
assert.Len(t, owners, 2, "esConfigSecret should be owned by 2 policies")
837+
// Verify both policies are in the owner list
838+
assert.Contains(t, owners, types.NamespacedName{Namespace: "ns", Name: "policy-low"}, "policy-low should be an owner of esConfigSecret")
839+
assert.Contains(t, owners, types.NamespacedName{Namespace: "ns", Name: "policy-high"}, "policy-high should be an owner of esConfigSecret")
840+
},
841+
},
842+
{
843+
name: "Policies with same weight cause conflict",
844+
policies: []policyv1alpha1.StackConfigPolicy{policy2, policy3Conflicting},
845+
reconcilePolicy: "policy-high",
846+
wantResources: 1,
847+
wantReady: 0,
848+
wantPhase: policyv1alpha1.ConflictPhase,
849+
wantErr: false,
850+
wantRequeueAfter: true,
851+
validateSettings: func(t *testing.T, r ReconcileStackConfigPolicy) {
852+
// Verify policy status shows conflict
853+
policy := r.getPolicy(t, types.NamespacedName{Namespace: "ns", Name: "policy-high"})
854+
855+
esStatus := policy.Status.Details["elasticsearch"]["ns/test-es"]
856+
assert.Equal(t, policyv1alpha1.ConflictPhase, esStatus.Phase)
857+
},
858+
},
859+
{
860+
name: "Reconciling second policy sees merged state",
861+
policies: []policyv1alpha1.StackConfigPolicy{policy1, policy2},
862+
reconcilePolicy: "policy-high",
863+
wantResources: 1,
864+
wantReady: 0,
865+
wantPhase: policyv1alpha1.ApplyingChangesPhase,
866+
wantErr: false,
867+
wantRequeueAfter: true,
868+
validateSettings: func(t *testing.T, r ReconcileStackConfigPolicy) {
869+
// Verify elasticsearch config secret exists with merged config
870+
var esConfigSecret corev1.Secret
871+
err := r.Client.Get(context.Background(), types.NamespacedName{
872+
Namespace: "ns",
873+
Name: esv1.StackConfigElasticsearchConfigSecretName("test-es"),
874+
}, &esConfigSecret)
875+
assert.NoError(t, err)
876+
877+
// Verify elasticsearch config secret is soft-owned by multiple policies
878+
owners, err := getSoftOwnerPolicies(&esConfigSecret)
879+
assert.NoError(t, err)
880+
assert.Len(t, owners, 2, "esConfigSecret should be owned by 2 policies")
881+
// Verify both policies are in the owner list
882+
assert.Contains(t, owners, types.NamespacedName{Namespace: "ns", Name: "policy-low"}, "policy-low should be an owner of esConfigSecret")
883+
assert.Contains(t, owners, types.NamespacedName{Namespace: "ns", Name: "policy-high"}, "policy-high should be an owner of esConfigSecret")
884+
885+
// Verify secret mount secret exists
886+
var secretMountSecret corev1.Secret
887+
err = r.Client.Get(context.Background(), types.NamespacedName{
888+
Namespace: "ns",
889+
Name: esv1.StackConfigAdditionalSecretName("test-es", "test-secret"),
890+
}, &secretMountSecret)
891+
assert.NoError(t, err)
892+
893+
// Verify secret mount secret is soft-owned by SINGLE policy (the one that defines SecretMounts)
894+
// Only policy-high has SecretMounts, so it should be the only owner
895+
owners, err = getSoftOwnerPolicies(&secretMountSecret)
896+
assert.NoError(t, err)
897+
assert.Len(t, owners, 1, "secretMountSecret should be owned by 1 policy")
898+
// Verify only policy-high is the owner
899+
assert.Contains(t, owners, types.NamespacedName{Namespace: "ns", Name: "policy-high"}, "policy-high should be an owner of secretMountSecret")
900+
},
901+
},
902+
}
903+
904+
for _, tt := range tests {
905+
t.Run(tt.name, func(t *testing.T) {
906+
// Create client with all resources
907+
clientObjects := []client.Object{&esFixture, &esFileSettingsSecret, esPod, &sourceSecret}
908+
for i := range tt.policies {
909+
clientObjects = append(clientObjects, &tt.policies[i])
910+
}
911+
912+
fakeRecorder := record.NewFakeRecorder(100)
913+
reconciler := ReconcileStackConfigPolicy{
914+
Client: k8s.NewFakeClient(clientObjects...),
915+
esClientProvider: fakeClientProvider(esclient.FileSettings{Version: 1}, nil),
916+
recorder: fakeRecorder,
917+
licenseChecker: &license.MockLicenseChecker{EnterpriseEnabled: true},
918+
params: operator.Parameters{
919+
OperatorNamespace: "elastic-system",
920+
},
921+
dynamicWatches: watches.NewDynamicWatches(),
922+
}
923+
924+
// Reconcile the specified policy
925+
got, err := reconciler.Reconcile(context.Background(), reconcile.Request{
926+
NamespacedName: types.NamespacedName{
927+
Namespace: "ns",
928+
Name: tt.reconcilePolicy,
929+
},
930+
})
931+
932+
if (err != nil) != tt.wantErr {
933+
t.Errorf("Reconcile() error = %v, wantErr %v", err, tt.wantErr)
934+
return
935+
}
936+
if (got.RequeueAfter > 0) != tt.wantRequeueAfter {
937+
t.Errorf("Reconcile() got = %v, wantRequeueAfter %v", got, tt.wantRequeueAfter)
938+
}
939+
940+
// Verify policy status
941+
policy := reconciler.getPolicy(t, types.NamespacedName{Namespace: "ns", Name: tt.reconcilePolicy})
942+
assert.Equal(t, tt.wantResources, policy.Status.Resources)
943+
assert.Equal(t, tt.wantReady, policy.Status.Ready)
944+
assert.Equal(t, tt.wantPhase, policy.Status.Phase)
945+
946+
// Run custom validation if provided
947+
if tt.validateSettings != nil {
948+
tt.validateSettings(t, reconciler)
949+
}
950+
})
951+
}
952+
}
953+
679954
func Test_cleanStackTrace(t *testing.T) {
680955
stacktrace := "Error processing slm state change: java.lang.IllegalArgumentException: Error on validating SLM requests\n\tat [email protected]/org.elasticsearch.xpack.slm.action.ReservedSnapshotAction.prepare(ReservedSnapshotAction.java:66)\n\tat [email protected]/org.elasticsearch.xpack.slm.action.ReservedSnapshotAction.transform(ReservedSnapshotAction.java:77)\n\tat [email protected]/org.elasticsearch.reservedstate.service.ReservedClusterStateService.trialRun(ReservedClusterStateService.java:328)\n\tat [email protected]/org.elasticsearch.reservedstate.service.ReservedClusterStateService.process(ReservedClusterStateService.java:169)\n\tat [email protected]/org.elasticsearch.reservedstate.service.ReservedClusterStateService.process(ReservedClusterStateService.java:122)\n\tat [email protected]/org.elasticsearch.reservedstate.service.FileSettingsService.processFileSettings(FileSettingsService.java:389)\n\tat [email protected]/org.elasticsearch.reservedstate.service.FileSettingsService.lambda$startWatcher$3(FileSettingsService.java:312)\n\tat java.base/java.lang.Thread.run(Thread.java:833)\n\tSuppressed: java.lang.IllegalArgumentException: no such repository [badrepo]\n\t\tat [email protected]/org.elasticsearch.xpack.slm.SnapshotLifecycleService.validateRepositoryExists(SnapshotLifecycleService.java:244)\n\t\tat [email protected]/org.elasticsearch.xpack.slm.action.ReservedSnapshotAction.prepare(ReservedSnapshotAction.java:57)\n\t\t... 7 more\n"
681956
err := cleanStackTrace([]string{stacktrace})

0 commit comments

Comments
 (0)