Files
ConfigKit/internal/engine/selected_overlay.go

185 lines
6.9 KiB
Go

package engine
import (
"bytes"
"errors"
"io/fs"
"strconv"
yamladapter "gitea.maximumdirect.net/eric/ConfigKit/internal/yaml"
)
func activeOverlay(name string) bool {
return name != "" && name != "default"
}
func validOverlayName(name string) bool {
if !activeOverlay(name) {
return true
}
for _, character := range name {
if (character >= 'a' && character <= 'z') ||
(character >= 'A' && character <= 'Z') ||
(character >= '0' && character <= '9') || character == '_' || character == '-' {
continue
}
return false
}
return true
}
type selectedOverlayRequest struct {
name string
filesystem resolvedFilesystemContext
protectionRule *VersionRule
sequencePolicy sequencePolicy
limits importLimits
versionState *sourceVersionState
}
func resolveOrdinaryOverlayFilesystem(source Source, context *FilesystemContext, main parsedSource, existing *resolvedFilesystemContext) (resolvedFilesystemContext, *Diagnostic) {
if source.kind == sourceNone {
if context == nil {
value := optionDiagnostic(CodeOptionsInvalid, "NoSource overlays require explicit filesystem context")
return resolvedFilesystemContext{}, &value
}
}
if existing != nil {
return *existing, nil
}
return resolveFilesystemContext(source, context, main, nil)
}
func openSelectedOverlayAccess(resolved resolvedFilesystemContext) (*filesystemAccess, *Diagnostic) {
access, err := openResolvedFilesystemAccess(resolved)
if err != nil {
value := globFilesystemDiagnostic(err, "configuration root could not be opened for the selected overlay", Location{})
value.Primary = nil
return nil, &value
}
return access, nil
}
func loadSelectedOverlay(request selectedOverlayRequest, access *filesystemAccess, base *assemblyAccumulator, sources []SourceInfo) ([]SourceInfo, []Diagnostic) {
policy := request.sequencePolicy
rule := request.protectionRule
limits := request.limits
versionState := request.versionState
closed := false
defer func() {
if !closed {
_ = access.close()
}
}()
overlayPath := access.join(access.rootPath, "overlays/"+request.name+".yml")
canonical, err := access.canonical(overlayPath)
if err != nil {
code := CodeOverlayInvalid
message := "selected overlay could not be resolved"
if errors.Is(err, fs.ErrNotExist) {
code, message = CodeOverlayMissing, "selected overlay does not exist"
} else if errors.Is(err, fs.ErrPermission) {
code, message = CodeImportPermission, "selected overlay cannot be accessed"
}
return cloneSourceInfos(sources), []Diagnostic{importDiagnostic(code, message, nil)}
}
if !access.contains(canonical) {
return cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeOverlayOutsideRoot, "selected overlay resolves outside the configuration root", nil)}
}
for _, existing := range sources {
if existing.Path != "" && existing.Path == canonical &&
((access.native == nil && existing.Kind == SourceKindFile) ||
(access.native != nil && existing.Kind == SourceKindFilesystem && existing.namespace == access.token)) {
return cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeOverlayRepeated, "selected overlay is already present in the base source graph", nil)}
}
}
if len(sources)+1 > limits.sources {
return cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeOverlayLimitExceeded, "selected overlay exceeds the source-count limit", nil)}
}
if _, err := access.relative(canonical); err != nil {
return cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeOverlayOutsideRoot, "selected overlay could not be addressed inside the configuration root", nil)}
}
sourceID := "source-" + strconv.Itoa(len(sources)+1)
overlayLocation := &Location{SourceID: sourceID}
kind := SourceKindFile
label := "overlays/" + request.name + ".yml"
if access.native != nil {
kind = SourceKindFilesystem
label = access.label + "/" + canonical
}
overlaySource := SourceInfo{ID: sourceID, Label: label, Path: canonical, Layer: LayerOverlay, Kind: kind}
if kind == SourceKindFilesystem {
overlaySource.namespace = access.token
}
sources = append(cloneSourceInfos(sources), overlaySource)
data, diagnostic := readOverlayFile(access, canonical, sourceID, limits.bytes-base.current.inputBytes)
if diagnostic != nil {
return sources, []Diagnostic{*diagnostic}
}
overlayRoot, issues := yamladapter.Parse(bytes.NewReader(data), sourceID, yamladapter.Limits{Depth: maxDocumentDepth, Nodes: maxDocumentNodes})
if len(issues) != 0 {
return sources, parserDiagnostics(sourceID, issues)
}
nodeCount := countInputNodes(overlayRoot)
if base.current.inputNodes+nodeCount > limits.nodes {
return sources, []Diagnostic{importDiagnostic(CodeOverlayLimitExceeded, "selected overlay exceeds the aggregate node limit", overlayLocation)}
}
if diagnostics := overlayDirectiveDiagnostics(overlayRoot, Path{}, true); len(diagnostics) != 0 {
return sources, diagnostics
}
var processed sourceVersionProcessingResult
var failures []Diagnostic
if versionState != nil {
processed, failures = versionState.processWithDirectiveContext(
overlayRoot, sourceVersionContribution, Path{}, sourceID, placementModeMapping, rootDirectiveContextOverlay,
)
} else {
processed = processSourceVersionWithDirectiveContext(
overlayRoot, rule, sourceVersionContribution, Path{}, sourceVersionModeDefault, rootDirectiveContextOverlay,
)
failures = processed.diagnostics
}
if len(failures) != 0 {
return sources, failures
}
overlayPayload := processed.payload
closeErr := access.close()
closed = true
if closeErr != nil {
return sources, []Diagnostic{importDiagnostic(CodeImportRead, "configuration root could not be closed after reading the selected overlay", nil)}
}
overlayDiagnostics := base.applyOverlayWithCollectionPolicy(overlayPayload, nil, collectionPolicyScope{}, nil, LayerOverlay, policy, processed.sequences)
if len(overlayDiagnostics) != 0 {
return sources, overlayDiagnostics
}
// The selected overlay retains one application with the actual item
// assignments. Raw list indexes cannot identify an upsert's destination.
application := base.current.applications[len(base.current.applications)-1]
if node := versionClaimFromApplication(application, rule); node != nil {
location := publicLocation(node.Location)
return sources, []Diagnostic{{
Code: CodeOverlayVersion, Severity: SeverityError, Path: rule.Path.clone(),
Message: "overlays cannot contribute to or replace the schema-version path", Primary: &location,
}}
}
base.current.inputBytes += int64(len(data))
base.current.inputNodes += nodeCount
return sources, nil
}
func readOverlayFile(access *filesystemAccess, name, sourceID string, remaining int64) ([]byte, *Diagnostic) {
limit := min(int64(maxSourceBytes), remaining)
if limit < 0 {
limit = 0
}
data, failure := readRegularFile(name, access.stat, access.open, limit)
if failure != nil {
value := sourceReadDiagnostic(failure, sourceID, CodeOverlayLimitExceeded, "selected overlay exceeds a source or aggregate byte limit")
return nil, &value
}
return data, nil
}