mirror of
https://github.com/yannh/kubeconform.git
synced 2026-02-12 06:29:23 +00:00
Compare commits
70 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e60892483e | ||
|
|
c7f8490e52 | ||
|
|
e65429b1e5 | ||
|
|
a23275d5ca | ||
|
|
3134f4477e | ||
|
|
9f04fec268 | ||
|
|
31e9679c96 | ||
|
|
df26febc54 | ||
|
|
1bd44986dd | ||
|
|
43a2445cb4 | ||
|
|
706cd56e87 | ||
|
|
50ce5f8ecb | ||
|
|
347cd5e4c9 | ||
|
|
142517fc45 | ||
|
|
7062384492 | ||
|
|
20805f652c | ||
|
|
9627dd185b | ||
|
|
14053aaa54 | ||
|
|
71a59d74f2 | ||
|
|
ad166c7f0d | ||
|
|
a8000fd445 | ||
|
|
b6728f181c | ||
|
|
a4d74ce7d2 | ||
|
|
808e6d4aa5 | ||
|
|
d8f00a3a30 | ||
|
|
6ae8c45bc1 | ||
|
|
b7d7b4d0dc | ||
|
|
2e50b79b16 | ||
|
|
13a78ebad8 | ||
|
|
ae67bb4709 | ||
|
|
278385f4c9 | ||
|
|
452f1fe1db | ||
|
|
f0a7d5203d | ||
|
|
71fd5f8386 | ||
|
|
c8bce62898 | ||
|
|
065fad003f | ||
|
|
c1a2c159de | ||
|
|
65cfe7e16e | ||
|
|
ce2f6de185 | ||
|
|
ad935b7e32 | ||
|
|
d038bf8840 | ||
|
|
8bc9f42f39 | ||
|
|
9294e94a8d | ||
|
|
16d52804d4 | ||
|
|
e3bb34851d | ||
|
|
aaecabe0b7 | ||
|
|
563e1db94c | ||
|
|
9860cde144 | ||
|
|
ee7c498580 | ||
|
|
84afe70659 | ||
|
|
752a33eaeb | ||
|
|
33cdbf16a4 | ||
|
|
f94844183f | ||
|
|
9d34445328 | ||
|
|
a31707ca58 | ||
|
|
46b7622a08 | ||
|
|
d8e348a597 | ||
|
|
466ec73ed7 | ||
|
|
dbcd787256 | ||
|
|
f5338b07f9 | ||
|
|
f68d6ec6ea | ||
|
|
3cb76bc5e6 | ||
|
|
5cbbd1a898 | ||
|
|
321cc0ea1d | ||
|
|
5e63bc5ad7 | ||
|
|
9a6fff13cb | ||
|
|
7bf1e01dec | ||
|
|
014cbf754f | ||
|
|
f8ab9ae49e | ||
|
|
b5f34caa70 |
243 changed files with 53545 additions and 10319 deletions
12
.github/workflows/main.yml
vendored
12
.github/workflows/main.yml
vendored
|
|
@ -5,7 +5,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: test
|
||||
run: make docker-test
|
||||
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: test
|
||||
working-directory: ./scripts
|
||||
|
|
@ -34,13 +34,17 @@ jobs:
|
|||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # https://github.com/goreleaser/goreleaser-action/issues/56
|
||||
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: goreleaser
|
||||
run: |
|
||||
echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin
|
||||
make release
|
||||
GITHUB_ACTOR=$(echo ${GITHUB_ACTOR} | tr '[:upper:]' '[:lower:]')
|
||||
GIT_OWNER=${GITHUB_ACTOR} make release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
dist/
|
||||
bin/
|
||||
.idea/
|
||||
**/*.pyc
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ builds:
|
|||
- CGO_ENABLED=0
|
||||
- GOFLAGS = -mod=vendor
|
||||
- GO111MODULE = on
|
||||
- GIT_OWNER = yannh
|
||||
goos:
|
||||
- windows
|
||||
- linux
|
||||
|
|
@ -31,23 +32,63 @@ archives:
|
|||
|
||||
dockers:
|
||||
- image_templates:
|
||||
- 'ghcr.io/yannh/kubeconform:latest'
|
||||
- 'ghcr.io/yannh/kubeconform:{{ .Tag }}'
|
||||
- 'ghcr.io/yannh/kubeconform:{{ .Tag }}-amd64'
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:{{ .Tag }}-amd64'
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:latest-amd64'
|
||||
dockerfile: Dockerfile
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--pull"
|
||||
- "--platform=linux/amd64"
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
- image_templates:
|
||||
- 'ghcr.io/yannh/kubeconform:latest-alpine'
|
||||
- 'ghcr.io/yannh/kubeconform:{{ .Tag }}-alpine'
|
||||
- 'ghcr.io/yannh/kubeconform:{{ .Tag }}-amd64-alpine'
|
||||
dockerfile: Dockerfile-alpine
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:{{ .Tag }}-arm64'
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:latest-arm64'
|
||||
dockerfile: Dockerfile
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--pull"
|
||||
- "--platform=linux/arm64"
|
||||
goos: linux
|
||||
goarch: arm64
|
||||
- image_templates:
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:{{ .Tag }}-amd64-alpine'
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:latest-amd64-alpine'
|
||||
dockerfile: Dockerfile-alpine
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--pull"
|
||||
- "--platform=linux/amd64"
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
- image_templates:
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:{{ .Tag }}-arm64-alpine'
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:latest-arm64-alpine'
|
||||
dockerfile: Dockerfile-alpine
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--pull"
|
||||
- "--platform=linux/arm64"
|
||||
goos: linux
|
||||
goarch: arm64
|
||||
|
||||
docker_manifests:
|
||||
- name_template: 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:{{ .Tag }}'
|
||||
image_templates:
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:{{ .Tag }}-amd64'
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:{{ .Tag }}-arm64'
|
||||
- name_template: 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:latest'
|
||||
image_templates:
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:latest-amd64'
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:latest-arm64'
|
||||
- name_template: 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:latest-alpine'
|
||||
image_templates:
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:latest-amd64-alpine'
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:latest-arm64-alpine'
|
||||
- name_template: 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:{{ .Tag }}-alpine'
|
||||
image_templates:
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:{{ .Tag }}-amd64-alpine'
|
||||
- 'ghcr.io/{{.Env.GIT_OWNER}}/kubeconform:{{ .Tag }}-arm64-alpine'
|
||||
|
||||
checksum:
|
||||
name_template: 'CHECKSUMS'
|
||||
|
|
|
|||
10
Dockerfile
10
Dockerfile
|
|
@ -1,8 +1,14 @@
|
|||
FROM alpine:3.14 as certs
|
||||
FROM alpine:3.21.3 as certs
|
||||
RUN apk add ca-certificates
|
||||
|
||||
FROM scratch AS kubeconform
|
||||
MAINTAINER Yann HAMON <yann@mandragor.org>
|
||||
LABEL org.opencontainers.image.authors="Yann Hamon <yann@mandragor.org>" \
|
||||
org.opencontainers.image.source="https://github.com/yannh/kubeconform/" \
|
||||
org.opencontainers.image.description="A Kubernetes manifests validation tool" \
|
||||
org.opencontainers.image.documentation="https://github.com/yannh/kubeconform/" \
|
||||
org.opencontainers.image.licenses="Apache License 2.0" \
|
||||
org.opencontainers.image.title="kubeconform" \
|
||||
org.opencontainers.image.url="https://github.com/yannh/kubeconform/"
|
||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY kubeconform /
|
||||
ENTRYPOINT ["/kubeconform"]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
FROM alpine:3.14 as certs
|
||||
MAINTAINER Yann HAMON <yann@mandragor.org>
|
||||
FROM alpine:3.20.2
|
||||
|
||||
LABEL org.opencontainers.image.authors="Yann Hamon <yann@mandragor.org>" \
|
||||
org.opencontainers.image.source="https://github.com/yannh/kubeconform/" \
|
||||
org.opencontainers.image.description="A Kubernetes manifests validation tool" \
|
||||
org.opencontainers.image.documentation="https://github.com/yannh/kubeconform/" \
|
||||
org.opencontainers.image.licenses="Apache License 2.0" \
|
||||
org.opencontainers.image.title="kubeconform" \
|
||||
org.opencontainers.image.url="https://github.com/yannh/kubeconform/"
|
||||
RUN apk add ca-certificates
|
||||
COPY kubeconform /
|
||||
ENTRYPOINT ["/kubeconform"]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
FROM bats/bats:v1.2.1
|
||||
RUN apk --no-cache add ca-certificates parallel
|
||||
COPY dist/kubeconform_linux_amd64/kubeconform /code/bin/
|
||||
FROM bats/bats:1.11.0
|
||||
RUN apk --no-cache add ca-certificates parallel libxml2-utils
|
||||
COPY bin/kubeconform /code/bin/
|
||||
COPY acceptance.bats acceptance-nonetwork.bats /code/
|
||||
COPY fixtures /code/fixtures
|
||||
|
|
|
|||
193
LICENSE
193
LICENSE
|
|
@ -1,12 +1,199 @@
|
|||
Kubeconform - Validate Kubernetes configuration files
|
||||
|
||||
Copyright (C) 2020 Yann Hamon
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2017-2022 Yann Hamon
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
|
|
|||
18
Makefile
18
Makefile
|
|
@ -5,9 +5,10 @@ RELEASE_VERSION ?= latest
|
|||
.PHONY: local-test local-build local-build-static docker-test docker-build docker-build-static build-bats docker-acceptance release update-deps build-single-target
|
||||
|
||||
local-test:
|
||||
go test -race ./...
|
||||
go test -race ./... -count=1
|
||||
|
||||
local-build:
|
||||
git config --global --add safe.directory $$PWD
|
||||
go build -o bin/ ./...
|
||||
|
||||
local-build-static:
|
||||
|
|
@ -15,13 +16,13 @@ local-build-static:
|
|||
|
||||
# These only used for development. Release artifacts and docker images are produced by goreleaser.
|
||||
docker-test:
|
||||
docker run -t -v $$PWD:/go/src/github.com/yannh/kubeconform -w /go/src/github.com/yannh/kubeconform golang:1.17 make local-test
|
||||
docker run -t -v $$PWD:/go/src/github.com/yannh/kubeconform -w /go/src/github.com/yannh/kubeconform golang:1.24.3 make local-test
|
||||
|
||||
docker-build:
|
||||
docker run -t -v $$PWD:/go/src/github.com/yannh/kubeconform -w /go/src/github.com/yannh/kubeconform golang:1.17 make local-build
|
||||
docker run -t -v $$PWD:/go/src/github.com/yannh/kubeconform -w /go/src/github.com/yannh/kubeconform golang:1.24.3 make local-build
|
||||
|
||||
docker-build-static:
|
||||
docker run -t -v $$PWD:/go/src/github.com/yannh/kubeconform -w /go/src/github.com/yannh/kubeconform golang:1.17 make local-build-static
|
||||
docker run -t -v $$PWD:/go/src/github.com/yannh/kubeconform -w /go/src/github.com/yannh/kubeconform golang:1.24.3 make local-build-static
|
||||
|
||||
build-bats:
|
||||
docker build -t bats -f Dockerfile.bats .
|
||||
|
|
@ -31,12 +32,15 @@ docker-acceptance: build-bats
|
|||
docker run --network none -t bats -p acceptance-nonetwork.bats
|
||||
|
||||
goreleaser-build-static:
|
||||
docker run -t -e GOOS=linux -e GOARCH=amd64 -v $$PWD:/go/src/github.com/yannh/kubeconform -w /go/src/github.com/yannh/kubeconform goreleaser/goreleaser:v0.176.0 build --single-target --skip-post-hooks --rm-dist --snapshot
|
||||
cp dist/kubeconform_linux_amd64/kubeconform bin/
|
||||
docker run -t -e GOOS=linux -e GOARCH=amd64 -v $$PWD:/go/src/github.com/yannh/kubeconform -w /go/src/github.com/yannh/kubeconform goreleaser/goreleaser:v2.9.0 build --clean --single-target --snapshot
|
||||
cp dist/kubeconform_linux_amd64_v1/kubeconform bin/
|
||||
|
||||
release:
|
||||
docker run -e GITHUB_TOKEN -t -v /var/run/docker.sock:/var/run/docker.sock -v $$PWD:/go/src/github.com/yannh/kubeconform -w /go/src/github.com/yannh/kubeconform goreleaser/goreleaser:v0.176.0 release --rm-dist
|
||||
docker run -e GITHUB_TOKEN -e GIT_OWNER -t -v /var/run/docker.sock:/var/run/docker.sock -v $$PWD:/go/src/github.com/yannh/kubeconform -w /go/src/github.com/yannh/kubeconform goreleaser/goreleaser:v2.9.0 release --clean
|
||||
|
||||
update-deps:
|
||||
go get -u ./...
|
||||
go mod tidy
|
||||
|
||||
update-junit-xsd:
|
||||
curl https://raw.githubusercontent.com/junit-team/junit5/main/platform-tests/src/test/resources/jenkins-junit.xsd > fixtures/junit.xsd
|
||||
|
|
|
|||
300
Readme.md
300
Readme.md
|
|
@ -1,13 +1,12 @@
|
|||
<img width="50%" alt="Kubeconform-GitHub-Hero" src="https://user-images.githubusercontent.com/19731161/142411871-f695e40c-bfa8-43ca-97c0-94c256749732.png">
|
||||
<hr>
|
||||
|
||||
[](https://github.com/yannh/kubeconform/actions?query=branch%3Amaster)
|
||||
[](https://github.com/yannh/kubeconform/actions?query=branch%3Amaster)
|
||||
[](https://formulae.brew.sh/formula/kubeconform)
|
||||
[](https://goreportcard.com/report/github.com/yannh/kubeconform)
|
||||
[](https://pkg.go.dev/github.com/yannh/kubeconform/pkg/validator)
|
||||
|
||||
Kubeconform is a Kubernetes manifests validation tool. Build it into your CI to validate your Kubernetes
|
||||
configuration!
|
||||
`Kubeconform` is a Kubernetes manifest validation tool. Incorporate it into your CI, or use it locally to validate your Kubernetes configuration!
|
||||
|
||||
It is inspired by, contains code from and is designed to stay close to
|
||||
[Kubeval](https://github.com/instrumenta/kubeval), but with the following improvements:
|
||||
|
|
@ -16,10 +15,45 @@ It is inspired by, contains code from and is designed to stay close to
|
|||
* configurable list of **remote, or local schemas locations**, enabling validating Kubernetes
|
||||
custom resources (CRDs) and offline validation capabilities
|
||||
* uses by default a [self-updating fork](https://github.com/yannh/kubernetes-json-schema) of the schemas registry maintained
|
||||
by the [kubernetes-json-schema](https://github.com/instrumenta/kubernetes-json-schema) project - which guarantees
|
||||
by the kubernetes-json-schema project - which guarantees
|
||||
up-to-date **schemas for all recent versions of Kubernetes**.
|
||||
|
||||
<details><summary><h4>Speed comparison with Kubeval</h4></summary><p>
|
||||
Running on a pretty large kubeconfigs setup, on a laptop with 4 cores:
|
||||
|
||||
```bash
|
||||
$ time kubeconform -ignore-missing-schemas -n 8 -summary preview staging production
|
||||
Summary: 50714 resources found in 35139 files - Valid: 27334, Invalid: 0, Errors: 0 Skipped: 23380
|
||||
real 0m6,710s
|
||||
user 0m38,701s
|
||||
sys 0m1,161s
|
||||
$ time kubeval -d preview,staging,production --ignore-missing-schemas --quiet
|
||||
[... Skipping output]
|
||||
real 0m35,336s
|
||||
user 0m0,717s
|
||||
sys 0m1,069s
|
||||
```
|
||||
</p></details>
|
||||
|
||||
### A small overview of Kubernetes manifest validation
|
||||
## Table of contents
|
||||
|
||||
* [A small overview of Kubernetes manifest validation](#a-small-overview-of-kubernetes-manifest-validation)
|
||||
* [Limits of Kubeconform validation](#Limits-of-Kubeconform-validation)
|
||||
* [Installation](#Installation)
|
||||
* [Usage](#Usage)
|
||||
* [Usage examples](#Usage-examples)
|
||||
* [Proxy support](#Proxy-support)
|
||||
* [Overriding schemas location](#Overriding-schemas-location)
|
||||
* [CustomResourceDefinition (CRD) Support](#CustomResourceDefinition-CRD-Support)
|
||||
* [OpenShift schema Support](#OpenShift-schema-Support)
|
||||
* [Integrating Kubeconform in the CI](#Integrating-Kubeconform-in-the-CI)
|
||||
* [Github Workflow](#Github-Workflow)
|
||||
* [Gitlab-CI](#Gitlab-CI)
|
||||
* [Helm charts](#helm-charts)
|
||||
* [Using kubeconform as a Go Module](#Using-kubeconform-as-a-Go-Module)
|
||||
* [Credits](#Credits)
|
||||
|
||||
## A small overview of Kubernetes manifest validation
|
||||
|
||||
Kubernetes's API is described using the [OpenAPI (formerly swagger) specification](https://www.openapis.org),
|
||||
in a [file](https://github.com/kubernetes/kubernetes/blob/master/api/openapi-spec/swagger.json) checked into
|
||||
|
|
@ -32,22 +66,14 @@ and break it down into multiple JSON schemas, stored in github at
|
|||
[instrumenta/kubernetes-json-schema](https://github.com/instrumenta/kubernetes-json-schema) and published on
|
||||
[kubernetesjsonschema.dev](https://kubernetesjsonschema.dev/).
|
||||
|
||||
Kubeconform relies on [a fork of kubernetes-json-schema](https://github.com/yannh/kubernetes-json-schema/)
|
||||
that is more aggressively kept up-to-date, and contains schemas for all recent versions of Kubernetes.
|
||||
`Kubeconform` relies on [a fork of kubernetes-json-schema](https://github.com/yannh/kubernetes-json-schema/)
|
||||
that is more meticulously kept up-to-date, and contains schemas for all recent versions of Kubernetes.
|
||||
|
||||
### Limits of Kubeconform validation
|
||||
|
||||
Kubeconform, similarly to kubeval, only validates manifests using the OpenAPI specifications. In some
|
||||
cases, the Kubernetes controllers might perform additional validation - so that manifests passing kubeval
|
||||
validation would still error when being deployed. See for example these bugs against kubeval:
|
||||
[#253](https://github.com/instrumenta/kubeval/issues/253)
|
||||
[#256](https://github.com/instrumenta/kubeval/issues/256)
|
||||
[#257](https://github.com/instrumenta/kubeval/issues/257)
|
||||
[#259](https://github.com/instrumenta/kubeval/issues/259). The validation logic mentioned in these
|
||||
bug reports is not part of Kubernetes' OpenAPI spec, and therefore kubeconform/kubeval will not detect the
|
||||
configuration errors.
|
||||
`Kubeconform`, similar to `kubeval`, only validates manifests using the official Kubernetes OpenAPI specifications. The Kubernetes controllers still perform additional server-side validations that are not part of the OpenAPI specifications. Those server-side validations are not covered by `Kubeconform` (examples: [#65](https://github.com/yannh/kubeconform/issues/65), [#122](https://github.com/yannh/kubeconform/issues/122), [#142](https://github.com/yannh/kubeconform/issues/142)). You can use a 3rd-party tool or the `kubectl --dry-run=server` command to fill the missing (validation) gap.
|
||||
|
||||
### Installation
|
||||
## Installation
|
||||
|
||||
If you are a [Homebrew](https://brew.sh/) user, you can install by running:
|
||||
|
||||
|
|
@ -55,59 +81,75 @@ If you are a [Homebrew](https://brew.sh/) user, you can install by running:
|
|||
$ brew install kubeconform
|
||||
```
|
||||
|
||||
If you are a Windows user, you can install with [winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/) by running:
|
||||
|
||||
```cmd
|
||||
winget install YannHamon.kubeconform
|
||||
```
|
||||
|
||||
You can also download the latest version from the [release page](https://github.com/yannh/kubeconform/releases).
|
||||
|
||||
### Usage
|
||||
Another way of installation is via Golang's package manager:
|
||||
|
||||
```bash
|
||||
# With a specific version tag
|
||||
$ go install github.com/yannh/kubeconform/cmd/kubeconform@v0.4.13
|
||||
|
||||
# Latest version
|
||||
$ go install github.com/yannh/kubeconform/cmd/kubeconform@latest
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
$ ./bin/kubeconform -h
|
||||
Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]...
|
||||
$ kubeconform -h
|
||||
Usage: kubeconform [OPTION]... [FILE OR FOLDER]...
|
||||
-cache string
|
||||
cache schemas downloaded via HTTP to this folder
|
||||
-cpu-prof string
|
||||
debug - log CPU profiling to file
|
||||
cache schemas downloaded via HTTP to this folder
|
||||
-debug
|
||||
print debug information
|
||||
-exit-on-error
|
||||
immediately stop execution when the first error is encountered
|
||||
-h show help information
|
||||
immediately stop execution when the first error is encountered
|
||||
-h show help information
|
||||
-ignore-filename-pattern value
|
||||
regular expression specifying paths to ignore (can be specified multiple times)
|
||||
regular expression specifying paths to ignore (can be specified multiple times)
|
||||
-ignore-missing-schemas
|
||||
skip files with missing schemas instead of failing
|
||||
skip files with missing schemas instead of failing
|
||||
-insecure-skip-tls-verify
|
||||
disable verification of the server's SSL certificate. This will make your HTTPS connections insecure
|
||||
disable verification of the server's SSL certificate. This will make your HTTPS connections insecure
|
||||
-kubernetes-version string
|
||||
version of Kubernetes to validate against, e.g.: 1.18.0 (default "master")
|
||||
version of Kubernetes to validate against, e.g.: 1.18.0 (default "master")
|
||||
-n int
|
||||
number of goroutines to run concurrently (default 4)
|
||||
number of goroutines to run concurrently (default 4)
|
||||
-output string
|
||||
output format - json, junit, tap, text (default "text")
|
||||
output format - json, junit, pretty, tap, text (default "text")
|
||||
-reject string
|
||||
comma-separated list of kinds to reject
|
||||
comma-separated list of kinds or GVKs to reject
|
||||
-schema-location value
|
||||
override schemas location search path (can be specified multiple times)
|
||||
override schemas location search path (can be specified multiple times)
|
||||
-skip string
|
||||
comma-separated list of kinds to ignore
|
||||
comma-separated list of kinds or GVKs to ignore
|
||||
-strict
|
||||
disallow additional properties not in schema
|
||||
disallow additional properties not in schema or duplicated keys
|
||||
-summary
|
||||
print a summary at the end (ignored for junit output)
|
||||
print a summary at the end (ignored for junit output)
|
||||
-v show version information
|
||||
-verbose
|
||||
print results for all resources (ignored for tap and junit output)
|
||||
print results for all resources (ignored for tap and junit output)
|
||||
```
|
||||
|
||||
### Usage examples
|
||||
|
||||
* Validating a single, valid file
|
||||
```
|
||||
$ ./bin/kubeconform fixtures/valid.yaml
|
||||
```bash
|
||||
$ kubeconform fixtures/valid.yaml
|
||||
$ echo $?
|
||||
0
|
||||
```
|
||||
|
||||
* Validating a single invalid file, setting output to json, and printing a summary
|
||||
```
|
||||
$ ./bin/kubeconform -summary -output json fixtures/invalid.yaml
|
||||
```bash
|
||||
$ kubeconform -summary -output json fixtures/invalid.yaml
|
||||
{
|
||||
"resources": [
|
||||
{
|
||||
|
|
@ -130,90 +172,133 @@ $ echo $?
|
|||
```
|
||||
|
||||
* Passing manifests via Stdin
|
||||
```
|
||||
```bash
|
||||
cat fixtures/valid.yaml | ./bin/kubeconform -summary
|
||||
Summary: 1 resource found parsing stdin - Valid: 1, Invalid: 0, Errors: 0 Skipped: 0
|
||||
```
|
||||
|
||||
* Validating a file, ignoring its resource using both Kind, and GVK (Group, Version, Kind) notations
|
||||
```
|
||||
# This will ignore ReplicationController for all apiVersions
|
||||
$ kubeconform -summary -skip ReplicationController fixtures/valid.yaml
|
||||
Summary: 1 resource found in 1 file - Valid: 0, Invalid: 0, Errors: 0, Skipped: 1
|
||||
|
||||
# This will ignore ReplicationController only for apiVersion v1
|
||||
$ kubeconform -summary -skip v1/ReplicationController fixtures/valid.yaml
|
||||
Summary: 1 resource found in 1 file - Valid: 0, Invalid: 0, Errors: 0, Skipped: 1
|
||||
```
|
||||
|
||||
* Validating a folder, increasing the number of parallel workers
|
||||
```
|
||||
$ ./bin/kubeconform -summary -n 16 fixtures
|
||||
$ kubeconform -summary -n 16 fixtures
|
||||
fixtures/crd_schema.yaml - CustomResourceDefinition trainingjobs.sagemaker.aws.amazon.com failed validation: could not find schema for CustomResourceDefinition
|
||||
fixtures/invalid.yaml - ReplicationController bob is invalid: Invalid type. Expected: [integer,null], given: string
|
||||
[...]
|
||||
Summary: 65 resources found in 34 files - Valid: 55, Invalid: 2, Errors: 8 Skipped: 0
|
||||
```
|
||||
|
||||
### Overriding schemas location - CRD and Openshift support
|
||||
### Proxy support
|
||||
|
||||
When the `-schema-location` parameter is not used, or set to "default", kubeconform will default to downloading
|
||||
schemas from `https://github.com/yannh/kubernetes-json-schema`. Kubeconform however supports passing one, or multiple,
|
||||
`Kubeconform` will respect the **HTTPS_PROXY** variable when downloading schema files.
|
||||
|
||||
```bash
|
||||
$ HTTPS_PROXY=proxy.local bin/kubeconform fixtures/valid.yaml
|
||||
```
|
||||
|
||||
## Overriding schemas location
|
||||
|
||||
When the `-schema-location` parameter is not used, or set to `default`, kubeconform will default to downloading
|
||||
schemas from https://github.com/yannh/kubernetes-json-schema. Kubeconform however supports passing one, or multiple,
|
||||
schemas locations - HTTP(s) URLs, or local filesystem paths, in which case it will lookup for schema definitions
|
||||
in each of them, in order, stopping as soon as a matching file is found.
|
||||
|
||||
* If the -schema-location value does not end with '.json', Kubeconform will assume filenames / a file
|
||||
structure identical to that of kubernetesjsonschema.dev or github.com/yannh/kubernetes-json-schema.
|
||||
* if the -schema-location value ends with '.json' - Kubeconform assumes the value is a Go templated
|
||||
string that indicates how to search for JSON schemas.
|
||||
* the -schema-location value of "default" is an alias for https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json.
|
||||
Both following command lines are equivalent:
|
||||
```
|
||||
$ ./bin/kubeconform fixtures/valid.yaml
|
||||
$ ./bin/kubeconform -schema-location default fixtures/valid.yaml
|
||||
$ ./bin/kubeconform -schema-location 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/valid.yaml
|
||||
```
|
||||
|
||||
To support validating CRDs, we need to convert OpenAPI files to JSON schema, storing the JSON schemas
|
||||
in a local folder - for example schemas. Then we specify this folder as an additional registry to lookup:
|
||||
* If the `-schema-location` value does not end with `.json`, Kubeconform will assume filenames / a file
|
||||
structure identical to that of [kubernetesjsonschema.dev](https://kubernetesjsonschema.dev/) or [yannh/kubernetes-json-schema](https://github.com/yannh/kubernetes-json-schema).
|
||||
* if the `-schema-location` value ends with `.json` - Kubeconform assumes the value is a **Go templated
|
||||
string** that indicates how to search for JSON schemas.
|
||||
* the `-schema-location` value of `default` is an alias for `https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{.NormalizedKubernetesVersion}}-standalone{{.StrictSuffix}}/{{.ResourceKind}}{{.KindSuffix}}.json`.
|
||||
|
||||
**The following command lines are equivalent:**
|
||||
```bash
|
||||
$ kubeconform fixtures/valid.yaml
|
||||
$ kubeconform -schema-location default fixtures/valid.yaml
|
||||
$ kubeconform -schema-location 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{.NormalizedKubernetesVersion}}-standalone{{.StrictSuffix}}/{{.ResourceKind}}{{.KindSuffix}}.json' fixtures/valid.yaml
|
||||
```
|
||||
# If the resource Kind is not found in kubernetesjsonschema.dev, also lookup in the schemas/ folder for a matching file
|
||||
$ ./bin/kubeconform -schema-location default -schema-location 'schemas/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/custom-resource.yaml
|
||||
```
|
||||
|
||||
You can validate Openshift manifests using a custom schema location. Set the OpenShift version to validate
|
||||
against using -kubernetes-version.
|
||||
|
||||
```
|
||||
bin/kubeconform -kubernetes-version 3.8.0 -schema-location 'https://raw.githubusercontent.com/garethr/openshift-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}.json' -summary fixtures/valid.yaml
|
||||
Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0 Skipped: 0
|
||||
```
|
||||
|
||||
Here are the variables you can use in -schema-location:
|
||||
* *NormalizedKubernetesVersion* - Kubernetes Version, prefixed by v
|
||||
* *StrictSuffix* - "-strict" or "" depending on whether validation is running in strict mode or not
|
||||
* *ResourceKind* - Kind of the Kubernetes Resource
|
||||
* *ResourceAPIVersion* - Version of API used for the resource - "v1" in "apiVersion: monitoring.coreos.com/v1"
|
||||
* *KindSuffix* - suffix computed from apiVersion - for compatibility with Kubeval schema registries
|
||||
* *Group* - the group name as stated in this resource's definition - "monitoring.coreos.com" in "apiVersion: monitoring.coreos.com/v1"
|
||||
* *KindSuffix* - suffix computed from apiVersion - for compatibility with `Kubeval` schema registries
|
||||
|
||||
### Converting an OpenAPI file to a JSON Schema
|
||||
### CustomResourceDefinition (CRD) Support
|
||||
|
||||
Kubeconform uses JSON schemas to validate Kubernetes resources. For Custom Resource, the CustomResourceDefinition
|
||||
Because Custom Resources (CR) are not native Kubernetes objects, they are not included in the default schema.
|
||||
If your CRs are present in [Datree's CRDs-catalog](https://github.com/datreeio/CRDs-catalog), you can specify this project as an additional registry to lookup:
|
||||
|
||||
```bash
|
||||
# Look in the CRDs-catalog for the desired schema/s
|
||||
$ kubeconform -schema-location default -schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' [MANIFEST]
|
||||
```
|
||||
|
||||
If your CRs are not present in the CRDs-catalog, you will need to manually pull the CRDs manifests from your cluster and convert the `OpenAPI.spec` to JSON schema format.
|
||||
|
||||
<details><summary>Converting an OpenAPI file to a JSON Schema</summary>
|
||||
<p>
|
||||
|
||||
`Kubeconform` uses JSON schemas to validate Kubernetes resources. For Custom Resource, the CustomResourceDefinition
|
||||
first needs to be converted to JSON Schema. A script is provided to convert these CustomResourceDefinitions
|
||||
to JSON schema. Here is an example how to use it:
|
||||
|
||||
```
|
||||
$ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml
|
||||
```bash
|
||||
$ python ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml
|
||||
JSON schema written to trainingjob_v1.json
|
||||
```
|
||||
|
||||
The `FILENAME_FORMAT` environment variable can be used to change the output file name (Available variables: `kind`, `group`, `version`) (Default: `{kind}_{version}`).
|
||||
By default, the file name output format is `{kind}_{version}`. The `FILENAME_FORMAT` environment variable can be used to change the output file name (Available variables: `kind`, `group`, `fullgroup`, `version`):
|
||||
|
||||
```
|
||||
$ export FILENAME_FORMAT='{kind}-{group}-{version}'
|
||||
$ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml
|
||||
JSON schema written to trainingjob-sagemaker-v1.json
|
||||
|
||||
$ export FILENAME_FORMAT='{kind}-{fullgroup}-{version}'
|
||||
$ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml
|
||||
JSON schema written to trainingjob-sagemaker.aws.amazon.com-v1.json
|
||||
```
|
||||
|
||||
Some CRD schemas do not have explicit validation for fields implicitly validated by the Kubernetes API like `apiVersion`, `kind`, and `metadata`, thus additional properties are allowed at the root of the JSON schema by default, if this is not desired the `DENY_ROOT_ADDITIONAL_PROPERTIES` environment variable can be set to any non-empty value.
|
||||
After converting your CRDs to JSON schema files, you can use `kubeconform` to validate your CRs against them:
|
||||
|
||||
### Usage as a Github Action
|
||||
```
|
||||
# If the resource Kind is not found in default, also lookup in the schemas/ folder for a matching file
|
||||
$ kubeconform -schema-location default -schema-location 'schemas/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/custom-resource.yaml
|
||||
```
|
||||
|
||||
Kubeconform is publishes Docker Images to Github's new Container Registry, ghcr.io. These images
|
||||
ℹ️ Datree's [CRD Extractor](https://github.com/datreeio/CRDs-catalog#crd-extractor) is a utility that can be used instead of this manual process.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
### OpenShift schema Support
|
||||
|
||||
You can validate Openshift manifests using a custom schema location. Set the OpenShift version (v3.10.0-4.1.0) to validate
|
||||
against using `-kubernetes-version`.
|
||||
|
||||
```
|
||||
kubeconform -kubernetes-version 3.8.0 -schema-location 'https://raw.githubusercontent.com/garethr/openshift-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}.json' -summary fixtures/valid.yaml
|
||||
Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0 Skipped: 0
|
||||
```
|
||||
|
||||
## Integrating Kubeconform in the CI
|
||||
|
||||
`Kubeconform` publishes Docker Images to Github's new Container Registry (ghcr.io). These images
|
||||
can be used directly in a Github Action, once logged in using a [_Github Token_](https://github.blog/changelog/2021-03-24-packages-container-registry-now-supports-github_token/).
|
||||
|
||||
### Github Workflow
|
||||
|
||||
Example:
|
||||
```
|
||||
```yaml
|
||||
name: kubeconform
|
||||
on: push
|
||||
jobs:
|
||||
|
|
@ -223,7 +308,7 @@ jobs:
|
|||
- name: login to Github Packages
|
||||
run: echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker://ghcr.io/yannh/kubeconform:master
|
||||
- uses: docker://ghcr.io/yannh/kubeconform:latest
|
||||
with:
|
||||
entrypoint: '/kubeconform'
|
||||
args: "-summary -output json kubeconfigs/"
|
||||
|
|
@ -235,44 +320,39 @@ bandwidth costs might be applicable. Since bandwidth from Github Packages within
|
|||
Github Container Registry to also be usable for free within Github Actions in the future. If that were not to be the
|
||||
case, I might publish the Docker image to a different platform.
|
||||
|
||||
### Proxy support
|
||||
### Gitlab-CI
|
||||
|
||||
Kubeconform will respect the HTTPS_PROXY variable when downloading schema files.
|
||||
|
||||
```
|
||||
$ HTTPS_PROXY=proxy.local bin/kubeconform fixtures/valid.yaml
|
||||
```
|
||||
### Speed comparison with Kubeval
|
||||
|
||||
Running on a pretty large kubeconfigs setup, on a laptop with 4 cores:
|
||||
|
||||
```
|
||||
$ time kubeconform -ignore-missing-schemas -n 8 -summary preview staging production
|
||||
Summary: 50714 resources found in 35139 files - Valid: 27334, Invalid: 0, Errors: 0 Skipped: 23380
|
||||
|
||||
real 0m6,710s
|
||||
user 0m38,701s
|
||||
sys 0m1,161s
|
||||
|
||||
$ time kubeval -d preview,staging,production --ignore-missing-schemas --quiet
|
||||
[... Skipping output]
|
||||
|
||||
real 0m35,336s
|
||||
user 0m0,717s
|
||||
sys 0m1,069s
|
||||
The Kubeconform Docker image can be used in Gitlab-CI. Here is an example of a Gitlab-CI job:
|
||||
|
||||
```yaml
|
||||
lint-kubeconform:
|
||||
stage: validate
|
||||
image:
|
||||
name: ghcr.io/yannh/kubeconform:latest-alpine
|
||||
entrypoint: [""]
|
||||
script:
|
||||
- /kubeconform -summary -output json kubeconfigs/
|
||||
```
|
||||
|
||||
### Using kubeconform as a Go Module
|
||||
See [issue 106](https://github.com/yannh/kubeconform/issues/106) for more details.
|
||||
|
||||
## Helm charts
|
||||
|
||||
There is a 3rd party [repository](https://github.com/jtyr/kubeconform-helm) that
|
||||
allows to use `kubeconform` to test [Helm charts](https://helm.sh) in the form of
|
||||
a [Helm plugin](https://helm.sh/docs/topics/plugins/) and [`pre-commit`
|
||||
hook](https://pre-commit.com/).
|
||||
|
||||
## Using kubeconform as a Go Module
|
||||
|
||||
**Warning**: This is a work-in-progress, the interface is not yet considered stable. Feedback is encouraged.
|
||||
|
||||
Kubeconform contains a package that can be used as a library.
|
||||
`Kubeconform` contains a package that can be used as a library.
|
||||
An example of usage can be found in [examples/main.go](examples/main.go)
|
||||
|
||||
Additional documentation on [pkg.go.dev](https://pkg.go.dev/github.com/yannh/kubeconform/pkg/validator)
|
||||
|
||||
### Credits
|
||||
## Credits
|
||||
|
||||
* @garethr for the [Kubeval](https://github.com/instrumenta/kubeval) and
|
||||
[kubernetes-json-schema](https://github.com/instrumenta/kubernetes-json-schema) projects ❤️
|
||||
|
|
|
|||
|
|
@ -19,3 +19,8 @@
|
|||
run bin/kubeconform -schema-location 'fixtures/{{ .ResourceKind }}.json' -schema-location './fixtures/registry/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/test_crd.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Pass when using a cached schema with external references" {
|
||||
run bin/kubeconform -cache fixtures/cache -summary -schema-location 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{ .NormalizedKubernetesVersion }}{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/valid.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,12 @@ resetCacheFolder() {
|
|||
[ "${lines[0]}" == 'Usage: bin/kubeconform [OPTION]... [FILE OR FOLDER]...' ]
|
||||
}
|
||||
|
||||
@test "Fail and display help when using an incorrect flag" {
|
||||
run bin/kubeconform -xyz
|
||||
[ "$status" -eq 1 ]
|
||||
[ "${lines[0]}" == 'flag provided but not defined: -xyz' ]
|
||||
}
|
||||
|
||||
@test "Pass when parsing a valid Kubernetes config YAML file" {
|
||||
run bin/kubeconform -summary fixtures/valid.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
|
|
@ -30,7 +36,7 @@ resetCacheFolder() {
|
|||
}
|
||||
|
||||
@test "Pass when parsing a valid Kubernetes config JSON file" {
|
||||
run bin/kubeconform -kubernetes-version 1.17.1 -summary fixtures/valid.json
|
||||
run bin/kubeconform -kubernetes-version 1.20.0 -summary fixtures/valid.json
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0, Skipped: 0" ]
|
||||
}
|
||||
|
|
@ -128,17 +134,32 @@ resetCacheFolder() {
|
|||
}
|
||||
|
||||
@test "Fail when parsing a config with additional properties and strict set" {
|
||||
run bin/kubeconform -strict -kubernetes-version 1.16.0 fixtures/extra_property.yaml
|
||||
run bin/kubeconform -strict -kubernetes-version 1.20.0 fixtures/extra_property.yaml
|
||||
[ "$status" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "Fail when parsing a config with duplicate properties and strict set" {
|
||||
run bin/kubeconform -strict -kubernetes-version 1.20.0 fixtures/duplicate_property.yaml
|
||||
[ "$status" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "Pass when parsing a config with duplicate properties and strict NOT set" {
|
||||
run bin/kubeconform -kubernetes-version 1.20.0 fixtures/duplicate_property.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Pass when using a valid, preset -schema-location" {
|
||||
run bin/kubeconform -schema-location default fixtures/valid.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Pass when using a valid HTTP -schema-location" {
|
||||
run bin/kubeconform -schema-location 'https://kubernetesjsonschema.dev/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/valid.yaml
|
||||
run bin/kubeconform -schema-location 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/valid.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Pass when using schemas with HTTP references" {
|
||||
run bin/kubeconform -summary -schema-location 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{ .NormalizedKubernetesVersion }}{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/valid.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
|
|
@ -159,6 +180,13 @@ resetCacheFolder() {
|
|||
[ "$status" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "Fail early when passing a non valid -kubernetes-version" {
|
||||
run bin/kubeconform -kubernetes-version 1.25 fixtures/valid.yaml
|
||||
[ "${lines[0]}" == 'invalid value "1.25" for flag -kubernetes-version: 1.25 is not a valid version. Valid values are "master" (default) or full version x.y.z (e.g. "1.27.2")' ]
|
||||
[[ "${lines[1]}" == "Usage:"* ]]
|
||||
[ "$status" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "Pass with a valid input when validating against openshift manifests" {
|
||||
run bin/kubeconform -kubernetes-version 3.8.0 -schema-location 'https://raw.githubusercontent.com/garethr/openshift-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}.json' -summary fixtures/valid.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
|
|
@ -202,6 +230,18 @@ resetCacheFolder() {
|
|||
[ "$output" = "fixtures/valid.yaml - bob ReplicationController skipped" ]
|
||||
}
|
||||
|
||||
@test "Skip when parsing a resource with a GVK to skip" {
|
||||
run bin/kubeconform -verbose -skip v1/ReplicationController fixtures/valid.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "fixtures/valid.yaml - bob ReplicationController skipped" ]
|
||||
}
|
||||
|
||||
@test "Do not skip when parsing a resource with a GVK to skip, where the Kind matches but not the version" {
|
||||
run bin/kubeconform -verbose -skip v2/ReplicationController fixtures/valid.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "fixtures/valid.yaml - ReplicationController bob is valid" ]
|
||||
}
|
||||
|
||||
@test "Fail when parsing a resource from a kind to reject" {
|
||||
run bin/kubeconform -verbose -reject ReplicationController fixtures/valid.yaml
|
||||
[ "$status" -eq 1 ]
|
||||
|
|
@ -224,7 +264,7 @@ resetCacheFolder() {
|
|||
|
||||
@test "Fail when no schema found, ensure 404 is not cached on disk" {
|
||||
resetCacheFolder
|
||||
run bin/kubeconform -cache cache -schema-location 'https://raw.githubusercontent.com/garethr/openshift-json-schema/master/doesnotexist.json' fixtures/valid.yaml
|
||||
run bin/kubeconform -cache cache -schema-location 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/doesnotexist.json' fixtures/valid.yaml
|
||||
[ "$status" -eq 1 ]
|
||||
[ "$output" == 'fixtures/valid.yaml - ReplicationController bob failed validation: could not find schema for ReplicationController' ]
|
||||
[ "`ls cache/ | wc -l`" -eq 0 ]
|
||||
|
|
@ -236,6 +276,13 @@ resetCacheFolder() {
|
|||
[ "$output" = "failed opening cache folder cache_does_not_exist: stat cache_does_not_exist: no such file or directory" ]
|
||||
}
|
||||
|
||||
@test "HTTP references should be cached" {
|
||||
resetCacheFolder
|
||||
run bin/kubeconform -cache cache -summary -schema-location 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{ .NormalizedKubernetesVersion }}{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/valid.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "`ls cache/ | wc -l`" -eq 2 ]
|
||||
}
|
||||
|
||||
@test "Produces correct TAP output" {
|
||||
run bin/kubeconform -output tap fixtures/valid.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
|
|
@ -259,14 +306,14 @@ resetCacheFolder() {
|
|||
@test "Fail when parsing a List that contains an invalid resource" {
|
||||
run bin/kubeconform -summary fixtures/list_invalid.yaml
|
||||
[ "$status" -eq 1 ]
|
||||
[ "${lines[0]}" == 'fixtures/list_invalid.yaml - ReplicationController bob is invalid: For field spec.replicas: Invalid type. Expected: [integer,null], given: string' ]
|
||||
[ "${lines[0]}" == 'fixtures/list_invalid.yaml - ReplicationController bob is invalid: problem validating schema. Check JSON formatting: jsonschema validation failed with '\''https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/master-standalone/replicationcontroller-v1.json#'\'' - at '\''/spec/replicas'\'': got string, want null or integer' ]
|
||||
[ "${lines[1]}" == 'Summary: 2 resources found in 1 file - Valid: 1, Invalid: 1, Errors: 0, Skipped: 0' ]
|
||||
}
|
||||
|
||||
@test "Fail when parsing a List that contains an invalid resource from stdin" {
|
||||
run bash -c "cat fixtures/list_invalid.yaml | bin/kubeconform -summary -"
|
||||
[ "$status" -eq 1 ]
|
||||
[ "${lines[0]}" == 'stdin - ReplicationController bob is invalid: For field spec.replicas: Invalid type. Expected: [integer,null], given: string' ]
|
||||
[ "${lines[0]}" == 'stdin - ReplicationController bob is invalid: problem validating schema. Check JSON formatting: jsonschema validation failed with '\''https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/master-standalone/replicationcontroller-v1.json#'\'' - at '\''/spec/replicas'\'': got string, want null or integer' ]
|
||||
[ "${lines[1]}" == 'Summary: 2 resources found parsing stdin - Valid: 1, Invalid: 1, Errors: 0, Skipped: 0' ]
|
||||
}
|
||||
|
||||
|
|
@ -300,3 +347,21 @@ resetCacheFolder() {
|
|||
[ "$status" -eq 0 ]
|
||||
[ "$output" = 'Summary: 100000 resources found parsing stdin - Valid: 100000, Invalid: 0, Errors: 0, Skipped: 0' ]
|
||||
}
|
||||
|
||||
@test "JUnit output can be validated against the Junit schema definition" {
|
||||
run bash -c "bin/kubeconform -output junit -summary fixtures/valid.yaml > output.xml"
|
||||
[ "$status" -eq 0 ]
|
||||
run xmllint --noout --schema fixtures/junit.xsd output.xml
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "passes when trying to use a CRD that does not have the JSONSchema set" {
|
||||
run bash -c "bin/kubeconform -schema-location default -schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' fixtures/httpproxy.yaml"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
# https://github.com/yannh/kubeconform/pull/309
|
||||
@test "passes when validating duration not in ISO8601" {
|
||||
run bash -c "./bin/kubeconform -schema-location ./fixtures/grafanaalertrulegroup_v1beta1.json ./fixtures/grafana-alert-rule-group-sample.yaml"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,31 +46,11 @@ func processResults(cancel context.CancelFunc, o output.Output, validationResult
|
|||
return result
|
||||
}
|
||||
|
||||
func realMain() int {
|
||||
cfg, out, err := config.FromFlags(os.Args[0], os.Args[1:])
|
||||
if out != "" {
|
||||
o := os.Stderr
|
||||
errCode := 1
|
||||
if cfg.Help {
|
||||
o = os.Stdout
|
||||
errCode = 0
|
||||
}
|
||||
fmt.Fprintln(o, out)
|
||||
return errCode
|
||||
}
|
||||
|
||||
if cfg.Version {
|
||||
fmt.Println(version)
|
||||
return 0
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed parsing command line: %s\n", err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
if cfg.CPUProfileFile != "" {
|
||||
f, err := os.Create(cfg.CPUProfileFile)
|
||||
func kubeconform(cfg config.Config) int {
|
||||
var err error
|
||||
cpuProfileFile := os.Getenv("KUBECONFORM_CPUPROFILE_FILE")
|
||||
if cpuProfileFile != "" {
|
||||
f, err := os.Create(cpuProfileFile)
|
||||
if err != nil {
|
||||
log.Fatal("could not create CPU profile: ", err)
|
||||
}
|
||||
|
|
@ -93,17 +73,18 @@ func realMain() int {
|
|||
}
|
||||
|
||||
var o output.Output
|
||||
if o, err = output.New(cfg.OutputFormat, cfg.Summary, useStdin, cfg.Verbose); err != nil {
|
||||
if o, err = output.New(os.Stdout, cfg.OutputFormat, cfg.Summary, useStdin, cfg.Verbose); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
|
||||
v, err := validator.New(cfg.SchemaLocations, validator.Opts{
|
||||
var v validator.Validator
|
||||
v, err = validator.New(cfg.SchemaLocations, validator.Opts{
|
||||
Cache: cfg.Cache,
|
||||
Debug: cfg.Debug,
|
||||
SkipTLS: cfg.SkipTLS,
|
||||
SkipKinds: cfg.SkipKinds,
|
||||
RejectKinds: cfg.RejectKinds,
|
||||
KubernetesVersion: cfg.KubernetesVersion,
|
||||
KubernetesVersion: cfg.KubernetesVersion.String(),
|
||||
Strict: cfg.Strict,
|
||||
IgnoreMissingSchemas: cfg.IgnoreMissingSchemas,
|
||||
})
|
||||
|
|
@ -176,5 +157,27 @@ func realMain() int {
|
|||
}
|
||||
|
||||
func main() {
|
||||
os.Exit(realMain())
|
||||
cfg, out, err := config.FromFlags(os.Args[0], os.Args[1:])
|
||||
if out != "" {
|
||||
o := os.Stderr
|
||||
errCode := 1
|
||||
if cfg.Help {
|
||||
o = os.Stdout
|
||||
errCode = 0
|
||||
}
|
||||
fmt.Fprintln(o, out)
|
||||
os.Exit(errCode)
|
||||
}
|
||||
|
||||
if cfg.Version {
|
||||
fmt.Println(version)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed parsing command line: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(kubeconform(cfg))
|
||||
}
|
||||
|
|
|
|||
46
fixtures/cache/603105c17f981119fec20ae25cfb97ff6dd99114a875ae841ef965d9345667e3
vendored
Normal file
46
fixtures/cache/603105c17f981119fec20ae25cfb97ff6dd99114a875ae841ef965d9345667e3
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"description": "ReplicationController represents the configuration of a replication controller.",
|
||||
"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",
|
||||
"null"
|
||||
],
|
||||
"enum": [
|
||||
"v1"
|
||||
]
|
||||
},
|
||||
"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",
|
||||
"null"
|
||||
],
|
||||
"enum": [
|
||||
"ReplicationController"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/master/_definitions.json#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta",
|
||||
"description": "If the Labels of a ReplicationController are empty, they are defaulted to be the same as the Pod(s) that the replication controller manages. Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata"
|
||||
},
|
||||
"spec": {
|
||||
"$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/master/_definitions.json#/definitions/io.k8s.api.core.v1.ReplicationControllerSpec",
|
||||
"description": "Spec defines the specification of the desired behavior of the replication controller. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/master/_definitions.json#/definitions/io.k8s.api.core.v1.ReplicationControllerStatus",
|
||||
"description": "Status is the most recently observed status of the replication controller. This data may be out of date by some window of time. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"x-kubernetes-group-version-kind": [
|
||||
{
|
||||
"group": "",
|
||||
"kind": "ReplicationController",
|
||||
"version": "v1"
|
||||
}
|
||||
],
|
||||
"$schema": "http://json-schema.org/schema#"
|
||||
}
|
||||
22067
fixtures/cache/6dc6142c64b944d783a3e783526114da8e747a14a11d8b32dd1f12e2d89a8330
vendored
Normal file
22067
fixtures/cache/6dc6142c64b944d783a3e783526114da8e747a14a11d8b32dd1f12e2d89a8330
vendored
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
18
fixtures/duplicate_property.yaml
Normal file
18
fixtures/duplicate_property.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: nginx-ds
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
k8s-app: nginx-ds
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- image: envoy
|
||||
name: envoy
|
||||
containers:
|
||||
- image: nginx
|
||||
name: nginx
|
||||
62
fixtures/grafana-alert-rule-group-sample.yaml
Normal file
62
fixtures/grafana-alert-rule-group-sample.yaml
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
---
|
||||
apiVersion: grafana.integreatly.org/v1beta1
|
||||
kind: GrafanaAlertRuleGroup
|
||||
metadata:
|
||||
name: grafanaalertrulegroup-sample
|
||||
spec:
|
||||
folderRef: test-folder
|
||||
instanceSelector:
|
||||
matchLabels:
|
||||
dashboards: "grafana"
|
||||
interval: 5m
|
||||
rules:
|
||||
- condition: B
|
||||
data:
|
||||
- datasourceUid: grafanacloud-demoinfra-prom
|
||||
model:
|
||||
datasource:
|
||||
type: prometheus
|
||||
uid: grafanacloud-demoinfra-prom
|
||||
editorMode: code
|
||||
expr: weather_temp_c{}
|
||||
instant: true
|
||||
intervalMs: 1000
|
||||
legendFormat: __auto
|
||||
maxDataPoints: 43200
|
||||
range: false
|
||||
refId: A
|
||||
refId: A
|
||||
relativeTimeRange:
|
||||
from: 600
|
||||
- datasourceUid: __expr__
|
||||
model:
|
||||
conditions:
|
||||
- evaluator:
|
||||
params:
|
||||
- 0
|
||||
type: lt
|
||||
operator:
|
||||
type: and
|
||||
query:
|
||||
params:
|
||||
- C
|
||||
reducer:
|
||||
params: []
|
||||
type: last
|
||||
type: query
|
||||
datasource:
|
||||
type: __expr__
|
||||
uid: __expr__
|
||||
expression: A
|
||||
intervalMs: 1000
|
||||
maxDataPoints: 43200
|
||||
refId: B
|
||||
type: threshold
|
||||
refId: B
|
||||
relativeTimeRange:
|
||||
from: 600
|
||||
execErrState: Error
|
||||
for: 5m0s
|
||||
noDataState: NoData
|
||||
title: Temperature below zero
|
||||
uid: 4843de5c-4f8a-4af0-9509-23526a04faf8
|
||||
334
fixtures/grafanaalertrulegroup_v1beta1.json
Normal file
334
fixtures/grafanaalertrulegroup_v1beta1.json
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
{
|
||||
"description": "GrafanaAlertRuleGroup is the Schema for the grafanaalertrulegroups API",
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"description": "APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore 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.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
"type": "string"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object"
|
||||
},
|
||||
"spec": {
|
||||
"description": "GrafanaAlertRuleGroupSpec defines the desired state of GrafanaAlertRuleGroup",
|
||||
"properties": {
|
||||
"allowCrossNamespaceImport": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"editable": {
|
||||
"description": "Whether to enable or disable editing of the alert rule group in Grafana UI",
|
||||
"type": "boolean",
|
||||
"x-kubernetes-validations": [
|
||||
{
|
||||
"message": "Value is immutable",
|
||||
"rule": "self == oldSelf"
|
||||
}
|
||||
]
|
||||
},
|
||||
"folderRef": {
|
||||
"description": "Match GrafanaFolders CRs to infer the uid",
|
||||
"type": "string"
|
||||
},
|
||||
"folderUID": {
|
||||
"description": "UID of the folder containing this rule group\nOverrides the FolderSelector",
|
||||
"type": "string"
|
||||
},
|
||||
"instanceSelector": {
|
||||
"description": "selects Grafanas for import",
|
||||
"properties": {
|
||||
"matchExpressions": {
|
||||
"description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.",
|
||||
"items": {
|
||||
"description": "A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.",
|
||||
"properties": {
|
||||
"key": {
|
||||
"description": "key is the label key that the selector applies to.",
|
||||
"type": "string"
|
||||
},
|
||||
"operator": {
|
||||
"description": "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.",
|
||||
"type": "string"
|
||||
},
|
||||
"values": {
|
||||
"description": "values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"x-kubernetes-list-type": "atomic"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key",
|
||||
"operator"
|
||||
],
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"type": "array",
|
||||
"x-kubernetes-list-type": "atomic"
|
||||
},
|
||||
"matchLabels": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is \"key\", the\noperator is \"In\", and the values array contains only \"value\". The requirements are ANDed.",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"x-kubernetes-map-type": "atomic",
|
||||
"x-kubernetes-validations": [
|
||||
{
|
||||
"message": "Value is immutable",
|
||||
"rule": "self == oldSelf"
|
||||
}
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"interval": {
|
||||
"format": "duration",
|
||||
"pattern": "^([0-9]+(\\.[0-9]+)?(ns|us|\u00b5s|ms|s|m|h))+$",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the alert rule group. If not specified, the resource name will be used.",
|
||||
"type": "string"
|
||||
},
|
||||
"resyncPeriod": {
|
||||
"default": "10m",
|
||||
"format": "duration",
|
||||
"pattern": "^([0-9]+(\\.[0-9]+)?(ns|us|\u00b5s|ms|s|m|h))+$",
|
||||
"type": "string"
|
||||
},
|
||||
"rules": {
|
||||
"items": {
|
||||
"description": "AlertRule defines a specific rule to be evaluated. It is based on the upstream model with some k8s specific type mappings",
|
||||
"properties": {
|
||||
"annotations": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"condition": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"datasourceUid": {
|
||||
"description": "Grafana data source unique identifier; it should be '__expr__' for a Server Side Expression operation.",
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"description": "JSON is the raw JSON query and includes the above properties as well as custom properties.",
|
||||
"x-kubernetes-preserve-unknown-fields": true
|
||||
},
|
||||
"queryType": {
|
||||
"description": "QueryType is an optional identifier for the type of query.\nIt can be used to distinguish different types of queries.",
|
||||
"type": "string"
|
||||
},
|
||||
"refId": {
|
||||
"description": "RefID is the unique identifier of the query, set by the frontend call.",
|
||||
"type": "string"
|
||||
},
|
||||
"relativeTimeRange": {
|
||||
"description": "relative time range",
|
||||
"properties": {
|
||||
"from": {
|
||||
"description": "from",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"to": {
|
||||
"description": "to",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"execErrState": {
|
||||
"enum": [
|
||||
"OK",
|
||||
"Alerting",
|
||||
"Error",
|
||||
"KeepLast"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"for": {
|
||||
"format": "duration",
|
||||
"pattern": "^([0-9]+(\\.[0-9]+)?(ns|us|\u00b5s|ms|s|m|h))+$",
|
||||
"type": "string"
|
||||
},
|
||||
"isPaused": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"labels": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"noDataState": {
|
||||
"enum": [
|
||||
"Alerting",
|
||||
"NoData",
|
||||
"OK",
|
||||
"KeepLast"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"properties": {
|
||||
"group_by": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"group_interval": {
|
||||
"type": "string"
|
||||
},
|
||||
"group_wait": {
|
||||
"type": "string"
|
||||
},
|
||||
"mute_time_intervals": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"receiver": {
|
||||
"type": "string"
|
||||
},
|
||||
"repeat_interval": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"receiver"
|
||||
],
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"title": {
|
||||
"example": "Always firing",
|
||||
"maxLength": 190,
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"uid": {
|
||||
"pattern": "^[a-zA-Z0-9-_]+$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"condition",
|
||||
"data",
|
||||
"execErrState",
|
||||
"for",
|
||||
"noDataState",
|
||||
"title",
|
||||
"uid"
|
||||
],
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"instanceSelector",
|
||||
"interval",
|
||||
"rules"
|
||||
],
|
||||
"type": "object",
|
||||
"x-kubernetes-validations": [
|
||||
{
|
||||
"message": "Only one of FolderUID or FolderRef can be set",
|
||||
"rule": "(has(self.folderUID) && !(has(self.folderRef))) || (has(self.folderRef) && !(has(self.folderUID)))"
|
||||
}
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"status": {
|
||||
"description": "GrafanaAlertRuleGroupStatus defines the observed state of GrafanaAlertRuleGroup",
|
||||
"properties": {
|
||||
"conditions": {
|
||||
"items": {
|
||||
"description": "Condition contains details for one aspect of the current state of this API Resource.",
|
||||
"properties": {
|
||||
"lastTransitionTime": {
|
||||
"description": "lastTransitionTime is the last time the condition transitioned from one status to another.\nThis should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.",
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"description": "message is a human readable message indicating details about the transition.\nThis may be an empty string.",
|
||||
"maxLength": 32768,
|
||||
"type": "string"
|
||||
},
|
||||
"observedGeneration": {
|
||||
"description": "observedGeneration represents the .metadata.generation that the condition was set based upon.\nFor instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date\nwith respect to the current state of the instance.",
|
||||
"format": "int64",
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"reason": {
|
||||
"description": "reason contains a programmatic identifier indicating the reason for the condition's last transition.\nProducers of specific condition types may define expected values and meanings for this field,\nand whether the values are considered a guaranteed API.\nThe value should be a CamelCase string.\nThis field may not be empty.",
|
||||
"maxLength": 1024,
|
||||
"minLength": 1,
|
||||
"pattern": "^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$",
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"description": "status of the condition, one of True, False, Unknown.",
|
||||
"enum": [
|
||||
"True",
|
||||
"False",
|
||||
"Unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "type of condition in CamelCase or in foo.example.com/CamelCase.",
|
||||
"maxLength": 316,
|
||||
"pattern": "^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"lastTransitionTime",
|
||||
"message",
|
||||
"reason",
|
||||
"status",
|
||||
"type"
|
||||
],
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"conditions"
|
||||
],
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
13
fixtures/httpproxy.yaml
Normal file
13
fixtures/httpproxy.yaml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
apiVersion: projectcontour.io/v1
|
||||
kind: HTTPProxy
|
||||
metadata:
|
||||
name: basic
|
||||
spec:
|
||||
virtualhost:
|
||||
fqdn: foo-basic.example.com
|
||||
routes:
|
||||
- conditions:
|
||||
- prefix: /
|
||||
services:
|
||||
- name: s1
|
||||
port: 80
|
||||
118
fixtures/junit.xsd
Normal file
118
fixtures/junit.xsd
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!--
|
||||
Source: https://svn.jenkins-ci.org/trunk/hudson/dtkit/dtkit-format/dtkit-junit-model/src/main/resources/com/thalesgroup/dtkit/junit/model/xsd/junit-4.xsd
|
||||
|
||||
This file available under the terms of the MIT License as follows:
|
||||
|
||||
*******************************************************************************
|
||||
* Copyright (c) 2010 Thales Corporate Services SAS *
|
||||
* *
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy *
|
||||
* of this software and associated documentation files (the "Software"), to deal*
|
||||
* in the Software without restriction, including without limitation the rights *
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell *
|
||||
* copies of the Software, and to permit persons to whom the Software is *
|
||||
* furnished to do so, subject to the following conditions: *
|
||||
* *
|
||||
* The above copyright notice and this permission notice shall be included in *
|
||||
* all copies or substantial portions of the Software. *
|
||||
* *
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE *
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,*
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN *
|
||||
* THE SOFTWARE. *
|
||||
********************************************************************************
|
||||
-->
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
|
||||
<xs:element name="failure">
|
||||
<xs:complexType mixed="true">
|
||||
<xs:attribute name="type" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="message" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
<xs:element name="error">
|
||||
<xs:complexType mixed="true">
|
||||
<xs:attribute name="type" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="message" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
<xs:element name="properties">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element ref="property" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
<xs:element name="property">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
<xs:attribute name="value" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
<xs:element name="skipped" type="xs:string"/>
|
||||
<xs:element name="system-err" type="xs:string"/>
|
||||
<xs:element name="system-out" type="xs:string"/>
|
||||
|
||||
<xs:element name="testcase">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element ref="skipped" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element ref="error" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element ref="failure" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element ref="system-out" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element ref="system-err" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
<xs:attribute name="assertions" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="time" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="classname" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="status" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
<xs:element name="testsuite">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element ref="properties" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element ref="testcase" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element ref="system-out" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element ref="system-err" minOccurs="0" maxOccurs="1"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
<xs:attribute name="tests" type="xs:string" use="required"/>
|
||||
<xs:attribute name="failures" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="errors" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="time" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="disabled" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="skipped" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="timestamp" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="hostname" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="id" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="package" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
<xs:element name="testsuites">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element ref="testsuite" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="time" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="tests" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="failures" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="disabled" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="errors" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
|
||||
</xs:schema>
|
||||
|
|
@ -1,46 +1,34 @@
|
|||
{
|
||||
"apiVersion": "apps/v1beta1",
|
||||
"kind": "Deployment",
|
||||
"metadata": {
|
||||
"name": "nginx-deployment",
|
||||
"namespace": "default"
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 2,
|
||||
"template": {
|
||||
"spec": {
|
||||
"affinity": { },
|
||||
"containers": [
|
||||
{
|
||||
"args": [ ],
|
||||
"command": [ ],
|
||||
"env": [ ],
|
||||
"envFrom": [ ],
|
||||
"image": "nginx:1.7.9",
|
||||
"lifecycle": { },
|
||||
"livenessProbe": { },
|
||||
"name": "nginx",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 80,
|
||||
"name": "http"
|
||||
}
|
||||
],
|
||||
"readinessProbe": { },
|
||||
"resources": { },
|
||||
"securityContext": { },
|
||||
"volumeMounts": [ ]
|
||||
}
|
||||
],
|
||||
"hostMappings": [ ],
|
||||
"imagePullSecrets": [ ],
|
||||
"initContainers": [ ],
|
||||
"nodeSelector": { },
|
||||
"securityContext": { },
|
||||
"tolerations": [ ],
|
||||
"volumes": [ ]
|
||||
}
|
||||
"apiVersion": "v1",
|
||||
"kind": "ReplicationController",
|
||||
"metadata": {
|
||||
"name": "bob"
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 2,
|
||||
"selector": {
|
||||
"app": "nginx"
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"name": "nginx",
|
||||
"labels": {
|
||||
"app": "nginx"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"name": "nginx",
|
||||
"image": "nginx",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
14
go.mod
14
go.mod
|
|
@ -1,12 +1,12 @@
|
|||
module github.com/yannh/kubeconform
|
||||
|
||||
go 1.17
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
github.com/beevik/etree v1.1.0
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
sigs.k8s.io/yaml v1.2.0
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1
|
||||
golang.org/x/text v0.25.0
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
)
|
||||
|
||||
require github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
|
|
|
|||
46
go.sum
46
go.sum
|
|
@ -1,24 +1,26 @@
|
|||
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
|
||||
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
|
|
|
|||
4
pkg/cache/cache.go
vendored
4
pkg/cache/cache.go
vendored
|
|
@ -1,6 +1,6 @@
|
|||
package cache
|
||||
|
||||
type Cache interface {
|
||||
Get(resourceKind, resourceAPIVersion, k8sVersion string) (interface{}, error)
|
||||
Set(resourceKind, resourceAPIVersion, k8sVersion string, schema interface{}) error
|
||||
Get(key string) (any, error)
|
||||
Set(key string, schema any) error
|
||||
}
|
||||
|
|
|
|||
18
pkg/cache/inmemory.go
vendored
18
pkg/cache/inmemory.go
vendored
|
|
@ -10,26 +10,21 @@ import (
|
|||
// - This cache caches the parsed Schemas
|
||||
type inMemory struct {
|
||||
sync.RWMutex
|
||||
schemas map[string]interface{}
|
||||
schemas map[string]any
|
||||
}
|
||||
|
||||
// New creates a new cache for downloaded schemas
|
||||
func NewInMemoryCache() Cache {
|
||||
return &inMemory{
|
||||
schemas: map[string]interface{}{},
|
||||
schemas: make(map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
func key(resourceKind, resourceAPIVersion, k8sVersion string) string {
|
||||
return fmt.Sprintf("%s-%s-%s", resourceKind, resourceAPIVersion, k8sVersion)
|
||||
}
|
||||
|
||||
// Get retrieves the JSON schema given a resource signature
|
||||
func (c *inMemory) Get(resourceKind, resourceAPIVersion, k8sVersion string) (interface{}, error) {
|
||||
k := key(resourceKind, resourceAPIVersion, k8sVersion)
|
||||
func (c *inMemory) Get(key string) (any, error) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
schema, ok := c.schemas[k]
|
||||
schema, ok := c.schemas[key]
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("schema not found in in-memory cache")
|
||||
|
|
@ -39,11 +34,10 @@ func (c *inMemory) Get(resourceKind, resourceAPIVersion, k8sVersion string) (int
|
|||
}
|
||||
|
||||
// Set adds a JSON schema to the schema cache
|
||||
func (c *inMemory) Set(resourceKind, resourceAPIVersion, k8sVersion string, schema interface{}) error {
|
||||
k := key(resourceKind, resourceAPIVersion, k8sVersion)
|
||||
func (c *inMemory) Set(key string, schema any) error {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.schemas[k] = schema
|
||||
c.schemas[key] = schema
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
24
pkg/cache/ondisk.go
vendored
24
pkg/cache/ondisk.go
vendored
|
|
@ -1,10 +1,9 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
|
|
@ -22,27 +21,32 @@ func NewOnDiskCache(cache string) Cache {
|
|||
}
|
||||
}
|
||||
|
||||
func cachePath(folder, resourceKind, resourceAPIVersion, k8sVersion string) string {
|
||||
hash := md5.Sum([]byte(fmt.Sprintf("%s-%s-%s", resourceKind, resourceAPIVersion, k8sVersion)))
|
||||
func cachePath(folder, key string) string {
|
||||
hash := sha256.Sum256([]byte(key))
|
||||
return path.Join(folder, hex.EncodeToString(hash[:]))
|
||||
}
|
||||
|
||||
// Get retrieves the JSON schema given a resource signature
|
||||
func (c *onDisk) Get(resourceKind, resourceAPIVersion, k8sVersion string) (interface{}, error) {
|
||||
func (c *onDisk) Get(key string) (any, error) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
|
||||
f, err := os.Open(cachePath(c.folder, resourceKind, resourceAPIVersion, k8sVersion))
|
||||
f, err := os.Open(cachePath(c.folder, key))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return ioutil.ReadAll(f)
|
||||
return io.ReadAll(f)
|
||||
}
|
||||
|
||||
// Set adds a JSON schema to the schema cache
|
||||
func (c *onDisk) Set(resourceKind, resourceAPIVersion, k8sVersion string, schema interface{}) error {
|
||||
func (c *onDisk) Set(key string, schema any) error {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
return ioutil.WriteFile(cachePath(c.folder, resourceKind, resourceAPIVersion, k8sVersion), schema.([]byte), 0644)
|
||||
|
||||
if _, err := os.Stat(cachePath(c.folder, key)); os.IsNotExist(err) {
|
||||
return os.WriteFile(cachePath(c.folder, key), schema.([]byte), 0644)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,28 +4,29 @@ import (
|
|||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Cache string
|
||||
CPUProfileFile string
|
||||
ExitOnError bool
|
||||
Files []string
|
||||
SchemaLocations []string
|
||||
SkipTLS bool
|
||||
SkipKinds map[string]struct{}
|
||||
RejectKinds map[string]struct{}
|
||||
OutputFormat string
|
||||
KubernetesVersion string
|
||||
NumberOfWorkers int
|
||||
Summary bool
|
||||
Strict bool
|
||||
Verbose bool
|
||||
IgnoreMissingSchemas bool
|
||||
IgnoreFilenamePatterns []string
|
||||
Help bool
|
||||
Version bool
|
||||
Cache string `yaml:"cache" json:"cache"`
|
||||
Debug bool `yaml:"debug" json:"debug"`
|
||||
ExitOnError bool `yaml:"exitOnError" json:"exitOnError"`
|
||||
Files []string `yaml:"files" json:"files"`
|
||||
Help bool `yaml:"help" json:"help"`
|
||||
IgnoreFilenamePatterns []string `yaml:"ignoreFilenamePatterns" json:"ignoreFilenamePatterns"`
|
||||
IgnoreMissingSchemas bool `yaml:"ignoreMissingSchemas" json:"ignoreMissingSchemas"`
|
||||
KubernetesVersion k8sVersionValue `yaml:"kubernetesVersion" json:"kubernetesVersion"`
|
||||
NumberOfWorkers int `yaml:"numberOfWorkers" json:"numberOfWorkers"`
|
||||
OutputFormat string `yaml:"output" json:"output"`
|
||||
RejectKinds map[string]struct{} `yaml:"reject" json:"reject"`
|
||||
SchemaLocations []string `yaml:"schemaLocations" json:"schemaLocations"`
|
||||
SkipKinds map[string]struct{} `yaml:"skip" json:"skip"`
|
||||
SkipTLS bool `yaml:"insecureSkipTLSVerify" json:"insecureSkipTLSVerify"`
|
||||
Strict bool `yaml:"strict" json:"strict"`
|
||||
Summary bool `yaml:"summary" json:"summary"`
|
||||
Verbose bool `yaml:"verbose" json:"verbose"`
|
||||
Version bool `yaml:"version" json:"version"`
|
||||
}
|
||||
|
||||
type arrayParam []string
|
||||
|
|
@ -39,11 +40,30 @@ func (ap *arrayParam) Set(value string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
type k8sVersionValue string
|
||||
|
||||
func (kv *k8sVersionValue) String() string {
|
||||
return string(*kv)
|
||||
}
|
||||
|
||||
func (kv k8sVersionValue) MarshalText() ([]byte, error) {
|
||||
return []byte(kv), nil
|
||||
}
|
||||
|
||||
func (kv *k8sVersionValue) UnmarshalText(v []byte) error {
|
||||
if ok, _ := regexp.MatchString(`^(master|\d+\.\d+\.\d+)$`, string(v)); ok != true {
|
||||
return fmt.Errorf("%v is not a valid version. Valid values are \"master\" (default) or full version x.y.z (e.g. \"1.27.2\")", string(v))
|
||||
}
|
||||
*kv = k8sVersionValue(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func splitCSV(csvStr string) map[string]struct{} {
|
||||
splitValues := strings.Split(csvStr, ",")
|
||||
valuesMap := map[string]struct{}{}
|
||||
|
||||
for _, kind := range splitValues {
|
||||
kind = strings.TrimSpace(kind)
|
||||
if len(kind) > 0 {
|
||||
valuesMap[kind] = struct{}{}
|
||||
}
|
||||
|
|
@ -56,28 +76,28 @@ func splitCSV(csvStr string) map[string]struct{} {
|
|||
func FromFlags(progName string, args []string) (Config, string, error) {
|
||||
var schemaLocationsParam, ignoreFilenamePatterns arrayParam
|
||||
var skipKindsCSV, rejectKindsCSV string
|
||||
flags := flag.NewFlagSet(progName, flag.ExitOnError)
|
||||
flags := flag.NewFlagSet(progName, flag.ContinueOnError)
|
||||
var buf bytes.Buffer
|
||||
flags.SetOutput(&buf)
|
||||
|
||||
c := Config{}
|
||||
c.Files = []string{}
|
||||
|
||||
flags.StringVar(&c.KubernetesVersion, "kubernetes-version", "master", "version of Kubernetes to validate against, e.g.: 1.18.0")
|
||||
flags.TextVar(&c.KubernetesVersion, "kubernetes-version", k8sVersionValue("master"), "version of Kubernetes to validate against, e.g.: 1.18.0")
|
||||
flags.Var(&schemaLocationsParam, "schema-location", "override schemas location search path (can be specified multiple times)")
|
||||
flags.StringVar(&skipKindsCSV, "skip", "", "comma-separated list of kinds to ignore")
|
||||
flags.StringVar(&rejectKindsCSV, "reject", "", "comma-separated list of kinds to reject")
|
||||
flags.StringVar(&skipKindsCSV, "skip", "", "comma-separated list of kinds or GVKs to ignore")
|
||||
flags.StringVar(&rejectKindsCSV, "reject", "", "comma-separated list of kinds or GVKs to reject")
|
||||
flags.BoolVar(&c.Debug, "debug", false, "print debug information")
|
||||
flags.BoolVar(&c.ExitOnError, "exit-on-error", false, "immediately stop execution when the first error is encountered")
|
||||
flags.BoolVar(&c.IgnoreMissingSchemas, "ignore-missing-schemas", false, "skip files with missing schemas instead of failing")
|
||||
flags.Var(&ignoreFilenamePatterns, "ignore-filename-pattern", "regular expression specifying paths to ignore (can be specified multiple times)")
|
||||
flags.BoolVar(&c.Summary, "summary", false, "print a summary at the end (ignored for junit output)")
|
||||
flags.IntVar(&c.NumberOfWorkers, "n", 4, "number of goroutines to run concurrently")
|
||||
flags.BoolVar(&c.Strict, "strict", false, "disallow additional properties not in schema")
|
||||
flags.StringVar(&c.OutputFormat, "output", "text", "output format - json, junit, tap, text")
|
||||
flags.BoolVar(&c.Strict, "strict", false, "disallow additional properties not in schema or duplicated keys")
|
||||
flags.StringVar(&c.OutputFormat, "output", "text", "output format - json, junit, pretty, tap, text")
|
||||
flags.BoolVar(&c.Verbose, "verbose", false, "print results for all resources (ignored for tap and junit output)")
|
||||
flags.BoolVar(&c.SkipTLS, "insecure-skip-tls-verify", false, "disable verification of the server's SSL certificate. This will make your HTTPS connections insecure")
|
||||
flags.StringVar(&c.Cache, "cache", "", "cache schemas downloaded via HTTP to this folder")
|
||||
flags.StringVar(&c.CPUProfileFile, "cpu-prof", "", "debug - log CPU profiling to file")
|
||||
flags.BoolVar(&c.Help, "h", false, "show help information")
|
||||
flags.BoolVar(&c.Version, "v", false, "show version information")
|
||||
flags.Usage = func() {
|
||||
|
|
|
|||
|
|
@ -95,6 +95,30 @@ func TestFromFlags(t *testing.T) {
|
|||
RejectKinds: map[string]struct{}{},
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"-skip", "a, b, c"},
|
||||
Config{
|
||||
Files: []string{},
|
||||
KubernetesVersion: "master",
|
||||
NumberOfWorkers: 4,
|
||||
OutputFormat: "text",
|
||||
SchemaLocations: nil,
|
||||
SkipKinds: map[string]struct{}{"a": {}, "b": {}, "c": {}},
|
||||
RejectKinds: map[string]struct{}{},
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"-skip", "a,b, c"},
|
||||
Config{
|
||||
Files: []string{},
|
||||
KubernetesVersion: "master",
|
||||
NumberOfWorkers: 4,
|
||||
OutputFormat: "text",
|
||||
SchemaLocations: nil,
|
||||
SkipKinds: map[string]struct{}{"a": {}, "b": {}, "c": {}},
|
||||
RejectKinds: map[string]struct{}{},
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"-summary", "-verbose", "file1", "file2"},
|
||||
Config{
|
||||
|
|
@ -112,9 +136,10 @@ func TestFromFlags(t *testing.T) {
|
|||
{
|
||||
[]string{"-cache", "cache", "-ignore-missing-schemas", "-kubernetes-version", "1.16.0", "-n", "2", "-output", "json",
|
||||
"-schema-location", "folder", "-schema-location", "anotherfolder", "-skip", "kinda,kindb", "-strict",
|
||||
"-reject", "kindc,kindd", "-summary", "-verbose", "file1", "file2"},
|
||||
"-reject", "kindc,kindd", "-summary", "-debug", "-verbose", "file1", "file2"},
|
||||
Config{
|
||||
Cache: "cache",
|
||||
Debug: true,
|
||||
Files: []string{"file1", "file2"},
|
||||
IgnoreMissingSchemas: true,
|
||||
KubernetesVersion: "1.16.0",
|
||||
|
|
|
|||
65
pkg/loader/file.go
Normal file
65
pkg/loader/file.go
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
package loader
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/santhosh-tekuri/jsonschema/v6"
|
||||
"github.com/yannh/kubeconform/pkg/cache"
|
||||
"io"
|
||||
gourl "net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FileLoader loads json file url.
|
||||
type FileLoader struct {
|
||||
cache cache.Cache
|
||||
}
|
||||
|
||||
func (l FileLoader) Load(url string) (any, error) {
|
||||
path, err := l.ToFile(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
msg := fmt.Sprintf("could not open file %s", path)
|
||||
return nil, NewNotFoundError(errors.New(msg))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
content, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return jsonschema.UnmarshalJSON(bytes.NewReader(content))
|
||||
}
|
||||
|
||||
// ToFile is helper method to convert file url to file path.
|
||||
func (l FileLoader) ToFile(url string) (string, error) {
|
||||
u, err := gourl.Parse(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if u.Scheme != "file" {
|
||||
return url, nil
|
||||
}
|
||||
path := u.Path
|
||||
if runtime.GOOS == "windows" {
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
path = filepath.FromSlash(path)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func NewFileLoader() *FileLoader {
|
||||
return &FileLoader{}
|
||||
}
|
||||
85
pkg/loader/http.go
Normal file
85
pkg/loader/http.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
package loader
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
"github.com/santhosh-tekuri/jsonschema/v6"
|
||||
"github.com/yannh/kubeconform/pkg/cache"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type HTTPURLLoader struct {
|
||||
client http.Client
|
||||
cache cache.Cache
|
||||
}
|
||||
|
||||
func (l *HTTPURLLoader) Load(url string) (any, error) {
|
||||
if l.cache != nil {
|
||||
if cached, err := l.cache.Get(url); err == nil {
|
||||
return jsonschema.UnmarshalJSON(bytes.NewReader(cached.([]byte)))
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := l.client.Get(url)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("failed downloading schema at %s: %s", url, err)
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
msg := fmt.Sprintf("could not find schema at %s", url)
|
||||
return nil, NewNotFoundError(errors.New(msg))
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
msg := fmt.Sprintf("error while downloading schema at %s - received HTTP status %d", url, resp.StatusCode)
|
||||
return nil, fmt.Errorf("%s", msg)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("failed parsing schema from %s: %s", url, err)
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
|
||||
if l.cache != nil {
|
||||
if err = l.cache.Set(url, body); err != nil {
|
||||
return nil, fmt.Errorf("failed to write cache to disk: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
s, err := jsonschema.UnmarshalJSON(bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, NewNonJSONResponseError(err)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func NewHTTPURLLoader(skipTLS bool, cache cache.Cache) (*HTTPURLLoader, error) {
|
||||
transport := &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 3 * time.Second,
|
||||
DisableCompression: true,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
|
||||
if skipTLS {
|
||||
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
|
||||
// retriable http client
|
||||
retryClient := retryablehttp.NewClient()
|
||||
retryClient.RetryMax = 2
|
||||
retryClient.HTTPClient = &http.Client{Transport: transport}
|
||||
retryClient.Logger = nil
|
||||
|
||||
httpLoader := HTTPURLLoader{client: *retryClient.StandardClient(), cache: cache}
|
||||
return &httpLoader, nil
|
||||
}
|
||||
216
pkg/loader/http_test.go
Normal file
216
pkg/loader/http_test.go
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
package loader
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type mockCache struct {
|
||||
data map[string]any
|
||||
}
|
||||
|
||||
func (m *mockCache) Get(key string) (any, error) {
|
||||
if val, ok := m.data[key]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return nil, errors.New("cache miss")
|
||||
}
|
||||
|
||||
func (m *mockCache) Set(key string, value any) error {
|
||||
m.data[key] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
// Test basic functionality of HTTPURLLoader
|
||||
func TestHTTPURLLoader_Load(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mockResponse string
|
||||
mockStatusCode int
|
||||
cacheEnabled bool
|
||||
expectError bool
|
||||
expectCacheHit bool
|
||||
}{
|
||||
{
|
||||
name: "successful load",
|
||||
mockResponse: `{"type": "object"}`,
|
||||
mockStatusCode: http.StatusOK,
|
||||
cacheEnabled: false,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "not found error",
|
||||
mockResponse: "",
|
||||
mockStatusCode: http.StatusNotFound,
|
||||
cacheEnabled: false,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "server error",
|
||||
mockResponse: "",
|
||||
mockStatusCode: http.StatusInternalServerError,
|
||||
cacheEnabled: false,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "cache hit",
|
||||
mockResponse: `{"type": "object"}`,
|
||||
mockStatusCode: http.StatusOK,
|
||||
cacheEnabled: true,
|
||||
expectError: false,
|
||||
expectCacheHit: true,
|
||||
},
|
||||
{
|
||||
name: "Partial response from server",
|
||||
mockResponse: `{"type": "objec`,
|
||||
mockStatusCode: http.StatusOK,
|
||||
cacheEnabled: false,
|
||||
expectError: true,
|
||||
expectCacheHit: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Mock HTTP server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(tt.mockStatusCode)
|
||||
w.Write([]byte(tt.mockResponse))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Create HTTPURLLoader
|
||||
loader := &HTTPURLLoader{
|
||||
client: *server.Client(),
|
||||
cache: nil,
|
||||
}
|
||||
|
||||
if tt.cacheEnabled {
|
||||
loader.cache = &mockCache{data: map[string]any{}}
|
||||
if tt.expectCacheHit {
|
||||
loader.cache.Set(server.URL, []byte(tt.mockResponse))
|
||||
}
|
||||
}
|
||||
|
||||
// Call Load and handle errors
|
||||
res, err := loader.Load(server.URL)
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Errorf("expected error, got nil")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if res == nil {
|
||||
t.Errorf("expected non-nil result, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test basic functionality of HTTPURLLoader
|
||||
func TestHTTPURLLoader_Load_Retries(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
expectError bool
|
||||
expectCallCount int
|
||||
consecutiveFailures int
|
||||
}{
|
||||
{
|
||||
name: "retries on 503",
|
||||
url: "/503",
|
||||
expectError: false,
|
||||
expectCallCount: 2,
|
||||
consecutiveFailures: 2,
|
||||
},
|
||||
{
|
||||
name: "fails when hitting max retries",
|
||||
url: "/503",
|
||||
expectError: true,
|
||||
expectCallCount: 3,
|
||||
consecutiveFailures: 5,
|
||||
},
|
||||
{
|
||||
name: "retry on connection reset",
|
||||
url: "/simulate-reset",
|
||||
expectError: false,
|
||||
expectCallCount: 2,
|
||||
consecutiveFailures: 1,
|
||||
},
|
||||
{
|
||||
name: "retry on connection reset",
|
||||
url: "/simulate-reset",
|
||||
expectError: true,
|
||||
expectCallCount: 3,
|
||||
consecutiveFailures: 5,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ccMutex := &sync.Mutex{}
|
||||
callCounts := map[string]int{}
|
||||
// Mock HTTP server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ccMutex.Lock()
|
||||
callCounts[r.URL.Path]++
|
||||
callCount := callCounts[r.URL.Path]
|
||||
ccMutex.Unlock()
|
||||
|
||||
switch r.URL.Path {
|
||||
case "/simulate-reset":
|
||||
if callCount <= tt.consecutiveFailures {
|
||||
if hj, ok := w.(http.Hijacker); ok {
|
||||
conn, _, err := hj.Hijack()
|
||||
if err != nil {
|
||||
fmt.Printf("Hijacking failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
conn.Close() // Close the connection to simulate a reset
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"type": "object"}`))
|
||||
|
||||
case "/503":
|
||||
s := http.StatusServiceUnavailable
|
||||
if callCount >= tt.consecutiveFailures {
|
||||
s = http.StatusOK
|
||||
}
|
||||
w.WriteHeader(s)
|
||||
w.Write([]byte(`{"type": "object"}`))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Create HTTPURLLoader
|
||||
loader, _ := NewHTTPURLLoader(false, nil)
|
||||
|
||||
fullurl := server.URL + tt.url
|
||||
// Call Load and handle errors
|
||||
_, err := loader.Load(fullurl)
|
||||
if tt.expectError && err == nil {
|
||||
t.Error("expected error, got none")
|
||||
}
|
||||
if !tt.expectError && err != nil {
|
||||
t.Errorf("expected no error, got %v", err)
|
||||
}
|
||||
ccMutex.Lock()
|
||||
if callCounts[tt.url] != tt.expectCallCount {
|
||||
t.Errorf("expected %d calls, got: %d", tt.expectCallCount, callCounts[tt.url])
|
||||
}
|
||||
ccMutex.Unlock()
|
||||
})
|
||||
}
|
||||
}
|
||||
22
pkg/loader/loaders.go
Normal file
22
pkg/loader/loaders.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package loader
|
||||
|
||||
// NotFoundError is returned when the registry does not contain a schema for the resource
|
||||
type NotFoundError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func NewNotFoundError(err error) *NotFoundError {
|
||||
return &NotFoundError{err}
|
||||
}
|
||||
func (e *NotFoundError) Error() string { return e.err.Error() }
|
||||
func (e *NotFoundError) Retryable() bool { return false }
|
||||
|
||||
type NonJSONResponseError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func NewNonJSONResponseError(err error) *NotFoundError {
|
||||
return &NotFoundError{err}
|
||||
}
|
||||
func (e *NonJSONResponseError) Error() string { return e.err.Error() }
|
||||
func (e *NonJSONResponseError) Retryable() bool { return false }
|
||||
|
|
@ -9,12 +9,13 @@ import (
|
|||
)
|
||||
|
||||
type oresult struct {
|
||||
Filename string `json:"filename"`
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Status string `json:"status"`
|
||||
Msg string `json:"msg"`
|
||||
Filename string `json:"filename"`
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Status string `json:"status"`
|
||||
Msg string `json:"msg"`
|
||||
ValidationErrors []validator.ValidationError `json:"validationErrors,omitempty"`
|
||||
}
|
||||
|
||||
type jsono struct {
|
||||
|
|
@ -49,11 +50,15 @@ func (o *jsono) Write(result validator.Result) error {
|
|||
o.nValid++
|
||||
case validator.Invalid:
|
||||
st = "statusInvalid"
|
||||
msg = result.Err.Error()
|
||||
if result.Err != nil {
|
||||
msg = result.Err.Error()
|
||||
}
|
||||
o.nInvalid++
|
||||
case validator.Error:
|
||||
st = "statusError"
|
||||
msg = result.Err.Error()
|
||||
if result.Err != nil {
|
||||
msg = result.Err.Error()
|
||||
}
|
||||
o.nErrors++
|
||||
case validator.Skipped:
|
||||
st = "statusSkipped"
|
||||
|
|
@ -63,7 +68,15 @@ func (o *jsono) Write(result validator.Result) error {
|
|||
|
||||
if o.verbose || (result.Status != validator.Valid && result.Status != validator.Skipped && result.Status != validator.Empty) {
|
||||
sig, _ := result.Resource.Signature()
|
||||
o.results = append(o.results, oresult{Filename: result.Resource.Path, Kind: sig.Kind, Name: sig.Name, Version: sig.Version, Status: st, Msg: msg})
|
||||
o.results = append(o.results, oresult{
|
||||
Filename: result.Resource.Path,
|
||||
Kind: sig.Kind,
|
||||
Name: sig.Name,
|
||||
Version: sig.Version,
|
||||
Status: st,
|
||||
Msg: msg,
|
||||
ValidationErrors: result.ValidationErrors,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -93,6 +93,60 @@ metadata:
|
|||
"skipped": 0
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
"a single invalid deployment, verbose, with summary",
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
[]validator.Result{
|
||||
{
|
||||
Resource: resource.Resource{
|
||||
Path: "deployment.yml",
|
||||
Bytes: []byte(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: "my-app"
|
||||
`),
|
||||
},
|
||||
Status: validator.Invalid,
|
||||
Err: &validator.ValidationError{
|
||||
Path: "foo",
|
||||
Msg: "bar",
|
||||
},
|
||||
ValidationErrors: []validator.ValidationError{
|
||||
{
|
||||
Path: "foo",
|
||||
Msg: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
`{
|
||||
"resources": [
|
||||
{
|
||||
"filename": "deployment.yml",
|
||||
"kind": "Deployment",
|
||||
"name": "my-app",
|
||||
"version": "apps/v1",
|
||||
"status": "statusInvalid",
|
||||
"msg": "bar",
|
||||
"validationErrors": [
|
||||
{
|
||||
"path": "foo",
|
||||
"msg": "bar"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"valid": 0,
|
||||
"invalid": 1,
|
||||
"errors": 0,
|
||||
"skipped": 0
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
} {
|
||||
|
|
|
|||
|
|
@ -33,22 +33,22 @@ type Property struct {
|
|||
}
|
||||
|
||||
type TestSuite struct {
|
||||
XMLName xml.Name `xml:"testsuite"`
|
||||
Properties []*Property `xml:"properties>property,omitempty"`
|
||||
Cases []TestCase `xml:"testcase"`
|
||||
Name string `xml:"name,attr"`
|
||||
Id int `xml:"id,attr"`
|
||||
Tests int `xml:"tests,attr"`
|
||||
Failures int `xml:"failures,attr"`
|
||||
Errors int `xml:"errors,attr"`
|
||||
Disabled int `xml:"disabled,attr"`
|
||||
Skipped int `xml:"skipped,attr"`
|
||||
XMLName xml.Name `xml:"testsuite"`
|
||||
Cases []TestCase `xml:"testcase"`
|
||||
Name string `xml:"name,attr"`
|
||||
Id int `xml:"id,attr"`
|
||||
Tests int `xml:"tests,attr"`
|
||||
Failures int `xml:"failures,attr"`
|
||||
Errors int `xml:"errors,attr"`
|
||||
Disabled int `xml:"disabled,attr"`
|
||||
Skipped int `xml:"skipped,attr"`
|
||||
}
|
||||
|
||||
type TestCase struct {
|
||||
XMLName xml.Name `xml:"testcase"`
|
||||
Name string `xml:"name,attr"`
|
||||
ClassName string `xml:"classname,attr"`
|
||||
Time int `xml:"time,attr"` // Optional, but for Buildkite support https://github.com/yannh/kubeconform/issues/127
|
||||
Skipped *TestCaseSkipped `xml:"skipped,omitempty"`
|
||||
Error *TestCaseError `xml:"error,omitempty"`
|
||||
Failure []TestCaseError `xml:"failure,omitempty"`
|
||||
|
|
@ -65,13 +65,13 @@ type TestCaseError struct {
|
|||
}
|
||||
|
||||
type junito struct {
|
||||
id int
|
||||
w io.Writer
|
||||
withSummary bool
|
||||
verbose bool
|
||||
suites map[string]*TestSuite // map filename to corresponding suite
|
||||
nValid, nInvalid, nErrors, nSkipped int
|
||||
startTime time.Time
|
||||
id int
|
||||
w io.Writer
|
||||
withSummary bool
|
||||
verbose bool
|
||||
suitesIndex map[string]int // map filename to index in suites
|
||||
suites []TestSuite
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
func junitOutput(w io.Writer, withSummary bool, isStdin, verbose bool) Output {
|
||||
|
|
@ -80,30 +80,28 @@ func junitOutput(w io.Writer, withSummary bool, isStdin, verbose bool) Output {
|
|||
w: w,
|
||||
withSummary: withSummary,
|
||||
verbose: verbose,
|
||||
suites: make(map[string]*TestSuite),
|
||||
nValid: 0,
|
||||
nInvalid: 0,
|
||||
nErrors: 0,
|
||||
nSkipped: 0,
|
||||
suites: []TestSuite{},
|
||||
suitesIndex: make(map[string]int),
|
||||
startTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// Write adds a result to the report.
|
||||
func (o *junito) Write(result validator.Result) error {
|
||||
var suite *TestSuite
|
||||
suite, found := o.suites[result.Resource.Path]
|
||||
var suite TestSuite
|
||||
i, found := o.suitesIndex[result.Resource.Path]
|
||||
|
||||
if !found {
|
||||
o.id++
|
||||
suite = &TestSuite{
|
||||
suite = TestSuite{
|
||||
Name: result.Resource.Path,
|
||||
Id: o.id,
|
||||
Tests: 0, Failures: 0, Errors: 0, Disabled: 0, Skipped: 0,
|
||||
Cases: make([]TestCase, 0),
|
||||
Properties: make([]*Property, 0),
|
||||
Cases: make([]TestCase, 0),
|
||||
}
|
||||
o.suites[result.Resource.Path] = suite
|
||||
o.suites = append(o.suites, suite)
|
||||
i = len(o.suites) - 1
|
||||
o.suitesIndex[result.Resource.Path] = i
|
||||
}
|
||||
|
||||
sig, _ := result.Resource.Signature()
|
||||
|
|
@ -118,23 +116,22 @@ func (o *junito) Write(result validator.Result) error {
|
|||
|
||||
switch result.Status {
|
||||
case validator.Valid:
|
||||
o.nValid++
|
||||
case validator.Invalid:
|
||||
o.nInvalid++
|
||||
o.suites[i].Failures++
|
||||
failure := TestCaseError{Message: result.Err.Error()}
|
||||
testCase.Failure = append(testCase.Failure, failure)
|
||||
case validator.Error:
|
||||
o.nErrors++
|
||||
o.suites[i].Errors++
|
||||
testCase.Error = &TestCaseError{Message: result.Err.Error()}
|
||||
case validator.Skipped:
|
||||
testCase.Skipped = &TestCaseSkipped{}
|
||||
o.nSkipped++
|
||||
o.suites[i].Skipped++
|
||||
case validator.Empty:
|
||||
return nil
|
||||
}
|
||||
|
||||
suite.Tests++
|
||||
suite.Cases = append(suite.Cases, testCase)
|
||||
o.suites[i].Tests++
|
||||
o.suites[i].Cases = append(o.suites[i].Cases, testCase)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -143,19 +140,33 @@ func (o *junito) Write(result validator.Result) error {
|
|||
func (o *junito) Flush() error {
|
||||
runtime := time.Now().Sub(o.startTime)
|
||||
|
||||
var suites = make([]TestSuite, 0)
|
||||
totalValid := 0
|
||||
totalInvalid := 0
|
||||
totalErrors := 0
|
||||
totalSkipped := 0
|
||||
|
||||
for _, suite := range o.suites {
|
||||
suites = append(suites, *suite)
|
||||
for _, tCase := range suite.Cases {
|
||||
if tCase.Error != nil {
|
||||
totalErrors++
|
||||
} else if tCase.Skipped != nil {
|
||||
totalSkipped++
|
||||
} else if len(tCase.Failure) > 0 {
|
||||
totalInvalid++
|
||||
} else {
|
||||
totalValid++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root := TestSuiteCollection{
|
||||
Name: "kubeconform",
|
||||
Time: runtime.Seconds(),
|
||||
Tests: o.nValid + o.nInvalid + o.nErrors + o.nSkipped,
|
||||
Failures: o.nInvalid,
|
||||
Errors: o.nErrors,
|
||||
Disabled: o.nSkipped,
|
||||
Suites: suites,
|
||||
Tests: totalValid + totalInvalid + totalErrors + totalSkipped,
|
||||
Failures: totalInvalid,
|
||||
Errors: totalErrors,
|
||||
Disabled: totalSkipped,
|
||||
Suites: o.suites,
|
||||
}
|
||||
|
||||
// 2-space indentation
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package output
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
|
|
@ -48,8 +49,7 @@ metadata:
|
|||
},
|
||||
"<testsuites name=\"kubeconform\" time=\"\" tests=\"1\" failures=\"0\" disabled=\"0\" errors=\"0\">\n" +
|
||||
" <testsuite name=\"deployment.yml\" id=\"1\" tests=\"1\" failures=\"0\" errors=\"0\" disabled=\"0\" skipped=\"0\">\n" +
|
||||
" <properties></properties>\n" +
|
||||
" <testcase name=\"my-app\" classname=\"Deployment@apps/v1\"></testcase>\n" +
|
||||
" <testcase name=\"my-app\" classname=\"Deployment@apps/v1\" time=\"\"></testcase>\n" +
|
||||
" </testsuite>\n" +
|
||||
"</testsuites>\n",
|
||||
},
|
||||
|
|
@ -82,8 +82,68 @@ metadata:
|
|||
},
|
||||
"<testsuites name=\"kubeconform\" time=\"\" tests=\"1\" failures=\"0\" disabled=\"0\" errors=\"0\">\n" +
|
||||
" <testsuite name=\"deployment.yml\" id=\"1\" tests=\"1\" failures=\"0\" errors=\"0\" disabled=\"0\" skipped=\"0\">\n" +
|
||||
" <properties></properties>\n" +
|
||||
" <testcase name=\"my-app\" classname=\"Deployment@apps/v1\"></testcase>\n" +
|
||||
" <testcase name=\"my-app\" classname=\"Deployment@apps/v1\" time=\"\"></testcase>\n" +
|
||||
" </testsuite>\n" +
|
||||
"</testsuites>\n",
|
||||
},
|
||||
{
|
||||
"one error, one invalid",
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
[]validator.Result{
|
||||
{
|
||||
Resource: resource.Resource{
|
||||
Path: "deployment.yml",
|
||||
Bytes: []byte(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: "my-app"
|
||||
`),
|
||||
},
|
||||
Status: validator.Error,
|
||||
Err: fmt.Errorf("error validating deployment.yml"),
|
||||
},
|
||||
{
|
||||
Resource: resource.Resource{
|
||||
Path: "deployment2.yml",
|
||||
Bytes: []byte(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: "my-app"
|
||||
`),
|
||||
},
|
||||
Status: validator.Error,
|
||||
Err: fmt.Errorf("error validating deployment.yml"),
|
||||
},
|
||||
{
|
||||
Resource: resource.Resource{
|
||||
Path: "deployment3.yml",
|
||||
Bytes: []byte(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: "my-app"
|
||||
`),
|
||||
},
|
||||
Status: validator.Invalid,
|
||||
Err: fmt.Errorf("deployment3.yml is invalid"),
|
||||
},
|
||||
},
|
||||
"<testsuites name=\"kubeconform\" time=\"\" tests=\"3\" failures=\"1\" disabled=\"0\" errors=\"2\">\n" +
|
||||
" <testsuite name=\"deployment.yml\" id=\"1\" tests=\"1\" failures=\"0\" errors=\"1\" disabled=\"0\" skipped=\"0\">\n" +
|
||||
" <testcase name=\"my-app\" classname=\"Deployment@apps/v1\" time=\"\">\n" +
|
||||
" <error message=\"error validating deployment.yml\" type=\"\"></error>\n" +
|
||||
" </testcase>\n" +
|
||||
" </testsuite>\n" +
|
||||
" <testsuite name=\"deployment2.yml\" id=\"2\" tests=\"1\" failures=\"0\" errors=\"1\" disabled=\"0\" skipped=\"0\">\n" +
|
||||
" <testcase name=\"my-app\" classname=\"Deployment@apps/v1\" time=\"\">\n" +
|
||||
" <error message=\"error validating deployment.yml\" type=\"\"></error>\n" +
|
||||
" </testcase>\n" +
|
||||
" </testsuite>\n" +
|
||||
" <testsuite name=\"deployment3.yml\" id=\"3\" tests=\"1\" failures=\"1\" errors=\"0\" disabled=\"0\" skipped=\"0\">\n" +
|
||||
" <testcase name=\"my-app\" classname=\"Deployment@apps/v1\" time=\"\">\n" +
|
||||
" <failure message=\"deployment3.yml is invalid\" type=\"\"></failure>\n" +
|
||||
" </testcase>\n" +
|
||||
" </testsuite>\n" +
|
||||
"</testsuites>\n",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package output
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"io"
|
||||
|
||||
"github.com/yannh/kubeconform/pkg/validator"
|
||||
)
|
||||
|
|
@ -12,19 +12,19 @@ type Output interface {
|
|||
Flush() error
|
||||
}
|
||||
|
||||
func New(outputFormat string, printSummary, isStdin, verbose bool) (Output, error) {
|
||||
w := os.Stdout
|
||||
|
||||
func New(w io.Writer, outputFormat string, printSummary, isStdin, verbose bool) (Output, error) {
|
||||
switch {
|
||||
case outputFormat == "json":
|
||||
return jsonOutput(w, printSummary, isStdin, verbose), nil
|
||||
case outputFormat == "junit":
|
||||
return junitOutput(w, printSummary, isStdin, verbose), nil
|
||||
case outputFormat == "pretty":
|
||||
return prettyOutput(w, printSummary, isStdin, verbose), nil
|
||||
case outputFormat == "tap":
|
||||
return tapOutput(w, printSummary, isStdin, verbose), nil
|
||||
case outputFormat == "text":
|
||||
return textOutput(w, printSummary, isStdin, verbose), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("`outputFormat` must be 'json', 'tap' or 'text'")
|
||||
return nil, fmt.Errorf("'outputFormat' must be 'json', 'junit', 'pretty', 'tap' or 'text'")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
109
pkg/output/pretty.go
Normal file
109
pkg/output/pretty.go
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/yannh/kubeconform/pkg/validator"
|
||||
)
|
||||
|
||||
type prettyo struct {
|
||||
sync.Mutex
|
||||
w io.Writer
|
||||
withSummary bool
|
||||
isStdin bool
|
||||
verbose bool
|
||||
files map[string]bool
|
||||
nValid, nInvalid, nErrors, nSkipped int
|
||||
}
|
||||
|
||||
// Text will output the results of the validation as a texto
|
||||
func prettyOutput(w io.Writer, withSummary, isStdin, verbose bool) Output {
|
||||
return &prettyo{
|
||||
w: w,
|
||||
withSummary: withSummary,
|
||||
isStdin: isStdin,
|
||||
verbose: verbose,
|
||||
files: map[string]bool{},
|
||||
nValid: 0,
|
||||
nInvalid: 0,
|
||||
nErrors: 0,
|
||||
nSkipped: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *prettyo) Write(result validator.Result) error {
|
||||
checkmark := "\u2714"
|
||||
multiplicationSign := "\u2716"
|
||||
reset := "\033[0m"
|
||||
cRed := "\033[31m"
|
||||
cGreen := "\033[32m"
|
||||
cYellow := "\033[33m"
|
||||
|
||||
o.Lock()
|
||||
defer o.Unlock()
|
||||
|
||||
var err error
|
||||
|
||||
sig, _ := result.Resource.Signature()
|
||||
|
||||
o.files[result.Resource.Path] = true
|
||||
switch result.Status {
|
||||
case validator.Valid:
|
||||
if o.verbose {
|
||||
fmt.Fprintf(o.w, "%s%s%s %s: %s%s %s is valid%s\n", cGreen, checkmark, reset, result.Resource.Path, cGreen, sig.Kind, sig.Name, reset)
|
||||
}
|
||||
o.nValid++
|
||||
case validator.Invalid:
|
||||
fmt.Fprintf(o.w, "%s%s%s %s: %s%s %s is invalid: %s%s\n", cRed, multiplicationSign, reset, result.Resource.Path, cRed, sig.Kind, sig.Name, result.Err.Error(), reset)
|
||||
|
||||
o.nInvalid++
|
||||
case validator.Error:
|
||||
fmt.Fprintf(o.w, "%s%s%s %s: ", cRed, multiplicationSign, reset, result.Resource.Path)
|
||||
if sig.Kind != "" && sig.Name != "" {
|
||||
fmt.Fprintf(o.w, "%s%s failed validation: %s %s%s\n", cRed, sig.Kind, sig.Name, result.Err.Error(), reset)
|
||||
} else {
|
||||
fmt.Fprintf(o.w, "%sfailed validation: %s %s%s\n", cRed, sig.Name, result.Err.Error(), reset)
|
||||
}
|
||||
o.nErrors++
|
||||
case validator.Skipped:
|
||||
if o.verbose {
|
||||
fmt.Fprintf(o.w, "%s-%s %s: ", cYellow, reset, result.Resource.Path)
|
||||
if sig.Kind != "" && sig.Name != "" {
|
||||
fmt.Fprintf(o.w, "%s%s %s skipped%s\n", cYellow, sig.Kind, sig.Name, reset)
|
||||
} else if sig.Kind != "" {
|
||||
fmt.Fprintf(o.w, "%s%s skipped%s\n", cYellow, sig.Kind, reset)
|
||||
} else {
|
||||
fmt.Fprintf(o.w, "%sskipped%s\n", cYellow, reset)
|
||||
}
|
||||
}
|
||||
o.nSkipped++
|
||||
case validator.Empty: // sent to ensure we count the filename as parsed
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (o *prettyo) Flush() error {
|
||||
var err error
|
||||
if o.withSummary {
|
||||
nFiles := len(o.files)
|
||||
nResources := o.nValid + o.nInvalid + o.nErrors + o.nSkipped
|
||||
resourcesPlural := ""
|
||||
if nResources > 1 {
|
||||
resourcesPlural = "s"
|
||||
}
|
||||
filesPlural := ""
|
||||
if nFiles > 1 {
|
||||
filesPlural = "s"
|
||||
}
|
||||
if o.isStdin {
|
||||
_, err = fmt.Fprintf(o.w, "Summary: %d resource%s found parsing stdin - Valid: %d, Invalid: %d, Errors: %d, Skipped: %d\n", nResources, resourcesPlural, o.nValid, o.nInvalid, o.nErrors, o.nSkipped)
|
||||
} else {
|
||||
_, err = fmt.Fprintf(o.w, "Summary: %d resource%s found in %d file%s - Valid: %d, Invalid: %d, Errors: %d, Skipped: %d\n", nResources, resourcesPlural, nFiles, filesPlural, o.nValid, o.nInvalid, o.nErrors, o.nSkipped)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
84
pkg/output/pretty_test.go
Normal file
84
pkg/output/pretty_test.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/yannh/kubeconform/pkg/resource"
|
||||
"github.com/yannh/kubeconform/pkg/validator"
|
||||
)
|
||||
|
||||
func TestPrettyTextWrite(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
withSummary bool
|
||||
isStdin bool
|
||||
verbose bool
|
||||
results []validator.Result
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
"a single deployment, no summary, no verbose",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
[]validator.Result{},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"a single deployment, summary, no verbose",
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
[]validator.Result{
|
||||
{
|
||||
Resource: resource.Resource{
|
||||
Path: "deployment.yml",
|
||||
Bytes: []byte(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: "my-app"
|
||||
`),
|
||||
},
|
||||
Status: validator.Valid,
|
||||
Err: nil,
|
||||
},
|
||||
},
|
||||
"Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0, Skipped: 0\n",
|
||||
},
|
||||
{
|
||||
"a single deployment, verbose, with summary",
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
[]validator.Result{
|
||||
{
|
||||
Resource: resource.Resource{
|
||||
Path: "deployment.yml",
|
||||
Bytes: []byte(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: "my-app"
|
||||
`),
|
||||
},
|
||||
Status: validator.Valid,
|
||||
Err: nil,
|
||||
},
|
||||
},
|
||||
"\033[32m✔\033[0m deployment.yml: \033[32mDeployment my-app is valid\033[0m\n" +
|
||||
"Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0, Skipped: 0\n",
|
||||
},
|
||||
} {
|
||||
w := new(bytes.Buffer)
|
||||
o := prettyOutput(w, testCase.withSummary, testCase.isStdin, testCase.verbose)
|
||||
|
||||
for _, res := range testCase.results {
|
||||
o.Write(res)
|
||||
}
|
||||
o.Flush()
|
||||
|
||||
if w.String() != testCase.expect {
|
||||
t.Errorf("%s - expected, but got:\n%s\n%s\n", testCase.name, testCase.expect, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,98 +1,34 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/yannh/kubeconform/pkg/cache"
|
||||
"github.com/santhosh-tekuri/jsonschema/v6"
|
||||
)
|
||||
|
||||
type httpGetter interface {
|
||||
Get(url string) (resp *http.Response, err error)
|
||||
}
|
||||
|
||||
// SchemaRegistry is a file repository (local or remote) that contains JSON schemas for Kubernetes resources
|
||||
type SchemaRegistry struct {
|
||||
c httpGetter
|
||||
schemaPathTemplate string
|
||||
cache cache.Cache
|
||||
strict bool
|
||||
debug bool
|
||||
loader jsonschema.URLLoader
|
||||
}
|
||||
|
||||
func newHTTPRegistry(schemaPathTemplate string, cacheFolder string, strict bool, skipTLS bool) (*SchemaRegistry, error) {
|
||||
reghttp := &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 3 * time.Second,
|
||||
DisableCompression: true,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
|
||||
if skipTLS {
|
||||
reghttp.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
|
||||
var filecache cache.Cache = nil
|
||||
if cacheFolder != "" {
|
||||
fi, err := os.Stat(cacheFolder)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed opening cache folder %s: %s", cacheFolder, err)
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return nil, fmt.Errorf("cache folder %s is not a directory", err)
|
||||
}
|
||||
|
||||
filecache = cache.NewOnDiskCache(cacheFolder)
|
||||
}
|
||||
|
||||
func newHTTPRegistry(schemaPathTemplate string, loader jsonschema.URLLoader, strict bool, debug bool) (*SchemaRegistry, error) {
|
||||
return &SchemaRegistry{
|
||||
c: &http.Client{Transport: reghttp},
|
||||
schemaPathTemplate: schemaPathTemplate,
|
||||
cache: filecache,
|
||||
strict: strict,
|
||||
loader: loader,
|
||||
debug: debug,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DownloadSchema downloads the schema for a particular resource from an HTTP server
|
||||
func (r SchemaRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVersion string) ([]byte, error) {
|
||||
func (r SchemaRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVersion string) (string, any, error) {
|
||||
url, err := schemaPath(r.schemaPathTemplate, resourceKind, resourceAPIVersion, k8sVersion, r.strict)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
if r.cache != nil {
|
||||
if b, err := r.cache.Get(resourceKind, resourceAPIVersion, k8sVersion); err == nil {
|
||||
return b.([]byte), nil
|
||||
}
|
||||
}
|
||||
resp, err := r.loader.Load(url)
|
||||
|
||||
resp, err := r.c.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed downloading schema at %s: %s", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, newNotFoundError(fmt.Errorf("no schema found"))
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("error while downloading schema at %s - received HTTP status %d", url, resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed downloading schema at %s: %s", url, err)
|
||||
}
|
||||
|
||||
if r.cache != nil {
|
||||
if err := r.cache.Set(resourceKind, resourceAPIVersion, k8sVersion, body); err != nil {
|
||||
return nil, fmt.Errorf("failed writing schema to cache: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return body, nil
|
||||
return url, resp, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,117 +0,0 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type mockHTTPGetter struct {
|
||||
httpGet func(string) (*http.Response, error)
|
||||
}
|
||||
|
||||
func newMockHTTPGetter(f func(string) (*http.Response, error)) *mockHTTPGetter {
|
||||
return &mockHTTPGetter{
|
||||
httpGet: f,
|
||||
}
|
||||
}
|
||||
func (m mockHTTPGetter) Get(url string) (resp *http.Response, err error) {
|
||||
return m.httpGet(url)
|
||||
}
|
||||
|
||||
func TestDownloadSchema(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
c httpGetter
|
||||
schemaPathTemplate string
|
||||
strict bool
|
||||
resourceKind, resourceAPIVersion, k8sversion string
|
||||
expect []byte
|
||||
expectErr error
|
||||
}{
|
||||
{
|
||||
"error when downloading",
|
||||
newMockHTTPGetter(func(url string) (resp *http.Response, err error) {
|
||||
return nil, fmt.Errorf("failed downloading from registry")
|
||||
}),
|
||||
"http://kubernetesjson.dev",
|
||||
true,
|
||||
"Deployment",
|
||||
"v1",
|
||||
"1.18.0",
|
||||
nil,
|
||||
fmt.Errorf("failed downloading schema at http://kubernetesjson.dev: failed downloading from registry"),
|
||||
},
|
||||
{
|
||||
"getting 404",
|
||||
newMockHTTPGetter(func(url string) (resp *http.Response, err error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: ioutil.NopCloser(strings.NewReader("http response mock body")),
|
||||
}, nil
|
||||
}),
|
||||
"http://kubernetesjson.dev",
|
||||
true,
|
||||
"Deployment",
|
||||
"v1",
|
||||
"1.18.0",
|
||||
nil,
|
||||
fmt.Errorf("no schema found"),
|
||||
},
|
||||
{
|
||||
"getting 503",
|
||||
newMockHTTPGetter(func(url string) (resp *http.Response, err error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusServiceUnavailable,
|
||||
Body: ioutil.NopCloser(strings.NewReader("http response mock body")),
|
||||
}, nil
|
||||
}),
|
||||
"http://kubernetesjson.dev",
|
||||
true,
|
||||
"Deployment",
|
||||
"v1",
|
||||
"1.18.0",
|
||||
nil,
|
||||
fmt.Errorf("error while downloading schema at http://kubernetesjson.dev - received HTTP status 503"),
|
||||
},
|
||||
{
|
||||
"200",
|
||||
newMockHTTPGetter(func(url string) (resp *http.Response, err error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(strings.NewReader("http response mock body")),
|
||||
}, nil
|
||||
}),
|
||||
"http://kubernetesjson.dev",
|
||||
true,
|
||||
"Deployment",
|
||||
"v1",
|
||||
"1.18.0",
|
||||
[]byte("http response mock body"),
|
||||
nil,
|
||||
},
|
||||
} {
|
||||
reg := SchemaRegistry{
|
||||
c: testCase.c,
|
||||
schemaPathTemplate: testCase.schemaPathTemplate,
|
||||
strict: testCase.strict,
|
||||
}
|
||||
|
||||
res, err := reg.DownloadSchema(testCase.resourceKind, testCase.resourceAPIVersion, testCase.k8sversion)
|
||||
if err == nil || testCase.expectErr == nil {
|
||||
if err != testCase.expectErr {
|
||||
t.Errorf("during test '%s': expected error, got:\n%s\n%s\n", testCase.name, testCase.expectErr, err)
|
||||
}
|
||||
} else if err.Error() != testCase.expectErr.Error() {
|
||||
t.Errorf("during test '%s': expected error, got:\n%s\n%s\n", testCase.name, testCase.expectErr, err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(res, testCase.expect) {
|
||||
t.Errorf("during test '%s': expected %s, got %s", testCase.name, testCase.expect, res)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,43 +1,33 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"github.com/santhosh-tekuri/jsonschema/v6"
|
||||
)
|
||||
|
||||
type LocalRegistry struct {
|
||||
pathTemplate string
|
||||
strict bool
|
||||
debug bool
|
||||
loader jsonschema.URLLoader
|
||||
}
|
||||
|
||||
// NewLocalSchemas creates a new "registry", that will serve schemas from files, given a list of schema filenames
|
||||
func newLocalRegistry(pathTemplate string, strict bool) (*LocalRegistry, error) {
|
||||
func newLocalRegistry(pathTemplate string, loader jsonschema.URLLoader, strict bool, debug bool) (*LocalRegistry, error) {
|
||||
return &LocalRegistry{
|
||||
pathTemplate,
|
||||
strict,
|
||||
debug,
|
||||
loader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DownloadSchema retrieves the schema from a file for the resource
|
||||
func (r LocalRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVersion string) ([]byte, error) {
|
||||
func (r LocalRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVersion string) (string, any, error) {
|
||||
schemaFile, err := schemaPath(r.pathTemplate, resourceKind, resourceAPIVersion, k8sVersion, r.strict)
|
||||
if err != nil {
|
||||
return []byte{}, nil
|
||||
}
|
||||
f, err := os.Open(schemaFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, newNotFoundError(fmt.Errorf("no schema found"))
|
||||
}
|
||||
return nil, fmt.Errorf("failed to open schema %s", schemaFile)
|
||||
return schemaFile, []byte{}, nil
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
content, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return content, nil
|
||||
s, err := r.loader.Load(schemaFile)
|
||||
return schemaFile, s, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ package registry
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/yannh/kubeconform/pkg/cache"
|
||||
"github.com/yannh/kubeconform/pkg/loader"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
|
@ -13,25 +16,9 @@ type Manifest struct {
|
|||
|
||||
// Registry is an interface that should be implemented by any source of Kubernetes schemas
|
||||
type Registry interface {
|
||||
DownloadSchema(resourceKind, resourceAPIVersion, k8sVersion string) ([]byte, error)
|
||||
DownloadSchema(resourceKind, resourceAPIVersion, k8sVersion string) (string, any, error)
|
||||
}
|
||||
|
||||
// Retryable indicates whether an error is a temporary or a permanent failure
|
||||
type Retryable interface {
|
||||
IsNotFound() bool
|
||||
}
|
||||
|
||||
// NotFoundError is returned when the registry does not contain a schema for the resource
|
||||
type NotFoundError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func newNotFoundError(err error) *NotFoundError {
|
||||
return &NotFoundError{err}
|
||||
}
|
||||
func (e *NotFoundError) Error() string { return e.err.Error() }
|
||||
func (e *NotFoundError) Retryable() bool { return false }
|
||||
|
||||
func schemaPath(tpl, resourceKind, resourceAPIVersion, k8sVersion string, strict bool) (string, error) {
|
||||
normalisedVersion := k8sVersion
|
||||
if normalisedVersion != "master" {
|
||||
|
|
@ -61,12 +48,14 @@ func schemaPath(tpl, resourceKind, resourceAPIVersion, k8sVersion string, strict
|
|||
StrictSuffix string
|
||||
ResourceKind string
|
||||
ResourceAPIVersion string
|
||||
Group string
|
||||
KindSuffix string
|
||||
}{
|
||||
normalisedVersion,
|
||||
strictSuffix,
|
||||
strings.ToLower(resourceKind),
|
||||
groupParts[len(groupParts)-1],
|
||||
groupParts[0],
|
||||
kindSuffix,
|
||||
}
|
||||
|
||||
|
|
@ -79,7 +68,7 @@ func schemaPath(tpl, resourceKind, resourceAPIVersion, k8sVersion string, strict
|
|||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func New(schemaLocation string, cache string, strict bool, skipTLS bool) (Registry, error) {
|
||||
func New(schemaLocation string, cacheFolder string, strict bool, skipTLS bool, debug bool) (Registry, error) {
|
||||
if schemaLocation == "default" {
|
||||
schemaLocation = "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json"
|
||||
} else if !strings.HasSuffix(schemaLocation, "json") { // If we dont specify a full templated path, we assume the paths of our fork of kubernetes-json-schema
|
||||
|
|
@ -91,9 +80,27 @@ func New(schemaLocation string, cache string, strict bool, skipTLS bool) (Regist
|
|||
return nil, fmt.Errorf("failed initialising schema location registry: %s", err)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(schemaLocation, "http") {
|
||||
return newHTTPRegistry(schemaLocation, cache, strict, skipTLS)
|
||||
var c cache.Cache = nil
|
||||
if cacheFolder != "" {
|
||||
fi, err := os.Stat(cacheFolder)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed opening cache folder %s: %s", cacheFolder, err)
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return nil, fmt.Errorf("cache folder %s is not a directory", err)
|
||||
}
|
||||
|
||||
c = cache.NewOnDiskCache(cacheFolder)
|
||||
}
|
||||
|
||||
return newLocalRegistry(schemaLocation, strict)
|
||||
if strings.HasPrefix(schemaLocation, "http") {
|
||||
httpLoader, err := loader.NewHTTPURLLoader(skipTLS, c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed creating HTTP loader: %s", err)
|
||||
}
|
||||
return newHTTPRegistry(schemaLocation, httpLoader, strict, debug)
|
||||
}
|
||||
|
||||
fileLoader := loader.NewFileLoader()
|
||||
return newLocalRegistry(schemaLocation, fileLoader, strict, debug)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,18 @@ type Signature struct {
|
|||
Kind, Version, Namespace, Name string
|
||||
}
|
||||
|
||||
// GroupVersionKind returns a string with the GVK encoding of a resource signature.
|
||||
// This encoding slightly differs from the Kubernetes upstream implementation
|
||||
// in order to be suitable for being used in the kubeconform command-line arguments.
|
||||
func (sig *Signature) GroupVersionKind() string {
|
||||
return fmt.Sprintf("%s/%s", sig.Version, sig.Kind)
|
||||
}
|
||||
|
||||
// QualifiedName returns a string for a signature in the format version/kind/namespace/name
|
||||
func (sig *Signature) QualifiedName() string {
|
||||
return fmt.Sprintf("%s/%s/%s/%s", sig.Version, sig.Kind, sig.Namespace, sig.Name)
|
||||
}
|
||||
|
||||
// Signature computes a signature for a resource, based on its Kind, Version, Namespace & Name
|
||||
func (res *Resource) Signature() (*Signature, error) {
|
||||
if res.sig != nil {
|
||||
|
|
@ -119,8 +131,3 @@ func (res *Resource) Resources() []Resource {
|
|||
|
||||
return []Resource{*res}
|
||||
}
|
||||
|
||||
// QualifiedName returns a string for a signature in the format version/kind/namespace/name
|
||||
func (sig *Signature) QualifiedName() string {
|
||||
return fmt.Sprintf("%s/%s/%s/%s", sig.Version, sig.Kind, sig.Namespace, sig.Name)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,15 +3,20 @@ package validator
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
jsonschema "github.com/santhosh-tekuri/jsonschema/v6"
|
||||
"github.com/yannh/kubeconform/pkg/cache"
|
||||
"github.com/yannh/kubeconform/pkg/loader"
|
||||
"github.com/yannh/kubeconform/pkg/registry"
|
||||
"github.com/yannh/kubeconform/pkg/resource"
|
||||
|
||||
"github.com/xeipuuv/gojsonschema"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
"io"
|
||||
"os"
|
||||
"sigs.k8s.io/yaml"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Different types of validation results
|
||||
|
|
@ -26,11 +31,21 @@ const (
|
|||
Empty // resource is empty. Note: is triggered for files starting with a --- separator.
|
||||
)
|
||||
|
||||
type ValidationError struct {
|
||||
Path string `json:"path"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
func (ve *ValidationError) Error() string {
|
||||
return ve.Msg
|
||||
}
|
||||
|
||||
// Result contains the details of the result of a resource validation
|
||||
type Result struct {
|
||||
Resource resource.Resource
|
||||
Err error
|
||||
Status Status
|
||||
Resource resource.Resource
|
||||
Err error
|
||||
Status Status
|
||||
ValidationErrors []ValidationError
|
||||
}
|
||||
|
||||
// Validator exposes multiple methods to validate your Kubernetes resources.
|
||||
|
|
@ -43,11 +58,12 @@ type Validator interface {
|
|||
// Opts contains a set of options for the validator.
|
||||
type Opts struct {
|
||||
Cache string // Cache schemas downloaded via HTTP to this folder
|
||||
Debug bool // Debug infos will be print here
|
||||
SkipTLS bool // skip TLS validation when downloading from an HTTP Schema Registry
|
||||
SkipKinds map[string]struct{} // List of resource Kinds to ignore
|
||||
RejectKinds map[string]struct{} // List of resource Kinds to reject
|
||||
KubernetesVersion string // Kubernetes Version - has to match one in https://github.com/instrumenta/kubernetes-json-schema
|
||||
Strict bool // thros an error if resources contain undocumented fields
|
||||
Strict bool // Throws an error if resources contain undocumented fields
|
||||
IgnoreMissingSchemas bool // skip a resource if no schema for that resource can be found
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +77,7 @@ func New(schemaLocations []string, opts Opts) (Validator, error) {
|
|||
|
||||
registries := []registry.Registry{}
|
||||
for _, schemaLocation := range schemaLocations {
|
||||
reg, err := registry.New(schemaLocation, opts.Cache, opts.Strict, opts.SkipTLS)
|
||||
reg, err := registry.New(schemaLocation, opts.Cache, opts.Strict, opts.SkipTLS, opts.Debug)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -79,30 +95,70 @@ func New(schemaLocations []string, opts Opts) (Validator, error) {
|
|||
opts.RejectKinds = map[string]struct{}{}
|
||||
}
|
||||
|
||||
var filecache cache.Cache = nil
|
||||
if opts.Cache != "" {
|
||||
fi, err := os.Stat(opts.Cache)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed opening cache folder %s: %s", opts.Cache, err)
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return nil, fmt.Errorf("cache folder %s is not a directory", err)
|
||||
}
|
||||
|
||||
filecache = cache.NewOnDiskCache(opts.Cache)
|
||||
}
|
||||
|
||||
httpLoader, err := loader.NewHTTPURLLoader(false, filecache)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed creating HTTP loader: %s", err)
|
||||
}
|
||||
|
||||
return &v{
|
||||
opts: opts,
|
||||
schemaDownload: downloadSchema,
|
||||
schemaCache: cache.NewInMemoryCache(),
|
||||
regs: registries,
|
||||
opts: opts,
|
||||
schemaDownload: downloadSchema,
|
||||
schemaMemoryCache: cache.NewInMemoryCache(),
|
||||
regs: registries,
|
||||
loader: jsonschema.SchemeURLLoader{
|
||||
"file": jsonschema.FileLoader{},
|
||||
"http": httpLoader,
|
||||
"https": httpLoader,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type v struct {
|
||||
opts Opts
|
||||
schemaCache cache.Cache
|
||||
schemaDownload func(registries []registry.Registry, kind, version, k8sVersion string) (*gojsonschema.Schema, error)
|
||||
regs []registry.Registry
|
||||
opts Opts
|
||||
schemaDiskCache cache.Cache
|
||||
schemaMemoryCache cache.Cache
|
||||
schemaDownload func(registries []registry.Registry, loader jsonschema.SchemeURLLoader, kind, version, k8sVersion string) (*jsonschema.Schema, error)
|
||||
regs []registry.Registry
|
||||
loader jsonschema.SchemeURLLoader
|
||||
}
|
||||
|
||||
func key(resourceKind, resourceAPIVersion, k8sVersion string) string {
|
||||
return fmt.Sprintf("%s-%s-%s", resourceKind, resourceAPIVersion, k8sVersion)
|
||||
}
|
||||
|
||||
// ValidateResource validates a single resource. This allows to validate
|
||||
// large resource streams using multiple Go Routines.
|
||||
func (val *v) ValidateResource(res resource.Resource) Result {
|
||||
// For backward compatibility reasons when determining whether
|
||||
// a resource should be skipped or rejected we use both
|
||||
// the GVK encoding of the resource signatures (the recommended method
|
||||
// for skipping/rejecting resources) and the raw Kind.
|
||||
|
||||
skip := func(signature resource.Signature) bool {
|
||||
if _, ok := val.opts.SkipKinds[signature.GroupVersionKind()]; ok {
|
||||
return ok
|
||||
}
|
||||
_, ok := val.opts.SkipKinds[signature.Kind]
|
||||
return ok
|
||||
}
|
||||
|
||||
reject := func(signature resource.Signature) bool {
|
||||
if _, ok := val.opts.RejectKinds[signature.GroupVersionKind()]; ok {
|
||||
return ok
|
||||
}
|
||||
_, ok := val.opts.RejectKinds[signature.Kind]
|
||||
return ok
|
||||
}
|
||||
|
|
@ -112,7 +168,12 @@ func (val *v) ValidateResource(res resource.Resource) Result {
|
|||
}
|
||||
|
||||
var r map[string]interface{}
|
||||
if err := yaml.Unmarshal(res.Bytes, &r); err != nil {
|
||||
unmarshaller := yaml.Unmarshal
|
||||
if val.opts.Strict {
|
||||
unmarshaller = yaml.UnmarshalStrict
|
||||
}
|
||||
|
||||
if err := unmarshaller(res.Bytes, &r); err != nil {
|
||||
return Result{Resource: res, Status: Error, Err: fmt.Errorf("error unmarshalling resource: %s", err)}
|
||||
}
|
||||
|
||||
|
|
@ -134,23 +195,23 @@ func (val *v) ValidateResource(res resource.Resource) Result {
|
|||
}
|
||||
|
||||
cached := false
|
||||
var schema *gojsonschema.Schema
|
||||
var schema *jsonschema.Schema
|
||||
|
||||
if val.schemaCache != nil {
|
||||
s, err := val.schemaCache.Get(sig.Kind, sig.Version, val.opts.KubernetesVersion)
|
||||
if val.schemaMemoryCache != nil {
|
||||
s, err := val.schemaMemoryCache.Get(key(sig.Kind, sig.Version, val.opts.KubernetesVersion))
|
||||
if err == nil {
|
||||
cached = true
|
||||
schema = s.(*gojsonschema.Schema)
|
||||
schema = s.(*jsonschema.Schema)
|
||||
}
|
||||
}
|
||||
|
||||
if !cached {
|
||||
if schema, err = val.schemaDownload(val.regs, sig.Kind, sig.Version, val.opts.KubernetesVersion); err != nil {
|
||||
if schema, err = val.schemaDownload(val.regs, val.loader, sig.Kind, sig.Version, val.opts.KubernetesVersion); err != nil {
|
||||
return Result{Resource: res, Err: err, Status: Error}
|
||||
}
|
||||
|
||||
if val.schemaCache != nil {
|
||||
val.schemaCache.Set(sig.Kind, sig.Version, val.opts.KubernetesVersion, schema)
|
||||
if val.schemaMemoryCache != nil {
|
||||
val.schemaMemoryCache.Set(key(sig.Kind, sig.Version, val.opts.KubernetesVersion), schema)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -162,28 +223,33 @@ func (val *v) ValidateResource(res resource.Resource) Result {
|
|||
return Result{Resource: res, Err: fmt.Errorf("could not find schema for %s", sig.Kind), Status: Error}
|
||||
}
|
||||
|
||||
resourceLoader := gojsonschema.NewGoLoader(r)
|
||||
|
||||
results, err := schema.Validate(resourceLoader)
|
||||
err = schema.Validate(r)
|
||||
if err != nil {
|
||||
// This error can only happen if the Object to validate is poorly formed. There's no hope of saving this one
|
||||
return Result{Resource: res, Status: Error, Err: fmt.Errorf("problem validating schema. Check JSON formatting: %s", err)}
|
||||
}
|
||||
validationErrors := []ValidationError{}
|
||||
var e *jsonschema.ValidationError
|
||||
if errors.As(err, &e) {
|
||||
for _, ve := range e.Causes {
|
||||
path := ""
|
||||
for _, f := range ve.InstanceLocation {
|
||||
path = path + "/" + f
|
||||
}
|
||||
validationErrors = append(validationErrors, ValidationError{
|
||||
Path: path,
|
||||
Msg: ve.ErrorKind.LocalizedString(message.NewPrinter(language.English)),
|
||||
})
|
||||
}
|
||||
|
||||
if results.Valid() {
|
||||
return Result{Resource: res, Status: Valid}
|
||||
}
|
||||
|
||||
msg := ""
|
||||
for _, errMsg := range results.Errors() {
|
||||
if msg != "" {
|
||||
msg += " - "
|
||||
}
|
||||
details := errMsg.Details()
|
||||
msg += fmt.Sprintf("For field %s: %s", details["field"].(string), errMsg.Description())
|
||||
|
||||
return Result{
|
||||
Resource: res,
|
||||
Status: Invalid,
|
||||
Err: fmt.Errorf("problem validating schema. Check JSON formatting: %s", strings.ReplaceAll(err.Error(), "\n", " ")),
|
||||
ValidationErrors: validationErrors,
|
||||
}
|
||||
}
|
||||
|
||||
return Result{Resource: res, Status: Invalid, Err: fmt.Errorf("%s", msg)}
|
||||
return Result{Resource: res, Status: Valid}
|
||||
}
|
||||
|
||||
// ValidateWithContext validates resources found in r
|
||||
|
|
@ -194,8 +260,9 @@ func (val *v) ValidateWithContext(ctx context.Context, filename string, r io.Rea
|
|||
for {
|
||||
select {
|
||||
case res, ok := <-resourcesChan:
|
||||
validationResults = append(validationResults, val.ValidateResource(res))
|
||||
if !ok {
|
||||
if ok {
|
||||
validationResults = append(validationResults, val.ValidateResource(res))
|
||||
} else {
|
||||
resourcesChan = nil
|
||||
}
|
||||
|
||||
|
|
@ -218,24 +285,112 @@ func (val *v) Validate(filename string, r io.ReadCloser) []Result {
|
|||
return val.ValidateWithContext(context.Background(), filename, r)
|
||||
}
|
||||
|
||||
func downloadSchema(registries []registry.Registry, kind, version, k8sVersion string) (*gojsonschema.Schema, error) {
|
||||
// validateDuration is a custom validator for the duration format
|
||||
// as JSONSchema only supports the ISO 8601 format, i.e. `PT1H30M`,
|
||||
// while Kubernetes API machinery expects the Go duration format, i.e. `1h30m`
|
||||
// which is commonly used in Kubernetes operators for specifying intervals.
|
||||
// https://github.com/kubernetes/apiextensions-apiserver/blob/1ecd29f74da0639e2e6e3b8fac0c9bfd217e05eb/pkg/apis/apiextensions/v1/types_jsonschema.go#L71
|
||||
func validateDuration(v any) error {
|
||||
// Try validation with the Go duration format
|
||||
if _, err := time.ParseDuration(v.(string)); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// must start with 'P'
|
||||
s, ok = strings.CutPrefix(s, "P")
|
||||
if !ok {
|
||||
return fmt.Errorf("must start with P")
|
||||
}
|
||||
if s == "" {
|
||||
return fmt.Errorf("nothing after P")
|
||||
}
|
||||
|
||||
// dur-week
|
||||
if s, ok := strings.CutSuffix(s, "W"); ok {
|
||||
if s == "" {
|
||||
return fmt.Errorf("no number in week")
|
||||
}
|
||||
for _, ch := range s {
|
||||
if ch < '0' || ch > '9' {
|
||||
return fmt.Errorf("invalid week")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
allUnits := []string{"YMD", "HMS"}
|
||||
for i, s := range strings.Split(s, "T") {
|
||||
if i != 0 && s == "" {
|
||||
return fmt.Errorf("no time elements")
|
||||
}
|
||||
if i >= len(allUnits) {
|
||||
return fmt.Errorf("more than one T")
|
||||
}
|
||||
units := allUnits[i]
|
||||
for s != "" {
|
||||
digitCount := 0
|
||||
for _, ch := range s {
|
||||
if ch >= '0' && ch <= '9' {
|
||||
digitCount++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if digitCount == 0 {
|
||||
return fmt.Errorf("missing number")
|
||||
}
|
||||
s = s[digitCount:]
|
||||
if s == "" {
|
||||
return fmt.Errorf("missing unit")
|
||||
}
|
||||
unit := s[0]
|
||||
j := strings.IndexByte(units, unit)
|
||||
if j == -1 {
|
||||
if strings.IndexByte(allUnits[i], unit) != -1 {
|
||||
return fmt.Errorf("unit %q out of order", unit)
|
||||
}
|
||||
return fmt.Errorf("invalid unit %q", unit)
|
||||
}
|
||||
units = units[j+1:]
|
||||
s = s[1:]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func downloadSchema(registries []registry.Registry, l jsonschema.SchemeURLLoader, kind, version, k8sVersion string) (*jsonschema.Schema, error) {
|
||||
var err error
|
||||
var schemaBytes []byte
|
||||
var path string
|
||||
var s any
|
||||
|
||||
for _, reg := range registries {
|
||||
schemaBytes, err = reg.DownloadSchema(kind, version, k8sVersion)
|
||||
path, s, err = reg.DownloadSchema(kind, version, k8sVersion)
|
||||
if err == nil {
|
||||
schema, err := gojsonschema.NewSchema(gojsonschema.NewBytesLoader(schemaBytes))
|
||||
|
||||
c := jsonschema.NewCompiler()
|
||||
c.RegisterFormat(&jsonschema.Format{"duration", validateDuration})
|
||||
c.UseLoader(l)
|
||||
c.DefaultDraft(jsonschema.Draft4)
|
||||
if err := c.AddResource(path, s); err != nil {
|
||||
continue
|
||||
}
|
||||
schema, err := c.Compile(path)
|
||||
// If we got a non-parseable response, we try the next registry
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return schema, err
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
// If we get a 404, we try the next registry, but we exit if we get a real failure
|
||||
if _, notfound := err.(*registry.NotFoundError); notfound {
|
||||
if _, notfound := err.(*loader.NotFoundError); notfound {
|
||||
continue
|
||||
}
|
||||
if _, nonJSONError := err.(*loader.NonJSONResponseError); nonJSONError {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -244,11 +399,3 @@ func downloadSchema(registries []registry.Registry, kind, version, k8sVersion st
|
|||
|
||||
return nil, nil // No schema found - we don't consider it an error, resource will be skipped
|
||||
}
|
||||
|
||||
// From kubeval - let's see if absolutely necessary
|
||||
// func init () {
|
||||
// gojsonschema.FormatCheckers.Add("int64", ValidFormat{})
|
||||
// gojsonschema.FormatCheckers.Add("byte", ValidFormat{})
|
||||
// gojsonschema.FormatCheckers.Add("int32", ValidFormat{})
|
||||
// gojsonschema.FormatCheckers.Add("int-or-string", ValidFormat{})
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,23 +1,29 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"github.com/yannh/kubeconform/pkg/registry"
|
||||
"bytes"
|
||||
"github.com/santhosh-tekuri/jsonschema/v6"
|
||||
"github.com/yannh/kubeconform/pkg/loader"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/yannh/kubeconform/pkg/registry"
|
||||
|
||||
"github.com/yannh/kubeconform/pkg/resource"
|
||||
)
|
||||
|
||||
type mockRegistry struct {
|
||||
SchemaDownloader func() ([]byte, error)
|
||||
SchemaDownloader func() (string, any, error)
|
||||
}
|
||||
|
||||
func newMockRegistry(f func() ([]byte, error)) *mockRegistry {
|
||||
func newMockRegistry(f func() (string, any, error)) *mockRegistry {
|
||||
return &mockRegistry{
|
||||
SchemaDownloader: f,
|
||||
}
|
||||
}
|
||||
|
||||
func (m mockRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVersion string) ([]byte, error) {
|
||||
func (m mockRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVersion string) (string, any, error) {
|
||||
return m.SchemaDownloader()
|
||||
}
|
||||
|
||||
|
|
@ -27,7 +33,9 @@ func TestValidate(t *testing.T) {
|
|||
rawResource, schemaRegistry1 []byte
|
||||
schemaRegistry2 []byte
|
||||
ignoreMissingSchema bool
|
||||
expect Status
|
||||
strict bool
|
||||
expectStatus Status
|
||||
expectErrors []ValidationError
|
||||
}{
|
||||
{
|
||||
"valid resource",
|
||||
|
|
@ -60,7 +68,9 @@ lastName: bar
|
|||
}`),
|
||||
nil,
|
||||
false,
|
||||
false,
|
||||
Valid,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"invalid resource",
|
||||
|
|
@ -93,7 +103,14 @@ lastName: bar
|
|||
}`),
|
||||
nil,
|
||||
false,
|
||||
false,
|
||||
Invalid,
|
||||
[]ValidationError{
|
||||
{
|
||||
Path: "/firstName",
|
||||
Msg: "got string, want number",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"missing required field",
|
||||
|
|
@ -125,7 +142,68 @@ firstName: foo
|
|||
}`),
|
||||
nil,
|
||||
false,
|
||||
false,
|
||||
Invalid,
|
||||
[]ValidationError{
|
||||
{
|
||||
Path: "",
|
||||
Msg: "missing property 'lastName'",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"key \"firstName\" already set in map",
|
||||
[]byte(`
|
||||
kind: name
|
||||
apiVersion: v1
|
||||
firstName: foo
|
||||
firstName: bar
|
||||
`),
|
||||
[]byte(`{
|
||||
"title": "Example Schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"firstName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["firstName"]
|
||||
}`),
|
||||
nil,
|
||||
false,
|
||||
true,
|
||||
Error,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"key firstname already set in map in non-strict mode",
|
||||
[]byte(`
|
||||
kind: name
|
||||
apiVersion: v1
|
||||
firstName: foo
|
||||
firstName: bar
|
||||
`),
|
||||
[]byte(`{
|
||||
"title": "Example Schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"firstName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["firstName"]
|
||||
}`),
|
||||
nil,
|
||||
false,
|
||||
false,
|
||||
Valid,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"resource has invalid yaml",
|
||||
|
|
@ -161,7 +239,9 @@ lastName: bar
|
|||
}`),
|
||||
nil,
|
||||
false,
|
||||
false,
|
||||
Error,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"missing schema in 1st registry",
|
||||
|
|
@ -196,8 +276,10 @@ lastName: bar
|
|||
},
|
||||
"required": ["firstName", "lastName"]
|
||||
}`),
|
||||
false,
|
||||
false,
|
||||
Valid,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"non-json response in 1st registry",
|
||||
|
|
@ -232,8 +314,10 @@ lastName: bar
|
|||
},
|
||||
"required": ["firstName", "lastName"]
|
||||
}`),
|
||||
false,
|
||||
false,
|
||||
Valid,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"missing schema in both registries, ignore missing",
|
||||
|
|
@ -246,7 +330,9 @@ lastName: bar
|
|||
nil,
|
||||
nil,
|
||||
true,
|
||||
false,
|
||||
Skipped,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"missing schema in both registries, do not ignore missing",
|
||||
|
|
@ -259,7 +345,9 @@ lastName: bar
|
|||
nil,
|
||||
nil,
|
||||
false,
|
||||
false,
|
||||
Error,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"non-json response in both registries, ignore missing",
|
||||
|
|
@ -272,7 +360,9 @@ lastName: bar
|
|||
[]byte(`<html>error page</html>`),
|
||||
[]byte(`<html>error page</html>`),
|
||||
true,
|
||||
false,
|
||||
Skipped,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"non-json response in both registries, do not ignore missing",
|
||||
|
|
@ -285,7 +375,90 @@ lastName: bar
|
|||
[]byte(`<html>error page</html>`),
|
||||
[]byte(`<html>error page</html>`),
|
||||
false,
|
||||
false,
|
||||
Error,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"valid resource duration - go format",
|
||||
[]byte(`
|
||||
kind: name
|
||||
apiVersion: v1
|
||||
interval: 5s
|
||||
`),
|
||||
[]byte(`{
|
||||
"title": "Example Schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"interval": {
|
||||
"type": "string",
|
||||
"format": "duration"
|
||||
}
|
||||
},
|
||||
"required": ["interval"]
|
||||
}`),
|
||||
nil,
|
||||
false,
|
||||
false,
|
||||
Valid,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"valid resource duration - iso8601 format",
|
||||
[]byte(`
|
||||
kind: name
|
||||
apiVersion: v1
|
||||
interval: PT1H
|
||||
`),
|
||||
[]byte(`{
|
||||
"title": "Example Schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"interval": {
|
||||
"type": "string",
|
||||
"format": "duration"
|
||||
}
|
||||
},
|
||||
"required": ["interval"]
|
||||
}`),
|
||||
nil,
|
||||
false,
|
||||
false,
|
||||
Valid,
|
||||
[]ValidationError{},
|
||||
},
|
||||
{
|
||||
"invalid resource duration",
|
||||
[]byte(`
|
||||
kind: name
|
||||
apiVersion: v1
|
||||
interval: test
|
||||
`),
|
||||
[]byte(`{
|
||||
"title": "Example Schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"interval": {
|
||||
"type": "string",
|
||||
"format": "duration"
|
||||
}
|
||||
},
|
||||
"required": ["interval"]
|
||||
}`),
|
||||
nil,
|
||||
false,
|
||||
false,
|
||||
Invalid,
|
||||
[]ValidationError{{Path: "/interval", Msg: "'test' is not valid duration: must start with P"}},
|
||||
},
|
||||
} {
|
||||
val := v{
|
||||
|
|
@ -293,20 +466,168 @@ lastName: bar
|
|||
SkipKinds: map[string]struct{}{},
|
||||
RejectKinds: map[string]struct{}{},
|
||||
IgnoreMissingSchemas: testCase.ignoreMissingSchema,
|
||||
Strict: testCase.strict,
|
||||
},
|
||||
schemaCache: nil,
|
||||
schemaDownload: downloadSchema,
|
||||
regs: []registry.Registry{
|
||||
newMockRegistry(func() ([]byte, error) {
|
||||
return testCase.schemaRegistry1, nil
|
||||
newMockRegistry(func() (string, any, error) {
|
||||
if testCase.schemaRegistry1 == nil {
|
||||
return "", nil, loader.NewNotFoundError(nil)
|
||||
}
|
||||
s, err := jsonschema.UnmarshalJSON(bytes.NewReader(testCase.schemaRegistry1))
|
||||
if err != nil {
|
||||
return "", s, loader.NewNonJSONResponseError(err)
|
||||
}
|
||||
return "", s, err
|
||||
}),
|
||||
newMockRegistry(func() ([]byte, error) {
|
||||
return testCase.schemaRegistry2, nil
|
||||
newMockRegistry(func() (string, any, error) {
|
||||
if testCase.schemaRegistry2 == nil {
|
||||
return "", nil, loader.NewNotFoundError(nil)
|
||||
}
|
||||
s, err := jsonschema.UnmarshalJSON(bytes.NewReader(testCase.schemaRegistry2))
|
||||
if err != nil {
|
||||
return "", s, loader.NewNonJSONResponseError(err)
|
||||
}
|
||||
return "", s, err
|
||||
}),
|
||||
},
|
||||
}
|
||||
if got := val.ValidateResource(resource.Resource{Bytes: testCase.rawResource}); got.Status != testCase.expect {
|
||||
t.Errorf("%d - expected %d, got %d: %s", i, testCase.expect, got.Status, got.Err.Error())
|
||||
got := val.ValidateResource(resource.Resource{Bytes: testCase.rawResource})
|
||||
if got.Status != testCase.expectStatus {
|
||||
if got.Err != nil {
|
||||
t.Errorf("Test '%s' - expected %d, got %d: %s", testCase.name, testCase.expectStatus, got.Status, got.Err.Error())
|
||||
} else {
|
||||
t.Errorf("Test '%s'- %d - expected %d, got %d", testCase.name, i, testCase.expectStatus, got.Status)
|
||||
}
|
||||
}
|
||||
|
||||
if len(got.ValidationErrors) != len(testCase.expectErrors) {
|
||||
t.Errorf("Test '%s': expected ValidationErrors: %+v, got: % v", testCase.name, testCase.expectErrors, got.ValidationErrors)
|
||||
}
|
||||
for i, _ := range testCase.expectErrors {
|
||||
if testCase.expectErrors[i] != got.ValidationErrors[i] {
|
||||
t.Errorf("Test '%s': expected ValidationErrors: %+v, got: % v", testCase.name, testCase.expectErrors, got.ValidationErrors)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidationErrors(t *testing.T) {
|
||||
rawResource := []byte(`
|
||||
kind: name
|
||||
apiVersion: v1
|
||||
firstName: foo
|
||||
age: not a number
|
||||
`)
|
||||
|
||||
schema := []byte(`{
|
||||
"title": "Example Schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"firstName": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastName": {
|
||||
"type": "string"
|
||||
},
|
||||
"age": {
|
||||
"description": "Age in years",
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": ["firstName", "lastName"]
|
||||
}`)
|
||||
|
||||
expectedErrors := []ValidationError{
|
||||
{Path: "", Msg: "missing property 'lastName'"},
|
||||
{Path: "/age", Msg: "got string, want integer"},
|
||||
}
|
||||
|
||||
val := v{
|
||||
opts: Opts{
|
||||
SkipKinds: map[string]struct{}{},
|
||||
RejectKinds: map[string]struct{}{},
|
||||
},
|
||||
schemaDownload: downloadSchema,
|
||||
regs: []registry.Registry{
|
||||
newMockRegistry(func() (string, any, error) {
|
||||
s, err := jsonschema.UnmarshalJSON(bytes.NewReader(schema))
|
||||
if err != nil {
|
||||
return "", s, loader.NewNonJSONResponseError(err)
|
||||
}
|
||||
return "", s, err
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
got := val.ValidateResource(resource.Resource{Bytes: rawResource})
|
||||
if !reflect.DeepEqual(expectedErrors, got.ValidationErrors) {
|
||||
t.Errorf("Expected %+v, got %+v", expectedErrors, got.ValidationErrors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFile(t *testing.T) {
|
||||
inputData := []byte(`
|
||||
kind: name
|
||||
apiVersion: v1
|
||||
firstName: bar
|
||||
lastName: qux
|
||||
---
|
||||
kind: name
|
||||
apiVersion: v1
|
||||
firstName: foo
|
||||
`)
|
||||
|
||||
schema := []byte(`{
|
||||
"title": "Example Schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"firstName": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["firstName", "lastName"]
|
||||
}`)
|
||||
|
||||
val := v{
|
||||
opts: Opts{
|
||||
SkipKinds: map[string]struct{}{},
|
||||
RejectKinds: map[string]struct{}{},
|
||||
},
|
||||
schemaDownload: downloadSchema,
|
||||
regs: []registry.Registry{
|
||||
newMockRegistry(func() (string, any, error) {
|
||||
s, err := jsonschema.UnmarshalJSON(bytes.NewReader(schema))
|
||||
return "", s, err
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
gotStatuses := []Status{}
|
||||
gotValidationErrors := []ValidationError{}
|
||||
for _, got := range val.Validate("test-file", io.NopCloser(bytes.NewReader(inputData))) {
|
||||
gotStatuses = append(gotStatuses, got.Status)
|
||||
gotValidationErrors = append(gotValidationErrors, got.ValidationErrors...)
|
||||
}
|
||||
|
||||
expectedStatuses := []Status{Valid, Invalid}
|
||||
expectedValidationErrors := []ValidationError{
|
||||
{Path: "", Msg: "missing property 'lastName'"},
|
||||
}
|
||||
if !reflect.DeepEqual(expectedStatuses, gotStatuses) {
|
||||
t.Errorf("Expected %+v, got %+v", expectedStatuses, gotStatuses)
|
||||
}
|
||||
if !reflect.DeepEqual(expectedValidationErrors, gotValidationErrors) {
|
||||
t.Errorf("Expected %+v, got %+v", expectedValidationErrors, gotValidationErrors)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
FROM python:3.9.7-alpine3.14
|
||||
RUN apk --no-cache add bats
|
||||
COPY acceptance.bats openapi2jsonschema.py requirements.txt /code/
|
||||
COPY requirements.txt /code/
|
||||
RUN pip install -r /code/requirements.txt
|
||||
COPY fixtures /code/fixtures
|
||||
COPY acceptance.bats openapi2jsonschema.py /code/
|
||||
WORKDIR /code
|
||||
RUN pip install -r requirements.txt
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -5,6 +5,42 @@ setup() {
|
|||
rm -f prometheus-monitoring-v1.json
|
||||
}
|
||||
|
||||
@test "Should generate expected prometheus resource while disable ssl env var is set" {
|
||||
run export DISABLE_SSL_CERT_VALIDATION=true
|
||||
run ./openapi2jsonschema.py fixtures/prometheus-operator-0prometheusCustomResourceDefinition.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "JSON schema written to prometheus_v1.json" ]
|
||||
run diff prometheus_v1.json ./fixtures/prometheus_v1-expected.json
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Should generate expected prometheus resource from an HTTPS resource while disable ssl env var is set" {
|
||||
run export DISABLE_SSL_CERT_VALIDATION=true
|
||||
run ./openapi2jsonschema.py https://raw.githubusercontent.com/yannh/kubeconform/aebc298047c386116eeeda9b1ada83671a58aedd/scripts/fixtures/prometheus-operator-0prometheusCustomResourceDefinition.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "JSON schema written to prometheus_v1.json" ]
|
||||
run diff prometheus_v1.json ./fixtures/prometheus_v1-expected.json
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Should output filename in {kind}-{group}-{version} format while disable ssl env var is set" {
|
||||
run export DISABLE_SSL_CERT_VALIDATION=true
|
||||
FILENAME_FORMAT='{kind}-{group}-{version}' run ./openapi2jsonschema.py fixtures/prometheus-operator-0prometheusCustomResourceDefinition.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "JSON schema written to prometheus-monitoring-v1.json" ]
|
||||
run diff prometheus-monitoring-v1.json ./fixtures/prometheus_v1-expected.json
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Should set 'additionalProperties: false' at the root while disable ssl env var is set" {
|
||||
run export DISABLE_SSL_CERT_VALIDATION=true
|
||||
DENY_ROOT_ADDITIONAL_PROPERTIES='true' run ./openapi2jsonschema.py fixtures/prometheus-operator-0prometheusCustomResourceDefinition.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "JSON schema written to prometheus_v1.json" ]
|
||||
run diff prometheus_v1.json ./fixtures/prometheus_v1-denyRootAdditionalProperties.json
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Should generate expected prometheus resource" {
|
||||
run ./openapi2jsonschema.py fixtures/prometheus-operator-0prometheusCustomResourceDefinition.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
|
|
@ -36,3 +72,10 @@ setup() {
|
|||
run diff prometheus_v1.json ./fixtures/prometheus_v1-denyRootAdditionalProperties.json
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Should output an error if no file is passed" {
|
||||
run ./openapi2jsonschema.py
|
||||
[ "$status" -eq 1 ]
|
||||
[ "${lines[0]}" == 'Missing FILE parameter.' ]
|
||||
[ "${lines[1]}" == 'Usage: ./openapi2jsonschema.py [FILE]' ]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import json
|
|||
import sys
|
||||
import os
|
||||
import urllib.request
|
||||
if 'DISABLE_SSL_CERT_VALIDATION' in os.environ:
|
||||
import ssl
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
|
||||
def test_additional_properties():
|
||||
for test in iter([{
|
||||
|
|
@ -72,7 +75,7 @@ def allow_null_optional_fields(data, parent=None, grand_parent=None, key=None):
|
|||
elif isinstance(v, str):
|
||||
is_non_null_type = k == "type" and v != "null"
|
||||
has_required_fields = grand_parent and "required" in grand_parent
|
||||
if is_non_null_type and not has_required_field:
|
||||
if is_non_null_type and not has_required_fields:
|
||||
new_v = [v, "null"]
|
||||
new[k] = new_v
|
||||
return new
|
||||
|
|
@ -106,11 +109,21 @@ def write_schema_file(schema, filename):
|
|||
print("JSON schema written to {filename}".format(filename=filename))
|
||||
|
||||
|
||||
def construct_value(load, node):
|
||||
# Handle nodes that start with '='
|
||||
# See https://github.com/yaml/pyyaml/issues/89
|
||||
if not isinstance(node, yaml.ScalarNode):
|
||||
raise yaml.constructor.ConstructorError(
|
||||
"while constructing a value",
|
||||
node.start_mark,
|
||||
"expected a scalar, but found %s" % node.id, node.start_mark
|
||||
)
|
||||
yield str(node.value)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) == 0:
|
||||
print("missing file")
|
||||
if len(sys.argv) < 2:
|
||||
print('Missing FILE parameter.\nUsage: %s [FILE]' % sys.argv[0])
|
||||
exit(1)
|
||||
|
||||
for crdFile in sys.argv[1:]:
|
||||
|
|
@ -120,6 +133,7 @@ if __name__ == "__main__":
|
|||
f = open(crdFile)
|
||||
with f:
|
||||
defs = []
|
||||
yaml.SafeLoader.add_constructor(u'tag:yaml.org,2002:value', construct_value)
|
||||
for y in yaml.load_all(f, Loader=yaml.SafeLoader):
|
||||
if y is None:
|
||||
continue
|
||||
|
|
@ -141,6 +155,7 @@ if __name__ == "__main__":
|
|||
filename = filename_format.format(
|
||||
kind=y["spec"]["names"]["kind"],
|
||||
group=y["spec"]["group"].split(".")[0],
|
||||
fullgroup=y["spec"]["group"],
|
||||
version=version["name"],
|
||||
).lower() + ".json"
|
||||
|
||||
|
|
@ -150,6 +165,7 @@ if __name__ == "__main__":
|
|||
filename = filename_format.format(
|
||||
kind=y["spec"]["names"]["kind"],
|
||||
group=y["spec"]["group"].split(".")[0],
|
||||
fullgroup=y["spec"]["group"],
|
||||
version=version["name"],
|
||||
).lower() + ".json"
|
||||
|
||||
|
|
@ -159,6 +175,7 @@ if __name__ == "__main__":
|
|||
filename = filename_format.format(
|
||||
kind=y["spec"]["names"]["kind"],
|
||||
group=y["spec"]["group"].split(".")[0],
|
||||
fullgroup=y["spec"]["group"],
|
||||
version=y["spec"]["version"],
|
||||
).lower() + ".json"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
---
|
||||
title: "Github Action"
|
||||
title: "GitHub Action"
|
||||
date: 2021-07-02T00:00:00Z
|
||||
draft: false
|
||||
tags: ["Kubeconform", "Usage"]
|
||||
weight: 6
|
||||
---
|
||||
|
||||
Kubeconform is publishes Docker Images to Github's new Container Registry, ghcr.io. These images
|
||||
can be used directly in a Github Action, once logged in using a [_Github Token_](https://github.blog/changelog/2021-03-24-packages-container-registry-now-supports-github_token/).
|
||||
Kubeconform is publishes Docker Images to GitHub's new Container Registry, ghcr.io. These images
|
||||
can be used directly in a GitHub Action, once logged in using a [_GitHub Token_](https://github.blog/changelog/2021-03-24-packages-container-registry-now-supports-github_token/).
|
||||
|
||||
{{< prism >}}name: kubeconform
|
||||
on: push
|
||||
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
kubeconform:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: login to Github Packages
|
||||
- name: login to GitHub Packages
|
||||
run: echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker://ghcr.io/yannh/kubeconform:master
|
||||
|
|
@ -24,8 +24,8 @@ jobs:
|
|||
args: "-summary -output json kubeconfigs/"
|
||||
{{< /prism >}}
|
||||
|
||||
_Note on pricing_: Kubeconform relies on Github Container Registry which is currently in Beta. During that period,
|
||||
_Note on pricing_: Kubeconform relies on GitHub Container Registry which is currently in Beta. During that period,
|
||||
[bandwidth is free](https://docs.github.com/en/packages/guides/about-github-container-registry). After that period,
|
||||
bandwidth costs might be applicable. Since bandwidth from Github Packages within Github Actions is free, I expect
|
||||
Github Container Registry to also be usable for free within Github Actions in the future. If that were not to be the
|
||||
bandwidth costs might be applicable. Since bandwidth from GitHub Packages within GitHub Actions is free, I expect
|
||||
GitHub Container Registry to also be usable for free within GitHub Actions in the future. If that were not to be the
|
||||
case, I might publish the Docker image to a different platform.
|
||||
|
|
@ -34,7 +34,7 @@ Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]...
|
|||
-skip string
|
||||
comma-separated list of kinds to ignore
|
||||
-strict
|
||||
disallow additional properties not in schema
|
||||
disallow additional properties not in schema or duplicated keys
|
||||
-summary
|
||||
print a summary at the end (ignored for junit output)
|
||||
-v show version information
|
||||
|
|
@ -83,4 +83,4 @@ fixtures/crd_schema.yaml - CustomResourceDefinition trainingjobs.sagemaker.aws.a
|
|||
fixtures/invalid.yaml - ReplicationController bob is invalid: Invalid type. Expected: [integer,null], given: string
|
||||
[...]
|
||||
Summary: 65 resources found in 34 files - Valid: 55, Invalid: 2, Errors: 8 Skipped: 0
|
||||
{{< /prism >}}
|
||||
{{< /prism >}}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<div id=main-container><div id=header>
|
||||
<ul id=navigation>
|
||||
<li><a href=/about>About</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>Github</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>GitHub</a></li>
|
||||
<li><a href=/docs/installation/>Docs</a></li>
|
||||
<li><a href=/>Home</a></li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<div id=main-container><div id=header>
|
||||
<ul id=navigation>
|
||||
<li><a href=/about>About</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>Github</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>GitHub</a></li>
|
||||
<li><a href=/docs/installation/>Docs</a></li>
|
||||
<li><a href=/>Home</a></li>
|
||||
</ul>
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
<li><a href=http://kubeconform.mandragor.org/docs/usage/>Usage</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/crd-support/>Custom Resources support</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/json-schema-conversion/>OpenAPI to JSON Schema conversion</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/>Github Action</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/>GitHub Action</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/using-as-a-go-module/>Kubeconform as a Go module</a></li>
|
||||
</ul>
|
||||
<div id=main>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ For example, for Linux on x86_64 architecture:
|
|||
curl -L https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz | tar xvzf - && \ sudo mv kubeconform /usr/local/bin/ MacOs Kubeconform is available to install using Homebrew: $ brew install kubeconform
|
||||
Windows Download the latest release from our release page.</description></item><item><title>Usage</title><link>http://kubeconform.mandragor.org/docs/usage/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/usage/</guid><description>$ ./bin/kubeconform -h Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]... -cache string cache schemas downloaded via HTTP to this folder -cpu-prof string debug - log CPU profiling to file -exit-on-error immediately stop execution when the first error is encountered -h show help information -ignore-filename-pattern value regular expression specifying paths to ignore (can be specified multiple times) -ignore-missing-schemas skip files with missing schemas instead of failing -insecure-skip-tls-verify disable verification of the server's SSL certificate.</description></item><item><title>Custom Resources support</title><link>http://kubeconform.mandragor.org/docs/crd-support/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/crd-support/</guid><description>When the -schema-location parameter is not used, or set to &ldquo;default&rdquo;, kubeconform will default to downloading schemas from https://github.com/yannh/kubernetes-json-schema. Kubeconform however supports passing one, or multiple, schemas locations - HTTP(s) URLs, or local filesystem paths, in which case it will lookup for schema definitions in each of them, in order, stopping as soon as a matching file is found.
|
||||
If the -schema-location value does not end with &lsquo;.json&rsquo;, Kubeconform will assume filenames / a file structure identical to that of kubernetesjsonschema.</description></item><item><title>OpenAPI to JSON Schema conversion</title><link>http://kubeconform.mandragor.org/docs/json-schema-conversion/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/json-schema-conversion/</guid><description>Kubeconform uses JSON schemas to validate Kubernetes resources. For custom resources, the CustomResourceDefinition first needs to be converted to JSON Schema. A script is provided to convert these CustomResourceDefinitions to JSON schema. Here is an example how to use it:
|
||||
#!/bin/bash $ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml JSON schema written to trainingjob_v1.json The FILENAME_FORMAT environment variable can be used to change the output file name (Available variables: kind, group, version) (Default: {kind}_{version}).</description></item><item><title>Github Action</title><link>http://kubeconform.mandragor.org/docs/usage-as-github-action/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/usage-as-github-action/</guid><description>Kubeconform is publishes Docker Images to Github&rsquo;s new Container Registry, ghcr.io. These images can be used directly in a Github Action, once logged in using a Github Token.
|
||||
name: kubeconform on: push jobs: kubeconform: runs-on: ubuntu-latest steps: - name: login to Github Packages run: echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin - uses: actions/checkout@v2 - uses: docker://ghcr.io/yannh/kubeconform:master with: entrypoint: '/kubeconform' args: "-summary -output json kubeconfigs/" Note on pricing: Kubeconform relies on Github Container Registry which is currently in Beta.</description></item><item><title>Kubeconform as a Go module</title><link>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</guid><description>Warning: This is a work-in-progress, the interface is not yet considered stable. Feedback is encouraged.
|
||||
#!/bin/bash $ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml JSON schema written to trainingjob_v1.json The FILENAME_FORMAT environment variable can be used to change the output file name (Available variables: kind, group, version) (Default: {kind}_{version}).</description></item><item><title>GitHub Action</title><link>http://kubeconform.mandragor.org/docs/usage-as-github-action/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/usage-as-github-action/</guid><description>Kubeconform is publishes Docker Images to GitHub&rsquo;s new Container Registry, ghcr.io. These images can be used directly in a GitHub Action, once logged in using a GitHub Token.
|
||||
name: kubeconform on: push jobs: kubeconform: runs-on: ubuntu-latest steps: - name: login to GitHub Packages run: echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin - uses: actions/checkout@v2 - uses: docker://ghcr.io/yannh/kubeconform:master with: entrypoint: '/kubeconform' args: "-summary -output json kubeconfigs/" Note on pricing: Kubeconform relies on GitHub Container Registry which is currently in Beta.</description></item><item><title>Kubeconform as a Go module</title><link>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</guid><description>Warning: This is a work-in-progress, the interface is not yet considered stable. Feedback is encouraged.
|
||||
Kubeconform contains a package that can be used as a library. An example of usage can be found in examples/main.go
|
||||
Additional documentation on pkg.go.dev</description></item></channel></rss>
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
<div id=main-container><div id=header>
|
||||
<ul id=navigation>
|
||||
<li><a href=/about>About</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>Github</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>GitHub</a></li>
|
||||
<li><a href=/docs/installation/>Docs</a></li>
|
||||
<li><a href=/>Home</a></li>
|
||||
</ul>
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
<li><a href=http://kubeconform.mandragor.org/docs/usage/>Usage</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/crd-support/>Custom Resources support</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/json-schema-conversion/>OpenAPI to JSON Schema conversion</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/>Github Action</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/>GitHub Action</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/using-as-a-go-module/>Kubeconform as a Go module</a></li>
|
||||
</ul>
|
||||
<div id=main>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<div id=main-container><div id=header>
|
||||
<ul id=navigation>
|
||||
<li><a href=/about>About</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>Github</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>GitHub</a></li>
|
||||
<li><a href=/docs/installation/>Docs</a></li>
|
||||
<li><a href=/>Home</a></li>
|
||||
</ul>
|
||||
|
|
@ -21,13 +21,13 @@
|
|||
<li><a href=http://kubeconform.mandragor.org/docs/usage/>Usage</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/crd-support/>Custom Resources support</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/json-schema-conversion/>OpenAPI to JSON Schema conversion</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/>Github Action</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/>GitHub Action</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/using-as-a-go-module/>Kubeconform as a Go module</a></li>
|
||||
</ul>
|
||||
<div id=main>
|
||||
<div class=navig>
|
||||
<a href=http://kubeconform.mandragor.org/docs/crd-support/ id=prev>< Custom Resources support</a>
|
||||
<a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/ id=next>Github Action ></a>
|
||||
<a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/ id=next>GitHub Action ></a>
|
||||
</div>
|
||||
<div id=content-text>
|
||||
<h1>OpenAPI to JSON Schema conversion</h1>
|
||||
|
|
@ -47,7 +47,7 @@ JSON schema written to trainingjob-sagemaker-v1.json
|
|||
</div>
|
||||
<div class=navig>
|
||||
<a href=http://kubeconform.mandragor.org/docs/crd-support/ id=prev>< Custom Resources support</a>
|
||||
<a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/ id=next>Github Action ></a>
|
||||
<a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/ id=next>GitHub Action ></a>
|
||||
</div>
|
||||
<script defer src=/js/prism.js></script>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
<meta charset=utf-8>
|
||||
<meta name=author content="Yann Hamon">
|
||||
<link rel=stylesheet type=text/css href=/css/style.css><link rel=stylesheet type=text/css href=/css/prism.css>
|
||||
<title>Kubeconform - Fast Kubernetes manifests validation! | Github Action</title>
|
||||
<title>Kubeconform - Fast Kubernetes manifests validation! | GitHub Action</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id=main-container><div id=header>
|
||||
<ul id=navigation>
|
||||
<li><a href=/about>About</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>Github</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>GitHub</a></li>
|
||||
<li><a href=/docs/installation/>Docs</a></li>
|
||||
<li><a href=/>Home</a></li>
|
||||
</ul>
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
<li><a href=http://kubeconform.mandragor.org/docs/usage/>Usage</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/crd-support/>Custom Resources support</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/json-schema-conversion/>OpenAPI to JSON Schema conversion</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/>Github Action</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/>GitHub Action</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/using-as-a-go-module/>Kubeconform as a Go module</a></li>
|
||||
</ul>
|
||||
<div id=main>
|
||||
|
|
@ -30,16 +30,16 @@
|
|||
<a href=http://kubeconform.mandragor.org/docs/using-as-a-go-module/ id=next>Kubeconform as a Go module ></a>
|
||||
</div>
|
||||
<div id=content-text>
|
||||
<h1>Github Action</h1>
|
||||
<p>Kubeconform is publishes Docker Images to Github’s new Container Registry, ghcr.io. These images
|
||||
can be used directly in a Github Action, once logged in using a <a href=https://github.blog/changelog/2021-03-24-packages-container-registry-now-supports-github_token/><em>Github Token</em></a>.</p>
|
||||
<h1>GitHub Action</h1>
|
||||
<p>Kubeconform is publishes Docker Images to GitHub’s new Container Registry, ghcr.io. These images
|
||||
can be used directly in a GitHub Action, once logged in using a <a href=https://github.blog/changelog/2021-03-24-packages-container-registry-now-supports-github_token/><em>GitHub Token</em></a>.</p>
|
||||
<pre><code class=language-bash>name: kubeconform
|
||||
on: push
|
||||
jobs:
|
||||
kubeconform:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: login to Github Packages
|
||||
- name: login to GitHub Packages
|
||||
run: echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker://ghcr.io/yannh/kubeconform:master
|
||||
|
|
@ -47,10 +47,10 @@ jobs:
|
|||
entrypoint: '/kubeconform'
|
||||
args: "-summary -output json kubeconfigs/"
|
||||
</code></pre>
|
||||
<p><em>Note on pricing</em>: Kubeconform relies on Github Container Registry which is currently in Beta. During that period,
|
||||
<p><em>Note on pricing</em>: Kubeconform relies on GitHub Container Registry which is currently in Beta. During that period,
|
||||
<a href=https://docs.github.com/en/packages/guides/about-github-container-registry>bandwidth is free</a>. After that period,
|
||||
bandwidth costs might be applicable. Since bandwidth from Github Packages within Github Actions is free, I expect
|
||||
Github Container Registry to also be usable for free within Github Actions in the future. If that were not to be the
|
||||
bandwidth costs might be applicable. Since bandwidth from GitHub Packages within GitHub Actions is free, I expect
|
||||
GitHub Container Registry to also be usable for free within GitHub Actions in the future. If that were not to be the
|
||||
case, I might publish the Docker image to a different platform.</p>
|
||||
</div>
|
||||
<div class=navig>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<div id=main-container><div id=header>
|
||||
<ul id=navigation>
|
||||
<li><a href=/about>About</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>Github</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>GitHub</a></li>
|
||||
<li><a href=/docs/installation/>Docs</a></li>
|
||||
<li><a href=/>Home</a></li>
|
||||
</ul>
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
<li><a href=http://kubeconform.mandragor.org/docs/usage/>Usage</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/crd-support/>Custom Resources support</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/json-schema-conversion/>OpenAPI to JSON Schema conversion</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/>Github Action</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/>GitHub Action</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/using-as-a-go-module/>Kubeconform as a Go module</a></li>
|
||||
</ul>
|
||||
<div id=main>
|
||||
|
|
@ -59,7 +59,7 @@ Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]...
|
|||
-skip string
|
||||
comma-separated list of kinds to ignore
|
||||
-strict
|
||||
disallow additional properties not in schema
|
||||
disallow additional properties not in schema or duplicated keys
|
||||
-summary
|
||||
print a summary at the end (ignored for junit output)
|
||||
-v show version information
|
||||
|
|
@ -117,4 +117,4 @@ Website powered by <a href=https://gohugo.io/>Hugo</a>
|
|||
</div>
|
||||
<script defer src=/js/prism.js></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<div id=main-container><div id=header>
|
||||
<ul id=navigation>
|
||||
<li><a href=/about>About</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>Github</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>GitHub</a></li>
|
||||
<li><a href=/docs/installation/>Docs</a></li>
|
||||
<li><a href=/>Home</a></li>
|
||||
</ul>
|
||||
|
|
@ -21,12 +21,12 @@
|
|||
<li><a href=http://kubeconform.mandragor.org/docs/usage/>Usage</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/crd-support/>Custom Resources support</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/json-schema-conversion/>OpenAPI to JSON Schema conversion</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/>Github Action</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/>GitHub Action</a></li>
|
||||
<li><a href=http://kubeconform.mandragor.org/docs/using-as-a-go-module/>Kubeconform as a Go module</a></li>
|
||||
</ul>
|
||||
<div id=main>
|
||||
<div class=navig>
|
||||
<a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/ id=prev>< Github Action</a>
|
||||
<a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/ id=prev>< GitHub Action</a>
|
||||
<a href=# id=prev></a>
|
||||
</div>
|
||||
<div id=content-text>
|
||||
|
|
@ -37,7 +37,7 @@ An example of usage can be found in <a href=https://github.com/yannh/kubeconform
|
|||
<p>Additional documentation on <a href=https://pkg.go.dev/github.com/yannh/kubeconform/pkg/validator>pkg.go.dev</a></p>
|
||||
</div>
|
||||
<div class=navig>
|
||||
<a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/ id=prev>< Github Action</a>
|
||||
<a href=http://kubeconform.mandragor.org/docs/usage-as-github-action/ id=prev>< GitHub Action</a>
|
||||
<a href=# id=prev></a>
|
||||
</div>
|
||||
<script defer src=/js/prism.js></script>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<div id=main-container><div id=header>
|
||||
<ul id=navigation>
|
||||
<li><a href=/about>About</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>Github</a></li>
|
||||
<li><a href=https://github.com/yannh/kubeconform/>GitHub</a></li>
|
||||
<li><a href=/docs/installation/>Docs</a></li>
|
||||
<li><a href=/>Home</a></li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ For example, for Linux on x86_64 architecture:
|
|||
curl -L https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz | tar xvzf - && \ sudo mv kubeconform /usr/local/bin/ MacOs Kubeconform is available to install using Homebrew: $ brew install kubeconform
|
||||
Windows Download the latest release from our release page.</description></item><item><title>Usage</title><link>http://kubeconform.mandragor.org/docs/usage/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/usage/</guid><description>$ ./bin/kubeconform -h Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]... -cache string cache schemas downloaded via HTTP to this folder -cpu-prof string debug - log CPU profiling to file -exit-on-error immediately stop execution when the first error is encountered -h show help information -ignore-filename-pattern value regular expression specifying paths to ignore (can be specified multiple times) -ignore-missing-schemas skip files with missing schemas instead of failing -insecure-skip-tls-verify disable verification of the server's SSL certificate.</description></item><item><title>Custom Resources support</title><link>http://kubeconform.mandragor.org/docs/crd-support/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/crd-support/</guid><description>When the -schema-location parameter is not used, or set to &ldquo;default&rdquo;, kubeconform will default to downloading schemas from https://github.com/yannh/kubernetes-json-schema. Kubeconform however supports passing one, or multiple, schemas locations - HTTP(s) URLs, or local filesystem paths, in which case it will lookup for schema definitions in each of them, in order, stopping as soon as a matching file is found.
|
||||
If the -schema-location value does not end with &lsquo;.json&rsquo;, Kubeconform will assume filenames / a file structure identical to that of kubernetesjsonschema.</description></item><item><title>OpenAPI to JSON Schema conversion</title><link>http://kubeconform.mandragor.org/docs/json-schema-conversion/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/json-schema-conversion/</guid><description>Kubeconform uses JSON schemas to validate Kubernetes resources. For custom resources, the CustomResourceDefinition first needs to be converted to JSON Schema. A script is provided to convert these CustomResourceDefinitions to JSON schema. Here is an example how to use it:
|
||||
#!/bin/bash $ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml JSON schema written to trainingjob_v1.json The FILENAME_FORMAT environment variable can be used to change the output file name (Available variables: kind, group, version) (Default: {kind}_{version}).</description></item><item><title>Github Action</title><link>http://kubeconform.mandragor.org/docs/usage-as-github-action/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/usage-as-github-action/</guid><description>Kubeconform is publishes Docker Images to Github&rsquo;s new Container Registry, ghcr.io. These images can be used directly in a Github Action, once logged in using a Github Token.
|
||||
name: kubeconform on: push jobs: kubeconform: runs-on: ubuntu-latest steps: - name: login to Github Packages run: echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin - uses: actions/checkout@v2 - uses: docker://ghcr.io/yannh/kubeconform:master with: entrypoint: '/kubeconform' args: "-summary -output json kubeconfigs/" Note on pricing: Kubeconform relies on Github Container Registry which is currently in Beta.</description></item><item><title>Kubeconform as a Go module</title><link>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</guid><description>Warning: This is a work-in-progress, the interface is not yet considered stable. Feedback is encouraged.
|
||||
#!/bin/bash $ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml JSON schema written to trainingjob_v1.json The FILENAME_FORMAT environment variable can be used to change the output file name (Available variables: kind, group, version) (Default: {kind}_{version}).</description></item><item><title>GitHub Action</title><link>http://kubeconform.mandragor.org/docs/usage-as-github-action/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/usage-as-github-action/</guid><description>Kubeconform is publishes Docker Images to GitHub&rsquo;s new Container Registry, ghcr.io. These images can be used directly in a GitHub Action, once logged in using a GitHub Token.
|
||||
name: kubeconform on: push jobs: kubeconform: runs-on: ubuntu-latest steps: - name: login to GitHub Packages run: echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin - uses: actions/checkout@v2 - uses: docker://ghcr.io/yannh/kubeconform:master with: entrypoint: '/kubeconform' args: "-summary -output json kubeconfigs/" Note on pricing: Kubeconform relies on GitHub Container Registry which is currently in Beta.</description></item><item><title>Kubeconform as a Go module</title><link>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</guid><description>Warning: This is a work-in-progress, the interface is not yet considered stable. Feedback is encouraged.
|
||||
Kubeconform contains a package that can be used as a library. An example of usage can be found in examples/main.go
|
||||
Additional documentation on pkg.go.dev</description></item><item><title>About</title><link>http://kubeconform.mandragor.org/about/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/about/</guid><description>Kubeconform is a Kubernetes manifests validation tool. Build it into your CI to validate your Kubernetes configuration!
|
||||
It is inspired by, contains code from and is designed to stay close to Kubeval, but with the following improvements:
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
</div>
|
||||
|
||||
</div><div id="footer">
|
||||
<h3>Github</h3>
|
||||
<h3>GitHub</h3>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ For example, for Linux on x86_64 architecture:
|
|||
curl -L https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz | tar xvzf - && \ sudo mv kubeconform /usr/local/bin/ MacOs Kubeconform is available to install using Homebrew: $ brew install kubeconform
|
||||
Windows Download the latest release from our release page.</description></item><item><title>Usage</title><link>http://kubeconform.mandragor.org/docs/usage/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/usage/</guid><description>$ ./bin/kubeconform -h Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]... -cache string cache schemas downloaded via HTTP to this folder -cpu-prof string debug - log CPU profiling to file -exit-on-error immediately stop execution when the first error is encountered -h show help information -ignore-filename-pattern value regular expression specifying paths to ignore (can be specified multiple times) -ignore-missing-schemas skip files with missing schemas instead of failing -insecure-skip-tls-verify disable verification of the server's SSL certificate.</description></item><item><title>Custom Resources support</title><link>http://kubeconform.mandragor.org/docs/crd-support/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/crd-support/</guid><description>When the -schema-location parameter is not used, or set to &ldquo;default&rdquo;, kubeconform will default to downloading schemas from https://github.com/yannh/kubernetes-json-schema. Kubeconform however supports passing one, or multiple, schemas locations - HTTP(s) URLs, or local filesystem paths, in which case it will lookup for schema definitions in each of them, in order, stopping as soon as a matching file is found.
|
||||
If the -schema-location value does not end with &lsquo;.json&rsquo;, Kubeconform will assume filenames / a file structure identical to that of kubernetesjsonschema.</description></item><item><title>OpenAPI to JSON Schema conversion</title><link>http://kubeconform.mandragor.org/docs/json-schema-conversion/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/json-schema-conversion/</guid><description>Kubeconform uses JSON schemas to validate Kubernetes resources. For custom resources, the CustomResourceDefinition first needs to be converted to JSON Schema. A script is provided to convert these CustomResourceDefinitions to JSON schema. Here is an example how to use it:
|
||||
#!/bin/bash $ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml JSON schema written to trainingjob_v1.json The FILENAME_FORMAT environment variable can be used to change the output file name (Available variables: kind, group, version) (Default: {kind}_{version}).</description></item><item><title>Github Action</title><link>http://kubeconform.mandragor.org/docs/usage-as-github-action/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/usage-as-github-action/</guid><description>Kubeconform is publishes Docker Images to Github&rsquo;s new Container Registry, ghcr.io. These images can be used directly in a Github Action, once logged in using a Github Token.
|
||||
name: kubeconform on: push jobs: kubeconform: runs-on: ubuntu-latest steps: - name: login to Github Packages run: echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin - uses: actions/checkout@v2 - uses: docker://ghcr.io/yannh/kubeconform:master with: entrypoint: '/kubeconform' args: "-summary -output json kubeconfigs/" Note on pricing: Kubeconform relies on Github Container Registry which is currently in Beta.</description></item><item><title>Kubeconform as a Go module</title><link>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</guid><description>Warning: This is a work-in-progress, the interface is not yet considered stable. Feedback is encouraged.
|
||||
#!/bin/bash $ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml JSON schema written to trainingjob_v1.json The FILENAME_FORMAT environment variable can be used to change the output file name (Available variables: kind, group, version) (Default: {kind}_{version}).</description></item><item><title>GitHub Action</title><link>http://kubeconform.mandragor.org/docs/usage-as-github-action/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/usage-as-github-action/</guid><description>Kubeconform is publishes Docker Images to GitHub&rsquo;s new Container Registry, ghcr.io. These images can be used directly in a GitHub Action, once logged in using a GitHub Token.
|
||||
name: kubeconform on: push jobs: kubeconform: runs-on: ubuntu-latest steps: - name: login to GitHub Packages run: echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin - uses: actions/checkout@v2 - uses: docker://ghcr.io/yannh/kubeconform:master with: entrypoint: '/kubeconform' args: "-summary -output json kubeconfigs/" Note on pricing: Kubeconform relies on GitHub Container Registry which is currently in Beta.</description></item><item><title>Kubeconform as a Go module</title><link>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</guid><description>Warning: This is a work-in-progress, the interface is not yet considered stable. Feedback is encouraged.
|
||||
Kubeconform contains a package that can be used as a library. An example of usage can be found in examples/main.go
|
||||
Additional documentation on pkg.go.dev</description></item><item><title>About</title><link>http://kubeconform.mandragor.org/about/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/about/</guid><description>Kubeconform is a Kubernetes manifests validation tool. Build it into your CI to validate your Kubernetes configuration!
|
||||
It is inspired by, contains code from and is designed to stay close to Kubeval, but with the following improvements:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Usage on Kubeconform - Fast Kubernetes manifests validation!</title><link>http://kubeconform.mandragor.org/tags/usage/</link><description>Recent content in Usage on Kubeconform - Fast Kubernetes manifests validation!</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Fri, 02 Jul 2021 00:00:00 +0000</lastBuildDate><atom:link href="http://kubeconform.mandragor.org/tags/usage/index.xml" rel="self" type="application/rss+xml"/><item><title>Usage</title><link>http://kubeconform.mandragor.org/docs/usage/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/usage/</guid><description>$ ./bin/kubeconform -h Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]... -cache string cache schemas downloaded via HTTP to this folder -cpu-prof string debug - log CPU profiling to file -exit-on-error immediately stop execution when the first error is encountered -h show help information -ignore-filename-pattern value regular expression specifying paths to ignore (can be specified multiple times) -ignore-missing-schemas skip files with missing schemas instead of failing -insecure-skip-tls-verify disable verification of the server's SSL certificate.</description></item><item><title>Custom Resources support</title><link>http://kubeconform.mandragor.org/docs/crd-support/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/crd-support/</guid><description>When the -schema-location parameter is not used, or set to &ldquo;default&rdquo;, kubeconform will default to downloading schemas from https://github.com/yannh/kubernetes-json-schema. Kubeconform however supports passing one, or multiple, schemas locations - HTTP(s) URLs, or local filesystem paths, in which case it will lookup for schema definitions in each of them, in order, stopping as soon as a matching file is found.
|
||||
If the -schema-location value does not end with &lsquo;.json&rsquo;, Kubeconform will assume filenames / a file structure identical to that of kubernetesjsonschema.</description></item><item><title>OpenAPI to JSON Schema conversion</title><link>http://kubeconform.mandragor.org/docs/json-schema-conversion/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/json-schema-conversion/</guid><description>Kubeconform uses JSON schemas to validate Kubernetes resources. For custom resources, the CustomResourceDefinition first needs to be converted to JSON Schema. A script is provided to convert these CustomResourceDefinitions to JSON schema. Here is an example how to use it:
|
||||
#!/bin/bash $ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml JSON schema written to trainingjob_v1.json The FILENAME_FORMAT environment variable can be used to change the output file name (Available variables: kind, group, version) (Default: {kind}_{version}).</description></item><item><title>Github Action</title><link>http://kubeconform.mandragor.org/docs/usage-as-github-action/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/usage-as-github-action/</guid><description>Kubeconform is publishes Docker Images to Github&rsquo;s new Container Registry, ghcr.io. These images can be used directly in a Github Action, once logged in using a Github Token.
|
||||
name: kubeconform on: push jobs: kubeconform: runs-on: ubuntu-latest steps: - name: login to Github Packages run: echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin - uses: actions/checkout@v2 - uses: docker://ghcr.io/yannh/kubeconform:master with: entrypoint: '/kubeconform' args: "-summary -output json kubeconfigs/" Note on pricing: Kubeconform relies on Github Container Registry which is currently in Beta.</description></item><item><title>Kubeconform as a Go module</title><link>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</guid><description>Warning: This is a work-in-progress, the interface is not yet considered stable. Feedback is encouraged.
|
||||
#!/bin/bash $ ./scripts/openapi2jsonschema.py https://raw.githubusercontent.com/aws/amazon-sagemaker-operator-for-k8s/master/config/crd/bases/sagemaker.aws.amazon.com_trainingjobs.yaml JSON schema written to trainingjob_v1.json The FILENAME_FORMAT environment variable can be used to change the output file name (Available variables: kind, group, version) (Default: {kind}_{version}).</description></item><item><title>GitHub Action</title><link>http://kubeconform.mandragor.org/docs/usage-as-github-action/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/usage-as-github-action/</guid><description>Kubeconform is publishes Docker Images to GitHub&rsquo;s new Container Registry, ghcr.io. These images can be used directly in a GitHub Action, once logged in using a GitHub Token.
|
||||
name: kubeconform on: push jobs: kubeconform: runs-on: ubuntu-latest steps: - name: login to GitHub Packages run: echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin - uses: actions/checkout@v2 - uses: docker://ghcr.io/yannh/kubeconform:master with: entrypoint: '/kubeconform' args: "-summary -output json kubeconfigs/" Note on pricing: Kubeconform relies on GitHub Container Registry which is currently in Beta.</description></item><item><title>Kubeconform as a Go module</title><link>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</link><pubDate>Fri, 02 Jul 2021 00:00:00 +0000</pubDate><guid>http://kubeconform.mandragor.org/docs/using-as-a-go-module/</guid><description>Warning: This is a work-in-progress, the interface is not yet considered stable. Feedback is encouraged.
|
||||
Kubeconform contains a package that can be used as a library. An example of usage can be found in examples/main.go
|
||||
Additional documentation on pkg.go.dev</description></item></channel></rss>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<div id="header">
|
||||
<ul id="navigation">
|
||||
<li><a href="/about">About</a></li>
|
||||
<li><a href="https://github.com/yannh/kubeconform/">Github</a></li>
|
||||
<li><a href="https://github.com/yannh/kubeconform/">GitHub</a></li>
|
||||
<li><a href="/docs/installation/">Docs</a></li>
|
||||
<li><a href="/">Home</a></li>
|
||||
</ul>
|
||||
|
|
|
|||
14
vendor/github.com/beevik/etree/.travis.yml
generated
vendored
14
vendor/github.com/beevik/etree/.travis.yml
generated
vendored
|
|
@ -1,14 +0,0 @@
|
|||
language: go
|
||||
sudo: false
|
||||
|
||||
go:
|
||||
- 1.11.x
|
||||
- tip
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
|
||||
script:
|
||||
- go vet ./...
|
||||
- go test -v ./...
|
||||
10
vendor/github.com/beevik/etree/CONTRIBUTORS
generated
vendored
10
vendor/github.com/beevik/etree/CONTRIBUTORS
generated
vendored
|
|
@ -1,10 +0,0 @@
|
|||
Brett Vickers (beevik)
|
||||
Felix Geisendörfer (felixge)
|
||||
Kamil Kisiel (kisielk)
|
||||
Graham King (grahamking)
|
||||
Matt Smith (ma314smith)
|
||||
Michal Jemala (michaljemala)
|
||||
Nicolas Piganeau (npiganeau)
|
||||
Chris Brown (ccbrown)
|
||||
Earncef Sequeira (earncef)
|
||||
Gabriel de Labachelerie (wuzuf)
|
||||
24
vendor/github.com/beevik/etree/LICENSE
generated
vendored
24
vendor/github.com/beevik/etree/LICENSE
generated
vendored
|
|
@ -1,24 +0,0 @@
|
|||
Copyright 2015-2019 Brett Vickers. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER ``AS IS'' AND ANY
|
||||
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
||||
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
205
vendor/github.com/beevik/etree/README.md
generated
vendored
205
vendor/github.com/beevik/etree/README.md
generated
vendored
|
|
@ -1,205 +0,0 @@
|
|||
[](https://travis-ci.org/beevik/etree)
|
||||
[](https://godoc.org/github.com/beevik/etree)
|
||||
|
||||
etree
|
||||
=====
|
||||
|
||||
The etree package is a lightweight, pure go package that expresses XML in
|
||||
the form of an element tree. Its design was inspired by the Python
|
||||
[ElementTree](http://docs.python.org/2/library/xml.etree.elementtree.html)
|
||||
module.
|
||||
|
||||
Some of the package's capabilities and features:
|
||||
|
||||
* Represents XML documents as trees of elements for easy traversal.
|
||||
* Imports, serializes, modifies or creates XML documents from scratch.
|
||||
* Writes and reads XML to/from files, byte slices, strings and io interfaces.
|
||||
* Performs simple or complex searches with lightweight XPath-like query APIs.
|
||||
* Auto-indents XML using spaces or tabs for better readability.
|
||||
* Implemented in pure go; depends only on standard go libraries.
|
||||
* Built on top of the go [encoding/xml](http://golang.org/pkg/encoding/xml)
|
||||
package.
|
||||
|
||||
### Creating an XML document
|
||||
|
||||
The following example creates an XML document from scratch using the etree
|
||||
package and outputs its indented contents to stdout.
|
||||
```go
|
||||
doc := etree.NewDocument()
|
||||
doc.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`)
|
||||
doc.CreateProcInst("xml-stylesheet", `type="text/xsl" href="style.xsl"`)
|
||||
|
||||
people := doc.CreateElement("People")
|
||||
people.CreateComment("These are all known people")
|
||||
|
||||
jon := people.CreateElement("Person")
|
||||
jon.CreateAttr("name", "Jon")
|
||||
|
||||
sally := people.CreateElement("Person")
|
||||
sally.CreateAttr("name", "Sally")
|
||||
|
||||
doc.Indent(2)
|
||||
doc.WriteTo(os.Stdout)
|
||||
```
|
||||
|
||||
Output:
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xml-stylesheet type="text/xsl" href="style.xsl"?>
|
||||
<People>
|
||||
<!--These are all known people-->
|
||||
<Person name="Jon"/>
|
||||
<Person name="Sally"/>
|
||||
</People>
|
||||
```
|
||||
|
||||
### Reading an XML file
|
||||
|
||||
Suppose you have a file on disk called `bookstore.xml` containing the
|
||||
following data:
|
||||
|
||||
```xml
|
||||
<bookstore xmlns:p="urn:schemas-books-com:prices">
|
||||
|
||||
<book category="COOKING">
|
||||
<title lang="en">Everyday Italian</title>
|
||||
<author>Giada De Laurentiis</author>
|
||||
<year>2005</year>
|
||||
<p:price>30.00</p:price>
|
||||
</book>
|
||||
|
||||
<book category="CHILDREN">
|
||||
<title lang="en">Harry Potter</title>
|
||||
<author>J K. Rowling</author>
|
||||
<year>2005</year>
|
||||
<p:price>29.99</p:price>
|
||||
</book>
|
||||
|
||||
<book category="WEB">
|
||||
<title lang="en">XQuery Kick Start</title>
|
||||
<author>James McGovern</author>
|
||||
<author>Per Bothner</author>
|
||||
<author>Kurt Cagle</author>
|
||||
<author>James Linn</author>
|
||||
<author>Vaidyanathan Nagarajan</author>
|
||||
<year>2003</year>
|
||||
<p:price>49.99</p:price>
|
||||
</book>
|
||||
|
||||
<book category="WEB">
|
||||
<title lang="en">Learning XML</title>
|
||||
<author>Erik T. Ray</author>
|
||||
<year>2003</year>
|
||||
<p:price>39.95</p:price>
|
||||
</book>
|
||||
|
||||
</bookstore>
|
||||
```
|
||||
|
||||
This code reads the file's contents into an etree document.
|
||||
```go
|
||||
doc := etree.NewDocument()
|
||||
if err := doc.ReadFromFile("bookstore.xml"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
```
|
||||
|
||||
You can also read XML from a string, a byte slice, or an `io.Reader`.
|
||||
|
||||
### Processing elements and attributes
|
||||
|
||||
This example illustrates several ways to access elements and attributes using
|
||||
etree selection queries.
|
||||
```go
|
||||
root := doc.SelectElement("bookstore")
|
||||
fmt.Println("ROOT element:", root.Tag)
|
||||
|
||||
for _, book := range root.SelectElements("book") {
|
||||
fmt.Println("CHILD element:", book.Tag)
|
||||
if title := book.SelectElement("title"); title != nil {
|
||||
lang := title.SelectAttrValue("lang", "unknown")
|
||||
fmt.Printf(" TITLE: %s (%s)\n", title.Text(), lang)
|
||||
}
|
||||
for _, attr := range book.Attr {
|
||||
fmt.Printf(" ATTR: %s=%s\n", attr.Key, attr.Value)
|
||||
}
|
||||
}
|
||||
```
|
||||
Output:
|
||||
```
|
||||
ROOT element: bookstore
|
||||
CHILD element: book
|
||||
TITLE: Everyday Italian (en)
|
||||
ATTR: category=COOKING
|
||||
CHILD element: book
|
||||
TITLE: Harry Potter (en)
|
||||
ATTR: category=CHILDREN
|
||||
CHILD element: book
|
||||
TITLE: XQuery Kick Start (en)
|
||||
ATTR: category=WEB
|
||||
CHILD element: book
|
||||
TITLE: Learning XML (en)
|
||||
ATTR: category=WEB
|
||||
```
|
||||
|
||||
### Path queries
|
||||
|
||||
This example uses etree's path functions to select all book titles that fall
|
||||
into the category of 'WEB'. The double-slash prefix in the path causes the
|
||||
search for book elements to occur recursively; book elements may appear at any
|
||||
level of the XML hierarchy.
|
||||
```go
|
||||
for _, t := range doc.FindElements("//book[@category='WEB']/title") {
|
||||
fmt.Println("Title:", t.Text())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
Title: XQuery Kick Start
|
||||
Title: Learning XML
|
||||
```
|
||||
|
||||
This example finds the first book element under the root bookstore element and
|
||||
outputs the tag and text of each of its child elements.
|
||||
```go
|
||||
for _, e := range doc.FindElements("./bookstore/book[1]/*") {
|
||||
fmt.Printf("%s: %s\n", e.Tag, e.Text())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
title: Everyday Italian
|
||||
author: Giada De Laurentiis
|
||||
year: 2005
|
||||
price: 30.00
|
||||
```
|
||||
|
||||
This example finds all books with a price of 49.99 and outputs their titles.
|
||||
```go
|
||||
path := etree.MustCompilePath("./bookstore/book[p:price='49.99']/title")
|
||||
for _, e := range doc.FindElementsPath(path) {
|
||||
fmt.Println(e.Text())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
XQuery Kick Start
|
||||
```
|
||||
|
||||
Note that this example uses the FindElementsPath function, which takes as an
|
||||
argument a pre-compiled path object. Use precompiled paths when you plan to
|
||||
search with the same path more than once.
|
||||
|
||||
### Other features
|
||||
|
||||
These are just a few examples of the things the etree package can do. See the
|
||||
[documentation](http://godoc.org/github.com/beevik/etree) for a complete
|
||||
description of its capabilities.
|
||||
|
||||
### Contributing
|
||||
|
||||
This project accepts contributions. Just fork the repo and submit a pull
|
||||
request!
|
||||
109
vendor/github.com/beevik/etree/RELEASE_NOTES.md
generated
vendored
109
vendor/github.com/beevik/etree/RELEASE_NOTES.md
generated
vendored
|
|
@ -1,109 +0,0 @@
|
|||
Release v1.1.0
|
||||
==============
|
||||
|
||||
**New Features**
|
||||
|
||||
* New attribute helpers.
|
||||
* Added the `Element.SortAttrs` method, which lexicographically sorts an
|
||||
element's attributes by key.
|
||||
* New `ReadSettings` properties.
|
||||
* Added `Entity` for the support of custom entity maps.
|
||||
* New `WriteSettings` properties.
|
||||
* Added `UseCRLF` to allow the output of CR-LF newlines instead of the
|
||||
default LF newlines. This is useful on Windows systems.
|
||||
* Additional support for text and CDATA sections.
|
||||
* The `Element.Text` method now returns the concatenation of all consecutive
|
||||
character data tokens immediately following an element's opening tag.
|
||||
* Added `Element.SetCData` to replace the character data immediately
|
||||
following an element's opening tag with a CDATA section.
|
||||
* Added `Element.CreateCData` to create and add a CDATA section child
|
||||
`CharData` token to an element.
|
||||
* Added `Element.CreateText` to create and add a child text `CharData` token
|
||||
to an element.
|
||||
* Added `NewCData` to create a parentless CDATA section `CharData` token.
|
||||
* Added `NewText` to create a parentless text `CharData`
|
||||
token.
|
||||
* Added `CharData.IsCData` to detect if the token contains a CDATA section.
|
||||
* Added `CharData.IsWhitespace` to detect if the token contains whitespace
|
||||
inserted by one of the document Indent functions.
|
||||
* Modified `Element.SetText` so that it replaces a run of consecutive
|
||||
character data tokens following the element's opening tag (instead of just
|
||||
the first one).
|
||||
* New "tail text" support.
|
||||
* Added the `Element.Tail` method, which returns the text immediately
|
||||
following an element's closing tag.
|
||||
* Added the `Element.SetTail` method, which modifies the text immediately
|
||||
following an element's closing tag.
|
||||
* New element child insertion and removal methods.
|
||||
* Added the `Element.InsertChildAt` method, which inserts a new child token
|
||||
before the specified child token index.
|
||||
* Added the `Element.RemoveChildAt` method, which removes the child token at
|
||||
the specified child token index.
|
||||
* New element and attribute queries.
|
||||
* Added the `Element.Index` method, which returns the element's index within
|
||||
its parent element's child token list.
|
||||
* Added the `Element.NamespaceURI` method to return the namespace URI
|
||||
associated with an element.
|
||||
* Added the `Attr.NamespaceURI` method to return the namespace URI
|
||||
associated with an element.
|
||||
* Added the `Attr.Element` method to return the element that an attribute
|
||||
belongs to.
|
||||
* New Path filter functions.
|
||||
* Added `[local-name()='val']` to keep elements whose unprefixed tag matches
|
||||
the desired value.
|
||||
* Added `[name()='val']` to keep elements whose full tag matches the desired
|
||||
value.
|
||||
* Added `[namespace-prefix()='val']` to keep elements whose namespace prefix
|
||||
matches the desired value.
|
||||
* Added `[namespace-uri()='val']` to keep elements whose namespace URI
|
||||
matches the desired value.
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
* A default XML `CharSetReader` is now used to prevent failed parsing of XML
|
||||
documents using certain encodings.
|
||||
([Issue](https://github.com/beevik/etree/issues/53)).
|
||||
* All characters are now properly escaped according to XML parsing rules.
|
||||
([Issue](https://github.com/beevik/etree/issues/55)).
|
||||
* The `Document.Indent` and `Document.IndentTabs` functions no longer insert
|
||||
empty string `CharData` tokens.
|
||||
|
||||
**Deprecated**
|
||||
|
||||
* `Element`
|
||||
* The `InsertChild` method is deprecated. Use `InsertChildAt` instead.
|
||||
* The `CreateCharData` method is deprecated. Use `CreateText` instead.
|
||||
* `CharData`
|
||||
* The `NewCharData` method is deprecated. Use `NewText` instead.
|
||||
|
||||
|
||||
Release v1.0.1
|
||||
==============
|
||||
|
||||
**Changes**
|
||||
|
||||
* Added support for absolute etree Path queries. An absolute path begins with
|
||||
`/` or `//` and begins its search from the element's document root.
|
||||
* Added [`GetPath`](https://godoc.org/github.com/beevik/etree#Element.GetPath)
|
||||
and [`GetRelativePath`](https://godoc.org/github.com/beevik/etree#Element.GetRelativePath)
|
||||
functions to the [`Element`](https://godoc.org/github.com/beevik/etree#Element)
|
||||
type.
|
||||
|
||||
**Breaking changes**
|
||||
|
||||
* A path starting with `//` is now interpreted as an absolute path.
|
||||
Previously, it was interpreted as a relative path starting from the element
|
||||
whose
|
||||
[`FindElement`](https://godoc.org/github.com/beevik/etree#Element.FindElement)
|
||||
method was called. To remain compatible with this release, all paths
|
||||
prefixed with `//` should be prefixed with `.//` when called from any
|
||||
element other than the document's root.
|
||||
* [**edit 2/1/2019**]: Minor releases should not contain breaking changes.
|
||||
Even though this breaking change was very minor, it was a mistake to include
|
||||
it in this minor release. In the future, all breaking changes will be
|
||||
limited to major releases (e.g., version 2.0.0).
|
||||
|
||||
Release v1.0.0
|
||||
==============
|
||||
|
||||
Initial release.
|
||||
1453
vendor/github.com/beevik/etree/etree.go
generated
vendored
1453
vendor/github.com/beevik/etree/etree.go
generated
vendored
File diff suppressed because it is too large
Load diff
276
vendor/github.com/beevik/etree/helpers.go
generated
vendored
276
vendor/github.com/beevik/etree/helpers.go
generated
vendored
|
|
@ -1,276 +0,0 @@
|
|||
// Copyright 2015-2019 Brett Vickers.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package etree
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// A simple stack
|
||||
type stack struct {
|
||||
data []interface{}
|
||||
}
|
||||
|
||||
func (s *stack) empty() bool {
|
||||
return len(s.data) == 0
|
||||
}
|
||||
|
||||
func (s *stack) push(value interface{}) {
|
||||
s.data = append(s.data, value)
|
||||
}
|
||||
|
||||
func (s *stack) pop() interface{} {
|
||||
value := s.data[len(s.data)-1]
|
||||
s.data[len(s.data)-1] = nil
|
||||
s.data = s.data[:len(s.data)-1]
|
||||
return value
|
||||
}
|
||||
|
||||
func (s *stack) peek() interface{} {
|
||||
return s.data[len(s.data)-1]
|
||||
}
|
||||
|
||||
// A fifo is a simple first-in-first-out queue.
|
||||
type fifo struct {
|
||||
data []interface{}
|
||||
head, tail int
|
||||
}
|
||||
|
||||
func (f *fifo) add(value interface{}) {
|
||||
if f.len()+1 >= len(f.data) {
|
||||
f.grow()
|
||||
}
|
||||
f.data[f.tail] = value
|
||||
if f.tail++; f.tail == len(f.data) {
|
||||
f.tail = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fifo) remove() interface{} {
|
||||
value := f.data[f.head]
|
||||
f.data[f.head] = nil
|
||||
if f.head++; f.head == len(f.data) {
|
||||
f.head = 0
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (f *fifo) len() int {
|
||||
if f.tail >= f.head {
|
||||
return f.tail - f.head
|
||||
}
|
||||
return len(f.data) - f.head + f.tail
|
||||
}
|
||||
|
||||
func (f *fifo) grow() {
|
||||
c := len(f.data) * 2
|
||||
if c == 0 {
|
||||
c = 4
|
||||
}
|
||||
buf, count := make([]interface{}, c), f.len()
|
||||
if f.tail >= f.head {
|
||||
copy(buf[0:count], f.data[f.head:f.tail])
|
||||
} else {
|
||||
hindex := len(f.data) - f.head
|
||||
copy(buf[0:hindex], f.data[f.head:])
|
||||
copy(buf[hindex:count], f.data[:f.tail])
|
||||
}
|
||||
f.data, f.head, f.tail = buf, 0, count
|
||||
}
|
||||
|
||||
// countReader implements a proxy reader that counts the number of
|
||||
// bytes read from its encapsulated reader.
|
||||
type countReader struct {
|
||||
r io.Reader
|
||||
bytes int64
|
||||
}
|
||||
|
||||
func newCountReader(r io.Reader) *countReader {
|
||||
return &countReader{r: r}
|
||||
}
|
||||
|
||||
func (cr *countReader) Read(p []byte) (n int, err error) {
|
||||
b, err := cr.r.Read(p)
|
||||
cr.bytes += int64(b)
|
||||
return b, err
|
||||
}
|
||||
|
||||
// countWriter implements a proxy writer that counts the number of
|
||||
// bytes written by its encapsulated writer.
|
||||
type countWriter struct {
|
||||
w io.Writer
|
||||
bytes int64
|
||||
}
|
||||
|
||||
func newCountWriter(w io.Writer) *countWriter {
|
||||
return &countWriter{w: w}
|
||||
}
|
||||
|
||||
func (cw *countWriter) Write(p []byte) (n int, err error) {
|
||||
b, err := cw.w.Write(p)
|
||||
cw.bytes += int64(b)
|
||||
return b, err
|
||||
}
|
||||
|
||||
// isWhitespace returns true if the byte slice contains only
|
||||
// whitespace characters.
|
||||
func isWhitespace(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if c := s[i]; c != ' ' && c != '\t' && c != '\n' && c != '\r' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// spaceMatch returns true if namespace a is the empty string
|
||||
// or if namespace a equals namespace b.
|
||||
func spaceMatch(a, b string) bool {
|
||||
switch {
|
||||
case a == "":
|
||||
return true
|
||||
default:
|
||||
return a == b
|
||||
}
|
||||
}
|
||||
|
||||
// spaceDecompose breaks a namespace:tag identifier at the ':'
|
||||
// and returns the two parts.
|
||||
func spaceDecompose(str string) (space, key string) {
|
||||
colon := strings.IndexByte(str, ':')
|
||||
if colon == -1 {
|
||||
return "", str
|
||||
}
|
||||
return str[:colon], str[colon+1:]
|
||||
}
|
||||
|
||||
// Strings used by indentCRLF and indentLF
|
||||
const (
|
||||
indentSpaces = "\r\n "
|
||||
indentTabs = "\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"
|
||||
)
|
||||
|
||||
// indentCRLF returns a CRLF newline followed by n copies of the first
|
||||
// non-CRLF character in the source string.
|
||||
func indentCRLF(n int, source string) string {
|
||||
switch {
|
||||
case n < 0:
|
||||
return source[:2]
|
||||
case n < len(source)-1:
|
||||
return source[:n+2]
|
||||
default:
|
||||
return source + strings.Repeat(source[2:3], n-len(source)+2)
|
||||
}
|
||||
}
|
||||
|
||||
// indentLF returns a LF newline followed by n copies of the first non-LF
|
||||
// character in the source string.
|
||||
func indentLF(n int, source string) string {
|
||||
switch {
|
||||
case n < 0:
|
||||
return source[1:2]
|
||||
case n < len(source)-1:
|
||||
return source[1 : n+2]
|
||||
default:
|
||||
return source[1:] + strings.Repeat(source[2:3], n-len(source)+2)
|
||||
}
|
||||
}
|
||||
|
||||
// nextIndex returns the index of the next occurrence of sep in s,
|
||||
// starting from offset. It returns -1 if the sep string is not found.
|
||||
func nextIndex(s, sep string, offset int) int {
|
||||
switch i := strings.Index(s[offset:], sep); i {
|
||||
case -1:
|
||||
return -1
|
||||
default:
|
||||
return offset + i
|
||||
}
|
||||
}
|
||||
|
||||
// isInteger returns true if the string s contains an integer.
|
||||
func isInteger(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if (s[i] < '0' || s[i] > '9') && !(i == 0 && s[i] == '-') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type escapeMode byte
|
||||
|
||||
const (
|
||||
escapeNormal escapeMode = iota
|
||||
escapeCanonicalText
|
||||
escapeCanonicalAttr
|
||||
)
|
||||
|
||||
// escapeString writes an escaped version of a string to the writer.
|
||||
func escapeString(w *bufio.Writer, s string, m escapeMode) {
|
||||
var esc []byte
|
||||
last := 0
|
||||
for i := 0; i < len(s); {
|
||||
r, width := utf8.DecodeRuneInString(s[i:])
|
||||
i += width
|
||||
switch r {
|
||||
case '&':
|
||||
esc = []byte("&")
|
||||
case '<':
|
||||
esc = []byte("<")
|
||||
case '>':
|
||||
if m == escapeCanonicalAttr {
|
||||
continue
|
||||
}
|
||||
esc = []byte(">")
|
||||
case '\'':
|
||||
if m != escapeNormal {
|
||||
continue
|
||||
}
|
||||
esc = []byte("'")
|
||||
case '"':
|
||||
if m == escapeCanonicalText {
|
||||
continue
|
||||
}
|
||||
esc = []byte(""")
|
||||
case '\t':
|
||||
if m != escapeCanonicalAttr {
|
||||
continue
|
||||
}
|
||||
esc = []byte("	")
|
||||
case '\n':
|
||||
if m != escapeCanonicalAttr {
|
||||
continue
|
||||
}
|
||||
esc = []byte("
")
|
||||
case '\r':
|
||||
if m == escapeNormal {
|
||||
continue
|
||||
}
|
||||
esc = []byte("
")
|
||||
default:
|
||||
if !isInCharacterRange(r) || (r == 0xFFFD && width == 1) {
|
||||
esc = []byte("\uFFFD")
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
w.WriteString(s[last : i-width])
|
||||
w.Write(esc)
|
||||
last = i
|
||||
}
|
||||
w.WriteString(s[last:])
|
||||
}
|
||||
|
||||
func isInCharacterRange(r rune) bool {
|
||||
return r == 0x09 ||
|
||||
r == 0x0A ||
|
||||
r == 0x0D ||
|
||||
r >= 0x20 && r <= 0xD7FF ||
|
||||
r >= 0xE000 && r <= 0xFFFD ||
|
||||
r >= 0x10000 && r <= 0x10FFFF
|
||||
}
|
||||
582
vendor/github.com/beevik/etree/path.go
generated
vendored
582
vendor/github.com/beevik/etree/path.go
generated
vendored
|
|
@ -1,582 +0,0 @@
|
|||
// Copyright 2015-2019 Brett Vickers.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package etree
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
A Path is a string that represents a search path through an etree starting
|
||||
from the document root or an arbitrary element. Paths are used with the
|
||||
Element object's Find* methods to locate and return desired elements.
|
||||
|
||||
A Path consists of a series of slash-separated "selectors", each of which may
|
||||
be modified by one or more bracket-enclosed "filters". Selectors are used to
|
||||
traverse the etree from element to element, while filters are used to narrow
|
||||
the list of candidate elements at each node.
|
||||
|
||||
Although etree Path strings are similar to XPath strings
|
||||
(https://www.w3.org/TR/1999/REC-xpath-19991116/), they have a more limited set
|
||||
of selectors and filtering options.
|
||||
|
||||
The following selectors are supported by etree Path strings:
|
||||
|
||||
. Select the current element.
|
||||
.. Select the parent of the current element.
|
||||
* Select all child elements of the current element.
|
||||
/ Select the root element when used at the start of a path.
|
||||
// Select all descendants of the current element.
|
||||
tag Select all child elements with a name matching the tag.
|
||||
|
||||
The following basic filters are supported by etree Path strings:
|
||||
|
||||
[@attrib] Keep elements with an attribute named attrib.
|
||||
[@attrib='val'] Keep elements with an attribute named attrib and value matching val.
|
||||
[tag] Keep elements with a child element named tag.
|
||||
[tag='val'] Keep elements with a child element named tag and text matching val.
|
||||
[n] Keep the n-th element, where n is a numeric index starting from 1.
|
||||
|
||||
The following function filters are also supported:
|
||||
|
||||
[text()] Keep elements with non-empty text.
|
||||
[text()='val'] Keep elements whose text matches val.
|
||||
[local-name()='val'] Keep elements whose un-prefixed tag matches val.
|
||||
[name()='val'] Keep elements whose full tag exactly matches val.
|
||||
[namespace-prefix()='val'] Keep elements whose namespace prefix matches val.
|
||||
[namespace-uri()='val'] Keep elements whose namespace URI matches val.
|
||||
|
||||
Here are some examples of Path strings:
|
||||
|
||||
- Select the bookstore child element of the root element:
|
||||
/bookstore
|
||||
|
||||
- Beginning from the root element, select the title elements of all
|
||||
descendant book elements having a 'category' attribute of 'WEB':
|
||||
//book[@category='WEB']/title
|
||||
|
||||
- Beginning from the current element, select the first descendant
|
||||
book element with a title child element containing the text 'Great
|
||||
Expectations':
|
||||
.//book[title='Great Expectations'][1]
|
||||
|
||||
- Beginning from the current element, select all child elements of
|
||||
book elements with an attribute 'language' set to 'english':
|
||||
./book/*[@language='english']
|
||||
|
||||
- Beginning from the current element, select all child elements of
|
||||
book elements containing the text 'special':
|
||||
./book/*[text()='special']
|
||||
|
||||
- Beginning from the current element, select all descendant book
|
||||
elements whose title child element has a 'language' attribute of 'french':
|
||||
.//book/title[@language='french']/..
|
||||
|
||||
- Beginning from the current element, select all book elements
|
||||
belonging to the http://www.w3.org/TR/html4/ namespace:
|
||||
.//book[namespace-uri()='http://www.w3.org/TR/html4/']
|
||||
|
||||
*/
|
||||
type Path struct {
|
||||
segments []segment
|
||||
}
|
||||
|
||||
// ErrPath is returned by path functions when an invalid etree path is provided.
|
||||
type ErrPath string
|
||||
|
||||
// Error returns the string describing a path error.
|
||||
func (err ErrPath) Error() string {
|
||||
return "etree: " + string(err)
|
||||
}
|
||||
|
||||
// CompilePath creates an optimized version of an XPath-like string that
|
||||
// can be used to query elements in an element tree.
|
||||
func CompilePath(path string) (Path, error) {
|
||||
var comp compiler
|
||||
segments := comp.parsePath(path)
|
||||
if comp.err != ErrPath("") {
|
||||
return Path{nil}, comp.err
|
||||
}
|
||||
return Path{segments}, nil
|
||||
}
|
||||
|
||||
// MustCompilePath creates an optimized version of an XPath-like string that
|
||||
// can be used to query elements in an element tree. Panics if an error
|
||||
// occurs. Use this function to create Paths when you know the path is
|
||||
// valid (i.e., if it's hard-coded).
|
||||
func MustCompilePath(path string) Path {
|
||||
p, err := CompilePath(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// A segment is a portion of a path between "/" characters.
|
||||
// It contains one selector and zero or more [filters].
|
||||
type segment struct {
|
||||
sel selector
|
||||
filters []filter
|
||||
}
|
||||
|
||||
func (seg *segment) apply(e *Element, p *pather) {
|
||||
seg.sel.apply(e, p)
|
||||
for _, f := range seg.filters {
|
||||
f.apply(p)
|
||||
}
|
||||
}
|
||||
|
||||
// A selector selects XML elements for consideration by the
|
||||
// path traversal.
|
||||
type selector interface {
|
||||
apply(e *Element, p *pather)
|
||||
}
|
||||
|
||||
// A filter pares down a list of candidate XML elements based
|
||||
// on a path filter in [brackets].
|
||||
type filter interface {
|
||||
apply(p *pather)
|
||||
}
|
||||
|
||||
// A pather is helper object that traverses an element tree using
|
||||
// a Path object. It collects and deduplicates all elements matching
|
||||
// the path query.
|
||||
type pather struct {
|
||||
queue fifo
|
||||
results []*Element
|
||||
inResults map[*Element]bool
|
||||
candidates []*Element
|
||||
scratch []*Element // used by filters
|
||||
}
|
||||
|
||||
// A node represents an element and the remaining path segments that
|
||||
// should be applied against it by the pather.
|
||||
type node struct {
|
||||
e *Element
|
||||
segments []segment
|
||||
}
|
||||
|
||||
func newPather() *pather {
|
||||
return &pather{
|
||||
results: make([]*Element, 0),
|
||||
inResults: make(map[*Element]bool),
|
||||
candidates: make([]*Element, 0),
|
||||
scratch: make([]*Element, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// traverse follows the path from the element e, collecting
|
||||
// and then returning all elements that match the path's selectors
|
||||
// and filters.
|
||||
func (p *pather) traverse(e *Element, path Path) []*Element {
|
||||
for p.queue.add(node{e, path.segments}); p.queue.len() > 0; {
|
||||
p.eval(p.queue.remove().(node))
|
||||
}
|
||||
return p.results
|
||||
}
|
||||
|
||||
// eval evalutes the current path node by applying the remaining
|
||||
// path's selector rules against the node's element.
|
||||
func (p *pather) eval(n node) {
|
||||
p.candidates = p.candidates[0:0]
|
||||
seg, remain := n.segments[0], n.segments[1:]
|
||||
seg.apply(n.e, p)
|
||||
|
||||
if len(remain) == 0 {
|
||||
for _, c := range p.candidates {
|
||||
if in := p.inResults[c]; !in {
|
||||
p.inResults[c] = true
|
||||
p.results = append(p.results, c)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, c := range p.candidates {
|
||||
p.queue.add(node{c, remain})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A compiler generates a compiled path from a path string.
|
||||
type compiler struct {
|
||||
err ErrPath
|
||||
}
|
||||
|
||||
// parsePath parses an XPath-like string describing a path
|
||||
// through an element tree and returns a slice of segment
|
||||
// descriptors.
|
||||
func (c *compiler) parsePath(path string) []segment {
|
||||
// If path ends with //, fix it
|
||||
if strings.HasSuffix(path, "//") {
|
||||
path = path + "*"
|
||||
}
|
||||
|
||||
var segments []segment
|
||||
|
||||
// Check for an absolute path
|
||||
if strings.HasPrefix(path, "/") {
|
||||
segments = append(segments, segment{new(selectRoot), []filter{}})
|
||||
path = path[1:]
|
||||
}
|
||||
|
||||
// Split path into segments
|
||||
for _, s := range splitPath(path) {
|
||||
segments = append(segments, c.parseSegment(s))
|
||||
if c.err != ErrPath("") {
|
||||
break
|
||||
}
|
||||
}
|
||||
return segments
|
||||
}
|
||||
|
||||
func splitPath(path string) []string {
|
||||
pieces := make([]string, 0)
|
||||
start := 0
|
||||
inquote := false
|
||||
for i := 0; i+1 <= len(path); i++ {
|
||||
if path[i] == '\'' {
|
||||
inquote = !inquote
|
||||
} else if path[i] == '/' && !inquote {
|
||||
pieces = append(pieces, path[start:i])
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
return append(pieces, path[start:])
|
||||
}
|
||||
|
||||
// parseSegment parses a path segment between / characters.
|
||||
func (c *compiler) parseSegment(path string) segment {
|
||||
pieces := strings.Split(path, "[")
|
||||
seg := segment{
|
||||
sel: c.parseSelector(pieces[0]),
|
||||
filters: []filter{},
|
||||
}
|
||||
for i := 1; i < len(pieces); i++ {
|
||||
fpath := pieces[i]
|
||||
if fpath[len(fpath)-1] != ']' {
|
||||
c.err = ErrPath("path has invalid filter [brackets].")
|
||||
break
|
||||
}
|
||||
seg.filters = append(seg.filters, c.parseFilter(fpath[:len(fpath)-1]))
|
||||
}
|
||||
return seg
|
||||
}
|
||||
|
||||
// parseSelector parses a selector at the start of a path segment.
|
||||
func (c *compiler) parseSelector(path string) selector {
|
||||
switch path {
|
||||
case ".":
|
||||
return new(selectSelf)
|
||||
case "..":
|
||||
return new(selectParent)
|
||||
case "*":
|
||||
return new(selectChildren)
|
||||
case "":
|
||||
return new(selectDescendants)
|
||||
default:
|
||||
return newSelectChildrenByTag(path)
|
||||
}
|
||||
}
|
||||
|
||||
var fnTable = map[string]struct {
|
||||
hasFn func(e *Element) bool
|
||||
getValFn func(e *Element) string
|
||||
}{
|
||||
"local-name": {nil, (*Element).name},
|
||||
"name": {nil, (*Element).FullTag},
|
||||
"namespace-prefix": {nil, (*Element).namespacePrefix},
|
||||
"namespace-uri": {nil, (*Element).NamespaceURI},
|
||||
"text": {(*Element).hasText, (*Element).Text},
|
||||
}
|
||||
|
||||
// parseFilter parses a path filter contained within [brackets].
|
||||
func (c *compiler) parseFilter(path string) filter {
|
||||
if len(path) == 0 {
|
||||
c.err = ErrPath("path contains an empty filter expression.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filter contains [@attr='val'], [fn()='val'], or [tag='val']?
|
||||
eqindex := strings.Index(path, "='")
|
||||
if eqindex >= 0 {
|
||||
rindex := nextIndex(path, "'", eqindex+2)
|
||||
if rindex != len(path)-1 {
|
||||
c.err = ErrPath("path has mismatched filter quotes.")
|
||||
return nil
|
||||
}
|
||||
|
||||
key := path[:eqindex]
|
||||
value := path[eqindex+2 : rindex]
|
||||
|
||||
switch {
|
||||
case key[0] == '@':
|
||||
return newFilterAttrVal(key[1:], value)
|
||||
case strings.HasSuffix(key, "()"):
|
||||
fn := key[:len(key)-2]
|
||||
if t, ok := fnTable[fn]; ok && t.getValFn != nil {
|
||||
return newFilterFuncVal(t.getValFn, value)
|
||||
}
|
||||
c.err = ErrPath("path has unknown function " + fn)
|
||||
return nil
|
||||
default:
|
||||
return newFilterChildText(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter contains [@attr], [N], [tag] or [fn()]
|
||||
switch {
|
||||
case path[0] == '@':
|
||||
return newFilterAttr(path[1:])
|
||||
case strings.HasSuffix(path, "()"):
|
||||
fn := path[:len(path)-2]
|
||||
if t, ok := fnTable[fn]; ok && t.hasFn != nil {
|
||||
return newFilterFunc(t.hasFn)
|
||||
}
|
||||
c.err = ErrPath("path has unknown function " + fn)
|
||||
return nil
|
||||
case isInteger(path):
|
||||
pos, _ := strconv.Atoi(path)
|
||||
switch {
|
||||
case pos > 0:
|
||||
return newFilterPos(pos - 1)
|
||||
default:
|
||||
return newFilterPos(pos)
|
||||
}
|
||||
default:
|
||||
return newFilterChild(path)
|
||||
}
|
||||
}
|
||||
|
||||
// selectSelf selects the current element into the candidate list.
|
||||
type selectSelf struct{}
|
||||
|
||||
func (s *selectSelf) apply(e *Element, p *pather) {
|
||||
p.candidates = append(p.candidates, e)
|
||||
}
|
||||
|
||||
// selectRoot selects the element's root node.
|
||||
type selectRoot struct{}
|
||||
|
||||
func (s *selectRoot) apply(e *Element, p *pather) {
|
||||
root := e
|
||||
for root.parent != nil {
|
||||
root = root.parent
|
||||
}
|
||||
p.candidates = append(p.candidates, root)
|
||||
}
|
||||
|
||||
// selectParent selects the element's parent into the candidate list.
|
||||
type selectParent struct{}
|
||||
|
||||
func (s *selectParent) apply(e *Element, p *pather) {
|
||||
if e.parent != nil {
|
||||
p.candidates = append(p.candidates, e.parent)
|
||||
}
|
||||
}
|
||||
|
||||
// selectChildren selects the element's child elements into the
|
||||
// candidate list.
|
||||
type selectChildren struct{}
|
||||
|
||||
func (s *selectChildren) apply(e *Element, p *pather) {
|
||||
for _, c := range e.Child {
|
||||
if c, ok := c.(*Element); ok {
|
||||
p.candidates = append(p.candidates, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// selectDescendants selects all descendant child elements
|
||||
// of the element into the candidate list.
|
||||
type selectDescendants struct{}
|
||||
|
||||
func (s *selectDescendants) apply(e *Element, p *pather) {
|
||||
var queue fifo
|
||||
for queue.add(e); queue.len() > 0; {
|
||||
e := queue.remove().(*Element)
|
||||
p.candidates = append(p.candidates, e)
|
||||
for _, c := range e.Child {
|
||||
if c, ok := c.(*Element); ok {
|
||||
queue.add(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// selectChildrenByTag selects into the candidate list all child
|
||||
// elements of the element having the specified tag.
|
||||
type selectChildrenByTag struct {
|
||||
space, tag string
|
||||
}
|
||||
|
||||
func newSelectChildrenByTag(path string) *selectChildrenByTag {
|
||||
s, l := spaceDecompose(path)
|
||||
return &selectChildrenByTag{s, l}
|
||||
}
|
||||
|
||||
func (s *selectChildrenByTag) apply(e *Element, p *pather) {
|
||||
for _, c := range e.Child {
|
||||
if c, ok := c.(*Element); ok && spaceMatch(s.space, c.Space) && s.tag == c.Tag {
|
||||
p.candidates = append(p.candidates, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// filterPos filters the candidate list, keeping only the
|
||||
// candidate at the specified index.
|
||||
type filterPos struct {
|
||||
index int
|
||||
}
|
||||
|
||||
func newFilterPos(pos int) *filterPos {
|
||||
return &filterPos{pos}
|
||||
}
|
||||
|
||||
func (f *filterPos) apply(p *pather) {
|
||||
if f.index >= 0 {
|
||||
if f.index < len(p.candidates) {
|
||||
p.scratch = append(p.scratch, p.candidates[f.index])
|
||||
}
|
||||
} else {
|
||||
if -f.index <= len(p.candidates) {
|
||||
p.scratch = append(p.scratch, p.candidates[len(p.candidates)+f.index])
|
||||
}
|
||||
}
|
||||
p.candidates, p.scratch = p.scratch, p.candidates[0:0]
|
||||
}
|
||||
|
||||
// filterAttr filters the candidate list for elements having
|
||||
// the specified attribute.
|
||||
type filterAttr struct {
|
||||
space, key string
|
||||
}
|
||||
|
||||
func newFilterAttr(str string) *filterAttr {
|
||||
s, l := spaceDecompose(str)
|
||||
return &filterAttr{s, l}
|
||||
}
|
||||
|
||||
func (f *filterAttr) apply(p *pather) {
|
||||
for _, c := range p.candidates {
|
||||
for _, a := range c.Attr {
|
||||
if spaceMatch(f.space, a.Space) && f.key == a.Key {
|
||||
p.scratch = append(p.scratch, c)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
p.candidates, p.scratch = p.scratch, p.candidates[0:0]
|
||||
}
|
||||
|
||||
// filterAttrVal filters the candidate list for elements having
|
||||
// the specified attribute with the specified value.
|
||||
type filterAttrVal struct {
|
||||
space, key, val string
|
||||
}
|
||||
|
||||
func newFilterAttrVal(str, value string) *filterAttrVal {
|
||||
s, l := spaceDecompose(str)
|
||||
return &filterAttrVal{s, l, value}
|
||||
}
|
||||
|
||||
func (f *filterAttrVal) apply(p *pather) {
|
||||
for _, c := range p.candidates {
|
||||
for _, a := range c.Attr {
|
||||
if spaceMatch(f.space, a.Space) && f.key == a.Key && f.val == a.Value {
|
||||
p.scratch = append(p.scratch, c)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
p.candidates, p.scratch = p.scratch, p.candidates[0:0]
|
||||
}
|
||||
|
||||
// filterFunc filters the candidate list for elements satisfying a custom
|
||||
// boolean function.
|
||||
type filterFunc struct {
|
||||
fn func(e *Element) bool
|
||||
}
|
||||
|
||||
func newFilterFunc(fn func(e *Element) bool) *filterFunc {
|
||||
return &filterFunc{fn}
|
||||
}
|
||||
|
||||
func (f *filterFunc) apply(p *pather) {
|
||||
for _, c := range p.candidates {
|
||||
if f.fn(c) {
|
||||
p.scratch = append(p.scratch, c)
|
||||
}
|
||||
}
|
||||
p.candidates, p.scratch = p.scratch, p.candidates[0:0]
|
||||
}
|
||||
|
||||
// filterFuncVal filters the candidate list for elements containing a value
|
||||
// matching the result of a custom function.
|
||||
type filterFuncVal struct {
|
||||
fn func(e *Element) string
|
||||
val string
|
||||
}
|
||||
|
||||
func newFilterFuncVal(fn func(e *Element) string, value string) *filterFuncVal {
|
||||
return &filterFuncVal{fn, value}
|
||||
}
|
||||
|
||||
func (f *filterFuncVal) apply(p *pather) {
|
||||
for _, c := range p.candidates {
|
||||
if f.fn(c) == f.val {
|
||||
p.scratch = append(p.scratch, c)
|
||||
}
|
||||
}
|
||||
p.candidates, p.scratch = p.scratch, p.candidates[0:0]
|
||||
}
|
||||
|
||||
// filterChild filters the candidate list for elements having
|
||||
// a child element with the specified tag.
|
||||
type filterChild struct {
|
||||
space, tag string
|
||||
}
|
||||
|
||||
func newFilterChild(str string) *filterChild {
|
||||
s, l := spaceDecompose(str)
|
||||
return &filterChild{s, l}
|
||||
}
|
||||
|
||||
func (f *filterChild) apply(p *pather) {
|
||||
for _, c := range p.candidates {
|
||||
for _, cc := range c.Child {
|
||||
if cc, ok := cc.(*Element); ok &&
|
||||
spaceMatch(f.space, cc.Space) &&
|
||||
f.tag == cc.Tag {
|
||||
p.scratch = append(p.scratch, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
p.candidates, p.scratch = p.scratch, p.candidates[0:0]
|
||||
}
|
||||
|
||||
// filterChildText filters the candidate list for elements having
|
||||
// a child element with the specified tag and text.
|
||||
type filterChildText struct {
|
||||
space, tag, text string
|
||||
}
|
||||
|
||||
func newFilterChildText(str, text string) *filterChildText {
|
||||
s, l := spaceDecompose(str)
|
||||
return &filterChildText{s, l, text}
|
||||
}
|
||||
|
||||
func (f *filterChildText) apply(p *pather) {
|
||||
for _, c := range p.candidates {
|
||||
for _, cc := range c.Child {
|
||||
if cc, ok := cc.(*Element); ok &&
|
||||
spaceMatch(f.space, cc.Space) &&
|
||||
f.tag == cc.Tag &&
|
||||
f.text == cc.Text() {
|
||||
p.scratch = append(p.scratch, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
p.candidates, p.scratch = p.scratch, p.candidates[0:0]
|
||||
}
|
||||
363
vendor/github.com/hashicorp/go-cleanhttp/LICENSE
generated
vendored
Normal file
363
vendor/github.com/hashicorp/go-cleanhttp/LICENSE
generated
vendored
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
Mozilla Public License, version 2.0
|
||||
|
||||
1. Definitions
|
||||
|
||||
1.1. "Contributor"
|
||||
|
||||
means each individual or legal entity that creates, contributes to the
|
||||
creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
|
||||
means the combination of the Contributions of others (if any) used by a
|
||||
Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
|
||||
means Source Code Form to which the initial Contributor has attached the
|
||||
notice in Exhibit A, the Executable Form of such Source Code Form, and
|
||||
Modifications of such Source Code Form, in each case including portions
|
||||
thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
a. that the initial Contributor has attached the notice described in
|
||||
Exhibit B to the Covered Software; or
|
||||
|
||||
b. that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the terms of
|
||||
a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
|
||||
means a work that combines Covered Software with other material, in a
|
||||
separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
|
||||
means having the right to grant, to the maximum extent possible, whether
|
||||
at the time of the initial grant or subsequently, any and all of the
|
||||
rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
|
||||
means any of the following:
|
||||
|
||||
a. any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered Software; or
|
||||
|
||||
b. any new file in Source Code Form that contains any Covered Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the License,
|
||||
by the making, using, selling, offering for sale, having made, import,
|
||||
or transfer of either its Contributions or its Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
|
||||
means either the GNU General Public License, Version 2.0, the GNU Lesser
|
||||
General Public License, Version 2.1, the GNU Affero General Public
|
||||
License, Version 3.0, or any later versions of those licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that controls, is
|
||||
controlled by, or is under common control with You. For purposes of this
|
||||
definition, "control" means (a) the power, direct or indirect, to cause
|
||||
the direction or management of such entity, whether by contract or
|
||||
otherwise, or (b) ownership of more than fifty percent (50%) of the
|
||||
outstanding shares or beneficial ownership of such entity.
|
||||
|
||||
|
||||
2. License Grants and Conditions
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
a. under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
b. under Patent Claims of such Contributor to make, use, sell, offer for
|
||||
sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
a. for any code that a Contributor has removed from Covered Software; or
|
||||
|
||||
b. for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
c. under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights to
|
||||
grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
|
||||
Section 2.1.
|
||||
|
||||
|
||||
3. Responsibilities
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
a. such Covered Software must also be made available in Source Code Form,
|
||||
as described in Section 3.1, and You must inform recipients of the
|
||||
Executable Form how they can obtain a copy of such Source Code Form by
|
||||
reasonable means in a timely manner, at a charge no more than the cost
|
||||
of distribution to the recipient; and
|
||||
|
||||
b. You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter the
|
||||
recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty, or
|
||||
limitations of liability) contained within the Source Code Form of the
|
||||
Covered Software, except that You may alter any license notices to the
|
||||
extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this License
|
||||
with respect to some or all of the Covered Software due to statute,
|
||||
judicial order, or regulation then You must: (a) comply with the terms of
|
||||
this License to the maximum extent possible; and (b) describe the
|
||||
limitations and the code they affect. Such description must be placed in a
|
||||
text file included with all distributions of the Covered Software under
|
||||
this License. Except to the extent prohibited by statute or regulation,
|
||||
such description must be sufficiently detailed for a recipient of ordinary
|
||||
skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically if You
|
||||
fail to comply with any of its terms. However, if You become compliant,
|
||||
then the rights granted under this License from a particular Contributor
|
||||
are reinstated (a) provisionally, unless and until such Contributor
|
||||
explicitly and finally terminates Your grants, and (b) on an ongoing
|
||||
basis, if such Contributor fails to notify You of the non-compliance by
|
||||
some reasonable means prior to 60 days after You have come back into
|
||||
compliance. Moreover, Your grants from a particular Contributor are
|
||||
reinstated on an ongoing basis if such Contributor notifies You of the
|
||||
non-compliance by some reasonable means, this is the first time You have
|
||||
received notice of non-compliance with this License from such
|
||||
Contributor, and You become compliant prior to 30 days after Your receipt
|
||||
of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
|
||||
license agreements (excluding distributors and resellers) which have been
|
||||
validly granted by You or Your distributors under this License prior to
|
||||
termination shall survive termination.
|
||||
|
||||
6. Disclaimer of Warranty
|
||||
|
||||
Covered Software is provided under this License on an "as is" basis,
|
||||
without warranty of any kind, either expressed, implied, or statutory,
|
||||
including, without limitation, warranties that the Covered Software is free
|
||||
of defects, merchantable, fit for a particular purpose or non-infringing.
|
||||
The entire risk as to the quality and performance of the Covered Software
|
||||
is with You. Should any Covered Software prove defective in any respect,
|
||||
You (not any Contributor) assume the cost of any necessary servicing,
|
||||
repair, or correction. This disclaimer of warranty constitutes an essential
|
||||
part of this License. No use of any Covered Software is authorized under
|
||||
this License except under this disclaimer.
|
||||
|
||||
7. Limitation of Liability
|
||||
|
||||
Under no circumstances and under no legal theory, whether tort (including
|
||||
negligence), contract, or otherwise, shall any Contributor, or anyone who
|
||||
distributes Covered Software as permitted above, be liable to You for any
|
||||
direct, indirect, special, incidental, or consequential damages of any
|
||||
character including, without limitation, damages for lost profits, loss of
|
||||
goodwill, work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses, even if such party shall have been
|
||||
informed of the possibility of such damages. This limitation of liability
|
||||
shall not apply to liability for death or personal injury resulting from
|
||||
such party's negligence to the extent applicable law prohibits such
|
||||
limitation. Some jurisdictions do not allow the exclusion or limitation of
|
||||
incidental or consequential damages, so this exclusion and limitation may
|
||||
not apply to You.
|
||||
|
||||
8. Litigation
|
||||
|
||||
Any litigation relating to this License may be brought only in the courts
|
||||
of a jurisdiction where the defendant maintains its principal place of
|
||||
business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions. Nothing
|
||||
in this Section shall prevent a party's ability to bring cross-claims or
|
||||
counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides that
|
||||
the language of a contract shall be construed against the drafter shall not
|
||||
be used to construe this License against a Contributor.
|
||||
|
||||
|
||||
10. Versions of the License
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses If You choose to distribute Source Code Form that is
|
||||
Incompatible With Secondary Licenses under the terms of this version of
|
||||
the License, the notice described in Exhibit B of this License must be
|
||||
attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular file,
|
||||
then You may include the notice in a location (such as a LICENSE file in a
|
||||
relevant directory) where a recipient would be likely to look for such a
|
||||
notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
|
||||
This Source Code Form is "Incompatible
|
||||
With Secondary Licenses", as defined by
|
||||
the Mozilla Public License, v. 2.0.
|
||||
|
||||
30
vendor/github.com/hashicorp/go-cleanhttp/README.md
generated
vendored
Normal file
30
vendor/github.com/hashicorp/go-cleanhttp/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# cleanhttp
|
||||
|
||||
Functions for accessing "clean" Go http.Client values
|
||||
|
||||
-------------
|
||||
|
||||
The Go standard library contains a default `http.Client` called
|
||||
`http.DefaultClient`. It is a common idiom in Go code to start with
|
||||
`http.DefaultClient` and tweak it as necessary, and in fact, this is
|
||||
encouraged; from the `http` package documentation:
|
||||
|
||||
> The Client's Transport typically has internal state (cached TCP connections),
|
||||
so Clients should be reused instead of created as needed. Clients are safe for
|
||||
concurrent use by multiple goroutines.
|
||||
|
||||
Unfortunately, this is a shared value, and it is not uncommon for libraries to
|
||||
assume that they are free to modify it at will. With enough dependencies, it
|
||||
can be very easy to encounter strange problems and race conditions due to
|
||||
manipulation of this shared value across libraries and goroutines (clients are
|
||||
safe for concurrent use, but writing values to the client struct itself is not
|
||||
protected).
|
||||
|
||||
Making things worse is the fact that a bare `http.Client` will use a default
|
||||
`http.Transport` called `http.DefaultTransport`, which is another global value
|
||||
that behaves the same way. So it is not simply enough to replace
|
||||
`http.DefaultClient` with `&http.Client{}`.
|
||||
|
||||
This repository provides some simple functions to get a "clean" `http.Client`
|
||||
-- one that uses the same default values as the Go standard library, but
|
||||
returns a client that does not share any state with other clients.
|
||||
58
vendor/github.com/hashicorp/go-cleanhttp/cleanhttp.go
generated
vendored
Normal file
58
vendor/github.com/hashicorp/go-cleanhttp/cleanhttp.go
generated
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package cleanhttp
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DefaultTransport returns a new http.Transport with similar default values to
|
||||
// http.DefaultTransport, but with idle connections and keepalives disabled.
|
||||
func DefaultTransport() *http.Transport {
|
||||
transport := DefaultPooledTransport()
|
||||
transport.DisableKeepAlives = true
|
||||
transport.MaxIdleConnsPerHost = -1
|
||||
return transport
|
||||
}
|
||||
|
||||
// DefaultPooledTransport returns a new http.Transport with similar default
|
||||
// values to http.DefaultTransport. Do not use this for transient transports as
|
||||
// it can leak file descriptors over time. Only use this for transports that
|
||||
// will be re-used for the same host(s).
|
||||
func DefaultPooledTransport() *http.Transport {
|
||||
transport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1,
|
||||
}
|
||||
return transport
|
||||
}
|
||||
|
||||
// DefaultClient returns a new http.Client with similar default values to
|
||||
// http.Client, but with a non-shared Transport, idle connections disabled, and
|
||||
// keepalives disabled.
|
||||
func DefaultClient() *http.Client {
|
||||
return &http.Client{
|
||||
Transport: DefaultTransport(),
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultPooledClient returns a new http.Client with similar default values to
|
||||
// http.Client, but with a shared Transport. Do not use this function for
|
||||
// transient clients as it can leak file descriptors over time. Only use this
|
||||
// for clients that will be re-used for the same host(s).
|
||||
func DefaultPooledClient() *http.Client {
|
||||
return &http.Client{
|
||||
Transport: DefaultPooledTransport(),
|
||||
}
|
||||
}
|
||||
20
vendor/github.com/hashicorp/go-cleanhttp/doc.go
generated
vendored
Normal file
20
vendor/github.com/hashicorp/go-cleanhttp/doc.go
generated
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// Package cleanhttp offers convenience utilities for acquiring "clean"
|
||||
// http.Transport and http.Client structs.
|
||||
//
|
||||
// Values set on http.DefaultClient and http.DefaultTransport affect all
|
||||
// callers. This can have detrimental effects, esepcially in TLS contexts,
|
||||
// where client or root certificates set to talk to multiple endpoints can end
|
||||
// up displacing each other, leading to hard-to-debug issues. This package
|
||||
// provides non-shared http.Client and http.Transport structs to ensure that
|
||||
// the configuration will not be overwritten by other parts of the application
|
||||
// or dependencies.
|
||||
//
|
||||
// The DefaultClient and DefaultTransport functions disable idle connections
|
||||
// and keepalives. Without ensuring that idle connections are closed before
|
||||
// garbage collection, short-term clients/transports can leak file descriptors,
|
||||
// eventually leading to "too many open files" errors. If you will be
|
||||
// connecting to the same hosts repeatedly from the same client, you can use
|
||||
// DefaultPooledClient to receive a client that has connection pooling
|
||||
// semantics similar to http.DefaultClient.
|
||||
//
|
||||
package cleanhttp
|
||||
48
vendor/github.com/hashicorp/go-cleanhttp/handlers.go
generated
vendored
Normal file
48
vendor/github.com/hashicorp/go-cleanhttp/handlers.go
generated
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package cleanhttp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// HandlerInput provides input options to cleanhttp's handlers
|
||||
type HandlerInput struct {
|
||||
ErrStatus int
|
||||
}
|
||||
|
||||
// PrintablePathCheckHandler is a middleware that ensures the request path
|
||||
// contains only printable runes.
|
||||
func PrintablePathCheckHandler(next http.Handler, input *HandlerInput) http.Handler {
|
||||
// Nil-check on input to make it optional
|
||||
if input == nil {
|
||||
input = &HandlerInput{
|
||||
ErrStatus: http.StatusBadRequest,
|
||||
}
|
||||
}
|
||||
|
||||
// Default to http.StatusBadRequest on error
|
||||
if input.ErrStatus == 0 {
|
||||
input.ErrStatus = http.StatusBadRequest
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r != nil {
|
||||
// Check URL path for non-printable characters
|
||||
idx := strings.IndexFunc(r.URL.Path, func(c rune) bool {
|
||||
return !unicode.IsPrint(c)
|
||||
})
|
||||
|
||||
if idx != -1 {
|
||||
w.WriteHeader(input.ErrStatus)
|
||||
return
|
||||
}
|
||||
|
||||
if next != nil {
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
}
|
||||
4
vendor/github.com/hashicorp/go-retryablehttp/.gitignore
generated
vendored
Normal file
4
vendor/github.com/hashicorp/go-retryablehttp/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
.idea/
|
||||
*.iml
|
||||
*.test
|
||||
.vscode/
|
||||
1
vendor/github.com/hashicorp/go-retryablehttp/.go-version
generated
vendored
Normal file
1
vendor/github.com/hashicorp/go-retryablehttp/.go-version
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
1.22.2
|
||||
33
vendor/github.com/hashicorp/go-retryablehttp/CHANGELOG.md
generated
vendored
Normal file
33
vendor/github.com/hashicorp/go-retryablehttp/CHANGELOG.md
generated
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
## 0.7.7 (May 30, 2024)
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
- client: avoid potentially leaking URL-embedded basic authentication credentials in logs (#158)
|
||||
|
||||
## 0.7.6 (May 9, 2024)
|
||||
|
||||
ENHANCEMENTS:
|
||||
|
||||
- client: support a `RetryPrepare` function for modifying the request before retrying (#216)
|
||||
- client: support HTTP-date values for `Retry-After` header value (#138)
|
||||
- client: avoid reading entire body when the body is a `*bytes.Reader` (#197)
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
- client: fix a broken check for invalid server certificate in go 1.20+ (#210)
|
||||
|
||||
## 0.7.5 (Nov 8, 2023)
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
- client: fixes an issue where the request body is not preserved on temporary redirects or re-established HTTP/2 connections (#207)
|
||||
|
||||
## 0.7.4 (Jun 6, 2023)
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
- client: fixing an issue where the Content-Type header wouldn't be sent with an empty payload when using HTTP/2 (#194)
|
||||
|
||||
## 0.7.3 (May 15, 2023)
|
||||
|
||||
Initial release
|
||||
1
vendor/github.com/hashicorp/go-retryablehttp/CODEOWNERS
generated
vendored
Normal file
1
vendor/github.com/hashicorp/go-retryablehttp/CODEOWNERS
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
* @hashicorp/go-retryablehttp-maintainers
|
||||
365
vendor/github.com/hashicorp/go-retryablehttp/LICENSE
generated
vendored
Normal file
365
vendor/github.com/hashicorp/go-retryablehttp/LICENSE
generated
vendored
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
Copyright (c) 2015 HashiCorp, Inc.
|
||||
|
||||
Mozilla Public License, version 2.0
|
||||
|
||||
1. Definitions
|
||||
|
||||
1.1. "Contributor"
|
||||
|
||||
means each individual or legal entity that creates, contributes to the
|
||||
creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
|
||||
means the combination of the Contributions of others (if any) used by a
|
||||
Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
|
||||
means Source Code Form to which the initial Contributor has attached the
|
||||
notice in Exhibit A, the Executable Form of such Source Code Form, and
|
||||
Modifications of such Source Code Form, in each case including portions
|
||||
thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
a. that the initial Contributor has attached the notice described in
|
||||
Exhibit B to the Covered Software; or
|
||||
|
||||
b. that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the terms of
|
||||
a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
|
||||
means a work that combines Covered Software with other material, in a
|
||||
separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
|
||||
means having the right to grant, to the maximum extent possible, whether
|
||||
at the time of the initial grant or subsequently, any and all of the
|
||||
rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
|
||||
means any of the following:
|
||||
|
||||
a. any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered Software; or
|
||||
|
||||
b. any new file in Source Code Form that contains any Covered Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the License,
|
||||
by the making, using, selling, offering for sale, having made, import,
|
||||
or transfer of either its Contributions or its Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
|
||||
means either the GNU General Public License, Version 2.0, the GNU Lesser
|
||||
General Public License, Version 2.1, the GNU Affero General Public
|
||||
License, Version 3.0, or any later versions of those licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that controls, is
|
||||
controlled by, or is under common control with You. For purposes of this
|
||||
definition, "control" means (a) the power, direct or indirect, to cause
|
||||
the direction or management of such entity, whether by contract or
|
||||
otherwise, or (b) ownership of more than fifty percent (50%) of the
|
||||
outstanding shares or beneficial ownership of such entity.
|
||||
|
||||
|
||||
2. License Grants and Conditions
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
a. under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
b. under Patent Claims of such Contributor to make, use, sell, offer for
|
||||
sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
a. for any code that a Contributor has removed from Covered Software; or
|
||||
|
||||
b. for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
c. under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights to
|
||||
grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
|
||||
Section 2.1.
|
||||
|
||||
|
||||
3. Responsibilities
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
a. such Covered Software must also be made available in Source Code Form,
|
||||
as described in Section 3.1, and You must inform recipients of the
|
||||
Executable Form how they can obtain a copy of such Source Code Form by
|
||||
reasonable means in a timely manner, at a charge no more than the cost
|
||||
of distribution to the recipient; and
|
||||
|
||||
b. You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter the
|
||||
recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty, or
|
||||
limitations of liability) contained within the Source Code Form of the
|
||||
Covered Software, except that You may alter any license notices to the
|
||||
extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this License
|
||||
with respect to some or all of the Covered Software due to statute,
|
||||
judicial order, or regulation then You must: (a) comply with the terms of
|
||||
this License to the maximum extent possible; and (b) describe the
|
||||
limitations and the code they affect. Such description must be placed in a
|
||||
text file included with all distributions of the Covered Software under
|
||||
this License. Except to the extent prohibited by statute or regulation,
|
||||
such description must be sufficiently detailed for a recipient of ordinary
|
||||
skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically if You
|
||||
fail to comply with any of its terms. However, if You become compliant,
|
||||
then the rights granted under this License from a particular Contributor
|
||||
are reinstated (a) provisionally, unless and until such Contributor
|
||||
explicitly and finally terminates Your grants, and (b) on an ongoing
|
||||
basis, if such Contributor fails to notify You of the non-compliance by
|
||||
some reasonable means prior to 60 days after You have come back into
|
||||
compliance. Moreover, Your grants from a particular Contributor are
|
||||
reinstated on an ongoing basis if such Contributor notifies You of the
|
||||
non-compliance by some reasonable means, this is the first time You have
|
||||
received notice of non-compliance with this License from such
|
||||
Contributor, and You become compliant prior to 30 days after Your receipt
|
||||
of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
|
||||
license agreements (excluding distributors and resellers) which have been
|
||||
validly granted by You or Your distributors under this License prior to
|
||||
termination shall survive termination.
|
||||
|
||||
6. Disclaimer of Warranty
|
||||
|
||||
Covered Software is provided under this License on an "as is" basis,
|
||||
without warranty of any kind, either expressed, implied, or statutory,
|
||||
including, without limitation, warranties that the Covered Software is free
|
||||
of defects, merchantable, fit for a particular purpose or non-infringing.
|
||||
The entire risk as to the quality and performance of the Covered Software
|
||||
is with You. Should any Covered Software prove defective in any respect,
|
||||
You (not any Contributor) assume the cost of any necessary servicing,
|
||||
repair, or correction. This disclaimer of warranty constitutes an essential
|
||||
part of this License. No use of any Covered Software is authorized under
|
||||
this License except under this disclaimer.
|
||||
|
||||
7. Limitation of Liability
|
||||
|
||||
Under no circumstances and under no legal theory, whether tort (including
|
||||
negligence), contract, or otherwise, shall any Contributor, or anyone who
|
||||
distributes Covered Software as permitted above, be liable to You for any
|
||||
direct, indirect, special, incidental, or consequential damages of any
|
||||
character including, without limitation, damages for lost profits, loss of
|
||||
goodwill, work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses, even if such party shall have been
|
||||
informed of the possibility of such damages. This limitation of liability
|
||||
shall not apply to liability for death or personal injury resulting from
|
||||
such party's negligence to the extent applicable law prohibits such
|
||||
limitation. Some jurisdictions do not allow the exclusion or limitation of
|
||||
incidental or consequential damages, so this exclusion and limitation may
|
||||
not apply to You.
|
||||
|
||||
8. Litigation
|
||||
|
||||
Any litigation relating to this License may be brought only in the courts
|
||||
of a jurisdiction where the defendant maintains its principal place of
|
||||
business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions. Nothing
|
||||
in this Section shall prevent a party's ability to bring cross-claims or
|
||||
counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides that
|
||||
the language of a contract shall be construed against the drafter shall not
|
||||
be used to construe this License against a Contributor.
|
||||
|
||||
|
||||
10. Versions of the License
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses If You choose to distribute Source Code Form that is
|
||||
Incompatible With Secondary Licenses under the terms of this version of
|
||||
the License, the notice described in Exhibit B of this License must be
|
||||
attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular file,
|
||||
then You may include the notice in a location (such as a LICENSE file in a
|
||||
relevant directory) where a recipient would be likely to look for such a
|
||||
notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
|
||||
This Source Code Form is "Incompatible
|
||||
With Secondary Licenses", as defined by
|
||||
the Mozilla Public License, v. 2.0.
|
||||
|
||||
11
vendor/github.com/hashicorp/go-retryablehttp/Makefile
generated
vendored
Normal file
11
vendor/github.com/hashicorp/go-retryablehttp/Makefile
generated
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
default: test
|
||||
|
||||
test:
|
||||
go vet ./...
|
||||
go test -v -race ./...
|
||||
|
||||
updatedeps:
|
||||
go get -f -t -u ./...
|
||||
go get -f -u ./...
|
||||
|
||||
.PHONY: default test updatedeps
|
||||
62
vendor/github.com/hashicorp/go-retryablehttp/README.md
generated
vendored
Normal file
62
vendor/github.com/hashicorp/go-retryablehttp/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
go-retryablehttp
|
||||
================
|
||||
|
||||
[][travis]
|
||||
[][godocs]
|
||||
|
||||
[travis]: http://travis-ci.org/hashicorp/go-retryablehttp
|
||||
[godocs]: http://godoc.org/github.com/hashicorp/go-retryablehttp
|
||||
|
||||
The `retryablehttp` package provides a familiar HTTP client interface with
|
||||
automatic retries and exponential backoff. It is a thin wrapper over the
|
||||
standard `net/http` client library and exposes nearly the same public API. This
|
||||
makes `retryablehttp` very easy to drop into existing programs.
|
||||
|
||||
`retryablehttp` performs automatic retries under certain conditions. Mainly, if
|
||||
an error is returned by the client (connection errors, etc.), or if a 500-range
|
||||
response code is received (except 501), then a retry is invoked after a wait
|
||||
period. Otherwise, the response is returned and left to the caller to
|
||||
interpret.
|
||||
|
||||
The main difference from `net/http` is that requests which take a request body
|
||||
(POST/PUT et. al) can have the body provided in a number of ways (some more or
|
||||
less efficient) that allow "rewinding" the request body if the initial request
|
||||
fails so that the full request can be attempted again. See the
|
||||
[godoc](http://godoc.org/github.com/hashicorp/go-retryablehttp) for more
|
||||
details.
|
||||
|
||||
Version 0.6.0 and before are compatible with Go prior to 1.12. From 0.6.1 onward, Go 1.12+ is required.
|
||||
From 0.6.7 onward, Go 1.13+ is required.
|
||||
|
||||
Example Use
|
||||
===========
|
||||
|
||||
Using this library should look almost identical to what you would do with
|
||||
`net/http`. The most simple example of a GET request is shown below:
|
||||
|
||||
```go
|
||||
resp, err := retryablehttp.Get("/foo")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
```
|
||||
|
||||
The returned response object is an `*http.Response`, the same thing you would
|
||||
usually get from `net/http`. Had the request failed one or more times, the above
|
||||
call would block and retry with exponential backoff.
|
||||
|
||||
## Getting a stdlib `*http.Client` with retries
|
||||
|
||||
It's possible to convert a `*retryablehttp.Client` directly to a `*http.Client`.
|
||||
This makes use of retryablehttp broadly applicable with minimal effort. Simply
|
||||
configure a `*retryablehttp.Client` as you wish, and then call `StandardClient()`:
|
||||
|
||||
```go
|
||||
retryClient := retryablehttp.NewClient()
|
||||
retryClient.RetryMax = 10
|
||||
|
||||
standardClient := retryClient.StandardClient() // *http.Client
|
||||
```
|
||||
|
||||
For more usage and examples see the
|
||||
[pkg.go.dev](https://pkg.go.dev/github.com/hashicorp/go-retryablehttp).
|
||||
14
vendor/github.com/hashicorp/go-retryablehttp/cert_error_go119.go
generated
vendored
Normal file
14
vendor/github.com/hashicorp/go-retryablehttp/cert_error_go119.go
generated
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build !go1.20
|
||||
// +build !go1.20
|
||||
|
||||
package retryablehttp
|
||||
|
||||
import "crypto/x509"
|
||||
|
||||
func isCertError(err error) bool {
|
||||
_, ok := err.(x509.UnknownAuthorityError)
|
||||
return ok
|
||||
}
|
||||
14
vendor/github.com/hashicorp/go-retryablehttp/cert_error_go120.go
generated
vendored
Normal file
14
vendor/github.com/hashicorp/go-retryablehttp/cert_error_go120.go
generated
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build go1.20
|
||||
// +build go1.20
|
||||
|
||||
package retryablehttp
|
||||
|
||||
import "crypto/tls"
|
||||
|
||||
func isCertError(err error) bool {
|
||||
_, ok := err.(*tls.CertificateVerificationError)
|
||||
return ok
|
||||
}
|
||||
919
vendor/github.com/hashicorp/go-retryablehttp/client.go
generated
vendored
Normal file
919
vendor/github.com/hashicorp/go-retryablehttp/client.go
generated
vendored
Normal file
|
|
@ -0,0 +1,919 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Package retryablehttp provides a familiar HTTP client interface with
|
||||
// automatic retries and exponential backoff. It is a thin wrapper over the
|
||||
// standard net/http client library and exposes nearly the same public API.
|
||||
// This makes retryablehttp very easy to drop into existing programs.
|
||||
//
|
||||
// retryablehttp performs automatic retries under certain conditions. Mainly, if
|
||||
// an error is returned by the client (connection errors etc), or if a 500-range
|
||||
// response is received, then a retry is invoked. Otherwise, the response is
|
||||
// returned and left to the caller to interpret.
|
||||
//
|
||||
// Requests which take a request body should provide a non-nil function
|
||||
// parameter. The best choice is to provide either a function satisfying
|
||||
// ReaderFunc which provides multiple io.Readers in an efficient manner, a
|
||||
// *bytes.Buffer (the underlying raw byte slice will be used) or a raw byte
|
||||
// slice. As it is a reference type, and we will wrap it as needed by readers,
|
||||
// we can efficiently re-use the request body without needing to copy it. If an
|
||||
// io.Reader (such as a *bytes.Reader) is provided, the full body will be read
|
||||
// prior to the first request, and will be efficiently re-used for any retries.
|
||||
// ReadSeeker can be used, but some users have observed occasional data races
|
||||
// between the net/http library and the Seek functionality of some
|
||||
// implementations of ReadSeeker, so should be avoided if possible.
|
||||
package retryablehttp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
||||
)
|
||||
|
||||
var (
|
||||
// Default retry configuration
|
||||
defaultRetryWaitMin = 1 * time.Second
|
||||
defaultRetryWaitMax = 30 * time.Second
|
||||
defaultRetryMax = 4
|
||||
|
||||
// defaultLogger is the logger provided with defaultClient
|
||||
defaultLogger = log.New(os.Stderr, "", log.LstdFlags)
|
||||
|
||||
// defaultClient is used for performing requests without explicitly making
|
||||
// a new client. It is purposely private to avoid modifications.
|
||||
defaultClient = NewClient()
|
||||
|
||||
// We need to consume response bodies to maintain http connections, but
|
||||
// limit the size we consume to respReadLimit.
|
||||
respReadLimit = int64(4096)
|
||||
|
||||
// timeNow sets the function that returns the current time.
|
||||
// This defaults to time.Now. Changes to this should only be done in tests.
|
||||
timeNow = time.Now
|
||||
|
||||
// A regular expression to match the error returned by net/http when the
|
||||
// configured number of redirects is exhausted. This error isn't typed
|
||||
// specifically so we resort to matching on the error string.
|
||||
redirectsErrorRe = regexp.MustCompile(`stopped after \d+ redirects\z`)
|
||||
|
||||
// A regular expression to match the error returned by net/http when the
|
||||
// scheme specified in the URL is invalid. This error isn't typed
|
||||
// specifically so we resort to matching on the error string.
|
||||
schemeErrorRe = regexp.MustCompile(`unsupported protocol scheme`)
|
||||
|
||||
// A regular expression to match the error returned by net/http when a
|
||||
// request header or value is invalid. This error isn't typed
|
||||
// specifically so we resort to matching on the error string.
|
||||
invalidHeaderErrorRe = regexp.MustCompile(`invalid header`)
|
||||
|
||||
// A regular expression to match the error returned by net/http when the
|
||||
// TLS certificate is not trusted. This error isn't typed
|
||||
// specifically so we resort to matching on the error string.
|
||||
notTrustedErrorRe = regexp.MustCompile(`certificate is not trusted`)
|
||||
)
|
||||
|
||||
// ReaderFunc is the type of function that can be given natively to NewRequest
|
||||
type ReaderFunc func() (io.Reader, error)
|
||||
|
||||
// ResponseHandlerFunc is a type of function that takes in a Response, and does something with it.
|
||||
// The ResponseHandlerFunc is called when the HTTP client successfully receives a response and the
|
||||
// CheckRetry function indicates that a retry of the base request is not necessary.
|
||||
// If an error is returned from this function, the CheckRetry policy will be used to determine
|
||||
// whether to retry the whole request (including this handler).
|
||||
//
|
||||
// Make sure to check status codes! Even if the request was completed it may have a non-2xx status code.
|
||||
//
|
||||
// The response body is not automatically closed. It must be closed either by the ResponseHandlerFunc or
|
||||
// by the caller out-of-band. Failure to do so will result in a memory leak.
|
||||
type ResponseHandlerFunc func(*http.Response) error
|
||||
|
||||
// LenReader is an interface implemented by many in-memory io.Reader's. Used
|
||||
// for automatically sending the right Content-Length header when possible.
|
||||
type LenReader interface {
|
||||
Len() int
|
||||
}
|
||||
|
||||
// Request wraps the metadata needed to create HTTP requests.
|
||||
type Request struct {
|
||||
// body is a seekable reader over the request body payload. This is
|
||||
// used to rewind the request data in between retries.
|
||||
body ReaderFunc
|
||||
|
||||
responseHandler ResponseHandlerFunc
|
||||
|
||||
// Embed an HTTP request directly. This makes a *Request act exactly
|
||||
// like an *http.Request so that all meta methods are supported.
|
||||
*http.Request
|
||||
}
|
||||
|
||||
// WithContext returns wrapped Request with a shallow copy of underlying *http.Request
|
||||
// with its context changed to ctx. The provided ctx must be non-nil.
|
||||
func (r *Request) WithContext(ctx context.Context) *Request {
|
||||
return &Request{
|
||||
body: r.body,
|
||||
responseHandler: r.responseHandler,
|
||||
Request: r.Request.WithContext(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
// SetResponseHandler allows setting the response handler.
|
||||
func (r *Request) SetResponseHandler(fn ResponseHandlerFunc) {
|
||||
r.responseHandler = fn
|
||||
}
|
||||
|
||||
// BodyBytes allows accessing the request body. It is an analogue to
|
||||
// http.Request's Body variable, but it returns a copy of the underlying data
|
||||
// rather than consuming it.
|
||||
//
|
||||
// This function is not thread-safe; do not call it at the same time as another
|
||||
// call, or at the same time this request is being used with Client.Do.
|
||||
func (r *Request) BodyBytes() ([]byte, error) {
|
||||
if r.body == nil {
|
||||
return nil, nil
|
||||
}
|
||||
body, err := r.body()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
_, err = buf.ReadFrom(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// SetBody allows setting the request body.
|
||||
//
|
||||
// It is useful if a new body needs to be set without constructing a new Request.
|
||||
func (r *Request) SetBody(rawBody interface{}) error {
|
||||
bodyReader, contentLength, err := getBodyReaderAndContentLength(rawBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.body = bodyReader
|
||||
r.ContentLength = contentLength
|
||||
if bodyReader != nil {
|
||||
r.GetBody = func() (io.ReadCloser, error) {
|
||||
body, err := bodyReader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rc, ok := body.(io.ReadCloser); ok {
|
||||
return rc, nil
|
||||
}
|
||||
return io.NopCloser(body), nil
|
||||
}
|
||||
} else {
|
||||
r.GetBody = func() (io.ReadCloser, error) { return http.NoBody, nil }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteTo allows copying the request body into a writer.
|
||||
//
|
||||
// It writes data to w until there's no more data to write or
|
||||
// when an error occurs. The return int64 value is the number of bytes
|
||||
// written. Any error encountered during the write is also returned.
|
||||
// The signature matches io.WriterTo interface.
|
||||
func (r *Request) WriteTo(w io.Writer) (int64, error) {
|
||||
body, err := r.body()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if c, ok := body.(io.Closer); ok {
|
||||
defer c.Close()
|
||||
}
|
||||
return io.Copy(w, body)
|
||||
}
|
||||
|
||||
func getBodyReaderAndContentLength(rawBody interface{}) (ReaderFunc, int64, error) {
|
||||
var bodyReader ReaderFunc
|
||||
var contentLength int64
|
||||
|
||||
switch body := rawBody.(type) {
|
||||
// If they gave us a function already, great! Use it.
|
||||
case ReaderFunc:
|
||||
bodyReader = body
|
||||
tmp, err := body()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if lr, ok := tmp.(LenReader); ok {
|
||||
contentLength = int64(lr.Len())
|
||||
}
|
||||
if c, ok := tmp.(io.Closer); ok {
|
||||
c.Close()
|
||||
}
|
||||
|
||||
case func() (io.Reader, error):
|
||||
bodyReader = body
|
||||
tmp, err := body()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if lr, ok := tmp.(LenReader); ok {
|
||||
contentLength = int64(lr.Len())
|
||||
}
|
||||
if c, ok := tmp.(io.Closer); ok {
|
||||
c.Close()
|
||||
}
|
||||
|
||||
// If a regular byte slice, we can read it over and over via new
|
||||
// readers
|
||||
case []byte:
|
||||
buf := body
|
||||
bodyReader = func() (io.Reader, error) {
|
||||
return bytes.NewReader(buf), nil
|
||||
}
|
||||
contentLength = int64(len(buf))
|
||||
|
||||
// If a bytes.Buffer we can read the underlying byte slice over and
|
||||
// over
|
||||
case *bytes.Buffer:
|
||||
buf := body
|
||||
bodyReader = func() (io.Reader, error) {
|
||||
return bytes.NewReader(buf.Bytes()), nil
|
||||
}
|
||||
contentLength = int64(buf.Len())
|
||||
|
||||
// We prioritize *bytes.Reader here because we don't really want to
|
||||
// deal with it seeking so want it to match here instead of the
|
||||
// io.ReadSeeker case.
|
||||
case *bytes.Reader:
|
||||
snapshot := *body
|
||||
bodyReader = func() (io.Reader, error) {
|
||||
r := snapshot
|
||||
return &r, nil
|
||||
}
|
||||
contentLength = int64(body.Len())
|
||||
|
||||
// Compat case
|
||||
case io.ReadSeeker:
|
||||
raw := body
|
||||
bodyReader = func() (io.Reader, error) {
|
||||
_, err := raw.Seek(0, 0)
|
||||
return io.NopCloser(raw), err
|
||||
}
|
||||
if lr, ok := raw.(LenReader); ok {
|
||||
contentLength = int64(lr.Len())
|
||||
}
|
||||
|
||||
// Read all in so we can reset
|
||||
case io.Reader:
|
||||
buf, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if len(buf) == 0 {
|
||||
bodyReader = func() (io.Reader, error) {
|
||||
return http.NoBody, nil
|
||||
}
|
||||
contentLength = 0
|
||||
} else {
|
||||
bodyReader = func() (io.Reader, error) {
|
||||
return bytes.NewReader(buf), nil
|
||||
}
|
||||
contentLength = int64(len(buf))
|
||||
}
|
||||
|
||||
// No body provided, nothing to do
|
||||
case nil:
|
||||
|
||||
// Unrecognized type
|
||||
default:
|
||||
return nil, 0, fmt.Errorf("cannot handle type %T", rawBody)
|
||||
}
|
||||
return bodyReader, contentLength, nil
|
||||
}
|
||||
|
||||
// FromRequest wraps an http.Request in a retryablehttp.Request
|
||||
func FromRequest(r *http.Request) (*Request, error) {
|
||||
bodyReader, _, err := getBodyReaderAndContentLength(r.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Could assert contentLength == r.ContentLength
|
||||
return &Request{body: bodyReader, Request: r}, nil
|
||||
}
|
||||
|
||||
// NewRequest creates a new wrapped request.
|
||||
func NewRequest(method, url string, rawBody interface{}) (*Request, error) {
|
||||
return NewRequestWithContext(context.Background(), method, url, rawBody)
|
||||
}
|
||||
|
||||
// NewRequestWithContext creates a new wrapped request with the provided context.
|
||||
//
|
||||
// The context controls the entire lifetime of a request and its response:
|
||||
// obtaining a connection, sending the request, and reading the response headers and body.
|
||||
func NewRequestWithContext(ctx context.Context, method, url string, rawBody interface{}) (*Request, error) {
|
||||
httpReq, err := http.NewRequestWithContext(ctx, method, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := &Request{
|
||||
Request: httpReq,
|
||||
}
|
||||
if err := req.SetBody(rawBody); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// Logger interface allows to use other loggers than
|
||||
// standard log.Logger.
|
||||
type Logger interface {
|
||||
Printf(string, ...interface{})
|
||||
}
|
||||
|
||||
// LeveledLogger is an interface that can be implemented by any logger or a
|
||||
// logger wrapper to provide leveled logging. The methods accept a message
|
||||
// string and a variadic number of key-value pairs. For log.Printf style
|
||||
// formatting where message string contains a format specifier, use Logger
|
||||
// interface.
|
||||
type LeveledLogger interface {
|
||||
Error(msg string, keysAndValues ...interface{})
|
||||
Info(msg string, keysAndValues ...interface{})
|
||||
Debug(msg string, keysAndValues ...interface{})
|
||||
Warn(msg string, keysAndValues ...interface{})
|
||||
}
|
||||
|
||||
// hookLogger adapts an LeveledLogger to Logger for use by the existing hook functions
|
||||
// without changing the API.
|
||||
type hookLogger struct {
|
||||
LeveledLogger
|
||||
}
|
||||
|
||||
func (h hookLogger) Printf(s string, args ...interface{}) {
|
||||
h.Info(fmt.Sprintf(s, args...))
|
||||
}
|
||||
|
||||
// RequestLogHook allows a function to run before each retry. The HTTP
|
||||
// request which will be made, and the retry number (0 for the initial
|
||||
// request) are available to users. The internal logger is exposed to
|
||||
// consumers.
|
||||
type RequestLogHook func(Logger, *http.Request, int)
|
||||
|
||||
// ResponseLogHook is like RequestLogHook, but allows running a function
|
||||
// on each HTTP response. This function will be invoked at the end of
|
||||
// every HTTP request executed, regardless of whether a subsequent retry
|
||||
// needs to be performed or not. If the response body is read or closed
|
||||
// from this method, this will affect the response returned from Do().
|
||||
type ResponseLogHook func(Logger, *http.Response)
|
||||
|
||||
// CheckRetry specifies a policy for handling retries. It is called
|
||||
// following each request with the response and error values returned by
|
||||
// the http.Client. If CheckRetry returns false, the Client stops retrying
|
||||
// and returns the response to the caller. If CheckRetry returns an error,
|
||||
// that error value is returned in lieu of the error from the request. The
|
||||
// Client will close any response body when retrying, but if the retry is
|
||||
// aborted it is up to the CheckRetry callback to properly close any
|
||||
// response body before returning.
|
||||
type CheckRetry func(ctx context.Context, resp *http.Response, err error) (bool, error)
|
||||
|
||||
// Backoff specifies a policy for how long to wait between retries.
|
||||
// It is called after a failing request to determine the amount of time
|
||||
// that should pass before trying again.
|
||||
type Backoff func(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration
|
||||
|
||||
// ErrorHandler is called if retries are expired, containing the last status
|
||||
// from the http library. If not specified, default behavior for the library is
|
||||
// to close the body and return an error indicating how many tries were
|
||||
// attempted. If overriding this, be sure to close the body if needed.
|
||||
type ErrorHandler func(resp *http.Response, err error, numTries int) (*http.Response, error)
|
||||
|
||||
// PrepareRetry is called before retry operation. It can be used for example to re-sign the request
|
||||
type PrepareRetry func(req *http.Request) error
|
||||
|
||||
// Client is used to make HTTP requests. It adds additional functionality
|
||||
// like automatic retries to tolerate minor outages.
|
||||
type Client struct {
|
||||
HTTPClient *http.Client // Internal HTTP client.
|
||||
Logger interface{} // Customer logger instance. Can be either Logger or LeveledLogger
|
||||
|
||||
RetryWaitMin time.Duration // Minimum time to wait
|
||||
RetryWaitMax time.Duration // Maximum time to wait
|
||||
RetryMax int // Maximum number of retries
|
||||
|
||||
// RequestLogHook allows a user-supplied function to be called
|
||||
// before each retry.
|
||||
RequestLogHook RequestLogHook
|
||||
|
||||
// ResponseLogHook allows a user-supplied function to be called
|
||||
// with the response from each HTTP request executed.
|
||||
ResponseLogHook ResponseLogHook
|
||||
|
||||
// CheckRetry specifies the policy for handling retries, and is called
|
||||
// after each request. The default policy is DefaultRetryPolicy.
|
||||
CheckRetry CheckRetry
|
||||
|
||||
// Backoff specifies the policy for how long to wait between retries
|
||||
Backoff Backoff
|
||||
|
||||
// ErrorHandler specifies the custom error handler to use, if any
|
||||
ErrorHandler ErrorHandler
|
||||
|
||||
// PrepareRetry can prepare the request for retry operation, for example re-sign it
|
||||
PrepareRetry PrepareRetry
|
||||
|
||||
loggerInit sync.Once
|
||||
clientInit sync.Once
|
||||
}
|
||||
|
||||
// NewClient creates a new Client with default settings.
|
||||
func NewClient() *Client {
|
||||
return &Client{
|
||||
HTTPClient: cleanhttp.DefaultPooledClient(),
|
||||
Logger: defaultLogger,
|
||||
RetryWaitMin: defaultRetryWaitMin,
|
||||
RetryWaitMax: defaultRetryWaitMax,
|
||||
RetryMax: defaultRetryMax,
|
||||
CheckRetry: DefaultRetryPolicy,
|
||||
Backoff: DefaultBackoff,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) logger() interface{} {
|
||||
c.loggerInit.Do(func() {
|
||||
if c.Logger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch c.Logger.(type) {
|
||||
case Logger, LeveledLogger:
|
||||
// ok
|
||||
default:
|
||||
// This should happen in dev when they are setting Logger and work on code, not in prod.
|
||||
panic(fmt.Sprintf("invalid logger type passed, must be Logger or LeveledLogger, was %T", c.Logger))
|
||||
}
|
||||
})
|
||||
|
||||
return c.Logger
|
||||
}
|
||||
|
||||
// DefaultRetryPolicy provides a default callback for Client.CheckRetry, which
|
||||
// will retry on connection errors and server errors.
|
||||
func DefaultRetryPolicy(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
||||
// do not retry on context.Canceled or context.DeadlineExceeded
|
||||
if ctx.Err() != nil {
|
||||
return false, ctx.Err()
|
||||
}
|
||||
|
||||
// don't propagate other errors
|
||||
shouldRetry, _ := baseRetryPolicy(resp, err)
|
||||
return shouldRetry, nil
|
||||
}
|
||||
|
||||
// ErrorPropagatedRetryPolicy is the same as DefaultRetryPolicy, except it
|
||||
// propagates errors back instead of returning nil. This allows you to inspect
|
||||
// why it decided to retry or not.
|
||||
func ErrorPropagatedRetryPolicy(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
||||
// do not retry on context.Canceled or context.DeadlineExceeded
|
||||
if ctx.Err() != nil {
|
||||
return false, ctx.Err()
|
||||
}
|
||||
|
||||
return baseRetryPolicy(resp, err)
|
||||
}
|
||||
|
||||
func baseRetryPolicy(resp *http.Response, err error) (bool, error) {
|
||||
if err != nil {
|
||||
if v, ok := err.(*url.Error); ok {
|
||||
// Don't retry if the error was due to too many redirects.
|
||||
if redirectsErrorRe.MatchString(v.Error()) {
|
||||
return false, v
|
||||
}
|
||||
|
||||
// Don't retry if the error was due to an invalid protocol scheme.
|
||||
if schemeErrorRe.MatchString(v.Error()) {
|
||||
return false, v
|
||||
}
|
||||
|
||||
// Don't retry if the error was due to an invalid header.
|
||||
if invalidHeaderErrorRe.MatchString(v.Error()) {
|
||||
return false, v
|
||||
}
|
||||
|
||||
// Don't retry if the error was due to TLS cert verification failure.
|
||||
if notTrustedErrorRe.MatchString(v.Error()) {
|
||||
return false, v
|
||||
}
|
||||
if isCertError(v.Err) {
|
||||
return false, v
|
||||
}
|
||||
}
|
||||
|
||||
// The error is likely recoverable so retry.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 429 Too Many Requests is recoverable. Sometimes the server puts
|
||||
// a Retry-After response header to indicate when the server is
|
||||
// available to start processing request from client.
|
||||
if resp.StatusCode == http.StatusTooManyRequests {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Check the response code. We retry on 500-range responses to allow
|
||||
// the server time to recover, as 500's are typically not permanent
|
||||
// errors and may relate to outages on the server side. This will catch
|
||||
// invalid response codes as well, like 0 and 999.
|
||||
if resp.StatusCode == 0 || (resp.StatusCode >= 500 && resp.StatusCode != http.StatusNotImplemented) {
|
||||
return true, fmt.Errorf("unexpected HTTP status %s", resp.Status)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// DefaultBackoff provides a default callback for Client.Backoff which
|
||||
// will perform exponential backoff based on the attempt number and limited
|
||||
// by the provided minimum and maximum durations.
|
||||
//
|
||||
// It also tries to parse Retry-After response header when a http.StatusTooManyRequests
|
||||
// (HTTP Code 429) is found in the resp parameter. Hence it will return the number of
|
||||
// seconds the server states it may be ready to process more requests from this client.
|
||||
func DefaultBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode == http.StatusServiceUnavailable {
|
||||
if sleep, ok := parseRetryAfterHeader(resp.Header["Retry-After"]); ok {
|
||||
return sleep
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mult := math.Pow(2, float64(attemptNum)) * float64(min)
|
||||
sleep := time.Duration(mult)
|
||||
if float64(sleep) != mult || sleep > max {
|
||||
sleep = max
|
||||
}
|
||||
return sleep
|
||||
}
|
||||
|
||||
// parseRetryAfterHeader parses the Retry-After header and returns the
|
||||
// delay duration according to the spec: https://httpwg.org/specs/rfc7231.html#header.retry-after
|
||||
// The bool returned will be true if the header was successfully parsed.
|
||||
// Otherwise, the header was either not present, or was not parseable according to the spec.
|
||||
//
|
||||
// Retry-After headers come in two flavors: Seconds or HTTP-Date
|
||||
//
|
||||
// Examples:
|
||||
// * Retry-After: Fri, 31 Dec 1999 23:59:59 GMT
|
||||
// * Retry-After: 120
|
||||
func parseRetryAfterHeader(headers []string) (time.Duration, bool) {
|
||||
if len(headers) == 0 || headers[0] == "" {
|
||||
return 0, false
|
||||
}
|
||||
header := headers[0]
|
||||
// Retry-After: 120
|
||||
if sleep, err := strconv.ParseInt(header, 10, 64); err == nil {
|
||||
if sleep < 0 { // a negative sleep doesn't make sense
|
||||
return 0, false
|
||||
}
|
||||
return time.Second * time.Duration(sleep), true
|
||||
}
|
||||
|
||||
// Retry-After: Fri, 31 Dec 1999 23:59:59 GMT
|
||||
retryTime, err := time.Parse(time.RFC1123, header)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
if until := retryTime.Sub(timeNow()); until > 0 {
|
||||
return until, true
|
||||
}
|
||||
// date is in the past
|
||||
return 0, true
|
||||
}
|
||||
|
||||
// LinearJitterBackoff provides a callback for Client.Backoff which will
|
||||
// perform linear backoff based on the attempt number and with jitter to
|
||||
// prevent a thundering herd.
|
||||
//
|
||||
// min and max here are *not* absolute values. The number to be multiplied by
|
||||
// the attempt number will be chosen at random from between them, thus they are
|
||||
// bounding the jitter.
|
||||
//
|
||||
// For instance:
|
||||
// * To get strictly linear backoff of one second increasing each retry, set
|
||||
// both to one second (1s, 2s, 3s, 4s, ...)
|
||||
// * To get a small amount of jitter centered around one second increasing each
|
||||
// retry, set to around one second, such as a min of 800ms and max of 1200ms
|
||||
// (892ms, 2102ms, 2945ms, 4312ms, ...)
|
||||
// * To get extreme jitter, set to a very wide spread, such as a min of 100ms
|
||||
// and a max of 20s (15382ms, 292ms, 51321ms, 35234ms, ...)
|
||||
func LinearJitterBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
|
||||
// attemptNum always starts at zero but we want to start at 1 for multiplication
|
||||
attemptNum++
|
||||
|
||||
if max <= min {
|
||||
// Unclear what to do here, or they are the same, so return min *
|
||||
// attemptNum
|
||||
return min * time.Duration(attemptNum)
|
||||
}
|
||||
|
||||
// Seed rand; doing this every time is fine
|
||||
source := rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
|
||||
|
||||
// Pick a random number that lies somewhere between the min and max and
|
||||
// multiply by the attemptNum. attemptNum starts at zero so we always
|
||||
// increment here. We first get a random percentage, then apply that to the
|
||||
// difference between min and max, and add to min.
|
||||
jitter := source.Float64() * float64(max-min)
|
||||
jitterMin := int64(jitter) + int64(min)
|
||||
return time.Duration(jitterMin * int64(attemptNum))
|
||||
}
|
||||
|
||||
// PassthroughErrorHandler is an ErrorHandler that directly passes through the
|
||||
// values from the net/http library for the final request. The body is not
|
||||
// closed.
|
||||
func PassthroughErrorHandler(resp *http.Response, err error, _ int) (*http.Response, error) {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Do wraps calling an HTTP method with retries.
|
||||
func (c *Client) Do(req *Request) (*http.Response, error) {
|
||||
c.clientInit.Do(func() {
|
||||
if c.HTTPClient == nil {
|
||||
c.HTTPClient = cleanhttp.DefaultPooledClient()
|
||||
}
|
||||
})
|
||||
|
||||
logger := c.logger()
|
||||
|
||||
if logger != nil {
|
||||
switch v := logger.(type) {
|
||||
case LeveledLogger:
|
||||
v.Debug("performing request", "method", req.Method, "url", redactURL(req.URL))
|
||||
case Logger:
|
||||
v.Printf("[DEBUG] %s %s", req.Method, redactURL(req.URL))
|
||||
}
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
var attempt int
|
||||
var shouldRetry bool
|
||||
var doErr, respErr, checkErr, prepareErr error
|
||||
|
||||
for i := 0; ; i++ {
|
||||
doErr, respErr, prepareErr = nil, nil, nil
|
||||
attempt++
|
||||
|
||||
// Always rewind the request body when non-nil.
|
||||
if req.body != nil {
|
||||
body, err := req.body()
|
||||
if err != nil {
|
||||
c.HTTPClient.CloseIdleConnections()
|
||||
return resp, err
|
||||
}
|
||||
if c, ok := body.(io.ReadCloser); ok {
|
||||
req.Body = c
|
||||
} else {
|
||||
req.Body = io.NopCloser(body)
|
||||
}
|
||||
}
|
||||
|
||||
if c.RequestLogHook != nil {
|
||||
switch v := logger.(type) {
|
||||
case LeveledLogger:
|
||||
c.RequestLogHook(hookLogger{v}, req.Request, i)
|
||||
case Logger:
|
||||
c.RequestLogHook(v, req.Request, i)
|
||||
default:
|
||||
c.RequestLogHook(nil, req.Request, i)
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt the request
|
||||
resp, doErr = c.HTTPClient.Do(req.Request)
|
||||
|
||||
// Check if we should continue with retries.
|
||||
shouldRetry, checkErr = c.CheckRetry(req.Context(), resp, doErr)
|
||||
if !shouldRetry && doErr == nil && req.responseHandler != nil {
|
||||
respErr = req.responseHandler(resp)
|
||||
shouldRetry, checkErr = c.CheckRetry(req.Context(), resp, respErr)
|
||||
}
|
||||
|
||||
err := doErr
|
||||
if respErr != nil {
|
||||
err = respErr
|
||||
}
|
||||
if err != nil {
|
||||
switch v := logger.(type) {
|
||||
case LeveledLogger:
|
||||
v.Error("request failed", "error", err, "method", req.Method, "url", redactURL(req.URL))
|
||||
case Logger:
|
||||
v.Printf("[ERR] %s %s request failed: %v", req.Method, redactURL(req.URL), err)
|
||||
}
|
||||
} else {
|
||||
// Call this here to maintain the behavior of logging all requests,
|
||||
// even if CheckRetry signals to stop.
|
||||
if c.ResponseLogHook != nil {
|
||||
// Call the response logger function if provided.
|
||||
switch v := logger.(type) {
|
||||
case LeveledLogger:
|
||||
c.ResponseLogHook(hookLogger{v}, resp)
|
||||
case Logger:
|
||||
c.ResponseLogHook(v, resp)
|
||||
default:
|
||||
c.ResponseLogHook(nil, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !shouldRetry {
|
||||
break
|
||||
}
|
||||
|
||||
// We do this before drainBody because there's no need for the I/O if
|
||||
// we're breaking out
|
||||
remain := c.RetryMax - i
|
||||
if remain <= 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// We're going to retry, consume any response to reuse the connection.
|
||||
if doErr == nil {
|
||||
c.drainBody(resp.Body)
|
||||
}
|
||||
|
||||
wait := c.Backoff(c.RetryWaitMin, c.RetryWaitMax, i, resp)
|
||||
if logger != nil {
|
||||
desc := fmt.Sprintf("%s %s", req.Method, redactURL(req.URL))
|
||||
if resp != nil {
|
||||
desc = fmt.Sprintf("%s (status: %d)", desc, resp.StatusCode)
|
||||
}
|
||||
switch v := logger.(type) {
|
||||
case LeveledLogger:
|
||||
v.Debug("retrying request", "request", desc, "timeout", wait, "remaining", remain)
|
||||
case Logger:
|
||||
v.Printf("[DEBUG] %s: retrying in %s (%d left)", desc, wait, remain)
|
||||
}
|
||||
}
|
||||
timer := time.NewTimer(wait)
|
||||
select {
|
||||
case <-req.Context().Done():
|
||||
timer.Stop()
|
||||
c.HTTPClient.CloseIdleConnections()
|
||||
return nil, req.Context().Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
|
||||
// Make shallow copy of http Request so that we can modify its body
|
||||
// without racing against the closeBody call in persistConn.writeLoop.
|
||||
httpreq := *req.Request
|
||||
req.Request = &httpreq
|
||||
|
||||
if c.PrepareRetry != nil {
|
||||
if err := c.PrepareRetry(req.Request); err != nil {
|
||||
prepareErr = err
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this is the closest we have to success criteria
|
||||
if doErr == nil && respErr == nil && checkErr == nil && prepareErr == nil && !shouldRetry {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
defer c.HTTPClient.CloseIdleConnections()
|
||||
|
||||
var err error
|
||||
if prepareErr != nil {
|
||||
err = prepareErr
|
||||
} else if checkErr != nil {
|
||||
err = checkErr
|
||||
} else if respErr != nil {
|
||||
err = respErr
|
||||
} else {
|
||||
err = doErr
|
||||
}
|
||||
|
||||
if c.ErrorHandler != nil {
|
||||
return c.ErrorHandler(resp, err, attempt)
|
||||
}
|
||||
|
||||
// By default, we close the response body and return an error without
|
||||
// returning the response
|
||||
if resp != nil {
|
||||
c.drainBody(resp.Body)
|
||||
}
|
||||
|
||||
// this means CheckRetry thought the request was a failure, but didn't
|
||||
// communicate why
|
||||
if err == nil {
|
||||
return nil, fmt.Errorf("%s %s giving up after %d attempt(s)",
|
||||
req.Method, redactURL(req.URL), attempt)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%s %s giving up after %d attempt(s): %w",
|
||||
req.Method, redactURL(req.URL), attempt, err)
|
||||
}
|
||||
|
||||
// Try to read the response body so we can reuse this connection.
|
||||
func (c *Client) drainBody(body io.ReadCloser) {
|
||||
defer body.Close()
|
||||
_, err := io.Copy(io.Discard, io.LimitReader(body, respReadLimit))
|
||||
if err != nil {
|
||||
if c.logger() != nil {
|
||||
switch v := c.logger().(type) {
|
||||
case LeveledLogger:
|
||||
v.Error("error reading response body", "error", err)
|
||||
case Logger:
|
||||
v.Printf("[ERR] error reading response body: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get is a shortcut for doing a GET request without making a new client.
|
||||
func Get(url string) (*http.Response, error) {
|
||||
return defaultClient.Get(url)
|
||||
}
|
||||
|
||||
// Get is a convenience helper for doing simple GET requests.
|
||||
func (c *Client) Get(url string) (*http.Response, error) {
|
||||
req, err := NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Do(req)
|
||||
}
|
||||
|
||||
// Head is a shortcut for doing a HEAD request without making a new client.
|
||||
func Head(url string) (*http.Response, error) {
|
||||
return defaultClient.Head(url)
|
||||
}
|
||||
|
||||
// Head is a convenience method for doing simple HEAD requests.
|
||||
func (c *Client) Head(url string) (*http.Response, error) {
|
||||
req, err := NewRequest("HEAD", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Do(req)
|
||||
}
|
||||
|
||||
// Post is a shortcut for doing a POST request without making a new client.
|
||||
func Post(url, bodyType string, body interface{}) (*http.Response, error) {
|
||||
return defaultClient.Post(url, bodyType, body)
|
||||
}
|
||||
|
||||
// Post is a convenience method for doing simple POST requests.
|
||||
func (c *Client) Post(url, bodyType string, body interface{}) (*http.Response, error) {
|
||||
req, err := NewRequest("POST", url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", bodyType)
|
||||
return c.Do(req)
|
||||
}
|
||||
|
||||
// PostForm is a shortcut to perform a POST with form data without creating
|
||||
// a new client.
|
||||
func PostForm(url string, data url.Values) (*http.Response, error) {
|
||||
return defaultClient.PostForm(url, data)
|
||||
}
|
||||
|
||||
// PostForm is a convenience method for doing simple POST operations using
|
||||
// pre-filled url.Values form data.
|
||||
func (c *Client) PostForm(url string, data url.Values) (*http.Response, error) {
|
||||
return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
|
||||
}
|
||||
|
||||
// StandardClient returns a stdlib *http.Client with a custom Transport, which
|
||||
// shims in a *retryablehttp.Client for added retries.
|
||||
func (c *Client) StandardClient() *http.Client {
|
||||
return &http.Client{
|
||||
Transport: &RoundTripper{Client: c},
|
||||
}
|
||||
}
|
||||
|
||||
// Taken from url.URL#Redacted() which was introduced in go 1.15.
|
||||
// We can switch to using it directly if we'll bump the minimum required go version.
|
||||
func redactURL(u *url.URL) string {
|
||||
if u == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
ru := *u
|
||||
if _, has := ru.User.Password(); has {
|
||||
ru.User = url.UserPassword(ru.User.Username(), "xxxxx")
|
||||
}
|
||||
return ru.String()
|
||||
}
|
||||
55
vendor/github.com/hashicorp/go-retryablehttp/roundtripper.go
generated
vendored
Normal file
55
vendor/github.com/hashicorp/go-retryablehttp/roundtripper.go
generated
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package retryablehttp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// RoundTripper implements the http.RoundTripper interface, using a retrying
|
||||
// HTTP client to execute requests.
|
||||
//
|
||||
// It is important to note that retryablehttp doesn't always act exactly as a
|
||||
// RoundTripper should. This is highly dependent on the retryable client's
|
||||
// configuration.
|
||||
type RoundTripper struct {
|
||||
// The client to use during requests. If nil, the default retryablehttp
|
||||
// client and settings will be used.
|
||||
Client *Client
|
||||
|
||||
// once ensures that the logic to initialize the default client runs at
|
||||
// most once, in a single thread.
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// init initializes the underlying retryable client.
|
||||
func (rt *RoundTripper) init() {
|
||||
if rt.Client == nil {
|
||||
rt.Client = NewClient()
|
||||
}
|
||||
}
|
||||
|
||||
// RoundTrip satisfies the http.RoundTripper interface.
|
||||
func (rt *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
rt.once.Do(rt.init)
|
||||
|
||||
// Convert the request to be retryable.
|
||||
retryableReq, err := FromRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Execute the request.
|
||||
resp, err := rt.Client.Do(retryableReq)
|
||||
// If we got an error returned by standard library's `Do` method, unwrap it
|
||||
// otherwise we will wind up erroneously re-nesting the error.
|
||||
if _, ok := err.(*url.Error); ok {
|
||||
return resp, errors.Unwrap(err)
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
4
vendor/github.com/santhosh-tekuri/jsonschema/v6/.gitmodules
generated
vendored
Normal file
4
vendor/github.com/santhosh-tekuri/jsonschema/v6/.gitmodules
generated
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[submodule "testdata/JSON-Schema-Test-Suite"]
|
||||
path = testdata/JSON-Schema-Test-Suite
|
||||
url = https://github.com/json-schema-org/JSON-Schema-Test-Suite.git
|
||||
branch = main
|
||||
5
vendor/github.com/santhosh-tekuri/jsonschema/v6/.golangci.yml
generated
vendored
Normal file
5
vendor/github.com/santhosh-tekuri/jsonschema/v6/.golangci.yml
generated
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
linters:
|
||||
enable:
|
||||
- nakedret
|
||||
- errname
|
||||
- godot
|
||||
7
vendor/github.com/santhosh-tekuri/jsonschema/v6/.pre-commit-hooks.yaml
generated
vendored
Normal file
7
vendor/github.com/santhosh-tekuri/jsonschema/v6/.pre-commit-hooks.yaml
generated
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
- id: jsonschema-validate
|
||||
name: Validate JSON against JSON Schema
|
||||
description: ensure json files follow specified JSON Schema
|
||||
entry: jv
|
||||
language: golang
|
||||
additional_dependencies:
|
||||
- ./cmd/jv
|
||||
|
|
@ -172,31 +172,4 @@
|
|||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2015 xeipuuv
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
of your accepting any such warranty or additional liability.
|
||||
86
vendor/github.com/santhosh-tekuri/jsonschema/v6/README.md
generated
vendored
Normal file
86
vendor/github.com/santhosh-tekuri/jsonschema/v6/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# jsonschema v6.0.0
|
||||
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://pkg.go.dev/github.com/santhosh-tekuri/jsonschema/v6)
|
||||
[](https://goreportcard.com/report/github.com/santhosh-tekuri/jsonschema/v6)
|
||||
[](https://github.com/santhosh-tekuri/jsonschema/actions/workflows/go.yaml)
|
||||
[](https://codecov.io/gh/santhosh-tekuri/jsonschema/tree/boon)
|
||||
|
||||
see [godoc](https://pkg.go.dev/github.com/santhosh-tekuri/jsonschema/v6) for examples
|
||||
|
||||
## Library Features
|
||||
|
||||
- [x] pass [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) excluding optional(compare with other impls at [bowtie](https://bowtie-json-schema.github.io/bowtie/#))
|
||||
- [x] [](https://bowtie.report/#/dialects/draft4)
|
||||
- [x] [](https://bowtie.report/#/dialects/draft6)
|
||||
- [x] [](https://bowtie.report/#/dialects/draft7)
|
||||
- [x] [](https://bowtie.report/#/dialects/draft2019-09)
|
||||
- [x] [](https://bowtie.report/#/dialects/draft2020-12)
|
||||
- [x] detect infinite loop traps
|
||||
- [x] `$schema` cycle
|
||||
- [x] validation cycle
|
||||
- [x] custom `$schema` url
|
||||
- [x] vocabulary based validation
|
||||
- [x] custom regex engine
|
||||
- [x] format assertions
|
||||
- [x] flag to enable in draft >= 2019-09
|
||||
- [x] custom format registration
|
||||
- [x] built-in formats
|
||||
- [x] regex, uuid
|
||||
- [x] ipv4, ipv6
|
||||
- [x] hostname, email
|
||||
- [x] date, time, date-time, duration
|
||||
- [x] json-pointer, relative-json-pointer
|
||||
- [x] uri, uri-reference, uri-template
|
||||
- [x] iri, iri-reference
|
||||
- [x] period, semver
|
||||
- [x] content assertions
|
||||
- [x] flag to enable in draft >= 7
|
||||
- [x] contentEncoding
|
||||
- [x] base64
|
||||
- [x] custom
|
||||
- [x] contentMediaType
|
||||
- [x] application/json
|
||||
- [x] custom
|
||||
- [x] contentSchema
|
||||
- [x] errors
|
||||
- [x] introspectable
|
||||
- [x] hierarchy
|
||||
- [x] alternative display with `#`
|
||||
- [x] output
|
||||
- [x] flag
|
||||
- [x] basic
|
||||
- [x] detailed
|
||||
- [x] custom vocabulary
|
||||
- enable via `$vocabulary` for draft >=2019-19
|
||||
- enable via flag for draft <= 7
|
||||
- [x] mixed dialect support
|
||||
|
||||
## CLI
|
||||
|
||||
to install: `go install github.com/santhosh-tekuri/jsonschema/cmd/jv@latest`
|
||||
|
||||
```
|
||||
Usage: jv [OPTIONS] SCHEMA [INSTANCE...]
|
||||
|
||||
Options:
|
||||
-c, --assert-content Enable content assertions with draft >= 7
|
||||
-f, --assert-format Enable format assertions with draft >= 2019
|
||||
--cacert pem-file Use the specified pem-file to verify the peer. The file may contain multiple CA certificates
|
||||
-d, --draft version Draft version used when '$schema' is missing. Valid values 4, 6, 7, 2019, 2020 (default 2020)
|
||||
-h, --help Print help information
|
||||
-k, --insecure Use insecure TLS connection
|
||||
-o, --output format Output format. Valid values simple, alt, flag, basic, detailed (default "simple")
|
||||
-q, --quiet Do not print errors
|
||||
-v, --version Print build information
|
||||
```
|
||||
|
||||
- [x] exit code `1` for validation erros, `2` for usage errors
|
||||
- [x] validate both schema and multiple instances
|
||||
- [x] support both json and yaml files
|
||||
- [x] support standard input, use `-`
|
||||
- [x] quite mode with parsable output
|
||||
- [x] http(s) url support
|
||||
- [x] custom certs for validation, use `--cacert`
|
||||
- [x] flag to skip certificate verification, use `--insecure`
|
||||
|
||||
332
vendor/github.com/santhosh-tekuri/jsonschema/v6/compiler.go
generated
vendored
Normal file
332
vendor/github.com/santhosh-tekuri/jsonschema/v6/compiler.go
generated
vendored
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
package jsonschema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// Compiler compiles json schema into *Schema.
|
||||
type Compiler struct {
|
||||
schemas map[urlPtr]*Schema
|
||||
roots *roots
|
||||
formats map[string]*Format
|
||||
decoders map[string]*Decoder
|
||||
mediaTypes map[string]*MediaType
|
||||
assertFormat bool
|
||||
assertContent bool
|
||||
}
|
||||
|
||||
// NewCompiler create Compiler Object.
|
||||
func NewCompiler() *Compiler {
|
||||
return &Compiler{
|
||||
schemas: map[urlPtr]*Schema{},
|
||||
roots: newRoots(),
|
||||
formats: map[string]*Format{},
|
||||
decoders: map[string]*Decoder{},
|
||||
mediaTypes: map[string]*MediaType{},
|
||||
assertFormat: false,
|
||||
assertContent: false,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultDraft overrides the draft used to
|
||||
// compile schemas without `$schema` field.
|
||||
//
|
||||
// By default, this library uses the latest
|
||||
// draft supported.
|
||||
//
|
||||
// The use of this option is HIGHLY encouraged
|
||||
// to ensure continued correct operation of your
|
||||
// schema. The current default value will not stay
|
||||
// the same overtime.
|
||||
func (c *Compiler) DefaultDraft(d *Draft) {
|
||||
c.roots.defaultDraft = d
|
||||
}
|
||||
|
||||
// AssertFormat always enables format assertions.
|
||||
//
|
||||
// Default Behavior:
|
||||
// for draft-07: enabled.
|
||||
// for draft/2019-09: disabled unless metaschema says `format` vocabulary is required.
|
||||
// for draft/2020-12: disabled unless metaschema says `format-assertion` vocabulary is required.
|
||||
func (c *Compiler) AssertFormat() {
|
||||
c.assertFormat = true
|
||||
}
|
||||
|
||||
// AssertContent enables content assertions.
|
||||
//
|
||||
// Content assertions include keywords:
|
||||
// - contentEncoding
|
||||
// - contentMediaType
|
||||
// - contentSchema
|
||||
//
|
||||
// Default behavior is always disabled.
|
||||
func (c *Compiler) AssertContent() {
|
||||
c.assertContent = true
|
||||
}
|
||||
|
||||
// RegisterFormat registers custom format.
|
||||
//
|
||||
// NOTE:
|
||||
// - "regex" format can not be overridden
|
||||
// - format assertions are disabled for draft >= 2019-09
|
||||
// see [Compiler.AssertFormat]
|
||||
func (c *Compiler) RegisterFormat(f *Format) {
|
||||
if f.Name != "regex" {
|
||||
c.formats[f.Name] = f
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterContentEncoding registers custom contentEncoding.
|
||||
//
|
||||
// NOTE: content assertions are disabled by default.
|
||||
// see [Compiler.AssertContent].
|
||||
func (c *Compiler) RegisterContentEncoding(d *Decoder) {
|
||||
c.decoders[d.Name] = d
|
||||
}
|
||||
|
||||
// RegisterContentMediaType registers custom contentMediaType.
|
||||
//
|
||||
// NOTE: content assertions are disabled by default.
|
||||
// see [Compiler.AssertContent].
|
||||
func (c *Compiler) RegisterContentMediaType(mt *MediaType) {
|
||||
c.mediaTypes[mt.Name] = mt
|
||||
}
|
||||
|
||||
// RegisterVocabulary registers custom vocabulary.
|
||||
//
|
||||
// NOTE:
|
||||
// - vocabularies are disabled for draft >= 2019-09
|
||||
// see [Compiler.AssertVocabs]
|
||||
func (c *Compiler) RegisterVocabulary(vocab *Vocabulary) {
|
||||
c.roots.vocabularies[vocab.URL] = vocab
|
||||
}
|
||||
|
||||
// AssertVocabs always enables user-defined vocabularies assertions.
|
||||
//
|
||||
// Default Behavior:
|
||||
// for draft-07: enabled.
|
||||
// for draft/2019-09: disabled unless metaschema enables a vocabulary.
|
||||
// for draft/2020-12: disabled unless metaschema enables a vocabulary.
|
||||
func (c *Compiler) AssertVocabs() {
|
||||
c.roots.assertVocabs = true
|
||||
}
|
||||
|
||||
// AddResource adds schema resource which gets used later in reference
|
||||
// resolution.
|
||||
//
|
||||
// The argument url can be file path or url. Any fragment in url is ignored.
|
||||
// The argument doc must be valid json value.
|
||||
func (c *Compiler) AddResource(url string, doc any) error {
|
||||
uf, err := absolute(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isMeta(string(uf.url)) {
|
||||
return &ResourceExistsError{string(uf.url)}
|
||||
}
|
||||
if !c.roots.loader.add(uf.url, doc) {
|
||||
return &ResourceExistsError{string(uf.url)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UseLoader overrides the default [URLLoader] used
|
||||
// to load schema resources.
|
||||
func (c *Compiler) UseLoader(loader URLLoader) {
|
||||
c.roots.loader.loader = loader
|
||||
}
|
||||
|
||||
// UseRegexpEngine changes the regexp-engine used.
|
||||
// By default it uses regexp package from go standard
|
||||
// library.
|
||||
//
|
||||
// NOTE: must be called before compiling any schemas.
|
||||
func (c *Compiler) UseRegexpEngine(engine RegexpEngine) {
|
||||
if engine == nil {
|
||||
engine = goRegexpCompile
|
||||
}
|
||||
c.roots.regexpEngine = engine
|
||||
}
|
||||
|
||||
func (c *Compiler) enqueue(q *queue, up urlPtr) *Schema {
|
||||
if sch, ok := c.schemas[up]; ok {
|
||||
// already got compiled
|
||||
return sch
|
||||
}
|
||||
if sch := q.get(up); sch != nil {
|
||||
return sch
|
||||
}
|
||||
sch := newSchema(up)
|
||||
q.append(sch)
|
||||
return sch
|
||||
}
|
||||
|
||||
// MustCompile is like [Compile] but panics if compilation fails.
|
||||
// It simplifies safe initialization of global variables holding
|
||||
// compiled schema.
|
||||
func (c *Compiler) MustCompile(loc string) *Schema {
|
||||
sch, err := c.Compile(loc)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("jsonschema: Compile(%q): %v", loc, err))
|
||||
}
|
||||
return sch
|
||||
}
|
||||
|
||||
// Compile compiles json-schema at given loc.
|
||||
func (c *Compiler) Compile(loc string) (*Schema, error) {
|
||||
uf, err := absolute(loc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
up, err := c.roots.resolveFragment(*uf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.doCompile(up)
|
||||
}
|
||||
|
||||
func (c *Compiler) doCompile(up urlPtr) (*Schema, error) {
|
||||
q := &queue{}
|
||||
compiled := 0
|
||||
|
||||
c.enqueue(q, up)
|
||||
for q.len() > compiled {
|
||||
sch := q.at(compiled)
|
||||
if err := c.roots.ensureSubschema(sch.up); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := c.roots.roots[sch.up.url]
|
||||
v, err := sch.up.lookup(r.doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.compileValue(v, sch, r, q); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
compiled++
|
||||
}
|
||||
for _, sch := range *q {
|
||||
c.schemas[sch.up] = sch
|
||||
}
|
||||
return c.schemas[up], nil
|
||||
}
|
||||
|
||||
func (c *Compiler) compileValue(v any, sch *Schema, r *root, q *queue) error {
|
||||
res := r.resource(sch.up.ptr)
|
||||
sch.DraftVersion = res.dialect.draft.version
|
||||
|
||||
base := urlPtr{sch.up.url, res.ptr}
|
||||
sch.resource = c.enqueue(q, base)
|
||||
|
||||
// if resource, enqueue dynamic anchors for compilation
|
||||
if sch.DraftVersion >= 2020 && sch.up == sch.resource.up {
|
||||
res := r.resource(sch.up.ptr)
|
||||
for anchor, anchorPtr := range res.anchors {
|
||||
if slices.Contains(res.dynamicAnchors, anchor) {
|
||||
up := urlPtr{sch.up.url, anchorPtr}
|
||||
danchorSch := c.enqueue(q, up)
|
||||
if sch.dynamicAnchors == nil {
|
||||
sch.dynamicAnchors = map[string]*Schema{}
|
||||
}
|
||||
sch.dynamicAnchors[string(anchor)] = danchorSch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch v := v.(type) {
|
||||
case bool:
|
||||
sch.Bool = &v
|
||||
case map[string]any:
|
||||
if err := c.compileObject(v, sch, r, q); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
sch.allPropsEvaluated = sch.AdditionalProperties != nil
|
||||
if sch.DraftVersion < 2020 {
|
||||
sch.allItemsEvaluated = sch.AdditionalItems != nil
|
||||
switch items := sch.Items.(type) {
|
||||
case *Schema:
|
||||
sch.allItemsEvaluated = true
|
||||
case []*Schema:
|
||||
sch.numItemsEvaluated = len(items)
|
||||
}
|
||||
} else {
|
||||
sch.allItemsEvaluated = sch.Items2020 != nil
|
||||
sch.numItemsEvaluated = len(sch.PrefixItems)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Compiler) compileObject(obj map[string]any, sch *Schema, r *root, q *queue) error {
|
||||
if len(obj) == 0 {
|
||||
b := true
|
||||
sch.Bool = &b
|
||||
return nil
|
||||
}
|
||||
oc := objCompiler{
|
||||
c: c,
|
||||
obj: obj,
|
||||
up: sch.up,
|
||||
r: r,
|
||||
res: r.resource(sch.up.ptr),
|
||||
q: q,
|
||||
}
|
||||
return oc.compile(sch)
|
||||
}
|
||||
|
||||
// queue --
|
||||
|
||||
type queue []*Schema
|
||||
|
||||
func (q *queue) append(sch *Schema) {
|
||||
*q = append(*q, sch)
|
||||
}
|
||||
|
||||
func (q *queue) at(i int) *Schema {
|
||||
return (*q)[i]
|
||||
}
|
||||
|
||||
func (q *queue) len() int {
|
||||
return len(*q)
|
||||
}
|
||||
|
||||
func (q *queue) get(up urlPtr) *Schema {
|
||||
i := slices.IndexFunc(*q, func(sch *Schema) bool { return sch.up == up })
|
||||
if i != -1 {
|
||||
return (*q)[i]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// regexp --
|
||||
|
||||
// Regexp is the representation of compiled regular expression.
|
||||
type Regexp interface {
|
||||
fmt.Stringer
|
||||
|
||||
// MatchString reports whether the string s contains
|
||||
// any match of the regular expression.
|
||||
MatchString(string) bool
|
||||
}
|
||||
|
||||
// RegexpEngine parses a regular expression and returns,
|
||||
// if successful, a Regexp object that can be used to
|
||||
// match against text.
|
||||
type RegexpEngine func(string) (Regexp, error)
|
||||
|
||||
func (re RegexpEngine) validate(v any) error {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
_, err := re(s)
|
||||
return err
|
||||
}
|
||||
|
||||
func goRegexpCompile(s string) (Regexp, error) {
|
||||
return regexp.Compile(s)
|
||||
}
|
||||
51
vendor/github.com/santhosh-tekuri/jsonschema/v6/content.go
generated
vendored
Normal file
51
vendor/github.com/santhosh-tekuri/jsonschema/v6/content.go
generated
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package jsonschema
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Decoder specifies how to decode specific contentEncoding.
|
||||
type Decoder struct {
|
||||
// Name of contentEncoding.
|
||||
Name string
|
||||
// Decode given string to byte array.
|
||||
Decode func(string) ([]byte, error)
|
||||
}
|
||||
|
||||
var decoders = map[string]*Decoder{
|
||||
"base64": {
|
||||
Name: "base64",
|
||||
Decode: func(s string) ([]byte, error) {
|
||||
return base64.StdEncoding.DecodeString(s)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// MediaType specified how to validate bytes against specific contentMediaType.
|
||||
type MediaType struct {
|
||||
// Name of contentMediaType.
|
||||
Name string
|
||||
|
||||
// Validate checks whether bytes conform to this mediatype.
|
||||
Validate func([]byte) error
|
||||
|
||||
// UnmarshalJSON unmarshals bytes into json value.
|
||||
// This must be nil if this mediatype is not compatible
|
||||
// with json.
|
||||
UnmarshalJSON func([]byte) (any, error)
|
||||
}
|
||||
|
||||
var mediaTypes = map[string]*MediaType{
|
||||
"application/json": {
|
||||
Name: "application/json",
|
||||
Validate: func(b []byte) error {
|
||||
var v any
|
||||
return json.Unmarshal(b, &v)
|
||||
},
|
||||
UnmarshalJSON: func(b []byte) (any, error) {
|
||||
return UnmarshalJSON(bytes.NewReader(b))
|
||||
},
|
||||
},
|
||||
}
|
||||
360
vendor/github.com/santhosh-tekuri/jsonschema/v6/draft.go
generated
vendored
Normal file
360
vendor/github.com/santhosh-tekuri/jsonschema/v6/draft.go
generated
vendored
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
package jsonschema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A Draft represents json-schema specification.
|
||||
type Draft struct {
|
||||
version int
|
||||
url string
|
||||
sch *Schema
|
||||
id string // property name used to represent id
|
||||
subschemas []SchemaPath // locations of subschemas
|
||||
vocabPrefix string // prefix used for vocabulary
|
||||
allVocabs map[string]*Schema // names of supported vocabs with its schemas
|
||||
defaultVocabs []string // names of default vocabs
|
||||
}
|
||||
|
||||
// String returns the specification url.
|
||||
func (d *Draft) String() string {
|
||||
return d.url
|
||||
}
|
||||
|
||||
var (
|
||||
Draft4 = &Draft{
|
||||
version: 4,
|
||||
url: "http://json-schema.org/draft-04/schema",
|
||||
id: "id",
|
||||
subschemas: []SchemaPath{
|
||||
// type agonistic
|
||||
schemaPath("definitions/*"),
|
||||
schemaPath("not"),
|
||||
schemaPath("allOf/[]"),
|
||||
schemaPath("anyOf/[]"),
|
||||
schemaPath("oneOf/[]"),
|
||||
// object
|
||||
schemaPath("properties/*"),
|
||||
schemaPath("additionalProperties"),
|
||||
schemaPath("patternProperties/*"),
|
||||
// array
|
||||
schemaPath("items"),
|
||||
schemaPath("items/[]"),
|
||||
schemaPath("additionalItems"),
|
||||
schemaPath("dependencies/*"),
|
||||
},
|
||||
vocabPrefix: "",
|
||||
allVocabs: map[string]*Schema{},
|
||||
defaultVocabs: []string{},
|
||||
}
|
||||
|
||||
Draft6 = &Draft{
|
||||
version: 6,
|
||||
url: "http://json-schema.org/draft-06/schema",
|
||||
id: "$id",
|
||||
subschemas: joinSubschemas(Draft4.subschemas,
|
||||
schemaPath("propertyNames"),
|
||||
schemaPath("contains"),
|
||||
),
|
||||
vocabPrefix: "",
|
||||
allVocabs: map[string]*Schema{},
|
||||
defaultVocabs: []string{},
|
||||
}
|
||||
|
||||
Draft7 = &Draft{
|
||||
version: 7,
|
||||
url: "http://json-schema.org/draft-07/schema",
|
||||
id: "$id",
|
||||
subschemas: joinSubschemas(Draft6.subschemas,
|
||||
schemaPath("if"),
|
||||
schemaPath("then"),
|
||||
schemaPath("else"),
|
||||
),
|
||||
vocabPrefix: "",
|
||||
allVocabs: map[string]*Schema{},
|
||||
defaultVocabs: []string{},
|
||||
}
|
||||
|
||||
Draft2019 = &Draft{
|
||||
version: 2019,
|
||||
url: "https://json-schema.org/draft/2019-09/schema",
|
||||
id: "$id",
|
||||
subschemas: joinSubschemas(Draft7.subschemas,
|
||||
schemaPath("$defs/*"),
|
||||
schemaPath("dependentSchemas/*"),
|
||||
schemaPath("unevaluatedProperties"),
|
||||
schemaPath("unevaluatedItems"),
|
||||
schemaPath("contentSchema"),
|
||||
),
|
||||
vocabPrefix: "https://json-schema.org/draft/2019-09/vocab/",
|
||||
allVocabs: map[string]*Schema{
|
||||
"core": nil,
|
||||
"applicator": nil,
|
||||
"validation": nil,
|
||||
"meta-data": nil,
|
||||
"format": nil,
|
||||
"content": nil,
|
||||
},
|
||||
defaultVocabs: []string{"core", "applicator", "validation"},
|
||||
}
|
||||
|
||||
Draft2020 = &Draft{
|
||||
version: 2020,
|
||||
url: "https://json-schema.org/draft/2020-12/schema",
|
||||
id: "$id",
|
||||
subschemas: joinSubschemas(Draft2019.subschemas,
|
||||
schemaPath("prefixItems/[]"),
|
||||
),
|
||||
vocabPrefix: "https://json-schema.org/draft/2020-12/vocab/",
|
||||
allVocabs: map[string]*Schema{
|
||||
"core": nil,
|
||||
"applicator": nil,
|
||||
"unevaluated": nil,
|
||||
"validation": nil,
|
||||
"meta-data": nil,
|
||||
"format-annotation": nil,
|
||||
"format-assertion": nil,
|
||||
"content": nil,
|
||||
},
|
||||
defaultVocabs: []string{"core", "applicator", "unevaluated", "validation"},
|
||||
}
|
||||
|
||||
draftLatest = Draft2020
|
||||
)
|
||||
|
||||
func init() {
|
||||
c := NewCompiler()
|
||||
c.AssertFormat()
|
||||
for _, d := range []*Draft{Draft4, Draft6, Draft7, Draft2019, Draft2020} {
|
||||
d.sch = c.MustCompile(d.url)
|
||||
for name := range d.allVocabs {
|
||||
d.allVocabs[name] = c.MustCompile(strings.TrimSuffix(d.url, "schema") + "meta/" + name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func draftFromURL(url string) *Draft {
|
||||
u, frag := split(url)
|
||||
if frag != "" {
|
||||
return nil
|
||||
}
|
||||
u, ok := strings.CutPrefix(u, "http://")
|
||||
if !ok {
|
||||
u, _ = strings.CutPrefix(u, "https://")
|
||||
}
|
||||
switch u {
|
||||
case "json-schema.org/schema":
|
||||
return draftLatest
|
||||
case "json-schema.org/draft/2020-12/schema":
|
||||
return Draft2020
|
||||
case "json-schema.org/draft/2019-09/schema":
|
||||
return Draft2019
|
||||
case "json-schema.org/draft-07/schema":
|
||||
return Draft7
|
||||
case "json-schema.org/draft-06/schema":
|
||||
return Draft6
|
||||
case "json-schema.org/draft-04/schema":
|
||||
return Draft4
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Draft) getID(obj map[string]any) string {
|
||||
if d.version < 2019 {
|
||||
if _, ok := obj["$ref"]; ok {
|
||||
// All other properties in a "$ref" object MUST be ignored
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
id, ok := strVal(obj, d.id)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
id, _ = split(id) // ignore fragment
|
||||
return id
|
||||
}
|
||||
|
||||
func (d *Draft) getVocabs(url url, doc any, vocabularies map[string]*Vocabulary) ([]string, error) {
|
||||
if d.version < 2019 {
|
||||
return nil, nil
|
||||
}
|
||||
obj, ok := doc.(map[string]any)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
v, ok := obj["$vocabulary"]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
obj, ok = v.(map[string]any)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var vocabs []string
|
||||
for vocab, reqd := range obj {
|
||||
if reqd, ok := reqd.(bool); !ok || !reqd {
|
||||
continue
|
||||
}
|
||||
name, ok := strings.CutPrefix(vocab, d.vocabPrefix)
|
||||
if ok {
|
||||
if _, ok := d.allVocabs[name]; ok {
|
||||
if !slices.Contains(vocabs, name) {
|
||||
vocabs = append(vocabs, name)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, ok := vocabularies[vocab]; !ok {
|
||||
return nil, &UnsupportedVocabularyError{url.String(), vocab}
|
||||
}
|
||||
if !slices.Contains(vocabs, vocab) {
|
||||
vocabs = append(vocabs, vocab)
|
||||
}
|
||||
}
|
||||
if !slices.Contains(vocabs, "core") {
|
||||
vocabs = append(vocabs, "core")
|
||||
}
|
||||
return vocabs, nil
|
||||
}
|
||||
|
||||
// --
|
||||
|
||||
type dialect struct {
|
||||
draft *Draft
|
||||
vocabs []string // nil means use draft.defaultVocabs
|
||||
}
|
||||
|
||||
func (d *dialect) hasVocab(name string) bool {
|
||||
if name == "core" || d.draft.version < 2019 {
|
||||
return true
|
||||
}
|
||||
if d.vocabs != nil {
|
||||
return slices.Contains(d.vocabs, name)
|
||||
}
|
||||
return slices.Contains(d.draft.defaultVocabs, name)
|
||||
}
|
||||
|
||||
func (d *dialect) activeVocabs(assertVocabs bool, vocabularies map[string]*Vocabulary) []string {
|
||||
if len(vocabularies) == 0 {
|
||||
return d.vocabs
|
||||
}
|
||||
if d.draft.version < 2019 {
|
||||
assertVocabs = true
|
||||
}
|
||||
if !assertVocabs {
|
||||
return d.vocabs
|
||||
}
|
||||
var vocabs []string
|
||||
if d.vocabs == nil {
|
||||
vocabs = slices.Clone(d.draft.defaultVocabs)
|
||||
} else {
|
||||
vocabs = slices.Clone(d.vocabs)
|
||||
}
|
||||
for vocab := range vocabularies {
|
||||
if !slices.Contains(vocabs, vocab) {
|
||||
vocabs = append(vocabs, vocab)
|
||||
}
|
||||
}
|
||||
return vocabs
|
||||
}
|
||||
|
||||
func (d *dialect) getSchema(assertVocabs bool, vocabularies map[string]*Vocabulary) *Schema {
|
||||
vocabs := d.activeVocabs(assertVocabs, vocabularies)
|
||||
if vocabs == nil {
|
||||
return d.draft.sch
|
||||
}
|
||||
|
||||
var allOf []*Schema
|
||||
for _, vocab := range vocabs {
|
||||
sch := d.draft.allVocabs[vocab]
|
||||
if sch == nil {
|
||||
if v, ok := vocabularies[vocab]; ok {
|
||||
sch = v.Schema
|
||||
}
|
||||
}
|
||||
if sch != nil {
|
||||
allOf = append(allOf, sch)
|
||||
}
|
||||
}
|
||||
if !slices.Contains(vocabs, "core") {
|
||||
sch := d.draft.allVocabs["core"]
|
||||
if sch == nil {
|
||||
sch = d.draft.sch
|
||||
}
|
||||
allOf = append(allOf, sch)
|
||||
}
|
||||
sch := &Schema{
|
||||
Location: "urn:mem:metaschema",
|
||||
up: urlPtr{url("urn:mem:metaschema"), ""},
|
||||
DraftVersion: d.draft.version,
|
||||
AllOf: allOf,
|
||||
}
|
||||
sch.resource = sch
|
||||
if sch.DraftVersion >= 2020 {
|
||||
sch.DynamicAnchor = "meta"
|
||||
sch.dynamicAnchors = map[string]*Schema{
|
||||
"meta": sch,
|
||||
}
|
||||
}
|
||||
return sch
|
||||
}
|
||||
|
||||
// --
|
||||
|
||||
type ParseIDError struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
func (e *ParseIDError) Error() string {
|
||||
return fmt.Sprintf("error in parsing id at %q", e.URL)
|
||||
}
|
||||
|
||||
// --
|
||||
|
||||
type ParseAnchorError struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
func (e *ParseAnchorError) Error() string {
|
||||
return fmt.Sprintf("error in parsing anchor at %q", e.URL)
|
||||
}
|
||||
|
||||
// --
|
||||
|
||||
type DuplicateIDError struct {
|
||||
ID string
|
||||
URL string
|
||||
Ptr1 string
|
||||
Ptr2 string
|
||||
}
|
||||
|
||||
func (e *DuplicateIDError) Error() string {
|
||||
return fmt.Sprintf("duplicate id %q in %q at %q and %q", e.ID, e.URL, e.Ptr1, e.Ptr2)
|
||||
}
|
||||
|
||||
// --
|
||||
|
||||
type DuplicateAnchorError struct {
|
||||
Anchor string
|
||||
URL string
|
||||
Ptr1 string
|
||||
Ptr2 string
|
||||
}
|
||||
|
||||
func (e *DuplicateAnchorError) Error() string {
|
||||
return fmt.Sprintf("duplicate anchor %q in %q at %q and %q", e.Anchor, e.URL, e.Ptr1, e.Ptr2)
|
||||
}
|
||||
|
||||
// --
|
||||
|
||||
func joinSubschemas(a1 []SchemaPath, a2 ...SchemaPath) []SchemaPath {
|
||||
var a []SchemaPath
|
||||
a = append(a, a1...)
|
||||
a = append(a, a2...)
|
||||
return a
|
||||
}
|
||||
708
vendor/github.com/santhosh-tekuri/jsonschema/v6/format.go
generated
vendored
Normal file
708
vendor/github.com/santhosh-tekuri/jsonschema/v6/format.go
generated
vendored
Normal file
|
|
@ -0,0 +1,708 @@
|
|||
package jsonschema
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
gourl "net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Format defined specific format.
|
||||
type Format struct {
|
||||
// Name of format.
|
||||
Name string
|
||||
|
||||
// Validate checks if given value is of this format.
|
||||
Validate func(v any) error
|
||||
}
|
||||
|
||||
var formats = map[string]*Format{
|
||||
"json-pointer": {"json-pointer", validateJSONPointer},
|
||||
"relative-json-pointer": {"relative-json-pointer", validateRelativeJSONPointer},
|
||||
"uuid": {"uuid", validateUUID},
|
||||
"duration": {"duration", validateDuration},
|
||||
"period": {"period", validatePeriod},
|
||||
"ipv4": {"ipv4", validateIPV4},
|
||||
"ipv6": {"ipv6", validateIPV6},
|
||||
"hostname": {"hostname", validateHostname},
|
||||
"email": {"email", validateEmail},
|
||||
"date": {"date", validateDate},
|
||||
"time": {"time", validateTime},
|
||||
"date-time": {"date-time", validateDateTime},
|
||||
"uri": {"uri", validateURI},
|
||||
"iri": {"iri", validateURI},
|
||||
"uri-reference": {"uri-reference", validateURIReference},
|
||||
"iri-reference": {"iri-reference", validateURIReference},
|
||||
"uri-template": {"uri-template", validateURITemplate},
|
||||
"semver": {"semver", validateSemver},
|
||||
}
|
||||
|
||||
// see https://www.rfc-editor.org/rfc/rfc6901#section-3
|
||||
func validateJSONPointer(v any) error {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
if !strings.HasPrefix(s, "/") {
|
||||
return LocalizableError("not starting with /")
|
||||
}
|
||||
for _, tok := range strings.Split(s, "/")[1:] {
|
||||
escape := false
|
||||
for _, ch := range tok {
|
||||
if escape {
|
||||
escape = false
|
||||
if ch != '0' && ch != '1' {
|
||||
return LocalizableError("~ must be followed by 0 or 1")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if ch == '~' {
|
||||
escape = true
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case ch >= '\x00' && ch <= '\x2E':
|
||||
case ch >= '\x30' && ch <= '\x7D':
|
||||
case ch >= '\x7F' && ch <= '\U0010FFFF':
|
||||
default:
|
||||
return LocalizableError("invalid character %q", ch)
|
||||
}
|
||||
}
|
||||
if escape {
|
||||
return LocalizableError("~ must be followed by 0 or 1")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// see https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01#section-3
|
||||
func validateRelativeJSONPointer(v any) error {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// start with non-negative-integer
|
||||
numDigits := 0
|
||||
for _, ch := range s {
|
||||
if ch >= '0' && ch <= '9' {
|
||||
numDigits++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if numDigits == 0 {
|
||||
return LocalizableError("must start with non-negative integer")
|
||||
}
|
||||
if numDigits > 1 && strings.HasPrefix(s, "0") {
|
||||
return LocalizableError("starts with zero")
|
||||
}
|
||||
s = s[numDigits:]
|
||||
|
||||
// followed by either json-pointer or '#'
|
||||
if s == "#" {
|
||||
return nil
|
||||
}
|
||||
return validateJSONPointer(s)
|
||||
}
|
||||
|
||||
// see https://datatracker.ietf.org/doc/html/rfc4122#page-4
|
||||
func validateUUID(v any) error {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
hexGroups := []int{8, 4, 4, 4, 12}
|
||||
groups := strings.Split(s, "-")
|
||||
if len(groups) != len(hexGroups) {
|
||||
return LocalizableError("must have %d elements", len(hexGroups))
|
||||
}
|
||||
for i, group := range groups {
|
||||
if len(group) != hexGroups[i] {
|
||||
return LocalizableError("element %d must be %d characters long", i+1, hexGroups[i])
|
||||
}
|
||||
for _, ch := range group {
|
||||
switch {
|
||||
case ch >= '0' && ch <= '9':
|
||||
case ch >= 'a' && ch <= 'f':
|
||||
case ch >= 'A' && ch <= 'F':
|
||||
default:
|
||||
return LocalizableError("non-hex character %q", ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// see https://datatracker.ietf.org/doc/html/rfc3339#appendix-A
|
||||
func validateDuration(v any) error {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// must start with 'P'
|
||||
s, ok = strings.CutPrefix(s, "P")
|
||||
if !ok {
|
||||
return LocalizableError("must start with P")
|
||||
}
|
||||
if s == "" {
|
||||
return LocalizableError("nothing after P")
|
||||
}
|
||||
|
||||
// dur-week
|
||||
if s, ok := strings.CutSuffix(s, "W"); ok {
|
||||
if s == "" {
|
||||
return LocalizableError("no number in week")
|
||||
}
|
||||
for _, ch := range s {
|
||||
if ch < '0' || ch > '9' {
|
||||
return LocalizableError("invalid week")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
allUnits := []string{"YMD", "HMS"}
|
||||
for i, s := range strings.Split(s, "T") {
|
||||
if i != 0 && s == "" {
|
||||
return LocalizableError("no time elements")
|
||||
}
|
||||
if i >= len(allUnits) {
|
||||
return LocalizableError("more than one T")
|
||||
}
|
||||
units := allUnits[i]
|
||||
for s != "" {
|
||||
digitCount := 0
|
||||
for _, ch := range s {
|
||||
if ch >= '0' && ch <= '9' {
|
||||
digitCount++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if digitCount == 0 {
|
||||
return LocalizableError("missing number")
|
||||
}
|
||||
s = s[digitCount:]
|
||||
if s == "" {
|
||||
return LocalizableError("missing unit")
|
||||
}
|
||||
unit := s[0]
|
||||
j := strings.IndexByte(units, unit)
|
||||
if j == -1 {
|
||||
if strings.IndexByte(allUnits[i], unit) != -1 {
|
||||
return LocalizableError("unit %q out of order", unit)
|
||||
}
|
||||
return LocalizableError("invalid unit %q", unit)
|
||||
}
|
||||
units = units[j+1:]
|
||||
s = s[1:]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateIPV4(v any) error {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
groups := strings.Split(s, ".")
|
||||
if len(groups) != 4 {
|
||||
return LocalizableError("expected four decimals")
|
||||
}
|
||||
for _, group := range groups {
|
||||
if len(group) > 1 && group[0] == '0' {
|
||||
return LocalizableError("leading zeros")
|
||||
}
|
||||
n, err := strconv.Atoi(group)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n < 0 || n > 255 {
|
||||
return LocalizableError("decimal must be between 0 and 255")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateIPV6(v any) error {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if !strings.Contains(s, ":") {
|
||||
return LocalizableError("missing colon")
|
||||
}
|
||||
addr, err := netip.ParseAddr(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if addr.Zone() != "" {
|
||||
return LocalizableError("zone id is not a part of ipv6 address")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// see https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
|
||||
func validateHostname(v any) error {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// entire hostname (including the delimiting dots but not a trailing dot) has a maximum of 253 ASCII characters
|
||||
s = strings.TrimSuffix(s, ".")
|
||||
if len(s) > 253 {
|
||||
return LocalizableError("more than 253 characters long")
|
||||
}
|
||||
|
||||
// Hostnames are composed of series of labels concatenated with dots, as are all domain names
|
||||
for _, label := range strings.Split(s, ".") {
|
||||
// Each label must be from 1 to 63 characters long
|
||||
if len(label) < 1 || len(label) > 63 {
|
||||
return LocalizableError("label must be 1 to 63 characters long")
|
||||
}
|
||||
|
||||
// labels must not start or end with a hyphen
|
||||
if strings.HasPrefix(label, "-") {
|
||||
return LocalizableError("label starts with hyphen")
|
||||
}
|
||||
if strings.HasSuffix(label, "-") {
|
||||
return LocalizableError("label ends with hyphen")
|
||||
}
|
||||
|
||||
// labels may contain only the ASCII letters 'a' through 'z' (in a case-insensitive manner),
|
||||
// the digits '0' through '9', and the hyphen ('-')
|
||||
for _, ch := range label {
|
||||
switch {
|
||||
case ch >= 'a' && ch <= 'z':
|
||||
case ch >= 'A' && ch <= 'Z':
|
||||
case ch >= '0' && ch <= '9':
|
||||
case ch == '-':
|
||||
default:
|
||||
return LocalizableError("invalid character %q", ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// see https://en.wikipedia.org/wiki/Email_address
|
||||
func validateEmail(v any) error {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
// entire email address to be no more than 254 characters long
|
||||
if len(s) > 254 {
|
||||
return LocalizableError("more than 255 characters long")
|
||||
}
|
||||
|
||||
// email address is generally recognized as having two parts joined with an at-sign
|
||||
at := strings.LastIndexByte(s, '@')
|
||||
if at == -1 {
|
||||
return LocalizableError("missing @")
|
||||
}
|
||||
local, domain := s[:at], s[at+1:]
|
||||
|
||||
// local part may be up to 64 characters long
|
||||
if len(local) > 64 {
|
||||
return LocalizableError("local part more than 64 characters long")
|
||||
}
|
||||
|
||||
if len(local) > 1 && strings.HasPrefix(local, `"`) && strings.HasPrefix(local, `"`) {
|
||||
// quoted
|
||||
local := local[1 : len(local)-1]
|
||||
if strings.IndexByte(local, '\\') != -1 || strings.IndexByte(local, '"') != -1 {
|
||||
return LocalizableError("backslash and quote are not allowed within quoted local part")
|
||||
}
|
||||
} else {
|
||||
// unquoted
|
||||
if strings.HasPrefix(local, ".") {
|
||||
return LocalizableError("starts with dot")
|
||||
}
|
||||
if strings.HasSuffix(local, ".") {
|
||||
return LocalizableError("ends with dot")
|
||||
}
|
||||
|
||||
// consecutive dots not allowed
|
||||
if strings.Contains(local, "..") {
|
||||
return LocalizableError("consecutive dots")
|
||||
}
|
||||
|
||||
// check allowed chars
|
||||
for _, ch := range local {
|
||||
switch {
|
||||
case ch >= 'a' && ch <= 'z':
|
||||
case ch >= 'A' && ch <= 'Z':
|
||||
case ch >= '0' && ch <= '9':
|
||||
case strings.ContainsRune(".!#$%&'*+-/=?^_`{|}~", ch):
|
||||
default:
|
||||
return LocalizableError("invalid character %q", ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// domain if enclosed in brackets, must match an IP address
|
||||
if strings.HasPrefix(domain, "[") && strings.HasSuffix(domain, "]") {
|
||||
domain = domain[1 : len(domain)-1]
|
||||
if rem, ok := strings.CutPrefix(domain, "IPv6:"); ok {
|
||||
if err := validateIPV6(rem); err != nil {
|
||||
return LocalizableError("invalid ipv6 address: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := validateIPV4(domain); err != nil {
|
||||
return LocalizableError("invalid ipv4 address: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// domain must match the requirements for a hostname
|
||||
if err := validateHostname(domain); err != nil {
|
||||
return LocalizableError("invalid domain: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// see see https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
|
||||
func validateDate(v any) error {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
_, err := time.Parse("2006-01-02", s)
|
||||
return err
|
||||
}
|
||||
|
||||
// see https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
|
||||
// NOTE: golang time package does not support leap seconds.
|
||||
func validateTime(v any) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// min: hh:mm:ssZ
|
||||
if len(str) < 9 {
|
||||
return LocalizableError("less than 9 characters long")
|
||||
}
|
||||
if str[2] != ':' || str[5] != ':' {
|
||||
return LocalizableError("missing colon in correct place")
|
||||
}
|
||||
|
||||
// parse hh:mm:ss
|
||||
var hms []int
|
||||
for _, tok := range strings.SplitN(str[:8], ":", 3) {
|
||||
i, err := strconv.Atoi(tok)
|
||||
if err != nil {
|
||||
return LocalizableError("invalid hour/min/sec")
|
||||
}
|
||||
if i < 0 {
|
||||
return LocalizableError("non-positive hour/min/sec")
|
||||
}
|
||||
hms = append(hms, i)
|
||||
}
|
||||
if len(hms) != 3 {
|
||||
return LocalizableError("missing hour/min/sec")
|
||||
}
|
||||
h, m, s := hms[0], hms[1], hms[2]
|
||||
if h > 23 || m > 59 || s > 60 {
|
||||
return LocalizableError("hour/min/sec out of range")
|
||||
}
|
||||
str = str[8:]
|
||||
|
||||
// parse sec-frac if present
|
||||
if rem, ok := strings.CutPrefix(str, "."); ok {
|
||||
numDigits := 0
|
||||
for _, ch := range rem {
|
||||
if ch >= '0' && ch <= '9' {
|
||||
numDigits++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if numDigits == 0 {
|
||||
return LocalizableError("no digits in second fraction")
|
||||
}
|
||||
str = rem[numDigits:]
|
||||
}
|
||||
|
||||
if str != "z" && str != "Z" {
|
||||
// parse time-numoffset
|
||||
if len(str) != 6 {
|
||||
return LocalizableError("offset must be 6 characters long")
|
||||
}
|
||||
var sign int
|
||||
switch str[0] {
|
||||
case '+':
|
||||
sign = -1
|
||||
case '-':
|
||||
sign = +1
|
||||
default:
|
||||
return LocalizableError("offset must begin with plus/minus")
|
||||
}
|
||||
str = str[1:]
|
||||
if str[2] != ':' {
|
||||
return LocalizableError("missing colon in offset in correct place")
|
||||
}
|
||||
|
||||
var zhm []int
|
||||
for _, tok := range strings.SplitN(str, ":", 2) {
|
||||
i, err := strconv.Atoi(tok)
|
||||
if err != nil {
|
||||
return LocalizableError("invalid hour/min in offset")
|
||||
}
|
||||
if i < 0 {
|
||||
return LocalizableError("non-positive hour/min in offset")
|
||||
}
|
||||
zhm = append(zhm, i)
|
||||
}
|
||||
zh, zm := zhm[0], zhm[1]
|
||||
if zh > 23 || zm > 59 {
|
||||
return LocalizableError("hour/min in offset out of range")
|
||||
}
|
||||
|
||||
// apply timezone
|
||||
hm := (h*60 + m) + sign*(zh*60+zm)
|
||||
if hm < 0 {
|
||||
hm += 24 * 60
|
||||
}
|
||||
h, m = hm/60, hm%60
|
||||
}
|
||||
|
||||
// check leap second
|
||||
if s >= 60 && (h != 23 || m != 59) {
|
||||
return LocalizableError("invalid leap second")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// see https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
|
||||
func validateDateTime(v any) error {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// min: yyyy-mm-ddThh:mm:ssZ
|
||||
if len(s) < 20 {
|
||||
return LocalizableError("less than 20 characters long")
|
||||
}
|
||||
|
||||
if s[10] != 't' && s[10] != 'T' {
|
||||
return LocalizableError("11th character must be t or T")
|
||||
}
|
||||
if err := validateDate(s[:10]); err != nil {
|
||||
return LocalizableError("invalid date element: %v", err)
|
||||
}
|
||||
if err := validateTime(s[11:]); err != nil {
|
||||
return LocalizableError("invalid time element: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseURL(s string) (*gourl.URL, error) {
|
||||
u, err := gourl.Parse(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// gourl does not validate ipv6 host address
|
||||
hostName := u.Hostname()
|
||||
if strings.Contains(hostName, ":") {
|
||||
if !strings.Contains(u.Host, "[") || !strings.Contains(u.Host, "]") {
|
||||
return nil, LocalizableError("ipv6 address not enclosed in brackets")
|
||||
}
|
||||
if err := validateIPV6(hostName); err != nil {
|
||||
return nil, LocalizableError("invalid ipv6 address: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func validateURI(v any) error {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
u, err := parseURL(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !u.IsAbs() {
|
||||
return LocalizableError("relative url")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateURIReference(v any) error {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(s, `\`) {
|
||||
return LocalizableError(`contains \`)
|
||||
}
|
||||
_, err := parseURL(s)
|
||||
return err
|
||||
}
|
||||
|
||||
func validateURITemplate(v any) error {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
u, err := parseURL(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, tok := range strings.Split(u.RawPath, "/") {
|
||||
tok, err = decode(tok)
|
||||
if err != nil {
|
||||
return LocalizableError("percent decode failed: %v", err)
|
||||
}
|
||||
want := true
|
||||
for _, ch := range tok {
|
||||
var got bool
|
||||
switch ch {
|
||||
case '{':
|
||||
got = true
|
||||
case '}':
|
||||
got = false
|
||||
default:
|
||||
continue
|
||||
}
|
||||
if got != want {
|
||||
return LocalizableError("nested curly braces")
|
||||
}
|
||||
want = !want
|
||||
}
|
||||
if !want {
|
||||
return LocalizableError("no matching closing brace")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePeriod(v any) error {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
slash := strings.IndexByte(s, '/')
|
||||
if slash == -1 {
|
||||
return LocalizableError("missing slash")
|
||||
}
|
||||
|
||||
start, end := s[:slash], s[slash+1:]
|
||||
if strings.HasPrefix(start, "P") {
|
||||
if err := validateDuration(start); err != nil {
|
||||
return LocalizableError("invalid start duration: %v", err)
|
||||
}
|
||||
if err := validateDateTime(end); err != nil {
|
||||
return LocalizableError("invalid end date-time: %v", err)
|
||||
}
|
||||
} else {
|
||||
if err := validateDateTime(start); err != nil {
|
||||
return LocalizableError("invalid start date-time: %v", err)
|
||||
}
|
||||
if strings.HasPrefix(end, "P") {
|
||||
if err := validateDuration(end); err != nil {
|
||||
return LocalizableError("invalid end duration: %v", err)
|
||||
}
|
||||
} else if err := validateDateTime(end); err != nil {
|
||||
return LocalizableError("invalid end date-time: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// see https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions
|
||||
func validateSemver(v any) error {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// build --
|
||||
if i := strings.IndexByte(s, '+'); i != -1 {
|
||||
build := s[i+1:]
|
||||
if build == "" {
|
||||
return LocalizableError("build is empty")
|
||||
}
|
||||
for _, buildID := range strings.Split(build, ".") {
|
||||
if buildID == "" {
|
||||
return LocalizableError("build identifier is empty")
|
||||
}
|
||||
for _, ch := range buildID {
|
||||
switch {
|
||||
case ch >= '0' && ch <= '9':
|
||||
case (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '-':
|
||||
default:
|
||||
return LocalizableError("invalid character %q in build identifier", ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
s = s[:i]
|
||||
}
|
||||
|
||||
// pre-release --
|
||||
if i := strings.IndexByte(s, '-'); i != -1 {
|
||||
preRelease := s[i+1:]
|
||||
for _, preReleaseID := range strings.Split(preRelease, ".") {
|
||||
if preReleaseID == "" {
|
||||
return LocalizableError("pre-release identifier is empty")
|
||||
}
|
||||
allDigits := true
|
||||
for _, ch := range preReleaseID {
|
||||
switch {
|
||||
case ch >= '0' && ch <= '9':
|
||||
case (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '-':
|
||||
allDigits = false
|
||||
default:
|
||||
return LocalizableError("invalid character %q in pre-release identifier", ch)
|
||||
}
|
||||
}
|
||||
if allDigits && len(preReleaseID) > 1 && preReleaseID[0] == '0' {
|
||||
return LocalizableError("pre-release numeric identifier starts with zero")
|
||||
}
|
||||
}
|
||||
s = s[:i]
|
||||
}
|
||||
|
||||
// versionCore --
|
||||
versions := strings.Split(s, ".")
|
||||
if len(versions) != 3 {
|
||||
return LocalizableError("versionCore must have 3 numbers separated by dot")
|
||||
}
|
||||
names := []string{"major", "minor", "patch"}
|
||||
for i, version := range versions {
|
||||
if version == "" {
|
||||
return LocalizableError("%s is empty", names[i])
|
||||
}
|
||||
if len(version) > 1 && version[0] == '0' {
|
||||
return LocalizableError("%s starts with zero", names[i])
|
||||
}
|
||||
for _, ch := range version {
|
||||
if ch < '0' || ch > '9' {
|
||||
return LocalizableError("%s contains non-digit", names[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
8
vendor/github.com/santhosh-tekuri/jsonschema/v6/go.work
generated
vendored
Normal file
8
vendor/github.com/santhosh-tekuri/jsonschema/v6/go.work
generated
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
go 1.21.1
|
||||
|
||||
use (
|
||||
.
|
||||
./cmd/jv
|
||||
)
|
||||
|
||||
replace github.com/santhosh-tekuri/jsonschema/v6 v6.0.0 => ./
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue