Command Palette

Search for a command to run...

YAML 유효성 검사기: 유효한 YAML이 여전히 잘못된 것을 배포하는 이유

YAML 유효성 검사기: 유효한 YAML이 여전히 잘못된 것을 배포하는 이유

T
Toolz Team
|Jul 5, 2026|19 최소 읽기

코딩 모음의 일부

여기에 물린 행동은 우연이 아니라 지정됩니다: the YAML 1.2 사양 인용되지 않은 스칼라가 유형으로 해결되는 방법을 정의합니다.

나는 한 번 버전에 서비스를 고정 배포를 발송 1.1 구성 파일이 명확하게 말했을 때 1.10. 오타가 아닙니다. 나쁜 찾기-및-바꾸기가 아닙니다. 내가 네 번 읽었던 YAML 파일은 이렇게 말했다:

image_tag: 1.10

그리고 파서는 내 배포 스크립트에 번호를 건네주었습니다 1.1. 왜냐하면 1.10 isn't는 YAML에 대한 버전 문자열입니다 - it's a 플로트 리터럴그리고,부동 don't 는 0 을 계속 따라갑니다. 10 은 1 점 1 이 됩니다. 파일은 100% 유효한 YAML 이었습니다. 린터는 행복했습니다. CI 는 녹색이었습니다. 잘못된 컨테이너가 나갔습니다.

That's 아무도 YAML 유효성 검사에 대해 알려주지 않는 것: "valid"는 "correct."와 같지 않습니다 예/아니오만 대답하는 구문 검사기는 쉬운 질문에 대답하는 것입니다. 어려운 질문 - 실제로 배포를 중단하는 질문 -은 내 YAML은 무엇으로 변했나요? YAML은 구성 형식이 아니기 때문에 it's는 구성 형식's 옷을 입은 유형 추론 엔진이며, 요청한 적이 없는 데이터에 대한 결정을 내립니다.

X-1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 10 야엘 검증인 on Toolz.dev 는 두 질문에 모두 답합니다. 문서가 구문 분석되는지 여부를 알려주고,그 다음 구문 분석된 결과를 JSON - 툴링이 받게 될 실제 데이터 구조 - 로 보여줍니다. 그 후반부는 저를 구했을 것입니다. "image_tag": 1.1 출력 창에서는 잘못 읽을 수 없습니다.

TL;DR: YAML을 붙여넣으세요 야엘 검증인 그리고 읽어보세요 JSON 출력녹색 체크표시뿐만 아니라. That's where type coercion shows itself: 1.101.1, 0123123, 인용되지 않은 값은 자동으로 숫자, 부울 또는 null이 됩니다. 브라우저에서 완전히 js-yaml(YAML 1.2)에서 실행되므로 Kubernetes 비밀 및 데이터베이스 자격 증명은 결코 머신을 떠나지 않습니다. 문자열로 유지되어야 하는 모든 결과를 JSON 구성과 비교해야 하는 경우 JSON 포맷터 그리고 JSON 차이점 거기에서 픽업.


YAML 검증자는 실제로 무엇을 확인합니까?

두 가지 다른 것, 그리고 it's는 다르게 실패하기 때문에 분리할 가치가 있습니다.

구문 유효성 검사 묻는다: 이 텍스트는 전혀 구문 분석할 수 있습니까? 공백이 속하는 탭,콜론 뒤의 누락된 공간,몸이 isn't 들여쓰기된 블록 스칼라,닫히지 않은 인용문입니다 시끄러운 실패. 파서가 던져지고 파이프라인이 빨간색으로 변하면 2 분 안에 고칠 수 있습니다. 짜증나지 위험하지 않습니다.

의미 검사 묻습니다: 무엇을 구문 분석했습니까 안으로? 조용한 실패가 사는 곳입니다. 문서는 유효합니다. 파이프 라인은 녹색입니다. 값은 단순히 당신이 썼다고 생각한 것이 아닙니다. 생산이 이상하게 행동 할 때까지 아무도 알아 내지 못하고 그때까지 nobody's 는 config 파일을보고 있습니다. config 파일은 "fine."이기 때문입니다

