add index template {ls,sh} (#33)

This commit is contained in:
T. von Dein
2026-06-15 15:18:21 +02:00
parent ec1ce56b2d
commit d0d1d69791
15 changed files with 875 additions and 100 deletions

View File

@@ -70,6 +70,11 @@ Command tree:
list
set
status
datastream
create
delete
list
show
debug
doc
add
@@ -90,6 +95,9 @@ Command tree:
list
modify
show
template
list
show
node
list
show

View File

@@ -27,7 +27,7 @@ import (
)
const (
SETTINGS = `https://www.elastic.co/docs/reference/elasticsearch/configuration-reference`
SETTINGS = ``
)
func ClusterSettings(conf *cfg.Config) *cli.Command {
@@ -85,7 +85,9 @@ func ClusterSettingsSet(conf *cfg.Config) *cli.Command {
Name: "set",
Usage: "set|update cluster settings",
Aliases: []string{"set", "update"},
UsageText: "set [options] setting:value [setting:value ...]\n\nReference: " + SETTINGS,
UsageText: "set [options] setting:value [setting:value ...]",
CustomHelpTemplate: addReference(
"https://www.elastic.co/docs/reference/elasticsearch/configuration-reference"),
Flags: []cli.Flag{
&cli.BoolFlag{

View File

@@ -41,8 +41,11 @@ func Index(conf *cfg.Config) *cli.Command {
IndexClose(conf),
IndexAllocation(conf),
IndexModify(conf),
IndexAlias(conf),
IndexFields(conf),
// sub commands
IndexAlias(conf),
IndexTemplate(conf),
},
}
}
@@ -165,6 +168,7 @@ Valid field mapping types: integer, text, date, keyword`,
Usage: "number of replicas to create",
Destination: &conf.Replicas,
Aliases: []string{"r"},
Value: 1,
},
},
@@ -240,6 +244,7 @@ func IndexModify(conf *cfg.Config) *cli.Command {
Usage: "number of replicas",
Destination: &conf.Replicas,
Aliases: []string{"r"},
Value: 1,
},
},

203
cmd/index_template.go Normal file
View File

@@ -0,0 +1,203 @@
/*
Copyright © 2026 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
import (
"context"
"errors"
"fmt"
"codeberg.org/scip/esctl/pkg/cfg"
"codeberg.org/scip/esctl/pkg/es"
"github.com/urfave/cli/v3"
)
func IndexTemplate(conf *cfg.Config) *cli.Command {
return &cli.Command{
Name: "template",
Aliases: []string{"tpl"},
Usage: "manage index templates",
Commands: []*cli.Command{
IndexTemplateList(conf),
IndexTemplateShow(conf),
IndexTemplateCreate(conf, false),
IndexTemplateCreate(conf, true),
IndexTemplateDelete(conf),
},
}
}
func IndexTemplateList(conf *cfg.Config) *cli.Command {
return &cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "list index templates",
Action: func(ctx context.Context, cmd *cli.Command) error {
return es.IndexTemplateList(conf)
},
}
}
func IndexTemplateShow(conf *cfg.Config) *cli.Command {
return &cli.Command{
Name: "show",
Aliases: []string{"sh"},
Usage: "show details about an index template",
Action: func(ctx context.Context, cmd *cli.Command) error {
index := cmd.Args().Get(0)
if index == "" {
return errors.New("no index template specified")
}
return es.IndexTemplateShow(conf, cmd.Args().Get(0))
},
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
},
}
}
func IndexTemplateCreate(conf *cfg.Config, modify bool) *cli.Command {
name := "create"
alias := "+"
required := true
if modify {
name = "modify"
alias = "mod"
required = false
}
return &cli.Command{
Name: name,
Aliases: []string{alias},
Usage: name + " a new index template",
UsageText: name + ` <name> <fieldmapping:type>...`,
CustomHelpTemplate: addReference(
`Valid field mapping types: integer, text, date, keyword
Doc for index settings:
https://www.elastic.co/docs/reference/elasticsearch/index-settings
`),
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "data-stream",
Usage: "template creates a data stream",
Destination: &conf.Stream,
Aliases: []string{"S"},
},
&cli.BoolFlag{
Name: "allow-auto-create",
Usage: "automatically create indices",
Destination: &conf.AutoCreate,
Aliases: []string{"a"},
},
&cli.StringFlag{
Name: "mode",
Usage: "index mode, one of: standard, timeseries, logsdb or lookup",
Destination: &conf.Mode,
Aliases: []string{"m"},
},
&cli.StringFlag{
Name: "retention",
Usage: "data retention, e.g. -1 (infinitely), 30d, 3m or 1y (data stream only)",
Destination: &conf.Retention,
Aliases: []string{"r"},
Required: required,
},
&cli.IntFlag{
Name: "priority",
Usage: "index template priority",
Destination: &conf.Priority,
Aliases: []string{"p"},
},
&cli.StringSliceFlag{
Name: "index-pattern",
Usage: "index matching pattern",
Destination: &conf.Patterns,
Aliases: []string{"i"},
Required: required,
},
&cli.StringSliceFlag{
Name: "settings",
Usage: "individual template settings, format: name:value, multiple possible",
Destination: &conf.Settings,
Aliases: []string{"s"},
},
&cli.StringSliceFlag{
Name: "component",
Usage: "include component template, multiple possible",
Destination: &conf.Components,
Aliases: []string{"c"},
},
&cli.StringSliceFlag{
Name: "meta",
Usage: "custom meta field, format: name:value, multiple possible",
Destination: &conf.Meta,
Aliases: []string{"M"},
},
&cli.StringSliceFlag{
Name: "aliases",
Usage: "set up aliases to associate with indices",
Destination: &conf.Aliases,
Aliases: []string{"A"},
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
args := cmd.Args()
if args.Len() == 0 {
return fmt.Errorf("no name specified")
}
mappings := args.Slice()[1:]
if modify {
return es.IndexTemplateModify(conf, args.Get(0), mappings)
}
return es.IndexTemplateCreate(conf, args.Get(0), mappings)
},
}
}
func IndexTemplateDelete(conf *cfg.Config) *cli.Command {
return &cli.Command{
Name: "delete",
Aliases: []string{"rm"},
Usage: "delete an index template",
Action: func(ctx context.Context, cmd *cli.Command) error {
index := cmd.Args().Get(0)
if index == "" {
return errors.New("no index template specified")
}
return es.IndexTemplateDelete(conf, cmd.Args().Get(0))
},
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cindex)
},
}
}

View File

@@ -25,7 +25,7 @@ import (
"github.com/urfave/cli/v3"
)
const SearchUsage = `<sep> might be one of:
const SearchUsage = `field <sep> might be one of:
=: Must match
!=: Must not match
@@ -43,15 +43,15 @@ For datetime range format refer to:
https://www.elastic.co/docs/reference/elasticsearch/rest-apis/common-options#date-math
For timestamp formats refer to:
https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-date-format
`
https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-date-format`
func Search(conf *cfg.Config) *cli.Command {
return &cli.Command{
Name: "search",
Aliases: []string{"/"},
Usage: "search within an index",
UsageText: "search [options] [<[field<sep>]pattern> ...]\n" + SearchUsage,
UsageText: "search [options] [<[field<sep>]pattern> ...]\n",
CustomHelpTemplate: addReference(SearchUsage),
Flags: []cli.Flag{
&cli.StringFlag{

35
cmd/utilities.go Normal file
View File

@@ -0,0 +1,35 @@
/*
Copyright © 2026 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
import (
"fmt"
"strings"
"github.com/urfave/cli/v3"
)
func addReference(ref string) string {
indentedRef := []string{}
for _, line := range strings.Split(ref, "\n") {
indentedRef = append(indentedRef, " "+line)
}
return fmt.Sprintf("%s\nREFERENCE:\n%s\n",
cli.SubcommandHelpTemplate,
strings.Join(indentedRef, "\n"))
}

4
go.mod
View File

@@ -25,6 +25,7 @@ require (
github.com/fatih/color v1.19.0
github.com/mattn/go-isatty v0.0.22
github.com/olekukonko/tablewriter v1.1.4
github.com/tidwall/gjson v1.19.0
github.com/tlinden/yadu v0.1.3
github.com/urfave/cli/v3 v3.9.1-0.20260524212652-be8b79d0c8de
gopkg.in/yaml.v3 v3.0.1
@@ -37,19 +38,16 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/lmittmann/tint v1.1.3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-runewidth v0.0.23 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.2.0 // indirect
github.com/olekukonko/ll v0.1.8 // indirect
github.com/tidwall/gjson v1.19.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/sys v0.42.0 // indirect
)

52
go.sum
View File

@@ -2,28 +2,24 @@ github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=
github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elastic/elastic-transport-go/v8 v8.9.0 h1:KeT/2P54F0xS0S8Y3Pf+tFDg4HmBgReQMB+BMz8dDAs=
github.com/elastic/elastic-transport-go/v8 v8.9.0/go.mod h1:ssMTvNS2hwf7CaiGsRRsx4gQHFZ/jS/DkLcISxekWzc=
github.com/elastic/elastic-transport-go/v8 v8.11.0 h1:taYmqC2M6+fZt/+W+ENYh/W5L9+KrlJGOSbEJs8egWc=
github.com/elastic/elastic-transport-go/v8 v8.11.0/go.mod h1:DZQ0szCNywc9F+C9l/Kkd4n69SvJVj0I3yK1Of7s3l8=
github.com/elastic/go-elasticsearch/v9 v9.3.2 h1:nvtvfN/Gsp/rzUPz/9yILwDAsYJ3s5L0VmhR16zPKCA=
github.com/elastic/go-elasticsearch/v9 v9.3.2/go.mod h1:ubKUMJCJbX5V/gW5MIn2NQZyaEZ61ubXwJmD5UMNrM8=
github.com/elastic/go-elasticsearch/v9 v9.3.4 h1:vnqXl6jnlA+ZwlfRaKF9BR9woZC1KSB17gtyvgf8zVU=
github.com/elastic/go-elasticsearch/v9 v9.3.4/go.mod h1:ubKUMJCJbX5V/gW5MIn2NQZyaEZ61ubXwJmD5UMNrM8=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -33,31 +29,34 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I=
github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA=
github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88=
github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
@@ -66,10 +65,6 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tlinden/yadu v0.1.3 h1:5cRCUmj+l5yvlM2irtpFBIJwVV2DPEgYSaWvF19FtcY=
github.com/tlinden/yadu v0.1.3/go.mod h1:l3bRmHKL9zGAR6pnBHY2HRPxBecf7L74BoBgOOpTcUA=
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/urfave/cli/v3 v3.9.0 h1:AV9lIiPv3ukYnxunaCUsHnEozptYmDN2F0+yWqLMn/c=
github.com/urfave/cli/v3 v3.9.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/urfave/cli/v3 v3.9.1-0.20260524212652-be8b79d0c8de h1:ESKPiS7inVoBnv4FmgGNZdjWBI/wmvaragoyD3D9nM4=
github.com/urfave/cli/v3 v3.9.1-0.20260524212652-be8b79d0c8de/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
@@ -78,18 +73,15 @@ go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo=
go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -61,6 +61,18 @@ type Config struct {
Primary bool // index allocation: -p
Searchable bool // index fields: -s
Aggretable bool // index fields: -a
Priority int // index template create: -p
Settings []string // index template create: -s
Patterns []string // index template create: -i
Components []string // index template create: -c
Meta []string // index template create: -M
Aliases []string // index template create: -A
Stream bool // index template create: -S
AutoCreate bool // index template create: -a
Mode string // index template create: -a
Retention string // index template create: -r
From, To, MaxItems int // search: flags
Filter []string // search: -F
Path string // search+doc sh: -p
@@ -75,7 +87,7 @@ type Config struct {
Ascending bool // sort: -a
Exclude string // cluster compare: -e (regexp)
All, Verbose bool // cluster status: -a -v
Persistent, Transient, Default bool // -p -t -D cluster settings set
Persistent, Transient, Default bool // cluster settings set: -p -t -D
Force bool // ccr follower renew: -f
HaveJQ bool // determined at runtime by ourselfes
DebugHTTP bool // root: --debug-http

View File

@@ -27,36 +27,6 @@ import (
"github.com/urfave/cli/v3"
)
// recursively traverse the raw settings hash and build a flat map
// consisting of the translated path and its value.
//
// e.g.
// logger:
//
// org:
// elasticsearch:
// transport:
// OutboundHandler: "ERROR"
//
// gets:
//
// logger.org.elasticsearch.transport.OutboundHandler: "ERROR"
func getJsonPath(raw map[string]any, topic string) map[string]string {
paths := map[string]string{}
for name, data := range raw {
path := topic + "." + name
switch value := data.(type) {
case string:
paths[path] = value
case map[string]any:
paths = getJsonPath(value, path)
}
}
return paths
}
func ClusterSettingsList(conf *cfg.Config) error {
res, err := conf.DefaultCluster.ES.Cluster.GetSettings().
Header("content-type", "application/json").
@@ -87,7 +57,7 @@ func ClusterSettingsList(conf *cfg.Config) error {
return fmt.Errorf("failed to unmarshall setting for topic %s: %s", topic, err)
}
paths := getJsonPath(data, topic)
paths := getJsonPath(map[string]string{}, data, topic)
slog.Debug("settings", topic, paths)
for setting, value := range paths {

View File

@@ -21,6 +21,7 @@ import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"sync"
@@ -397,3 +398,44 @@ func getClusterData(es *elasticsearch.TypedClient, wg *sync.WaitGroup, reschan c
reschan <- ar
}
// recursively traverse the raw settings hash and build a flat map
// consisting of the translated path and its value.
//
// e.g.
// logger:
//
// org:
// elasticsearch:
// transport:
// OutboundHandler: "ERROR"
//
// gets:
//
// logger.org.elasticsearch.transport.OutboundHandler: "ERROR"
func getJsonPath(paths map[string]string, raw map[string]any, topic string) map[string]string {
for name, data := range raw {
path := topic + "." + name
switch value := data.(type) {
case string:
paths[path] = value
case *string:
paths[path] = *value
case int:
paths[path] = strconv.Itoa(value)
case *int:
paths[path] = strconv.Itoa(*value)
case map[string]any:
paths = getJsonPath(paths, value, path)
case []any:
val := []string{}
for _, item := range value {
val = append(val, fmt.Sprintf("%v", item))
}
paths[path] = strings.Join(val, ",")
}
}
return paths
}

View File

@@ -126,11 +126,7 @@ func IndexList(conf *cfg.Config) error {
}
table.Sort()
if err := table.Print(); err != nil {
return err
}
return nil
return table.Print()
}
func IndexShow(conf *cfg.Config, indexpattern string) error {

449
pkg/es/index_template.go Normal file
View File

@@ -0,0 +1,449 @@
/*
Copyright © 2026 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package es
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"slices"
"strconv"
"strings"
"codeberg.org/scip/esctl/pkg/cfg"
"codeberg.org/scip/esctl/pkg/printer"
"github.com/elastic/go-elasticsearch/v9/typedapi/esdsl"
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
)
// used for completion
func IndexTemplateList(conf *cfg.Config) error {
res, err := conf.DefaultCluster.ES.Indices.GetIndexTemplate().
Header("content-type", "application/json").
Header("accept", "application/json").
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to get index templates: %s", esErrorString(err))
}
slog.Debug("res", "index templates", res)
table := printer.NewTable(conf, 5, len(res.IndexTemplates))
table.Addheaders("name", "description", "priority")
for idx, tpl := range res.IndexTemplates {
desc, err := json.Marshal(tpl.IndexTemplate.Meta_["description"])
if err != nil {
return fmt.Errorf("failed to unmarshal meta json data: %w", err)
}
table.Entries[idx] = []string{
tpl.Name,
string(desc),
fmt.Sprintf("%d", tpl.IndexTemplate.Priority),
}
}
table.Sort()
return table.Print()
}
func IndexTemplateShow(conf *cfg.Config, tplname string) error {
res, err := conf.DefaultCluster.ES.Indices.GetIndexTemplate().
Name(tplname).
Header("content-type", "application/json").
Header("accept", "application/json").
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to get index template: %s", esErrorString(err))
}
slog.Debug("res", "index template", res)
if len(res.IndexTemplates) != 1 {
return errors.New("multiple or no index template matched the pattern")
}
tpl := res.IndexTemplates[0]
table := printer.NewTable(conf, 2, 6)
table.Addheaders("index template property", "value")
desc, err := json.Marshal(tpl.IndexTemplate.Meta_["description"])
if err != nil {
return fmt.Errorf("failed to unmarshal meta json data: %w", err)
}
hasds := tpl.IndexTemplate.DataStream != nil
aliases := []string{}
for alias := range tpl.IndexTemplate.Template.Aliases {
aliases = append(aliases, alias)
}
table.Entries = [][]string{
{"name", tpl.Name},
{"description", string(desc)},
{"index patterns", strings.Join(tpl.IndexTemplate.IndexPatterns, ",")},
{"composed of", strings.Join(tpl.IndexTemplate.ComposedOf, ",")},
{"data stream enabled", fmt.Sprintf("%t", hasds)},
{"aliases", strings.Join(aliases, ",")},
}
if err := table.Print(); err != nil {
return err
}
table = printer.NewTable(conf, 2, 0)
table.Addheaders("index setting property", "value")
err = getIndexTemplateSettings(conf, tplname, table)
if err != nil {
return nil
}
fmt.Println()
if err := table.Print(); err != nil {
return err
}
table = printer.NewTable(conf, 2, 0)
table.Addheaders("index field mapping", "type")
for name, field := range tpl.IndexTemplate.Template.Mappings.Properties {
typeval := ""
switch val := field.(type) {
case *types.IntegerNumberProperty:
typeval = val.Type
case *types.KeywordProperty:
typeval = val.Type
case *types.DateProperty:
typeval = val.Type
}
table.Entries = append(table.Entries, []string{
name, typeval,
})
}
fmt.Println()
if err := table.Print(); err != nil {
return err
}
return nil
}
func IndexTemplateCreate(conf *cfg.Config, name string, mappings []string) error {
settings := esdsl.NewIndexSettings()
maps := esdsl.NewIndexTemplateMapping()
create := conf.DefaultCluster.ES.Indices.PutIndexTemplate(name).
Header("content-type", "application/json").
Header("accept", "application/json")
if conf.Shards > 0 {
settings = settings.NumberOfShards(strconv.Itoa(conf.Shards))
}
if conf.Replicas > 0 {
settings = settings.NumberOfReplicas(strconv.Itoa(conf.Replicas))
}
if conf.Stream {
create.DataStream(esdsl.NewDataStreamVisibility())
if conf.Retention != "" {
maps.Lifecycle(
esdsl.NewDataStreamLifecycle().
DataRetention(
esdsl.NewDuration().String(conf.Retention)))
}
}
if conf.AutoCreate {
create.AllowAutoCreate(true)
}
if conf.Mode != "" {
if !slices.Contains([]string{"standard", "timeseries", "logsdb", "lookup"}, conf.Mode) {
return errors.New("mode must be one of: standard, timeseries, logsdb or lookup")
}
settings = settings.Mode(conf.Mode)
}
if len(mappings) > 0 {
typemaps := esdsl.NewTypeMapping()
for _, mapping := range mappings {
parts := strings.Split(mapping, ":")
if len(parts) != 2 {
return fmt.Errorf(
"invalid mapping %s, expect <name:type> (type: integer, text, date, keyword)",
mapping)
}
switch parts[1] {
case "text":
typemaps.AddProperty(parts[0], esdsl.NewTextProperty())
case "integer":
typemaps.AddProperty(parts[0], esdsl.NewIntegerNumberProperty())
case "date":
typemaps.AddProperty(parts[0], esdsl.NewDateProperty())
case "keyword":
typemaps.AddProperty(parts[0], esdsl.NewKeywordProperty())
}
}
maps.Mappings(typemaps)
}
if len(conf.Components) > 0 {
create.ComposedOf(conf.Components...)
}
for _, alias := range conf.Aliases {
maps.AddAlias(alias, esdsl.NewAlias())
}
if len(conf.Meta) > 0 {
metadata := map[string]json.RawMessage{}
for _, meta := range conf.Meta {
parts := strings.Split(meta, ":")
if len(parts) != 2 {
return errors.New("meta data must be in the form key:value")
}
msg, err := json.Marshal(parts[1])
if err != nil {
return fmt.Errorf("failed to json marshal metadata %s: %w", meta, err)
}
metadata[parts[0]] = msg
}
create.Meta_(esdsl.NewMetadata(metadata))
}
if len(conf.Settings) > 0 {
usersettings := map[string]json.RawMessage{}
for _, meta := range conf.Settings {
parts := strings.Split(meta, ":")
if len(parts) != 2 {
return errors.New("settings data must be in the form key:value")
}
msg, err := json.Marshal(parts[1])
if err != nil {
return fmt.Errorf("failed to json marshal metadata %s: %w", meta, err)
}
usersettings[parts[0]] = msg
}
settings = settings.IndexSettings(usersettings)
}
maps.Settings(settings)
create.Template(maps)
create.IndexPatterns(conf.Patterns...)
_, err := create.Do(context.Background())
if err != nil {
return fmt.Errorf("failed to create index template: %s", esErrorString(err))
}
return nil
}
// FIXME: func's too long, refactor
// FIXME: it's not yet possible to remove items in lists (aliases, mappings, settings), just overwrite them
func IndexTemplateModify(conf *cfg.Config, name string, mappings []string) error {
maps := esdsl.NewIndexTemplateMapping()
// load existing index mapping
res, err := conf.DefaultCluster.ES.Indices.GetIndexTemplate().
Name(name).
Header("content-type", "application/json").
Header("accept", "application/json").
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to get index template: %s", esErrorString(err))
}
slog.Debug("res", "index template", res)
if len(res.IndexTemplates) != 1 {
return errors.New("multiple or no index template matched the pattern")
}
tpl := res.IndexTemplates[0]
// our modify PUT request
modify := conf.DefaultCluster.ES.Indices.PutIndexTemplate(name).
Header("content-type", "application/json").
Header("accept", "application/json")
// load existing settings, if any
settings := tpl.IndexTemplate.Template.Settings
// pre fill mappings and aliases
maps.Mappings(tpl.IndexTemplate.Template.Mappings)
maps.Aliases(tpl.IndexTemplate.Template.Aliases)
// pre fill meta, if any
metadata := map[string]json.RawMessage{}
for key, value := range tpl.IndexTemplate.Meta_ {
metadata[key] = value
}
// pre fill components
modify.ComposedOf(tpl.IndexTemplate.ComposedOf...)
if conf.Stream {
modify.DataStream(esdsl.NewDataStreamVisibility())
if conf.Retention != "" {
maps.Lifecycle(
esdsl.NewDataStreamLifecycle().
DataRetention(
esdsl.NewDuration().String(conf.Retention)))
}
}
modify.AllowAutoCreate(conf.AutoCreate)
if conf.Mode != "" {
if !slices.Contains([]string{"standard", "timeseries", "logsdb", "lookup"}, conf.Mode) {
return errors.New("mode must be one of: standard, timeseries, logsdb or lookup")
}
*settings.Mode = conf.Mode
}
if len(mappings) > 0 {
typemaps := esdsl.NewTypeMapping()
for _, mapping := range mappings {
parts := strings.Split(mapping, ":")
if len(parts) != 2 {
return fmt.Errorf(
"invalid mapping %s, expect <name:type> (type: integer, text, date, keyword)",
mapping)
}
switch parts[1] {
case "text":
typemaps.AddProperty(parts[0], esdsl.NewTextProperty())
case "integer":
typemaps.AddProperty(parts[0], esdsl.NewIntegerNumberProperty())
case "date":
typemaps.AddProperty(parts[0], esdsl.NewDateProperty())
case "keyword":
typemaps.AddProperty(parts[0], esdsl.NewKeywordProperty())
}
}
maps.Mappings(typemaps)
}
if len(conf.Components) > 0 {
modify.ComposedOf(conf.Components...)
}
for _, alias := range conf.Aliases {
maps.AddAlias(alias, esdsl.NewAlias())
}
if len(conf.Meta) > 0 {
for _, meta := range conf.Meta {
parts := strings.Split(meta, ":")
if len(parts) != 2 {
return errors.New("meta data must be in the form key:value")
}
msg, err := json.Marshal(parts[1])
if err != nil {
return fmt.Errorf("failed to json marshal metadata %s: %w", meta, err)
}
metadata[parts[0]] = msg
}
}
modify.Meta_(esdsl.NewMetadata(metadata))
if len(conf.Settings) > 0 {
usersettings := map[string]json.RawMessage{}
for _, meta := range conf.Settings {
parts := strings.Split(meta, ":")
if len(parts) != 2 {
return errors.New("settings data must be in the form key:value")
}
msg, err := json.Marshal(parts[1])
if err != nil {
return fmt.Errorf("failed to json marshal metadata %s: %w", meta, err)
}
usersettings[parts[0]] = msg
}
settings.IndexSettings = usersettings
}
patterns := tpl.IndexTemplate.IndexPatterns
if len(conf.Patterns) > 0 {
patterns = conf.Patterns
}
maps.Settings(settings)
modify.Template(maps)
modify.IndexPatterns(patterns...)
_, err = modify.Do(context.Background())
if err != nil {
return fmt.Errorf("failed to modify index template: %s", esErrorString(err))
}
return nil
}
func IndexTemplateDelete(conf *cfg.Config, name string) error {
_, err := conf.DefaultCluster.ES.Indices.DeleteIndexTemplate(name).
Header("content-type", "application/json").
Header("accept", "application/json").
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to delete index template: %s", esErrorString(err))
}
return nil
}

View File

@@ -0,0 +1,61 @@
package es
import (
"encoding/json"
"fmt"
"codeberg.org/scip/esctl/pkg/cfg"
"codeberg.org/scip/esctl/pkg/printer"
)
type TplTemplateData struct {
Settings map[string]any `json:"settings"`
Aliases map[string]any `json:"aliases"`
Lifecycle map[string]any `json:"lifecycle"`
Mappings map[string]any `json:"mappings"`
}
type TplTemplate struct {
Template TplTemplateData `json:"template"`
}
type TplTpl struct {
IndexTemplate TplTemplate `json:"index_template"`
}
type Tpl struct {
IndexTemplates []TplTpl `json:"index_templates"`
}
func getIndexTemplateSettings(conf *cfg.Config, tplname string, table *printer.Table) error {
raw, err := CallAPI(conf, "GET", "/_index_template/"+tplname, "")
if err != nil {
return err
}
data := Tpl{}
if err = json.Unmarshal(raw, &data); err != nil {
return err
}
if conf.Debug {
if err := prettyfiJson(conf, raw); err != nil {
return err
}
}
if len(data.IndexTemplates) == 0 {
return fmt.Errorf("index_template %s not found", tplname)
}
tpl := data.IndexTemplates[0].IndexTemplate.Template.Settings
for topic, val := range tpl {
paths := getJsonPath(map[string]string{}, val.(map[string]any), topic)
for setting, value := range paths {
table.Entries = append(table.Entries, []string{setting, fmt.Sprintf("%v", value)})
}
}
return nil
}

View File

@@ -110,12 +110,14 @@ func Repl(conf *cfg.Config) error {
fmt.Println(err)
}
err = CallAPI(conf, parts[0], parts[1], data)
raw, err := CallAPI(conf, parts[0], parts[1], data)
if err != nil {
fmt.Printf("failed to call API: %s\n", esErrorString(err))
}
reader.SetPrompt("> ")
if err := prettyfiJson(conf, raw); err != nil {
fmt.Println(err)
}
}
return nil
@@ -125,7 +127,7 @@ func encodeAuth(username, password string) string {
return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
}
func CallAPI(conf *cfg.Config, verb, path, data string) error {
func CallAPI(conf *cfg.Config, verb, path, data string) ([]byte, error) {
verb = strings.ToUpper(verb)
// we're using port-forwards anyway
@@ -137,7 +139,7 @@ func CallAPI(conf *cfg.Config, verb, path, data string) error {
req, err := http.NewRequest(verb, conf.DefaultCluster.Uri+path, bytes.NewBuffer([]byte(data)))
if err != nil {
return err
return nil, err
}
req.Header.Add("Content-Type", "application/json")
@@ -147,16 +149,16 @@ func CallAPI(conf *cfg.Config, verb, path, data string) error {
// actually execute the request
resp, err := client.Do(req)
if err != nil {
return err
return nil, err
}
// Read and print response
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %s", err)
return nil, fmt.Errorf("failed to read response body: %s", err)
}
return prettyfiJson(conf, body)
return body, nil
}
func prettyfiJson(conf *cfg.Config, raw []byte) error {