Skip to content

Commit b855d2c

Browse files
feat: Add modify command and pipeline tag generator (#3213)
- Add new modify command to provide a facility for making arbitrary changes to packages - Add pipeline-tag modifier to generate missing ingest pipeline processor tags - Add yamledit package to provide functions for modifying yaml documents - Add fleetpkg package to provide types and functions for fleet package types and to support modifying package yaml files via yamledit.
1 parent ba9289e commit b855d2c

File tree

16 files changed

+2324
-1
lines changed

16 files changed

+2324
-1
lines changed

‎README.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,17 @@ Use this command to validate the contents of a package using the package specifi
439439

440440
The command ensures that the package is aligned with the package spec and the README file is up-to-date with its template (if present).
441441

442+
### `elastic-package modify`
443+
444+
_Context: package_
445+
446+
Use this command to apply modifications to a package.
447+
448+
These modifications can range from applying best practices, generating ingest pipeline tags, and more. Run this command without any arguments to see a list of modifiers.
449+
450+
Use --modifiers to specify which modifiers to run, separated by commas.
451+
452+
442453
### `elastic-package profiles`
443454

444455
_Context: global_

‎cmd/modify.go‎

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package cmd
6+
7+
import (
8+
"fmt"
9+
"io"
10+
"sort"
11+
"text/tabwriter"
12+
13+
"github.com/spf13/cobra"
14+
"github.com/spf13/pflag"
15+
16+
"github.com/elastic/elastic-package/internal/cobraext"
17+
"github.com/elastic/elastic-package/internal/fleetpkg"
18+
"github.com/elastic/elastic-package/internal/modify"
19+
"github.com/elastic/elastic-package/internal/modify/pipelinetag"
20+
"github.com/elastic/elastic-package/internal/packages"
21+
)
22+
23+
const modifyLongDescription = `Use this command to apply modifications to a package.
24+
25+
These modifications can range from applying best practices, generating ingest pipeline tags, and more. Run this command without any arguments to see a list of modifiers.
26+
27+
Use --modifiers to specify which modifiers to run, separated by commas.
28+
`
29+
30+
func setupModifyCommand() *cobraext.Command {
31+
modifiers := []*modify.Modifier{
32+
pipelinetag.Modifier,
33+
}
34+
sort.Slice(modifiers, func(i, j int) bool {
35+
return modifiers[i].Name < modifiers[j].Name
36+
})
37+
38+
validModifier := func(name string) bool {
39+
for _, modifier := range modifiers {
40+
if modifier.Name == name {
41+
return true
42+
}
43+
}
44+
45+
return false
46+
}
47+
48+
listModifiers := func(w io.Writer) {
49+
tw := tabwriter.NewWriter(w, 0, 2, 3, ' ', 0)
50+
for _, a := range modifiers {
51+
_, _ = fmt.Fprintf(tw, "%s\t%s\n", a.Name, a.Doc)
52+
}
53+
_ = tw.Flush()
54+
_, _ = fmt.Fprintln(w, "")
55+
}
56+
57+
cmd := &cobra.Command{
58+
Use: "modify",
59+
Short: "Modify package assets",
60+
Long: modifyLongDescription,
61+
RunE: func(cmd *cobra.Command, args []string) error {
62+
cmd.Println("Modify package assets")
63+
64+
selectedModifiers, err := cmd.Flags().GetStringSlice("modifiers")
65+
if err != nil {
66+
return cobraext.FlagParsingError(err, "modifiers")
67+
}
68+
if len(selectedModifiers) == 0 {
69+
_, _ = fmt.Fprint(cmd.OutOrStderr(), "Please provide at least one modifier:\n\n")
70+
listModifiers(cmd.OutOrStderr())
71+
return nil
72+
}
73+
for _, selected := range selectedModifiers {
74+
if !validModifier(selected) {
75+
_, _ = fmt.Fprint(cmd.OutOrStderr(), "Please provide at a valid modifier:\n\n")
76+
listModifiers(cmd.OutOrStderr())
77+
return cobraext.FlagParsingError(fmt.Errorf("invalid modifier: %q", selected), "modifiers")
78+
}
79+
}
80+
81+
pkgRootPath, err := packages.FindPackageRoot()
82+
if err != nil {
83+
return fmt.Errorf("locating package root failed: %w", err)
84+
}
85+
86+
for _, modifier := range modifiers {
87+
pkg, err := fleetpkg.Load(pkgRootPath)
88+
if err != nil {
89+
return fmt.Errorf("failed to load package from %q: %w", pkgRootPath, err)
90+
}
91+
if err = modifier.Run(pkg); err != nil {
92+
return fmt.Errorf("failed to apply modifier %q: %w", modifier.Name, err)
93+
}
94+
}
95+
96+
return nil
97+
},
98+
}
99+
100+
cmd.PersistentFlags().StringSliceP("modifiers", "m", nil, "List of modifiers to run, separated by commas")
101+
102+
for _, m := range modifiers {
103+
prefix := m.Name + "."
104+
105+
m.Flags.VisitAll(func(f *pflag.Flag) {
106+
name := prefix + f.Name
107+
cmd.Flags().Var(f.Value, name, f.Usage)
108+
})
109+
}
110+
111+
return cobraext.NewCommand(cmd, cobraext.ContextPackage)
112+
}

‎cmd/root.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ var commands = []*cobraext.Command{
3232
setupInstallCommand(),
3333
setupLinksCommand(),
3434
setupLintCommand(),
35+
setupModifyCommand(),
3536
setupProfilesCommand(),
3637
setupReportsCommand(),
3738
setupServiceCommand(),

‎go.mod‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ require (
2222
github.com/fatih/color v1.18.0
2323
github.com/go-viper/mapstructure/v2 v2.5.0
2424
github.com/gobwas/glob v0.2.3
25+
github.com/goccy/go-yaml v1.18.0
2526
github.com/google/go-cmp v0.7.0
2627
github.com/google/go-github/v32 v32.1.0
2728
github.com/google/go-querystring v1.2.0
@@ -35,6 +36,7 @@ require (
3536
github.com/rogpeppe/go-internal v1.14.1
3637
github.com/shirou/gopsutil/v3 v3.24.5
3738
github.com/spf13/cobra v1.10.2
39+
github.com/spf13/pflag v1.0.10
3840
github.com/stretchr/testify v1.11.1
3941
go.yaml.in/yaml/v2 v2.4.3
4042
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
@@ -144,7 +146,6 @@ require (
144146
github.com/shopspring/decimal v1.4.0 // indirect
145147
github.com/spf13/afero v1.15.0 // indirect
146148
github.com/spf13/cast v1.7.0 // indirect
147-
github.com/spf13/pflag v1.0.10 // indirect
148149
github.com/stretchr/objx v0.5.2 // indirect
149150
github.com/tklauser/go-sysconf v0.3.12 // indirect
150151
github.com/tklauser/numcpus v0.6.1 // indirect

‎go.sum‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPE
144144
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
145145
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
146146
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
147+
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
148+
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
147149
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
148150
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
149151
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=

‎internal/fleetpkg/fleetpkg.go‎

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package fleetpkg
6+
7+
import (
8+
"encoding/json"
9+
10+
"github.com/goccy/go-yaml"
11+
"github.com/goccy/go-yaml/ast"
12+
13+
"github.com/elastic/elastic-package/internal/yamledit"
14+
)
15+
16+
// Package is a fleet package.
17+
type Package struct {
18+
Manifest Manifest
19+
Input *DataStream
20+
DataStreams map[string]*DataStream
21+
22+
sourceDir string
23+
}
24+
25+
// Path is the path to the root of the package.
26+
func (i *Package) Path() string {
27+
return i.sourceDir
28+
}
29+
30+
// Manifest is the package manifest.
31+
type Manifest struct {
32+
Name string `yaml:"name"`
33+
Title string `yaml:"title"`
34+
Version string `yaml:"version"`
35+
Description string `yaml:"description"`
36+
Type string `yaml:"type"`
37+
FormatVersion string `yaml:"format_version"`
38+
Owner struct {
39+
Github string `yaml:"github"`
40+
Type string `yaml:"type"`
41+
} `yaml:"owner"`
42+
43+
Doc *yamledit.Document `yaml:"-"`
44+
}
45+
46+
// Path is the path to the manifest file.
47+
func (m *Manifest) Path() string {
48+
return m.Doc.Filename()
49+
}
50+
51+
// DataStreamManifest is the data stream manifest file.
52+
type DataStreamManifest struct {
53+
Title string `yaml:"title"`
54+
Type string `yaml:"type"`
55+
56+
Doc *yamledit.Document `yaml:"-"`
57+
}
58+
59+
// Path is the path to the manifest file.
60+
func (m *DataStreamManifest) Path() string {
61+
return m.Doc.Filename()
62+
}
63+
64+
// DataStream is a data stream within the package.
65+
type DataStream struct {
66+
Manifest DataStreamManifest
67+
Pipelines map[string]*Pipeline
68+
69+
sourceDir string
70+
}
71+
72+
// Path is the path to the data stream.
73+
func (d *DataStream) Path() string {
74+
return d.sourceDir
75+
}
76+
77+
// Pipeline is an ingest pipeline.
78+
type Pipeline struct {
79+
Description string `yaml:"description"`
80+
Processors []*Processor `yaml:"processors,omitempty"`
81+
OnFailure []*Processor `yaml:"on_failure,omitempty"`
82+
83+
Doc *yamledit.Document `yaml:"-"`
84+
}
85+
86+
// Path is the path to the pipeline.
87+
func (p *Pipeline) Path() string {
88+
return p.Doc.Filename()
89+
}
90+
91+
// Processor is an ingest pipeline processor.
92+
type Processor struct {
93+
Type string
94+
Attributes map[string]any
95+
OnFailure []*Processor
96+
97+
Node ast.Node
98+
}
99+
100+
// GetAttribute gets an attribute of the processor.
101+
func (p *Processor) GetAttribute(key string) (any, bool) {
102+
v, ok := p.Attributes[key]
103+
if !ok {
104+
return nil, false
105+
}
106+
107+
return v, true
108+
}
109+
110+
// GetAttributeString gets a string attribute of the processor.
111+
func (p *Processor) GetAttributeString(key string) (string, bool) {
112+
v, ok := p.Attributes[key].(string)
113+
if !ok {
114+
return "", false
115+
}
116+
117+
return v, true
118+
}
119+
120+
// GetAttributeFloat gets a float attribute of the processor.
121+
func (p *Processor) GetAttributeFloat(key string) (float64, bool) {
122+
v, ok := p.Attributes[key].(float64)
123+
if !ok {
124+
return 0, false
125+
}
126+
127+
return v, true
128+
}
129+
130+
// GetAttributeInt gets an int attribute of the processor.
131+
func (p *Processor) GetAttributeInt(key string) (int, bool) {
132+
v, ok := p.Attributes[key].(int)
133+
if !ok {
134+
return 0, false
135+
}
136+
137+
return v, true
138+
}
139+
140+
// GetAttributeBool gets a bool attribute of the processor.
141+
func (p *Processor) GetAttributeBool(key string) (bool, bool) {
142+
v, ok := p.Attributes[key].(bool)
143+
if !ok {
144+
return false, false
145+
}
146+
147+
return v, true
148+
}
149+
150+
// UnmarshalYAML implements a YAML unmarshaler.
151+
func (p *Processor) UnmarshalYAML(node ast.Node) error {
152+
var procMap map[string]struct {
153+
Attributes map[string]any `yaml:",inline"`
154+
OnFailure []*Processor `yaml:"on_failure"`
155+
}
156+
if err := yaml.NodeToValue(node, &procMap); err != nil {
157+
return err
158+
}
159+
160+
// The struct representation used here is much more convenient
161+
// to work with than the original map of map format.
162+
for k, v := range procMap {
163+
p.Type = k
164+
p.Attributes = v.Attributes
165+
p.OnFailure = v.OnFailure
166+
167+
delete(p.Attributes, "on_failure")
168+
169+
break
170+
}
171+
172+
p.Node = node
173+
174+
return nil
175+
}
176+
177+
// MarshalJSON implements a JSON marshaler.
178+
func (p *Processor) MarshalJSON() ([]byte, error) {
179+
properties := make(map[string]any, len(p.Attributes)+1)
180+
for k, v := range p.Attributes {
181+
properties[k] = v
182+
}
183+
if len(p.OnFailure) > 0 {
184+
properties["on_failure"] = p.OnFailure
185+
}
186+
return json.Marshal(map[string]any{
187+
p.Type: properties,
188+
})
189+
}
190+
191+
// Validation is the validation.yml file of a package.
192+
type Validation struct {
193+
Errors struct {
194+
ExcludeChecks []string `yaml:"exclude_checks,omitempty"`
195+
} `yaml:"errors,omitempty"`
196+
197+
DocsStructureEnforced struct {
198+
Enabled bool `yaml:"enabled"`
199+
Version int `yaml:"version"`
200+
Skip []struct {
201+
Title string `yaml:"title"`
202+
Reason string `yaml:"reason"`
203+
} `yaml:"skip,omitempty"`
204+
} `yaml:"docs_structure_enforced"`
205+
206+
Doc *yamledit.Document `yaml:"-"`
207+
}

0 commit comments

Comments
 (0)