From 18aea51f6d6e4b111358c49d06a5c6caf7c7ba7c Mon Sep 17 00:00:00 2001
From: Pavol Loffay
Date: Fri, 6 Aug 2021 14:53:53 +0200
Subject: [PATCH] Add golang implementation for es-index-cleaner
Signed-off-by: Pavol Loffay
---
cmd/es-index-cleaner/app/flags.go | 69 ++++
cmd/es-index-cleaner/app/flags_test.go | 55 +++
cmd/es-index-cleaner/app/index_client.go | 144 ++++++++
cmd/es-index-cleaner/app/index_client_test.go | 214 +++++++++++
cmd/es-index-cleaner/app/index_filter.go | 80 ++++
cmd/es-index-cleaner/app/index_filter_test.go | 346 ++++++++++++++++++
cmd/es-index-cleaner/main.go | 120 ++++++
7 files changed, 1028 insertions(+)
create mode 100644 cmd/es-index-cleaner/app/flags.go
create mode 100644 cmd/es-index-cleaner/app/flags_test.go
create mode 100644 cmd/es-index-cleaner/app/index_client.go
create mode 100644 cmd/es-index-cleaner/app/index_client_test.go
create mode 100644 cmd/es-index-cleaner/app/index_filter.go
create mode 100644 cmd/es-index-cleaner/app/index_filter_test.go
create mode 100644 cmd/es-index-cleaner/main.go
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..22916f99f2e
--- /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 with 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)
+ response, err := i.Client.Do(r)
+ if err != nil {
+ return nil, fmt.Errorf("failed to query Jaeger indices: %q", err)
+ }
+
+ if response.StatusCode != 200 {
+ body, err := ioutil.ReadAll(response.Body)
+ if err != nil {
+ return nil, err
+ }
+ return nil, fmt.Errorf("failed to query Jaeger indices: %s", string(body))
+ }
+
+ body, err := ioutil.ReadAll(response.Body)
+ if err != nil {
+ return nil, 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 unmarshall response: %q", 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)
+
+ response, err := i.Client.Do(r)
+ if err != nil {
+ return err
+ }
+ if response.StatusCode != 200 {
+ var body string
+ if response.Body != nil {
+ bodyBytes, err := ioutil.ReadAll(response.Body)
+ if err != nil {
+ return fmt.Errorf("deleting indices failed: %q", err)
+ }
+ body = string(bodyBytes)
+ }
+ return fmt.Errorf("deleting indices failed: %s", body)
+ }
+ return nil
+}
+
+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..c886f9580ed
--- /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 Jaeger indices: ",
+ },
+ {
+ name: "unmarshall error",
+ responseCode: http.StatusOK,
+ response: "AAA",
+ errContains: `failed to unmarshall response: "invalid character 'A' looking for beginning of value`,
+ },
+ }
+ 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: "deleting indices failed: ",
+ },
+ }
+ 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..1f3bc1c7200
--- /dev/null
+++ b/cmd/es-index-cleaner/app/index_filter_test.go
@@ -0,0 +1,346 @@
+// 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)
+ //firstDay := tomorrowMidnight.Add(-time.Hour*24*time.Duration(numOfDays))
+ 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 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",
+ 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..d65425b8c60
--- /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.NewDevelopment()
+ 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 index",
+ Long: "Jaeger es-index-cleaner removes Jaeger indexes",
+ 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: %q", 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))
+}