Skip to content

Commit 39b856d

Browse files
Use optionalField for creation in ExplainSMPolicy serialization (#1507)
Co-authored-by: Tarun-kishore <[email protected]> Co-authored-by: bowenlan-amzn <[email protected]>
1 parent a8e15da commit 39b856d

File tree

3 files changed

+65
-6
lines changed

3 files changed

+65
-6
lines changed

src/main/kotlin/org/opensearch/indexmanagement/snapshotmanagement/model/ExplainSMPolicy.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package org.opensearch.indexmanagement.snapshotmanagement.model
77

8+
import org.opensearch.Version
89
import org.opensearch.core.common.io.stream.StreamInput
910
import org.opensearch.core.common.io.stream.StreamOutput
1011
import org.opensearch.core.common.io.stream.Writeable
@@ -36,10 +37,15 @@ data class ExplainSMPolicy(
3637
override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder {
3738
metadata?.let {
3839
builder
39-
.field(SMMetadata.CREATION_FIELD, it.creation)
4040
.optionalField(SMMetadata.DELETION_FIELD, it.deletion)
4141
.field(SMMetadata.POLICY_SEQ_NO_FIELD, it.policySeqNo)
4242
.field(SMMetadata.POLICY_PRIMARY_TERM_FIELD, it.policyPrimaryTerm)
43+
44+
if (Version.CURRENT.onOrAfter(Version.V_3_3_0)) {
45+
builder.optionalField(SMMetadata.CREATION_FIELD, it.creation)
46+
} else {
47+
builder.field(SMMetadata.CREATION_FIELD, it.creation)
48+
}
4349
}
4450
return builder.field(SMPolicy.ENABLED_FIELD, enabled)
4551
}

src/test/kotlin/org/opensearch/indexmanagement/bwc/SMBackwardsCompatibilityIT.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ class SMBackwardsCompatibilityIT : SnapshotManagementRestTestCase() {
8484
val traditionalPolicy = getSMPolicy(traditionalPolicyName)
8585
assertNotNull("Traditional policy creation should exist", traditionalPolicy.creation)
8686
assertNotNull("Traditional policy deletion should exist", traditionalPolicy.deletion)
87+
88+
// Verify explain response works in old version (covers old version serialization path)
89+
val explainResponse = explainSMPolicy(traditionalPolicyName)
90+
val explainMetadata = parseExplainResponse(explainResponse.entity.content)
91+
assertTrue("Explain should return results", explainMetadata.isNotEmpty())
8792
}
8893
ClusterType.MIXED -> {
8994
assertTrue(pluginNames.contains("opensearch-index-management"))
@@ -92,6 +97,11 @@ class SMBackwardsCompatibilityIT : SnapshotManagementRestTestCase() {
9297
val traditionalPolicy = getSMPolicy(traditionalPolicyName)
9398
assertNotNull("Traditional policy creation should exist", traditionalPolicy.creation)
9499
assertNotNull("Traditional policy deletion should exist", traditionalPolicy.deletion)
100+
101+
// Verify explain response works during rolling upgrade (mixed old/new version serialization)
102+
val explainResponse = explainSMPolicy(traditionalPolicyName)
103+
val explainMetadata = parseExplainResponse(explainResponse.entity.content)
104+
assertTrue("Explain should return results", explainMetadata.isNotEmpty())
95105
}
96106
ClusterType.UPGRADED -> {
97107
assertTrue(pluginNames.contains("opensearch-index-management"))
@@ -101,6 +111,11 @@ class SMBackwardsCompatibilityIT : SnapshotManagementRestTestCase() {
101111
assertNotNull("Traditional policy creation should exist", traditionalPolicy.creation)
102112
assertNotNull("Traditional policy deletion should exist", traditionalPolicy.deletion)
103113

114+
// Verify explain response for traditional policy on upgraded cluster (new version serialization)
115+
val traditionalExplainResponse = explainSMPolicy(traditionalPolicyName)
116+
val traditionalExplainMetadata = parseExplainResponse(traditionalExplainResponse.entity.content)
117+
assertTrue("Explain should return results for traditional policy", traditionalExplainMetadata.isNotEmpty())
118+
104119
// Now test new features on upgraded cluster
105120

106121
// Create deletion-only policy (3.2.0+ feature)
@@ -109,12 +124,22 @@ class SMBackwardsCompatibilityIT : SnapshotManagementRestTestCase() {
109124
assertNull("Deletion-only policy creation should be null", deletionOnlyPolicy.creation)
110125
assertNotNull("Deletion-only policy deletion should exist", deletionOnlyPolicy.deletion)
111126

127+
// Verify explain response for deletion-only policy (tests null creation serialization with optionalField)
128+
val deletionOnlyExplainResponse = explainSMPolicy(deletionOnlyPolicyName)
129+
val deletionOnlyExplainMetadata = parseExplainResponse(deletionOnlyExplainResponse.entity.content)
130+
assertTrue("Explain should return results for deletion-only policy", deletionOnlyExplainMetadata.isNotEmpty())
131+
112132
// Create policy with snapshot pattern (3.2.0+ feature)
113133
createPatternPolicy(patternPolicyName)
114134
val patternPolicy = getSMPolicy(patternPolicyName)
115135
assertNotNull("Pattern policy creation should exist", patternPolicy.creation)
116136
assertNotNull("Pattern policy deletion should exist", patternPolicy.deletion)
117137
assertEquals("Pattern policy should have snapshot pattern", "external-backup-*", patternPolicy.deletion?.snapshotPattern)
138+
139+
// Verify explain response for pattern policy
140+
val patternExplainResponse = explainSMPolicy(patternPolicyName)
141+
val patternExplainMetadata = parseExplainResponse(patternExplainResponse.entity.content)
142+
assertTrue("Explain should return results for pattern policy", patternExplainMetadata.isNotEmpty())
118143
}
119144
}
120145
break

src/test/kotlin/org/opensearch/indexmanagement/snapshotmanagement/action/ResponseTests.kt

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
package org.opensearch.indexmanagement.snapshotmanagement.action
77

88
import org.opensearch.common.io.stream.BytesStreamOutput
9+
import org.opensearch.common.xcontent.XContentFactory
910
import org.opensearch.core.common.io.stream.StreamInput
1011
import org.opensearch.core.rest.RestStatus
12+
import org.opensearch.core.xcontent.ToXContent
1113
import org.opensearch.indexmanagement.indexstatemanagement.util.XCONTENT_WITHOUT_TYPE_AND_USER
14+
import org.opensearch.indexmanagement.opensearchapi.toMap
1215
import org.opensearch.indexmanagement.snapshotmanagement.api.transport.explain.ExplainSMPolicyResponse
1316
import org.opensearch.indexmanagement.snapshotmanagement.api.transport.get.GetSMPoliciesResponse
1417
import org.opensearch.indexmanagement.snapshotmanagement.api.transport.get.GetSMPolicyResponse
@@ -17,8 +20,8 @@ import org.opensearch.indexmanagement.snapshotmanagement.model.ExplainSMPolicy
1720
import org.opensearch.indexmanagement.snapshotmanagement.randomSMMetadata
1821
import org.opensearch.indexmanagement.snapshotmanagement.randomSMPolicy
1922
import org.opensearch.indexmanagement.snapshotmanagement.smDocIdToPolicyName
20-
import org.opensearch.indexmanagement.snapshotmanagement.toMap
2123
import org.opensearch.test.OpenSearchTestCase
24+
import org.opensearch.indexmanagement.snapshotmanagement.toMap as toMapHelper
2225

2326
class ResponseTests : OpenSearchTestCase() {
2427
fun `test index sm policy response`() {
@@ -38,8 +41,8 @@ class ResponseTests : OpenSearchTestCase() {
3841
fun `test index sm policy toXContent`() {
3942
val smPolicy = randomSMPolicy()
4043
val res = IndexSMPolicyResponse("someid", 1L, 2L, 3L, smPolicy, RestStatus.OK)
41-
val resMap = res.toMap()
42-
assertEquals(resMap["sm_policy"], smPolicy.toMap(XCONTENT_WITHOUT_TYPE_AND_USER))
44+
val resMap = res.toMapHelper()
45+
assertEquals(resMap["sm_policy"], smPolicy.toMapHelper(XCONTENT_WITHOUT_TYPE_AND_USER))
4346
}
4447

4548
fun `test get sm policy response`() {
@@ -81,7 +84,32 @@ class ResponseTests : OpenSearchTestCase() {
8184
fun `test get sm policy toXContent`() {
8285
val smPolicy = randomSMPolicy()
8386
val res = GetSMPolicyResponse("someid", 1L, 2L, 3L, smPolicy)
84-
val resMap = res.toMap()
85-
assertEquals(resMap["sm_policy"], smPolicy.toMap(XCONTENT_WITHOUT_TYPE_AND_USER))
87+
val resMap = res.toMapHelper()
88+
assertEquals(resMap["sm_policy"], smPolicy.toMapHelper(XCONTENT_WITHOUT_TYPE_AND_USER))
89+
}
90+
91+
fun `test explain sm policy toXContent with null creation`() {
92+
// Test that ExplainSMPolicy.toXContent() handles null creation correctly (for V_3_3_0+)
93+
val smMetadata = randomSMMetadata()
94+
val smMetadataWithNullCreation = smMetadata.copy(creation = null)
95+
val explainSMPolicy = ExplainSMPolicy(smMetadataWithNullCreation, true)
96+
97+
// Properly convert ToXContentFragment to map by wrapping in an object
98+
val builder = XContentFactory.jsonBuilder().startObject()
99+
explainSMPolicy.toXContent(builder, ToXContent.EMPTY_PARAMS)
100+
builder.endObject()
101+
val explainMap = builder.toMap()
102+
103+
// Verify that creation field is NOT present when null (optionalField behavior for V_3_3_0+)
104+
assertFalse("Creation field should not be present when null", explainMap.containsKey("creation"))
105+
106+
// Verify deletion field IS present
107+
assertTrue("Deletion field should be present", explainMap.containsKey("deletion"))
108+
109+
// Verify other mandatory fields are present
110+
assertTrue("Policy seq_no should be present", explainMap.containsKey("policy_seq_no"))
111+
assertTrue("Policy primary_term should be present", explainMap.containsKey("policy_primary_term"))
112+
assertTrue("Enabled field should be present", explainMap.containsKey("enabled"))
113+
assertEquals(true, explainMap["enabled"])
86114
}
87115
}

0 commit comments

Comments
 (0)