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 }