├── test_data ├── noseparator ├── values.yaml ├── values.yml ├── invalid.yaml ├── values.json ├── values.txt ├── invalid.json └── schema.json ├── renovate.json ├── .gitignore ├── .github └── workflows │ ├── semrelease.yaml │ ├── test-check-go.yaml │ ├── gorelease.yaml │ ├── scorecards.yml │ └── codeql-analysis.yml ├── Makefile ├── go.mod ├── LICENSE ├── .goreleaser.yaml ├── README.md ├── schemacheck_test.go ├── go.sum └── schemacheck.go /test_data/noseparator: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_data/values.yaml: -------------------------------------------------------------------------------- 1 | key1: SithLord1 2 | key2: 3 | name: Vader4Ever 4 | id: 1 5 | coolOrNot: true 6 | key3: 100 7 | -------------------------------------------------------------------------------- /test_data/values.yml: -------------------------------------------------------------------------------- 1 | key1: SithLord1 2 | key2: 3 | name: Vader4Ever 4 | id: 1 5 | coolOrNot: true 6 | key3: 100 7 | -------------------------------------------------------------------------------- /test_data/invalid.yaml: -------------------------------------------------------------------------------- 1 | key1: true 2 | key2: 3 | name: Vader4Ever 4 | id: Vaderrrrrrrr 5 | coolOrNot: true 6 | key3: 100 7 | -------------------------------------------------------------------------------- /test_data/values.json: -------------------------------------------------------------------------------- 1 | { 2 | "key1": "SithLord1", 3 | "key2": { 4 | "name": "Vader4Ever", 5 | "id": 1, 6 | "coolOrNot": true 7 | }, 8 | "key3": 100 9 | } 10 | -------------------------------------------------------------------------------- /test_data/values.txt: -------------------------------------------------------------------------------- 1 | { 2 | "key1": "SithLord1", 3 | "key2": { 4 | "name": "Vader4Ever", 5 | "id": 1, 6 | "coolOrNot": true 7 | }, 8 | "key3": 100 9 | } 10 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "rangeStrategy": "pin", 7 | "automerge": true 8 | } 9 | -------------------------------------------------------------------------------- /test_data/invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "key1": 1091, 3 | "key2": { 4 | "name": "Vader4Ever", 5 | "id": "InvalidVader", 6 | "coolOrNot": true 7 | }, 8 | "key3": 100 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General folders 2 | .vscode/ 3 | dist/ 4 | 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | dist/ 22 | -------------------------------------------------------------------------------- /.github/workflows/semrelease.yaml: -------------------------------------------------------------------------------- 1 | # This workflow runs conventional commit checker, semantic release, and goreleaser. 2 | 3 | name: Semantic Release 4 | 5 | on: 6 | push: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | test-check-go: 12 | uses: ./.github/workflows/test-check-go.yaml 13 | 14 | semrelease: 15 | 16 | name: Sem Release 17 | runs-on: ubuntu-latest 18 | needs: test-check-go 19 | 20 | steps: 21 | - name: Checkout source 22 | uses: actions/checkout@v4 23 | 24 | - uses: go-semantic-release/action@v1 25 | with: 26 | github-token: ${{ secrets.SCHEMACHECK_RELEASE_TOKEN }} 27 | changelog-generator-opt: "emojis=true" 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | INSTALL_PATH ?= /usr/local/bin 3 | BIN_NAME ?= schemacheck 4 | BINDIR := $(CURDIR)/bin 5 | 6 | default: build 7 | 8 | .PHONY: tidy 9 | tidy: 10 | @go mod tidy 11 | 12 | .PHONY: build 13 | build: 14 | @goreleaser build \ 15 | --clean \ 16 | --skip=validate \ 17 | --single-target \ 18 | --output dist/$(BIN_NAME) 19 | 20 | .PHONY: install 21 | install: build 22 | @install dist/$(BIN_NAME) $(INSTALL_PATH)/$(BIN_NAME) 23 | @schemacheck --version 24 | 25 | .PHONY: release 26 | release: 27 | @goreleaser build --clean 28 | 29 | .PHONY: test 30 | test: 31 | @go test -v 32 | 33 | .PHONY: checks 34 | checks: 35 | @go fmt ./... 36 | @go vet ./... 37 | @staticcheck ./... 38 | @gosec ./... 39 | @goimports -w ./ 40 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/adrielp/schemacheck 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/fatih/color v1.18.0 7 | github.com/spf13/pflag v1.0.5 8 | github.com/stretchr/testify v1.9.0 9 | github.com/xeipuuv/gojsonschema v1.2.0 10 | sigs.k8s.io/yaml v1.4.0 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/mattn/go-colorable v0.1.13 // indirect 16 | github.com/mattn/go-isatty v0.0.20 // indirect 17 | github.com/pmezard/go-difflib v1.0.0 // indirect 18 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 19 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 20 | golang.org/x/sys v0.25.0 // indirect 21 | gopkg.in/yaml.v3 v3.0.1 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /.github/workflows/test-check-go.yaml: -------------------------------------------------------------------------------- 1 | # This workflow runs tests and checks via the makefile against the project 2 | 3 | name: Make Test and Checks 4 | 5 | on: 6 | pull_request: 7 | branches: [ main ] 8 | workflow_call: 9 | 10 | jobs: 11 | 12 | test: 13 | 14 | name: Test and Check 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout source 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: 1.21 25 | 26 | - name: Install Dependencies 27 | run: | 28 | go install honnef.co/go/tools/cmd/staticcheck@latest 29 | go install github.com/securego/gosec/v2/cmd/gosec@latest 30 | go install golang.org/x/tools/cmd/goimports@latest 31 | 32 | - name: Make Test 33 | run: | 34 | make test 35 | make checks 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /test_data/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://example.com/product.schema.json", 4 | "title": "Environment", 5 | "description": "A JSON File.", 6 | "type": "object", 7 | "properties": { 8 | "key1": { 9 | "description": "Super cool key name", 10 | "type": "string" 11 | }, 12 | "key2": { 13 | "type": "object", 14 | "properties": { 15 | "name": { 16 | "description": "Super cool name", 17 | "type": "string" 18 | }, 19 | "id": { 20 | "description": "Super cool ID number", 21 | "type": "integer" 22 | }, 23 | "coolOrNot": { 24 | "description": "Whether or not we're cool", 25 | "type": "boolean" 26 | } 27 | } 28 | }, 29 | "key3": { 30 | "description": "Coolness factor", 31 | "type": "integer" 32 | } 33 | }, 34 | "required": ["key1", "key2"] 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Adriel Perkins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/gorelease.yaml: -------------------------------------------------------------------------------- 1 | name: Go Releaser 2 | 3 | on: 4 | push: 5 | # run only against tags 6 | tags: 7 | - "*" 8 | 9 | jobs: 10 | goreleaser: 11 | permissions: 12 | id-token: write 13 | contents: read 14 | attestations: write 15 | packages: write 16 | issues: write 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: Fetch all tags 25 | run: git fetch --force --tags 26 | 27 | - name: Set up Go 28 | uses: actions/setup-go@v5 29 | with: 30 | go-version: 1.21 31 | 32 | - name: Run GoReleaser 33 | uses: goreleaser/goreleaser-action@v6 34 | with: 35 | distribution: goreleaser 36 | version: latest 37 | args: release --clean 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.SCHEMACHECK_RELEASE_TOKEN }} 40 | 41 | - name: Attest Provinance 42 | uses: actions/attest-build-provenance@v2 43 | with: 44 | subject-path: "./dist/schemacheck**" 45 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | 4 | # The lines below are called `modelines`. See `:help modeline` 5 | # Feel free to remove those if you don't want/need to use them. 6 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 7 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 8 | 9 | version: 1 10 | 11 | before: 12 | hooks: 13 | # You may remove this if you don't use go modules. 14 | - go mod tidy 15 | # you may remove this if you don't need go generate 16 | - go generate ./... 17 | 18 | builds: 19 | - env: 20 | - CGO_ENABLED=0 21 | goos: 22 | - linux 23 | - windows 24 | - darwin 25 | goarch: 26 | - amd64 27 | - arm64 28 | - arm 29 | - 386 30 | 31 | archives: 32 | - format: tar.gz 33 | # this name template makes the OS and Arch compatible with the results of `uname`. 34 | name_template: >- 35 | {{ .ProjectName }}_ 36 | {{- title .Os }}_ 37 | {{- if eq .Arch "amd64" }}x86_64 38 | {{- else if eq .Arch "386" }}i386 39 | {{- else }}{{ .Arch }}{{ end }} 40 | {{- if .Arm }}v{{ .Arm }}{{ end }} 41 | # use zip for windows archives 42 | format_overrides: 43 | - goos: windows 44 | format: zip 45 | 46 | changelog: 47 | sort: asc 48 | filters: 49 | exclude: 50 | - "^docs:" 51 | - "^test:" 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/adrielp/schemacheck/badge)](https://api.securityscorecards.dev/projects/github.com/adrielp/schemacheck) 2 | ![CodeQL](https://github.com/adrielp/schemacheck/workflows/CodeQL/badge.svg?branch=main) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/adrielp/schemacheck)](https://goreportcard.com/report/github.com/adrielp/schemacheck) 4 | ![Tests Passing](https://github.com/adrielp/schemacheck/workflows/Make%20Test%20and%20Checks/badge.svg) 5 | 6 | # schemacheck 7 | A CLI utility written in [go](go.dev) that validates `json` and `yaml` files 8 | against a `schema`. 9 | 10 | ## Usage 11 | `schemacheck` is meant to be used against one schema and one or more `yaml` or 12 | `json` files. 13 | 14 | After installation, you can run it like: 15 | ``` 16 | schemacheck --schema myschema.json --file myjson.json --file myyaml.yaml ....... 17 | ``` 18 | 19 | You can get the usage at any time by running: 20 | ``` 21 | schemacheck --help 22 | ``` 23 | 24 | You can also call this CLI from other command line utililties like `find`. 25 | ``` 26 | find . -type f -name "*.json" -exec ./dist/bin/schemacheck -s test_data/schema.json -f {} \+ 27 | ``` 28 | 29 | ## Install 30 | There are a couple different methods to install `schemacheck`. 31 | 32 | ### Preferred methods 33 | * Via `go` (recommended): `go install github.com/adrielp/schemacheck` 34 | * Via `brew`: `brew install adrielp/tap/schemacheck` (Mac / Linux) 35 | 36 | 37 | ### Mac/Linux during local development 38 | * Clone down this repository and run `make install` 39 | 40 | ### Windows 41 | There's a binary for that, but it's not directly supported or tested because `#windows` 42 | 43 | ## Getting Started 44 | ### Prereqs 45 | * Have [make](https://www.gnu.org/software/make/) installed 46 | * Have [GoReleaser](https://goreleaser.com/) installed 47 | 48 | ### Instructions 49 | * Clone down this repository 50 | * Run commands in the [Makefile](./Makefile) like `make build` 51 | -------------------------------------------------------------------------------- /.github/workflows/scorecards.yml: -------------------------------------------------------------------------------- 1 | name: Scorecards supply-chain security 2 | on: 3 | # Only the default branch is supported. 4 | branch_protection_rule: 5 | schedule: 6 | - cron: '27 21 * * 2' 7 | push: 8 | branches: [ "main" ] 9 | 10 | # Declare default permissions as read only. 11 | permissions: read-all 12 | 13 | jobs: 14 | analysis: 15 | name: Scorecards analysis 16 | runs-on: ubuntu-latest 17 | permissions: 18 | # Needed to upload the results to code-scanning dashboard. 19 | security-events: write 20 | # Used to receive a badge. 21 | id-token: write 22 | # Needs for private repositories. 23 | #contents: read 24 | #actions: read 25 | 26 | steps: 27 | - name: "Checkout code" 28 | uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 29 | with: 30 | persist-credentials: false 31 | 32 | - name: "Run analysis" 33 | uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 34 | with: 35 | results_file: results.sarif 36 | results_format: sarif 37 | # (Optional) Read-only PAT token. Uncomment the `repo_token` line below if: 38 | # - you want to enable the Branch-Protection check on a *public* repository, or 39 | # - you are installing Scorecards on a *private* repository 40 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 41 | repo_token: ${{ secrets.SCHEMACHECK_RELEASE_TOKEN }} 42 | 43 | # Publish the results for public repositories to enable scorecard badges. For more details, see 44 | # https://github.com/ossf/scorecard-action#publishing-results. 45 | # For private repositories, `publish_results` will automatically be set to `false`, regardless 46 | # of the value entered here. 47 | publish_results: true 48 | 49 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 50 | # format to the repository Actions tab. 51 | - name: "Upload artifact" 52 | uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 53 | with: 54 | name: SARIF file 55 | path: results.sarif 56 | retention-days: 5 57 | 58 | # Upload the results to GitHub's code scanning dashboard. 59 | - name: "Upload to code-scanning" 60 | uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13 61 | with: 62 | sarif_file: results.sarif 63 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '15 21 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v3 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v3 73 | with: 74 | category: "/language:${{matrix.language}}" 75 | -------------------------------------------------------------------------------- /schemacheck_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/xeipuuv/gojsonschema" 10 | ) 11 | 12 | // =============================================== 13 | // Tests against the CheckFileIsSupported function for various file types 14 | func TestCheckFileIsSupportedYaml(t *testing.T) { 15 | file := "test_data/values.yaml" 16 | valid, err := CheckFileIsSupported(file, "yaml") 17 | if err != nil { 18 | t.Fatalf("An error occured during validation of file that should not have occurred:\n %s", err) 19 | } 20 | assert.True(t, valid) 21 | } 22 | 23 | func TestCheckFileIsSupportedYml(t *testing.T) { 24 | file := "test_data/values.yml" 25 | valid, err := CheckFileIsSupported(file, "yml") 26 | if err != nil { 27 | t.Fatalf("An error occured during validation of file that should not have occurred:\n %s", err) 28 | } 29 | assert.True(t, valid) 30 | } 31 | 32 | func TestCheckFileIsSupportedJSON(t *testing.T) { 33 | file := "test_data/values.json" 34 | valid, err := CheckFileIsSupported(file, "json") 35 | if err != nil { 36 | t.Fatalf("An error occured during validation of file that should not have occurred:\n %s", err) 37 | } 38 | assert.True(t, valid) 39 | } 40 | 41 | func TestCheckFileIsSupportedTxt(t *testing.T) { 42 | file := "test_data/values.txt" 43 | _, err := CheckFileIsSupported(file, "txt") 44 | assert.Error(t, err) 45 | } 46 | 47 | // =============================================== 48 | // Tests GetFileExt function for various filetypes 49 | func TestGetFileExtYaml(t *testing.T) { 50 | file := "test_data/values.yaml" 51 | fileExt, _ := GetFileExt(file) 52 | assert.Equal(t, "yaml", fileExt) 53 | } 54 | 55 | func TestGetFileExtYml(t *testing.T) { 56 | file := "test_data/values.yml" 57 | fileExt, _ := GetFileExt(file) 58 | assert.Equal(t, "yml", fileExt) 59 | } 60 | 61 | func TestGetFileExtJSON(t *testing.T) { 62 | file := "test_data/values.json" 63 | fileExt, _ := GetFileExt(file) 64 | assert.Equal(t, "json", fileExt) 65 | } 66 | 67 | func TestGetFileExtNoSeparator(t *testing.T) { 68 | file := "test_data/noseparator" 69 | _, err := GetFileExt(file) 70 | assert.Error(t, err) 71 | } 72 | 73 | // =============================================== 74 | // Tests Validate against test data files 75 | func TestValidateValidYaml(t *testing.T) { 76 | file := "test_data/values.yaml" 77 | fileExt := "yaml" 78 | schema, err := os.ReadFile(filepath.Clean("test_data/schema.json")) 79 | if err != nil { 80 | errLogger.Panicf("Could not read schema file: '%s' cleanly.", Schema) 81 | } 82 | loadedSchema := gojsonschema.NewBytesLoader(schema) 83 | assert.NoError(t, Validate(file, fileExt, loadedSchema)) 84 | } 85 | 86 | func TestValidateValidJSON(t *testing.T) { 87 | file := "test_data/values.json" 88 | fileExt := "yaml" 89 | schema, err := os.ReadFile(filepath.Clean("test_data/schema.json")) 90 | if err != nil { 91 | errLogger.Panicf("Could not read schema file: '%s' cleanly.", Schema) 92 | } 93 | loadedSchema := gojsonschema.NewBytesLoader(schema) 94 | assert.NoError(t, Validate(file, fileExt, loadedSchema)) 95 | } 96 | 97 | func TestValidateInvalidYaml(t *testing.T) { 98 | file := "test_data/invalid.yaml" 99 | fileExt := "yaml" 100 | schema, err := os.ReadFile(filepath.Clean("test_data/schema.json")) 101 | if err != nil { 102 | errLogger.Panicf("Could not read schema file: '%s' cleanly.", Schema) 103 | } 104 | loadedSchema := gojsonschema.NewBytesLoader(schema) 105 | assert.Error(t, Validate(file, fileExt, loadedSchema)) 106 | } 107 | 108 | func TestValidateInvalidJSON(t *testing.T) { 109 | file := "test_data/invalid.json" 110 | fileExt := "yaml" 111 | schema, err := os.ReadFile(filepath.Clean("test_data/schema.json")) 112 | if err != nil { 113 | errLogger.Panicf("Could not read schema file: '%s' cleanly.", Schema) 114 | } 115 | loadedSchema := gojsonschema.NewBytesLoader(schema) 116 | assert.Error(t, Validate(file, fileExt, loadedSchema)) 117 | } 118 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 5 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 6 | github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= 7 | github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= 8 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 9 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 10 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 11 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 12 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 13 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 14 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 15 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 16 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 20 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 21 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 22 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 23 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 24 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 25 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 26 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 27 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 28 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= 29 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 30 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 31 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 32 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= 33 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 34 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 35 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= 37 | golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 38 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 39 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 40 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 41 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 42 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 43 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 44 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 45 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 46 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 47 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 48 | -------------------------------------------------------------------------------- /schemacheck.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/fatih/color" 12 | flag "github.com/spf13/pflag" 13 | "github.com/xeipuuv/gojsonschema" 14 | "sigs.k8s.io/yaml" 15 | ) 16 | 17 | // Set default constants for flag usage messages. 18 | const ( 19 | schemaUsage = "A valid JSON schema file to use for validation. Default: schema.json" 20 | fileUsage = "A Yaml or JSON file to check against a given schema. Default: values.json (can acceptable multiples)" 21 | versionUsage = "Prints out the version of schemacheck" 22 | ignoreValidationErrUsage = "Ignores when a document is not valid but provides a warning." 23 | noColorUsage = "Disables color usage for the logger" 24 | ) 25 | 26 | // Core variables for flag pointers and info, warning, and error loggers. 27 | var ( 28 | // Core flag variables 29 | File []string 30 | Schema string 31 | IgnoreValidationErr bool 32 | NoColor bool 33 | VersionFlag bool 34 | 35 | // version is set through ldflags by GoReleaser upon build, taking in the most recent tag 36 | // and appending -snapshot in the event that --snapshot is set in GoReleaser. 37 | version string 38 | 39 | // Info, warning, and error loggers. 40 | logger = log.New(os.Stderr, "INFO: ", log.Lshortfile) 41 | warnLogger = log.New(os.Stderr, color.HiYellowString("WARN: "), log.Lshortfile) 42 | errLogger = log.New(os.Stderr, color.HiRedString("ERROR: "), log.Lshortfile) 43 | ) 44 | 45 | // Initialize the flags from the command line and their shorthand counterparts. 46 | func init() { 47 | flag.StringVarP(&Schema, "schema", "s", "", schemaUsage) 48 | flag.StringSliceVarP(&File, "file", "f", []string{}, fileUsage) 49 | flag.BoolVar(&IgnoreValidationErr, "ignore-val-err", false, ignoreValidationErrUsage) 50 | flag.BoolVar(&NoColor, "no-color", false, noColorUsage) 51 | flag.BoolVarP(&VersionFlag, "version", "v", false, versionUsage) 52 | } 53 | 54 | // Check whether or not a required flag like file and schema is set and return true or false. 55 | func CheckForEmptyArg() bool { 56 | schemaArgEmpty := true 57 | fileArgEmpty := true 58 | flag.VisitAll(func(f *flag.Flag) { 59 | if f.Name == "schema" { 60 | if f.Changed { 61 | schemaArgEmpty = false 62 | } 63 | } else if f.Name == "file" { 64 | if f.Changed { 65 | fileArgEmpty = false 66 | } 67 | } 68 | }) 69 | if schemaArgEmpty || fileArgEmpty { 70 | return true 71 | } 72 | return false 73 | } 74 | 75 | // Checks whether a given file is of the supported extension type and if not 76 | // returns false with an error. 77 | // Valid file extensions are currently .yaml, .yml, and .json 78 | func CheckFileIsSupported(file string, fileExt string) (bool, error) { 79 | // default to false 80 | fileValid := false 81 | 82 | // supported file extensions to check 83 | supportedTypes := []string{"yaml", "yml", "json"} 84 | 85 | for _, ext := range supportedTypes { 86 | if strings.HasSuffix(file, ext) { 87 | logger.Printf("File: \"%s\" has valid file extension: \"%s\"", file, ext) 88 | fileValid = true 89 | } 90 | } 91 | 92 | if !fileValid { 93 | return fileValid, errors.New("file type not supported") 94 | } 95 | 96 | return fileValid, nil 97 | 98 | } 99 | 100 | func GetFileExt(file string) (string, error) { 101 | _, fileExt, found := strings.Cut(file, ".") 102 | if !found { 103 | return "", errors.New("file separator not found") 104 | } 105 | 106 | return fileExt, nil 107 | } 108 | 109 | func Validate(file string, fileExt string, loadedSchema gojsonschema.JSONLoader) error { 110 | data, err := os.ReadFile(filepath.Clean(file)) 111 | if err != nil { 112 | errLogger.Fatalf("Could not read file: '%s' cleanly.", file) 113 | } 114 | 115 | if fileExt == "yaml" || fileExt == "yml" { 116 | data, err = yaml.YAMLToJSON(data) 117 | if err != nil { 118 | logger.Fatalf("Failed to convert yaml to json in yaml file %s", file) 119 | } 120 | } 121 | 122 | documentLoader := gojsonschema.NewBytesLoader(data) 123 | 124 | // Validate the JSON data against the loaded JSON Schema 125 | result, err := gojsonschema.Validate(loadedSchema, documentLoader) 126 | if err != nil { 127 | errLogger.Printf("There was a problem validating %s", file) 128 | logger.Fatalf(err.Error()) 129 | } 130 | 131 | // Check the validity of the result and throw a message is the document is valid or if it's not with errors. 132 | if result.Valid() { 133 | logger.Printf("%s is a valid document.\n", file) 134 | } else { 135 | logger.Printf("%s is not a valid document...\n", file) 136 | for _, desc := range result.Errors() { 137 | errLogger.Printf("--- %s\n", desc) 138 | } 139 | return errors.New("document not valid") 140 | } 141 | 142 | return nil 143 | } 144 | 145 | func main() { 146 | 147 | // parse the flags set in the init() function 148 | flag.Parse() 149 | 150 | // If version flag is set, output version of app and exit 151 | if VersionFlag { 152 | fmt.Printf("schemacheck version: %s\n", version) 153 | os.Exit(0) 154 | } 155 | 156 | // set first to false for CI based on fatih/color docs 157 | color.NoColor = false 158 | // set nocolor to true, and reset err and warn loggers because nocolor is not respected by logger definitions when set 159 | // at var level 160 | if NoColor { 161 | color.NoColor = true 162 | warnLogger = log.New(os.Stderr, "WARN: ", log.Lshortfile) 163 | errLogger = log.New(os.Stderr, "ERROR: ", log.Lshortfile) 164 | } 165 | 166 | // Check to ensure required flags aren't empty 167 | missingArgs := CheckForEmptyArg() 168 | if missingArgs { 169 | fmt.Fprintf(os.Stderr, "Usage of schemacheck\n") 170 | flag.PrintDefaults() 171 | errLogger.Fatal("One or more missing args not set.") 172 | } 173 | 174 | // Load schema file before running through and validating the other files to 175 | // reduce how many times it's loaded. 176 | schema, err := os.ReadFile(filepath.Clean(Schema)) 177 | if err != nil { 178 | errLogger.Fatalf("Could not read schema file: '%s' cleanly.", Schema) 179 | } 180 | loadedSchema := gojsonschema.NewBytesLoader(schema) 181 | 182 | // Iterate through the files declared in the arguments and run validations 183 | for _, file := range File { 184 | // Create a specific logger with an ERROR message for easy readability. 185 | 186 | // Print out the values passed on the command line 187 | logger.Printf("Validating %s file against %s schema...", file, Schema) 188 | 189 | // Get the file extension and error if it failed 190 | fileExt, err := GetFileExt(file) 191 | if err != nil { 192 | errLogger.Fatalf(err.Error()) 193 | } 194 | 195 | // Pass the file name and extension to ensure it's a supported file type 196 | if _, err := CheckFileIsSupported(file, fileExt); err != nil { 197 | errLogger.Fatal(err.Error()) 198 | } 199 | 200 | // Validate against the schema and if IgnoreValidationErr is set, exit with a warning. 201 | if err := Validate(file, fileExt, loadedSchema); err != nil { 202 | if IgnoreValidationErr { 203 | warnLogger.Printf("Ignoring validation error.") 204 | os.Exit(0) 205 | } 206 | errLogger.Fatal(err.Error()) 207 | } 208 | } 209 | } 210 | --------------------------------------------------------------------------------