Skip to content

Commit 9e67328

Browse files
authored
NGINX Configs to Reference Remote External Files (#1389)
Added support to reference external resources in config apply
1 parent e95bc52 commit 9e67328

17 files changed

+1816
-52
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/cenkalti/backoff/v4 v4.3.0
1111
github.com/docker/docker v28.5.2+incompatible
1212
github.com/fsnotify/fsnotify v1.9.0
13+
github.com/gabriel-vasile/mimetype v1.4.8
1314
github.com/go-resty/resty/v2 v2.16.5
1415
github.com/goccy/go-yaml v1.18.0
1516
github.com/google/go-cmp v0.7.0
@@ -151,7 +152,6 @@ require (
151152
github.com/felixge/httpsnoop v1.0.4 // indirect
152153
github.com/foxboron/go-tpm-keyfiles v0.0.0-20250903184740-5d135037bd4d // indirect
153154
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
154-
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
155155
github.com/gin-contrib/sse v1.1.0 // indirect
156156
github.com/go-logr/logr v1.4.3 // indirect
157157
github.com/go-logr/stdr v1.2.2 // indirect

internal/config/config.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ const (
5050
regexLabelPattern = "^[a-zA-Z0-9]([a-zA-Z0-9-_.]{0,254}[a-zA-Z0-9])?$"
5151
)
5252

53+
var domainRegex = regexp.MustCompile(
54+
`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`,
55+
)
56+
5357
var viperInstance = viper.NewWithOptions(viper.KeyDelimiter(KeyDelimiter))
5458

5559
func RegisterRunner(r func(cmd *cobra.Command, args []string)) {
@@ -158,6 +162,7 @@ func ResolveConfig() (*Config, error) {
158162
Labels: resolveLabels(),
159163
LibDir: viperInstance.GetString(LibDirPathKey),
160164
SyslogServer: resolveSyslogServer(),
165+
ExternalDataSource: resolveExternalDataSource(),
161166
}
162167

163168
defaultCollector(collector, config)
@@ -475,6 +480,7 @@ func registerFlags() {
475480
registerCollectorFlags(fs)
476481
registerClientFlags(fs)
477482
registerDataPlaneFlags(fs)
483+
registerExternalDataSourceFlags(fs)
478484

479485
fs.SetNormalizeFunc(normalizeFunc)
480486

@@ -489,6 +495,29 @@ func registerFlags() {
489495
})
490496
}
491497

498+
func registerExternalDataSourceFlags(fs *flag.FlagSet) {
499+
fs.String(
500+
ExternalDataSourceProxyUrlKey,
501+
DefExternalDataSourceProxyUrl,
502+
"Url to the proxy service for fetching external files.",
503+
)
504+
fs.StringSlice(
505+
ExternalDataSourceAllowDomainsKey,
506+
[]string{},
507+
"List of allowed domains for external data sources.",
508+
)
509+
fs.StringSlice(
510+
ExternalDataSourceAllowedFileTypesKey,
511+
[]string{},
512+
"List of allowed file types for external data sources.",
513+
)
514+
fs.Int64(
515+
ExternalDataSourceMaxBytesKey,
516+
DefExternalDataSourceMaxBytes,
517+
"Maximum size in bytes for external data sources.",
518+
)
519+
}
520+
492521
func registerDataPlaneFlags(fs *flag.FlagSet) {
493522
fs.Duration(
494523
NginxReloadMonitoringPeriodKey,
@@ -646,6 +675,11 @@ func registerClientFlags(fs *flag.FlagSet) {
646675
DefMaxParallelFileOperations,
647676
"Maximum number of file downloads or uploads performed in parallel",
648677
)
678+
fs.Duration(
679+
ClientFileDownloadTimeoutKey,
680+
DefClientFileDownloadTimeout,
681+
"Timeout value in seconds, for downloading a file during a config apply.",
682+
)
649683
}
650684

651685
func registerCommandFlags(fs *flag.FlagSet) {
@@ -1134,6 +1168,7 @@ func resolveClient() *Client {
11341168
RandomizationFactor: viperInstance.GetFloat64(ClientBackoffRandomizationFactorKey),
11351169
Multiplier: viperInstance.GetFloat64(ClientBackoffMultiplierKey),
11361170
},
1171+
FileDownloadTimeout: viperInstance.GetDuration(ClientFileDownloadTimeoutKey),
11371172
}
11381173
}
11391174

@@ -1574,3 +1609,37 @@ func areCommandServerProxyTLSSettingsSet() bool {
15741609
viperInstance.IsSet(CommandServerProxyTLSSkipVerifyKey) ||
15751610
viperInstance.IsSet(CommandServerProxyTLSServerNameKey)
15761611
}
1612+
1613+
func resolveExternalDataSource() *ExternalDataSource {
1614+
proxyURLStruct := ProxyURL{
1615+
URL: viperInstance.GetString(ExternalDataSourceProxyUrlKey),
1616+
}
1617+
externalDataSource := &ExternalDataSource{
1618+
ProxyURL: proxyURLStruct,
1619+
AllowedDomains: viperInstance.GetStringSlice(ExternalDataSourceAllowDomainsKey),
1620+
AllowedFileTypes: viperInstance.GetStringSlice(ExternalDataSourceAllowedFileTypesKey),
1621+
MaxBytes: viperInstance.GetInt64(ExternalDataSourceMaxBytesKey),
1622+
}
1623+
1624+
if err := validateAllowedDomains(externalDataSource.AllowedDomains); err != nil {
1625+
slog.Error("External data source not configured due to invalid configuration", "error", err)
1626+
return nil
1627+
}
1628+
1629+
return externalDataSource
1630+
}
1631+
1632+
func validateAllowedDomains(domains []string) error {
1633+
if len(domains) == 0 {
1634+
return nil
1635+
}
1636+
1637+
for _, domain := range domains {
1638+
// Validating syntax using the RFC-compliant regex
1639+
if !domainRegex.MatchString(domain) || domain == "" {
1640+
return errors.New("invalid domain found in allowed_domains")
1641+
}
1642+
}
1643+
1644+
return nil
1645+
}

internal/config/config_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,7 @@ func agentConfig() *Config {
11611161
}
11621162
}
11631163

1164+
//nolint:maintidx // createConfig creates a sample Config object for testing purposes.
11641165
func createConfig() *Config {
11651166
return &Config{
11661167
Log: &Log{
@@ -1428,6 +1429,13 @@ func createConfig() *Config {
14281429
config.FeatureCertificates, config.FeatureFileWatcher, config.FeatureMetrics,
14291430
config.FeatureAPIAction, config.FeatureLogsNap,
14301431
},
1432+
ExternalDataSource: &ExternalDataSource{
1433+
ProxyURL: ProxyURL{
1434+
URL: "http://proxy.example.com",
1435+
},
1436+
AllowedDomains: []string{"example.com", "api.example.com"},
1437+
MaxBytes: 1048576,
1438+
},
14311439
}
14321440
}
14331441

@@ -1622,3 +1630,64 @@ func TestValidateLabel(t *testing.T) {
16221630
})
16231631
}
16241632
}
1633+
1634+
func TestValidateAllowedDomains(t *testing.T) {
1635+
tests := []struct {
1636+
expectedErr error
1637+
name string
1638+
domains []string
1639+
}{
1640+
{
1641+
name: "Test 1: Success: Empty slice",
1642+
domains: []string{},
1643+
expectedErr: nil,
1644+
},
1645+
{
1646+
name: "Test 2: Success: Nil slice",
1647+
domains: nil,
1648+
expectedErr: nil,
1649+
},
1650+
{
1651+
name: "Test 3: Success: Valid domains",
1652+
domains: []string{"example.com", "api.nginx.com", "sub.domain.io"},
1653+
expectedErr: nil,
1654+
},
1655+
{
1656+
name: "Test 4: Failure: Domain contains space",
1657+
domains: []string{"valid.com", "bad domain.com"},
1658+
expectedErr: errors.New("invalid domain found in allowed_domains"),
1659+
},
1660+
{
1661+
name: "Test 5: Failure: Empty string domain",
1662+
domains: []string{"valid.com", ""},
1663+
expectedErr: errors.New("invalid domain found in allowed_domains"),
1664+
},
1665+
{
1666+
name: "Test 6: Failure: Domain contains forward slash /",
1667+
domains: []string{"domain.com/path"},
1668+
expectedErr: errors.New("invalid domain found in allowed_domains"),
1669+
},
1670+
{
1671+
name: "Test 7: Failure: Domain contains backward slash \\",
1672+
domains: []string{"domain.com\\path"},
1673+
expectedErr: errors.New("invalid domain found in allowed_domains"),
1674+
},
1675+
{
1676+
name: "Test 8: Failure: Mixed valid and invalid (first is invalid)",
1677+
domains: []string{" only.com", "good.com"},
1678+
expectedErr: errors.New("invalid domain found in allowed_domains"),
1679+
},
1680+
}
1681+
1682+
for _, tt := range tests {
1683+
t.Run(tt.name, func(t *testing.T) {
1684+
err := validateAllowedDomains(tt.domains)
1685+
if tt.expectedErr == nil {
1686+
assert.NoError(t, err)
1687+
} else {
1688+
require.Error(t, err)
1689+
assert.EqualError(t, err, tt.expectedErr.Error())
1690+
}
1691+
})
1692+
}
1693+
}

