diff --git a/cmd/es-index-cleaner/app/flags.go b/cmd/es-index-cleaner/app/flags.go new file mode 100644 index 00000000000..7dfef44c240 --- /dev/null +++ b/cmd/es-index-cleaner/app/flags.go @@ -0,0 +1,69 @@ +// Copyright (c) 2021 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package app + +import ( + "flag" + + "github.com/spf13/viper" +) + +const ( + indexPrefix = "index-prefix" + archive = "archive" + rollover = "rollover" + timeout = "timeout" + indexDateSeparator = "index-date-separator" + username = "es.username" + password = "es.password" +) + +// Config holds configuration for index cleaner binary. +type Config struct { + IndexPrefix string + Archive bool + Rollover bool + MasterNodeTimeoutSeconds int + IndexDateSeparator string + Username string + Password string + TLSEnabled bool +} + +// AddFlags adds flags for TLS to the FlagSet. +func (c *Config) AddFlags(flags *flag.FlagSet) { + flags.String(indexPrefix, "", "Index prefix") + flags.Bool(archive, false, "Whether to remove archive indices") + flags.Bool(rollover, false, "Whether to remove indices created by rollover") + flags.Int(timeout, 120, "Number of seconds to wait for master node response") + flags.String(indexDateSeparator, "-", "Index date separator") + flags.String(username, "", "The username required by storage") + flags.String(password, "", "The password required by storage") +} + +// InitFromViper initializes config from viper.Viper. +func (c *Config) InitFromViper(v *viper.Viper) { + c.IndexPrefix = v.GetString(indexPrefix) + if c.IndexPrefix != "" { + c.IndexPrefix += "-" + } + + c.Archive = v.GetBool(archive) + c.Rollover = v.GetBool(rollover) + c.MasterNodeTimeoutSeconds = v.GetInt(timeout) + c.IndexDateSeparator = v.GetString(indexDateSeparator) + c.Username = v.GetString(username) + c.Password = v.GetString(password) +} diff --git a/cmd/es-index-cleaner/app/flags_test.go b/cmd/es-index-cleaner/app/flags_test.go new file mode 100644 index 00000000000..e4696a18a2e --- /dev/null +++ b/cmd/es-index-cleaner/app/flags_test.go @@ -0,0 +1,55 @@ +// Copyright (c) 2021 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package app + +import ( + "flag" + "testing" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBindFlags(t *testing.T) { + v := viper.New() + c := &Config{} + command := cobra.Command{} + flags := &flag.FlagSet{} + c.AddFlags(flags) + command.PersistentFlags().AddGoFlagSet(flags) + v.BindPFlags(command.PersistentFlags()) + + err := command.ParseFlags([]string{ + "--index-prefix=tenant1", + "--rollover=true", + "--archive=true", + "--timeout=150", + "--index-date-separator=@", + "--es.username=admin", + "--es.password=admin", + }) + require.NoError(t, err) + + c.InitFromViper(v) + assert.Equal(t, "tenant1-", c.IndexPrefix) + assert.Equal(t, true, c.Rollover) + assert.Equal(t, true, c.Archive) + assert.Equal(t, 150, c.MasterNodeTimeoutSeconds) + assert.Equal(t, "@", c.IndexDateSeparator) + assert.Equal(t, "admin", c.Username) + assert.Equal(t, "admin", c.Password) +} diff --git a/cmd/es-index-cleaner/app/index_client.go b/cmd/es-index-cleaner/app/index_client.go new file mode 100644 index 00000000000..3a523bc0243 --- /dev/null +++ b/cmd/es-index-cleaner/app/index_client.go @@ -0,0 +1,144 @@ +// Copyright (c) 2021 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package app + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strconv" + "time" +) + +// Index represents ES index. +type Index struct { + // Index name. + Index string + // Index creation time. + CreationTime time.Time + // Aliases + Aliases map[string]bool +} + +// IndicesClient is a client used to manipulate indices. +type IndicesClient struct { + // Http client. + Client *http.Client + // ES server endpoint. + Endpoint string + // ES master_timeout parameter. + MasterTimeoutSeconds int + BasicAuth string +} + +// GetJaegerIndices queries all Jaeger indices including the archive and rollover. +// Jaeger daily indices are: +// jaeger-span-2019-01-01, jaeger-service-2019-01-01, jaeger-dependencies-2019-01-01 +// jaeger-span-archive +// Rollover indices: +// aliases: jaeger-span-read, jaeger-span-write, jaeger-service-read, jaeger-service-write +// indices: jaeger-span-000001, jaeger-service-000001 etc. +// aliases: jaeger-span-archive-read, jaeger-span-archive-write +// indices: jaeger-span-archive-000001 +func (i *IndicesClient) GetJaegerIndices(prefix string) ([]Index, error) { + prefix += "jaeger-*" + r, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/%s?flat_settings=true&filter_path=*.aliases,*.settings", i.Endpoint, prefix), nil) + if err != nil { + return nil, err + } + i.setAuthorization(r) + res, err := i.Client.Do(r) + if err != nil { + return nil, fmt.Errorf("failed to query indices: %w", err) + } + + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to query indices: %w", handleFailedRequest(res)) + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("failed to query indices and read response body: %w", err) + } + + type indexInfo struct { + Aliases map[string]interface{} `json:"aliases"` + Settings map[string]string `json:"settings"` + } + var indicesInfo map[string]indexInfo + if err = json.Unmarshal(body, &indicesInfo); err != nil { + return nil, fmt.Errorf("failed to query indices and unmarshall response body: %q: %w", body, err) + } + + var indices []Index + for k, v := range indicesInfo { + aliases := map[string]bool{} + for alias := range v.Aliases { + aliases[alias] = true + } + // ignoring error, ES should return valid date + creationDate, _ := strconv.ParseInt(v.Settings["index.creation_date"], 10, 64) + + indices = append(indices, Index{ + Index: k, + CreationTime: time.Unix(0, int64(time.Millisecond)*creationDate), + Aliases: aliases, + }) + } + return indices, nil +} + +// DeleteIndices deletes specified set of indices. +func (i *IndicesClient) DeleteIndices(indices []Index) error { + concatIndices := "" + for _, i := range indices { + concatIndices += i.Index + concatIndices += "," + } + + r, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/%s?master_timeout=%ds", i.Endpoint, concatIndices, i.MasterTimeoutSeconds), nil) + if err != nil { + return err + } + i.setAuthorization(r) + + res, err := i.Client.Do(r) + if err != nil { + return fmt.Errorf("failed to delete indices: %w", err) + } + if res.StatusCode != http.StatusOK { + return fmt.Errorf("failed to delete indices: %s, %w", concatIndices, handleFailedRequest(res)) + } + return nil +} + +func handleFailedRequest(res *http.Response) error { + var body string + if res.Body != nil { + bodyBytes, err := ioutil.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("request failed and failed to read response body, status code: %d, %w", res.StatusCode, err) + } + body = string(bodyBytes) + } + return fmt.Errorf("request failed, status code: %d, body: %s", res.StatusCode, body) +} + +func (i *IndicesClient) setAuthorization(r *http.Request) { + if i.BasicAuth != "" { + r.Header.Add("Authorization", fmt.Sprintf("Basic %s", i.BasicAuth)) + } +} diff --git a/cmd/es-index-cleaner/app/index_client_test.go b/cmd/es-index-cleaner/app/index_client_test.go new file mode 100644 index 00000000000..7c7db3829ba --- /dev/null +++ b/cmd/es-index-cleaner/app/index_client_test.go @@ -0,0 +1,214 @@ +// Copyright (c) 2021 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package app + +import ( + "net/http" + "net/http/httptest" + "sort" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const esIndexResponse = ` +{ + "jaeger-service-2021-08-06" : { + "aliases" : { }, + "settings" : { + "index.creation_date" : "1628259381266", + "index.mapper.dynamic" : "false", + "index.mapping.nested_fields.limit" : "50", + "index.number_of_replicas" : "1", + "index.number_of_shards" : "5", + "index.provided_name" : "jaeger-service-2021-08-06", + "index.requests.cache.enable" : "true", + "index.uuid" : "2kKdvrvAT7qXetRzmWhjYQ", + "index.version.created" : "5061099" + } + }, + "jaeger-span-2021-08-06" : { + "aliases" : { }, + "settings" : { + "index.creation_date" : "1628259381326", + "index.mapper.dynamic" : "false", + "index.mapping.nested_fields.limit" : "50", + "index.number_of_replicas" : "1", + "index.number_of_shards" : "5", + "index.provided_name" : "jaeger-span-2021-08-06", + "index.requests.cache.enable" : "true", + "index.uuid" : "zySRY_FfRFa5YMWxNsNViA", + "index.version.created" : "5061099" + } + }, + "jaeger-span-000001" : { + "aliases" : { + "jaeger-span-read" : { }, + "jaeger-span-write" : { } + }, + "settings" : { + "index.creation_date" : "1628259381326" + } + } +}` + +const esErrResponse = `{"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"request [/jaeger-*] contains unrecognized parameter: [help]"}],"type":"illegal_argument_exception","reason":"request [/jaeger-*] contains unrecognized parameter: [help]"},"status":400}` + +func TestClientGetIndices(t *testing.T) { + tests := []struct { + name string + responseCode int + response string + errContains string + indices []Index + }{ + { + name: "no error", + responseCode: http.StatusOK, + response: esIndexResponse, + indices: []Index{ + { + Index: "jaeger-service-2021-08-06", + CreationTime: time.Unix(0, int64(time.Millisecond)*1628259381266), + Aliases: map[string]bool{}, + }, + { + Index: "jaeger-span-000001", + CreationTime: time.Unix(0, int64(time.Millisecond)*1628259381326), + Aliases: map[string]bool{"jaeger-span-read": true, "jaeger-span-write": true}, + }, + { + Index: "jaeger-span-2021-08-06", + CreationTime: time.Unix(0, int64(time.Millisecond)*1628259381326), + Aliases: map[string]bool{}, + }, + }, + }, + { + name: "client error", + responseCode: http.StatusBadRequest, + response: esErrResponse, + errContains: "failed to query indices: request failed, status code: 400", + }, + { + name: "unmarshall error", + responseCode: http.StatusOK, + response: "AAA", + errContains: `failed to query indices and unmarshall response body: "AAA"`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(test.responseCode) + res.Write([]byte(test.response)) + })) + defer testServer.Close() + + c := &IndicesClient{ + Client: testServer.Client(), + Endpoint: testServer.URL, + } + + indices, err := c.GetJaegerIndices("") + if test.errContains != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), test.errContains) + assert.Nil(t, indices) + } else { + require.NoError(t, err) + sort.Slice(indices, func(i, j int) bool { + return strings.Compare(indices[i].Index, indices[j].Index) < 0 + }) + assert.Equal(t, test.indices, indices) + } + }) + } +} + +func TestClientDeleteIndices(t *testing.T) { + tests := []struct { + name string + responseCode int + response string + errContains string + }{ + { + name: "no error", + responseCode: http.StatusOK, + }, + { + name: "client error", + responseCode: http.StatusBadRequest, + response: esErrResponse, + errContains: "ailed to delete indices: jaeger-span", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + assert.True(t, strings.Contains(req.URL.String(), "jaeger-span")) + assert.Equal(t, "Basic foobar", req.Header.Get("Authorization")) + res.WriteHeader(test.responseCode) + res.Write([]byte(test.response)) + })) + defer testServer.Close() + + c := &IndicesClient{ + Client: testServer.Client(), + Endpoint: testServer.URL, + BasicAuth: "foobar", + } + + err := c.DeleteIndices([]Index{ + { + Index: "jaeger-span", + }, + }) + + if test.errContains != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), test.errContains) + } + }) + } +} + +func TestClientRequestError(t *testing.T) { + c := &IndicesClient{ + Endpoint: "%", + } + err := c.DeleteIndices([]Index{}) + require.Error(t, err) + indices, err := c.GetJaegerIndices("") + require.Error(t, err) + assert.Nil(t, indices) +} + +func TestClientDoError(t *testing.T) { + c := &IndicesClient{ + Endpoint: "localhost:1", + Client: &http.Client{}, + } + err := c.DeleteIndices([]Index{}) + require.Error(t, err) + indices, err := c.GetJaegerIndices("") + require.Error(t, err) + assert.Nil(t, indices) +} diff --git a/cmd/es-index-cleaner/app/index_filter.go b/cmd/es-index-cleaner/app/index_filter.go new file mode 100644 index 00000000000..e173ae6dd12 --- /dev/null +++ b/cmd/es-index-cleaner/app/index_filter.go @@ -0,0 +1,80 @@ +// Copyright (c) 2021 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package app + +import ( + "fmt" + "regexp" + "time" +) + +// IndexFilter holds configuration for index filtering. +type IndexFilter struct { + // Index prefix. + IndexPrefix string + // Separator between date fragments. + IndexDateSeparator string + // Whether to filter archive indices. + Archive bool + // Whether to filter rollover indices. + Rollover bool + // Indices created before this date will be deleted. + DeleteBeforeThisDate time.Time +} + +// Filter filters indices. +func (i *IndexFilter) Filter(indices []Index) []Index { + indices = i.filter(indices) + return i.filterByDate(indices) +} + +func (i *IndexFilter) filter(indices []Index) []Index { + var reg *regexp.Regexp + if !i.Rollover && !i.Archive { + // daily indices + reg, _ = regexp.Compile(fmt.Sprintf("^%sjaeger-(span|service|dependencies)-\\d{4}%s\\d{2}%s\\d{2}", i.IndexPrefix, i.IndexDateSeparator, i.IndexDateSeparator)) + } else if !i.Rollover && i.Archive { + // daily archive + reg, _ = regexp.Compile(fmt.Sprintf("^%sjaeger-span-archive", i.IndexPrefix)) + } else if i.Rollover && !i.Archive { + // rollover + reg, _ = regexp.Compile(fmt.Sprintf("^%sjaeger-(span|service)-\\d{6}", i.IndexPrefix)) + } else { + // rollover archive + reg, _ = regexp.Compile(fmt.Sprintf("^%sjaeger-span-archive-\\d{6}", i.IndexPrefix)) + } + + var filtered []Index + for _, in := range indices { + if reg.MatchString(in.Index) { + // index in write alias cannot be removed + if in.Aliases[i.IndexPrefix+"jaeger-span-write"] || in.Aliases[i.IndexPrefix+"jaeger-service-write"] || in.Aliases[i.IndexPrefix+"jaeger-span-archive-write"] { + continue + } + filtered = append(filtered, in) + } + } + return filtered +} + +func (i *IndexFilter) filterByDate(indices []Index) []Index { + var filtered []Index + for _, in := range indices { + if in.CreationTime.Before(i.DeleteBeforeThisDate) { + filtered = append(filtered, in) + } + } + return filtered +} diff --git a/cmd/es-index-cleaner/app/index_filter_test.go b/cmd/es-index-cleaner/app/index_filter_test.go new file mode 100644 index 00000000000..da280e6ba68 --- /dev/null +++ b/cmd/es-index-cleaner/app/index_filter_test.go @@ -0,0 +1,345 @@ +// Copyright (c) 2021 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package app + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestIndexFilter(t *testing.T) { + testIndexFilter(t, "") +} + +func TestIndexFilter_prefix(t *testing.T) { + testIndexFilter(t, "tenant1-") +} + +func testIndexFilter(t *testing.T, prefix string) { + time20200807 := time.Date(2020, time.August, 06, 0, 0, 0, 0, time.UTC).AddDate(0, 0, 1) + indices := []Index{ + { + Index: prefix + "jaeger-span-2020-08-06", + CreationTime: time.Date(2020, time.August, 06, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{}, + }, + { + Index: prefix + "jaeger-span-2020-08-05", + CreationTime: time.Date(2020, time.August, 05, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{}, + }, + { + Index: prefix + "jaeger-service-2020-08-06", + CreationTime: time.Date(2020, time.August, 06, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{}, + }, + { + Index: prefix + "jaeger-service-2020-08-05", + CreationTime: time.Date(2020, time.August, 05, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{}, + }, + { + Index: prefix + "jaeger-dependencies-2020-08-06", + CreationTime: time.Date(2020, time.August, 06, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{}, + }, + { + Index: prefix + "jaeger-dependencies-2020-08-05", + CreationTime: time.Date(2020, time.August, 05, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{}, + }, + { + Index: prefix + "jaeger-span-archive", + CreationTime: time.Date(2020, time.August, 0, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{}, + }, + { + Index: prefix + "jaeger-span-000001", + CreationTime: time.Date(2020, time.August, 05, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{ + prefix + "jaeger-span-read": true, + }, + }, + { + Index: prefix + "jaeger-span-000002", + CreationTime: time.Date(2020, time.August, 06, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{ + prefix + "jaeger-span-read": true, + prefix + "jaeger-span-write": true, + }, + }, + { + Index: prefix + "jaeger-service-000001", + CreationTime: time.Date(2020, time.August, 05, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{ + prefix + "jaeger-service-read": true, + }, + }, + { + Index: prefix + "jaeger-service-000002", + CreationTime: time.Date(2020, time.August, 06, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{ + prefix + "jaeger-service-read": true, + prefix + "jaeger-service-write": true, + }, + }, + { + Index: prefix + "jaeger-span-archive-000001", + CreationTime: time.Date(2020, time.August, 05, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{ + prefix + "jaeger-span-archive-read": true, + }, + }, + { + Index: prefix + "jaeger-span-archive-000002", + CreationTime: time.Date(2020, time.August, 06, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{ + prefix + "jaeger-span-archive-read": true, + prefix + "jaeger-span-archive-write": true, + }, + }, + { + Index: "other-jaeger-span-2020-08-05", + CreationTime: time.Date(2020, time.August, 05, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{}, + }, + { + Index: "other-jaeger-service-2020-08-06", + CreationTime: time.Date(2020, time.August, 06, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{}, + }, + { + Index: "other-bar-jaeger-span-000002", + CreationTime: time.Date(2020, time.August, 05, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{ + "other-jaeger-span-read": true, + "other-jaeger-span-write": true, + }, + }, + { + Index: "otherfoo-jaeger-span-archive", + CreationTime: time.Date(2020, time.August, 0, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{}, + }, + { + Index: "foo-jaeger-span-archive-000001", + CreationTime: time.Date(2020, time.August, 05, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{ + "foo-jaeger-span-archive-read": true, + }, + }, + } + + tests := []struct { + name string + filter *IndexFilter + expected []Index + }{ + { + name: "normal indices, remove older than 2 days", + filter: &IndexFilter{ + IndexPrefix: prefix, + IndexDateSeparator: "-", + Archive: false, + Rollover: false, + DeleteBeforeThisDate: time20200807.Add(-time.Hour * 24 * time.Duration(2)), + }, + }, + { + name: "normal indices, remove older 1 days", + filter: &IndexFilter{ + IndexPrefix: prefix, + IndexDateSeparator: "-", + Archive: false, + Rollover: false, + DeleteBeforeThisDate: time20200807.Add(-time.Hour * 24 * time.Duration(1)), + }, + expected: []Index{ + { + Index: prefix + "jaeger-span-2020-08-05", + CreationTime: time.Date(2020, time.August, 05, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{}, + }, + { + Index: prefix + "jaeger-service-2020-08-05", + CreationTime: time.Date(2020, time.August, 05, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{}, + }, + { + Index: prefix + "jaeger-dependencies-2020-08-05", + CreationTime: time.Date(2020, time.August, 05, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{}, + }, + }, + }, + { + name: "normal indices, remove older 0 days - it should remove all indices", + filter: &IndexFilter{ + IndexPrefix: prefix, + IndexDateSeparator: "-", + Archive: false, + Rollover: false, + DeleteBeforeThisDate: time20200807.Add(-time.Hour * 24 * time.Duration(0)), + }, + expected: []Index{ + { + Index: prefix + "jaeger-span-2020-08-06", + CreationTime: time.Date(2020, time.August, 06, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{}, + }, + { + Index: prefix + "jaeger-span-2020-08-05", + CreationTime: time.Date(2020, time.August, 05, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{}, + }, + { + Index: prefix + "jaeger-service-2020-08-06", + CreationTime: time.Date(2020, time.August, 06, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{}, + }, + { + Index: prefix + "jaeger-service-2020-08-05", + CreationTime: time.Date(2020, time.August, 05, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{}, + }, + { + Index: prefix + "jaeger-dependencies-2020-08-06", + CreationTime: time.Date(2020, time.August, 06, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{}, + }, + { + Index: prefix + "jaeger-dependencies-2020-08-05", + CreationTime: time.Date(2020, time.August, 05, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{}, + }, + }, + }, + { + name: "archive indices, remove older 2 days", + filter: &IndexFilter{ + IndexPrefix: prefix, + IndexDateSeparator: "-", + Archive: true, + Rollover: false, + DeleteBeforeThisDate: time20200807.Add(-time.Hour * 24 * time.Duration(2)), + }, + expected: []Index{ + { + Index: prefix + "jaeger-span-archive", + CreationTime: time.Date(2020, time.August, 0, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{}, + }, + }, + }, + { + name: "rollover indices, remove older 1 days", + filter: &IndexFilter{ + IndexPrefix: prefix, + IndexDateSeparator: "-", + Archive: false, + Rollover: true, + DeleteBeforeThisDate: time20200807.Add(-time.Hour * 24 * time.Duration(1)), + }, + expected: []Index{ + { + Index: prefix + "jaeger-span-000001", + CreationTime: time.Date(2020, time.August, 05, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{ + prefix + "jaeger-span-read": true, + }, + }, + { + Index: prefix + "jaeger-service-000001", + CreationTime: time.Date(2020, time.August, 05, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{ + prefix + "jaeger-service-read": true, + }, + }, + }, + }, + { + name: "rollover indices, remove older 0 days, index in write alias cannot be removed", + filter: &IndexFilter{ + IndexPrefix: prefix, + IndexDateSeparator: "-", + Archive: false, + Rollover: true, + DeleteBeforeThisDate: time20200807.Add(-time.Hour * 24 * time.Duration(0)), + }, + expected: []Index{ + { + Index: prefix + "jaeger-span-000001", + CreationTime: time.Date(2020, time.August, 05, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{ + prefix + "jaeger-span-read": true, + }, + }, + { + Index: prefix + "jaeger-service-000001", + CreationTime: time.Date(2020, time.August, 05, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{ + prefix + "jaeger-service-read": true, + }, + }, + }, + }, + { + name: "rollover archive indices, remove older 1 days", + filter: &IndexFilter{ + IndexPrefix: prefix, + IndexDateSeparator: "-", + Archive: true, + Rollover: true, + DeleteBeforeThisDate: time20200807.Add(-time.Hour * 24 * time.Duration(1)), + }, + expected: []Index{ + { + Index: prefix + "jaeger-span-archive-000001", + CreationTime: time.Date(2020, time.August, 05, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{ + prefix + "jaeger-span-archive-read": true, + }, + }, + }, + }, + { + name: "rollover archive indices, remove older 0 days, index in write alias cannot be removed", + filter: &IndexFilter{ + IndexPrefix: prefix, + IndexDateSeparator: "-", + Archive: true, + Rollover: true, + DeleteBeforeThisDate: time20200807.Add(-time.Hour * 24 * time.Duration(0)), + }, + expected: []Index{ + { + Index: prefix + "jaeger-span-archive-000001", + CreationTime: time.Date(2020, time.August, 05, 15, 0, 0, 0, time.UTC), + Aliases: map[string]bool{ + prefix + "jaeger-span-archive-read": true, + }, + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + indices := test.filter.Filter(indices) + assert.Equal(t, test.expected, indices) + }) + } +} diff --git a/cmd/es-index-cleaner/main.go b/cmd/es-index-cleaner/main.go new file mode 100644 index 00000000000..e38f15330e8 --- /dev/null +++ b/cmd/es-index-cleaner/main.go @@ -0,0 +1,120 @@ +// Copyright (c) 2021 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "encoding/base64" + "fmt" + "log" + "net/http" + "strconv" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uber.org/zap" + + "github.com/jaegertracing/jaeger/cmd/es-index-cleaner/app" + "github.com/jaegertracing/jaeger/pkg/config" + "github.com/jaegertracing/jaeger/pkg/config/tlscfg" +) + +func main() { + logger, _ := zap.NewProduction() + v := viper.New() + cfg := &app.Config{} + tlsFlags := tlscfg.ClientFlagsConfig{Prefix: "es"} + + var command = &cobra.Command{ + Use: "jaeger-es-index-cleaner NUM_OF_DAYS http://HOSTNAME:PORT", + Short: "Jaeger es-index-cleaner removes Jaeger indices", + Long: "Jaeger es-index-cleaner removes Jaeger indices", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 2 { + return fmt.Errorf("wrong number of arguments") + } + numOfDays, err := strconv.Atoi(args[0]) + if err != nil { + return fmt.Errorf("could not parse NUM_OF_DAYS argument: %w", err) + } + + cfg.InitFromViper(v) + tlsOpts := tlsFlags.InitFromViper(v) + tlsCfg, err := tlsOpts.Config(logger) + if err != nil { + return err + } + defer tlsOpts.Close() + + c := &http.Client{ + Timeout: time.Duration(cfg.MasterNodeTimeoutSeconds) * time.Second, + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: tlsCfg, + }, + } + i := app.IndicesClient{ + Client: c, + Endpoint: args[1], + MasterTimeoutSeconds: cfg.MasterNodeTimeoutSeconds, + BasicAuth: basicAuth(cfg.Username, cfg.Password), + } + + indices, err := i.GetJaegerIndices(cfg.IndexPrefix) + if err != nil { + return err + } + + year, month, day := time.Now().Date() + tomorrowMidnight := time.Date(year, month, day, 0, 0, 0, 0, time.Now().Location()).AddDate(0, 0, 1) + deleteIndicesBefore := tomorrowMidnight.Add(-time.Hour * 24 * time.Duration(numOfDays)) + logger.Info("Indices before this date will be deleted", zap.Time("date", deleteIndicesBefore)) + + filter := &app.IndexFilter{ + IndexPrefix: cfg.IndexPrefix, + IndexDateSeparator: cfg.IndexDateSeparator, + Archive: cfg.Archive, + Rollover: cfg.Rollover, + DeleteBeforeThisDate: deleteIndicesBefore, + } + indices = filter.Filter(indices) + + if len(indices) == 0 { + logger.Info("No indices to delete") + return nil + } + logger.Info("Deleting indices", zap.Any("indices", indices)) + return i.DeleteIndices(indices) + }, + } + + config.AddFlags( + v, + command, + cfg.AddFlags, + tlsFlags.AddFlags, + ) + + if err := command.Execute(); err != nil { + log.Fatalln(err) + } +} + +func basicAuth(username, password string) string { + if username == "" || password == "" { + return "" + } + return base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) +}