Skip to content

Commit

Permalink
Add otelcol-config (#1607)
Browse files Browse the repository at this point in the history
Add otelcol-config

This commit adds otelcol-config, a tool designed to be used for
configuration otelcol-sumo installations.

The tool will mainly write values to sumologic.yaml, and link and
unlink files in conf.d.

Signed-off-by: Eric Chlebek <echlebek@sumologic.com>
  • Loading branch information
echlebek authored Jul 9, 2024
1 parent ed644b1 commit 9214063
Show file tree
Hide file tree
Showing 22 changed files with 1,345 additions and 7 deletions.
7 changes: 1 addition & 6 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,11 @@ run:
linters-settings:
errcheck:
check-type-assertions: false
check-blank: true
check-blank: false

gosimple:
go: "1.21"

maligned:
# print struct with more effective memory layout or not, false by default
suggest-new: true

unused:
go: "1.21"

Expand All @@ -37,7 +33,6 @@ linters:
- ineffassign
- typecheck
- unused
- gosimple
- staticcheck

issues:
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
GOLANGCI_LINT_VERSION ?= v1.55.2
GOLANGCI_LINT_VERSION ?= v1.59.1
PRETTIER_VERSION ?= 3.0.3
TOWNCRIER_VERSION ?= 23.6.0
SHELL := /usr/bin/env bash
Expand Down
1 change: 1 addition & 0 deletions pkg/tools/otelcol-config/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include ../../Makefile.Common
9 changes: 9 additions & 0 deletions pkg/tools/otelcol-config/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# otelcol-config

`otelcol-config` manipulates files in /etc/otelcol-config/conf.d (or in a
user-specified config directory).

It is used by the install.sh script to configure the collector for first-time
use, and also to adjust the collector configuration after installation.

Run `otelcol-config --help` for usage information.
16 changes: 16 additions & 0 deletions pkg/tools/otelcol-config/action_context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package main

import (
"io"
"io/fs"
)

// actionContext provides an abstraction of I/O for all actions
type actionContext struct {
ConfigDir fs.FS
Flags *flagValues
Stdout io.Writer
Stderr io.Writer
WriteConfD func([]byte) error
WriteConfDOverrides func([]byte) error
}
78 changes: 78 additions & 0 deletions pkg/tools/otelcol-config/action_context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package main

import (
"bytes"
"errors"
"fmt"
"io"
"io/fs"
"testing"
)

func discardWriter([]byte) error {
return nil
}

type errWriter struct {
}

func (errWriter) Write([]byte) error {
return errors.New("writer called")
}

func makeTestActionContext(t testing.TB,
confD fs.FS,
flags []string,
stdout, stderr io.Writer,
writeConfD, writeConfDOverrides func([]byte) error) *actionContext {

t.Helper()

flagValues := newFlagValues()
fs := makeFlagSet(flagValues)
if err := fs.Parse(flags); err != nil {
t.Fatal(err)
}

if stdout == nil {
stdout = io.Discard
}

if stderr == nil {
stderr = io.Discard
}

if writeConfD == nil {
writeConfD = discardWriter
}

if writeConfDOverrides == nil {
writeConfDOverrides = discardWriter
}

return &actionContext{
ConfigDir: confD,
Flags: flagValues,
Stdout: stdout,
Stderr: stderr,
WriteConfD: writeConfD,
WriteConfDOverrides: writeConfDOverrides,
}
}

type testWriter struct {
exp []byte
}

func (t *testWriter) Write(data []byte) error {
if got, want := data, t.exp; !bytes.Equal(got, want) {
return fmt.Errorf("bad conf.d write: got %q, want %q", got, want)
}
return nil
}

func newTestWriter(exp []byte) *testWriter {
return &testWriter{
exp: exp,
}
}
94 changes: 94 additions & 0 deletions pkg/tools/otelcol-config/conf_loader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package main

import (
"errors"
"fmt"
"io/fs"
)

const (
SumologicDotYaml = "sumologic.yaml"
SumologicRemoteDotYaml = "sumologic-remote.yaml"
ConfDotD = "conf.d"
ConfDotDAvailable = "conf.d-available"
ConfDSettings = "00-otelcol-config-settings.yaml"
ConfDOverrides = "99-otelcol-config-overrides.yaml"
)

type ConfDir struct {
// Sumologic is the contents of sumologic.yaml.
Sumologic []byte

// SumologicRemote is the contents of sumologic-remote.yaml.
SumologicRemote []byte

// ConfD is a mapping of file name to contents of the conf.d directory.
ConfD map[string][]byte

// ConfDAvailable is a mapping of file name to contents of the
// conf.d-available directory.
ConfDAvailable map[string][]byte
}

// ReadConfigDir reads the files in /etc/otelcol-sumo according to an expected
// layout. It produces a ConfDir that contains the data read from the files.
func ReadConfigDir(root fs.FS) (conf ConfDir, err error) {
const errMsg = "error reading otelcol-sumo config dir: %s"

conf.Sumologic, err = fs.ReadFile(root, SumologicDotYaml)
if err != nil {
// sumologic.yaml is not strictly required
if !errors.Is(err, fs.ErrNotExist) {
return conf, fmt.Errorf(errMsg, err)
}
}

conf.SumologicRemote, err = fs.ReadFile(root, SumologicRemoteDotYaml)
if err != nil {
// sumologic-remote.yaml is not strictly required
if !errors.Is(err, fs.ErrNotExist) {
return conf, fmt.Errorf(errMsg, err)
}
}

conf.ConfD, err = getDir(root, ConfDotD)
if err != nil {
// conf.d is not strictly required
if !errors.Is(err, fs.ErrNotExist) {
return conf, fmt.Errorf(errMsg, err)
}
}

conf.ConfDAvailable, err = getDir(root, ConfDotDAvailable)
if err != nil {
// conf.d-available is not strictly required
if !errors.Is(err, fs.ErrNotExist) {
return conf, fmt.Errorf(errMsg, err)
}
}

return conf, nil
}

func getDir(root fs.FS, dirName string) (result map[string][]byte, err error) {
result = make(map[string][]byte)
dirFS, err := fs.Sub(root, dirName)
if err != nil {
return nil, err
}
entries, err := fs.ReadDir(dirFS, ".")
if err != nil {
return nil, err
}
for _, entry := range entries {
if entry.Type() == fs.ModeDir {
continue
}
contents, err := fs.ReadFile(dirFS, entry.Name())
if err != nil {
return nil, err
}
result[entry.Name()] = contents
}
return result, nil
}
146 changes: 146 additions & 0 deletions pkg/tools/otelcol-config/conf_loader_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package main

import (
"io/fs"
"testing"
"testing/fstest"

"github.com/google/go-cmp/cmp"
)

func TestConfLoader(t *testing.T) {
tests := []struct {
Name string
FS fs.FS
Expected ConfDir
ErrExpected bool
}{
{
Name: "load sumologic.yaml",
FS: func() fs.FS {
return fstest.MapFS{
"sumologic.yaml": &fstest.MapFile{
Data: []byte(`{"extensions":{"sumologic":{}}}`),
},
}
}(),
Expected: ConfDir{
Sumologic: []byte(`{"extensions":{"sumologic":{}}}`),
},
},
{
Name: "load sumologic-remote.yaml",
FS: func() fs.FS {
return fstest.MapFS{
"sumologic-remote.yaml": &fstest.MapFile{
Data: []byte(`{"extensions":{"opamp":{}}}`),
},
}
}(),
Expected: ConfDir{
SumologicRemote: []byte(`{"extensions":{"opamp":{}}}`),
},
},
{
Name: "load conf.d",
FS: func() fs.FS {
return fstest.MapFS{
"conf.d/a.yaml": &fstest.MapFile{
Data: []byte(`{"extensions":{"sumologic":{}}}`),
},
"conf.d/b.yaml": &fstest.MapFile{
Data: []byte(`{"extensions":{"opamp":{}}}`),
},
"conf.d/emptydir": &fstest.MapFile{
Mode: fs.ModeDir,
},
}
}(),
Expected: ConfDir{
ConfD: map[string][]byte{
"a.yaml": []byte(`{"extensions":{"sumologic":{}}}`),
"b.yaml": []byte(`{"extensions":{"opamp":{}}}`),
},
},
},
{
Name: "load conf.d-available",
FS: func() fs.FS {
return fstest.MapFS{
"conf.d-available/a.yaml": &fstest.MapFile{
Data: []byte(`{"extensions":{"sumologic":{}}}`),
},
"conf.d-available/b.yaml": &fstest.MapFile{
Data: []byte(`{"extensions":{"opamp":{}}}`),
},
"conf.d-available/emptydir": &fstest.MapFile{
Mode: fs.ModeDir,
},
}
}(),
Expected: ConfDir{
ConfDAvailable: map[string][]byte{
"a.yaml": []byte(`{"extensions":{"sumologic":{}}}`),
"b.yaml": []byte(`{"extensions":{"opamp":{}}}`),
},
},
},
{
Name: "load all",
FS: func() fs.FS {
return fstest.MapFS{
"sumologic.yaml": &fstest.MapFile{
Data: []byte(`{"extensions":{"sumologic":{}}}`),
},
"sumologic-remote.yaml": &fstest.MapFile{
Data: []byte(`{"extensions":{"opamp":{}}}`),
},
"conf.d/a.yaml": &fstest.MapFile{
Data: []byte(`{"extensions":{"sumologic":{}}}`),
},
"conf.d/b.yaml": &fstest.MapFile{
Data: []byte(`{"extensions":{"opamp":{}}}`),
},
"conf.d/emptydir": &fstest.MapFile{
Mode: fs.ModeDir,
},
"conf.d-available/a.yaml": &fstest.MapFile{
Data: []byte(`{"extensions":{"sumologic":{}}}`),
},
"conf.d-available/b.yaml": &fstest.MapFile{
Data: []byte(`{"extensions":{"opamp":{}}}`),
},
"conf.d-available/emptydir": &fstest.MapFile{
Mode: fs.ModeDir,
},
}
}(),
Expected: ConfDir{
Sumologic: []byte(`{"extensions":{"sumologic":{}}}`),
SumologicRemote: []byte(`{"extensions":{"opamp":{}}}`),
ConfD: map[string][]byte{
"a.yaml": []byte(`{"extensions":{"sumologic":{}}}`),
"b.yaml": []byte(`{"extensions":{"opamp":{}}}`),
},
ConfDAvailable: map[string][]byte{
"a.yaml": []byte(`{"extensions":{"sumologic":{}}}`),
"b.yaml": []byte(`{"extensions":{"opamp":{}}}`),
},
},
},
}

for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
conf, err := ReadConfigDir(test.FS)
if err != nil {
if !test.ErrExpected {
t.Fatal(err)
}
}
if got, want := conf, test.Expected; !cmp.Equal(got, want) {
t.Errorf("conf dir not as expected: %s", cmp.Diff(want, got))
}
})
}
}
Loading

0 comments on commit 9214063

Please sign in to comment.