Harden parser against real YAML fixtures
This commit is contained in:
parent
906f0c454d
commit
bc54528353
1
.gitignore
vendored
1
.gitignore
vendored
@ -72,6 +72,7 @@
|
||||
# Tooling logs (cron, hooks, local scripts)
|
||||
*.log
|
||||
**/*.log
|
||||
.derived-data-log-*
|
||||
|
||||
# Local SQLite DB created by autoMigrate at first boot.
|
||||
# Codeweaver scaffolds (template-time/bookings/crm/inventory) all default
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
--wrapparameters before-first
|
||||
--wrapcollections before-first
|
||||
--self remove
|
||||
--exclude .build,DerivedData
|
||||
|
||||
# PureYAML uses OpenAPIKit-style `test_...` method names with Swift Testing.
|
||||
--disable swiftTestingTestCaseNames
|
||||
|
||||
@ -7,6 +7,7 @@ opt_in_rules:
|
||||
- force_unwrapping
|
||||
excluded:
|
||||
- .build
|
||||
- DerivedData
|
||||
force_unwrapping: error
|
||||
force_try: error
|
||||
force_cast: error
|
||||
|
||||
@ -30,11 +30,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Add first-class complex mapping keys through `Model.Pair.keyNode`, including
|
||||
parsing, deterministic validation paths, duplicate-key checks, and block/flow
|
||||
dumping for sequence and mapping keys.
|
||||
- Add a real-world YAML fixture corpus with representative assertions for
|
||||
short, medium, and very large documents, including OpenAPI and Kubernetes
|
||||
examples.
|
||||
- Add deliberately failing YAML fixtures that demonstrate exact validation
|
||||
diagnostics, strict failures, and non-strict warning returns.
|
||||
|
||||
### Changed
|
||||
|
||||
- Tokenize YAML document start and end markers so scanner, event-parser, and
|
||||
stream tests can pin exact document-boundary behavior.
|
||||
- Harden block mapping and multiline plain-scalar parsing around nested
|
||||
sequences, compact mapping entries, explicit complex keys, tagged keys, and
|
||||
lower-column mapping siblings.
|
||||
- Keep quoted `"<<":` and explicitly string-tagged `!!str <<` keys as ordinary
|
||||
string keys while merge syntax is expanded during parsing.
|
||||
- Keep Foundation-backed tags such as `!!timestamp` and `!!binary` as ordinary
|
||||
|
||||
@ -20,6 +20,9 @@ let package = Package(
|
||||
name: "PureYAMLTests",
|
||||
dependencies: ["PureYAML"],
|
||||
path: "Tests",
|
||||
resources: [
|
||||
.copy("Fixtures"),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@ -37,6 +37,13 @@ extension PureYAML.Parsing.Scanner {
|
||||
|| character == "}"
|
||||
}
|
||||
|
||||
func isBlockMappingValueBoundary(_ character: Character?) -> Bool {
|
||||
guard let character else {
|
||||
return true
|
||||
}
|
||||
return character == " " || isLineBreak(character)
|
||||
}
|
||||
|
||||
func isTokenTerminator(_ character: Character) -> Bool {
|
||||
character == " "
|
||||
|| isLineBreak(character)
|
||||
@ -48,6 +55,29 @@ extension PureYAML.Parsing.Scanner {
|
||||
|| character == "#"
|
||||
}
|
||||
|
||||
func canStartAnchorName(_ character: Character?) -> Bool {
|
||||
guard let character else {
|
||||
return false
|
||||
}
|
||||
return !isTokenTerminator(character)
|
||||
&& character != "*"
|
||||
&& character != "&"
|
||||
&& character != "!"
|
||||
&& character != "\""
|
||||
&& character != "'"
|
||||
}
|
||||
|
||||
func hasNodeOnSameLine(afterCurrentPositionIn reader: PureYAML.Parsing.Reader) -> Bool {
|
||||
var offset = 0
|
||||
while reader.peek(offset: offset) == " " {
|
||||
offset += 1
|
||||
}
|
||||
guard let character = reader.peek(offset: offset) else {
|
||||
return false
|
||||
}
|
||||
return !isLineBreak(character) && character != "#"
|
||||
}
|
||||
|
||||
func isFlowDelimiter(_ character: Character) -> Bool {
|
||||
character == ","
|
||||
|| character == "["
|
||||
@ -74,6 +104,91 @@ extension PureYAML.Parsing.Scanner {
|
||||
return String(value[..<end])
|
||||
}
|
||||
|
||||
func scanPlainScalarContinuations(
|
||||
after firstLine: String,
|
||||
state: inout State,
|
||||
) -> String {
|
||||
var value = firstLine
|
||||
while true {
|
||||
var probe = state.reader
|
||||
guard isLineBreak(probe.peek()) else {
|
||||
return value
|
||||
}
|
||||
|
||||
probe.advance()
|
||||
var blankLineCount = 0
|
||||
while true {
|
||||
var blankProbe = probe
|
||||
_ = blankProbe.consume { character in
|
||||
character == " "
|
||||
}
|
||||
guard isLineBreak(blankProbe.peek()) else {
|
||||
break
|
||||
}
|
||||
blankProbe.advance()
|
||||
probe = blankProbe
|
||||
blankLineCount += 1
|
||||
}
|
||||
|
||||
_ = probe.consume { character in
|
||||
character == " "
|
||||
}
|
||||
let indentationWidth = probe.mark.column - 1
|
||||
|
||||
guard !probe.isAtEnd, !isLineBreak(probe.peek()), probe.peek() != "#" else {
|
||||
return value
|
||||
}
|
||||
guard !isBlockEntry(probe) else {
|
||||
return value
|
||||
}
|
||||
|
||||
let baseIndentation = plainScalarContinuationBaseIndentation(state: state)
|
||||
guard indentationWidth > baseIndentation else {
|
||||
return value
|
||||
}
|
||||
|
||||
var continuationState = state
|
||||
continuationState.reader = probe
|
||||
let continuation = scanPlainScalarContinuationLine(&continuationState)
|
||||
guard !isAtMappingValueIndicator(state: continuationState) else {
|
||||
return value
|
||||
}
|
||||
|
||||
state = continuationState
|
||||
if blankLineCount == 0 {
|
||||
value += " "
|
||||
} else {
|
||||
value += String(repeating: "\n", count: blankLineCount)
|
||||
}
|
||||
value += continuation
|
||||
}
|
||||
}
|
||||
|
||||
func plainScalarContinuationBaseIndentation(state: State) -> Int {
|
||||
state.indentation.last ?? 0
|
||||
}
|
||||
|
||||
func scanPlainScalarContinuationLine(_ state: inout State) -> String {
|
||||
var value = ""
|
||||
while let character = state.reader.peek(), !isLineBreak(character) {
|
||||
if character == " ", state.reader.peek(offset: 1) == "#" {
|
||||
break
|
||||
}
|
||||
if character == "#", value.isEmpty || value.last == " " {
|
||||
break
|
||||
}
|
||||
if character == ":", shouldEndPlainScalarAtMappingValue(state: state) {
|
||||
break
|
||||
}
|
||||
if state.flowDepth > 0, isFlowDelimiter(character) {
|
||||
break
|
||||
}
|
||||
value.append(character)
|
||||
state.reader.advance()
|
||||
}
|
||||
return trimTrailingSpaces(value)
|
||||
}
|
||||
|
||||
func skipLine(_ state: inout State) {
|
||||
_ = state.reader.consume { character in
|
||||
!isLineBreak(character)
|
||||
@ -108,7 +223,7 @@ extension PureYAML.Parsing.Scanner {
|
||||
if character == "#", accumulated.isEmpty || accumulated.last == " " {
|
||||
return true
|
||||
}
|
||||
if character == ":", isMappingValueBoundary(state.reader.peek(offset: 1)) {
|
||||
if character == ":", shouldEndPlainScalarAtMappingValue(state: state) {
|
||||
return true
|
||||
}
|
||||
if state.flowDepth > 0, isFlowDelimiter(character) {
|
||||
@ -116,4 +231,15 @@ extension PureYAML.Parsing.Scanner {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func shouldEndPlainScalarAtMappingValue(state: State) -> Bool {
|
||||
if state.flowDepth > 0 {
|
||||
return isMappingValueBoundary(state.reader.peek(offset: 1))
|
||||
}
|
||||
return isBlockMappingValueBoundary(state.reader.peek(offset: 1))
|
||||
}
|
||||
|
||||
func isAtMappingValueIndicator(state: State) -> Bool {
|
||||
state.reader.peek() == ":" && shouldEndPlainScalarAtMappingValue(state: state)
|
||||
}
|
||||
}
|
||||
|
||||
153
Sources/Parsing/Scanner.Indentation.swift
Normal file
153
Sources/Parsing/Scanner.Indentation.swift
Normal file
@ -0,0 +1,153 @@
|
||||
extension PureYAML.Parsing.Scanner {
|
||||
func scanIndentation(_ state: inout State) throws {
|
||||
let start = state.reader.mark
|
||||
let width = try scanIndentationWidth(&state)
|
||||
guard shouldProcessIndentation(state) else {
|
||||
return
|
||||
}
|
||||
|
||||
let end = state.reader.mark
|
||||
let current = state.indentation.last ?? 0
|
||||
if consumeBlockScalarContentIndentation(width: width, state: &state) {
|
||||
return
|
||||
}
|
||||
|
||||
if let indicator = state.pendingBlockScalarIndentationIndicator {
|
||||
try applyExplicitBlockScalarIndentation(
|
||||
indicator: indicator,
|
||||
width: width,
|
||||
current: current,
|
||||
marks: (start: start, end: end),
|
||||
state: &state,
|
||||
)
|
||||
} else if width > current {
|
||||
applyIncreasedIndentation(width: width, start: start, end: end, state: &state)
|
||||
} else if width < current {
|
||||
try applyDedentation(width: width, start: start, end: end, state: &state)
|
||||
}
|
||||
|
||||
pruneImplicitIndentation(&state)
|
||||
clearPendingBlockScalarIndentation(&state)
|
||||
}
|
||||
|
||||
func scanIndentationWidth(_ state: inout State) throws -> Int {
|
||||
var width = 0
|
||||
while let character = state.reader.peek() {
|
||||
if character == " " {
|
||||
width += 1
|
||||
state.reader.advance()
|
||||
} else if character == "\t" {
|
||||
throw PureYAML.Parsing.ParseError.tabIndentation(line: state.reader.mark.line)
|
||||
} else {
|
||||
return width
|
||||
}
|
||||
}
|
||||
return width
|
||||
}
|
||||
|
||||
func shouldProcessIndentation(_ state: State) -> Bool {
|
||||
guard !state.reader.isAtEnd, !isLineBreak(state.reader.peek()) else {
|
||||
return false
|
||||
}
|
||||
guard state.reader.peek() != "#" else {
|
||||
return false
|
||||
}
|
||||
return state.flowDepth == 0
|
||||
}
|
||||
|
||||
func consumeBlockScalarContentIndentation(
|
||||
width: Int,
|
||||
state: inout State,
|
||||
) -> Bool {
|
||||
guard let blockScalarIndentation = state.blockScalarIndentation else {
|
||||
return false
|
||||
}
|
||||
guard width < blockScalarIndentation else {
|
||||
state.blockScalarContentPrefix = String(repeating: " ", count: width - blockScalarIndentation)
|
||||
return true
|
||||
}
|
||||
state.blockScalarIndentation = nil
|
||||
state.blockScalarContentPrefix = ""
|
||||
return false
|
||||
}
|
||||
|
||||
func applyExplicitBlockScalarIndentation(
|
||||
indicator: Int,
|
||||
width: Int,
|
||||
current: Int,
|
||||
marks: (start: PureYAML.Parsing.Mark, end: PureYAML.Parsing.Mark),
|
||||
state: inout State,
|
||||
) throws {
|
||||
let blockScalarIndentation = current + indicator
|
||||
guard width >= blockScalarIndentation else {
|
||||
throw PureYAML.Parsing.ParseError.unexpectedIndentation(line: marks.end.line)
|
||||
}
|
||||
state.indentation.append(blockScalarIndentation)
|
||||
state.append(.indent(width: blockScalarIndentation), mark: marks.start, endMark: marks.end)
|
||||
state.blockScalarIndentation = blockScalarIndentation
|
||||
state.blockScalarContentPrefix = String(repeating: " ", count: width - blockScalarIndentation)
|
||||
clearPendingBlockScalarIndentation(&state)
|
||||
}
|
||||
|
||||
func applyIncreasedIndentation(
|
||||
width: Int,
|
||||
start: PureYAML.Parsing.Mark,
|
||||
end: PureYAML.Parsing.Mark,
|
||||
state: inout State,
|
||||
) {
|
||||
state.indentation.append(width)
|
||||
state.append(.indent(width: width), mark: start, endMark: end)
|
||||
guard state.pendingBlockScalarIndentation else {
|
||||
return
|
||||
}
|
||||
state.blockScalarIndentation = width
|
||||
state.blockScalarContentPrefix = ""
|
||||
clearPendingBlockScalarIndentation(&state)
|
||||
}
|
||||
|
||||
func applyDedentation(
|
||||
width: Int,
|
||||
start: PureYAML.Parsing.Mark,
|
||||
end: PureYAML.Parsing.Mark,
|
||||
state: inout State,
|
||||
) throws {
|
||||
while state.indentation.count > 1, width < (state.indentation.last ?? 0) {
|
||||
state.indentation.removeLast()
|
||||
state.append(.dedent(width: state.indentation.last ?? 0), mark: end, endMark: end)
|
||||
}
|
||||
if canUseImplicitIndentation(width: width, state: state) {
|
||||
state.indentation.append(width)
|
||||
state.append(.indent(width: width), mark: start, endMark: end)
|
||||
}
|
||||
guard width == state.indentation.last else {
|
||||
throw PureYAML.Parsing.ParseError.unexpectedIndentation(line: end.line)
|
||||
}
|
||||
}
|
||||
|
||||
func canUseImplicitIndentation(
|
||||
width: Int,
|
||||
state: State,
|
||||
) -> Bool {
|
||||
width != state.indentation.last
|
||||
&& width > (state.indentation.last ?? 0)
|
||||
&& state.validImplicitIndentation.contains(width)
|
||||
}
|
||||
|
||||
func pruneImplicitIndentation(_ state: inout State) {
|
||||
let current = state.indentation.last ?? 0
|
||||
state.validImplicitIndentation = state.validImplicitIndentation.filter { $0 <= current }
|
||||
}
|
||||
|
||||
func clearPendingBlockScalarIndentation(_ state: inout State) {
|
||||
state.pendingBlockScalarIndentation = false
|
||||
state.pendingBlockScalarIndentationIndicator = nil
|
||||
}
|
||||
|
||||
func closeIndentation(_ state: inout State) {
|
||||
let mark = state.reader.mark
|
||||
while state.indentation.count > 1 {
|
||||
state.indentation.removeLast()
|
||||
state.append(.dedent(width: state.indentation.last ?? 0), mark: mark, endMark: mark)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -47,6 +47,11 @@ extension PureYAML.Parsing.Scanner {
|
||||
var hasDocumentStartMarker = false
|
||||
var isDocumentClosed = false
|
||||
var flowDepth = 0
|
||||
var validImplicitIndentation: Set<Int> = []
|
||||
var pendingBlockScalarIndentation = false
|
||||
var pendingBlockScalarIndentationIndicator: Int?
|
||||
var blockScalarIndentation: Int?
|
||||
var blockScalarContentPrefix = ""
|
||||
var tagHandles = [
|
||||
"!": "!",
|
||||
"!!": "tag:yaml.org,2002:",
|
||||
@ -79,60 +84,16 @@ extension PureYAML.Parsing.Scanner {
|
||||
}
|
||||
|
||||
extension PureYAML.Parsing.Scanner {
|
||||
func scanIndentation(_ state: inout State) throws {
|
||||
let start = state.reader.mark
|
||||
var width = 0
|
||||
while let character = state.reader.peek() {
|
||||
if character == " " {
|
||||
width += 1
|
||||
state.reader.advance()
|
||||
} else if character == "\t" {
|
||||
throw PureYAML.Parsing.ParseError.tabIndentation(line: state.reader.mark.line)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
guard !state.reader.isAtEnd, !isLineBreak(state.reader.peek()) else {
|
||||
return
|
||||
}
|
||||
guard state.reader.peek() != "#" else {
|
||||
return
|
||||
}
|
||||
guard state.flowDepth == 0 else {
|
||||
return
|
||||
}
|
||||
|
||||
let end = state.reader.mark
|
||||
let current = state.indentation.last ?? 0
|
||||
if width > current {
|
||||
state.indentation.append(width)
|
||||
state.append(.indent(width: width), mark: start, endMark: end)
|
||||
} else if width < current {
|
||||
while state.indentation.count > 1, width < (state.indentation.last ?? 0) {
|
||||
state.indentation.removeLast()
|
||||
state.append(.dedent(width: state.indentation.last ?? 0), mark: end, endMark: end)
|
||||
}
|
||||
guard width == state.indentation.last else {
|
||||
throw PureYAML.Parsing.ParseError.unexpectedIndentation(line: end.line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func closeIndentation(_ state: inout State) {
|
||||
let mark = state.reader.mark
|
||||
while state.indentation.count > 1 {
|
||||
state.indentation.removeLast()
|
||||
state.append(.dedent(width: state.indentation.last ?? 0), mark: mark, endMark: mark)
|
||||
}
|
||||
}
|
||||
|
||||
func scanToken(_ state: inout State) throws {
|
||||
skipSeparation(&state)
|
||||
guard let character = state.reader.peek(), !isLineBreak(character) else {
|
||||
return
|
||||
}
|
||||
|
||||
if state.blockScalarIndentation != nil {
|
||||
scanBlockScalarContent(&state)
|
||||
return
|
||||
}
|
||||
if try scanLineIndicatorToken(character, state: &state) {
|
||||
return
|
||||
}
|
||||
@ -261,6 +222,9 @@ extension PureYAML.Parsing.Scanner {
|
||||
let start = state.reader.mark
|
||||
state.reader.advance()
|
||||
state.append(kind, mark: start, endMark: state.reader.mark)
|
||||
if case .blockEntry = kind, hasNodeOnSameLine(afterCurrentPositionIn: state.reader) {
|
||||
state.validImplicitIndentation.insert(start.column + 1)
|
||||
}
|
||||
}
|
||||
|
||||
func scanComment(_ state: inout State) {
|
||||
@ -316,10 +280,13 @@ extension PureYAML.Parsing.Scanner {
|
||||
) {
|
||||
let start = state.reader.mark
|
||||
var chomping = PureYAML.Parsing.BlockScalarChomping.clip
|
||||
var indentationIndicator: Int?
|
||||
state.reader.advance()
|
||||
while let character = state.reader.peek(), isBlockScalarHeaderCharacter(character) {
|
||||
if character == "-" {
|
||||
chomping = .strip
|
||||
} else if character.isNumber {
|
||||
indentationIndicator = Int(String(character))
|
||||
}
|
||||
state.reader.advance()
|
||||
}
|
||||
@ -328,6 +295,17 @@ extension PureYAML.Parsing.Scanner {
|
||||
mark: start,
|
||||
endMark: state.reader.mark,
|
||||
)
|
||||
state.pendingBlockScalarIndentation = true
|
||||
state.pendingBlockScalarIndentationIndicator = indentationIndicator
|
||||
}
|
||||
|
||||
func scanBlockScalarContent(_ state: inout State) {
|
||||
let start = state.reader.mark
|
||||
let value = state.blockScalarContentPrefix + state.reader.consume { character in
|
||||
!isLineBreak(character)
|
||||
}
|
||||
state.blockScalarContentPrefix = ""
|
||||
state.append(.scalar(value: value, style: .plain), mark: start, endMark: state.reader.mark)
|
||||
}
|
||||
|
||||
func scanNamedToken(
|
||||
@ -383,18 +361,26 @@ extension PureYAML.Parsing.Scanner {
|
||||
value.append(character)
|
||||
state.reader.advance()
|
||||
}
|
||||
let trimmed = trimTrailingSpaces(value)
|
||||
appendMappingKeyIfNeeded(start: start, state: &state)
|
||||
var trimmed = trimTrailingSpaces(value)
|
||||
let isMappingKey = appendMappingKeyIfNeeded(start: start, state: &state)
|
||||
if !isMappingKey {
|
||||
trimmed = scanPlainScalarContinuations(
|
||||
after: trimmed,
|
||||
state: &state,
|
||||
)
|
||||
}
|
||||
state.append(.scalar(value: trimmed, style: .plain), mark: start, endMark: state.reader.mark)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func appendMappingKeyIfNeeded(
|
||||
start: PureYAML.Parsing.Mark,
|
||||
state: inout State,
|
||||
) {
|
||||
) -> Bool {
|
||||
guard state.reader.peek() == ":", isMappingValueBoundary(state.reader.peek(offset: 1)) else {
|
||||
return
|
||||
return false
|
||||
}
|
||||
state.append(.mappingKey, mark: start, endMark: start)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,14 @@ extension PureYAML.Parsing {
|
||||
return tokens[previousIndex]
|
||||
}
|
||||
|
||||
func peek(offset: Int = 1) -> Token? {
|
||||
let targetIndex = index + offset
|
||||
guard tokens.indices.contains(targetIndex) else {
|
||||
return nil
|
||||
}
|
||||
return tokens[targetIndex]
|
||||
}
|
||||
|
||||
mutating func advance() -> Token? {
|
||||
guard let token = current else {
|
||||
return nil
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
extension PureYAML.Parsing.TokenEventParser {
|
||||
mutating func parseBlockSequence(
|
||||
properties: PureYAML.Parsing.NodeProperties,
|
||||
allowMappingKeyTerminator: Bool = false,
|
||||
) throws -> PureYAML.Parsing.NodeResult {
|
||||
let start = try expect("block sequence entry") { $0.isBlockEntry }
|
||||
let startMark = properties.mark ?? start.mark
|
||||
@ -12,12 +13,24 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
mark: startMark,
|
||||
))
|
||||
|
||||
try parseBlockSequenceItem(after: start, endMark: &endMark)
|
||||
try parseBlockSequenceItem(
|
||||
after: start,
|
||||
endMark: &endMark,
|
||||
allowMappingKeyTerminator: allowMappingKeyTerminator,
|
||||
)
|
||||
try consumeSequenceItemBoundaryDedent(sequenceIndentWidth: start.mark.column - 1)
|
||||
try consumeSequenceItemBoundaryIndent(sequenceIndentWidth: start.mark.column - 1)
|
||||
while cursor.current?.kind.isBlockEntry == true {
|
||||
let entry = try expect("block sequence entry") { $0.isBlockEntry }
|
||||
try parseBlockSequenceItem(after: entry, endMark: &endMark)
|
||||
try parseBlockSequenceItem(
|
||||
after: entry,
|
||||
endMark: &endMark,
|
||||
allowMappingKeyTerminator: allowMappingKeyTerminator,
|
||||
)
|
||||
try consumeSequenceItemBoundaryDedent(sequenceIndentWidth: entry.mark.column - 1)
|
||||
try consumeSequenceItemBoundaryIndent(sequenceIndentWidth: entry.mark.column - 1)
|
||||
}
|
||||
if let current = cursor.current, current.kind.isMappingKey {
|
||||
if let current = cursor.current, current.kind.isMappingKey, !allowMappingKeyTerminator {
|
||||
throw PureYAML.Parsing.ParseError.mixedCollectionStyles(line: current.mark.line)
|
||||
}
|
||||
|
||||
@ -25,9 +38,28 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
return PureYAML.Parsing.NodeResult(kind: .sequence, endMark: endMark)
|
||||
}
|
||||
|
||||
mutating func consumeSequenceItemBoundaryDedent(sequenceIndentWidth: Int) throws {
|
||||
guard dedentWidth(cursor.current) == sequenceIndentWidth,
|
||||
cursor.peek()?.kind.isBlockEntry == true
|
||||
else {
|
||||
return
|
||||
}
|
||||
_ = try expect("dedent") { $0.isDedent }
|
||||
}
|
||||
|
||||
mutating func consumeSequenceItemBoundaryIndent(sequenceIndentWidth: Int) throws {
|
||||
guard indentWidth(cursor.current) == sequenceIndentWidth,
|
||||
cursor.peek()?.kind.isBlockEntry == true
|
||||
else {
|
||||
return
|
||||
}
|
||||
_ = try expect("indent") { $0.isIndent }
|
||||
}
|
||||
|
||||
mutating func parseBlockSequenceItem(
|
||||
after entry: PureYAML.Parsing.Token,
|
||||
endMark: inout PureYAML.Parsing.Mark,
|
||||
allowMappingKeyTerminator: Bool,
|
||||
) throws {
|
||||
if hasNodeOnSameLine(after: entry) {
|
||||
let properties = consumeProperties()
|
||||
@ -41,6 +73,12 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
endMark = try parseBlockMapping(
|
||||
properties: properties,
|
||||
includeIndentedContinuation: true,
|
||||
minimumColumn: entry.mark.column + 1,
|
||||
).endMark
|
||||
} else if cursor.current?.kind.isBlockEntry == true {
|
||||
endMark = try parseBlockSequence(
|
||||
properties: properties,
|
||||
allowMappingKeyTerminator: allowMappingKeyTerminator,
|
||||
).endMark
|
||||
} else {
|
||||
endMark = try parseNode(properties: properties).endMark
|
||||
@ -59,8 +97,12 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
mutating func parseBlockMapping(
|
||||
properties: PureYAML.Parsing.NodeProperties,
|
||||
includeIndentedContinuation: Bool,
|
||||
minimumColumn: Int? = nil,
|
||||
) throws -> PureYAML.Parsing.NodeResult {
|
||||
let startMark = properties.mark ?? cursor.current?.mark ?? .start
|
||||
let tokenColumn = cursor.current?.mark.column ?? startMark.column
|
||||
let contentColumn = min(startMark.column, tokenColumn)
|
||||
let boundaryColumn = minimumColumn ?? contentColumn
|
||||
var endMark = startMark
|
||||
events.append(.mappingStart(
|
||||
anchor: properties.anchor,
|
||||
@ -68,20 +110,13 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
style: .block,
|
||||
mark: startMark,
|
||||
))
|
||||
while cursor.current?.kind.isMappingKey == true {
|
||||
endMark = try parseMappingPair().endMark
|
||||
}
|
||||
while includeIndentedContinuation, cursor.current?.kind.isIndent == true {
|
||||
_ = try expect("indent") { $0.isIndent }
|
||||
guard cursor.current?.kind.isMappingKey == true else {
|
||||
throw unexpectedToken(expected: "mapping key")
|
||||
}
|
||||
while cursor.current?.kind.isMappingKey == true {
|
||||
endMark = try parseMappingPair().endMark
|
||||
}
|
||||
_ = try expect("dedent") { $0.isDedent }
|
||||
}
|
||||
if let current = cursor.current, !canEndBlockMapping(at: current) {
|
||||
|
||||
try parseBlockMappingPairs(
|
||||
boundaryColumn: boundaryColumn,
|
||||
includeIndentedContinuation: includeIndentedContinuation,
|
||||
endMark: &endMark,
|
||||
)
|
||||
if let current = cursor.current, !canEndBlockMapping(at: current, minimumColumn: boundaryColumn) {
|
||||
throw PureYAML.Parsing.ParseError.unexpectedToken(
|
||||
expected: "mapping key",
|
||||
actual: current.kind.description,
|
||||
@ -93,6 +128,57 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
return PureYAML.Parsing.NodeResult(kind: .mapping, endMark: endMark)
|
||||
}
|
||||
|
||||
mutating func parseBlockMappingPairs(
|
||||
boundaryColumn: Int,
|
||||
includeIndentedContinuation: Bool,
|
||||
endMark: inout PureYAML.Parsing.Mark,
|
||||
) throws {
|
||||
while canContinueBlockMapping(minimumColumn: boundaryColumn) {
|
||||
endMark = try parseMappingPairAndBoundaries(
|
||||
boundaryColumn: boundaryColumn,
|
||||
includeIndentedContinuation: includeIndentedContinuation,
|
||||
).endMark
|
||||
}
|
||||
while includeIndentedContinuation, cursor.current?.kind.isIndent == true {
|
||||
endMark = try parseIndentedBlockMappingPairs(boundaryColumn: boundaryColumn).endMark
|
||||
}
|
||||
}
|
||||
|
||||
mutating func parseIndentedBlockMappingPairs(
|
||||
boundaryColumn: Int,
|
||||
) throws -> PureYAML.Parsing.NodeResult {
|
||||
_ = try expect("indent") { $0.isIndent }
|
||||
guard cursor.current?.kind.isMappingKey == true else {
|
||||
throw unexpectedToken(expected: "mapping key")
|
||||
}
|
||||
var endMark = cursor.current?.mark ?? .start
|
||||
while canContinueBlockMapping(minimumColumn: boundaryColumn) {
|
||||
endMark = try parseMappingPairAndBoundaries(
|
||||
boundaryColumn: boundaryColumn,
|
||||
includeIndentedContinuation: true,
|
||||
).endMark
|
||||
}
|
||||
if !canEndBlockMapping(at: cursor.current, minimumColumn: boundaryColumn) {
|
||||
_ = try expect("dedent") { $0.isDedent }
|
||||
}
|
||||
return PureYAML.Parsing.NodeResult(kind: .mapping, endMark: endMark)
|
||||
}
|
||||
|
||||
mutating func parseMappingPairAndBoundaries(
|
||||
boundaryColumn: Int,
|
||||
includeIndentedContinuation: Bool,
|
||||
) throws -> PureYAML.Parsing.NodeResult {
|
||||
let result = try parseMappingPair()
|
||||
try consumeMappingSiblingBoundaryDedents(minimumColumn: boundaryColumn)
|
||||
try consumeMappingSiblingBoundaryReindent(minimumColumn: boundaryColumn)
|
||||
if includeIndentedContinuation {
|
||||
try consumeDedents(atOrAbove: boundaryColumn)
|
||||
try consumeMappingSiblingBoundaryDedents(minimumColumn: boundaryColumn)
|
||||
try consumeMappingSiblingBoundaryReindent(minimumColumn: boundaryColumn)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
mutating func parseBlockScalar(
|
||||
properties: PureYAML.Parsing.NodeProperties,
|
||||
) throws -> PureYAML.Parsing.NodeResult {
|
||||
|
||||
@ -12,19 +12,71 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
let properties = consumeProperties()
|
||||
if cursor.current?.kind.isIndent == true {
|
||||
_ = try expect("indent") { $0.isIndent }
|
||||
let result = try parseNode(properties: properties)
|
||||
let result: PureYAML.Parsing.NodeResult = if cursor.current?.kind.isBlockEntry == true {
|
||||
try parseBlockSequence(
|
||||
properties: properties,
|
||||
allowMappingKeyTerminator: true,
|
||||
)
|
||||
} else {
|
||||
try parseNode(properties: properties)
|
||||
}
|
||||
if cursor.current?.kind.isDedent == true {
|
||||
_ = try expect("dedent") { $0.isDedent }
|
||||
} else if cursor.current?.kind.isMappingKey != true {
|
||||
_ = try expect("dedent") { $0.isDedent }
|
||||
}
|
||||
return result
|
||||
}
|
||||
if properties == .none, canParsePlainScalarWithContinuation(after: valueIndicator) {
|
||||
return try parsePlainScalarWithContinuation(properties: properties)
|
||||
}
|
||||
if properties != .none || hasNodeOnSameLine(after: valueIndicator) {
|
||||
return try parseNode(properties: properties)
|
||||
}
|
||||
if cursor.current?.kind.isBlockEntry == true {
|
||||
return try parseBlockSequence(
|
||||
properties: properties,
|
||||
allowMappingKeyTerminator: true,
|
||||
)
|
||||
}
|
||||
|
||||
let mark = valueIndicator.endMark
|
||||
events.append(.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: mark))
|
||||
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: mark)
|
||||
}
|
||||
|
||||
mutating func parsePlainScalarWithContinuation(
|
||||
properties: PureYAML.Parsing.NodeProperties,
|
||||
) throws -> PureYAML.Parsing.NodeResult {
|
||||
let first = try expect("plain scalar") { kind in
|
||||
if case .scalar(_, .plain) = kind {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
var value = try decodedScalar(from: first)
|
||||
var endMark = first.endMark
|
||||
|
||||
_ = try expect("indent") { $0.isIndent }
|
||||
while isScalar(cursor.current) {
|
||||
let token = cursor.advance() ?? first
|
||||
let continuation = try plainContinuationText(from: token)
|
||||
value += " " + continuation
|
||||
endMark = token.endMark
|
||||
}
|
||||
_ = try expect("dedent") { $0.isDedent }
|
||||
|
||||
let mark = properties.mark ?? first.mark
|
||||
events.append(.scalar(
|
||||
value: value,
|
||||
anchor: properties.anchor,
|
||||
tag: properties.tag,
|
||||
style: .plain,
|
||||
mark: mark,
|
||||
))
|
||||
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: endMark)
|
||||
}
|
||||
|
||||
mutating func consumeProperties() -> PureYAML.Parsing.NodeProperties {
|
||||
var properties = PureYAML.Parsing.NodeProperties.none
|
||||
while let token = cursor.current {
|
||||
@ -61,6 +113,27 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
}
|
||||
}
|
||||
|
||||
func decodedScalar(from token: PureYAML.Parsing.Token) throws -> String {
|
||||
guard case let .scalar(value, style) = token.kind else {
|
||||
throw unexpectedToken(expected: "scalar")
|
||||
}
|
||||
return try decodeScalar(value, style: style, line: token.mark.line)
|
||||
}
|
||||
|
||||
func plainContinuationText(from token: PureYAML.Parsing.Token) throws -> String {
|
||||
guard case let .scalar(value, style) = token.kind else {
|
||||
throw unexpectedToken(expected: "scalar")
|
||||
}
|
||||
switch style {
|
||||
case .doubleQuoted:
|
||||
return "\"\(value)\""
|
||||
case .singleQuoted:
|
||||
return "'\(value)'"
|
||||
case .folded, .literal, .plain:
|
||||
return try decodeScalar(value, style: style, line: token.mark.line)
|
||||
}
|
||||
}
|
||||
|
||||
func hasNodeOnSameLine(after token: PureYAML.Parsing.Token) -> Bool {
|
||||
guard let current = cursor.current, !current.kind.isTerminator else {
|
||||
return false
|
||||
@ -68,15 +141,129 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
return current.mark.line == token.mark.line
|
||||
}
|
||||
|
||||
func canEndBlockMapping(at token: PureYAML.Parsing.Token) -> Bool {
|
||||
token.kind.isBlockEntry
|
||||
func canParsePlainScalarWithContinuation(after token: PureYAML.Parsing.Token) -> Bool {
|
||||
guard case .scalar(_, .plain) = cursor.current?.kind else {
|
||||
return false
|
||||
}
|
||||
guard cursor.current?.mark.line == token.mark.line else {
|
||||
return false
|
||||
}
|
||||
guard cursor.peek()?.kind.isIndent == true else {
|
||||
return false
|
||||
}
|
||||
guard isScalar(cursor.peek(offset: 2)) else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func canParsePlainScalarContinuation() -> Bool {
|
||||
guard case .scalar(_, .plain) = cursor.current?.kind else {
|
||||
return false
|
||||
}
|
||||
guard cursor.peek()?.kind.isIndent == true else {
|
||||
return false
|
||||
}
|
||||
guard isScalar(cursor.peek(offset: 2)) else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isScalar(_ token: PureYAML.Parsing.Token?) -> Bool {
|
||||
guard case .scalar = token?.kind else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
mutating func consumeDedents(atOrAbove minimumColumn: Int) throws {
|
||||
while let width = dedentWidth(cursor.current), width >= minimumColumn {
|
||||
_ = try expect("dedent") { $0.isDedent }
|
||||
}
|
||||
}
|
||||
|
||||
mutating func consumeMappingSiblingBoundaryDedents(minimumColumn: Int) throws {
|
||||
while canConsumeMappingSiblingBoundaryDedent(minimumColumn: minimumColumn) {
|
||||
_ = try expect("dedent") { $0.isDedent }
|
||||
}
|
||||
}
|
||||
|
||||
func canConsumeMappingSiblingBoundaryDedent(minimumColumn: Int) -> Bool {
|
||||
guard let width = dedentWidth(cursor.current),
|
||||
let next = cursor.peek(),
|
||||
next.kind.isMappingKey,
|
||||
next.mark.column >= minimumColumn
|
||||
else {
|
||||
return false
|
||||
}
|
||||
return width == next.mark.column - 1
|
||||
}
|
||||
|
||||
mutating func consumeMappingSiblingBoundaryReindent(minimumColumn: Int) throws {
|
||||
guard let dedent = dedentWidth(cursor.current),
|
||||
dedent < minimumColumn,
|
||||
indentWidth(cursor.peek()) == minimumColumn,
|
||||
let mappingKey = cursor.peek(offset: 2),
|
||||
mappingKey.kind.isMappingKey,
|
||||
mappingKey.mark.column == minimumColumn + 1
|
||||
else {
|
||||
return
|
||||
}
|
||||
_ = try expect("dedent") { $0.isDedent }
|
||||
_ = try expect("indent") { $0.isIndent }
|
||||
}
|
||||
|
||||
func dedentWidth(_ token: PureYAML.Parsing.Token?) -> Int? {
|
||||
guard case let .dedent(width) = token?.kind else {
|
||||
return nil
|
||||
}
|
||||
return width
|
||||
}
|
||||
|
||||
func indentWidth(_ token: PureYAML.Parsing.Token?) -> Int? {
|
||||
guard case let .indent(width) = token?.kind else {
|
||||
return nil
|
||||
}
|
||||
return width
|
||||
}
|
||||
|
||||
func canContinueBlockMapping(minimumColumn: Int?) -> Bool {
|
||||
guard let current = cursor.current, current.kind.isMappingKey else {
|
||||
return false
|
||||
}
|
||||
guard let minimumColumn else {
|
||||
return true
|
||||
}
|
||||
return current.mark.column >= minimumColumn
|
||||
}
|
||||
|
||||
func canEndBlockMapping(
|
||||
at token: PureYAML.Parsing.Token?,
|
||||
minimumColumn: Int? = nil,
|
||||
) -> Bool {
|
||||
guard let token else {
|
||||
return true
|
||||
}
|
||||
return token.kind.isBlockEntry
|
||||
|| token.kind.isDedent
|
||||
|| token.kind.isDocumentEnd
|
||||
|| token.kind.isDocumentStart
|
||||
|| token.kind.isFlowTerminator
|
||||
|| isLowerColumnMappingKey(token, minimumColumn: minimumColumn)
|
||||
|| token.kind.isStreamEnd
|
||||
}
|
||||
|
||||
func isLowerColumnMappingKey(
|
||||
_ token: PureYAML.Parsing.Token?,
|
||||
minimumColumn: Int?,
|
||||
) -> Bool {
|
||||
guard let token, token.kind.isMappingKey, let minimumColumn else {
|
||||
return false
|
||||
}
|
||||
return token.mark.column < minimumColumn
|
||||
}
|
||||
|
||||
mutating func expect(
|
||||
_ expected: String,
|
||||
matching: (PureYAML.Parsing.TokenKind) -> Bool,
|
||||
|
||||
@ -62,6 +62,10 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
|
||||
let result = try parseNode()
|
||||
var endMark = result.endMark
|
||||
while cursor.current?.kind.isDedent == true {
|
||||
let dedent = try expect("dedent") { $0.isDedent }
|
||||
endMark = dedent.endMark
|
||||
}
|
||||
if cursor.current?.kind.isDocumentEnd == true {
|
||||
let end = try expect("document end") { $0.isDocumentEnd }
|
||||
endMark = end.endMark
|
||||
@ -129,6 +133,9 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
includeIndentedContinuation: false,
|
||||
)
|
||||
case let .scalar(value, style):
|
||||
if canParsePlainScalarContinuation() {
|
||||
return try parsePlainScalarWithContinuation(properties: properties)
|
||||
}
|
||||
_ = cursor.advance()
|
||||
let decoded = try decodeScalar(value, style: style, line: token.mark.line)
|
||||
let mark = properties.mark ?? token.mark
|
||||
|
||||
38
Tests/Fixtures/MANIFEST.md
Normal file
38
Tests/Fixtures/MANIFEST.md
Normal file
@ -0,0 +1,38 @@
|
||||
# Real YAML Fixture Manifest
|
||||
|
||||
These fixtures are vendored test inputs. They are pinned to immutable GitHub
|
||||
commit SHAs so compatibility failures can be reproduced without network access.
|
||||
The files are not implementation source and are used only by
|
||||
`RealYAMLFixtureTests`.
|
||||
|
||||
| Local file | Size | Lines | Bytes | Upstream license | Pinned source |
|
||||
|---|---:|---:|---:|---|---|
|
||||
| `real-yaml/kubernetes-simple-pod.yaml` | very short | 12 | 186 | MIT | `ContainerSolutions/kubernetes-examples@fa6e84542f4350b97991bfffae553d248953e443/Pod/simple.yaml` |
|
||||
| `real-yaml/kustomize-wordpress.yaml` | very short | 18 | 262 | Apache-2.0 | `kubernetes-sigs/kustomize@313aacedd3adcb6564fd362bcef52f5aa3abef85/examples/wordpress/kustomization.yaml` |
|
||||
| `real-yaml/docker-compose-nginx-golang-postgres.yaml` | short | 48 | 852 | CC0-1.0 | `docker/awesome-compose@8ded149643ef0ba21fce5a5d39894b39fff6c7ee/nginx-golang-postgres/compose.yaml` |
|
||||
| `real-yaml/github-actions-swift-format.yml` | short | 84 | 3591 | Apache-2.0 | `swiftlang/swift-format@fffd8df6153456838039aae4b9b901f6f31f5687/.github/workflows/pull_request.yml` |
|
||||
| `real-yaml/openapi-petstore.yaml` | short | 119 | 2768 | Apache-2.0 | `OAI/OpenAPI-Specification@18c3204bc55c8c75fe25a039a3cf085f0e732208/_archive_/schemas/v3.0/pass/petstore.yaml` |
|
||||
| `real-yaml/prow-cluster-api-presubmits.yaml` | medium | 427 | 14213 | Apache-2.0 | `kubernetes/test-infra@9001df901ba65b1b74d16be5cc910e68a5f35b91/config/jobs/kubernetes-sigs/cluster-api/cluster-api-release-1-13-presubmits.yaml` |
|
||||
| `real-yaml/prometheus-conf-good.yml` | medium | 481 | 11562 | Apache-2.0 | `prometheus/prometheus@5e3c892bfc1209b739562f885a8cfa17c6cd7fa2/config/testdata/conf.good.yml` |
|
||||
| `real-yaml/cert-manager-certificate-crd.yaml` | large | 883 | 44059 | Apache-2.0 | `cert-manager/cert-manager@59c5f5361a5b1cd2d0ca84cd9983a3beac3846bd/deploy/crds/cert-manager.io_certificates.yaml` |
|
||||
| `real-yaml/cert-manager-values.yaml` | large | 1759 | 65536 | Apache-2.0 | `cert-manager/cert-manager@59c5f5361a5b1cd2d0ca84cd9983a3beac3846bd/deploy/charts/cert-manager/values.yaml` |
|
||||
| `real-yaml/github-rest-api.yaml` | extreme | 257280 | 9549118 | MIT | `github/rest-api-description@0d4e436c347b444cd71b4eb1bd73948fd51c3402/descriptions-next/api.github.com/api.github.com.yaml` |
|
||||
|
||||
Selection covers GitHub Actions, Docker Compose, Kubernetes manifests,
|
||||
Kustomize, OpenAPI, Prow jobs, Prometheus config, Helm values, CRDs, and one
|
||||
extreme OpenAPI document. If a fixture is parseable by mature YAML tools, this
|
||||
suite expects PureYAML to parse it or to pin an exact unsupported feature error
|
||||
with a linked issue.
|
||||
|
||||
## Validation Failure Fixtures
|
||||
|
||||
These fixtures are intentionally valid YAML that should fail validation. They
|
||||
exist to demonstrate exact validation output for user-facing diagnostic reports,
|
||||
strict throwing behavior, non-strict warning returns, path rendering, required
|
||||
presence checks, and required absence checks.
|
||||
|
||||
| Local file | Purpose |
|
||||
|---|---|
|
||||
| `validation-failures/duplicate-keys.yaml` | Default duplicate-key diagnostics with root and punctuation-heavy nested keys. |
|
||||
| `validation-failures/missing-required-and-forbidden.yaml` | Custom required-presence plus required-absence diagnostics in one document. |
|
||||
| `validation-failures/warnings.yaml` | Warning-only diagnostics that throw in strict mode and return in non-strict mode. |
|
||||
883
Tests/Fixtures/real-yaml/cert-manager-certificate-crd.yaml
Normal file
883
Tests/Fixtures/real-yaml/cert-manager-certificate-crd.yaml
Normal file
@ -0,0 +1,883 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.21.0
|
||||
name: certificates.cert-manager.io
|
||||
spec:
|
||||
group: cert-manager.io
|
||||
names:
|
||||
categories:
|
||||
- cert-manager
|
||||
kind: Certificate
|
||||
listKind: CertificateList
|
||||
plural: certificates
|
||||
shortNames:
|
||||
- cert
|
||||
- certs
|
||||
singular: certificate
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .status.conditions[?(@.type == "Ready")].status
|
||||
name: Ready
|
||||
type: string
|
||||
- jsonPath: .spec.secretName
|
||||
name: Secret
|
||||
type: string
|
||||
- jsonPath: .spec.issuerRef.name
|
||||
name: Issuer
|
||||
priority: 1
|
||||
type: string
|
||||
- jsonPath: .status.conditions[?(@.type == "Ready")].message
|
||||
name: Status
|
||||
priority: 1
|
||||
type: string
|
||||
- description: CreationTimestamp is a timestamp representing the server time when
|
||||
this object was created. It is not guaranteed to be set in happens-before
|
||||
order across separate operations. Clients may not set this value. It is represented
|
||||
in RFC3339 form and is in UTC.
|
||||
jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
type: date
|
||||
name: v1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: |-
|
||||
A Certificate resource should be created to ensure an up to date and signed
|
||||
X.509 certificate is stored in the Kubernetes Secret resource named in `spec.secretName`.
|
||||
|
||||
The stored certificate will be renewed before it expires (as configured by `spec.renewBefore`).
|
||||
properties:
|
||||
apiVersion:
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: |-
|
||||
Specification of the desired state of the Certificate resource.
|
||||
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
|
||||
properties:
|
||||
additionalOutputFormats:
|
||||
description: |-
|
||||
Defines extra output formats of the private key and signed certificate chain
|
||||
to be written to this Certificate's target Secret.
|
||||
items:
|
||||
description: |-
|
||||
CertificateAdditionalOutputFormat defines an additional output format of a
|
||||
Certificate resource. These contain supplementary data formats of the signed
|
||||
certificate chain and paired private key.
|
||||
properties:
|
||||
type:
|
||||
description: |-
|
||||
Type is the name of the format type that should be written to the
|
||||
Certificate's target Secret.
|
||||
enum:
|
||||
- DER
|
||||
- CombinedPEM
|
||||
type: string
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
commonName:
|
||||
description: |-
|
||||
Requested common name X509 certificate subject attribute.
|
||||
More info: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6
|
||||
NOTE: TLS clients will ignore this value when any subject alternative name is
|
||||
set (see https://tools.ietf.org/html/rfc6125#section-6.4.4).
|
||||
|
||||
Should have a length of 64 characters or fewer to avoid generating invalid CSRs.
|
||||
Cannot be set if the `literalSubject` field is set.
|
||||
type: string
|
||||
dnsNames:
|
||||
description: Requested DNS subject alternative names.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
duration:
|
||||
description: |-
|
||||
Requested 'duration' (i.e. lifetime) of the Certificate. Note that the
|
||||
issuer may choose to ignore the requested duration, just like any other
|
||||
requested attribute.
|
||||
|
||||
If unset, this defaults to 90 days.
|
||||
Minimum accepted duration is 1 hour.
|
||||
Value must be in units accepted by Go time.ParseDuration https://golang.org/pkg/time/#ParseDuration.
|
||||
type: string
|
||||
emailAddresses:
|
||||
description: Requested email subject alternative names.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
encodeUsagesInRequest:
|
||||
description: |-
|
||||
Whether the KeyUsage and ExtKeyUsage extensions should be set in the encoded CSR.
|
||||
|
||||
This option defaults to true, and should only be disabled if the target
|
||||
issuer does not support CSRs with these X509 KeyUsage/ ExtKeyUsage extensions.
|
||||
type: boolean
|
||||
ipAddresses:
|
||||
description: Requested IP address subject alternative names.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
isCA:
|
||||
description: |-
|
||||
Requested basic constraints isCA value.
|
||||
The isCA value is used to set the `isCA` field on the created CertificateRequest
|
||||
resources. Note that the issuer may choose to ignore the requested isCA value, just
|
||||
like any other requested attribute.
|
||||
|
||||
If true, this will automatically add the `cert sign` usage to the list
|
||||
of requested `usages`.
|
||||
type: boolean
|
||||
issuerRef:
|
||||
description: |-
|
||||
Reference to the issuer responsible for issuing the certificate.
|
||||
If the issuer is namespace-scoped, it must be in the same namespace
|
||||
as the Certificate. If the issuer is cluster-scoped, it can be used
|
||||
from any namespace.
|
||||
|
||||
The `name` field of the reference must always be specified.
|
||||
properties:
|
||||
group:
|
||||
description: |-
|
||||
Group of the issuer being referred to.
|
||||
Defaults to 'cert-manager.io'.
|
||||
type: string
|
||||
kind:
|
||||
description: |-
|
||||
Kind of the issuer being referred to.
|
||||
Defaults to 'Issuer'.
|
||||
type: string
|
||||
name:
|
||||
description: Name of the issuer being referred to.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
keystores:
|
||||
description: Additional keystore output formats to be stored in the
|
||||
Certificate's Secret.
|
||||
properties:
|
||||
jks:
|
||||
description: |-
|
||||
JKS configures options for storing a JKS keystore in the
|
||||
`spec.secretName` Secret resource.
|
||||
properties:
|
||||
alias:
|
||||
description: |-
|
||||
Alias specifies the alias of the key in the keystore, required by the JKS format.
|
||||
If not provided, the default alias `certificate` will be used.
|
||||
type: string
|
||||
create:
|
||||
description: |-
|
||||
Create enables JKS keystore creation for the Certificate.
|
||||
If true, a file named `keystore.jks` will be created in the target
|
||||
Secret resource, encrypted using the password stored in
|
||||
`passwordSecretRef` or `password`.
|
||||
The keystore file will be updated immediately.
|
||||
If the issuer provided a CA certificate, a file named `truststore.jks`
|
||||
will also be created in the target Secret resource, encrypted using the
|
||||
password stored in `passwordSecretRef`
|
||||
containing the issuing Certificate Authority
|
||||
type: boolean
|
||||
password:
|
||||
description: |-
|
||||
Password provides a literal password used to encrypt the JKS keystore.
|
||||
Mutually exclusive with passwordSecretRef.
|
||||
One of password or passwordSecretRef must provide a password with a non-zero length.
|
||||
type: string
|
||||
passwordSecretRef:
|
||||
description: |-
|
||||
PasswordSecretRef is a reference to a non-empty key in a Secret resource
|
||||
containing the password used to encrypt the JKS keystore.
|
||||
Mutually exclusive with password.
|
||||
One of password or passwordSecretRef must provide a password with a non-zero length.
|
||||
properties:
|
||||
key:
|
||||
description: |-
|
||||
The key of the entry in the Secret resource's `data` field to be used.
|
||||
Some instances of this field may be defaulted, in others it may be
|
||||
required.
|
||||
type: string
|
||||
name:
|
||||
description: |-
|
||||
Name of the resource being referred to.
|
||||
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
required:
|
||||
- create
|
||||
type: object
|
||||
pkcs12:
|
||||
description: |-
|
||||
PKCS12 configures options for storing a PKCS12 keystore in the
|
||||
`spec.secretName` Secret resource.
|
||||
properties:
|
||||
create:
|
||||
description: |-
|
||||
Create enables PKCS12 keystore creation for the Certificate.
|
||||
If true, a file named `keystore.p12` will be created in the target
|
||||
Secret resource, encrypted using the password stored in
|
||||
`passwordSecretRef` or in `password`.
|
||||
The keystore file will be updated immediately.
|
||||
If the issuer provided a CA certificate, a file named `truststore.p12` will
|
||||
also be created in the target Secret resource, encrypted using the
|
||||
password stored in `passwordSecretRef` containing the issuing Certificate
|
||||
Authority
|
||||
type: boolean
|
||||
password:
|
||||
description: |-
|
||||
Password provides a literal password used to encrypt the PKCS#12 keystore.
|
||||
Mutually exclusive with passwordSecretRef.
|
||||
One of password or passwordSecretRef must provide a password with a non-zero length.
|
||||
type: string
|
||||
passwordSecretRef:
|
||||
description: |-
|
||||
PasswordSecretRef is a reference to a non-empty key in a Secret resource
|
||||
containing the password used to encrypt the PKCS#12 keystore.
|
||||
Mutually exclusive with password.
|
||||
One of password or passwordSecretRef must provide a password with a non-zero length.
|
||||
properties:
|
||||
key:
|
||||
description: |-
|
||||
The key of the entry in the Secret resource's `data` field to be used.
|
||||
Some instances of this field may be defaulted, in others it may be
|
||||
required.
|
||||
type: string
|
||||
name:
|
||||
description: |-
|
||||
Name of the resource being referred to.
|
||||
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
profile:
|
||||
description: |-
|
||||
Profile specifies the key and certificate encryption algorithms and the HMAC algorithm
|
||||
used to create the PKCS12 keystore. Default value is `LegacyRC2` for backward compatibility.
|
||||
|
||||
If provided, allowed values are:
|
||||
`LegacyRC2`: Deprecated. Not supported by default in OpenSSL 3 or Java 20.
|
||||
`LegacyDES`: Less secure algorithm. Use this option for maximal compatibility.
|
||||
`Modern2023`: Secure algorithm. Use this option in case you have to always use secure algorithms
|
||||
(e.g., because of company policy). Please note that the security of the algorithm is not that important
|
||||
in reality, because the unencrypted certificate and private key are also stored in the Secret.
|
||||
`Modern2026`: Encodes PKCS#12 files using algorithms that are considered modern as of 2026.
|
||||
Private keys and certificates are encrypted using PBES2 with PBKDF2-HMAC-SHA-256 and AES-256-CBC.
|
||||
The MAC algorithm is PBMAC1 with PBKDF2-HMAC-SHA-256 and HMAC-SHA256.
|
||||
Files produced with this profile can be read by OpenSSL 3.4.0 and higher, Java 26 and higher,
|
||||
or with Java using compatible versions of Bouncy Castle. Meets FIPS 140-3 requirements.
|
||||
enum:
|
||||
- LegacyRC2
|
||||
- LegacyDES
|
||||
- Modern2023
|
||||
- Modern2026
|
||||
type: string
|
||||
required:
|
||||
- create
|
||||
type: object
|
||||
type: object
|
||||
literalSubject:
|
||||
description: |-
|
||||
Requested X.509 certificate subject, represented using the LDAP "String
|
||||
Representation of a Distinguished Name" [1].
|
||||
Important: the LDAP string format also specifies the order of the attributes
|
||||
in the subject, this is important when issuing certs for LDAP authentication.
|
||||
Example: `CN=foo,DC=corp,DC=example,DC=com`
|
||||
More info [1]: https://datatracker.ietf.org/doc/html/rfc4514
|
||||
More info: https://github.com/cert-manager/cert-manager/issues/3203
|
||||
More info: https://github.com/cert-manager/cert-manager/issues/4424
|
||||
|
||||
Cannot be set if the `subject` or `commonName` field is set.
|
||||
type: string
|
||||
nameConstraints:
|
||||
description: |-
|
||||
x.509 certificate NameConstraint extension which MUST NOT be used in a non-CA certificate.
|
||||
More Info: https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.10
|
||||
|
||||
This is an Alpha Feature and is only enabled with the
|
||||
`--feature-gates=NameConstraints=true` option set on both
|
||||
the controller and webhook components.
|
||||
properties:
|
||||
critical:
|
||||
description: if true then the name constraints are marked critical.
|
||||
type: boolean
|
||||
excluded:
|
||||
description: |-
|
||||
Excluded contains the constraints which must be disallowed. Any name matching a
|
||||
restriction in the excluded field is invalid regardless
|
||||
of information appearing in the permitted
|
||||
properties:
|
||||
dnsDomains:
|
||||
description: DNSDomains is a list of DNS domains that are
|
||||
permitted or excluded.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
emailAddresses:
|
||||
description: EmailAddresses is a list of Email Addresses that
|
||||
are permitted or excluded.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
ipRanges:
|
||||
description: |-
|
||||
IPRanges is a list of IP Ranges that are permitted or excluded.
|
||||
This should be a valid CIDR notation.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
uriDomains:
|
||||
description: URIDomains is a list of URI domains that are
|
||||
permitted or excluded.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
type: object
|
||||
permitted:
|
||||
description: Permitted contains the constraints in which the names
|
||||
must be located.
|
||||
properties:
|
||||
dnsDomains:
|
||||
description: DNSDomains is a list of DNS domains that are
|
||||
permitted or excluded.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
emailAddresses:
|
||||
description: EmailAddresses is a list of Email Addresses that
|
||||
are permitted or excluded.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
ipRanges:
|
||||
description: |-
|
||||
IPRanges is a list of IP Ranges that are permitted or excluded.
|
||||
This should be a valid CIDR notation.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
uriDomains:
|
||||
description: URIDomains is a list of URI domains that are
|
||||
permitted or excluded.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
type: object
|
||||
type: object
|
||||
otherNames:
|
||||
description: |-
|
||||
`otherNames` is an escape hatch for SAN that allows any type. We currently restrict the support to string like otherNames, cf RFC 5280 p 37
|
||||
Any UTF8 String valued otherName can be passed with by setting the keys oid: x.x.x.x and UTF8Value: somevalue for `otherName`.
|
||||
Most commonly this would be UPN set with oid: 1.3.6.1.4.1.311.20.2.3
|
||||
You should ensure that any OID passed is valid for the UTF8String type as we do not explicitly validate this.
|
||||
items:
|
||||
properties:
|
||||
oid:
|
||||
description: |-
|
||||
OID is the object identifier for the otherName SAN.
|
||||
The object identifier must be expressed as a dotted string, for
|
||||
example, "1.2.840.113556.1.4.221".
|
||||
type: string
|
||||
utf8Value:
|
||||
description: |-
|
||||
utf8Value is the string value of the otherName SAN.
|
||||
The utf8Value accepts any valid UTF8 string to set as value for the otherName SAN.
|
||||
type: string
|
||||
type: object
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
privateKey:
|
||||
description: |-
|
||||
Private key options. These include the key algorithm and size, the used
|
||||
encoding and the rotation policy.
|
||||
properties:
|
||||
algorithm:
|
||||
description: |-
|
||||
Algorithm is the private key algorithm of the corresponding private key
|
||||
for this certificate.
|
||||
|
||||
If provided, allowed values are either `RSA`, `ECDSA` or `Ed25519`.
|
||||
If `algorithm` is specified and `size` is not provided,
|
||||
key size of 2048 will be used for `RSA` key algorithm and
|
||||
key size of 256 will be used for `ECDSA` key algorithm.
|
||||
key size is ignored when using the `Ed25519` key algorithm.
|
||||
enum:
|
||||
- RSA
|
||||
- ECDSA
|
||||
- Ed25519
|
||||
type: string
|
||||
encoding:
|
||||
description: |-
|
||||
The private key cryptography standards (PKCS) encoding for this
|
||||
certificate's private key to be encoded in.
|
||||
|
||||
If provided, allowed values are `PKCS1` and `PKCS8` standing for PKCS#1
|
||||
and PKCS#8, respectively.
|
||||
Defaults to `PKCS1` if not specified.
|
||||
enum:
|
||||
- PKCS1
|
||||
- PKCS8
|
||||
type: string
|
||||
rotationPolicy:
|
||||
description: |-
|
||||
RotationPolicy controls how private keys should be regenerated when a
|
||||
re-issuance is being processed.
|
||||
|
||||
If set to `Never`, a private key will only be generated if one does not
|
||||
already exist in the target `spec.secretName`. If one does exist but it
|
||||
does not have the correct algorithm or size, a warning will be raised
|
||||
to await user intervention.
|
||||
If set to `Always`, a private key matching the specified requirements
|
||||
will be generated whenever a re-issuance occurs.
|
||||
Default is `Always`.
|
||||
The default was changed from `Never` to `Always` in cert-manager >=v1.18.0.
|
||||
enum:
|
||||
- Never
|
||||
- Always
|
||||
type: string
|
||||
size:
|
||||
description: |-
|
||||
Size is the key bit size of the corresponding private key for this certificate.
|
||||
|
||||
If `algorithm` is set to `RSA`, valid values are `2048`, `4096` or `8192`,
|
||||
and will default to `2048` if not specified.
|
||||
If `algorithm` is set to `ECDSA`, valid values are `256`, `384` or `521`,
|
||||
and will default to `256` if not specified.
|
||||
If `algorithm` is set to `Ed25519`, Size is ignored.
|
||||
No other values are allowed.
|
||||
type: integer
|
||||
type: object
|
||||
renewBefore:
|
||||
description: |-
|
||||
How long before the currently issued certificate's expiry cert-manager should
|
||||
renew the certificate. For example, if a certificate is valid for 60 minutes,
|
||||
and `renewBefore=10m`, cert-manager will begin to attempt to renew the certificate
|
||||
50 minutes after it was issued (i.e. when there are 10 minutes remaining until
|
||||
the certificate is no longer valid).
|
||||
|
||||
NOTE: The actual lifetime of the issued certificate is used to determine the
|
||||
renewal time. If an issuer returns a certificate with a different lifetime than
|
||||
the one requested, cert-manager will use the lifetime of the issued certificate.
|
||||
|
||||
If unset, this defaults to 1/3 of the issued certificate's lifetime.
|
||||
Minimum accepted value is 5 minutes.
|
||||
Value must be in units accepted by Go time.ParseDuration https://golang.org/pkg/time/#ParseDuration.
|
||||
Cannot be set if the `renewBeforePercentage` field is set.
|
||||
type: string
|
||||
renewBeforePercentage:
|
||||
description: |-
|
||||
`renewBeforePercentage` is like `renewBefore`, except it is a relative percentage
|
||||
rather than an absolute duration. For example, if a certificate is valid for 60
|
||||
minutes, and `renewBeforePercentage=25`, cert-manager will begin to attempt to
|
||||
renew the certificate 45 minutes after it was issued (i.e. when there are 15
|
||||
minutes (25%) remaining until the certificate is no longer valid).
|
||||
|
||||
NOTE: The actual lifetime of the issued certificate is used to determine the
|
||||
renewal time. If an issuer returns a certificate with a different lifetime than
|
||||
the one requested, cert-manager will use the lifetime of the issued certificate.
|
||||
|
||||
Value must be an integer in the range (0,100). The minimum effective
|
||||
`renewBefore` derived from the `renewBeforePercentage` and `duration` fields is 5
|
||||
minutes.
|
||||
Cannot be set if the `renewBefore` field is set.
|
||||
format: int32
|
||||
type: integer
|
||||
renewal:
|
||||
description: |-
|
||||
`renewal` allows configuration of how your certificate is renewed. If the policy mentioned is
|
||||
`RenewBefore` then the controller respects `renewBefore` and `renewBeforePercentage`.
|
||||
properties:
|
||||
policy:
|
||||
description: '`policy` must be one of `Disabled`, `RenewBefore`.'
|
||||
enum:
|
||||
- RenewBefore
|
||||
- Disabled
|
||||
type: string
|
||||
windows:
|
||||
description: '`windows` mentions the behavior of when the renewal
|
||||
must happen.'
|
||||
items:
|
||||
description: CertificateRenewalWindows is the definition for
|
||||
renewal windows
|
||||
properties:
|
||||
cron:
|
||||
description: |-
|
||||
`cron` is a cron compliant string to allow when the renewal should be allowed. Format is as shown below:
|
||||
* * * * *
|
||||
| | | | |
|
||||
| | | | day of the week (0–6) (Sunday to Saturday;
|
||||
| | | month (1–12) 7 is also Sunday on some systems)
|
||||
| | day of the month (1–31)
|
||||
| hour (0–23)
|
||||
minute (0–59)
|
||||
minLength: 1
|
||||
type: string
|
||||
timezone:
|
||||
description: |-
|
||||
`timezone` is IANA compliant timezone. For example America/Denver.
|
||||
If this field is not set, timezone is treated as UTC.
|
||||
minLength: 1
|
||||
type: string
|
||||
windowDuration:
|
||||
description: |-
|
||||
`windowDuration` is how long the cron definition is active for.
|
||||
Value must be in units accepted by Go time.ParseDuration https://golang.org/pkg/time/#ParseDuration.
|
||||
pattern: ^([0-9]+(\.[0-9]+)?(s|m|h))+$
|
||||
type: string
|
||||
required:
|
||||
- cron
|
||||
- windowDuration
|
||||
type: object
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
type: object
|
||||
revisionHistoryLimit:
|
||||
description: |-
|
||||
The maximum number of CertificateRequest revisions that are maintained in
|
||||
the Certificate's history. Each revision represents a single `CertificateRequest`
|
||||
created by this Certificate, either when it was created, renewed, or Spec
|
||||
was changed. Revisions will be removed by oldest first if the number of
|
||||
revisions exceeds this number.
|
||||
|
||||
If set, revisionHistoryLimit must be a value of `1` or greater.
|
||||
Default value is `1`.
|
||||
format: int32
|
||||
type: integer
|
||||
secretName:
|
||||
description: |-
|
||||
Name of the Secret resource that will be automatically created and
|
||||
managed by this Certificate resource. It will be populated with a
|
||||
private key and certificate, signed by the denoted issuer. The Secret
|
||||
resource lives in the same namespace as the Certificate resource.
|
||||
type: string
|
||||
secretTemplate:
|
||||
description: |-
|
||||
Defines annotations and labels to be copied to the Certificate's Secret.
|
||||
Labels and annotations on the Secret will be changed as they appear on the
|
||||
SecretTemplate when added or removed. SecretTemplate annotations are added
|
||||
in conjunction with, and cannot overwrite, the base set of annotations
|
||||
cert-manager sets on the Certificate's Secret.
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Annotations is a key value map to be copied to the
|
||||
target Kubernetes Secret.
|
||||
type: object
|
||||
labels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Labels is a key value map to be copied to the target
|
||||
Kubernetes Secret.
|
||||
type: object
|
||||
type: object
|
||||
signatureAlgorithm:
|
||||
description: |-
|
||||
Signature algorithm to use.
|
||||
Allowed values for RSA keys: SHA256WithRSA, SHA384WithRSA, SHA512WithRSA.
|
||||
Allowed values for ECDSA keys: ECDSAWithSHA256, ECDSAWithSHA384, ECDSAWithSHA512.
|
||||
Allowed values for Ed25519 keys: PureEd25519.
|
||||
enum:
|
||||
- SHA256WithRSA
|
||||
- SHA384WithRSA
|
||||
- SHA512WithRSA
|
||||
- ECDSAWithSHA256
|
||||
- ECDSAWithSHA384
|
||||
- ECDSAWithSHA512
|
||||
- PureEd25519
|
||||
type: string
|
||||
subject:
|
||||
description: |-
|
||||
Requested set of X509 certificate subject attributes.
|
||||
More info: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6
|
||||
|
||||
The common name attribute is specified separately in the `commonName` field.
|
||||
Cannot be set if the `literalSubject` field is set.
|
||||
properties:
|
||||
countries:
|
||||
description: Countries to be used on the Certificate.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
localities:
|
||||
description: Cities to be used on the Certificate.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
organizationalUnits:
|
||||
description: Organizational Units to be used on the Certificate.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
organizations:
|
||||
description: Organizations to be used on the Certificate.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
postalCodes:
|
||||
description: Postal codes to be used on the Certificate.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
provinces:
|
||||
description: State/Provinces to be used on the Certificate.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
serialNumber:
|
||||
description: Serial number to be used on the Certificate.
|
||||
type: string
|
||||
streetAddresses:
|
||||
description: Street addresses to be used on the Certificate.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
type: object
|
||||
uris:
|
||||
description: Requested URI subject alternative names.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
usages:
|
||||
description: |-
|
||||
Requested key usages and extended key usages.
|
||||
These usages are used to set the `usages` field on the created CertificateRequest
|
||||
resources. If `encodeUsagesInRequest` is unset or set to `true`, the usages
|
||||
will additionally be encoded in the `request` field which contains the CSR blob.
|
||||
|
||||
If unset, defaults to `digital signature` and `key encipherment`.
|
||||
items:
|
||||
description: |-
|
||||
KeyUsage specifies valid usage contexts for keys.
|
||||
See:
|
||||
https://tools.ietf.org/html/rfc5280#section-4.2.1.3
|
||||
https://tools.ietf.org/html/rfc5280#section-4.2.1.12
|
||||
|
||||
Valid KeyUsage values are as follows:
|
||||
"signing",
|
||||
"digital signature",
|
||||
"content commitment",
|
||||
"key encipherment",
|
||||
"key agreement",
|
||||
"data encipherment",
|
||||
"cert sign",
|
||||
"crl sign",
|
||||
"encipher only",
|
||||
"decipher only",
|
||||
"any",
|
||||
"server auth",
|
||||
"client auth",
|
||||
"code signing",
|
||||
"email protection",
|
||||
"s/mime",
|
||||
"ipsec end system",
|
||||
"ipsec tunnel",
|
||||
"ipsec user",
|
||||
"timestamping",
|
||||
"ocsp signing",
|
||||
"microsoft sgc",
|
||||
"netscape sgc"
|
||||
enum:
|
||||
- signing
|
||||
- digital signature
|
||||
- content commitment
|
||||
- key encipherment
|
||||
- key agreement
|
||||
- data encipherment
|
||||
- cert sign
|
||||
- crl sign
|
||||
- encipher only
|
||||
- decipher only
|
||||
- any
|
||||
- server auth
|
||||
- client auth
|
||||
- code signing
|
||||
- email protection
|
||||
- s/mime
|
||||
- ipsec end system
|
||||
- ipsec tunnel
|
||||
- ipsec user
|
||||
- timestamping
|
||||
- ocsp signing
|
||||
- microsoft sgc
|
||||
- netscape sgc
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
required:
|
||||
- issuerRef
|
||||
- secretName
|
||||
type: object
|
||||
status:
|
||||
description: |-
|
||||
Status of the Certificate.
|
||||
This is set and managed automatically.
|
||||
Read-only.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
|
||||
properties:
|
||||
conditions:
|
||||
description: |-
|
||||
List of status conditions to indicate the status of certificates.
|
||||
Known condition types are `Ready` and `Issuing`.
|
||||
items:
|
||||
description: CertificateCondition contains condition information
|
||||
for a Certificate.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: |-
|
||||
LastTransitionTime is the timestamp corresponding to the last status
|
||||
change of this condition.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: |-
|
||||
Message is a human readable description of the details of the last
|
||||
transition, complementing reason.
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: |-
|
||||
If set, this represents the .metadata.generation that the condition was
|
||||
set based upon.
|
||||
For instance, if .metadata.generation is currently 12, but the
|
||||
.status.condition[x].observedGeneration is 9, the condition is out of date
|
||||
with respect to the current state of the Certificate.
|
||||
format: int64
|
||||
type: integer
|
||||
reason:
|
||||
description: |-
|
||||
Reason is a brief machine readable explanation for the condition's last
|
||||
transition.
|
||||
type: string
|
||||
status:
|
||||
description: Status of the condition, one of (`True`, `False`,
|
||||
`Unknown`).
|
||||
enum:
|
||||
- "True"
|
||||
- "False"
|
||||
- Unknown
|
||||
type: string
|
||||
type:
|
||||
description: Type of the condition, known values are (`Ready`,
|
||||
`Issuing`).
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
failedIssuanceAttempts:
|
||||
description: |-
|
||||
The number of continuous failed issuance attempts up till now. This
|
||||
field gets removed (if set) on a successful issuance and gets set to
|
||||
1 if unset and an issuance has failed. If an issuance has failed, the
|
||||
delay till the next issuance will be calculated using formula
|
||||
time.Hour * 2 ^ (failedIssuanceAttempts - 1).
|
||||
type: integer
|
||||
lastFailureTime:
|
||||
description: |-
|
||||
LastFailureTime is set only if the latest issuance for this
|
||||
Certificate failed and contains the time of the failure. If an
|
||||
issuance has failed, the delay till the next issuance will be
|
||||
calculated using formula time.Hour * 2 ^ (failedIssuanceAttempts -
|
||||
1). If the latest issuance has succeeded this field will be unset.
|
||||
format: date-time
|
||||
type: string
|
||||
nextPrivateKeySecretName:
|
||||
description: |-
|
||||
The name of the Secret resource containing the private key to be used
|
||||
for the next certificate iteration.
|
||||
The keymanager controller will automatically set this field if the
|
||||
`Issuing` condition is set to `True`.
|
||||
It will automatically unset this field when the Issuing condition is
|
||||
not set or False.
|
||||
type: string
|
||||
notAfter:
|
||||
description: |-
|
||||
The expiration time of the certificate stored in the secret named
|
||||
by this resource in `spec.secretName`.
|
||||
format: date-time
|
||||
type: string
|
||||
notBefore:
|
||||
description: |-
|
||||
The time after which the certificate stored in the secret named
|
||||
by this resource in `spec.secretName` is valid.
|
||||
format: date-time
|
||||
type: string
|
||||
renewalTime:
|
||||
description: |-
|
||||
RenewalTime is the time at which the certificate will be next
|
||||
renewed.
|
||||
If not set, no upcoming renewal is scheduled.
|
||||
format: date-time
|
||||
type: string
|
||||
revision:
|
||||
description: |-
|
||||
The current 'revision' of the certificate as issued.
|
||||
|
||||
When a CertificateRequest resource is created, it will have the
|
||||
`cert-manager.io/certificate-revision` set to one greater than the
|
||||
current value of this field.
|
||||
|
||||
Upon issuance, this field will be set to the value of the annotation
|
||||
on the CertificateRequest resource used to issue the certificate.
|
||||
|
||||
Persisting the value on the CertificateRequest resource allows the
|
||||
certificates controller to know whether a request is part of an old
|
||||
issuance or if it is part of the ongoing revision's issuance by
|
||||
checking if the revision value in the annotation is greater than this
|
||||
field.
|
||||
type: integer
|
||||
type: object
|
||||
type: object
|
||||
selectableFields:
|
||||
- jsonPath: .spec.issuerRef.group
|
||||
- jsonPath: .spec.issuerRef.kind
|
||||
- jsonPath: .spec.issuerRef.name
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
1759
Tests/Fixtures/real-yaml/cert-manager-values.yaml
Normal file
1759
Tests/Fixtures/real-yaml/cert-manager-values.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,48 @@
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: backend
|
||||
target: builder
|
||||
secrets:
|
||||
- db-password
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
restart: always
|
||||
user: postgres
|
||||
secrets:
|
||||
- db-password
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_DB=example
|
||||
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
|
||||
expose:
|
||||
- 5432
|
||||
healthcheck:
|
||||
test: [ "CMD", "pg_isready" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
proxy:
|
||||
image: nginx
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./proxy/nginx.conf
|
||||
target: /etc/nginx/conf.d/default.conf
|
||||
read_only: true
|
||||
ports:
|
||||
- 80:80
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
|
||||
secrets:
|
||||
db-password:
|
||||
file: db/password.txt
|
||||
84
Tests/Fixtures/real-yaml/github-actions-swift-format.yml
Normal file
84
Tests/Fixtures/real-yaml/github-actions-swift-format.yml
Normal file
@ -0,0 +1,84 @@
|
||||
name: Pull request
|
||||
|
||||
# PRs created by GitHub Actions don't kick off further actions (https://github.com/peter-evans/create-pull-request/blob/d57e551ebc1a16dee0b8c9ea6d24dba7627a6e35/docs/concepts-guidelines.md#triggering-further-workflow-runs).
|
||||
# As a workaround, we mark automerge PRs that are created by GitHub actions as draft and trigger the GitHub actions by marking the PR as ready for review. We'd prefer not re-triggering testing on a normal user's PR in this case, but skipping them causes the checks to reset.
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: Test
|
||||
uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@0.0.11
|
||||
with:
|
||||
enable_cross_pr_testing: true
|
||||
linux_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}]"
|
||||
windows_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}]"
|
||||
soundness:
|
||||
name: Soundness
|
||||
uses: swiftlang/github-workflows/.github/workflows/soundness.yml@0.0.11
|
||||
with:
|
||||
license_header_check_project_name: "Swift.org"
|
||||
api_breakage_check_allowlist_path: "api-breakages.txt"
|
||||
compatibility_check:
|
||||
# Format a list of real-world projects that enforce formatting using the latest swift-format version.
|
||||
name: Check compatibility with real world projects
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: swift:6.2
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Mark the workspace as safe
|
||||
# https://github.com/actions/checkout/issues/766
|
||||
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||
- name: Check out related PRs
|
||||
run: |
|
||||
apt-get update && apt-get install -y curl
|
||||
curl -s https://raw.githubusercontent.com/swiftlang/github-workflows/refs/heads/main/.github/workflows/scripts/cross-pr-checkout.swift > /tmp/cross-pr-checkout.swift
|
||||
swift /tmp/cross-pr-checkout.swift "${{ github.repository }}" "${{ github.event.number }}"
|
||||
- name: Build
|
||||
run: |
|
||||
swift build --configuration release
|
||||
- name: Add local swift-format to PATH
|
||||
run: |
|
||||
# Add the locally built version of swift-format to PATH so it gets picked up by check-swift-format.sh
|
||||
echo "PATH=$(pwd)/.build/release:$PATH" >> "$GITHUB_ENV"
|
||||
- name: Download check format script
|
||||
run: |
|
||||
curl https://raw.githubusercontent.com/swiftlang/github-workflows/refs/heads/main/.github/workflows/scripts/check-swift-format.sh > /tmp/check-swift-format.sh
|
||||
chmod +x /tmp/check-swift-format.sh
|
||||
- name: Format swift-format
|
||||
run: |
|
||||
cd "$GITHUB_WORKSPACE"
|
||||
/tmp/check-swift-format.sh
|
||||
- name: Checkout swift-syntax
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: swiftlang/swift-syntax
|
||||
persist-credentials: false
|
||||
path: swift-syntax
|
||||
- name: Format swift-syntax
|
||||
run: |
|
||||
cd "$GITHUB_WORKSPACE/swift-syntax"
|
||||
/tmp/check-swift-format.sh
|
||||
- name: Checkout sourcekit-lsp
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: swiftlang/sourcekit-lsp
|
||||
persist-credentials: false
|
||||
path: sourcekit-lsp
|
||||
- name: Format sourcekit-lsp
|
||||
run: |
|
||||
cd "$GITHUB_WORKSPACE/sourcekit-lsp"
|
||||
/tmp/check-swift-format.sh
|
||||
257280
Tests/Fixtures/real-yaml/github-rest-api.yaml
Normal file
257280
Tests/Fixtures/real-yaml/github-rest-api.yaml
Normal file
File diff suppressed because one or more lines are too long
12
Tests/Fixtures/real-yaml/kubernetes-simple-pod.yaml
Normal file
12
Tests/Fixtures/real-yaml/kubernetes-simple-pod.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: pods-simple-pod
|
||||
spec:
|
||||
containers:
|
||||
- command:
|
||||
- sleep
|
||||
- "3600"
|
||||
image: busybox
|
||||
name: pods-simple-container
|
||||
18
Tests/Fixtures/real-yaml/kustomize-wordpress.yaml
Normal file
18
Tests/Fixtures/real-yaml/kustomize-wordpress.yaml
Normal file
@ -0,0 +1,18 @@
|
||||
resources:
|
||||
- wordpress
|
||||
- mysql
|
||||
patches:
|
||||
- path: patch.yaml
|
||||
namePrefix: demo-
|
||||
|
||||
vars:
|
||||
- name: WORDPRESS_SERVICE
|
||||
objref:
|
||||
kind: Service
|
||||
name: wordpress
|
||||
apiVersion: v1
|
||||
- name: MYSQL_SERVICE
|
||||
objref:
|
||||
kind: Service
|
||||
name: mysql
|
||||
apiVersion: v1
|
||||
119
Tests/Fixtures/real-yaml/openapi-petstore.yaml
Normal file
119
Tests/Fixtures/real-yaml/openapi-petstore.yaml
Normal file
@ -0,0 +1,119 @@
|
||||
openapi: "3.0.0"
|
||||
info:
|
||||
version: 1.0.0
|
||||
title: Swagger Petstore
|
||||
license:
|
||||
name: MIT
|
||||
servers:
|
||||
- url: http://petstore.swagger.io/v1
|
||||
paths:
|
||||
/pets:
|
||||
get:
|
||||
summary: List all pets
|
||||
operationId: listPets
|
||||
tags:
|
||||
- pets
|
||||
parameters:
|
||||
- name: limit
|
||||
in: query
|
||||
description: How many items to return at one time (max 100)
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
maximum: 100
|
||||
format: int32
|
||||
responses:
|
||||
'200':
|
||||
description: A paged array of pets
|
||||
headers:
|
||||
x-next:
|
||||
description: A link to the next page of responses
|
||||
schema:
|
||||
type: string
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Pets"
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
post:
|
||||
summary: Create a pet
|
||||
operationId: createPets
|
||||
tags:
|
||||
- pets
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
required: true
|
||||
responses:
|
||||
'201':
|
||||
description: Null response
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/pets/{petId}:
|
||||
get:
|
||||
summary: Info for a specific pet
|
||||
operationId: showPetById
|
||||
tags:
|
||||
- pets
|
||||
parameters:
|
||||
- name: petId
|
||||
in: path
|
||||
required: true
|
||||
description: The id of the pet to retrieve
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Expected response to a valid request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Pet"
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
components:
|
||||
schemas:
|
||||
Pet:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
name:
|
||||
type: string
|
||||
tag:
|
||||
type: string
|
||||
Pets:
|
||||
type: array
|
||||
maxItems: 100
|
||||
items:
|
||||
$ref: "#/components/schemas/Pet"
|
||||
Error:
|
||||
type: object
|
||||
required:
|
||||
- code
|
||||
- message
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
format: int32
|
||||
message:
|
||||
type: string
|
||||
481
Tests/Fixtures/real-yaml/prometheus-conf-good.yml
Normal file
481
Tests/Fixtures/real-yaml/prometheus-conf-good.yml
Normal file
@ -0,0 +1,481 @@
|
||||
# my global config
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 30s
|
||||
body_size_limit: 15MB
|
||||
sample_limit: 1500
|
||||
target_limit: 30
|
||||
label_limit: 30
|
||||
label_name_length_limit: 200
|
||||
label_value_length_limit: 200
|
||||
query_log_file: query.log
|
||||
scrape_failure_log_file: fail.log
|
||||
# scrape_timeout is set to the global default (10s).
|
||||
|
||||
external_labels:
|
||||
monitor: codelab
|
||||
foo: bar
|
||||
|
||||
runtime:
|
||||
gogc: 42
|
||||
|
||||
rule_files:
|
||||
- "first.rules"
|
||||
- "my/*.rules"
|
||||
|
||||
remote_write:
|
||||
- url: http://remote1/push
|
||||
name: drop_expensive
|
||||
write_relabel_configs:
|
||||
- source_labels: [__name__]
|
||||
regex: expensive.*
|
||||
action: drop
|
||||
oauth2:
|
||||
client_id: "123"
|
||||
client_secret: "456"
|
||||
token_url: "http://remote1/auth"
|
||||
tls_config:
|
||||
cert_file: valid_cert_file
|
||||
key_file: valid_key_file
|
||||
|
||||
- url: http://remote2/push
|
||||
protobuf_message: io.prometheus.write.v2.Request
|
||||
name: rw_tls
|
||||
tls_config:
|
||||
cert_file: valid_cert_file
|
||||
key_file: valid_key_file
|
||||
headers:
|
||||
name: value
|
||||
|
||||
otlp:
|
||||
promote_resource_attributes: ["k8s.cluster.name", "k8s.job.name", "k8s.namespace.name"]
|
||||
|
||||
remote_read:
|
||||
- url: http://remote1/read
|
||||
read_recent: true
|
||||
name: default
|
||||
enable_http2: false
|
||||
- url: http://remote3/read
|
||||
read_recent: false
|
||||
name: read_special
|
||||
required_matchers:
|
||||
job: special
|
||||
tls_config:
|
||||
cert_file: valid_cert_file
|
||||
key_file: valid_key_file
|
||||
|
||||
scrape_configs:
|
||||
- job_name: prometheus
|
||||
|
||||
honor_labels: true
|
||||
# scrape_interval is defined by the configured global (15s).
|
||||
# scrape_timeout is defined by the global default (10s).
|
||||
|
||||
# metrics_path defaults to '/metrics'
|
||||
# scheme defaults to 'http'.
|
||||
|
||||
fallback_scrape_protocol: PrometheusText0.0.4
|
||||
|
||||
scrape_failure_log_file: fail_prom.log
|
||||
file_sd_configs:
|
||||
- files:
|
||||
- foo/*.slow.json
|
||||
- foo/*.slow.yml
|
||||
- single/file.yml
|
||||
refresh_interval: 10m
|
||||
- files:
|
||||
- bar/*.yaml
|
||||
|
||||
static_configs:
|
||||
- targets: ["localhost:9090", "localhost:9191"]
|
||||
labels:
|
||||
my: label
|
||||
your: label
|
||||
|
||||
http_headers:
|
||||
foo:
|
||||
values: ["foobar"]
|
||||
secrets: ["bar", "foo"]
|
||||
files: ["valid_password_file"]
|
||||
|
||||
relabel_configs:
|
||||
- source_labels: [job, __meta_dns_name]
|
||||
regex: (.*)some-[regex]
|
||||
target_label: job
|
||||
replacement: foo-${1}
|
||||
# action defaults to 'replace'
|
||||
- source_labels: [abc]
|
||||
target_label: cde
|
||||
- replacement: static
|
||||
target_label: abc
|
||||
- regex:
|
||||
replacement: static
|
||||
target_label: abc
|
||||
- source_labels: [foo]
|
||||
target_label: abc
|
||||
action: keepequal
|
||||
- source_labels: [foo]
|
||||
target_label: abc
|
||||
action: dropequal
|
||||
|
||||
authorization:
|
||||
credentials_file: valid_token_file
|
||||
|
||||
tls_config:
|
||||
min_version: TLS10
|
||||
|
||||
- job_name: service-x
|
||||
|
||||
basic_auth:
|
||||
username: admin_name
|
||||
password: "multiline\nmysecret\ntest"
|
||||
|
||||
scrape_interval: 50s
|
||||
scrape_timeout: 5s
|
||||
scrape_protocols: ["PrometheusText0.0.4"]
|
||||
|
||||
body_size_limit: 10MB
|
||||
sample_limit: 1000
|
||||
target_limit: 35
|
||||
label_limit: 35
|
||||
label_name_length_limit: 210
|
||||
label_value_length_limit: 210
|
||||
|
||||
metrics_path: /my_path
|
||||
scheme: https
|
||||
|
||||
dns_sd_configs:
|
||||
- refresh_interval: 15s
|
||||
names:
|
||||
- first.dns.address.domain.com
|
||||
- second.dns.address.domain.com
|
||||
- names:
|
||||
- first.dns.address.domain.com
|
||||
|
||||
relabel_configs:
|
||||
- source_labels: [job]
|
||||
regex: (.*)some-[regex]
|
||||
action: drop
|
||||
- source_labels: [__address__]
|
||||
modulus: 8
|
||||
target_label: __tmp_hash
|
||||
action: hashmod
|
||||
- source_labels: [__tmp_hash]
|
||||
regex: 1
|
||||
action: keep
|
||||
- action: labelmap
|
||||
regex: 1
|
||||
- action: labeldrop
|
||||
regex: d
|
||||
- action: labelkeep
|
||||
regex: k
|
||||
|
||||
metric_relabel_configs:
|
||||
- source_labels: [__name__]
|
||||
regex: expensive_metric.*
|
||||
action: drop
|
||||
|
||||
- job_name: service-y
|
||||
|
||||
consul_sd_configs:
|
||||
- server: "localhost:1234"
|
||||
token: mysecret
|
||||
path_prefix: /consul
|
||||
services: ["nginx", "cache", "mysql"]
|
||||
health_filter: 'Service.Tags contains "canary"'
|
||||
tags: ["canary", "v1"]
|
||||
node_meta:
|
||||
rack: "123"
|
||||
allow_stale: true
|
||||
scheme: https
|
||||
tls_config:
|
||||
ca_file: valid_ca_file
|
||||
cert_file: valid_cert_file
|
||||
key_file: valid_key_file
|
||||
insecure_skip_verify: false
|
||||
|
||||
relabel_configs:
|
||||
- source_labels: [__meta_sd_consul_tags]
|
||||
separator: ","
|
||||
regex: label:([^=]+)=([^,]+)
|
||||
target_label: ${1}
|
||||
replacement: ${2}
|
||||
|
||||
- job_name: service-z
|
||||
|
||||
tls_config:
|
||||
cert_file: valid_cert_file
|
||||
key_file: valid_key_file
|
||||
|
||||
authorization:
|
||||
credentials: mysecret
|
||||
|
||||
- job_name: service-kubernetes
|
||||
|
||||
kubernetes_sd_configs:
|
||||
- role: endpoints
|
||||
api_server: "https://localhost:1234"
|
||||
tls_config:
|
||||
cert_file: valid_cert_file
|
||||
key_file: valid_key_file
|
||||
|
||||
basic_auth:
|
||||
username: "myusername"
|
||||
password: "mysecret"
|
||||
|
||||
- job_name: service-kubernetes-namespaces
|
||||
|
||||
kubernetes_sd_configs:
|
||||
- role: endpoints
|
||||
api_server: "https://localhost:1234"
|
||||
namespaces:
|
||||
names:
|
||||
- default
|
||||
|
||||
basic_auth:
|
||||
username: "myusername"
|
||||
password_file: valid_password_file
|
||||
|
||||
- job_name: service-kuma
|
||||
|
||||
kuma_sd_configs:
|
||||
- server: http://kuma-control-plane.kuma-system.svc:5676
|
||||
client_id: main-prometheus
|
||||
|
||||
- job_name: service-marathon
|
||||
marathon_sd_configs:
|
||||
- servers:
|
||||
- "https://marathon.example.com:443"
|
||||
|
||||
auth_token: "mysecret"
|
||||
tls_config:
|
||||
cert_file: valid_cert_file
|
||||
key_file: valid_key_file
|
||||
|
||||
- job_name: service-nomad
|
||||
nomad_sd_configs:
|
||||
- server: 'http://localhost:4646'
|
||||
|
||||
- job_name: service-ec2
|
||||
ec2_sd_configs:
|
||||
- region: us-east-1
|
||||
access_key: access
|
||||
secret_key: mysecret
|
||||
profile: profile
|
||||
filters:
|
||||
- name: tag:environment
|
||||
values:
|
||||
- prod
|
||||
|
||||
- name: tag:service
|
||||
values:
|
||||
- web
|
||||
- db
|
||||
|
||||
- job_name: service-lightsail
|
||||
lightsail_sd_configs:
|
||||
- region: us-east-1
|
||||
access_key: access
|
||||
secret_key: mysecret
|
||||
profile: profile
|
||||
|
||||
- job_name: service-azure
|
||||
azure_sd_configs:
|
||||
- environment: AzurePublicCloud
|
||||
authentication_method: OAuth
|
||||
subscription_id: 11AAAA11-A11A-111A-A111-1111A1111A11
|
||||
resource_group: my-resource-group
|
||||
tenant_id: BBBB222B-B2B2-2B22-B222-2BB2222BB2B2
|
||||
client_id: 333333CC-3C33-3333-CCC3-33C3CCCCC33C
|
||||
client_secret: mysecret
|
||||
port: 9100
|
||||
|
||||
- job_name: service-nerve
|
||||
nerve_sd_configs:
|
||||
- servers:
|
||||
- localhost
|
||||
paths:
|
||||
- /monitoring
|
||||
|
||||
- job_name: 0123service-xxx
|
||||
metrics_path: /metrics
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:9090
|
||||
|
||||
- job_name: badfederation
|
||||
honor_timestamps: false
|
||||
metrics_path: /federate
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:9090
|
||||
|
||||
- job_name: 測試
|
||||
metrics_path: /metrics
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:9090
|
||||
|
||||
- job_name: httpsd
|
||||
http_sd_configs:
|
||||
- url: "http://example.com/prometheus"
|
||||
|
||||
- job_name: service-triton
|
||||
triton_sd_configs:
|
||||
- account: "testAccount"
|
||||
dns_suffix: "triton.example.com"
|
||||
endpoint: "triton.example.com"
|
||||
port: 9163
|
||||
refresh_interval: 1m
|
||||
version: 1
|
||||
tls_config:
|
||||
cert_file: valid_cert_file
|
||||
key_file: valid_key_file
|
||||
|
||||
- job_name: digitalocean-droplets
|
||||
digitalocean_sd_configs:
|
||||
- authorization:
|
||||
credentials: abcdef
|
||||
|
||||
- job_name: docker
|
||||
docker_sd_configs:
|
||||
- host: unix:///var/run/docker.sock
|
||||
|
||||
- job_name: dockerswarm
|
||||
dockerswarm_sd_configs:
|
||||
- host: http://127.0.0.1:2375
|
||||
role: nodes
|
||||
|
||||
- job_name: service-openstack
|
||||
openstack_sd_configs:
|
||||
- role: instance
|
||||
region: RegionOne
|
||||
port: 80
|
||||
refresh_interval: 1m
|
||||
tls_config:
|
||||
ca_file: valid_ca_file
|
||||
cert_file: valid_cert_file
|
||||
key_file: valid_key_file
|
||||
|
||||
- job_name: service-puppetdb
|
||||
puppetdb_sd_configs:
|
||||
- url: https://puppetserver/
|
||||
query: 'resources { type = "Package" and title = "httpd" }'
|
||||
include_parameters: true
|
||||
port: 80
|
||||
refresh_interval: 1m
|
||||
tls_config:
|
||||
ca_file: valid_ca_file
|
||||
cert_file: valid_cert_file
|
||||
key_file: valid_key_file
|
||||
|
||||
- job_name: hetzner
|
||||
scrape_native_histograms: true
|
||||
relabel_configs:
|
||||
- action: uppercase
|
||||
source_labels: [instance]
|
||||
target_label: instance
|
||||
hetzner_sd_configs:
|
||||
- role: hcloud
|
||||
authorization:
|
||||
credentials: abcdef
|
||||
- role: robot
|
||||
basic_auth:
|
||||
username: abcdef
|
||||
password: abcdef
|
||||
|
||||
- job_name: service-eureka
|
||||
eureka_sd_configs:
|
||||
- server: "http://eureka.example.com:8761/eureka"
|
||||
|
||||
- job_name: ovhcloud
|
||||
ovhcloud_sd_configs:
|
||||
- service: vps
|
||||
endpoint: ovh-eu
|
||||
application_key: testAppKey
|
||||
application_secret: testAppSecret
|
||||
consumer_key: testConsumerKey
|
||||
refresh_interval: 1m
|
||||
- service: dedicated_server
|
||||
endpoint: ovh-eu
|
||||
application_key: testAppKey
|
||||
application_secret: testAppSecret
|
||||
consumer_key: testConsumerKey
|
||||
refresh_interval: 1m
|
||||
|
||||
- job_name: scaleway
|
||||
scaleway_sd_configs:
|
||||
- role: instance
|
||||
project_id: 11111111-1111-1111-1111-111111111112
|
||||
access_key: SCWXXXXXXXXXXXXXXXXX
|
||||
secret_key: 11111111-1111-1111-1111-111111111111
|
||||
- role: baremetal
|
||||
project_id: 11111111-1111-1111-1111-111111111112
|
||||
access_key: SCWXXXXXXXXXXXXXXXXX
|
||||
secret_key: 11111111-1111-1111-1111-111111111111
|
||||
|
||||
- job_name: linode-instances
|
||||
linode_sd_configs:
|
||||
- authorization:
|
||||
credentials: abcdef
|
||||
|
||||
- job_name: stackit-servers
|
||||
stackit_sd_configs:
|
||||
- project: 11111111-1111-1111-1111-111111111111
|
||||
service_account_key: mysecret_sa_key
|
||||
private_key: mysecret_private_key
|
||||
authorization:
|
||||
credentials: abcdef
|
||||
|
||||
- job_name: uyuni
|
||||
uyuni_sd_configs:
|
||||
- server: https://localhost:1234
|
||||
username: gopher
|
||||
password: hole
|
||||
|
||||
- job_name: ionos
|
||||
ionos_sd_configs:
|
||||
- datacenter_id: 8feda53f-15f0-447f-badf-ebe32dad2fc0
|
||||
authorization:
|
||||
credentials: abcdef
|
||||
|
||||
- job_name: vultr
|
||||
vultr_sd_configs:
|
||||
- authorization:
|
||||
credentials: abcdef
|
||||
|
||||
- job_name: outscale
|
||||
outscale_sd_configs:
|
||||
- region: eu-west-2
|
||||
access_key: A1B2C3D4E5F6G7H8I9J0
|
||||
secret_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
|
||||
alerting:
|
||||
alertmanagers:
|
||||
- scheme: https
|
||||
static_configs:
|
||||
- targets:
|
||||
- "1.2.3.4:9093"
|
||||
- "1.2.3.5:9093"
|
||||
- "1.2.3.6:9093"
|
||||
|
||||
storage:
|
||||
tsdb:
|
||||
out_of_order_time_window: 30m
|
||||
stale_series_compaction_threshold: 0.5
|
||||
retention:
|
||||
time: 1d
|
||||
size: 1GB
|
||||
percentage: 28
|
||||
|
||||
tracing:
|
||||
endpoint: "localhost:4317"
|
||||
client_type: "grpc"
|
||||
headers:
|
||||
foo: "bar"
|
||||
timeout: 5s
|
||||
compression: "gzip"
|
||||
tls_config:
|
||||
cert_file: valid_cert_file
|
||||
key_file: valid_key_file
|
||||
insecure_skip_verify: true
|
||||
427
Tests/Fixtures/real-yaml/prow-cluster-api-presubmits.yaml
Normal file
427
Tests/Fixtures/real-yaml/prow-cluster-api-presubmits.yaml
Normal file
@ -0,0 +1,427 @@
|
||||
# Code generated by cluster-api's prowjob-gen. DO NOT EDIT.
|
||||
presubmits:
|
||||
kubernetes-sigs/cluster-api:
|
||||
- name: pull-cluster-api-build-release-1-13
|
||||
cluster: eks-prow-build-cluster
|
||||
decorate: true
|
||||
decoration_config:
|
||||
timeout: 180m
|
||||
path_alias: sigs.k8s.io/cluster-api
|
||||
always_run: true
|
||||
branches:
|
||||
# The script this job runs is not in all branches.
|
||||
- ^release-1.13$
|
||||
spec:
|
||||
containers:
|
||||
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
|
||||
command:
|
||||
- runner.sh
|
||||
- ./scripts/ci-build.sh
|
||||
resources:
|
||||
requests:
|
||||
cpu: 6000m
|
||||
memory: 4Gi
|
||||
limits:
|
||||
cpu: 6000m
|
||||
memory: 4Gi
|
||||
annotations:
|
||||
testgrid-dashboards: cluster-api-core-1.13
|
||||
testgrid-tab-name: capi-pr-build-release-1-13
|
||||
- name: pull-cluster-api-apidiff-release-1-13
|
||||
cluster: eks-prow-build-cluster
|
||||
decorate: true
|
||||
decoration_config:
|
||||
timeout: 180m
|
||||
path_alias: sigs.k8s.io/cluster-api
|
||||
optional: true
|
||||
branches:
|
||||
# The script this job runs is not in all branches.
|
||||
- ^release-1.13$
|
||||
run_if_changed: '^((api|bootstrap|cmd|config|controllers|controlplane|errors|exp|feature|hack|internal|scripts|test|util|webhooks|version)/|main\.go|go\.mod|go\.sum|Dockerfile|Makefile)'
|
||||
spec:
|
||||
containers:
|
||||
- command:
|
||||
- runner.sh
|
||||
- ./scripts/ci-apidiff.sh
|
||||
image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
|
||||
resources:
|
||||
requests:
|
||||
cpu: 6000m
|
||||
memory: 2Gi
|
||||
limits:
|
||||
cpu: 6000m
|
||||
memory: 2Gi
|
||||
annotations:
|
||||
testgrid-dashboards: cluster-api-core-1.13
|
||||
testgrid-tab-name: capi-pr-apidiff-release-1-13
|
||||
- name: pull-cluster-api-verify-release-1-13
|
||||
cluster: eks-prow-build-cluster
|
||||
decorate: true
|
||||
decoration_config:
|
||||
timeout: 180m
|
||||
path_alias: sigs.k8s.io/cluster-api
|
||||
always_run: true
|
||||
labels:
|
||||
preset-dind-enabled: "true"
|
||||
branches:
|
||||
# The script this job runs is not in all branches.
|
||||
- ^release-1.13$
|
||||
spec:
|
||||
containers:
|
||||
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
|
||||
command:
|
||||
- "runner.sh"
|
||||
- ./scripts/ci-verify.sh
|
||||
resources:
|
||||
requests:
|
||||
cpu: 5000m
|
||||
memory: 3Gi
|
||||
limits:
|
||||
cpu: 5000m
|
||||
memory: 3Gi
|
||||
securityContext:
|
||||
privileged: true
|
||||
annotations:
|
||||
testgrid-dashboards: cluster-api-core-1.13
|
||||
testgrid-tab-name: capi-pr-verify-release-1-13
|
||||
- name: pull-cluster-api-test-release-1-13
|
||||
cluster: eks-prow-build-cluster
|
||||
decorate: true
|
||||
decoration_config:
|
||||
timeout: 180m
|
||||
path_alias: sigs.k8s.io/cluster-api
|
||||
branches:
|
||||
# The script this job runs is not in all branches.
|
||||
- ^release-1.13$
|
||||
run_if_changed: '^((api|bootstrap|cmd|config|controllers|controlplane|errors|exp|feature|hack|internal|scripts|test|util|webhooks|version)/|main\.go|go\.mod|go\.sum|Dockerfile|Makefile)'
|
||||
spec:
|
||||
containers:
|
||||
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
|
||||
args:
|
||||
- runner.sh
|
||||
- ./scripts/ci-test.sh
|
||||
resources:
|
||||
requests:
|
||||
cpu: 7
|
||||
memory: 8Gi
|
||||
limits:
|
||||
cpu: 7
|
||||
memory: 8Gi
|
||||
annotations:
|
||||
testgrid-dashboards: cluster-api-core-1.13
|
||||
testgrid-tab-name: capi-pr-test-release-1-13
|
||||
- name: pull-cluster-api-test-mink8s-release-1-13
|
||||
cluster: eks-prow-build-cluster
|
||||
decorate: true
|
||||
decoration_config:
|
||||
timeout: 180m
|
||||
path_alias: sigs.k8s.io/cluster-api
|
||||
always_run: false
|
||||
branches:
|
||||
# The script this job runs is not in all branches.
|
||||
- ^release-1.13$
|
||||
spec:
|
||||
containers:
|
||||
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
|
||||
args:
|
||||
- runner.sh
|
||||
- ./scripts/ci-test.sh
|
||||
env:
|
||||
# This value determines the minimum Kubernetes
|
||||
# supported version for Cluster API management cluster
|
||||
# and can be found by referring to [Supported Kubernetes Version](https://cluster-api.sigs.k8s.io/reference/versions.html#supported-kubernetes-versions)
|
||||
# docs (look for minimum supported k8s version for management cluster, i.e N-3).
|
||||
#
|
||||
# To check the latest available envtest in Kubebuilder for the minor version we determined above, please
|
||||
# refer to https://github.com/kubernetes-sigs/kubebuilder/tree/tools-releases.
|
||||
- name: KUBEBUILDER_ENVTEST_KUBERNETES_VERSION
|
||||
value: "1.32.0"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 7
|
||||
memory: 8Gi
|
||||
limits:
|
||||
cpu: 7
|
||||
memory: 8Gi
|
||||
annotations:
|
||||
testgrid-dashboards: cluster-api-core-1.13
|
||||
testgrid-tab-name: capi-pr-test-mink8s-release-1-13
|
||||
- name: pull-cluster-api-e2e-mink8s-release-1-13
|
||||
cluster: eks-prow-build-cluster
|
||||
labels:
|
||||
preset-dind-enabled: "true"
|
||||
preset-kind-volume-mounts: "true"
|
||||
extra_refs:
|
||||
- org: kubernetes
|
||||
repo: kubernetes
|
||||
base_ref: master
|
||||
path_alias: k8s.io/kubernetes
|
||||
decorate: true
|
||||
decoration_config:
|
||||
timeout: 180m
|
||||
path_alias: sigs.k8s.io/cluster-api
|
||||
always_run: false
|
||||
branches:
|
||||
# The script this job runs is not in all branches.
|
||||
- ^release-1.13$
|
||||
spec:
|
||||
containers:
|
||||
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
|
||||
args:
|
||||
- runner.sh
|
||||
- ./scripts/ci-e2e.sh
|
||||
env:
|
||||
# enable IPV6 in bootstrap image
|
||||
- name: "DOCKER_IN_DOCKER_IPV6_ENABLED"
|
||||
value: "true"
|
||||
- name: GINKGO_LABEL_FILTER
|
||||
value: "!Conformance"
|
||||
# Ensure required kind images get built.
|
||||
- name: KIND_BUILD_IMAGES
|
||||
value: "KUBERNETES_VERSION,KUBERNETES_VERSION_LATEST_CI,KUBERNETES_VERSION_UPGRADE_TO,KUBERNETES_VERSION_UPGRADE_FROM"
|
||||
# This value determines the minimum Kubernetes
|
||||
# supported version for Cluster API management cluster
|
||||
# and can be found by referring to [Supported Kubernetes Version](https://cluster-api.sigs.k8s.io/reference/versions.html#supported-kubernetes-versions)
|
||||
# docs (look for minimum supported k8s version for management cluster, i.e N-3).
|
||||
# Please also make sure to refer a version where a kindest/node image exists
|
||||
# for (see https://github.com/kubernetes-sigs/kind/releases/)
|
||||
- name: KUBERNETES_VERSION_MANAGEMENT
|
||||
value: "v1.32.8"
|
||||
# we need privileged mode in order to do docker in docker
|
||||
securityContext:
|
||||
privileged: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: 3000m
|
||||
memory: 8Gi
|
||||
limits:
|
||||
cpu: 3000m
|
||||
memory: 8Gi
|
||||
annotations:
|
||||
testgrid-dashboards: cluster-api-core-1.13
|
||||
testgrid-tab-name: capi-pr-e2e-mink8s-release-1-13
|
||||
- name: pull-cluster-api-e2e-blocking-release-1-13
|
||||
cluster: eks-prow-build-cluster
|
||||
labels:
|
||||
preset-dind-enabled: "true"
|
||||
preset-kind-volume-mounts: "true"
|
||||
extra_refs:
|
||||
- org: kubernetes
|
||||
repo: kubernetes
|
||||
base_ref: master
|
||||
path_alias: k8s.io/kubernetes
|
||||
decorate: true
|
||||
decoration_config:
|
||||
timeout: 180m
|
||||
branches:
|
||||
# The script this job runs is not in all branches.
|
||||
- ^release-1.13$
|
||||
path_alias: sigs.k8s.io/cluster-api
|
||||
run_if_changed: '^((api|bootstrap|cmd|config|controllers|controlplane|errors|exp|feature|hack|internal|scripts|test|util|webhooks|version)/|main\.go|go\.mod|go\.sum|Dockerfile|Makefile)'
|
||||
spec:
|
||||
containers:
|
||||
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
|
||||
args:
|
||||
- runner.sh
|
||||
- "./scripts/ci-e2e.sh"
|
||||
env:
|
||||
- name: GINKGO_LABEL_FILTER
|
||||
value: "PR-Blocking"
|
||||
# Ensure required kind images get built.
|
||||
- name: KIND_BUILD_IMAGES
|
||||
value: "KUBERNETES_VERSION"
|
||||
# we need privileged mode in order to do docker in docker
|
||||
securityContext:
|
||||
privileged: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: 3000m
|
||||
memory: 8Gi
|
||||
limits:
|
||||
cpu: 3000m
|
||||
memory: 8Gi
|
||||
annotations:
|
||||
testgrid-dashboards: cluster-api-core-1.13
|
||||
testgrid-tab-name: capi-pr-e2e-blocking-release-1-13
|
||||
- name: pull-cluster-api-e2e-release-1-13
|
||||
cluster: eks-prow-build-cluster
|
||||
labels:
|
||||
preset-dind-enabled: "true"
|
||||
preset-kind-volume-mounts: "true"
|
||||
extra_refs:
|
||||
- org: kubernetes
|
||||
repo: kubernetes
|
||||
base_ref: master
|
||||
path_alias: k8s.io/kubernetes
|
||||
decorate: true
|
||||
decoration_config:
|
||||
timeout: 180m
|
||||
always_run: false
|
||||
branches:
|
||||
# The script this job runs is not in all branches.
|
||||
- ^release-1.13$
|
||||
path_alias: sigs.k8s.io/cluster-api
|
||||
spec:
|
||||
containers:
|
||||
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
|
||||
args:
|
||||
- runner.sh
|
||||
- "./scripts/ci-e2e.sh"
|
||||
env:
|
||||
# enable IPV6 in bootstrap image
|
||||
- name: "DOCKER_IN_DOCKER_IPV6_ENABLED"
|
||||
value: "true"
|
||||
- name: GINKGO_LABEL_FILTER
|
||||
value: "!Conformance"
|
||||
# Ensure required kind images get built.
|
||||
- name: KIND_BUILD_IMAGES
|
||||
value: "KUBERNETES_VERSION,KUBERNETES_VERSION_LATEST_CI,KUBERNETES_VERSION_UPGRADE_TO,KUBERNETES_VERSION_UPGRADE_FROM"
|
||||
# we need privileged mode in order to do docker in docker
|
||||
securityContext:
|
||||
privileged: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: 3000m
|
||||
memory: 8Gi
|
||||
limits:
|
||||
cpu: 3000m
|
||||
memory: 8Gi
|
||||
annotations:
|
||||
testgrid-dashboards: cluster-api-core-1.13
|
||||
testgrid-tab-name: capi-pr-e2e-release-1-13
|
||||
- name: pull-cluster-api-e2e-upgrade-1-36-1-37-release-1-13
|
||||
cluster: eks-prow-build-cluster
|
||||
labels:
|
||||
preset-dind-enabled: "true"
|
||||
preset-kind-volume-mounts: "true"
|
||||
decorate: true
|
||||
decoration_config:
|
||||
timeout: 180m
|
||||
always_run: false
|
||||
branches:
|
||||
# The script this job runs is not in all branches.
|
||||
- ^release-1.13$
|
||||
path_alias: sigs.k8s.io/cluster-api
|
||||
extra_refs:
|
||||
- org: kubernetes
|
||||
repo: kubernetes
|
||||
base_ref: master
|
||||
path_alias: k8s.io/kubernetes
|
||||
spec:
|
||||
containers:
|
||||
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
|
||||
args:
|
||||
- runner.sh
|
||||
- "./scripts/ci-e2e.sh"
|
||||
env:
|
||||
- name: ALWAYS_BUILD_KIND_IMAGES
|
||||
value: "true"
|
||||
- name: KUBERNETES_VERSION_UPGRADE_FROM
|
||||
value: "stable-1.36"
|
||||
- name: KUBERNETES_VERSION_UPGRADE_TO
|
||||
value: "ci/latest-1.37"
|
||||
- name: ETCD_VERSION_UPGRADE_TO
|
||||
value: "3.6.8-0"
|
||||
- name: COREDNS_VERSION_UPGRADE_TO
|
||||
value: "v1.14.2"
|
||||
- name: GINKGO_LABEL_FILTER
|
||||
value: "(Conformance && K8s-Upgrade)"
|
||||
# we need privileged mode in order to do docker in docker
|
||||
securityContext:
|
||||
privileged: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: 6000m
|
||||
memory: 6Gi
|
||||
limits:
|
||||
cpu: 6000m
|
||||
memory: 6Gi
|
||||
annotations:
|
||||
testgrid-dashboards: cluster-api-core-1.13
|
||||
testgrid-tab-name: capi-pr-e2e-release-1-13-1-36-1-37
|
||||
- name: pull-cluster-api-e2e-conformance-release-1-13
|
||||
cluster: eks-prow-build-cluster
|
||||
labels:
|
||||
preset-dind-enabled: "true"
|
||||
preset-kind-volume-mounts: "true"
|
||||
extra_refs:
|
||||
- org: kubernetes
|
||||
repo: kubernetes
|
||||
base_ref: master
|
||||
path_alias: k8s.io/kubernetes
|
||||
decorate: true
|
||||
decoration_config:
|
||||
timeout: 180m
|
||||
always_run: false
|
||||
branches:
|
||||
# The script this job runs is not in all branches.
|
||||
- ^release-1.13$
|
||||
path_alias: sigs.k8s.io/cluster-api
|
||||
spec:
|
||||
containers:
|
||||
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
|
||||
args:
|
||||
- runner.sh
|
||||
- "./scripts/ci-e2e.sh"
|
||||
env:
|
||||
- name: GINKGO_LABEL_FILTER
|
||||
value: "(Conformance && K8s-Install)"
|
||||
# Ensure required kind images get built.
|
||||
- name: KIND_BUILD_IMAGES
|
||||
value: "KUBERNETES_VERSION"
|
||||
# we need privileged mode in order to do docker in docker
|
||||
securityContext:
|
||||
privileged: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: 4000m
|
||||
memory: 4Gi
|
||||
limits:
|
||||
cpu: 4000m
|
||||
memory: 4Gi
|
||||
annotations:
|
||||
testgrid-dashboards: cluster-api-core-1.13
|
||||
testgrid-tab-name: capi-pr-e2e-conformance-release-1-13
|
||||
- name: pull-cluster-api-e2e-conformance-ci-latest-release-1-13
|
||||
cluster: eks-prow-build-cluster
|
||||
labels:
|
||||
preset-dind-enabled: "true"
|
||||
preset-kind-volume-mounts: "true"
|
||||
extra_refs:
|
||||
- org: kubernetes
|
||||
repo: kubernetes
|
||||
base_ref: master
|
||||
path_alias: k8s.io/kubernetes
|
||||
decorate: true
|
||||
decoration_config:
|
||||
timeout: 180m
|
||||
always_run: false
|
||||
branches:
|
||||
# The script this job runs is not in all branches.
|
||||
- ^release-1.13$
|
||||
path_alias: sigs.k8s.io/cluster-api
|
||||
spec:
|
||||
containers:
|
||||
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
|
||||
args:
|
||||
- runner.sh
|
||||
- "./scripts/ci-e2e.sh"
|
||||
env:
|
||||
- name: GINKGO_LABEL_FILTER
|
||||
value: "(Conformance && K8s-Install-ci-latest)"
|
||||
# Ensure required kind images get built.
|
||||
- name: KIND_BUILD_IMAGES
|
||||
value: "KUBERNETES_VERSION_LATEST_CI"
|
||||
# we need privileged mode in order to do docker in docker
|
||||
securityContext:
|
||||
privileged: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: 4000m
|
||||
memory: 4Gi
|
||||
limits:
|
||||
cpu: 4000m
|
||||
memory: 4Gi
|
||||
annotations:
|
||||
testgrid-dashboards: cluster-api-core-1.13
|
||||
testgrid-tab-name: capi-pr-e2e-conformance-ci-latest-release-1-13
|
||||
7
Tests/Fixtures/validation-failures/duplicate-keys.yaml
Normal file
7
Tests/Fixtures/validation-failures/duplicate-keys.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
title: First
|
||||
title: Second
|
||||
paths:
|
||||
/users:
|
||||
get: {}
|
||||
/users:
|
||||
post: {}
|
||||
@ -0,0 +1,3 @@
|
||||
slug: missing-title
|
||||
metadata:
|
||||
draft: true
|
||||
4
Tests/Fixtures/validation-failures/warnings.yaml
Normal file
4
Tests/Fixtures/validation-failures/warnings.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
title: Legacy
|
||||
deprecated: true
|
||||
nested:
|
||||
deprecated: true
|
||||
22
Tests/ParsingAliasTests.swift
Normal file
22
Tests/ParsingAliasTests.swift
Normal file
@ -0,0 +1,22 @@
|
||||
@testable import PureYAML
|
||||
import Testing
|
||||
|
||||
@Suite("Parsing Aliases")
|
||||
struct ParsingAliasTests {
|
||||
@Test("Resolves anchors and aliases through the event composer")
|
||||
func test_anchorsAndAliases() throws {
|
||||
let root = try requireMapping(PureYAML.parse(
|
||||
"""
|
||||
first: &item {name: Example}
|
||||
second: *item
|
||||
""",
|
||||
))
|
||||
|
||||
#expect(root?["first"] == root?["second"])
|
||||
guard case let .mapping(second)? = root?["second"] else {
|
||||
recordIssue("expected aliased mapping")
|
||||
return
|
||||
}
|
||||
#expect(second["name"] == .string("Example"))
|
||||
}
|
||||
}
|
||||
67
Tests/ParsingErrorTests.swift
Normal file
67
Tests/ParsingErrorTests.swift
Normal file
@ -0,0 +1,67 @@
|
||||
@testable import PureYAML
|
||||
import Testing
|
||||
|
||||
@Suite("Parsing Errors")
|
||||
struct ParsingErrorTests {
|
||||
@Test("Reports exact parser errors")
|
||||
func test_errorReporting() {
|
||||
expectParseError("", .emptyDocument)
|
||||
expectParseError("# comment only", .emptyDocument)
|
||||
expectParseError("\tkey: value", .tabIndentation(line: 1))
|
||||
expectParseError("root:\n child: yes\n wrong: no", .unexpectedIndentation(line: 3))
|
||||
expectParseError("- one\nkey: value", .mixedCollectionStyles(line: 2))
|
||||
expectParseError("root:\n child: value\n dangling", .unexpectedToken(
|
||||
expected: "mapping key",
|
||||
actual: "scalar value=\"dangling\" style=plain",
|
||||
line: 3,
|
||||
column: 3,
|
||||
))
|
||||
expectParseError("name: \"open", .unterminatedQuotedString(line: 1))
|
||||
expectParseError("name: 'open", .unterminatedQuotedString(line: 1))
|
||||
expectParseError("second: *missing", .undefinedAlias(anchor: "missing", line: 1, column: 9))
|
||||
expectParseError("%YAML 1.3\n---\nvalue: yes", .incompatibleYAMLDirective(line: 1))
|
||||
expectParseError("value: !!int nope", .invalidTaggedScalar(
|
||||
tag: "tag:yaml.org,2002:int",
|
||||
value: "nope",
|
||||
line: 1,
|
||||
column: 8,
|
||||
))
|
||||
}
|
||||
|
||||
@Test("Parser errors describe returned failures")
|
||||
func test_errorDescriptions() {
|
||||
#expect(PureYAML.Parsing.ParseError.emptyDocument.description == "document is empty")
|
||||
#expect(PureYAML.Parsing.ParseError.tabIndentation(line: 2).description == "tabs are not allowed for indentation at line 2")
|
||||
#expect(PureYAML.Parsing.ParseError.unexpectedIndentation(line: 3).description == "unexpected indentation at line 3")
|
||||
#expect(PureYAML.Parsing.ParseError.mixedCollectionStyles(line: 4).description == "mapping and sequence entries are mixed at line 4")
|
||||
#expect(PureYAML.Parsing.ParseError.expectedMappingKey(line: 5).description == "expected a mapping key at line 5")
|
||||
#expect(PureYAML.Parsing.ParseError.expectedAnchorName(line: 6).description == "expected an anchor name at line 6")
|
||||
#expect(PureYAML.Parsing.ParseError.expectedAliasName(line: 7).description == "expected an alias name at line 7")
|
||||
#expect(PureYAML.Parsing.ParseError.unterminatedTag(line: 8).description == "unterminated tag at line 8")
|
||||
#expect(PureYAML.Parsing.ParseError.unterminatedQuotedString(line: 9).description == "unterminated quoted string at line 9")
|
||||
#expect(PureYAML.Parsing.ParseError.incompatibleYAMLDirective(line: 10).description == "incompatible YAML directive at line 10")
|
||||
#expect(PureYAML.Parsing.ParseError.unsupportedDirective(name: "%FOO", line: 10).description == "unsupported directive '%FOO' at line 10")
|
||||
#expect(PureYAML.Parsing.ParseError.unsupportedMultiDocumentStream(line: 10).description == "multi-document streams are not supported at line 10")
|
||||
#expect(PureYAML.Parsing.ParseError.expectedNode(line: 10, column: 2).description == "expected a YAML node at line 10, column 2")
|
||||
#expect(PureYAML.Parsing.ParseError.expectedScalarKey(line: 11, column: 3).description == "expected a scalar mapping key at line 11, column 3")
|
||||
#expect(PureYAML.Parsing.ParseError.undefinedAlias(anchor: "item", line: 12, column: 4).description == "undefined alias 'item' at line 12, column 4")
|
||||
#expect(PureYAML.Parsing.ParseError.invalidTaggedScalar(
|
||||
tag: "tag:yaml.org,2002:int",
|
||||
value: "nope",
|
||||
line: 13,
|
||||
column: 5,
|
||||
).description == "invalid scalar 'nope' for tag 'tag:yaml.org,2002:int' at line 13, column 5")
|
||||
#expect(PureYAML.Parsing.ParseError.unexpectedEvent(
|
||||
expected: "mapping key",
|
||||
actual: "sequenceStart anchor=- tag=- style=flow @1:1@0",
|
||||
line: 13,
|
||||
column: 5,
|
||||
).description == "expected mapping key at line 13, column 5, found sequenceStart anchor=- tag=- style=flow @1:1@0")
|
||||
#expect(PureYAML.Parsing.ParseError.unexpectedToken(
|
||||
expected: "stream end",
|
||||
actual: "mappingKey",
|
||||
line: 14,
|
||||
column: 1,
|
||||
).description == "expected stream end at line 14, column 1, found mappingKey")
|
||||
}
|
||||
}
|
||||
218
Tests/ParsingIndentationRegressionTests.swift
Normal file
218
Tests/ParsingIndentationRegressionTests.swift
Normal file
@ -0,0 +1,218 @@
|
||||
@testable import PureYAML
|
||||
import Testing
|
||||
|
||||
@Suite("Parsing Indentation Regressions")
|
||||
struct ParsingIndentationRegressionTests {
|
||||
@Test("Parses compact sequence item mappings without swallowing lower-column siblings")
|
||||
func test_compactSequenceItemMappingsDoNotSwallowLowerColumnSiblings() throws {
|
||||
let root = try requireMapping(PureYAML.parse(
|
||||
"""
|
||||
patches:
|
||||
- path: patch.yaml
|
||||
namePrefix: demo-
|
||||
|
||||
vars:
|
||||
- name: WORDPRESS_SERVICE
|
||||
objref:
|
||||
kind: Service
|
||||
""",
|
||||
))
|
||||
|
||||
let patch = root?.sequence("patches")?.first?.mapping
|
||||
let variable = root?.sequence("vars")?.first?.mapping
|
||||
|
||||
#expect(patch?["path"] == .string("patch.yaml"))
|
||||
#expect(patch?["namePrefix"] == nil)
|
||||
#expect(root?["namePrefix"] == .string("demo-"))
|
||||
#expect(variable?["name"] == .string("WORDPRESS_SERVICE"))
|
||||
#expect(variable?.mapping("objref")?["kind"] == .string("Service"))
|
||||
#expect(root?["missing"] == nil)
|
||||
}
|
||||
|
||||
@Test("Parses compact sequence item mappings after indentless sequence values")
|
||||
func test_compactSequenceItemMappingsAfterIndentlessSequenceValues() throws {
|
||||
let root = try requireMapping(PureYAML.parse(
|
||||
"""
|
||||
spec:
|
||||
containers:
|
||||
- command:
|
||||
- runner.sh
|
||||
- ./scripts/ci-apidiff.sh
|
||||
image: gcr.io/k8s-staging-test-infra/kubekins-e2e
|
||||
resources:
|
||||
requests:
|
||||
cpu: 6000m
|
||||
""",
|
||||
))
|
||||
|
||||
let container = root?.mapping("spec")?.sequence("containers")?.first?.mapping
|
||||
#expect(container?.sequence("command") == [
|
||||
.string("runner.sh"),
|
||||
.string("./scripts/ci-apidiff.sh"),
|
||||
])
|
||||
#expect(container?["image"] == .string("gcr.io/k8s-staging-test-infra/kubekins-e2e"))
|
||||
#expect(container?.mapping("resources")?.mapping("requests")?["cpu"] == .string("6000m"))
|
||||
#expect(container?["missing"] == nil)
|
||||
}
|
||||
|
||||
@Test("Parses lower-column mapping siblings after nested compact sequence item mappings")
|
||||
func test_lowerColumnMappingSiblingsAfterNestedCompactSequenceItemMappings() throws {
|
||||
let root = try requireMapping(PureYAML.parse(
|
||||
"""
|
||||
presubmits:
|
||||
org/repo:
|
||||
- name: first
|
||||
spec:
|
||||
containers:
|
||||
- command:
|
||||
- runner.sh
|
||||
resources:
|
||||
requests:
|
||||
cpu: 6000m
|
||||
limits:
|
||||
memory: 2Gi
|
||||
annotations:
|
||||
testgrid-dashboards: core
|
||||
- name: second
|
||||
cluster: build
|
||||
""",
|
||||
))
|
||||
|
||||
let jobs = root?.mapping("presubmits")?.sequence("org/repo")
|
||||
let first = jobs?.first?.mapping
|
||||
let second = jobs?.dropFirst().first?.mapping
|
||||
let container = first?.mapping("spec")?.sequence("containers")?.first?.mapping
|
||||
|
||||
#expect(container?.sequence("command") == [.string("runner.sh")])
|
||||
#expect(container?.mapping("resources")?.mapping("requests")?["cpu"] == .string("6000m"))
|
||||
#expect(container?.mapping("resources")?.mapping("limits")?["memory"] == .string("2Gi"))
|
||||
#expect(first?.mapping("annotations")?["testgrid-dashboards"] == .string("core"))
|
||||
#expect(second?["name"] == .string("second"))
|
||||
#expect(second?["cluster"] == .string("build"))
|
||||
#expect(first?["missing"] == nil)
|
||||
}
|
||||
|
||||
@Test("Parses indentless sequence values followed by flow siblings")
|
||||
func test_indentlessSequenceValuesFollowedByFlowSiblings() throws {
|
||||
let root = try requireMapping(PureYAML.parse(
|
||||
"""
|
||||
approveSignerNames:
|
||||
- issuers.cert-manager.io/*
|
||||
- clusterissuers.cert-manager.io/*
|
||||
extraArgs: []
|
||||
""",
|
||||
))
|
||||
|
||||
#expect(root?.sequence("approveSignerNames") == [
|
||||
.string("issuers.cert-manager.io/*"),
|
||||
.string("clusterissuers.cert-manager.io/*"),
|
||||
])
|
||||
#expect(root?.sequence("extraArgs") == [])
|
||||
#expect(root?["missing"] == nil)
|
||||
}
|
||||
|
||||
@Test("Parses nested block sequence values followed by mapping siblings")
|
||||
func test_nestedBlockSequenceValuesFollowedByMappingSiblings() throws {
|
||||
let root = try requireMapping(PureYAML.parse(
|
||||
"""
|
||||
sort_by:
|
||||
- - 123
|
||||
- asc
|
||||
- - 456
|
||||
- desc
|
||||
group_by:
|
||||
- 123
|
||||
vertical_group_by:
|
||||
- 456
|
||||
""",
|
||||
))
|
||||
|
||||
#expect(root?.sequence("sort_by") == [
|
||||
.sequence([.int(123), .string("asc")]),
|
||||
.sequence([.int(456), .string("desc")]),
|
||||
])
|
||||
#expect(root?.sequence("group_by") == [.int(123)])
|
||||
#expect(root?.sequence("vertical_group_by") == [.int(456)])
|
||||
#expect(root?["missing"] == nil)
|
||||
}
|
||||
|
||||
@Test("Parses explicit block scalar indentation indicators")
|
||||
func test_explicitBlockScalarIndentationIndicators() throws {
|
||||
let root = try requireMapping(PureYAML.parse(
|
||||
"""
|
||||
example:
|
||||
value: |2
|
||||
MMM. .MMM
|
||||
MMMMMMMMMMMMMMMMMMM
|
||||
MMMMMMMMMMMMMMMMMMMMM | Avoid administrative distraction. |
|
||||
next: done
|
||||
""",
|
||||
))
|
||||
|
||||
#expect(
|
||||
root?.mapping("example")?["value"] ==
|
||||
.string(" MMM. .MMM\n MMMMMMMMMMMMMMMMMMM\nMMMMMMMMMMMMMMMMMMMMM | Avoid administrative distraction. |\n"),
|
||||
)
|
||||
#expect(root?.mapping("example")?["next"] == .string("done"))
|
||||
#expect(root?.mapping("example")?["missing"] == nil)
|
||||
}
|
||||
|
||||
@Test("Parses sequence item mappings after nested values")
|
||||
func test_sequenceItemMappingsAfterNestedValues() throws {
|
||||
let root = try requireMapping(PureYAML.parse(
|
||||
"""
|
||||
containers:
|
||||
- command:
|
||||
- sleep
|
||||
- "3600"
|
||||
image: busybox
|
||||
name: worker
|
||||
""",
|
||||
))
|
||||
|
||||
let containers = requireSequence(root?["containers"])
|
||||
guard let firstContainer = containers?.first else {
|
||||
recordIssue("expected first container")
|
||||
return
|
||||
}
|
||||
let container = requireMapping(firstContainer)
|
||||
|
||||
#expect(container?["command"] == .sequence([
|
||||
.string("sleep"),
|
||||
.string("3600"),
|
||||
]))
|
||||
#expect(container?["image"] == .string("busybox"))
|
||||
#expect(container?["name"] == .string("worker"))
|
||||
}
|
||||
|
||||
@Test("Parses sequence item mapping siblings after deep nested mapping values")
|
||||
func test_sequenceItemMappingSiblingsAfterDeepNestedMappingValues() throws {
|
||||
let root = try requireMapping(PureYAML.parse(
|
||||
"""
|
||||
runs:
|
||||
- tool:
|
||||
driver:
|
||||
name: CodeQL
|
||||
rules:
|
||||
- id: js/unused-local-variable
|
||||
name: js/unused-local-variable
|
||||
results:
|
||||
- guid: 326aa09f
|
||||
message:
|
||||
text: Unused variable foo.
|
||||
""",
|
||||
))
|
||||
|
||||
let run = root?.sequence("runs")?.first?.mapping
|
||||
let driver = run?.mapping("tool")?.mapping("driver")
|
||||
let rule = driver?.sequence("rules")?.first?.mapping
|
||||
let result = run?.sequence("results")?.first?.mapping
|
||||
|
||||
#expect(driver?["name"] == .string("CodeQL"))
|
||||
#expect(rule?["id"] == .string("js/unused-local-variable"))
|
||||
#expect(rule?["name"] == .string("js/unused-local-variable"))
|
||||
#expect(result?["guid"] == .string("326aa09f"))
|
||||
#expect(result?.mapping("message")?["text"] == .string("Unused variable foo."))
|
||||
#expect(run?["missing"] == nil)
|
||||
}
|
||||
}
|
||||
199
Tests/ParsingMultilineScalarTests.swift
Normal file
199
Tests/ParsingMultilineScalarTests.swift
Normal file
@ -0,0 +1,199 @@
|
||||
@testable import PureYAML
|
||||
import Testing
|
||||
|
||||
@Suite("Parsing Multiline Scalars")
|
||||
struct ParsingMultilineScalarTests {
|
||||
@Test("Parses multiline plain scalars in mappings")
|
||||
func test_multilinePlainScalarsInMappings() throws {
|
||||
let root = try requireMapping(PureYAML.parse(
|
||||
"""
|
||||
columns:
|
||||
- description: CreationTimestamp is a timestamp representing the server time when
|
||||
this object was created. It is not guaranteed to be set in happens-before
|
||||
order across separate operations.
|
||||
jsonPath: .metadata.creationTimestamp
|
||||
""",
|
||||
))
|
||||
|
||||
let expectedDescription = [
|
||||
"CreationTimestamp is a timestamp representing the server time when this object was created.",
|
||||
"It is not guaranteed to be set in happens-before order across separate operations.",
|
||||
].joined(separator: " ")
|
||||
let column = root?.sequence("columns")?.first?.mapping
|
||||
#expect(column?["description"] == .string(expectedDescription))
|
||||
#expect(column?["jsonPath"] == .string(".metadata.creationTimestamp"))
|
||||
#expect(column?["missing"] == nil)
|
||||
}
|
||||
|
||||
@Test("Parses multiline plain scalars before the next sequence entry")
|
||||
func test_multilinePlainScalarsBeforeNextSequenceEntry() throws {
|
||||
let value = try PureYAML.parse(
|
||||
"""
|
||||
- name: code-quality
|
||||
description: Insights into reliability, maintainability, and efficiency of your
|
||||
codebase.
|
||||
- name: codespaces
|
||||
description: Endpoints to manage Codespaces using the REST API.
|
||||
""",
|
||||
)
|
||||
|
||||
let tags = requireSequence(value)
|
||||
let first = tags?.first?.mapping
|
||||
let second = tags?.dropFirst().first?.mapping
|
||||
|
||||
#expect(first?["name"] == .string("code-quality"))
|
||||
#expect(first?["description"] == .string("Insights into reliability, maintainability, and efficiency of your codebase."))
|
||||
#expect(second?["name"] == .string("codespaces"))
|
||||
#expect(second?["description"] == .string("Endpoints to manage Codespaces using the REST API."))
|
||||
#expect(first?["missing"] == nil)
|
||||
}
|
||||
|
||||
@Test("Parses multiline plain scalar sequence items")
|
||||
func test_multilinePlainScalarSequenceItems() throws {
|
||||
let root = try requireMapping(PureYAML.parse(
|
||||
"""
|
||||
examples:
|
||||
- Updated instructions to help the team with planning and coordination
|
||||
tasks
|
||||
next: done
|
||||
""",
|
||||
))
|
||||
|
||||
#expect(root?.sequence("examples") == [
|
||||
.string("Updated instructions to help the team with planning and coordination tasks"),
|
||||
])
|
||||
#expect(root?["next"] == .string("done"))
|
||||
#expect(root?["missing"] == nil)
|
||||
}
|
||||
|
||||
@Test("Parses multiline plain scalar continuations that start with quotes")
|
||||
func test_multilinePlainScalarContinuationsThatStartWithQuotes() throws {
|
||||
let root = try requireMapping(PureYAML.parse(
|
||||
"""
|
||||
get:
|
||||
description: Gets a team using the team's slug. To create the slug, GitHub replaces
|
||||
special characters in the name string, changes all words to lowercase, and
|
||||
replaces spaces with a `-` separator and adds the "ent:" prefix. For example,
|
||||
"My TEam Näme" would become `ent:my-team-name`.
|
||||
tags:
|
||||
- enterprise-teams
|
||||
""",
|
||||
))
|
||||
|
||||
let expectedDescription = [
|
||||
"Gets a team using the team's slug.",
|
||||
"To create the slug, GitHub replaces special characters in the name string,",
|
||||
"changes all words to lowercase, and replaces spaces with a `-` separator",
|
||||
"and adds the \"ent:\" prefix. For example,",
|
||||
"\"My TEam Näme\" would become `ent:my-team-name`.",
|
||||
].joined(separator: " ")
|
||||
let get = root?.mapping("get")
|
||||
#expect(get?["description"] == .string(expectedDescription))
|
||||
#expect(get?.sequence("tags") == [.string("enterprise-teams")])
|
||||
#expect(get?["missing"] == nil)
|
||||
}
|
||||
|
||||
@Test("Parses multiline plain scalar continuations that start with flow markers")
|
||||
func test_multilinePlainScalarContinuationsThatStartWithFlowMarkers() throws {
|
||||
let root = try requireMapping(PureYAML.parse(
|
||||
"""
|
||||
selected_repository_ids:
|
||||
description: An array of repository ids that can access the organization
|
||||
secret. You can manage the list of
|
||||
selected repositories using the [List selected repositories for
|
||||
an organization secret](https://docs.example.com/list),
|
||||
[Set selected repositories for an organization secret](https://docs.example.com/set),
|
||||
and [Remove selected repository from an organization secret](https://docs.example.com/remove)
|
||||
endpoints.
|
||||
items:
|
||||
type: integer
|
||||
""",
|
||||
))
|
||||
|
||||
let expectedDescription = [
|
||||
"An array of repository ids that can access the organization secret.",
|
||||
"You can manage the list of selected repositories using the",
|
||||
"[List selected repositories for an organization secret](https://docs.example.com/list),",
|
||||
"[Set selected repositories for an organization secret](https://docs.example.com/set),",
|
||||
"and [Remove selected repository from an organization secret](https://docs.example.com/remove) endpoints.",
|
||||
].joined(separator: " ")
|
||||
let selected = root?.mapping("selected_repository_ids")
|
||||
#expect(selected?["description"] == .string(expectedDescription))
|
||||
#expect(selected?.mapping("items")?["type"] == .string("integer"))
|
||||
#expect(selected?["missing"] == nil)
|
||||
}
|
||||
|
||||
@Test("Parses flow-marker continuations after previous sequence indentation")
|
||||
func test_flowMarkerContinuationsAfterPreviousSequenceIndentation() throws {
|
||||
let root = try requireMapping(PureYAML.parse(
|
||||
"""
|
||||
schema:
|
||||
enum:
|
||||
- all
|
||||
- private
|
||||
selected_repository_ids:
|
||||
description: An array of repository ids that can access the organization
|
||||
[Set selected repositories](https://docs.example.com/set),
|
||||
and more.
|
||||
items:
|
||||
type: integer
|
||||
""",
|
||||
))
|
||||
|
||||
let selected = root?.mapping("schema")?.mapping("selected_repository_ids")
|
||||
#expect(
|
||||
selected?["description"] ==
|
||||
.string("An array of repository ids that can access the organization [Set selected repositories](https://docs.example.com/set), and more."),
|
||||
)
|
||||
#expect(selected?.mapping("items")?["type"] == .string("integer"))
|
||||
#expect(selected?["missing"] == nil)
|
||||
#expect(root?.mapping("schema")?.sequence("enum") == [.string("all"), .string("private")])
|
||||
}
|
||||
|
||||
@Test("Parses colon-style markup inside multiline plain scalars")
|
||||
func test_colonStyleMarkupInsideMultilinePlainScalars() throws {
|
||||
let root = try requireMapping(PureYAML.parse(
|
||||
"""
|
||||
label:
|
||||
description: The name of the label. Emoji can be added to label
|
||||
names, using either native emoji or colon-style markup. For example,
|
||||
typing `:strawberry:` will render the emoji . For a full list of available emoji and codes.
|
||||
color: f29513
|
||||
""",
|
||||
))
|
||||
|
||||
let expectedDescription = [
|
||||
"The name of the label. Emoji can be added to label names,",
|
||||
"using either native emoji or colon-style markup. For example,",
|
||||
"typing `:strawberry:` will render the emoji",
|
||||
".",
|
||||
"For a full list of available emoji and codes.",
|
||||
].joined(separator: " ")
|
||||
#expect(root?.mapping("label")?["description"] == .string(expectedDescription))
|
||||
#expect(root?.mapping("label")?["color"] == .string("f29513"))
|
||||
#expect(root?.mapping("label")?["missing"] == nil)
|
||||
}
|
||||
|
||||
@Test("Parses block scalar text that starts with YAML indicators")
|
||||
func test_blockScalarTextThatStartsWithYAMLIndicators() throws {
|
||||
let root = try requireMapping(PureYAML.parse(
|
||||
"""
|
||||
description: |
|
||||
**Fine-grained access tokens**
|
||||
|
||||
* [GitHub App user access tokens](https://docs.github.com)
|
||||
- dash list item stays text
|
||||
""",
|
||||
))
|
||||
|
||||
#expect(root?["description"] == .string(
|
||||
"""
|
||||
**Fine-grained access tokens**
|
||||
* [GitHub App user access tokens](https://docs.github.com)
|
||||
- dash list item stays text
|
||||
|
||||
""",
|
||||
))
|
||||
}
|
||||
}
|
||||
@ -130,6 +130,89 @@ struct ParsingTests {
|
||||
#expect(second["description"] == .string("Production"))
|
||||
}
|
||||
|
||||
@Test("Parses indentless sequences as mapping values")
|
||||
func test_indentlessSequencesAsMappingValues() throws {
|
||||
let root = try requireMapping(PureYAML.parse(
|
||||
"""
|
||||
resources:
|
||||
- wordpress
|
||||
- mysql
|
||||
patches:
|
||||
- path: patch.yaml
|
||||
""",
|
||||
))
|
||||
|
||||
#expect(root?["resources"] == .sequence([
|
||||
.string("wordpress"),
|
||||
.string("mysql"),
|
||||
]))
|
||||
#expect(root?["patches"] == .sequence([
|
||||
.mapping(.init([
|
||||
.init(key: "path", value: .string("patch.yaml")),
|
||||
])),
|
||||
]))
|
||||
#expect(root?["missing"] == nil)
|
||||
}
|
||||
|
||||
@Test("Parses nested indentless sequence values followed by mapping siblings")
|
||||
func test_nestedIndentlessSequenceValuesInsideMappingsFollowedBySiblings() throws {
|
||||
let root = try requireMapping(PureYAML.parse(
|
||||
"""
|
||||
spec:
|
||||
names:
|
||||
categories:
|
||||
- cert-manager
|
||||
kind: Certificate
|
||||
shortNames:
|
||||
- cert
|
||||
- certs
|
||||
singular: certificate
|
||||
""",
|
||||
))
|
||||
|
||||
let names = root?.mapping("spec")?.mapping("names")
|
||||
#expect(names?.sequence("categories") == [.string("cert-manager")])
|
||||
#expect(names?["kind"] == .string("Certificate"))
|
||||
#expect(names?.sequence("shortNames") == [.string("cert"), .string("certs")])
|
||||
#expect(names?["singular"] == .string("certificate"))
|
||||
#expect(names?["missing"] == nil)
|
||||
}
|
||||
|
||||
@Test("Parses indentless sequence values inside sequence item mappings")
|
||||
func test_indentlessSequenceValuesInsideSequenceItemMappings() throws {
|
||||
let root = try requireMapping(PureYAML.parse(
|
||||
"""
|
||||
presubmits:
|
||||
kubernetes-sigs/cluster-api:
|
||||
- name: pull-cluster-api-build-release-1-13
|
||||
branches:
|
||||
- ^release-1.13$
|
||||
spec:
|
||||
containers:
|
||||
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e
|
||||
command:
|
||||
- runner.sh
|
||||
- ./scripts/ci-build.sh
|
||||
annotations:
|
||||
testgrid-dashboards: cluster-api-core-1.13
|
||||
""",
|
||||
))
|
||||
|
||||
let jobs = root?.mapping("presubmits")?.sequence("kubernetes-sigs/cluster-api")
|
||||
let job = jobs?.first?.mapping
|
||||
let container = job?.mapping("spec")?.sequence("containers")?.first?.mapping
|
||||
|
||||
#expect(job?["name"] == .string("pull-cluster-api-build-release-1-13"))
|
||||
#expect(job?.sequence("branches") == [.string("^release-1.13$")])
|
||||
#expect(container?["image"] == .string("gcr.io/k8s-staging-test-infra/kubekins-e2e"))
|
||||
#expect(container?.sequence("command") == [
|
||||
.string("runner.sh"),
|
||||
.string("./scripts/ci-build.sh"),
|
||||
])
|
||||
#expect(job?.mapping("annotations")?["testgrid-dashboards"] == .string("cluster-api-core-1.13"))
|
||||
#expect(job?["missing"] == nil)
|
||||
}
|
||||
|
||||
@Test("Parses top-level sequences")
|
||||
func test_topLevelSequences() throws {
|
||||
let value = try PureYAML.parse(
|
||||
@ -189,83 +272,4 @@ struct ParsingTests {
|
||||
#expect(root?["stripped"] == .string("one\ntwo"))
|
||||
#expect(root?["folded"] == .string("one two\n"))
|
||||
}
|
||||
|
||||
@Test("Resolves anchors and aliases through the event composer")
|
||||
func test_anchorsAndAliases() throws {
|
||||
let root = try requireMapping(PureYAML.parse(
|
||||
"""
|
||||
first: &item {name: Example}
|
||||
second: *item
|
||||
""",
|
||||
))
|
||||
|
||||
#expect(root?["first"] == root?["second"])
|
||||
guard case let .mapping(second)? = root?["second"] else {
|
||||
recordIssue("expected aliased mapping")
|
||||
return
|
||||
}
|
||||
#expect(second["name"] == .string("Example"))
|
||||
}
|
||||
|
||||
@Test("Reports exact parser errors")
|
||||
func test_errorReporting() {
|
||||
expectParseError("", .emptyDocument)
|
||||
expectParseError("# comment only", .emptyDocument)
|
||||
expectParseError("\tkey: value", .tabIndentation(line: 1))
|
||||
expectParseError("root:\n child: yes\n wrong: no", .unexpectedIndentation(line: 3))
|
||||
expectParseError("- one\nkey: value", .mixedCollectionStyles(line: 2))
|
||||
expectParseError("root:\n child: value\n dangling", .unexpectedToken(
|
||||
expected: "mapping key",
|
||||
actual: "scalar value=\"dangling\" style=plain",
|
||||
line: 3,
|
||||
column: 3,
|
||||
))
|
||||
expectParseError("name: \"open", .unterminatedQuotedString(line: 1))
|
||||
expectParseError("name: 'open", .unterminatedQuotedString(line: 1))
|
||||
expectParseError("second: *missing", .undefinedAlias(anchor: "missing", line: 1, column: 9))
|
||||
expectParseError("%YAML 1.3\n---\nvalue: yes", .incompatibleYAMLDirective(line: 1))
|
||||
expectParseError("value: !!int nope", .invalidTaggedScalar(
|
||||
tag: "tag:yaml.org,2002:int",
|
||||
value: "nope",
|
||||
line: 1,
|
||||
column: 8,
|
||||
))
|
||||
}
|
||||
|
||||
@Test("Parser errors describe returned failures")
|
||||
func test_errorDescriptions() {
|
||||
#expect(PureYAML.Parsing.ParseError.emptyDocument.description == "document is empty")
|
||||
#expect(PureYAML.Parsing.ParseError.tabIndentation(line: 2).description == "tabs are not allowed for indentation at line 2")
|
||||
#expect(PureYAML.Parsing.ParseError.unexpectedIndentation(line: 3).description == "unexpected indentation at line 3")
|
||||
#expect(PureYAML.Parsing.ParseError.mixedCollectionStyles(line: 4).description == "mapping and sequence entries are mixed at line 4")
|
||||
#expect(PureYAML.Parsing.ParseError.expectedMappingKey(line: 5).description == "expected a mapping key at line 5")
|
||||
#expect(PureYAML.Parsing.ParseError.expectedAnchorName(line: 6).description == "expected an anchor name at line 6")
|
||||
#expect(PureYAML.Parsing.ParseError.expectedAliasName(line: 7).description == "expected an alias name at line 7")
|
||||
#expect(PureYAML.Parsing.ParseError.unterminatedTag(line: 8).description == "unterminated tag at line 8")
|
||||
#expect(PureYAML.Parsing.ParseError.unterminatedQuotedString(line: 9).description == "unterminated quoted string at line 9")
|
||||
#expect(PureYAML.Parsing.ParseError.incompatibleYAMLDirective(line: 10).description == "incompatible YAML directive at line 10")
|
||||
#expect(PureYAML.Parsing.ParseError.unsupportedDirective(name: "%FOO", line: 10).description == "unsupported directive '%FOO' at line 10")
|
||||
#expect(PureYAML.Parsing.ParseError.unsupportedMultiDocumentStream(line: 10).description == "multi-document streams are not supported at line 10")
|
||||
#expect(PureYAML.Parsing.ParseError.expectedNode(line: 10, column: 2).description == "expected a YAML node at line 10, column 2")
|
||||
#expect(PureYAML.Parsing.ParseError.expectedScalarKey(line: 11, column: 3).description == "expected a scalar mapping key at line 11, column 3")
|
||||
#expect(PureYAML.Parsing.ParseError.undefinedAlias(anchor: "item", line: 12, column: 4).description == "undefined alias 'item' at line 12, column 4")
|
||||
#expect(PureYAML.Parsing.ParseError.invalidTaggedScalar(
|
||||
tag: "tag:yaml.org,2002:int",
|
||||
value: "nope",
|
||||
line: 13,
|
||||
column: 5,
|
||||
).description == "invalid scalar 'nope' for tag 'tag:yaml.org,2002:int' at line 13, column 5")
|
||||
#expect(PureYAML.Parsing.ParseError.unexpectedEvent(
|
||||
expected: "mapping key",
|
||||
actual: "sequenceStart anchor=- tag=- style=flow @1:1@0",
|
||||
line: 13,
|
||||
column: 5,
|
||||
).description == "expected mapping key at line 13, column 5, found sequenceStart anchor=- tag=- style=flow @1:1@0")
|
||||
#expect(PureYAML.Parsing.ParseError.unexpectedToken(
|
||||
expected: "stream end",
|
||||
actual: "mappingKey",
|
||||
line: 14,
|
||||
column: 1,
|
||||
).description == "expected stream end at line 14, column 1, found mappingKey")
|
||||
}
|
||||
}
|
||||
|
||||
355
Tests/RealYAMLFixtureTests.swift
Normal file
355
Tests/RealYAMLFixtureTests.swift
Normal file
@ -0,0 +1,355 @@
|
||||
import Foundation
|
||||
@testable import PureYAML
|
||||
import Testing
|
||||
|
||||
@Suite("Real YAML Fixtures")
|
||||
struct RealYAMLFixtureTests {
|
||||
@Test("Real YAML fixture files match their pinned manifest", arguments: RealYAMLFixture.allCases)
|
||||
func test_realYAMLFixtureFilesMatchPinnedManifest(fixture: RealYAMLFixture) throws {
|
||||
let source = try fixture.load()
|
||||
|
||||
#expect(source.utf8.count == fixture.byteCount)
|
||||
#expect(source.count(where: { $0 == "\n" }) == fixture.lineCount)
|
||||
|
||||
for requiredSubstring in fixture.requiredSubstrings {
|
||||
#expect(source.contains(requiredSubstring))
|
||||
}
|
||||
for forbiddenSubstring in fixture.forbiddenSubstrings {
|
||||
#expect(!source.contains(forbiddenSubstring))
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Parses real YAML fixtures with exact representative values", arguments: RealYAMLFixture.allCases)
|
||||
func test_parsesRealYAMLFixturesWithExactRepresentativeValues(fixture: RealYAMLFixture) throws {
|
||||
let value = try PureYAML.parse(fixture.load())
|
||||
|
||||
fixture.expectRepresentativeValues(in: value)
|
||||
#expect(PureYAML.Validation.Validator().collect(value) == PureYAML.Validation.Result())
|
||||
}
|
||||
}
|
||||
|
||||
enum RealYAMLFixture: String, CaseIterable, CustomStringConvertible {
|
||||
case certManagerCertificateCRD = "cert-manager-certificate-crd.yaml"
|
||||
case certManagerValues = "cert-manager-values.yaml"
|
||||
case dockerComposeNginxGolangPostgres = "docker-compose-nginx-golang-postgres.yaml"
|
||||
case githubActionsSwiftFormat = "github-actions-swift-format.yml"
|
||||
case githubRestAPI = "github-rest-api.yaml"
|
||||
case kubernetesSimplePod = "kubernetes-simple-pod.yaml"
|
||||
case kustomizeWordPress = "kustomize-wordpress.yaml"
|
||||
case openAPIPetstore = "openapi-petstore.yaml"
|
||||
case prometheusGoodConfig = "prometheus-conf-good.yml"
|
||||
case prowClusterAPIPresubmits = "prow-cluster-api-presubmits.yaml"
|
||||
|
||||
var description: String {
|
||||
rawValue
|
||||
}
|
||||
|
||||
var lineCount: Int {
|
||||
switch self {
|
||||
case .certManagerCertificateCRD:
|
||||
883
|
||||
case .certManagerValues:
|
||||
1759
|
||||
case .dockerComposeNginxGolangPostgres:
|
||||
48
|
||||
case .githubActionsSwiftFormat:
|
||||
84
|
||||
case .githubRestAPI:
|
||||
257_280
|
||||
case .kubernetesSimplePod:
|
||||
12
|
||||
case .kustomizeWordPress:
|
||||
18
|
||||
case .openAPIPetstore:
|
||||
119
|
||||
case .prometheusGoodConfig:
|
||||
481
|
||||
case .prowClusterAPIPresubmits:
|
||||
427
|
||||
}
|
||||
}
|
||||
|
||||
var byteCount: Int {
|
||||
switch self {
|
||||
case .certManagerCertificateCRD:
|
||||
44059
|
||||
case .certManagerValues:
|
||||
65536
|
||||
case .dockerComposeNginxGolangPostgres:
|
||||
852
|
||||
case .githubActionsSwiftFormat:
|
||||
3591
|
||||
case .githubRestAPI:
|
||||
9_549_118
|
||||
case .kubernetesSimplePod:
|
||||
186
|
||||
case .kustomizeWordPress:
|
||||
262
|
||||
case .openAPIPetstore:
|
||||
2768
|
||||
case .prometheusGoodConfig:
|
||||
11562
|
||||
case .prowClusterAPIPresubmits:
|
||||
14213
|
||||
}
|
||||
}
|
||||
|
||||
var requiredSubstrings: [String] {
|
||||
switch self {
|
||||
case .certManagerCertificateCRD:
|
||||
[
|
||||
"apiVersion: apiextensions.k8s.io/v1",
|
||||
"jsonPath: .status.conditions[?(@.type == \"Ready\")].status",
|
||||
"Known condition types are `Ready` and `Issuing`.",
|
||||
]
|
||||
case .certManagerValues:
|
||||
[
|
||||
"installCRDs: false",
|
||||
"hostUsers: false",
|
||||
"webhook:",
|
||||
]
|
||||
case .dockerComposeNginxGolangPostgres:
|
||||
[
|
||||
"services:",
|
||||
"test: [ \"CMD\", \"pg_isready\" ]",
|
||||
"file: db/password.txt",
|
||||
]
|
||||
case .githubActionsSwiftFormat:
|
||||
[
|
||||
"name: Pull request",
|
||||
"types: [opened, reopened, synchronize, ready_for_review]",
|
||||
"run: |",
|
||||
]
|
||||
case .githubRestAPI:
|
||||
[
|
||||
"openapi: 3.1.0",
|
||||
"title: GitHub v3 REST API",
|
||||
"components:",
|
||||
]
|
||||
case .kubernetesSimplePod:
|
||||
[
|
||||
"kind: Pod",
|
||||
"name: pods-simple-container",
|
||||
]
|
||||
case .kustomizeWordPress:
|
||||
[
|
||||
"namePrefix: demo-",
|
||||
"name: WORDPRESS_SERVICE",
|
||||
]
|
||||
case .openAPIPetstore:
|
||||
[
|
||||
"title: Swagger Petstore",
|
||||
"operationId: listPets",
|
||||
"'201':",
|
||||
]
|
||||
case .prometheusGoodConfig:
|
||||
[
|
||||
"scrape_interval: 15s",
|
||||
"fallback_scrape_protocol: PrometheusText0.0.4",
|
||||
"password: \"multiline\\nmysecret\\ntest\"",
|
||||
]
|
||||
case .prowClusterAPIPresubmits:
|
||||
[
|
||||
"presubmits:",
|
||||
"run_if_changed: '^((api|bootstrap|cmd|config|controllers|controlplane|errors|exp|feature|hack|internal|scripts|test|util"
|
||||
+ "|webhooks|version)/|main\\.go|go\\.mod|go\\.sum|Dockerfile|Makefile)'",
|
||||
"securityContext:",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
var forbiddenSubstrings: [String] {
|
||||
[
|
||||
"<<<<<<<",
|
||||
"=======",
|
||||
">>>>>>>",
|
||||
]
|
||||
}
|
||||
|
||||
func load() throws -> String {
|
||||
let url = try #require(Bundle.module.url(
|
||||
forResource: resourceName,
|
||||
withExtension: resourceExtension,
|
||||
subdirectory: "Fixtures/real-yaml",
|
||||
))
|
||||
return try String(contentsOf: url, encoding: .utf8)
|
||||
}
|
||||
|
||||
func expectRepresentativeValues(in value: PureYAML.Model.Value) {
|
||||
switch self {
|
||||
case .certManagerCertificateCRD:
|
||||
expectCertManagerCertificateCRD(value)
|
||||
case .certManagerValues:
|
||||
expectCertManagerValues(value)
|
||||
case .dockerComposeNginxGolangPostgres:
|
||||
expectDockerComposeNginxGolangPostgres(value)
|
||||
case .githubActionsSwiftFormat:
|
||||
expectGitHubActionsSwiftFormat(value)
|
||||
case .githubRestAPI:
|
||||
expectGitHubRestAPI(value)
|
||||
case .kubernetesSimplePod:
|
||||
expectKubernetesSimplePod(value)
|
||||
case .kustomizeWordPress:
|
||||
expectKustomizeWordPress(value)
|
||||
case .openAPIPetstore:
|
||||
expectOpenAPIPetstore(value)
|
||||
case .prometheusGoodConfig:
|
||||
expectPrometheusGoodConfig(value)
|
||||
case .prowClusterAPIPresubmits:
|
||||
expectProwClusterAPIPresubmits(value)
|
||||
}
|
||||
}
|
||||
|
||||
private var resourceName: String {
|
||||
guard let dotIndex = rawValue.lastIndex(of: ".") else {
|
||||
return rawValue
|
||||
}
|
||||
return String(rawValue[..<dotIndex])
|
||||
}
|
||||
|
||||
private var resourceExtension: String {
|
||||
guard let dotIndex = rawValue.lastIndex(of: ".") else {
|
||||
return ""
|
||||
}
|
||||
return String(rawValue[rawValue.index(after: dotIndex)...])
|
||||
}
|
||||
}
|
||||
|
||||
private func expectCertManagerCertificateCRD(_ value: PureYAML.Model.Value) {
|
||||
let root = requireMapping(value)
|
||||
|
||||
#expect(root?["apiVersion"] == .string("apiextensions.k8s.io/v1"))
|
||||
#expect(root?["kind"] == .string("CustomResourceDefinition"))
|
||||
#expect(root?.mapping("metadata")?["name"] == .string("certificates.cert-manager.io"))
|
||||
|
||||
let spec = root?.mapping("spec")
|
||||
#expect(spec?.mapping("names")?["kind"] == .string("Certificate"))
|
||||
|
||||
let versions = requireSequence(spec?["versions"])
|
||||
let firstVersion = versions?.first?.mapping
|
||||
let firstColumn = firstVersion?.sequence("additionalPrinterColumns")?.first?.mapping
|
||||
|
||||
#expect(firstColumn?["name"] == .string("Ready"))
|
||||
#expect(firstColumn?["jsonPath"] == .string(".status.conditions[?(@.type == \"Ready\")].status"))
|
||||
}
|
||||
|
||||
private func expectCertManagerValues(_ value: PureYAML.Model.Value) {
|
||||
let root = requireMapping(value)
|
||||
|
||||
#expect(root?.mapping("global")?.mapping("rbac")?["create"] == .bool(true))
|
||||
#expect(root?["installCRDs"] == .bool(false))
|
||||
#expect(root?.mapping("crds")?["enabled"] == .bool(false))
|
||||
#expect(root?.mapping("crds")?["keep"] == .bool(true))
|
||||
#expect(root?["replicaCount"] == .int(1))
|
||||
#expect(root?.mapping("webhook")?["timeoutSeconds"] == .int(30))
|
||||
}
|
||||
|
||||
private func expectDockerComposeNginxGolangPostgres(_ value: PureYAML.Model.Value) {
|
||||
let root = requireMapping(value)
|
||||
let services = root?.mapping("services")
|
||||
|
||||
#expect(services?.mapping("backend")?.mapping("build")?["context"] == .string("backend"))
|
||||
#expect(services?.mapping("backend")?.mapping("depends_on")?.mapping("db")?["condition"] == .string("service_healthy"))
|
||||
#expect(services?.mapping("db")?["image"] == .string("postgres"))
|
||||
#expect(services?.mapping("db")?.sequence("healthcheck", "test") == [.string("CMD"), .string("pg_isready")])
|
||||
#expect(services?.mapping("proxy")?.sequence("depends_on") == [.string("backend")])
|
||||
#expect(root?.mapping("secrets")?.mapping("db-password")?["file"] == .string("db/password.txt"))
|
||||
}
|
||||
|
||||
private func expectGitHubActionsSwiftFormat(_ value: PureYAML.Model.Value) {
|
||||
let root = requireMapping(value)
|
||||
|
||||
#expect(root?["name"] == .string("Pull request"))
|
||||
#expect(root?.mapping("permissions")?["contents"] == .string("read"))
|
||||
#expect(root?.mapping("on")?.mapping("pull_request")?.sequence("types") == [
|
||||
.string("opened"),
|
||||
.string("reopened"),
|
||||
.string("synchronize"),
|
||||
.string("ready_for_review"),
|
||||
])
|
||||
#expect(root?.mapping("concurrency")?["cancel-in-progress"] == .bool(true))
|
||||
#expect(
|
||||
root?.mapping("jobs")?.mapping("tests")?["uses"] ==
|
||||
.string("swiftlang/github-workflows/.github/workflows/swift_package_test.yml@0.0.11"),
|
||||
)
|
||||
#expect(root?.mapping("jobs")?.mapping("compatibility_check")?.mapping("container")?["image"] == .string("swift:6.2"))
|
||||
}
|
||||
|
||||
private func expectGitHubRestAPI(_ value: PureYAML.Model.Value) {
|
||||
let root = requireMapping(value)
|
||||
|
||||
#expect(root?["openapi"] == .string("3.1.0"))
|
||||
#expect(root?.mapping("info")?["title"] == .string("GitHub v3 REST API"))
|
||||
#expect(root?.mapping("info")?["version"] == .string("1.1.4"))
|
||||
|
||||
let firstTag = root?.sequence("tags")?.first?.mapping
|
||||
#expect(firstTag?["name"] == .string("actions"))
|
||||
#expect(root?.mapping("components")?.mapping("schemas")?.mapping("repository")?.mapping("properties")?.mapping("full_name")?["type"] == .string("string"))
|
||||
}
|
||||
|
||||
private func expectKubernetesSimplePod(_ value: PureYAML.Model.Value) {
|
||||
let root = requireMapping(value)
|
||||
|
||||
#expect(root?["apiVersion"] == .string("v1"))
|
||||
#expect(root?["kind"] == .string("Pod"))
|
||||
#expect(root?.mapping("metadata")?["name"] == .string("pods-simple-pod"))
|
||||
|
||||
let container = root?.mapping("spec")?.sequence("containers")?.first?.mapping
|
||||
#expect(container?["image"] == .string("busybox"))
|
||||
#expect(container?["name"] == .string("pods-simple-container"))
|
||||
#expect(container?.sequence("command") == [.string("sleep"), .string("3600")])
|
||||
}
|
||||
|
||||
private func expectKustomizeWordPress(_ value: PureYAML.Model.Value) {
|
||||
let root = requireMapping(value)
|
||||
|
||||
#expect(root?.sequence("resources") == [.string("wordpress"), .string("mysql")])
|
||||
#expect(root?.sequence("patches")?.first?.mapping?["path"] == .string("patch.yaml"))
|
||||
#expect(root?["namePrefix"] == .string("demo-"))
|
||||
|
||||
let firstVariable = root?.sequence("vars")?.first?.mapping
|
||||
#expect(firstVariable?["name"] == .string("WORDPRESS_SERVICE"))
|
||||
#expect(firstVariable?.mapping("objref")?["kind"] == .string("Service"))
|
||||
#expect(firstVariable?.mapping("objref")?["name"] == .string("wordpress"))
|
||||
}
|
||||
|
||||
private func expectOpenAPIPetstore(_ value: PureYAML.Model.Value) {
|
||||
let root = requireMapping(value)
|
||||
|
||||
#expect(root?["openapi"] == .string("3.0.0"))
|
||||
#expect(root?.mapping("info")?["title"] == .string("Swagger Petstore"))
|
||||
#expect(root?.mapping("paths")?.mapping("/pets")?.mapping("get")?["operationId"] == .string("listPets"))
|
||||
#expect(root?.mapping("paths")?.mapping("/pets")?.mapping("post")?.mapping("responses")?["201"] != nil)
|
||||
#expect(root?.mapping("components")?.mapping("schemas")?.mapping("Pet")?.sequence("required") == [
|
||||
.string("id"),
|
||||
.string("name"),
|
||||
])
|
||||
}
|
||||
|
||||
private func expectPrometheusGoodConfig(_ value: PureYAML.Model.Value) {
|
||||
let root = requireMapping(value)
|
||||
|
||||
#expect(root?.mapping("global")?["scrape_interval"] == .string("15s"))
|
||||
#expect(root?.mapping("global")?.mapping("external_labels")?["monitor"] == .string("codelab"))
|
||||
#expect(root?.sequence("remote_write")?.first?.mapping?.mapping("oauth2")?["client_id"] == .string("123"))
|
||||
|
||||
let staticTargets = root?
|
||||
.sequence("scrape_configs")?.first?
|
||||
.mapping?
|
||||
.sequence("static_configs")?.first?
|
||||
.mapping?
|
||||
.sequence("targets")
|
||||
#expect(staticTargets == [.string("localhost:9090"), .string("localhost:9191")])
|
||||
}
|
||||
|
||||
private func expectProwClusterAPIPresubmits(_ value: PureYAML.Model.Value) {
|
||||
let root = requireMapping(value)
|
||||
let jobs = root?.mapping("presubmits")?.sequence("kubernetes-sigs/cluster-api")
|
||||
let firstJob = jobs?.first?.mapping
|
||||
|
||||
#expect(firstJob?["name"] == .string("pull-cluster-api-build-release-1-13"))
|
||||
#expect(firstJob?["cluster"] == .string("eks-prow-build-cluster"))
|
||||
#expect(firstJob?["always_run"] == .bool(true))
|
||||
#expect(firstJob?.sequence("branches") == [.string("^release-1.13$")])
|
||||
#expect(firstJob?.mapping("spec")?.sequence("containers")?.first?.mapping?["image"] == .string("gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35"))
|
||||
}
|
||||
@ -31,6 +31,35 @@ func requireSequence(
|
||||
return values
|
||||
}
|
||||
|
||||
extension PureYAML.Model.Value {
|
||||
var mapping: PureYAML.Model.Mapping? {
|
||||
guard case let .mapping(mapping) = self else {
|
||||
return nil
|
||||
}
|
||||
return mapping
|
||||
}
|
||||
}
|
||||
|
||||
extension PureYAML.Model.Mapping {
|
||||
func mapping(_ key: String) -> PureYAML.Model.Mapping? {
|
||||
self[key]?.mapping
|
||||
}
|
||||
|
||||
func sequence(_ keys: String...) -> [PureYAML.Model.Value]? {
|
||||
var value: PureYAML.Model.Value?
|
||||
for key in keys {
|
||||
let mapping: PureYAML.Model.Mapping? = if let currentValue = value {
|
||||
currentValue.mapping
|
||||
} else {
|
||||
self
|
||||
}
|
||||
value = mapping?[key]
|
||||
}
|
||||
|
||||
return requireSequence(value)
|
||||
}
|
||||
}
|
||||
|
||||
func expectParseError(
|
||||
_ yaml: String,
|
||||
_ expected: PureYAML.Parsing.ParseError,
|
||||
|
||||
134
Tests/ValidationFailureFixtureTests.swift
Normal file
134
Tests/ValidationFailureFixtureTests.swift
Normal file
@ -0,0 +1,134 @@
|
||||
import Foundation
|
||||
@testable import PureYAML
|
||||
import Testing
|
||||
|
||||
@Suite("Validation Failure Fixtures")
|
||||
struct ValidationFailureFixtureTests {
|
||||
@Test("Deliberately failing YAML fixtures report exact validation descriptions", arguments: validationFailureFixtures)
|
||||
func test_deliberatelyFailingFixturesReportExactValidationDescriptions(testCase: ValidationFailureFixture) throws {
|
||||
let value = try testCase.loadValue()
|
||||
let result = testCase.validator.collect(value)
|
||||
|
||||
#expect(result.issues == testCase.expectedIssues)
|
||||
#expect(result.issues.map(\.description) == testCase.expectedDescriptions)
|
||||
#expect(result.descriptionLines == testCase.expectedDescriptions)
|
||||
#expect(!result.isValid == testCase.expectedIssues.contains { $0.severity == .error })
|
||||
|
||||
expectValidationError(value, using: testCase.validator) { collection in
|
||||
#expect(collection.issues == testCase.expectedIssues)
|
||||
#expect(collection.description == testCase.expectedDescriptions.joined(separator: "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Warning-only failure fixture returns warnings in non-strict mode")
|
||||
func test_warningOnlyFailureFixtureReturnsWarningsInNonStrictMode() throws {
|
||||
let testCase = try #require(validationFailureFixtures.first { $0.name == "warning-only validation output" })
|
||||
let value = try testCase.loadValue()
|
||||
let warnings = try testCase.validator.validate(value, strict: false)
|
||||
|
||||
#expect(warnings == testCase.expectedIssues)
|
||||
#expect(warnings.map(\.description) == testCase.expectedDescriptions)
|
||||
#expect(warnings.allSatisfy { $0.severity == .warning })
|
||||
#expect(!warnings.contains(.init(
|
||||
severity: .warning,
|
||||
reason: "Forbidden key 'deprecated' is present",
|
||||
path: .init([.key("title")]),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
let validationFailureFixtures: [ValidationFailureFixture] = [
|
||||
.init(
|
||||
name: "duplicate key validation output",
|
||||
fileName: "duplicate-keys",
|
||||
validator: .init(),
|
||||
expectedIssues: [
|
||||
.init(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key 'title'",
|
||||
path: .init([.key("title")]),
|
||||
),
|
||||
.init(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key '/users'",
|
||||
path: .init([.key("paths"), .key("/users")]),
|
||||
),
|
||||
],
|
||||
expectedDescriptions: [
|
||||
"error: Duplicate mapping key 'title' at $.title",
|
||||
"error: Duplicate mapping key '/users' at $.paths[\"/users\"]",
|
||||
],
|
||||
),
|
||||
.init(
|
||||
name: "required and forbidden validation output",
|
||||
fileName: "missing-required-and-forbidden",
|
||||
validator: .blank
|
||||
.validating(requiredRootStringKeyRule("title"))
|
||||
.validating(forbiddenMappingKeyRule("draft", severity: .error)),
|
||||
expectedIssues: [
|
||||
.init(
|
||||
severity: .error,
|
||||
reason: "Required string key 'title' is missing",
|
||||
path: .init([.key("title")]),
|
||||
),
|
||||
.init(
|
||||
severity: .error,
|
||||
reason: "Forbidden key 'draft' is present",
|
||||
path: .init([.key("metadata"), .key("draft")]),
|
||||
),
|
||||
],
|
||||
expectedDescriptions: [
|
||||
"error: Required string key 'title' is missing at $.title",
|
||||
"error: Forbidden key 'draft' is present at $.metadata.draft",
|
||||
],
|
||||
),
|
||||
.init(
|
||||
name: "warning-only validation output",
|
||||
fileName: "warnings",
|
||||
validator: .blank.validating(forbiddenMappingKeyRule("deprecated", severity: .warning)),
|
||||
expectedIssues: [
|
||||
.init(
|
||||
severity: .warning,
|
||||
reason: "Forbidden key 'deprecated' is present",
|
||||
path: .init([.key("deprecated")]),
|
||||
),
|
||||
.init(
|
||||
severity: .warning,
|
||||
reason: "Forbidden key 'deprecated' is present",
|
||||
path: .init([.key("nested"), .key("deprecated")]),
|
||||
),
|
||||
],
|
||||
expectedDescriptions: [
|
||||
"warning: Forbidden key 'deprecated' is present at $.deprecated",
|
||||
"warning: Forbidden key 'deprecated' is present at $.nested.deprecated",
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
struct ValidationFailureFixture: CustomStringConvertible {
|
||||
var name: String
|
||||
var fileName: String
|
||||
var validator: PureYAML.Validation.Validator
|
||||
var expectedIssues: [PureYAML.Validation.Issue]
|
||||
var expectedDescriptions: [String]
|
||||
|
||||
var description: String {
|
||||
name
|
||||
}
|
||||
|
||||
func loadValue() throws -> PureYAML.Model.Value {
|
||||
let url = try #require(Bundle.module.url(
|
||||
forResource: fileName,
|
||||
withExtension: "yaml",
|
||||
subdirectory: "Fixtures/validation-failures",
|
||||
))
|
||||
let yaml = try String(contentsOf: url, encoding: .utf8)
|
||||
return try PureYAML.parse(yaml)
|
||||
}
|
||||
}
|
||||
|
||||
private extension PureYAML.Validation.Result {
|
||||
var descriptionLines: [String] {
|
||||
issues.map(\.description)
|
||||
}
|
||||
}
|
||||
@ -22,6 +22,24 @@ validation rule must make these parts explicit:
|
||||
- **Default status**: state whether the rule belongs in
|
||||
`Validation.Validator.defaultRules` or must be opt-in.
|
||||
|
||||
## Validation Document Rule
|
||||
|
||||
User-facing tools must offer a validation document/report instead of exposing
|
||||
only a raw crash, trap, stack trace, or unstructured thrown error. Library APIs
|
||||
may throw typed errors, but CLIs, batch tools, and corpus runners must be able
|
||||
to write or print a readable validation document that includes:
|
||||
|
||||
- input URL or file path;
|
||||
- failure category;
|
||||
- line and column when available;
|
||||
- failing reference or path when applicable;
|
||||
- original error text;
|
||||
- whether processing continued or stopped.
|
||||
|
||||
Input-level failures must be data. A tool may return a non-zero exit code after
|
||||
reporting failures, but it must not silently drop them or terminate before the
|
||||
validation document can be produced.
|
||||
|
||||
## Test Contract
|
||||
|
||||
Every validator change must include Swift Testing coverage for:
|
||||
|
||||
@ -66,12 +66,18 @@ source_singletons=$(
|
||||
)
|
||||
report_matches "Sources must not introduce singleton or service-locator access patterns" "$source_singletons"
|
||||
|
||||
source_traps=$(
|
||||
grep -RInE '\bfatalError[[:space:]]*\(|\bpreconditionFailure[[:space:]]*\(|\bassertionFailure[[:space:]]*\(|\btry!|\bas!|[A-Za-z0-9_)\]}][[:space:]]*![[:space:]]*([).,\];:]|$)' Sources --include '*.swift' 2>/dev/null || true
|
||||
)
|
||||
report_matches "Sources must not use traps, forced casts, try!, or force unwraps" "$source_traps"
|
||||
|
||||
test_imports=$(
|
||||
grep -RInE '^[[:space:]]*(@testable[[:space:]]+)?import[[:space:]]+[A-Za-z_][A-Za-z0-9_]*' Tests --include '*.swift' 2>/dev/null \
|
||||
| grep -vE '^[^:]+:[0-9]+:[[:space:]]*@testable[[:space:]]+import[[:space:]]+PureYAML[[:space:]]*$' \
|
||||
| grep -vE '^[^:]+:[0-9]+:[[:space:]]*import[[:space:]]+Foundation[[:space:]]*$' \
|
||||
| grep -vE '^[^:]+:[0-9]+:[[:space:]]*import[[:space:]]+Testing[[:space:]]*$' || true
|
||||
)
|
||||
report_matches "Tests may import only Testing and @testable PureYAML" "$test_imports"
|
||||
report_matches "Tests may import only Foundation, Testing, and @testable PureYAML" "$test_imports"
|
||||
|
||||
test_xctest=$(
|
||||
grep -RInE '\bXCTest(Case|Expectation)?\b|\bXCTAssert[A-Za-z]*\b|^[[:space:]]*import[[:space:]]+XCTest[[:space:]]*$' Tests --include '*.swift' 2>/dev/null || true
|
||||
|
||||
Loading…
Reference in New Issue
Block a user