대부분의 온라인 YAML 검사기는 단지 첫번째 것을 한다. Toolz.dev 유효성 검사기는 첫번째를 하고 그 후에 당신에게 두번째를 건네준다 - JSON 로 렌더링되는 구문 분석된 문서는 당신의 입력 바로 옆에 그 창을 읽는 습관을 들인다. It's "the difference between "the file is well-formed"and "the file means what I mean."

실제로 빌드를 중단하는 YAML 오류는 무엇입니까?

Here's 진정으로 나타나는 것은 각각 내 삶의 비용이 얼마나 드는지에 따라 순위가 매겨집니다. 아래의 모든 행동에 대해 확인했습니다 js-yaml 4이는 Toolz.dev 유효성 검사기가 실행하고 구현하는 파서입니다 YAML 1.2 스펙.

1. 탭. 항상 탭.

YAML 은 들여쓰기를 위한 탭 문자를 금지합니다. Not "discourages" - forbids. spec 은 명시적이며,오류 메시지는 상쾌하게 직접적입니다:

tab characters must not be used in indentation

이런 일이 계속 일어나는 이유는 탭이 보이지 않기 때문입니다. 편집기는 멋지게 정렬된 파일을 보여줍니다; 파서는 제어 문자를 봅니다. 소스에서 수정하십시오: 편집기를 공백을 삽입하도록 설정하고 YAML 파일의 경우 "render 공백" 레벨당 두 개의 공백으로 모든 주요 YAML 생태계가 정한 규칙입니다.

2. 중복된 키

database:
  host: localhost
  port: 5432
  host: production-db.example.com

host 두 번 나타납니다. 어떻게 될까요? 그것은 전적으로 당신의 파서에 달려 있는데,이것은 config 형식에 대해 쓰기에는 끔찍한 문장입니다.

js-yaml 던진다: duplicated mapping key. 좋은. That's 당신이 원하는 행동,그리고 it's Toolz.dev 유효성 검사기가 당신에게 보여줄 것입니다. 그러나 PyYAML - Ansible 이 무엇이며,많은 Python 툴링이 앉아있는 - 은 조용히 걸립니다 마지막 값을 매기고 계속 진행합니다. 경고 없음. 데이터베이스 호스트는 이제 마지막 중복이 말한 것이 무엇이든지간에 병합 된 긴 파일에서 심하게 멀게 될 수도 있습니다. three hundred lines away from where you're looking.

이것은 프로덕션 툴링이 이를 수락하는 경우에도 엄격한 유효성 검사기를 통해 구성을 실행하는 데 가장 적합한 단일 인수입니다. A validator that's 더 엄격합니다 런타임은 버그를 찾는 검증기입니다.

3. 유형 강압 - 나를 얻은 것

YAML 은 인용되지 않은 스칼라로부터 타입을 추론합니다. 매우 자신감이 넘치고 종종 당신의 의도에 대해 틀립니다:

당신이 썼어요 당신은 의미했다 YAML 1.2가 당신을 제공합니다
version: 1.10 문자열 "1.10" 플로트 1.1
pin: 0123 문자열 "0123" 정수 123
port: "8080" 숫자 8080 문자열 "8080"
enabled: true 부울 부울 true - 옳은
value: 빈 문자열일까요? null
value: ~ 물결표 null

0123 행은 사람에 하나의 I & #39;d 문신입니다. 우편 번호,PIN, 계좌 번호,제로 패딩 ID - 당신이 이유에 대해 쓴 모든 선행 제로는 먹게됩니다. 그들을 인용.

단 한 번도 나를 실망시킨 적이 없는 규칙: 값이 식별자, 버전, 코드 또는 아무것도가 you'd에 산술을하지 않으면 따옴표로 넣어. 포트와 복제본 수는 그대로 유지될 수 있습니다. 모든 것은 단지 그렇습니다 보인다 숫자는 다음과 같아야 합니다 "quoted".

4. 버전 의존 부울 (일명 노르웨이 문제)

이것은 정말 악명 높으며 밈보다 세부 사항이 더 중요합니다.

에서 YAML 1.1는, 부울 유형은 받아들입니다 yes, no, on, off, y, n및 그 대문자 외에 true 그리고 false. 그래서 country: NO - 노르웨이's ISO 국가 코드 - 부울로 구문 분석합니다 거짓. 안에 YAML 1.2, 이것을 정리했습니다 true 그리고 false 부울입니다; NO 는 그냥 문자열입니다 "NO".

