diff --git a/README.md b/README.md
index 2c9e6d9..2512a92 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/cmd/cluster_settings.go b/cmd/cluster_settings.go
index f7d8bd9..b394816 100644
--- a/cmd/cluster_settings.go
+++ b/cmd/cluster_settings.go
@@ -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{
diff --git a/cmd/index.go b/cmd/index.go
index 8b40687..60b2de7 100644
--- a/cmd/index.go
+++ b/cmd/index.go
@@ -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,
},
},
diff --git a/cmd/index_template.go b/cmd/index_template.go
new file mode 100644
index 0000000..96b6084
--- /dev/null
+++ b/cmd/index_template.go
@@ -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 .
+*/
+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 + ` ...`,
+ 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)
+ },
+ }
+}
diff --git a/cmd/search.go b/cmd/search.go
index 2ebfc94..0f33cd7 100644
--- a/cmd/search.go
+++ b/cmd/search.go
@@ -25,7 +25,7 @@ import (
"github.com/urfave/cli/v3"
)
-const SearchUsage = ` might be one of:
+const SearchUsage = `field 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]pattern> ...]\n" + SearchUsage,
+ Name: "search",
+ Aliases: []string{"/"},
+ Usage: "search within an index",
+ UsageText: "search [options] [<[field]pattern> ...]\n",
+ CustomHelpTemplate: addReference(SearchUsage),
Flags: []cli.Flag{
&cli.StringFlag{
diff --git a/cmd/utilities.go b/cmd/utilities.go
new file mode 100644
index 0000000..85702eb
--- /dev/null
+++ b/cmd/utilities.go
@@ -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 .
+*/
+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"))
+}
diff --git a/go.mod b/go.mod
index 9e38700..3945ec7 100644
--- a/go.mod
+++ b/go.mod
@@ -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
)
diff --git a/go.sum b/go.sum
index 973f050..61994e5 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
diff --git a/pkg/cfg/config.go b/pkg/cfg/config.go
index 29228bb..bb80b87 100644
--- a/pkg/cfg/config.go
+++ b/pkg/cfg/config.go
@@ -48,19 +48,31 @@ type Cluster struct {
}
type Config struct {
- ConfigFile string // -c
- CurrentCluster string // -C
- Debug bool // -d
- Output string // -o
- Clusters map[string]*Cluster
- DefaultCluster *Cluster
- Index string // index: -i
- Failed, Partials bool // index: flags
- Shards, Replicas int // index create+allocation: -s -r
- Wait bool // index create: -w
- Primary bool // index allocation: -p
- Searchable bool // index fields: -s
- Aggretable bool // index fields: -a
+ ConfigFile string // -c
+ CurrentCluster string // -C
+ Debug bool // -d
+ Output string // -o
+ Clusters map[string]*Cluster
+ DefaultCluster *Cluster
+ Index string // index: -i
+ Failed, Partials bool // index: flags
+ Shards, Replicas int // index create+allocation: -s -r
+ Wait bool // index create: -w
+ 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
diff --git a/pkg/es/cluster_settings.go b/pkg/es/cluster_settings.go
index e27fb91..4b0b5cd 100644
--- a/pkg/es/cluster_settings.go
+++ b/pkg/es/cluster_settings.go
@@ -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 {
diff --git a/pkg/es/cluster_util.go b/pkg/es/cluster_util.go
index 019920c..35a6697 100644
--- a/pkg/es/cluster_util.go
+++ b/pkg/es/cluster_util.go
@@ -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
+}
diff --git a/pkg/es/index.go b/pkg/es/index.go
index 0aace00..a14bc45 100644
--- a/pkg/es/index.go
+++ b/pkg/es/index.go
@@ -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 {
diff --git a/pkg/es/index_template.go b/pkg/es/index_template.go
new file mode 100644
index 0000000..cd6e187
--- /dev/null
+++ b/pkg/es/index_template.go
@@ -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 .
+*/
+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 (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 (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
+}
diff --git a/pkg/es/index_template_settings.go b/pkg/es/index_template_settings.go
new file mode 100644
index 0000000..5923e83
--- /dev/null
+++ b/pkg/es/index_template_settings.go
@@ -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
+}
diff --git a/pkg/es/repl.go b/pkg/es/repl.go
index ab30120..2d96ef7 100644
--- a/pkg/es/repl.go
+++ b/pkg/es/repl.go
@@ -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 {