Skip to content

Commit 1a58614

Browse files
committed
Makes addon generation more generic, adds more tests, and allows for
multiple addons to be included in splunk startup
1 parent 6005fb5 commit 1a58614

File tree

20 files changed

+443
-195
lines changed

20 files changed

+443
-195
lines changed

packaging/technical-addon/cmd/modinput_config_generator/README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,17 @@ func main() {
105105
}
106106
```
107107

108-
Alternatively, if you need platform specific code, you may use `//go:build` flags, providing a file for every needed variation of code.
108+
Alternatively, if you need platform specific code, you may use `//go:build` flags, providing a file for every needed variation of code.
109+
110+
# Debugging a container from testcommon.StartSplunk
111+
1. Change `autoremove` to false in the hostmodifierconfig
112+
2. Likely add a time.Sleep before the require.NoError check but after the container start
113+
3. In a terminal, run `docker container ls --all`
114+
4. Run `docker exec -it <container-id> /bin/bash`
115+
5. Inspect/debug a TA as normal, ex looking into `/opt/splunk/etc/apps/Sample_Addon` or `/opt/splunk/var/log/splunkd.log`
116+
117+
To get the modular input in XML form, you can use the following command, replacing `Sample_Addon` with the addon name of your choice
118+
119+
```bash
120+
/opt/splunk/bin/splunk cmd splunkd print-modinput-config Sample_Addon Sample_Addon://Sample_Addon
121+
```

packaging/technical-addon/cmd/modinput_config_generator/generator.go

Lines changed: 66 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package main
1616

