github.com/verrazzano/verrazzano-monitoring-operator@v0.0.30/pkg/opensearch/ism.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  	"fmt"
    10  	vmcontrollerv1 "github.com/verrazzano/verrazzano-monitoring-operator/pkg/apis/vmcontroller/v1"
    11  	"net/http"
    12  	"reflect"
    13  	"strings"
    14  )
    15  
    16  type (
    17  	PolicyList struct {
    18  		Policies      []ISMPolicy `json:"policies"`
    19  		TotalPolicies int         `json:"total_policies"`
    20  	}
    21  
    22  	ISMPolicy struct {
    23  		ID             *string      `json:"_id,omitempty"`
    24  		PrimaryTerm    *int         `json:"_primary_term,omitempty"`
    25  		SequenceNumber *int         `json:"_seq_no,omitempty"`
    26  		Status         *int         `json:"status,omitempty"`
    27  		Policy         InlinePolicy `json:"policy"`
    28  	}
    29  
    30  	InlinePolicy struct {
    31  		DefaultState string        `json:"default_state"`
    32  		Description  string        `json:"description"`
    33  		States       []PolicyState `json:"states"`
    34  		ISMTemplate  []ISMTemplate `json:"ism_template"`
    35  	}
    36  
    37  	ISMTemplate struct {
    38  		IndexPatterns []string `json:"index_patterns"`
    39  		Priority      int      `json:"priority"`
    40  	}
    41  
    42  	PolicyState struct {
    43  		Name        string                   `json:"name"`
    44  		Actions     []map[string]interface{} `json:"actions"`
    45  		Transitions []PolicyTransition       `json:"transitions"`
    46  	}
    47  
    48  	PolicyTransition struct {
    49  		StateName  string           `json:"state_name"`
    50  		Conditions PolicyConditions `json:"conditions"`
    51  	}
    52  
    53  	PolicyConditions struct {
    54  		MinIndexAge string `json:"min_index_age"`
    55  	}
    56  )
    57  
    58  const (
    59  	minIndexAgeKey = "min_index_age"
    60  
    61  	// Default amount of time before a policy-managed index is deleted
    62  	defaultMinIndexAge = "7d"
    63  	// Default amount of time before a policy-managed index is rolled over
    64  	defaultRolloverIndexAge = "1d"
    65  	// Descriptor to identify policies as being managed by the VMI
    66  	vmiManagedPolicy = "__vmi-managed__"
    67  )
    68  
    69  // createISMPolicy creates an ISM policy if it does not exist, else the policy will be updated.
    70  // If the policy already exsts and its spec matches the VMO policy spec, no update will be issued
    71  func (o *OSClient) createISMPolicy(opensearchEndpoint string, policy vmcontrollerv1.IndexManagementPolicy) error {
    72  	policyURL := fmt.Sprintf("%s/_plugins/_ism/policies/%s", opensearchEndpoint, policy.PolicyName)
    73  	existingPolicy, err := o.getPolicyByName(policyURL)
    74  	if err != nil {
    75  		return err
    76  	}
    77  	updatedPolicy, err := o.putUpdatedPolicy(opensearchEndpoint, &policy, existingPolicy)
    78  	if err != nil {
    79  		return err
    80  	}
    81  	return o.addPolicyToExistingIndices(opensearchEndpoint, &policy, updatedPolicy)
    82  }
    83  
    84  func (o *OSClient) getPolicyByName(policyURL string) (*ISMPolicy, error) {
    85  	req, err := http.NewRequest("GET", policyURL, nil)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  	resp, err := o.DoHTTP(req)
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  	defer resp.Body.Close()
    94  	existingPolicy := &ISMPolicy{}
    95  	if err := json.NewDecoder(resp.Body).Decode(existingPolicy); err != nil {
    96  		return nil, err
    97  	}
    98  	existingPolicy.Status = &resp.StatusCode
    99  	return existingPolicy, nil
   100  }
   101  
   102  // putUpdatedPolicy updates a policy in place, if the update is required. If no update was necessary, the returned
   103  // ISMPolicy will be nil.
   104  func (o *OSClient) putUpdatedPolicy(opensearchEndpoint string, policy *vmcontrollerv1.IndexManagementPolicy, existingPolicy *ISMPolicy) (*ISMPolicy, error) {
   105  	if !policyNeedsUpdate(policy, existingPolicy) {
   106  		return nil, nil
   107  	}
   108  
   109  	payload, err := serializeIndexManagementPolicy(policy)
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  
   114  	var url string
   115  	var statusCode int
   116  	existingPolicyStatus := *existingPolicy.Status
   117  	switch existingPolicyStatus {
   118  	case http.StatusOK: // The policy exists and must be updated in place if it has changed
   119  		url = fmt.Sprintf("%s/_plugins/_ism/policies/%s?if_seq_no=%d&if_primary_term=%d",
   120  			opensearchEndpoint,
   121  			policy.PolicyName,
   122  			*existingPolicy.SequenceNumber,
   123  			*existingPolicy.PrimaryTerm,
   124  		)
   125  		statusCode = http.StatusOK
   126  	case http.StatusNotFound: // The policy doesn't exist and must be updated
   127  		url = fmt.Sprintf("%s/_plugins/_ism/policies/%s", opensearchEndpoint, policy.PolicyName)
   128  		statusCode = http.StatusCreated
   129  	default:
   130  		return nil, fmt.Errorf("invalid status when fetching ISM Policy %s: %d", policy.PolicyName, existingPolicy.Status)
   131  	}
   132  	req, err := http.NewRequest("PUT", url, bytes.NewReader(payload))
   133  	if err != nil {
   134  		return nil, err
   135  	}
   136  	req.Header.Add(contentTypeHeader, applicationJSON)
   137  	resp, err := o.DoHTTP(req)
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  	if resp.StatusCode != statusCode {
   142  		return nil, fmt.Errorf("got status code %d when updating policy %s, expected %d", resp.StatusCode, policy.PolicyName, statusCode)
   143  	}
   144  	updatedISMPolicy := &ISMPolicy{}
   145  	err = json.NewDecoder(resp.Body).Decode(updatedISMPolicy)
   146  	if err != nil {
   147  		return nil, err
   148  	}
   149  
   150  	return updatedISMPolicy, nil
   151  }
   152  
   153  // addPolicyToExistingIndices updates any pre-existing cluster indices to be managed by the ISMPolicy
   154  func (o *OSClient) addPolicyToExistingIndices(opensearchEndpoint string, policy *vmcontrollerv1.IndexManagementPolicy, updatedPolicy *ISMPolicy) error {
   155  	// If no policy was updated, then there is nothing to do
   156  	if updatedPolicy == nil {
   157  		return nil
   158  	}
   159  	url := fmt.Sprintf("%s/_plugins/_ism/add/%s", opensearchEndpoint, policy.IndexPattern)
   160  	body := strings.NewReader(fmt.Sprintf(`{"policy_id": "%s"}`, *updatedPolicy.ID))
   161  	req, err := http.NewRequest("POST", url, body)
   162  	if err != nil {
   163  		return err
   164  	}
   165  	req.Header.Add(contentTypeHeader, applicationJSON)
   166  	resp, err := o.DoHTTP(req)
   167  	if err != nil {
   168  		return err
   169  	}
   170  	defer resp.Body.Close()
   171  	if resp.StatusCode != http.StatusOK {
   172  		return fmt.Errorf("got status code %d when updating indicies for policy %s", resp.StatusCode, policy.PolicyName)
   173  	}
   174  	return nil
   175  }
   176  
   177  func (o *OSClient) cleanupPolicies(opensearchEndpoint string, policies []vmcontrollerv1.IndexManagementPolicy) error {
   178  	policyList, err := o.getAllPolicies(opensearchEndpoint)
   179  	if err != nil {
   180  		return err
   181  	}
   182  
   183  	expectedPolicyMap := map[string]bool{}
   184  	for _, policy := range policies {
   185  		expectedPolicyMap[policy.PolicyName] = true
   186  	}
   187  
   188  	// A policy is eligible for deletion if it is marked as VMI managed, but the VMI no longer
   189  	// has a policy entry for it
   190  	for _, policy := range policyList.Policies {
   191  		if isEligibleForDeletion(policy, expectedPolicyMap) {
   192  			if err := o.deletePolicy(opensearchEndpoint, *policy.ID); err != nil {
   193  				return err
   194  			}
   195  		}
   196  	}
   197  	return nil
   198  }
   199  
   200  func (o *OSClient) getAllPolicies(opensearchEndpoint string) (*PolicyList, error) {
   201  	url := fmt.Sprintf("%s/_plugins/_ism/policies", opensearchEndpoint)
   202  	req, err := http.NewRequest("GET", url, nil)
   203  	if err != nil {
   204  		return nil, err
   205  	}
   206  	resp, err := o.DoHTTP(req)
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  	defer resp.Body.Close()
   211  	if resp.StatusCode != http.StatusOK {
   212  		return nil, fmt.Errorf("got status code %d when querying policies for cleanup", resp.StatusCode)
   213  	}
   214  	policies := &PolicyList{}
   215  	if err := json.NewDecoder(resp.Body).Decode(policies); err != nil {
   216  		return nil, err
   217  	}
   218  	return policies, nil
   219  }
   220  
   221  func (o *OSClient) deletePolicy(opensearchEndpoint, policyName string) error {
   222  	url := fmt.Sprintf("%s/_plugins/_ism/policies/%s", opensearchEndpoint, policyName)
   223  	req, err := http.NewRequest("DELETE", url, nil)
   224  	if err != nil {
   225  		return err
   226  	}
   227  	resp, err := o.DoHTTP(req)
   228  	if err != nil {
   229  		return err
   230  	}
   231  	defer resp.Body.Close()
   232  	if resp.StatusCode != http.StatusOK {
   233  		return fmt.Errorf("got status code %d when deleting policy %s", resp.StatusCode, policyName)
   234  	}
   235  	return nil
   236  }
   237  
   238  func isEligibleForDeletion(policy ISMPolicy, expectedPolicyMap map[string]bool) bool {
   239  	return policy.Policy.Description == vmiManagedPolicy &&
   240  		!expectedPolicyMap[*policy.ID]
   241  }
   242  
   243  // policyNeedsUpdate returns true if the policy document has changed
   244  func policyNeedsUpdate(policy *vmcontrollerv1.IndexManagementPolicy, existingPolicy *ISMPolicy) bool {
   245  	newPolicyDocument := toISMPolicy(policy).Policy
   246  	oldPolicyDocument := existingPolicy.Policy
   247  
   248  	return newPolicyDocument.DefaultState != oldPolicyDocument.DefaultState ||
   249  		!reflect.DeepEqual(newPolicyDocument.States, oldPolicyDocument.States) ||
   250  		!reflect.DeepEqual(newPolicyDocument.ISMTemplate, oldPolicyDocument.ISMTemplate)
   251  }
   252  
   253  func createRolloverAction(rollover *vmcontrollerv1.RolloverPolicy) map[string]interface{} {
   254  	rolloverAction := map[string]interface{}{}
   255  	if rollover.MinDocCount != nil {
   256  		rolloverAction["min_doc_count"] = *rollover.MinDocCount
   257  	}
   258  	if rollover.MinSize != nil {
   259  		rolloverAction["min_size"] = *rollover.MinSize
   260  	}
   261  	var rolloverMinIndexAge = defaultRolloverIndexAge
   262  	if rollover.MinIndexAge != nil {
   263  		rolloverMinIndexAge = *rollover.MinIndexAge
   264  	}
   265  	rolloverAction[minIndexAgeKey] = rolloverMinIndexAge
   266  	return rolloverAction
   267  }
   268  
   269  func serializeIndexManagementPolicy(policy *vmcontrollerv1.IndexManagementPolicy) ([]byte, error) {
   270  	return json.Marshal(toISMPolicy(policy))
   271  }
   272  
   273  func toISMPolicy(policy *vmcontrollerv1.IndexManagementPolicy) *ISMPolicy {
   274  	rolloverAction := map[string]interface{}{
   275  		"rollover": createRolloverAction(&policy.Rollover),
   276  	}
   277  	var minIndexAge = defaultMinIndexAge
   278  	if policy.MinIndexAge != nil {
   279  		minIndexAge = *policy.MinIndexAge
   280  	}
   281  
   282  	return &ISMPolicy{
   283  		Policy: InlinePolicy{
   284  			DefaultState: "ingest",
   285  			Description:  vmiManagedPolicy,
   286  			ISMTemplate: []ISMTemplate{
   287  				{
   288  					Priority: 1,
   289  					IndexPatterns: []string{
   290  						policy.IndexPattern,
   291  					},
   292  				},
   293  			},
   294  			States: []PolicyState{
   295  				{
   296  					Name: "ingest",
   297  					Actions: []map[string]interface{}{
   298  						rolloverAction,
   299  					},
   300  					Transitions: []PolicyTransition{
   301  						{
   302  							StateName: "delete",
   303  							Conditions: PolicyConditions{
   304  								MinIndexAge: minIndexAge,
   305  							},
   306  						},
   307  					},
   308  				},
   309  				{
   310  					Name: "delete",
   311  					Actions: []map[string]interface{}{
   312  						{
   313  							"delete": map[string]interface{}{},
   314  						},
   315  					},
   316  					Transitions: []PolicyTransition{},
   317  				},
   318  			},
   319  		},
   320  	}
   321  }