Centralized defaults into internal/config/defaults.go
This commit is contained in:
@@ -65,6 +65,8 @@ Optional secrets-from-files config:
|
|||||||
|
|
||||||
YAML decoding is strict (`KnownFields(true)`), so unknown fields fail fast.
|
YAML decoding is strict (`KnownFields(true)`), so unknown fields fail fast.
|
||||||
|
|
||||||
|
Maintainer note: application defaults are centralized in [`internal/config/defaults.go`](internal/config/defaults.go).
|
||||||
|
|
||||||
## Storage And Archive Foundations
|
## Storage And Archive Foundations
|
||||||
|
|
||||||
Narratio now includes configuration and path-model foundations for archive support, plus implemented prepare-stage S3 audio input.
|
Narratio now includes configuration and path-model foundations for archive support, plus implemented prepare-stage S3 audio input.
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ CLI pipeline config path resolution:
|
|||||||
- when `--config` is omitted, Narratio searches defaults in order:
|
- when `--config` is omitted, Narratio searches defaults in order:
|
||||||
- `/usr/local/etc/narratio/pipeline.yml`
|
- `/usr/local/etc/narratio/pipeline.yml`
|
||||||
- `/etc/narratio/pipeline.yml`
|
- `/etc/narratio/pipeline.yml`
|
||||||
|
- default values are centralized in `internal/config/defaults.go`
|
||||||
|
|
||||||
CLI session config path resolution:
|
CLI session config path resolution:
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package artifacts
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SessionPaths contains canonical local paths for one session work directory.
|
// SessionPaths contains canonical local paths for one session work directory.
|
||||||
@@ -22,34 +24,33 @@ type SessionPaths struct {
|
|||||||
|
|
||||||
// SessionWorkDir returns the work directory for one session.
|
// SessionWorkDir returns the work directory for one session.
|
||||||
func SessionWorkDir(rootDir, sessionID string) string {
|
func SessionWorkDir(rootDir, sessionID string) string {
|
||||||
return filepath.Join(rootDir, "work", sessionID)
|
return filepath.Join(rootDir, config.PathWorkDirSegment, sessionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SessionRunWorkDir returns the campaign/session/run scoped local work directory.
|
// SessionRunWorkDir returns the campaign/session/run scoped local work directory.
|
||||||
func SessionRunWorkDir(rootDir, campaign, sessionID, runID string) string {
|
func SessionRunWorkDir(rootDir, campaign, sessionID, runID string) string {
|
||||||
return filepath.Join(rootDir, "work", campaign, sessionID, runID)
|
return filepath.Join(rootDir, config.PathWorkDirSegment, campaign, sessionID, runID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SessionSpoolAudioDir returns the campaign/session/run scoped local spool audio path.
|
// SessionSpoolAudioDir returns the campaign/session/run scoped local spool audio path.
|
||||||
func SessionSpoolAudioDir(spoolRoot, campaign, sessionID, runID string) string {
|
func SessionSpoolAudioDir(spoolRoot, campaign, sessionID, runID string) string {
|
||||||
return filepath.Join(spoolRoot, campaign, sessionID, runID, "audio")
|
return filepath.Join(spoolRoot, campaign, sessionID, runID, config.PathAudioDirSegment)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildSessionPaths(workspaceRoot, sessionID string) SessionPaths {
|
func buildSessionPaths(workspaceRoot, sessionID string) SessionPaths {
|
||||||
root := SessionWorkDir(workspaceRoot, sessionID)
|
root := SessionWorkDir(workspaceRoot, sessionID)
|
||||||
transcripts := filepath.Join(root, "transcripts")
|
|
||||||
return SessionPaths{
|
return SessionPaths{
|
||||||
WorkspaceRoot: workspaceRoot,
|
WorkspaceRoot: workspaceRoot,
|
||||||
Root: root,
|
Root: root,
|
||||||
InputsDir: filepath.Join(root, "inputs"),
|
InputsDir: filepath.Join(root, config.PathInputsDirSegment),
|
||||||
AudioDir: filepath.Join(root, "audio"),
|
AudioDir: filepath.Join(root, config.PathAudioDirSegment),
|
||||||
TranscriptsDir: transcripts,
|
TranscriptsDir: filepath.Join(root, config.PathTranscriptsSegment),
|
||||||
TranscriptsRawDir: filepath.Join(transcripts, "raw"),
|
TranscriptsRawDir: filepath.Join(root, filepath.FromSlash(config.PathTranscriptsRaw)),
|
||||||
TranscriptsTrimmedDir: filepath.Join(transcripts, "trimmed"),
|
TranscriptsTrimmedDir: filepath.Join(root, filepath.FromSlash(config.PathTranscriptsTrimmed)),
|
||||||
ArtifactsDir: filepath.Join(root, "artifacts"),
|
ArtifactsDir: filepath.Join(root, config.PathArtifactsDirSegment),
|
||||||
ConfigDir: filepath.Join(root, "config"),
|
ConfigDir: filepath.Join(root, config.PathConfigDirSegment),
|
||||||
LogsDir: filepath.Join(root, "logs"),
|
LogsDir: filepath.Join(root, config.PathLogsDirSegment),
|
||||||
ManifestPath: filepath.Join(root, "manifest.json"),
|
ManifestPath: filepath.Join(root, config.PathManifestFile),
|
||||||
LockPath: filepath.Join(root, ".lock"),
|
LockPath: filepath.Join(root, config.PathLockFile),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package artifacts
|
|||||||
import (
|
import (
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// S3SessionPrefix builds the canonical S3 session prefix.
|
// S3SessionPrefix builds the canonical S3 session prefix.
|
||||||
@@ -10,9 +12,9 @@ import (
|
|||||||
func S3SessionPrefix(rootPrefix, campaign, sessionID string) string {
|
func S3SessionPrefix(rootPrefix, campaign, sessionID string) string {
|
||||||
prefix := path.Join(
|
prefix := path.Join(
|
||||||
cleanS3PathPart(rootPrefix),
|
cleanS3PathPart(rootPrefix),
|
||||||
"campaigns",
|
config.S3CampaignsSegment,
|
||||||
cleanS3PathPart(campaign),
|
cleanS3PathPart(campaign),
|
||||||
"sessions",
|
config.S3SessionsSegment,
|
||||||
cleanS3PathPart(sessionID),
|
cleanS3PathPart(sessionID),
|
||||||
)
|
)
|
||||||
return ensureS3TrailingSlash(prefix)
|
return ensureS3TrailingSlash(prefix)
|
||||||
@@ -21,7 +23,7 @@ func S3SessionPrefix(rootPrefix, campaign, sessionID string) string {
|
|||||||
// S3RunPrefix builds the canonical S3 run prefix.
|
// S3RunPrefix builds the canonical S3 run prefix.
|
||||||
// Format: {session_prefix}/runs/{run_id}/
|
// Format: {session_prefix}/runs/{run_id}/
|
||||||
func S3RunPrefix(sessionPrefix, runID string) string {
|
func S3RunPrefix(sessionPrefix, runID string) string {
|
||||||
prefix := path.Join(strings.TrimSuffix(cleanS3Key(sessionPrefix), "/"), "runs", cleanS3PathPart(runID))
|
prefix := path.Join(strings.TrimSuffix(cleanS3Key(sessionPrefix), "/"), config.S3RunsSegment, cleanS3PathPart(runID))
|
||||||
return ensureS3TrailingSlash(prefix)
|
return ensureS3TrailingSlash(prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,13 +37,13 @@ func S3AudioPrefix(sessionPrefix, audioPrefix string) string {
|
|||||||
// S3CurrentManifestKey returns the current manifest pointer key.
|
// S3CurrentManifestKey returns the current manifest pointer key.
|
||||||
// Format: {session_prefix}/current/manifest.json
|
// Format: {session_prefix}/current/manifest.json
|
||||||
func S3CurrentManifestKey(sessionPrefix string) string {
|
func S3CurrentManifestKey(sessionPrefix string) string {
|
||||||
return path.Join(strings.TrimSuffix(cleanS3Key(sessionPrefix), "/"), "current", "manifest.json")
|
return path.Join(strings.TrimSuffix(cleanS3Key(sessionPrefix), "/"), config.S3CurrentSegment, config.S3ManifestFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// S3CurrentRunPointerKey returns the current run pointer key.
|
// S3CurrentRunPointerKey returns the current run pointer key.
|
||||||
// Format: {session_prefix}/current/run_id.txt
|
// Format: {session_prefix}/current/run_id.txt
|
||||||
func S3CurrentRunPointerKey(sessionPrefix string) string {
|
func S3CurrentRunPointerKey(sessionPrefix string) string {
|
||||||
return path.Join(strings.TrimSuffix(cleanS3Key(sessionPrefix), "/"), "current", "run_id.txt")
|
return path.Join(strings.TrimSuffix(cleanS3Key(sessionPrefix), "/"), config.S3CurrentSegment, config.S3RunIDFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// S3PromotedArtifactKey returns the destination key for one promoted artifact.
|
// S3PromotedArtifactKey returns the destination key for one promoted artifact.
|
||||||
|
|||||||
@@ -10,8 +10,69 @@ const (
|
|||||||
DefaultSessionConfigPathEtc = "/etc/narratio/session.yml"
|
DefaultSessionConfigPathEtc = "/etc/narratio/session.yml"
|
||||||
DefaultS3AccessKeyIDEnv = "OBJECT_STORAGE_KEY_ID"
|
DefaultS3AccessKeyIDEnv = "OBJECT_STORAGE_KEY_ID"
|
||||||
DefaultS3SecretAccessKeyEnv = "OBJECT_STORAGE_KEY"
|
DefaultS3SecretAccessKeyEnv = "OBJECT_STORAGE_KEY"
|
||||||
|
DefaultStorageS3RootPrefix = "dnd"
|
||||||
|
DefaultSpoolRoot = "/var/spool/narratio"
|
||||||
|
|
||||||
|
DefaultWhisperXLanguage = "en"
|
||||||
|
DefaultWhisperXTimeout = "30m"
|
||||||
|
DefaultWhisperXRetryDelay = "2s"
|
||||||
|
DefaultWhisperXConcurrency = 2
|
||||||
|
DefaultWhisperXRetries = 3
|
||||||
|
|
||||||
|
DefaultSeriatimBinary = "seriatim"
|
||||||
|
DefaultSeriatimTimeout = "10m"
|
||||||
|
DefaultSeriatimOutputSchema = "seriatim-intermediate"
|
||||||
|
DefaultSeriatimCoalesceGap = 3.0
|
||||||
|
DefaultSeriatimReport = true
|
||||||
|
|
||||||
|
DefaultAuditaBinary = "audita"
|
||||||
|
DefaultAuditaTimeout = "3h"
|
||||||
|
DefaultAuditaReport = true
|
||||||
|
|
||||||
|
DefaultScriptoriumBinary = "scriptorium"
|
||||||
|
DefaultScriptoriumTimeout = "10m"
|
||||||
|
|
||||||
|
DefaultTrimBoundsTimeout = "10m"
|
||||||
|
DefaultTrimSeriatimReport = false
|
||||||
|
|
||||||
|
DefaultNormalizeOutputPath = "transcripts/normalized.json"
|
||||||
|
DefaultNormalizeOutputSchema = "seriatim-intermediate"
|
||||||
|
DefaultNormalizeReport = true
|
||||||
|
|
||||||
|
DefaultArchiveEnabled = true
|
||||||
|
DefaultArchiveUploadRun = true
|
||||||
|
|
||||||
|
PathWorkDirSegment = "work"
|
||||||
|
PathInputsDirSegment = "inputs"
|
||||||
|
PathAudioDirSegment = "audio"
|
||||||
|
PathTranscriptsSegment = "transcripts"
|
||||||
|
PathTranscriptsRaw = "transcripts/raw"
|
||||||
|
PathTranscriptsTrimmed = "transcripts/trimmed"
|
||||||
|
PathArtifactsDirSegment = "artifacts"
|
||||||
|
PathConfigDirSegment = "config"
|
||||||
|
PathLogsDirSegment = "logs"
|
||||||
|
PathManifestFile = "manifest.json"
|
||||||
|
PathLockFile = ".lock"
|
||||||
|
PathTranscriptMerged = "transcripts/merged.json"
|
||||||
|
PathTranscriptProcessed = "transcripts/processed.json"
|
||||||
|
PathTranscriptNormalized = "transcripts/normalized.json"
|
||||||
|
PathTranscriptTrimmed = "transcripts/trimmed.json"
|
||||||
|
|
||||||
|
S3CampaignsSegment = "campaigns"
|
||||||
|
S3SessionsSegment = "sessions"
|
||||||
|
S3RunsSegment = "runs"
|
||||||
|
S3CurrentSegment = "current"
|
||||||
|
S3ManifestFile = "manifest.json"
|
||||||
|
S3RunIDFile = "run_id.txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DefaultArchivePromoteArtifacts defines the default archive promotion rules.
|
||||||
|
// Callers should copy this slice before mutating.
|
||||||
|
var DefaultArchivePromoteArtifacts = []ArchivePromotionRule{
|
||||||
|
{From: PathTranscriptTrimmed, To: PathTranscriptTrimmed},
|
||||||
|
{From: "artifacts/session_recap.md", To: "artifacts/session_recap.md"},
|
||||||
|
}
|
||||||
|
|
||||||
// DefaultPipelineConfigSearchPaths defines the default search order for
|
// DefaultPipelineConfigSearchPaths defines the default search order for
|
||||||
// pipeline.yml when callers do not provide an explicit path.
|
// pipeline.yml when callers do not provide an explicit path.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ func applyStorageDefaults(cfg *StorageConfig) {
|
|||||||
cfg.S3 = &StorageS3Config{}
|
cfg.S3 = &StorageS3Config{}
|
||||||
}
|
}
|
||||||
if cfg.S3.RootPrefix == "" {
|
if cfg.S3.RootPrefix == "" {
|
||||||
cfg.S3.RootPrefix = "dnd"
|
cfg.S3.RootPrefix = DefaultStorageS3RootPrefix
|
||||||
}
|
}
|
||||||
if cfg.S3.AccessKeyIDEnv == "" {
|
if cfg.S3.AccessKeyIDEnv == "" {
|
||||||
cfg.S3.AccessKeyIDEnv = DefaultS3AccessKeyIDEnv
|
cfg.S3.AccessKeyIDEnv = DefaultS3AccessKeyIDEnv
|
||||||
@@ -189,7 +189,7 @@ func applySpoolDefaults(cfg *SpoolConfig) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if cfg.Root == "" {
|
if cfg.Root == "" {
|
||||||
cfg.Root = "/var/spool/narratio"
|
cfg.Root = DefaultSpoolRoot
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,16 +202,13 @@ func applyArchiveDefaults(cfg **ArchiveConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (*cfg).Enabled == nil {
|
if (*cfg).Enabled == nil {
|
||||||
(*cfg).Enabled = boolPtr(true)
|
(*cfg).Enabled = boolPtr(DefaultArchiveEnabled)
|
||||||
}
|
}
|
||||||
if (*cfg).UploadRun == nil {
|
if (*cfg).UploadRun == nil {
|
||||||
(*cfg).UploadRun = boolPtr(true)
|
(*cfg).UploadRun = boolPtr(DefaultArchiveUploadRun)
|
||||||
}
|
}
|
||||||
if len((*cfg).PromoteArtifacts) == 0 {
|
if len((*cfg).PromoteArtifacts) == 0 {
|
||||||
(*cfg).PromoteArtifacts = []ArchivePromotionRule{
|
(*cfg).PromoteArtifacts = append([]ArchivePromotionRule(nil), DefaultArchivePromoteArtifacts...)
|
||||||
{From: "transcripts/trimmed.json", To: "transcripts/trimmed.json", Required: boolPtr(true)},
|
|
||||||
{From: "artifacts/session_recap.md", To: "artifacts/session_recap.md", Required: boolPtr(true)},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for i := range (*cfg).PromoteArtifacts {
|
for i := range (*cfg).PromoteArtifacts {
|
||||||
if (*cfg).PromoteArtifacts[i].Required == nil {
|
if (*cfg).PromoteArtifacts[i].Required == nil {
|
||||||
@@ -225,19 +222,19 @@ func applyWhisperXDefaults(cfg *WhisperXConfig) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if cfg.Language == "" {
|
if cfg.Language == "" {
|
||||||
cfg.Language = "en"
|
cfg.Language = DefaultWhisperXLanguage
|
||||||
}
|
}
|
||||||
if cfg.Timeout == "" {
|
if cfg.Timeout == "" {
|
||||||
cfg.Timeout = "30m"
|
cfg.Timeout = DefaultWhisperXTimeout
|
||||||
}
|
}
|
||||||
if cfg.RetryDelay == "" {
|
if cfg.RetryDelay == "" {
|
||||||
cfg.RetryDelay = "2s"
|
cfg.RetryDelay = DefaultWhisperXRetryDelay
|
||||||
}
|
}
|
||||||
if cfg.Concurrency == nil {
|
if cfg.Concurrency == nil {
|
||||||
cfg.Concurrency = intPtr(2)
|
cfg.Concurrency = intPtr(DefaultWhisperXConcurrency)
|
||||||
}
|
}
|
||||||
if cfg.Retries == nil {
|
if cfg.Retries == nil {
|
||||||
cfg.Retries = intPtr(3)
|
cfg.Retries = intPtr(DefaultWhisperXRetries)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,19 +248,19 @@ func applySeriatimDefaults(cfg *SeriatimConfig) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if cfg.Binary == "" {
|
if cfg.Binary == "" {
|
||||||
cfg.Binary = "seriatim"
|
cfg.Binary = DefaultSeriatimBinary
|
||||||
}
|
}
|
||||||
if cfg.Timeout == "" {
|
if cfg.Timeout == "" {
|
||||||
cfg.Timeout = "10m"
|
cfg.Timeout = DefaultSeriatimTimeout
|
||||||
}
|
}
|
||||||
if cfg.OutputSchema == "" {
|
if cfg.OutputSchema == "" {
|
||||||
cfg.OutputSchema = "seriatim-intermediate"
|
cfg.OutputSchema = DefaultSeriatimOutputSchema
|
||||||
}
|
}
|
||||||
if cfg.CoalesceGap == nil {
|
if cfg.CoalesceGap == nil {
|
||||||
cfg.CoalesceGap = float64Ptr(3.0)
|
cfg.CoalesceGap = float64Ptr(DefaultSeriatimCoalesceGap)
|
||||||
}
|
}
|
||||||
if cfg.Report == nil {
|
if cfg.Report == nil {
|
||||||
cfg.Report = boolPtr(true)
|
cfg.Report = boolPtr(DefaultSeriatimReport)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,13 +269,13 @@ func applyAuditaDefaults(cfg *AuditaConfig) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if cfg.Binary == "" {
|
if cfg.Binary == "" {
|
||||||
cfg.Binary = "audita"
|
cfg.Binary = DefaultAuditaBinary
|
||||||
}
|
}
|
||||||
if cfg.Timeout == "" {
|
if cfg.Timeout == "" {
|
||||||
cfg.Timeout = "3h"
|
cfg.Timeout = DefaultAuditaTimeout
|
||||||
}
|
}
|
||||||
if cfg.Report == nil {
|
if cfg.Report == nil {
|
||||||
cfg.Report = boolPtr(true)
|
cfg.Report = boolPtr(DefaultAuditaReport)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,10 +284,10 @@ func applyScriptoriumDefaults(cfg *ScriptoriumConfig) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if cfg.Binary == "" {
|
if cfg.Binary == "" {
|
||||||
cfg.Binary = "scriptorium"
|
cfg.Binary = DefaultScriptoriumBinary
|
||||||
}
|
}
|
||||||
if cfg.Timeout == "" {
|
if cfg.Timeout == "" {
|
||||||
cfg.Timeout = "10m"
|
cfg.Timeout = DefaultScriptoriumTimeout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,10 +296,10 @@ func applyTrimDefaults(cfg *TrimConfig) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if cfg.Bounds.Timeout == "" {
|
if cfg.Bounds.Timeout == "" {
|
||||||
cfg.Bounds.Timeout = "10m"
|
cfg.Bounds.Timeout = DefaultTrimBoundsTimeout
|
||||||
}
|
}
|
||||||
if cfg.Seriatim.Report == nil {
|
if cfg.Seriatim.Report == nil {
|
||||||
cfg.Seriatim.Report = boolPtr(false)
|
cfg.Seriatim.Report = boolPtr(DefaultTrimSeriatimReport)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,13 +308,13 @@ func applyNormalizeDefaults(cfg *NormalizeConfig) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if cfg.OutputSchema == "" {
|
if cfg.OutputSchema == "" {
|
||||||
cfg.OutputSchema = defaultNormalizeOutputSchema
|
cfg.OutputSchema = DefaultNormalizeOutputSchema
|
||||||
}
|
}
|
||||||
if cfg.OutputPath == "" && !cfg.outputPathWasSet() {
|
if cfg.OutputPath == "" && !cfg.outputPathWasSet() {
|
||||||
cfg.OutputPath = defaultNormalizeOutputPath
|
cfg.OutputPath = DefaultNormalizeOutputPath
|
||||||
}
|
}
|
||||||
if cfg.Report == nil {
|
if cfg.Report == nil {
|
||||||
cfg.Report = boolPtr(true)
|
cfg.Report = boolPtr(DefaultNormalizeReport)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
defaultNormalizeOutputPath = "transcripts/normalized.json"
|
|
||||||
defaultNormalizeOutputSchema = "seriatim-intermediate"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UnmarshalYAML tracks explicit normalize.output_path presence so validation can
|
// UnmarshalYAML tracks explicit normalize.output_path presence so validation can
|
||||||
// distinguish omitted vs explicitly empty values.
|
// distinguish omitted vs explicitly empty values.
|
||||||
func (cfg *NormalizeConfig) UnmarshalYAML(node *yaml.Node) error {
|
func (cfg *NormalizeConfig) UnmarshalYAML(node *yaml.Node) error {
|
||||||
|
|||||||
Reference in New Issue
Block a user