1717
import (
1818
"fmt"
19+
"io/fs"
1920
"log"
2021
"os"
2122
"path/filepath"
@@ -28,25 +29,44 @@ import (
2829

2930
const generatedFileName = "modinput_config.go"
3031

31-
const configTemplate = `// Code generated by runner_config_generator; DO NOT EDIT.
32+
const configTemplate = `// Code generated by modinput_config_generator; DO NOT EDIT.
3233
package main
3334
3435
import (
3536
"github.com/splunk/splunk-technical-addon/internal/modularinput"
3637
)
3738
38-
type {{ toPascal .SchemaName }}ModularInput struct {
39-
SchemaName string
40-
ModularInputs map[string]modularinput.ModInput
39+
const SchemaName = "{{ $.SchemaName }}";
40+
41+
type {{ toPascal .SchemaName }}ModInput struct {
42+
Value string
43+
Name string
44+
}
45+
46+
type {{ toPascal .SchemaName }}ModularInputs struct {
47+
{{- range $name, $inputConfig := .ModularInputs }}
48+
{{ toPascal $name }} {{ toPascal $.SchemaName }}ModInput
49+
{{- end }}
50+
}
51+
52+
func Get{{ toPascal .SchemaName }}ModularInputs(mip *modularinput.ModinputProcessor) *{{ toPascal .SchemaName }}ModularInputs {
53+
return &{{ toPascal .SchemaName }}ModularInputs{
54+
{{- range $name, $inputConfig := .ModularInputs }}
55+
{{ toPascal $name }}: {{ toPascal $.SchemaName }}ModInput{
56+
Value: mip.ModularInputs["{{ $name }}"].Value,
57+
Name: "{{ $name }}",
58+
},
59+
{{- end }}
60+
}
4161
}
4262
4363
// GetDefault{{ toPascal .SchemaName }}ModularInputs returns the embedded modular input configuration
44-
func GetDefault{{ toPascal .SchemaName }}ModularInputs() {{ toPascal .SchemaName }}ModularInput {
45-
return {{ toPascal .SchemaName }}ModularInput{
64+
func GetDefault{{ toPascal .SchemaName }}ModularInputs() modularinput.GenericModularInput {
65+
return modularinput.GenericModularInput{
4666
SchemaName: "{{ $.SchemaName}}",
47-
ModularInputs: map[string]modularinput.ModInput{
67+
ModularInputs: map[string]*modularinput.ModInput{
4868
{{- range $name, $inputConfig := .ModularInputs }}
49-
"{{ $name }}": modularinput.ModInput{
69+
"{{ $name }}": &modularinput.ModInput{
5070
Config: {{ printf "%#v" $inputConfig }},
5171
{{- if $inputConfig.Default }}
5272
Value: "{{ $inputConfig.Default }}",
@@ -81,45 +101,54 @@ func generateModinputConfig(config *modularinput.TemplateData, outDir string) er
81101
}
82102
defer outputFile.Close()
83103

84-
if err := tmpl.Execute(outputFile, config); err != nil {
104+
if err = tmpl.Execute(outputFile, config); err != nil {
85105
return fmt.Errorf("failed to execute template: %w", err)
86106
}
87107

88108
log.Printf("Generated code: %s\n", outputPath)
89109
return nil
90110
}
91111

92-
// Given a yaml file specifying the modular inputs and a .tmpl file, generates a new inputs.conf.spec file and inputs.conf file
93-
func generateTaModInputConfs(config *modularinput.TemplateData, addonSourceDir string, outDir string) error {
94-
// handle README/inputs.conf.spec
95-
specTemplatePath := filepath.Join(addonSourceDir, "assets", "inputs.conf.spec.tmpl")
96-
if _, err := os.Stat(specTemplatePath); os.IsNotExist(err) {
97-
return fmt.Errorf("template file not found: %s", specTemplatePath)
98-
}
99-
specOutputDir := filepath.Join(outDir, "README")
100-
if err := os.MkdirAll(specOutputDir, 0755); err != nil {
101-
return fmt.Errorf("failed to create output directory: %w", err)
102-
}
103-
specOutputPath := filepath.Join(specOutputDir, "inputs.conf.spec")
104-
if err := modularinput.RenderTemplate(specTemplatePath, specOutputPath, config); err != nil {
105-
return err
106-
}
107-
log.Printf("Generated: %s\n", specOutputPath)
108-
109-
// Handle default/inputs.conf
110-
defaultConfTemplatePath := filepath.Join(addonSourceDir, "assets", "inputs.conf.tmpl")
111-
if _, err := os.Stat(defaultConfTemplatePath); os.IsNotExist(err) {
112-
return fmt.Errorf("template file not found: %s", defaultConfTemplatePath)
112+
// Given a yaml file specifying the modular inputs, renders all templates (.tmpl) under assets directory to same relative
113+
// file tree in addon
114+
func generateTaModInputConfs(config *modularinput.TemplateData, addonSourceDir string, buildDir string) error {
115+
assetsDir := filepath.Join(addonSourceDir, "assets")
116+
templateSuffix := ".tmpl"
117+
outDir := filepath.Join(buildDir, config.SchemaName)
118+
err := os.MkdirAll(outDir, 0755)
119+
if err != nil {
120+
return fmt.Errorf("failed create output dir %s: %w", outDir, err)
113121
}
114-
defaultConfOutputDir := filepath.Join(outDir, "default")
115-
if err := os.MkdirAll(defaultConfOutputDir, 0755); err != nil {
116-
return fmt.Errorf("failed to create output directory: %w", err)
122+
err = os.CopyFS(outDir, os.DirFS(assetsDir))
123+
if err != nil {
124+
return fmt.Errorf("failed to copy from %s to %s: %w", assetsDir, outDir, err)
117125
}
118-
defaultConfOutputPath := filepath.Join(defaultConfOutputDir, "inputs.conf")
119-
if err := modularinput.RenderTemplate(defaultConfTemplatePath, defaultConfOutputPath, config); err != nil {
120-
return err
126+
err = filepath.WalkDir(outDir, func(path string, d fs.DirEntry, err error) error {
127+
if err != nil {
128+
return err
129+
}
130+
if !d.IsDir() && strings.HasSuffix(d.Name(), templateSuffix) {
131+
// Render new file to same path, chopping off the .tmpl suffix
132+
relPath, err := filepath.Rel(outDir, path)
133+
if err != nil {
134+
return err
135+
}
136+
outputPath := filepath.Join(outDir, filepath.Dir(relPath), d.Name()[:len(d.Name())-len(templateSuffix)])
137+
if err = modularinput.RenderTemplate(path, outputPath, config); err != nil {
138+
return fmt.Errorf("could not render template: %w", err)
139+
}
140+
log.Printf("Generated template: %s\n", outputPath)
141+
err = os.Remove(path)
142+
if err != nil {
143+
return err
144+
}
145+
log.Printf("Removed template: %s\n", path)
146+
}
147+
return nil
148+
})
149+
if err != nil {
150+
return fmt.Errorf("failed to render contents from %s to %s: %w", assetsDir, outDir, err)
121151
}
122-
log.Printf("Generated: %s\n", defaultConfOutputPath)
123152

124153
return nil
125154
}

packaging/technical-addon/cmd/modinput_config_generator/generator_test.go

Lines changed: 29 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,32 @@ import (
1818
"bytes"
1919
"context"
2020
"encoding/json"
21-
"fmt"
2221
"io"
23-
"os"
2422
"path/filepath"
25-
"strings"
2623
"testing"
2724
"time"
2825

29-
"github.com/docker/docker/api/types/container"
30-
"github.com/docker/docker/api/types/mount"
31-
"github.com/google/go-cmp/cmp"
3226
"github.com/splunk/splunk-technical-addon/internal/packaging"
3327
"github.com/splunk/splunk-technical-addon/internal/testcommon"
3428
"github.com/stretchr/testify/assert"
3529
"github.com/stretchr/testify/require"
36-
"github.com/testcontainers/testcontainers-go"
3730
"github.com/testcontainers/testcontainers-go/wait"
38-
"go.uber.org/zap"
3931
)
4032

4133
type ExampleOutput struct {
34+
Flags []string
35+
EnvVars []string
36+
37+
SplunkHome string
38+
TaHome string
39+
PlatformHome string
40+
41+
EverythingSet string
42+
MinimalSet string
43+
MinimalSetRequired string
44+
UnaryFlagWithEverythingSet string
45+
4246
Platform string
43-
Flags []string
44-
EnvVars []string
4547
}
4648

4749
func TestPascalization(t *testing.T) {
@@ -78,11 +80,14 @@ func TestRunner(t *testing.T) {
7880
ctx := context.Background()
7981
addonPath := filepath.Join(t.TempDir(), "Sample_Addon.tgz")
8082

81-
buildDir := testcommon.GetBuildDir()
83+
buildDir := packaging.GetBuildDir()
8284
require.NotEmpty(t, buildDir)
8385
err := packaging.PackageAddon(filepath.Join(buildDir, "Sample_Addon"), addonPath)
8486
require.NoError(t, err)
85-
tc := startSplunk(t, addonPath)
87+
tc := testcommon.StartSplunk(t, testcommon.SplunkStartOpts{
88+
AddonPaths: []string{addonPath},
89+
WaitStrategy: wait.ForExec([]string{"sudo", "stat", "/opt/splunk/var/log/splunk/Sample_Addon.log"}).WithStartupTimeout(time.Minute * 4),
90+
})
8691

8792
// Check Schema
8893
code, output, err := tc.Exec(ctx, []string{"sudo", "/opt/splunk/bin/splunk", "btool", "check", "--debug"})
@@ -98,10 +103,12 @@ func TestRunner(t *testing.T) {
98103
require.NoError(t, err)
99104
read, err = io.ReadAll(output)
100105
assert.NoError(t, err)
101-
expectedJSON := `{"Flags":["--test-flag","$SPLUNK_OTEL_TA_HOME/local/access_token","--test-flag"],"EnvVars":["EVERYTHING_SET=$SPLUNK_OTEL_TA_HOME/local/access_token","UNARY_FLAG_WITH_EVERYTHING_SET=$SPLUNK_OTEL_TA_HOME/local/access_token"],"Platform":"linux"}`
106+
expectedJSON := `{"Flags":["--test-flag","/opt/splunk/etc/apps/Sample_Addon/local/access_token","--test-flag"],"EnvVars":["EVERYTHING_SET=/opt/splunk/etc/apps/Sample_Addon/local/access_token","UNARY_FLAG_WITH_EVERYTHING_SET=/opt/splunk/etc/apps/Sample_Addon/local/access_token"], "SplunkHome":"/opt/splunk/etc", "TaHome":"/opt/splunk/etc/apps/Sample_Addon", "PlatformHome":"/opt/splunk/etc/apps/Sample_Addon/linux_x86_64", "EverythingSet":"/opt/splunk/etc/apps/Sample_Addon/local/access_token", "MinimalSet":"", "MinimalSetRequired":"", "UnaryFlagWithEverythingSet":"/opt/splunk/etc/apps/Sample_Addon/local/access_token","Platform":"linux"}`
102107
i := bytes.Index(read, []byte("Sample output:"))
103108
unmarshalled := &ExampleOutput{}
104-
require.NoError(t, json.Unmarshal(read[i+len("Sample output:"):], unmarshalled))
109+
dec := json.NewDecoder(bytes.NewReader(read[i+len("Sample output:"):]))
110+
dec.DisallowUnknownFields()
111+
require.NoError(t, dec.Decode(unmarshalled))
105112
expected := &ExampleOutput{}
106113
require.NoError(t, json.Unmarshal([]byte(expectedJSON), expected))
107114
assert.EqualValues(t, expected, unmarshalled)
@@ -110,7 +117,7 @@ func TestRunner(t *testing.T) {
110117
}
111118

112119
func TestRunnerConfigGeneration(t *testing.T) {
113-
sourceDir, err := testcommon.GetSourceDir()
120+
sourceDir, err := packaging.GetSourceDir()
114121
require.NoError(t, err)
115122
sourceDir = filepath.Join(sourceDir, "cmd", "modinput_config_generator", "internal", "testdata")
116123
tests := []struct {
@@ -138,102 +145,33 @@ func TestRunnerConfigGeneration(t *testing.T) {
138145
}
139146

140147
func TestInputsConfGeneration(t *testing.T) {
141-
sourceDir, err := testcommon.GetSourceDir()
148+
sourceDir, err := packaging.GetSourceDir()
142149
require.NoError(t, err)
143150
sourceDir = filepath.Join(sourceDir, "cmd", "modinput_config_generator", "internal", "testdata")
144151
tests := []struct {
145152
testSchemaName string
146153
sampleYamlPath string
147154
outDir string
148-
sourceDir string
155+
addonSourceDir string
149156
expectedSpecPath string
150157
shouldError bool
151158
}{
152159
{
153160
testSchemaName: "Sample_Addon",
154161
outDir: t.TempDir(),
155-
sourceDir: filepath.Join(sourceDir, "pkg/sample_addon"),
156-
sampleYamlPath: filepath.Join(sourceDir, "pkg/sample_addon/runner/modular-inputs.yaml"),
162+
addonSourceDir: filepath.Join(sourceDir, "pkg", "sample_addon"),
163+
sampleYamlPath: filepath.Join(sourceDir, "pkg", "sample_addon", "runner", "modular-inputs.yaml"),
157164
},
158165
}
159166

160167
for _, tc := range tests {
161168
t.Run(tc.testSchemaName, func(tt *testing.T) {
162169
config, err := loadYaml(tc.sampleYamlPath, tc.testSchemaName)
163170
assert.NoError(tt, err)
164-
err = generateTaModInputConfs(config, tc.sourceDir, tc.outDir)
171+
err = generateTaModInputConfs(config, tc.addonSourceDir, tc.outDir)
165172
assert.NoError(tt, err)
166-
assertFilesMatch(tt, filepath.Join("internal", "testdata", "pkg", "sample_addon", "expected", "inputs.conf"), filepath.Join(tc.outDir, "default", "inputs.conf"))
167-
assertFilesMatch(tt, filepath.Join("internal", "testdata", "pkg", "sample_addon", "expected", "inputs.conf.spec"), filepath.Join(tc.outDir, "README", "inputs.conf.spec"))
173+
testcommon.AssertFilesMatch(tt, filepath.Join("internal", "testdata", "pkg", "sample_addon", "expected", "inputs.conf"), filepath.Join(tc.outDir, tc.testSchemaName, "default", "inputs.conf"))
174+
testcommon.AssertFilesMatch(tt, filepath.Join("internal", "testdata", "pkg", "sample_addon", "expected", "inputs.conf.spec"), filepath.Join(tc.outDir, tc.testSchemaName, "README", "inputs.conf.spec"))
168175
})
169176
}
170177
}
171-
172-
func assertFilesMatch(tt *testing.T, expectedPath string, actualPath string) {
173-
require.FileExists(tt, actualPath)
174-
require.FileExists(tt, expectedPath)
175-
expected, err := os.ReadFile(expectedPath)
176-
if err != nil {
177-
tt.Fatalf("Failed to read expected file: %v", err)
178-
}
179-
180-
actual, err := os.ReadFile(actualPath)
181-
if err != nil {
182-
tt.Fatalf("Failed to read actual file: %v", err)
183-
}
184-
185-
if diff := cmp.Diff(string(expected), string(actual)); diff != "" {
186-
tt.Errorf("File contents mismatch (-expected +actual)\npaths: (%s, %s):\n%s", expectedPath, actualPath, diff)
187-
}
188-
}
189-
190-
func startSplunk(t *testing.T, taPath string) testcontainers.Container {
191-
logger, err := zap.NewProduction()
192-
if err != nil {
193-
panic(err)
194-
}
195-
conContext := context.Background()
196-
addonLocation := fmt.Sprintf("/tmp/local-tas/%v", filepath.Base(taPath))
197-
198-
req := testcontainers.ContainerRequest{
199-
Image: "splunk/splunk:9.1.2",
200-
HostConfigModifier: func(c *container.HostConfig) {
201-
c.NetworkMode = "host"
202-
c.Mounts = append(c.Mounts, mount.Mount{
203-
Source: filepath.Dir(taPath),
204-
Target: filepath.Dir(addonLocation),
205-
Type: mount.TypeBind,
206-
})
207-
},
208-
Env: map[string]string{
209-
"SPLUNK_START_ARGS": "--accept-license",
210-
"SPLUNK_PASSWORD": "Chang3d!",
211-
"SPLUNK_APPS_URL": addonLocation,
212-
},
213-
WaitingFor: wait.ForAll(
214-
wait.NewHTTPStrategy("/en-US/account/login").WithPort("8000"),
215-
wait.ForExec([]string{"sudo", "stat", "/opt/splunk/var/log/splunk/Sample_Addon.log"}),
216-
).WithDeadline(4*time.Minute + 20*time.Second).WithStartupTimeoutDefault(4 * time.Minute),
217-
LogConsumerCfg: &testcontainers.LogConsumerConfig{
218-
Consumers: []testcontainers.LogConsumer{&testLogConsumer{t: t}},
219-
},
220-
}
221-
222-
tc, err := testcontainers.GenericContainer(conContext, testcontainers.GenericContainerRequest{
223-
ContainerRequest: req,
224-
Started: true,
225-
})
226-
if err != nil {
227-
logger.Info("Error while creating container")
228-
panic(err)
229-
}
230-
return tc
231-
}
232-
233-
type testLogConsumer struct {
234-
t *testing.T
235-
}
236-
237-
func (l *testLogConsumer) Accept(log testcontainers.Log) {
238-
l.t.Log(log.LogType + ": " + strings.TrimSpace(string(log.Content)))
239-
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This addon is used both as an example for how to author addons, and additionally is used as a test for our framework of doing such.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
######################################################
2+
#
3+
# {{ .SchemaName }}
4+
#
5+
# Copyright (C) 2005-2020 Splunk Inc. All Rights Reserved.
6+
#
7+
######################################################
8+
9+
[install]
10+
state = enabled
11+
is_configured = false
12+
build = 0
13+
14+
[ui]
15+
is_visible = false
16+
label = Sample Addon for testing
17+
18+
[launcher]
19+
author = Splunk, Inc.
20+
description = Use this add on as a reference/example for creating your own and to test the generation logic
21+
version = {{ .Version }}
22+
23+
[package]
24+
id = {{ .SchemaName }}
25+
26+
[id]
27+
name = {{ .SchemaName }}
28+
version = {{ .Version }}

0 commit comments

Comments
 (0)