이는 동일한 파일을 의미합니다 다른 도구에서 다른 것을 의미합니다:

country: NO
feature_flag: on
  • js-yaml 4(YAML 1.2 및 이 검증자가 사용하는 것): {"country": "NO", "feature_flag": "on"} - 문자열.
  • PyYAML(YAML 1.1): {"country": False, "feature_flag": True} - 부울.

동일한 바이트. 다른 데이터. CI 가 Node 서비스가 소비하는 config 를 통해 Python linter 를 실행하면 파일에 대해 동의하지 않는 두 개의 파서가 있으며 둘 다 잘못되지 않습니다.

이것은 또한 GitHub Actions&#39의 기원이기도 합니다; 가장 이상한 특이한 점: the on: 모든 워크플로우가 시작하는 키는 다음과 같습니다 부울 YAML 1.1 파서로 이동하여 Python의 워크플로 파일을 연결하는 스크립트는 라는 키를 찾습니다 True 대신 on. 인용("on":) 는 합법적이며 그것을 고친다.

수비 동작은 이전과 동일합니다: 인용해 보세요. country: "NO" 수단 "NO" 지금까지 존재했던 모든 파서에.

5. 스칼라 들여쓰기 차단

description: |
This is not indented

X-1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 10 | (문자 그대로) 그리고 > (접힌) 블록 스칼라는 키를 기준으로 들여쓰기된 콘텐츠가 필요합니다. 들여쓰기되지 않은 콘텐츠는 블록을 즉시 종료하고 파서는 YAML 키로 산문을 읽기 시작하며, 이로 인해 실제 실수와는 아무런 관련이 없는 것처럼 보이는 오류 메시지가 생성됩니다.

가치 있는 아는 chomping 지표 동안 you're here: | 단일 후행 개행을 유지합니다 |- 벗겨내고, |+ 그들 모두를 유지합니다. 만약 you're embedding 개인 키 또는 스크립트와 다운 스트림 뭔가 후행 개행에 대해 불평, 이것은 당신의 손잡이입니다.

6. 인용되지 않은 특수 문자

인용되지 않은 값 안에 있는 콜론 공간은 값을 끝내고 새 키를 시작합니다. This bites in error messages and URLs:

message: Error: file not found   # parse error
regex: [a-z]+                    # parsed as a LIST, not a string
time: "22:22"                    # quote it — in YAML 1.1 this was base-60!

