github.com/verrazzano/verrazzano-monitoring-operator@v0.0.30/pkg/opensearch/ism_test.go (about) 1 // Copyright (C) 2022, Oracle and/or its affiliates. 2 // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. 3 4 package opensearch 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "io" 12 "net/http" 13 "strings" 14 "testing" 15 16 "github.com/stretchr/testify/assert" 17 vmcontrollerv1 "github.com/verrazzano/verrazzano-monitoring-operator/pkg/apis/vmcontroller/v1" 18 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 ) 20 21 const ( 22 testPolicyNotFound = `{"error":{"root_cause":[{"type":"status_exception","reason":"Policy not found"}],"type":"status_exception","reason":"Policy not found"},"status":404}` 23 testSystemPolicy = ` 24 { 25 "_id" : "verrazzano-system", 26 "_seq_no" : 0, 27 "_primary_term" : 1, 28 "policy" : { 29 "policy_id" : "verrazzano-system", 30 "description" : "__vmi-managed__", 31 "last_updated_time" : 1647551644420, 32 "schema_version" : 12, 33 "error_notification" : null, 34 "default_state" : "ingest", 35 "states" : [ 36 { 37 "name" : "ingest", 38 "actions" : [ 39 { 40 "rollover" : { 41 "min_index_age" : "1d" 42 } 43 } 44 ], 45 "transitions" : [ 46 { 47 "state_name" : "delete", 48 "conditions" : { 49 "min_index_age" : "7d" 50 } 51 } 52 ] 53 }, 54 { 55 "name" : "delete", 56 "actions" : [ 57 { 58 "delete" : { } 59 } 60 ], 61 "transitions" : [ ] 62 } 63 ], 64 "ism_template" : [ 65 { 66 "index_patterns" : [ 67 "verrazzano-system" 68 ], 69 "priority" : 1, 70 "last_updated_time" : 1647551644419 71 } 72 ] 73 } 74 } 75 ` 76 ) 77 78 var testPolicyList = fmt.Sprintf(`{ 79 "policies": [ 80 %s 81 ] 82 }`, testSystemPolicy) 83 84 func createTestPolicy(age, rolloverAge, indexPattern, minSize string, minDocCount int) *vmcontrollerv1.IndexManagementPolicy { 85 return &vmcontrollerv1.IndexManagementPolicy{ 86 PolicyName: "verrazzano-system", 87 IndexPattern: indexPattern, 88 MinIndexAge: &age, 89 Rollover: vmcontrollerv1.RolloverPolicy{ 90 MinIndexAge: &rolloverAge, 91 MinSize: &minSize, 92 MinDocCount: &minDocCount, 93 }, 94 } 95 } 96 97 func createISMVMI(age string, enabled bool) *vmcontrollerv1.VerrazzanoMonitoringInstance { 98 v := &vmcontrollerv1.VerrazzanoMonitoringInstance{ 99 ObjectMeta: metav1.ObjectMeta{ 100 Name: "test", 101 }, 102 Spec: vmcontrollerv1.VerrazzanoMonitoringInstanceSpec{ 103 Elasticsearch: vmcontrollerv1.Elasticsearch{ 104 Enabled: enabled, 105 Policies: []vmcontrollerv1.IndexManagementPolicy{ 106 *createTestPolicy(age, age, "*", "1gb", 1), 107 }, 108 }, 109 }, 110 } 111 112 return v 113 } 114 115 // TestConfigureIndexManagementPluginISMDisabled Tests that ISM configuration when disabled 116 // GIVEN a default VMI instance 117 // WHEN I call Configure 118 // THEN the ISM configuration does nothing because it is disabled 119 func TestConfigureIndexManagementPluginISMDisabled(t *testing.T) { 120 o := NewOSClient(statefulSetLister) 121 assert.NoError(t, <-o.ConfigureISM(&vmcontrollerv1.VerrazzanoMonitoringInstance{})) 122 } 123 124 // TestConfigureIndexManagementPluginHappyPath Tests configuration of the ISM plugin 125 // GIVEN a VMI instance with an ISM Policy 126 // WHEN I call Configure 127 // THEN the ISM configuration is created in OpenSearch 128 func TestConfigureIndexManagementPluginHappyPath(t *testing.T) { 129 o := NewOSClient(statefulSetLister) 130 o.DoHTTP = func(request *http.Request) (*http.Response, error) { 131 switch request.Method { 132 case "GET": 133 if strings.Contains(request.URL.Path, "verrazzano-system") { 134 return &http.Response{ 135 StatusCode: http.StatusNotFound, 136 Body: io.NopCloser(strings.NewReader(testPolicyNotFound)), 137 }, nil 138 } 139 return &http.Response{ 140 StatusCode: http.StatusOK, 141 Body: io.NopCloser(strings.NewReader(testPolicyList)), 142 }, nil 143 144 case "PUT": 145 return &http.Response{ 146 StatusCode: http.StatusCreated, 147 Body: io.NopCloser(strings.NewReader(testSystemPolicy)), 148 }, nil 149 default: 150 return &http.Response{ 151 StatusCode: http.StatusOK, 152 Body: io.NopCloser(strings.NewReader("")), 153 }, nil 154 } 155 } 156 vmi := createISMVMI("1d", true) 157 ch := o.ConfigureISM(vmi) 158 assert.NoError(t, <-ch) 159 } 160 161 // TestGetPolicyByName Tests retrieving ISM policies by name 162 // GIVEN an OpenSearch instance 163 // WHEN I call getPolicyByName 164 // THEN the specified policy should be returned, if it exists 165 func TestGetPolicyByName(t *testing.T) { 166 var tests = []struct { 167 name string 168 policyName string 169 status int 170 }{ 171 { 172 "policy is fetched when it exists", 173 "verrazzano-system", 174 200, 175 }, 176 } 177 178 o := NewOSClient(statefulSetLister) 179 o.DoHTTP = func(request *http.Request) (*http.Response, error) { 180 if strings.Contains(request.URL.Path, "verrazzano-system") { 181 return &http.Response{ 182 StatusCode: http.StatusOK, 183 Body: io.NopCloser(strings.NewReader(testSystemPolicy)), 184 }, nil 185 } 186 return &http.Response{ 187 StatusCode: http.StatusNotFound, 188 Body: io.NopCloser(strings.NewReader(testPolicyNotFound)), 189 }, nil 190 } 191 192 for _, tt := range tests { 193 t.Run(tt.name, func(t *testing.T) { 194 policy, err := o.getPolicyByName("http://localhost:9200/" + tt.policyName) 195 assert.NoError(t, err) 196 assert.Equal(t, tt.status, *policy.Status) 197 if tt.status == http.StatusOK { 198 assert.Equal(t, 0, *policy.SequenceNumber) 199 assert.Equal(t, 1, *policy.PrimaryTerm) 200 } 201 }) 202 } 203 } 204 205 // TestPutUpdatedPolicy_PolicyExists Tests updating a policy in place 206 // GIVEN a policy that already exists in the server 207 // WHEN I call putUpdatedPolicy 208 // THEN the ISM policy should be updated in place IFF there are changes to the policy 209 func TestPutUpdatedPolicy_PolicyExists(t *testing.T) { 210 httpFunc := func(request *http.Request) (*http.Response, error) { 211 return &http.Response{ 212 StatusCode: http.StatusOK, 213 Body: io.NopCloser(strings.NewReader(testSystemPolicy)), 214 }, nil 215 } 216 217 var tests = []struct { 218 name string 219 age string 220 httpFunc func(request *http.Request) (*http.Response, error) 221 policyUpdated bool 222 hasError bool 223 }{ 224 { 225 "Policy should be updated when it already exists and the index lifecycle has changed", 226 "1d", 227 httpFunc, 228 true, 229 false, 230 }, 231 { 232 "Policy should not be updated when the index lifecycle has not changed", 233 "7d", 234 httpFunc, 235 false, 236 false, 237 }, 238 { 239 "Policy should not be updated when the update call fails", 240 "1d", 241 func(request *http.Request) (*http.Response, error) { 242 return nil, errors.New("boom") 243 }, 244 false, 245 true, 246 }, 247 } 248 249 for _, tt := range tests { 250 existingPolicy := &ISMPolicy{} 251 err := json.NewDecoder(strings.NewReader(testSystemPolicy)).Decode(existingPolicy) 252 assert.NoError(t, err) 253 status := http.StatusOK 254 existingPolicy.Status = &status 255 o := NewOSClient(statefulSetLister) 256 o.DoHTTP = tt.httpFunc 257 t.Run(tt.name, func(t *testing.T) { 258 newPolicy := &vmcontrollerv1.IndexManagementPolicy{ 259 PolicyName: "verrazzano-system", 260 IndexPattern: "verrazzano-system", 261 MinIndexAge: &tt.age, 262 } 263 updatedPolicy, err := o.putUpdatedPolicy("http://localhost:9200", newPolicy, existingPolicy) 264 if tt.hasError { 265 assert.Error(t, err) 266 } else { 267 assert.NoError(t, err) 268 } 269 if tt.policyUpdated { 270 assert.NotNil(t, updatedPolicy) 271 } else { 272 assert.Nil(t, updatedPolicy) 273 } 274 }) 275 } 276 } 277 278 // TestPolicyNeedsUpdate Tests that the ISM policy will only be updated when it changes 279 // GIVEN a new ISM policy and the existing ISM policy 280 // WHEN I call policyNeedsUpdate 281 // THEN true is only returned if the new ISM policy has changed 282 func TestPolicyNeedsUpdate(t *testing.T) { 283 basePolicy := createTestPolicy("7d", "1d", "verrazzano-system", "10gb", 1000) 284 policyExtraState := toISMPolicy(basePolicy) 285 policyExtraState.Policy.States = append(policyExtraState.Policy.States, PolicyState{ 286 Name: "warm", 287 Actions: []map[string]interface{}{}, 288 Transitions: []PolicyTransition{}, 289 }) 290 var tests = []struct { 291 name string 292 p1 *vmcontrollerv1.IndexManagementPolicy 293 p2 *ISMPolicy 294 needsUpdate bool 295 }{ 296 { 297 "no update when equal", 298 basePolicy, 299 toISMPolicy(basePolicy), 300 false, 301 }, 302 { 303 "needs update when age changed", 304 basePolicy, 305 toISMPolicy(createTestPolicy("14d", "1d", "verrazzano-system", "10gb", 1000)), 306 true, 307 }, 308 { 309 "needs update when rollover age changed", 310 basePolicy, 311 toISMPolicy(createTestPolicy("7d", "2d", "verrazzano-system", "10gb", 1000)), 312 true, 313 }, 314 { 315 "needs update when index pattern changed", 316 basePolicy, 317 toISMPolicy(createTestPolicy("7d", "1d", "verrazzano-system-*", "10gb", 1000)), 318 true, 319 }, 320 { 321 "needs update when min size changed", 322 basePolicy, 323 toISMPolicy(createTestPolicy("7d", "1d", "verrazzano-system", "20gb", 1000)), 324 true, 325 }, 326 { 327 "needs update when min doc count changed", 328 basePolicy, 329 toISMPolicy(createTestPolicy("7d", "1d", "verrazzano-system", "10gb", 5000)), 330 true, 331 }, 332 { 333 "needs update when states changed", 334 basePolicy, 335 policyExtraState, 336 true, 337 }, 338 } 339 340 for _, tt := range tests { 341 t.Run(tt.name, func(t *testing.T) { 342 needsUpdate := policyNeedsUpdate(tt.p1, tt.p2) 343 assert.Equal(t, tt.needsUpdate, needsUpdate) 344 }) 345 } 346 } 347 348 // TestCleanupPolicies Tests cleaning up policies no longer managed by the VMI 349 // GIVEN a list of expected policies 350 // WHEN I call cleanupPolicies 351 // THEN then the existing policies should be queried and any non-matching members removed 352 func TestCleanupPolicies(t *testing.T) { 353 o := NewOSClient(statefulSetLister) 354 355 id1 := "myapp" 356 id2 := "anotherapp" 357 358 p1 := createTestPolicy("1d", "1d", id1, "1d", 1) 359 p1.PolicyName = id1 360 p2 := createTestPolicy("1d", "1d", id2, "1d", 1) 361 p2.PolicyName = id2 362 expectedPolicies := []vmcontrollerv1.IndexManagementPolicy{ 363 *p1, 364 } 365 366 p1ISM := toISMPolicy(p1) 367 p1ISM.ID = &id1 368 p2ISM := toISMPolicy(p2) 369 p2ISM.ID = &id2 370 existingPolicies := &PolicyList{ 371 Policies: []ISMPolicy{ 372 *p1ISM, 373 *p2ISM, 374 }, 375 } 376 existingPolicyJSON, err := json.Marshal(existingPolicies) 377 assert.NoError(t, err) 378 379 var getCalls, deleteCalls int 380 o.DoHTTP = func(request *http.Request) (*http.Response, error) { 381 switch request.Method { 382 case "GET": 383 getCalls++ 384 return &http.Response{ 385 StatusCode: http.StatusOK, 386 Body: io.NopCloser(bytes.NewReader(existingPolicyJSON)), 387 }, nil 388 default: 389 deleteCalls++ 390 return &http.Response{ 391 StatusCode: http.StatusOK, 392 Body: io.NopCloser(strings.NewReader("")), 393 }, nil 394 } 395 } 396 397 err = o.cleanupPolicies("http://localhost:9200", expectedPolicies) 398 assert.NoError(t, err) 399 assert.Equal(t, 1, getCalls) 400 assert.Equal(t, 1, deleteCalls) 401 } 402 403 // TestIsEligibleForDeletion Tests whether a policy is eligible for deletion or not 404 // GIVEN a policy and the expected policy set 405 // WHEN I call isEligibleForDeletion 406 // THEN only managed policies that are not expected are eligible for deletion 407 func TestIsEligibleForDeletion(t *testing.T) { 408 id1 := "id1" 409 p1 := ISMPolicy{ID: &id1, Policy: InlinePolicy{Description: vmiManagedPolicy}} 410 id2 := "id2" 411 p2 := ISMPolicy{ID: &id2} 412 var tests = []struct { 413 name string 414 p ISMPolicy 415 e map[string]bool 416 eligible bool 417 }{ 418 { 419 "eligible when policy is managed and policy isn't expected", 420 p1, 421 map[string]bool{}, 422 true, 423 }, 424 { 425 "ineligible when policy is not managed", 426 p2, 427 map[string]bool{ 428 id1: true, 429 }, 430 false, 431 }, 432 { 433 "ineligible when policy is managed and policy is expected", 434 p1, 435 map[string]bool{ 436 id1: true, 437 }, 438 false, 439 }, 440 } 441 442 for _, tt := range tests { 443 t.Run(tt.name, func(t *testing.T) { 444 res := isEligibleForDeletion(tt.p, tt.e) 445 assert.Equal(t, tt.eligible, res) 446 }) 447 } 448 } 449 450 // TestConfigureIndexManagementPluginOpenSearchNotReady Tests that ConfigureISM does not return error when OpenSearch is not ready 451 // GIVEN a default VMI instance 452 // WHEN I call ConfigureISM 453 // THEN the ISM configuration does nothing because OpenSearch is not ready 454 func TestConfigureIndexManagementPluginOpenSearchNotReady(t *testing.T) { 455 o := NewOSClient(statefulSetLister) 456 assert.NoError(t, <-o.ConfigureISM(&vmcontrollerv1.VerrazzanoMonitoringInstance{Spec: vmcontrollerv1.VerrazzanoMonitoringInstanceSpec{ 457 Elasticsearch: vmcontrollerv1.Elasticsearch{ 458 Enabled: true, 459 }, 460 }})) 461 }