988 lines
29 KiB
Go
988 lines
29 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestLoadFileValidMinimalLocalToLocalConfig(t *testing.T) {
|
|
cfg := loadConfig(t, `
|
|
pipelines:
|
|
- id: local-copy
|
|
source:
|
|
backend: local
|
|
path: /var/spool/reports
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /srv/archive
|
|
`)
|
|
|
|
if got, want := len(cfg.Pipelines), 1; got != want {
|
|
t.Fatalf("pipeline count = %d, want %d", got, want)
|
|
}
|
|
destination := cfg.Pipelines[0].Destinations[0]
|
|
if !destination.Publish.Source || destination.Publish.HTML {
|
|
t.Fatalf("publish defaults = source:%t html:%t, want source:true html:false", destination.Publish.Source, destination.Publish.HTML)
|
|
}
|
|
if got, want := cfg.Pipelines[0].Validation.OnDigestMismatch, ValidationActionFail; got != want {
|
|
t.Fatalf("validation default = %q, want %q", got, want)
|
|
}
|
|
if got, want := destination.Transfer.OnDestinationOlder, TransferActionReplace; got != want {
|
|
t.Fatalf("transfer default = %q, want %q", got, want)
|
|
}
|
|
if got, want := destination.Reconciliation.Mode, ReconciliationModeReplace; got != want {
|
|
t.Fatalf("reconciliation mode default = %q, want %q", got, want)
|
|
}
|
|
if got, want := destination.State.Mode, StateModeSingleOwner; got != want {
|
|
t.Fatalf("state mode default = %q, want %q", got, want)
|
|
}
|
|
if destination.Retention.Prune.Enabled {
|
|
t.Fatal("retention.prune.enabled default = true, want false")
|
|
}
|
|
if cfg.Secrets.Directory != "" {
|
|
t.Fatalf("secrets.directory = %q, want empty", cfg.Secrets.Directory)
|
|
}
|
|
}
|
|
|
|
func TestLoadFileValidSecretsDirectoryConfig(t *testing.T) {
|
|
cfg := loadConfig(t, `
|
|
secrets:
|
|
directory: /run/secrets/distributor
|
|
pipelines:
|
|
- id: local-copy
|
|
source:
|
|
backend: local
|
|
path: /var/spool/reports
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /srv/archive
|
|
`)
|
|
|
|
if got, want := cfg.Secrets.Directory, "/run/secrets/distributor"; got != want {
|
|
t.Fatalf("secrets.directory = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestLoadFileValidFanOutConfig(t *testing.T) {
|
|
cfg := loadConfig(t, `
|
|
pipelines:
|
|
- id: fan-out
|
|
source:
|
|
backend: local
|
|
path: /var/spool/reports
|
|
destinations:
|
|
- id: markdown-archive
|
|
backend: s3
|
|
endpoint: https://s3.example.com
|
|
bucket: reports
|
|
prefix: archive
|
|
publish:
|
|
source: true
|
|
html: false
|
|
- id: static-site
|
|
backend: ssh
|
|
host: example.com
|
|
user: deploy
|
|
port: 22
|
|
path: /srv/www/reports
|
|
publish:
|
|
source: false
|
|
html: true
|
|
transform:
|
|
markdown_to_html:
|
|
enabled: true
|
|
mode: sidecar
|
|
`)
|
|
|
|
if got, want := len(cfg.Pipelines[0].Destinations), 2; got != want {
|
|
t.Fatalf("destination count = %d, want %d", got, want)
|
|
}
|
|
}
|
|
|
|
func TestLoadFileValidMarkdownIndexConfig(t *testing.T) {
|
|
cfg := loadConfig(t, `
|
|
pipelines:
|
|
- id: static-site
|
|
source:
|
|
backend: local
|
|
path: /var/spool/reports
|
|
destinations:
|
|
- id: web
|
|
backend: local
|
|
path: /srv/www/reports
|
|
publish:
|
|
source: false
|
|
html: true
|
|
transform:
|
|
markdown_to_html:
|
|
enabled: true
|
|
mode: index
|
|
input: report.md
|
|
`)
|
|
|
|
markdown := cfg.Pipelines[0].Destinations[0].Transform.MarkdownToHTML
|
|
if markdown == nil || markdown.Mode != TransformModeIndex || markdown.Input != "report.md" {
|
|
t.Fatalf("markdown config = %#v, want index input", markdown)
|
|
}
|
|
}
|
|
|
|
func TestLoadFileDefaultsMarkdownModeToSidecar(t *testing.T) {
|
|
cfg := loadConfig(t, `
|
|
pipelines:
|
|
- id: static-site
|
|
source:
|
|
backend: local
|
|
path: /var/spool/reports
|
|
destinations:
|
|
- id: web
|
|
backend: local
|
|
path: /srv/www/reports
|
|
publish:
|
|
source: false
|
|
html: true
|
|
transform:
|
|
markdown_to_html:
|
|
enabled: true
|
|
`)
|
|
|
|
markdown := cfg.Pipelines[0].Destinations[0].Transform.MarkdownToHTML
|
|
if markdown == nil || markdown.Mode != TransformModeSidecar {
|
|
t.Fatalf("markdown mode = %#v, want sidecar default", markdown)
|
|
}
|
|
}
|
|
|
|
func TestLoadFileDefaultsPathMappingToPreserveRelative(t *testing.T) {
|
|
cfg := loadConfig(t, `
|
|
pipelines:
|
|
- id: reports
|
|
source:
|
|
backend: local
|
|
path: /source
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /destination
|
|
`)
|
|
|
|
if got, want := cfg.Pipelines[0].Destinations[0].PathMap.Mode, PathMappingPreserveRelative; got != want {
|
|
t.Fatalf("path mapping mode = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestLoadFileAcceptsExplicitReconciliationModes(t *testing.T) {
|
|
cfg := loadConfig(t, `
|
|
pipelines:
|
|
- id: reports
|
|
source:
|
|
backend: local
|
|
path: /source
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /archive
|
|
reconciliation:
|
|
mode: replace
|
|
- id: web
|
|
backend: local
|
|
path: /web
|
|
reconciliation:
|
|
mode: merge
|
|
`)
|
|
|
|
destinations := cfg.Pipelines[0].Destinations
|
|
if got, want := destinations[0].Reconciliation.Mode, ReconciliationModeReplace; got != want {
|
|
t.Fatalf("archive reconciliation mode = %q, want %q", got, want)
|
|
}
|
|
if got, want := destinations[1].Reconciliation.Mode, ReconciliationModeMerge; got != want {
|
|
t.Fatalf("web reconciliation mode = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestLoadFileAcceptsExplicitStateModes(t *testing.T) {
|
|
cfg := loadConfig(t, `
|
|
pipelines:
|
|
- id: reports
|
|
source:
|
|
backend: local
|
|
path: /source
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /archive
|
|
state:
|
|
mode: single_owner
|
|
- id: web
|
|
backend: local
|
|
path: /web
|
|
state:
|
|
mode: shared_root
|
|
`)
|
|
|
|
destinations := cfg.Pipelines[0].Destinations
|
|
if got, want := destinations[0].State.Mode, StateModeSingleOwner; got != want {
|
|
t.Fatalf("archive state mode = %q, want %q", got, want)
|
|
}
|
|
if got, want := destinations[1].State.Mode, StateModeSharedRoot; got != want {
|
|
t.Fatalf("web state mode = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestLoadFileAcceptsRetentionPruneConfig(t *testing.T) {
|
|
cfg := loadConfig(t, `
|
|
pipelines:
|
|
- id: reports
|
|
source:
|
|
backend: local
|
|
path: /source
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /archive
|
|
retention:
|
|
prune:
|
|
enabled: true
|
|
older_than: 168h
|
|
keep_latest: 3
|
|
`)
|
|
|
|
prune := cfg.Pipelines[0].Destinations[0].Retention.Prune
|
|
if !prune.Enabled {
|
|
t.Fatal("retention.prune.enabled = false, want true")
|
|
}
|
|
if prune.OlderThan == nil || prune.OlderThan.String() != "168h0m0s" {
|
|
t.Fatalf("retention.prune.older_than = %v, want 168h", prune.OlderThan)
|
|
}
|
|
if prune.KeepLatest == nil || *prune.KeepLatest != 3 {
|
|
t.Fatalf("retention.prune.keep_latest = %v, want 3", prune.KeepLatest)
|
|
}
|
|
}
|
|
|
|
func TestLoadFileAcceptsFixedPathMapping(t *testing.T) {
|
|
cfg := loadConfig(t, `
|
|
pipelines:
|
|
- id: reports
|
|
source:
|
|
backend: local
|
|
path: /source
|
|
destinations:
|
|
- id: latest
|
|
backend: local
|
|
path: /destination/latest
|
|
path_mapping:
|
|
mode: fixed
|
|
`)
|
|
|
|
if got, want := cfg.Pipelines[0].Destinations[0].PathMap.Mode, PathMappingFixed; got != want {
|
|
t.Fatalf("path mapping mode = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestLoadFileDefaultsLinksPrimaryToAuto(t *testing.T) {
|
|
cfg := loadConfig(t, `
|
|
pipelines:
|
|
- id: reports
|
|
source:
|
|
backend: local
|
|
path: /source
|
|
destinations:
|
|
- id: web
|
|
backend: local
|
|
path: /destination
|
|
links:
|
|
base_url: https://reports.example.com/archive
|
|
`)
|
|
|
|
links := cfg.Pipelines[0].Destinations[0].Links
|
|
if links == nil {
|
|
t.Fatal("links = nil, want config")
|
|
}
|
|
if links.BaseURL != "https://reports.example.com/archive" || links.Primary != LinkPrimaryAuto {
|
|
t.Fatalf("links = %#v, want base URL with auto primary", links)
|
|
}
|
|
}
|
|
|
|
func TestLoadFileDefaultsHTTPServerConfig(t *testing.T) {
|
|
cfg := loadConfig(t, `
|
|
pipelines:
|
|
- id: reports
|
|
source:
|
|
backend: local
|
|
path: /source
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /archive
|
|
`)
|
|
|
|
server := cfg.Server.HTTP
|
|
if got, want := server.Bind, DefaultHTTPBind; got != want {
|
|
t.Fatalf("server.http.bind = %q, want %q", got, want)
|
|
}
|
|
if got, want := server.StagingRoot, DefaultHTTPStagingRoot; got != want {
|
|
t.Fatalf("server.http.staging_root = %q, want %q", got, want)
|
|
}
|
|
if got, want := *server.MaxUploadSize, DefaultHTTPMaxUploadSize; got != want {
|
|
t.Fatalf("server.http.max_upload_size = %s, want %s", got, want)
|
|
}
|
|
if got, want := server.QueueSize, DefaultHTTPQueueSize; got != want {
|
|
t.Fatalf("server.http.queue_size = %d, want %d", got, want)
|
|
}
|
|
if got, want := server.MaxConcurrency, DefaultHTTPMaxConcurrency; got != want {
|
|
t.Fatalf("server.http.max_concurrency = %d, want %d", got, want)
|
|
}
|
|
if got, want := *server.Retention, DefaultHTTPRetention; got != want {
|
|
t.Fatalf("server.http.retention = %s, want %s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestLoadFileAcceptsHTTPUploadSourceConfig(t *testing.T) {
|
|
cfg := loadConfig(t, `
|
|
server:
|
|
http:
|
|
bind: 127.0.0.1:9090
|
|
staging_root: /srv/distributor/staging
|
|
max_upload_size: 64MB
|
|
queue_size: 32
|
|
max_concurrency: 2
|
|
retention: 48h
|
|
pipelines:
|
|
- id: weather-daily
|
|
source:
|
|
backend: http_upload
|
|
staging_path: /srv/distributor/staging/weather-daily
|
|
max_upload_size: 32MB
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /archive
|
|
upload_tokens:
|
|
- id: weather-reporter
|
|
token_env: WEATHER_DAILY_UPLOAD_TOKEN
|
|
allow_pipelines:
|
|
- weather-daily
|
|
`)
|
|
|
|
server := cfg.Server.HTTP
|
|
if got, want := server.Bind, "127.0.0.1:9090"; got != want {
|
|
t.Fatalf("server.http.bind = %q, want %q", got, want)
|
|
}
|
|
if got, want := server.StagingRoot, "/srv/distributor/staging"; got != want {
|
|
t.Fatalf("server.http.staging_root = %q, want %q", got, want)
|
|
}
|
|
if got, want := *server.MaxUploadSize, ByteSize(64*1024*1024); got != want {
|
|
t.Fatalf("server.http.max_upload_size = %s, want %s", got, want)
|
|
}
|
|
if got, want := server.QueueSize, 32; got != want {
|
|
t.Fatalf("server.http.queue_size = %d, want %d", got, want)
|
|
}
|
|
if got, want := server.MaxConcurrency, 2; got != want {
|
|
t.Fatalf("server.http.max_concurrency = %d, want %d", got, want)
|
|
}
|
|
if got, want := server.Retention.String(), "48h0m0s"; got != want {
|
|
t.Fatalf("server.http.retention = %s, want %s", got, want)
|
|
}
|
|
|
|
source := cfg.Pipelines[0].Source
|
|
if got, want := source.Backend, BackendHTTPUpload; got != want {
|
|
t.Fatalf("source.backend = %q, want %q", got, want)
|
|
}
|
|
if got, want := source.Upload.StagingPath, "/srv/distributor/staging/weather-daily"; got != want {
|
|
t.Fatalf("source.staging_path = %q, want %q", got, want)
|
|
}
|
|
if got, want := *source.Upload.MaxUploadSize, ByteSize(32*1024*1024); got != want {
|
|
t.Fatalf("source.max_upload_size = %s, want %s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestLoadFileDefaultsHTTPUploadSourceConfig(t *testing.T) {
|
|
cfg := loadConfig(t, `
|
|
server:
|
|
http:
|
|
max_upload_size: 12MB
|
|
pipelines:
|
|
- id: weather-daily
|
|
source:
|
|
backend: http_upload
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /archive
|
|
upload_tokens:
|
|
- id: weather-reporter
|
|
token_env: WEATHER_DAILY_UPLOAD_TOKEN
|
|
allow_pipelines:
|
|
- weather-daily
|
|
`)
|
|
|
|
source := cfg.Pipelines[0].Source
|
|
if got, want := source.Upload.StagingPath, "/var/spool/distributor/weather-daily"; got != want {
|
|
t.Fatalf("source.staging_path = %q, want %q", got, want)
|
|
}
|
|
if got, want := *source.Upload.MaxUploadSize, ByteSize(12*1024*1024); got != want {
|
|
t.Fatalf("source.max_upload_size = %s, want %s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestLoadFileAcceptsHTTPUploadTokens(t *testing.T) {
|
|
tests := map[string]string{
|
|
"valid multi pipeline token": `
|
|
pipelines:
|
|
- id: weather-daily
|
|
source:
|
|
backend: http_upload
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /archive/weather
|
|
- id: calendar-daily
|
|
source:
|
|
backend: http_upload
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /archive/calendar
|
|
upload_tokens:
|
|
- id: reporter
|
|
token_env: REPORTER_UPLOAD_TOKEN
|
|
allow_pipelines:
|
|
- weather-daily
|
|
- calendar-daily
|
|
`,
|
|
"multiple tokens for one pipeline": `
|
|
pipelines:
|
|
- id: reports
|
|
source:
|
|
backend: http_upload
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /archive
|
|
upload_tokens:
|
|
- id: reporter-a
|
|
token_env: REPORTER_A_UPLOAD_TOKEN
|
|
allow_pipelines:
|
|
- reports
|
|
- id: reporter-b
|
|
token_env: REPORTER_B_UPLOAD_TOKEN
|
|
allow_pipelines:
|
|
- reports
|
|
`,
|
|
}
|
|
for name, body := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
loadConfig(t, body)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadFileRejectsInvalidUploadTokens(t *testing.T) {
|
|
tests := map[string]struct {
|
|
body string
|
|
want string
|
|
}{
|
|
"missing token list": {
|
|
body: `pipelines: [{id: reports, source: {backend: http_upload}, destinations: [{id: archive, backend: local, path: /archive}]}]`,
|
|
want: "upload_tokens is required",
|
|
},
|
|
"duplicate token ids": {
|
|
body: `upload_tokens: [{id: reporter, token_env: ONE_UPLOAD_TOKEN, allow_pipelines: [reports]}, {id: reporter, token_env: TWO_UPLOAD_TOKEN, allow_pipelines: [reports]}]
|
|
pipelines: [{id: reports, source: {backend: http_upload}, destinations: [{id: archive, backend: local, path: /archive}]}]`,
|
|
want: "upload token id reporter is duplicated",
|
|
},
|
|
"duplicate allowlist entries": {
|
|
body: `upload_tokens: [{id: reporter, token_env: UPLOAD_TOKEN, allow_pipelines: [reports, reports]}]
|
|
pipelines: [{id: reports, source: {backend: http_upload}, destinations: [{id: archive, backend: local, path: /archive}]}]`,
|
|
want: "allow_pipelines contains duplicate pipeline id reports",
|
|
},
|
|
"unknown allowed pipeline id": {
|
|
body: `upload_tokens: [{id: reporter, token_env: UPLOAD_TOKEN, allow_pipelines: [missing]}]
|
|
pipelines: [{id: reports, source: {backend: http_upload}, destinations: [{id: archive, backend: local, path: /archive}]}]`,
|
|
want: "references unknown pipeline missing",
|
|
},
|
|
"non upload allowed pipeline id": {
|
|
body: `upload_tokens: [{id: reporter, token_env: UPLOAD_TOKEN, allow_pipelines: [reports]}, {id: uploader, token_env: OTHER_UPLOAD_TOKEN, allow_pipelines: [upload]}]
|
|
pipelines: [{id: reports, source: {backend: local, path: /source}, destinations: [{id: archive, backend: local, path: /archive}]}, {id: upload, source: {backend: http_upload}, destinations: [{id: archive, backend: local, path: /archive-upload}]}]`,
|
|
want: "references non-http_upload pipeline reports",
|
|
},
|
|
"upload pipeline not allowed": {
|
|
body: `upload_tokens: [{id: reporter, token_env: UPLOAD_TOKEN, allow_pipelines: [reports]}]
|
|
pipelines: [{id: reports, source: {backend: http_upload}, destinations: [{id: archive, backend: local, path: /archive}]}, {id: other, source: {backend: http_upload}, destinations: [{id: archive, backend: local, path: /archive-other}]}]`,
|
|
want: "http_upload pipeline other is not allowed by any upload token",
|
|
},
|
|
"missing token id": {
|
|
body: `upload_tokens: [{token_env: UPLOAD_TOKEN, allow_pipelines: [reports]}]
|
|
pipelines: [{id: reports, source: {backend: http_upload}, destinations: [{id: archive, backend: local, path: /archive}]}]`,
|
|
want: "upload_tokens[0].id is required",
|
|
},
|
|
"invalid token id": {
|
|
body: `upload_tokens: [{id: ".reporter", token_env: UPLOAD_TOKEN, allow_pipelines: [reports]}]
|
|
pipelines: [{id: reports, source: {backend: http_upload}, destinations: [{id: archive, backend: local, path: /archive}]}]`,
|
|
want: "upload_tokens[0].id must be a slug-like identifier",
|
|
},
|
|
"missing token env": {
|
|
body: `upload_tokens: [{id: reporter, allow_pipelines: [reports]}]
|
|
pipelines: [{id: reports, source: {backend: http_upload}, destinations: [{id: archive, backend: local, path: /archive}]}]`,
|
|
want: "upload_tokens[0].token_env is required",
|
|
},
|
|
"missing allowlist": {
|
|
body: `upload_tokens: [{id: reporter, token_env: UPLOAD_TOKEN}]
|
|
pipelines: [{id: reports, source: {backend: http_upload}, destinations: [{id: archive, backend: local, path: /archive}]}]`,
|
|
want: "upload_tokens[0].allow_pipelines is required",
|
|
},
|
|
}
|
|
for name, tt := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
assertLoadError(t, tt.body, tt.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadFileValidBackendConfigs(t *testing.T) {
|
|
tests := map[string]string{
|
|
"local": `
|
|
pipelines:
|
|
- id: local-backend
|
|
source:
|
|
backend: local
|
|
path: /source
|
|
destinations:
|
|
- id: local-destination
|
|
backend: local
|
|
path: /destination
|
|
`,
|
|
"ssh": `
|
|
pipelines:
|
|
- id: ssh-backend
|
|
source:
|
|
backend: ssh
|
|
host: source.example.com
|
|
user: reports
|
|
path: /source
|
|
destinations:
|
|
- id: ssh-destination
|
|
backend: ssh
|
|
host: destination.example.com
|
|
user: deploy
|
|
port: 2222
|
|
path: /destination
|
|
ssh_key_file: /home/deploy/.ssh/id_ed25519
|
|
known_hosts: /home/deploy/.ssh/known_hosts
|
|
host_key_policy: strict
|
|
`,
|
|
"s3": `
|
|
pipelines:
|
|
- id: s3-backend
|
|
source:
|
|
backend: s3
|
|
endpoint: https://s3.example.com
|
|
bucket: source
|
|
prefix: incoming
|
|
region: us-east-1
|
|
force_path_style: true
|
|
credentials:
|
|
access_key_id_env: DISTRIBUTOR_S3_ACCESS_KEY_ID
|
|
secret_access_key_env: DISTRIBUTOR_S3_SECRET_ACCESS_KEY
|
|
destinations:
|
|
- id: s3-destination
|
|
backend: s3
|
|
endpoint: https://s3.example.com
|
|
bucket: destination
|
|
prefix: archive
|
|
`,
|
|
}
|
|
|
|
for name, body := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
loadConfig(t, body)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadFileDefaultsS3Config(t *testing.T) {
|
|
cfg := loadConfig(t, `
|
|
pipelines:
|
|
- id: s3-defaults
|
|
source:
|
|
backend: s3
|
|
endpoint: http://127.0.0.1:9000
|
|
bucket: source
|
|
prefix: /incoming/reports/
|
|
destinations:
|
|
- id: archive
|
|
backend: s3
|
|
endpoint: http://127.0.0.1:9000
|
|
bucket: destination
|
|
`)
|
|
|
|
source := cfg.Pipelines[0].Source
|
|
if got, want := source.Region, DefaultS3Region; got != want {
|
|
t.Fatalf("source region = %q, want %q", got, want)
|
|
}
|
|
if got, want := source.Prefix, "incoming/reports"; got != want {
|
|
t.Fatalf("source prefix = %q, want %q", got, want)
|
|
}
|
|
if !ForcePathStyle(source.ForcePath) {
|
|
t.Fatal("source force_path_style = false, want true")
|
|
}
|
|
destination := cfg.Pipelines[0].Destinations[0]
|
|
if got, want := destination.Region, DefaultS3Region; got != want {
|
|
t.Fatalf("destination region = %q, want %q", got, want)
|
|
}
|
|
if !ForcePathStyle(destination.ForcePath) {
|
|
t.Fatal("destination force_path_style = false, want true")
|
|
}
|
|
}
|
|
|
|
func TestLoadFilePreservesExplicitS3ForcePathStyleFalse(t *testing.T) {
|
|
cfg := loadConfig(t, `
|
|
pipelines:
|
|
- id: s3-force-path
|
|
source:
|
|
backend: s3
|
|
endpoint: https://s3.example.com
|
|
bucket: source
|
|
force_path_style: false
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /archive
|
|
`)
|
|
|
|
if ForcePathStyle(cfg.Pipelines[0].Source.ForcePath) {
|
|
t.Fatal("force_path_style = true, want explicit false")
|
|
}
|
|
}
|
|
|
|
func TestLoadFileRejectsDuplicatePipelineIDs(t *testing.T) {
|
|
assertLoadError(t, `
|
|
pipelines:
|
|
- id: duplicate
|
|
source:
|
|
backend: local
|
|
path: /one
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /archive
|
|
- id: duplicate
|
|
source:
|
|
backend: local
|
|
path: /two
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /archive
|
|
`, "pipeline id duplicate is duplicated")
|
|
}
|
|
|
|
func TestLoadFileRejectsDuplicateDestinationIDs(t *testing.T) {
|
|
assertLoadError(t, `
|
|
pipelines:
|
|
- id: reports
|
|
source:
|
|
backend: local
|
|
path: /source
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /archive-one
|
|
- id: archive
|
|
backend: local
|
|
path: /archive-two
|
|
`, "destination id archive is duplicated")
|
|
}
|
|
|
|
func TestLoadFileRejectsMissingRequiredFields(t *testing.T) {
|
|
tests := map[string]string{
|
|
"pipelines": ``,
|
|
"pipeline id": `pipelines: [{source: {backend: local, path: /source}, destinations: [{id: archive, backend: local, path: /archive}]}]`,
|
|
"source backend": `pipelines: [{id: reports, source: {path: /source}, destinations: [{id: archive, backend: local, path: /archive}]}]`,
|
|
"destinations": `pipelines: [{id: reports, source: {backend: local, path: /source}}]`,
|
|
"destination id": `pipelines: [{id: reports, source: {backend: local, path: /source}, destinations: [{backend: local, path: /archive}]}]`,
|
|
"local path": `pipelines: [{id: reports, source: {backend: local}, destinations: [{id: archive, backend: local, path: /archive}]}]`,
|
|
"ssh host": `pipelines: [{id: reports, source: {backend: ssh, path: /source}, destinations: [{id: archive, backend: local, path: /archive}]}]`,
|
|
"s3 bucket": `pipelines: [{id: reports, source: {backend: s3, endpoint: "https://s3.example.com"}, destinations: [{id: archive, backend: local, path: /archive}]}]`,
|
|
"publish outputs": `pipelines: [{id: reports, source: {backend: local, path: /source}, destinations: [{id: archive, backend: local, path: /archive, publish: {source: false, html: false}}]}]`,
|
|
}
|
|
|
|
for name, body := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
assertLoadError(t, body, "")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadFileRejectsInvalidS3Config(t *testing.T) {
|
|
tests := map[string]string{
|
|
"prefix traversal": `pipelines: [{id: reports, source: {backend: s3, endpoint: "https://s3.example.com", bucket: source, prefix: "../reports"}, destinations: [{id: archive, backend: local, path: /archive}]}]`,
|
|
"prefix backslash": `pipelines: [{id: reports, source: {backend: s3, endpoint: "https://s3.example.com", bucket: source, prefix: 'a\b'}, destinations: [{id: archive, backend: local, path: /archive}]}]`,
|
|
"partial creds": `pipelines: [{id: reports, source: {backend: s3, endpoint: "https://s3.example.com", bucket: source, credentials: {access_key_id_env: ACCESS_KEY_ID}}, destinations: [{id: archive, backend: local, path: /archive}]}]`,
|
|
}
|
|
for name, body := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
assertLoadError(t, body, "")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadFileRejectsInvalidHTTPUploadConfig(t *testing.T) {
|
|
tests := map[string]string{
|
|
"server size": `server: {http: {max_upload_size: 20XB}}`,
|
|
"source size": `upload_tokens: [{id: reporter, token_env: UPLOAD_TOKEN, allow_pipelines: [reports]}]
|
|
pipelines: [{id: reports, source: {backend: http_upload, max_upload_size: 20XB}, destinations: [{id: archive, backend: local, path: /archive}]}]`,
|
|
"zero source size": `upload_tokens: [{id: reporter, token_env: UPLOAD_TOKEN, allow_pipelines: [reports]}]
|
|
pipelines: [{id: reports, source: {backend: http_upload, max_upload_size: 0B}, destinations: [{id: archive, backend: local, path: /archive}]}]`,
|
|
"server duration": `server: {http: {retention: forever}}`,
|
|
"zero server duration": `server: {http: {retention: 0s}}`,
|
|
"prune duration": `pipelines: [{id: reports, source: {backend: local, path: /source}, destinations: [{id: archive, backend: local, path: /archive, retention: {prune: {enabled: true, older_than: forever}}}]}]`,
|
|
"missing upload tokens": `pipelines: [{id: reports, source: {backend: http_upload}, destinations: [{id: archive, backend: local, path: /archive}]}]`,
|
|
"destination http upload": `pipelines: [{id: reports, source: {backend: local, path: /source}, destinations: [{id: ingest, backend: http_upload}]}]`,
|
|
"literal token": `upload_tokens: [{id: reporter, token: secret, token_env: UPLOAD_TOKEN, allow_pipelines: [reports]}]
|
|
pipelines: [{id: reports, source: {backend: http_upload}, destinations: [{id: archive, backend: local, path: /archive}]}]`,
|
|
"unknown server field": `server: {http: {surprise: true}}`,
|
|
"legacy source token env": `upload_tokens: [{id: reporter, token_env: UPLOAD_TOKEN, allow_pipelines: [reports]}]
|
|
pipelines: [{id: reports, source: {backend: http_upload, token_env: UPLOAD_TOKEN}, destinations: [{id: archive, backend: local, path: /archive}]}]`,
|
|
"unknown source field": `upload_tokens: [{id: reporter, token_env: UPLOAD_TOKEN, allow_pipelines: [reports]}]
|
|
pipelines: [{id: reports, source: {backend: http_upload, surprise: true}, destinations: [{id: archive, backend: local, path: /archive}]}]`,
|
|
}
|
|
for name, body := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
assertLoadError(t, body, "")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadFileDefaultsSSHConfig(t *testing.T) {
|
|
cfg := loadConfig(t, `
|
|
pipelines:
|
|
- id: ssh-defaults
|
|
source:
|
|
backend: ssh
|
|
host: source.example.com
|
|
path: /source
|
|
destinations:
|
|
- id: archive
|
|
backend: ssh
|
|
host: destination.example.com
|
|
path: /archive
|
|
host_key_policy: false
|
|
`)
|
|
|
|
source := cfg.Pipelines[0].Source
|
|
if source.Port != 22 {
|
|
t.Fatalf("source port = %d, want 22", source.Port)
|
|
}
|
|
if source.SSH.HostKeyPolicy != HostKeyPolicyAcceptNew {
|
|
t.Fatalf("source host key policy = %q, want accept-new", source.SSH.HostKeyPolicy)
|
|
}
|
|
destination := cfg.Pipelines[0].Destinations[0]
|
|
if destination.Port != 22 {
|
|
t.Fatalf("destination port = %d, want 22", destination.Port)
|
|
}
|
|
if destination.SSH.HostKeyPolicy != HostKeyPolicyOff {
|
|
t.Fatalf("destination host key policy = %q, want off", destination.SSH.HostKeyPolicy)
|
|
}
|
|
}
|
|
|
|
func TestLoadFileNormalizesSSHHostKeyPolicies(t *testing.T) {
|
|
tests := map[string]HostKeyPolicy{
|
|
`true`: HostKeyPolicyStrict,
|
|
`"true"`: HostKeyPolicyStrict,
|
|
`strict`: HostKeyPolicyStrict,
|
|
`accept-new`: HostKeyPolicyAcceptNew,
|
|
`false`: HostKeyPolicyOff,
|
|
`"false"`: HostKeyPolicyOff,
|
|
`off`: HostKeyPolicyOff,
|
|
`"STRICT"`: HostKeyPolicyStrict,
|
|
`"ACCEPT-NEW"`: HostKeyPolicyAcceptNew,
|
|
`"OFF"`: HostKeyPolicyOff,
|
|
}
|
|
for value, want := range tests {
|
|
t.Run(value, func(t *testing.T) {
|
|
cfg := loadConfig(t, `
|
|
pipelines:
|
|
- id: ssh-policy
|
|
source:
|
|
backend: ssh
|
|
host: source.example.com
|
|
path: /source
|
|
host_key_policy: `+value+`
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /archive
|
|
`)
|
|
if got := cfg.Pipelines[0].Source.SSH.HostKeyPolicy; got != want {
|
|
t.Fatalf("host key policy = %q, want %q", got, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadFileRejectsLegacySSHURIFieldAsUnknown(t *testing.T) {
|
|
assertLoadError(t, `
|
|
pipelines:
|
|
- id: reports
|
|
source:
|
|
backend: ssh
|
|
uri: ssh://reports@example.com:22
|
|
path: /source
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /archive
|
|
`, "field uri not found")
|
|
}
|
|
|
|
func TestLoadFileRejectsUnsupportedBackend(t *testing.T) {
|
|
assertLoadError(t, `
|
|
pipelines:
|
|
- id: reports
|
|
source:
|
|
backend: ftp
|
|
path: /source
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /archive
|
|
`, "backend ftp is unsupported")
|
|
}
|
|
|
|
func TestLoadFileRejectsInvalidTransferAction(t *testing.T) {
|
|
assertLoadError(t, `
|
|
pipelines:
|
|
- id: reports
|
|
source:
|
|
backend: local
|
|
path: /source
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /archive
|
|
transfer:
|
|
on_destination_older: overwrite
|
|
`, "on_destination_older must be replace or fail")
|
|
}
|
|
|
|
func TestLoadFileRejectsInvalidValidationAction(t *testing.T) {
|
|
assertLoadError(t, `
|
|
pipelines:
|
|
- id: reports
|
|
source:
|
|
backend: local
|
|
path: /source
|
|
validation:
|
|
on_digest_mismatch: warn
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /archive
|
|
`, "on_digest_mismatch must be fail")
|
|
}
|
|
|
|
func TestLoadFileRejectsHTMLPublishWithoutTransform(t *testing.T) {
|
|
assertLoadError(t, `
|
|
pipelines:
|
|
- id: reports
|
|
source:
|
|
backend: local
|
|
path: /source
|
|
destinations:
|
|
- id: static
|
|
backend: local
|
|
path: /srv/www
|
|
publish:
|
|
source: false
|
|
html: true
|
|
`, "markdown_to_html is required")
|
|
}
|
|
|
|
func TestLoadFileRejectsUnknownFields(t *testing.T) {
|
|
assertLoadError(t, `
|
|
pipelines:
|
|
- id: reports
|
|
surprise: true
|
|
source:
|
|
backend: local
|
|
path: /source
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /archive
|
|
`, "field surprise not found")
|
|
}
|
|
|
|
func TestLoadFileRejectsUnknownSecretsFields(t *testing.T) {
|
|
assertLoadError(t, `
|
|
secrets:
|
|
directory: /run/secrets/distributor
|
|
surprise: true
|
|
pipelines:
|
|
- id: reports
|
|
source:
|
|
backend: local
|
|
path: /source
|
|
destinations:
|
|
- id: archive
|
|
backend: local
|
|
path: /archive
|
|
`, "field surprise not found")
|
|
}
|
|
|
|
func TestExampleConfigsLoad(t *testing.T) {
|
|
for _, path := range []string{
|
|
"../../examples/local-to-local.yml",
|
|
"../../examples/local-publish.yml",
|
|
"../../examples/local-html.yml",
|
|
"../../examples/local-index.yml",
|
|
"../../examples/fan-out.yml",
|
|
"../../examples/archive-and-latest.yml",
|
|
"../../examples/merge-reconciliation.yml",
|
|
"../../examples/shared-root.yml",
|
|
"../../examples/http-upload-local.yml",
|
|
"../../examples/ssh-destination.yml",
|
|
"../../examples/s3-destination.yml",
|
|
} {
|
|
t.Run(path, func(t *testing.T) {
|
|
if _, err := LoadFile(path); err != nil {
|
|
t.Fatalf("LoadFile(%q) error = %v", path, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func loadConfig(t *testing.T, body string) Config {
|
|
t.Helper()
|
|
path := writeConfig(t, body)
|
|
cfg, err := LoadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("LoadFile() error = %v", err)
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
func assertLoadError(t *testing.T, body, want string) {
|
|
t.Helper()
|
|
path := writeConfig(t, body)
|
|
_, err := LoadFile(path)
|
|
if err == nil {
|
|
t.Fatal("LoadFile() error = nil, want error")
|
|
}
|
|
if want != "" && !strings.Contains(err.Error(), want) {
|
|
t.Fatalf("LoadFile() error = %q, want substring %q", err.Error(), want)
|
|
}
|
|
}
|
|
|
|
func writeConfig(t *testing.T, body string) string {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "config.yml")
|
|
if err := os.WriteFile(path, []byte(strings.TrimSpace(body)+"\n"), 0o600); err != nil {
|
|
t.Fatalf("write config: %v", err)
|
|
}
|
|
return path
|
|
}
|