Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support manager config environment variable expansion #1946

Merged
merged 4 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 65 additions & 18 deletions opamp/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package opamp

import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
Expand All @@ -27,10 +28,15 @@ import (
"github.com/google/uuid"
"github.com/oklog/ulid/v2"
"github.com/open-telemetry/opamp-go/client/types"
"gopkg.in/yaml.v3"
"go.opentelemetry.io/collector/confmap"
"go.opentelemetry.io/collector/confmap/provider/envprovider"
"go.opentelemetry.io/collector/confmap/provider/fileprovider"
)

var (
// errPrefixResolverInitialization for error when initializing config file resolver
errPrefixResolverInitialization = "failed to initialize OpAmp config resolver"

// errPrefixReadFile for error when reading config file
errPrefixReadFile = "failed to read OpAmp config file"

Expand Down Expand Up @@ -155,26 +161,54 @@ func (a *AgentID) UnmarshalYAML(unmarshal func(any) error) error {
return nil
}

// UnmarshalText implements the encoding.TextUnmarshaler interface
func (a *AgentID) UnmarshalText(text []byte) error {
s := string(text)

if s == "" {
// Empty string = keep the 0 value
return nil
}

u, err := ParseAgentID(s)
if err != nil {
// In order to maintain backwards compatability, we don't error here.
// Instead, in main, we will regenerate a new agent ID
*a = EmptyAgentID
return nil
}

*a = AgentID(u)

return nil
}

// MarshalText implements the encoding.TextMarshaler interface
func (a *AgentID) MarshalText() ([]byte, error) {
// Format the time in your desired format
return []byte(a.String()), nil
}

// Config contains the configuration for the collector to communicate with an OpAmp enabled platform.
type Config struct {
Endpoint string `yaml:"endpoint"`
SecretKey *string `yaml:"secret_key,omitempty"`
AgentID AgentID `yaml:"agent_id"`
TLS *TLSConfig `yaml:"tls_config,omitempty"`
Endpoint string `yaml:"endpoint" mapstructure:"endpoint"`
SecretKey *string `yaml:"secret_key,omitempty" mapstructure:"secret_key,omitempty"`
AgentID AgentID `yaml:"agent_id" mapstructure:"agent_id"`
TLS *TLSConfig `yaml:"tls_config,omitempty" mapstructure:"tls_config,omitempty"`

// Updatable fields
Labels *string `yaml:"labels,omitempty"`
AgentName *string `yaml:"agent_name,omitempty"`
MeasurementsInterval time.Duration `yaml:"measurements_interval,omitempty"`
ExtraMeasurementsAttributes map[string]string `yaml:"extra_measurements_attributes,omitempty"`
Labels *string `yaml:"labels,omitempty" mapstructure:"labels,omitempty"`
AgentName *string `yaml:"agent_name,omitempty" mapstructure:"agent_name,omitempty"`
MeasurementsInterval time.Duration `yaml:"measurements_interval,omitempty" mapstructure:"measurements_interval,omitempty"`
ExtraMeasurementsAttributes map[string]string `yaml:"extra_measurements_attributes,omitempty" mapstructure:"extra_measurements_attributes,omitempty"`
}

// TLSConfig represents the TLS config to connect to OpAmp server
type TLSConfig struct {
InsecureSkipVerify bool `yaml:"insecure_skip_verify"`
KeyFile *string `yaml:"key_file"`
CertFile *string `yaml:"cert_file"`
CAFile *string `yaml:"ca_file"`
InsecureSkipVerify bool `yaml:"insecure_skip_verify" mapstructure:"insecure_skip_verify"`
KeyFile *string `yaml:"key_file" mapstructure:"key_file"`
CertFile *string `yaml:"cert_file" mapstructure:"cert_file"`
CAFile *string `yaml:"ca_file" mapstructure:"ca_file"`
}

// ToTLS converts the config to a tls.Config
Expand Down Expand Up @@ -221,20 +255,33 @@ func (c Config) ToTLS() (*tls.Config, error) {
func ParseConfig(configLocation string) (*Config, error) {
configPath := filepath.Clean(configLocation)

// Read in config file contents
data, err := os.ReadFile(configPath)
resolverSettings := confmap.ResolverSettings{
URIs: []string{configPath},
ProviderFactories: []confmap.ProviderFactory{
fileprovider.NewFactory(),
envprovider.NewFactory(),
},
ConverterFactories: []confmap.ConverterFactory{},
DefaultScheme: "env",
}

resolver, err := confmap.NewResolver(resolverSettings)
if err != nil {
return nil, fmt.Errorf("%s: %w", errPrefixResolverInitialization, err)
}

conf, err := resolver.Resolve(context.Background())
if err != nil {
return nil, fmt.Errorf("%s: %w", errPrefixReadFile, err)
}

// Parse the config
var config Config
if err := yaml.Unmarshal(data, &config); err != nil {
if err = conf.Unmarshal(&config); err != nil {
return nil, fmt.Errorf("%s: %w", errPrefixParse, err)
}

// Using Secure TLS check files
if config.TLS != nil && config.TLS.InsecureSkipVerify == false {
if config.TLS != nil && !config.TLS.InsecureSkipVerify {
// If CA file is specified
if config.TLS.CAFile != nil {
// Validate CA file exists on disk
Expand Down
197 changes: 190 additions & 7 deletions opamp/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ package opamp

import (
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -168,6 +167,7 @@ func TestToTLS(t *testing.T) {
}

cert, err := tls.LoadX509KeyPair(certFileContents, keyFileContents)
require.NoError(t, err)
expectedConfig.Certificates = []tls.Certificate{cert}

actual, err := cfg.ToTLS()
Expand All @@ -188,15 +188,17 @@ func TestToTLS(t *testing.T) {
}

expectedConfig := tls.Config{
MinVersion: tls.VersionTLS12,
// MinVersion: tls.VersionTLS12,
jsirianni marked this conversation as resolved.
Show resolved Hide resolved
}

caCert, err := os.ReadFile(caFileContents)
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
expectedConfig.RootCAs = caCertPool
// caCert, err := os.ReadFile(caFileContents)
// require.NoError(t, err)
// caCertPool := x509.NewCertPool()
// caCertPool.AppendCertsFromPEM(caCert)
// expectedConfig.RootCAs = caCertPool

cert, err := tls.LoadX509KeyPair(certFileContents, keyFileContents)
require.NoError(t, err)
expectedConfig.Certificates = []tls.Certificate{cert}

actual, err := cfg.ToTLS()
Expand Down Expand Up @@ -246,7 +248,7 @@ func TestParseConfig(t *testing.T) {
require.NoError(t, err)

cfg, err := ParseConfig(configPath)
assert.ErrorContains(t, err, errPrefixParse)
assert.ErrorContains(t, err, errPrefixReadFile)
assert.Nil(t, cfg)
},
},
Expand Down Expand Up @@ -636,6 +638,187 @@ tls_config:
},
}

cfg, err := ParseConfig(configPath)
assert.NoError(t, err)
assert.Equal(t, expectedConfig, cfg)
},
},
{
desc: "Successful Parse With Environment Variables",
testFunc: func(t *testing.T) {
endpointEnvVar := "TEST_ENDPOINT"
require.NoError(t, os.Setenv(endpointEnvVar, "localhost:1234"))
defer func() {
require.NoError(t, os.Unsetenv(endpointEnvVar))
}()

secretKeyEnvVar := "TEST_SECRET_KEY"
require.NoError(t, os.Setenv(secretKeyEnvVar, secretKeyContents))
defer func() {
require.NoError(t, os.Unsetenv(secretKeyEnvVar))
}()

agentIDEnvVar := "TEST_AGENT_ID"
require.NoError(t, os.Setenv(agentIDEnvVar, testAgentIDString))
defer func() {
require.NoError(t, os.Unsetenv(agentIDEnvVar))
}()

labelsEnvVar := "TEST_LABELS"
require.NoError(t, os.Setenv(labelsEnvVar, "one=foo,two=bar"))
defer func() {
require.NoError(t, os.Unsetenv(labelsEnvVar))
}()

agentNameEnvVar := "TEST_AGENT_NAME"
require.NoError(t, os.Setenv(agentNameEnvVar, "My Agent"))
defer func() {
require.NoError(t, os.Unsetenv(agentNameEnvVar))
}()

configContents := fmt.Sprintf(`
endpoint: ${%s}
secret_key: ${%s}
agent_id: ${%s}
labels: ${%s}
agent_name: ${%s}
`, endpointEnvVar, secretKeyEnvVar, agentIDEnvVar, labelsEnvVar, agentNameEnvVar)

tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "manager.yml")

err := os.WriteFile(configPath, []byte(configContents), os.ModePerm)
require.NoError(t, err)

expectedConfig := &Config{
Endpoint: "localhost:1234",
SecretKey: &secretKeyContents,
AgentID: testAgentID,
Labels: &labelsContents,
AgentName: &agentNameContents,
}

cfg, err := ParseConfig(configPath)
assert.NoError(t, err)
assert.Equal(t, expectedConfig, cfg)
},
},
{
desc: "Successful Parse With env:Environment Variables",
testFunc: func(t *testing.T) {
endpointEnvVar := "TEST_ENDPOINT"
require.NoError(t, os.Setenv(endpointEnvVar, "localhost:1234"))
defer func() {
require.NoError(t, os.Unsetenv(endpointEnvVar))
}()

secretKeyEnvVar := "TEST_SECRET_KEY"
require.NoError(t, os.Setenv(secretKeyEnvVar, secretKeyContents))
defer func() {
require.NoError(t, os.Unsetenv(secretKeyEnvVar))
}()

agentIDEnvVar := "TEST_AGENT_ID"
require.NoError(t, os.Setenv(agentIDEnvVar, testAgentIDString))
defer func() {
require.NoError(t, os.Unsetenv(agentIDEnvVar))
}()

labelsEnvVar := "TEST_LABELS"
require.NoError(t, os.Setenv(labelsEnvVar, "one=foo,two=bar"))
defer func() {
require.NoError(t, os.Unsetenv(labelsEnvVar))
}()

agentNameEnvVar := "TEST_AGENT_NAME"
require.NoError(t, os.Setenv(agentNameEnvVar, "My Agent"))
defer func() {
require.NoError(t, os.Unsetenv(agentNameEnvVar))
}()

configContents := fmt.Sprintf(`
endpoint: ${env:%s}
secret_key: ${env:%s}
agent_id: ${env:%s}
labels: ${env:%s}
agent_name: ${env:%s}
`, endpointEnvVar, secretKeyEnvVar, agentIDEnvVar, labelsEnvVar, agentNameEnvVar)

tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "manager.yml")

err := os.WriteFile(configPath, []byte(configContents), os.ModePerm)
require.NoError(t, err)

expectedConfig := &Config{
Endpoint: "localhost:1234",
SecretKey: &secretKeyContents,
AgentID: testAgentID,
Labels: &labelsContents,
AgentName: &agentNameContents,
}

cfg, err := ParseConfig(configPath)
assert.NoError(t, err)
assert.Equal(t, expectedConfig, cfg)
},
},
{
desc: "Successful Full Parse with TLS Valid Key and Cert Environment Variables",
testFunc: func(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "manager.yml")

keyPath := filepath.Join(tmpDir, "file-key.crt")
k, err := os.Create(keyPath)
require.NoError(t, err)
defer k.Close()

certPath := filepath.Join(tmpDir, "file-cert.crt")
c, err := os.Create(certPath)
require.NoError(t, err)
defer c.Close()

keyEnvVariable := "TEST_TLS_KEY"
require.NoError(t, os.Setenv(keyEnvVariable, keyPath))
defer func() {
require.NoError(t, os.Unsetenv(keyEnvVariable))
}()

certEnvVariable := "TEST_TLS_CERT"
require.NoError(t, os.Setenv(certEnvVariable, certPath))
defer func() {
require.NoError(t, os.Unsetenv(certEnvVariable))
}()

configContents := fmt.Sprintf(`
endpoint: localhost:1234
secret_key: b92222ee-a1fc-4bb1-98db-26de3448541b
agent_id: %s
labels: "one=foo,two=bar"
agent_name: "My Agent"
tls_config:
insecure_skip_verify: false
key_file: ${%s}
cert_file: ${%s}
`, testAgentIDString, keyEnvVariable, certEnvVariable)

err = os.WriteFile(configPath, []byte(configContents), os.ModePerm)
require.NoError(t, err)

expectedConfig := &Config{
Endpoint: "localhost:1234",
SecretKey: &secretKeyContents,
AgentID: testAgentID,
Labels: &labelsContents,
AgentName: &agentNameContents,
TLS: &TLSConfig{
InsecureSkipVerify: false,
KeyFile: &keyPath,
CertFile: &certPath,
},
}

cfg, err := ParseConfig(configPath)
assert.NoError(t, err)
assert.Equal(t, expectedConfig, cfg)
Expand Down
Loading