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  }