internal/config/defaults.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ const (
8484
DefBackoffMaxInterval = 20 * time.Second
8585
DefBackoffMaxElapsedTime = 1 * time.Minute
8686

87+
DefClientFileDownloadTimeout = 60 * time.Second
88+
8789
// Watcher defaults
8890
DefInstanceWatcherMonitoringFrequency = 5 * time.Second
8991
DefInstanceHealthWatcherMonitoringFrequency = 5 * time.Second
@@ -116,6 +118,9 @@ const (
116118

117119
// File defaults
118120
DefLibDir = "/var/lib/nginx-agent"
121+
122+
DefExternalDataSourceProxyUrl = ""
123+
DefExternalDataSourceMaxBytes = 100 * 1024 * 1024 // default 100MB
119124
)
120125

121126
func DefaultFeatures() []string {

internal/config/flags.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const (
2525
InstanceHealthWatcherMonitoringFrequencyKey = "watchers_instance_health_watcher_monitoring_frequency"
2626
FileWatcherKey = "watchers_file_watcher"
2727
LibDirPathKey = "lib_dir"
28+
ExternalDataSourceRootKey = "external_data_source"
2829
)
2930

3031
var (
@@ -49,6 +50,7 @@ var (
4950
ClientBackoffMaxElapsedTimeKey = pre(ClientRootKey) + "backoff_max_elapsed_time"
5051
ClientBackoffRandomizationFactorKey = pre(ClientRootKey) + "backoff_randomization_factor"
5152
ClientBackoffMultiplierKey = pre(ClientRootKey) + "backoff_multiplier"
53+
ClientFileDownloadTimeoutKey = pre(ClientRootKey) + "file_download_timeout"
5254

5355
CollectorConfigPathKey = pre(CollectorRootKey) + "config_path"
5456
CollectorAdditionalConfigPathsKey = pre(CollectorRootKey) + "additional_config_paths"
@@ -143,6 +145,12 @@ var (
143145

144146
FileWatcherMonitoringFrequencyKey = pre(FileWatcherKey) + "monitoring_frequency"
145147
NginxExcludeFilesKey = pre(FileWatcherKey) + "exclude_files"
148+
149+
ExternalDataSourceProxyKey = pre(ExternalDataSourceRootKey) + "proxy"
150+
ExternalDataSourceProxyUrlKey = pre(ExternalDataSourceProxyKey) + "url"
151+
ExternalDataSourceMaxBytesKey = pre(ExternalDataSourceRootKey) + "max_bytes"
152+
ExternalDataSourceAllowDomainsKey = pre(ExternalDataSourceRootKey) + "allowed_domains"
153+
ExternalDataSourceAllowedFileTypesKey = pre(ExternalDataSourceRootKey) + "allowed_file_types"
146154
)
147155

148156
func pre(prefixes ...string) string {

internal/config/testdata/nginx-agent.conf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,11 @@ collector:
184184
log:
185185
level: "INFO"
186186
path: "/var/log/nginx-agent/opentelemetry-collector-agent.log"
187+
188+
external_data_source:
189+
proxy:
190+
url: "http://proxy.example.com"
191+
allowed_domains:
192+
- example.com
193+
- api.example.com
194+
max_bytes: 1048576

internal/config/types.go

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,22 @@ func parseServerType(str string) (ServerType, bool) {
3636

3737
type (
3838
Config struct {
39-
Command *Command `yaml:"command" mapstructure:"command"`
40-
AuxiliaryCommand *Command `yaml:"auxiliary_command" mapstructure:"auxiliary_command"`
41-
Log *Log `yaml:"log" mapstructure:"log"`
42-
DataPlaneConfig *DataPlaneConfig `yaml:"data_plane_config" mapstructure:"data_plane_config"`
43-
Client *Client `yaml:"client" mapstructure:"client"`
44-
Collector *Collector `yaml:"collector" mapstructure:"collector"`
45-
Watchers *Watchers `yaml:"watchers" mapstructure:"watchers"`
46-
SyslogServer *SyslogServer `yaml:"syslog_server" mapstructure:"syslog_server"`
47-
Labels map[string]any `yaml:"labels" mapstructure:"labels"`
48-
Version string `yaml:"-"`
49-
Path string `yaml:"-"`
50-
UUID string `yaml:"-"`
51-
LibDir string `yaml:"-"`
52-
AllowedDirectories []string `yaml:"allowed_directories" mapstructure:"allowed_directories"`
53-
Features []string `yaml:"features" mapstructure:"features"`
39+
Command *Command `yaml:"command" mapstructure:"command"`
40+
AuxiliaryCommand *Command `yaml:"auxiliary_command" mapstructure:"auxiliary_command"`
41+
Log *Log `yaml:"log" mapstructure:"log"`
42+
DataPlaneConfig *DataPlaneConfig `yaml:"data_plane_config" mapstructure:"data_plane_config"`
43+
Client *Client `yaml:"client" mapstructure:"client"`
44+
Collector *Collector `yaml:"collector" mapstructure:"collector"`
45+
Watchers *Watchers `yaml:"watchers" mapstructure:"watchers"`
46+
ExternalDataSource *ExternalDataSource `yaml:"external_data_source" mapstructure:"external_data_source"`
47+
SyslogServer *SyslogServer `yaml:"syslog_server" mapstructure:"syslog_server"`
48+
Labels map[string]any `yaml:"labels" mapstructure:"labels"`
49+
Version string `yaml:"-"`
50+
Path string `yaml:"-"`
51+
UUID string `yaml:"-"`
52+
LibDir string `yaml:"-"`
53+
AllowedDirectories []string `yaml:"allowed_directories" mapstructure:"allowed_directories"`
54+
Features []string `yaml:"features" mapstructure:"features"`
5455
}
5556

5657
Log struct {
@@ -74,9 +75,10 @@ type (
7475
}
7576

7677
Client struct {
77-
HTTP *HTTP `yaml:"http" mapstructure:"http"`
78-
Grpc *GRPC `yaml:"grpc" mapstructure:"grpc"`
79-
Backoff *BackOff `yaml:"backoff" mapstructure:"backoff"`
78+
HTTP *HTTP `yaml:"http" mapstructure:"http"`
79+
Grpc *GRPC `yaml:"grpc" mapstructure:"grpc"`
80+
Backoff *BackOff `yaml:"backoff" mapstructure:"backoff"`
81+
FileDownloadTimeout time.Duration `yaml:"file_download_timeout" mapstructure:"file_download_timeout"`
8082
}
8183

8284
HTTP struct {
@@ -360,6 +362,17 @@ type (
360362
Token string `yaml:"token,omitempty" mapstructure:"token"`
361363
Timeout time.Duration `yaml:"timeout" mapstructure:"timeout"`
362364
}
365+
366+
ProxyURL struct {
367+
URL string `yaml:"url" mapstructure:"url"`
368+
}
369+
370+
ExternalDataSource struct {
371+
ProxyURL ProxyURL `yaml:"proxy" mapstructure:"proxy"`
372+
AllowedDomains []string `yaml:"allowed_domains" mapstructure:"allowed_domains"`
373+
AllowedFileTypes []string `yaml:"allowed_file_types" mapstructure:"allowed_file_types"`
374+
MaxBytes int64 `yaml:"max_bytes" mapstructure:"max_bytes"`
375+
}
363376
)
364377

365378
func (col *Collector) Validate(allowedDirectories []string) error {

0 commit comments

Comments
 (0)