[, {, #, &, *, !, |, >, %, @ 스칼라의 시작 부분에서 모두 무언가를 의미합니다. 먼저 인용하고 나중에 질문하십시오.


Toolz.dev에서 YAML을 어떻게 검증합니까?

  1. 열다 야엘 검증인. 계정도 없고 업로드도 없습니다.
  2. 문서를 붙여넣으세요. Helm은 파일, 도커 구성, 워크플로우를 값으로 지정합니다. whatever's misbehaving.
  3. 유효성 검사를 누르세요. 오류는 정확히 찾아옵니다 선과 란 파서에서 parser's 자신의 이유 문자열(bad indentation of a mapping entry, duplicated mapping key등등).
  4. JSON 출력 창 읽기. 이것은 단계 사람들이 건너뛰고 it's 중요한 것 이다. 당신이 걱정 하는 가치에 대 한 스캔. 이다 image_tag 문자열인가 숫자인가? 그 포트는 인용되었는가? 빈 값이 되었는가 null?
  5. 수정, 재검증. 오류는 서로를 마스크 할 수 있습니다 - 파서는 can't 복구에서 첫 번째에서 중지, 그래서 하나를 고정하면 때로는 두 가지를 더 드러낸다. That's 정상, 아니 기호 일이 악화되고있다.

한 가지 알려진 제한 사항이 명확하게 명시되어 있습니다

유효성 검사기는 현재 a를 구문 분석합니다 단일 YAML 문서. 다중 문서 파일을 붙여넣는 경우 - 여러 개의 쿠버네티스 매니페스트가 구분되어 있습니다 --- 매우 일반적인 패턴인 하나의 파일에서 다음을 보고합니다:

expected a single document in the stream, but found more

That's 파서가 깨지는 파일이 아니라 정확합니다. 오늘의 해결 방법은 각 문서를 별도로 검증하는 것입니다: 위의 모든 것을 붙여넣습니다 ---를, 그것을 검사하고, 그 후에 다음 덩어리를 풀칠하십시오.다 문서 지원은 나의 명부에 정확하게 Kubernetes 사용자가 이것을 즉각 명중하기 때문에 이고, I'd 오히려 당신이 중간 사건을 발견하게 한다 보다는 간격에 관하여 당신에게 말한다.


YAML vs JSON: 언제 어떤 것을 사용해야 하나요?

YAML 1,2 는 JSON 의 엄격한 상위 집합입니다 - 모든 유효한 JSON 문서는 유효한 YAML 입니다,왜 검증자가 당신에게 JSON 산출을 전혀 건네줄 수 있는지 입니다. 그러나 체재에는 반대 성격이 있습니다.

JSON
에 의해 정의된 구조 들여쓰기(공백 중요) 중괄호 및 괄호 (명시적)
코멘트 예 (#) 아니요
유형 추론 공격적 - 숫자, 부울, null, 날짜를 추론합니다 없음 - 따옴표는 문자열을 의미, 항상
다중 문서 예 (--- 구분자) 아니요
재사용 앵커 (&), 별칭(*), 키 병합(<<) 없음
실패 모드 침묵의 오해 큰 소리로 구문 분석 오류
에서 최고 인간이 쓰고 편집하는 파일 데이터 머신 교환

무역은 진짜이고 그것은 두 방법 다 간다. YAML&#39;s 가독성과 코멘트는 인프라 구성이 거기에 사는 정확한 이유이다 - 아무도 코멘트가없는 JSON 에서 400 줄의 쿠버네티스의 매니페스트를 유지하고 싶어하지 않는다. JSON&#39;s 영리함의 총 부족은 정확히 API 가 그것을 사용하는 이유이다: "1.10" 이다 "1.10" 그리고 의논할 것도 없다.

내 규칙: 사람들이 편집하는 파일을 위한 YAML, 데이터 머신을 위한 JSON이 통과됩니다. 그리고 YAML 파일이 사람이 입력하지 않고 프로그램에 의해 생성되면 that&#39;s 냄새 - 기계 생성 구성은 YAML&#39;s 혜택과 모든 위험을 전혀 얻지 못합니다.

만약 당신이&#39;이 둘 사이를 이동하고 있다면, the JSON 에서 YAML 변환기 변환을 처리합니다 JSON 포맷터 반대편을 정리할 것입니다.

앵커와 별칭이란 무엇이며 이를 사용해야 합니까?

YAML 을 사용하면 블록을 한 번 정의하고 다시 사용할 수 있습니다. Anchor with &, 참조 *, 와 함께 지도에 병합합니다 <<:

defaults: &defaults
  adapter: postgres
  host: localhost
  port: 5432

development:
  <<: *defaults
  database: myapp_dev

test:
  <<: *defaults
  database: myapp_test

둘 다 development 그리고 test 어댑터,호스트, 포트가 병합된 상태로 나오세요. It&#39;s 는 정말로 유용하며,js-yaml 이 처리합니다 - 병합이 올바르게 해결되는지 확인했습니다.

하지만 두 가지 경고가 있습니다.

첫째, 병합 키는 YAML 1.1 확장입니다는, YAML 1.2 핵심의 일부가 아닙니다 지원은 널리 퍼져 있지만 보편적이지 않으며, - 사람을 잡는 것 - GitHub Actions는 이를 지원하지 않습니다. 워크플로 파일의 앵커는 원하는 작업을 수행하지 않습니다. 이것에 기대기 전에 소비자를 확인하십시오.

둘째, 앵커는 다음 사람이 파일을 읽기가 더 어려워지며 구성에서 다음 사람은 일반적으로 오전 2시에 귀하가 됩니다. 저는 이를 진정으로 반복되는 블록에 사용하고 영리함을 위해 사용하지 않습니다.

YAML 의 위험한 측면에 we&#39;re 동안: 형식은 일부 파서가 임의의 객체를 구성하는 데 사용하는 사용자 정의 태그를 지원합니다. Python&#39;s yaml.load() 이런 식으로 악용될 수 있는 것으로 유명합니다 yaml.safe_load() 존재하며 팀 외부에서 온 모든 YAML에서 항상 사용해야 하는 이유입니다. js-yaml&#39;s load() 에서 v4 는 기본적으로 안전합니다 (그것은 won&#39;t 임의의 유형을 구성), 여기서 걱정할 일이 하나 적습니다.

처음에 깨진 YAML 쓰기를 어떻게 중단합니까?

예방은 유효성 검사를 능가하며 대부분은 편집기 구성입니다:

  • 두 칸, 절대 탭이 아닙니다. 파일 형식별로 설정하여 can&#39;t forget.
  • 공백 렌더링을 켭니다 위한 .yml/.yaml니다. 탭을 볼 수 있다면 won&#39;t 탭 커밋.
  • YAML 언어 서버를 설치합니다. Kubernetes,GitHub Actions,docker-compose 스키마에 대한 실시간 스키마 유효성 검사는 구문 유효성 검사 can&#39;t: 철자가 틀린 키가 있는 유효한 YAML 인 전체 오류 클래스를 포착합니다.
  • 의심스러울 때는 기본적으로 인용하세요. 불필요한 견적의 비용은 0 입니다. 빠진 것의 비용은 배포입니다.
  • 밀어붙이기 전에 검증하세요CI가 실패한 후에는 그렇지 않습니다. 브라우저 탭에 붙여넣는 데 8초가 걸립니다; 실패한 파이프라인에는 8분이 걸립니다.
  • 쿠버네티스의 경우, 체크를 레이어화한다. 구문 검증은 구조를 포착합니다; kubectl apply --dry-run=client 스키마를 잡습니다. 그들은 다른 버그를 찾고 당신은 둘 다 원합니다.

그리고 실제로 나를 위해 일을 변경 습관: 구성 중심의 배포가 설명할 수없는 일을 할 때, 다른 것을 보기 전에 파싱된 출력을 살펴보세요. 파일이 아니라. 파싱된 출력. 파일은 당신이 의미한 것에 대한 이야기입니다. 파싱된 출력은 실제로 일어난 일입니다.

That&#39;s 내 모든 것을 지배하는 동일한 본능 API 디버깅 워크플로 - 코드가 아닌 데이터를 읽으십시오. 이는 응답과 마찬가지로 구성에도 적용됩니다. 해당 도구 상자에 있는 다른 항목에 대한 더 넓은 투어를 원한다면, 코딩 도구 가이드 덮는다.


자주 묻는 질문

YAML이 유효성을 검사하지만 여전히 배포를 중단하는 이유는 무엇입니까?

왜냐하면 구문 타당성과 의미론적 정확성은 다른 것이기 때문입니다. YAML 은 인용되지 않은 값으로부터 타입을 추론합니다,그래서 1.10 플로트가 됩니다 1.1, 0123 가 정수가 된다 123이고, 빈 값이 됩니다 null - 모두 완벽하게 유효한 문서에 있습니다. pass/fail 결과뿐만 아니라 구문 분석된 JSON 출력을 읽고 문자열을 유지해야 하는 값을 인용합니다.

YAML이 내 버전 번호를 다른 번호로 바꾸는 이유는 무엇입니까?

1.10 YAML을 문자 그대로 나타내는 플로트이며 플로트는 후행 0을 보존하지 않으므로 해결됩니다 1.1. 모든 버전,빌드 번호,또는 제로 패딩 식별자는 인용되어야 합니다: version: "1.10". 파일이 올바르게 보이고 구문 분석이 성공하기 때문에 가장 비싼 YAML 실수 중 하나입니다.

YAML의 노르웨이 문제는 무엇입니까?

YAML 1.1에서는 값이 표시됩니다 no, NO, off, 그리고 yes 는 부울이므로 노르웨이&#39;s 국가 코드 NO 다음과 같이 구문 분석합니다 false. YAML 1.2는 이 문제를 해결했습니다 true 그리고 false 는 부울입니다 - 그러나 많은 도구 (특히 Ansible 이 사용하는 PyYAML) 는 여전히 1,1 을 구현합니다. 따라서 동일한 파일은 다른 도구에서 다른 것을 의미 할 수 있습니다. 값을 인용 (country: "NO") 는 모든 곳에 문자열을 만듭니다.

YAML 에서 들여쓰기에 탭을 사용할 수 있습니까?

아니요. YAML 사양은 들여쓰기에서 탭 문자를 금지하고,파서는 &quot;tab 문자를 들여쓰기에서 사용해서는 안 됩니다.&quot; YAML 파일의 공백을 삽입하도록 편집기를 구성합니다. 레벨당 두 개의 공백이 표준 규칙입니다.

Toolz.dev YAML Validator 는 다중 문서 파일을 지원합니까?

현재는 아닙니다. 단일 문서의 유효성을 검사하므로 여러 쿠버네티가 포함된 파일은 다음과 같이 구분되어 나타납니다 --- returns &quot;stream.&quot 에서 단일 문서를 예상했습니다; 각 문서를 해결 방법으로 별도로 검증합니다. 다중 문서 지원이 계획되어 있습니다.

YAML 에서 중복 키가 허용됩니까?

사양에 따르면 매핑 키는 고유해야 하지만 파서는 실제로 동의하지 않습니다. js-yaml - 이 유효성 검사기가 사용하는 - 은 &quot;duplicated mapping key&quot; 오류를 발생시킵니다. PyYAML 은 묵묵히 마지막 값을 유지하며,이는 중복이 경고 없이 구성을 조용히 재정의할 수 있음을 의미합니다. 엄격한 유효성 검사기를 통해 구성을 실행하면 런타임이 묵묵히 이를 수락하기 전에 이를 포착합니다.

쿠버네티스 비밀과 자격 증명을 온라인으로 검증하는 것이 안전한가?

Toolz.dev 유효성 검사기를 사용하면 예 - 구문 분석은 JavaScript 를 통해 브라우저에서 완전히 발생하며 아무 것도 서버로 전송되지 않습니다. 요청이 없음을 확인하고 관찰하는 동안 browser&#39;s 네트워크 탭을 열어 직접 확인할 수 있습니다. 인프라 구성을 붙여넣기 전에 동일한 검사를 모든 온라인 도구에 적용하십시오.

.yml 과.yaml 의 차이점은 무엇입니까?

기능적인 것은 없습니다. 두 확장 모두 모든 YAML 파서에서 인식됩니다. 공식 권장 사항은 다음과 같습니다 .yaml; .yml 3 자 확장 시대부터 살아남아 극히 일반적입니다 (Docker Compose 와 GitHub Actions 모두 기본값으로 적용). 하나를 선택하고 프로젝트 내에서 일관성을 유지하세요.

YAML 을 JSON 으로 변환하려면 어떻게 해야 하나요?

YAML 을 유효성 검사기에 붙여넣고 출력 창을 읽습니다 - 구문 분석된 문서를 변환인 JSON 으로 렌더링합니다. YAML 1,2 는 JSON 의 상위 집합이기 때문에 모든 유효한 YAML 문서는 JSON 에 해당하는 것을 갖지만 유형 추론이 먼저 적용되므로 인용되지 않습니다 1.10 로 도착합니다 1.1 그리고 0123 처럼 123. 문자열로 보존해야 하는 경우 해당 값을 먼저 인용하세요.

스키마에 대해 YAML을 어떻게 검증합니까?

이 유효성 검사기는 구문을 검사하고 구문 분석된 결과를 보여 주지만 스키마에 대해서는 유효성을 검사하지 않습니다. 즉, 키와 값 유형이 Kubernetes 또는 GitHub Actions와 같은 도구가 기대하는 것과 일치하는지 확인하는 별도의 검사입니다. 스키마 유효성 검사의 경우 편집기에서 YAML 언어 서버를 사용하거나 다음과 같은 스키마 인식 CLI를 사용하십시오 kubeconform 쿠버네티스의 경우 또는 kubectl apply --dry-run=client. 구문과 스키마 유효성 검사는 서로 다른 버그를 잡아내므로 둘 다 실행합니다.


Comments

0 comments

0/2000 characters

No comments yet. Be the first to share your thoughts!