diff --git a/.earthlyignore b/.earthlyignore
index 838458f..9c0f752 100644
--- a/.earthlyignore
+++ b/.earthlyignore
@@ -1 +1,3 @@
-/dist/
\ No newline at end of file
+/dist/
+/airgapify
+*.tar*
\ No newline at end of file
diff --git a/.github/workflows/main.yml b/.github/workflows/earthly.yml
similarity index 62%
rename from .github/workflows/main.yml
rename to .github/workflows/earthly.yml
index 9071413..7365b0c 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/earthly.yml
@@ -1,4 +1,4 @@
-name: Build and Test
+name: Build, Test, and Release
on:
push:
@@ -10,11 +10,13 @@ on:
jobs:
build-and-test:
runs-on: ubuntu-latest
+ env:
+ DO_NOT_TRACK: '1'
steps:
- uses: earthly/actions-setup@v1
with:
- version: v0.7.23
+ version: v0.8.14
- name: Check Out Repo
uses: actions/checkout@v3
@@ -22,11 +24,6 @@ jobs:
- name: Lint
run: earthly +lint
- - name: Build
- run: |
- earthly +generate
- earthly +build
-
- name: Test
run: earthly +test
@@ -34,26 +31,35 @@ jobs:
needs: build-and-test
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
+ env:
+ DO_NOT_TRACK: '1'
steps:
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v2
+
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v2
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
- uses: earthly/actions-setup@v1
with:
- version: v0.7.23
+ version: v0.8.14
- name: Check Out Repo
uses: actions/checkout@v3
- name: Build
- run: |
- earthly +generate
- earthly +all
+ run: earthly +all --VERSION=${{ github.ref_name }}
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
dist/*
- checksums.txt
LICENSE
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 4d21ed9..58b3661 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
#
# Binaries for programs and plugins
/dist/
+/airgapify
*.exe
*.exe~
*.dll
diff --git a/Earthfile b/Earthfile
index 0e6225d..ed5d484 100644
--- a/Earthfile
+++ b/Earthfile
@@ -1,18 +1,22 @@
-VERSION 0.7
-FROM golang:1.21-bookworm
-WORKDIR /app
+VERSION 0.8
+FROM golang:1.22-bookworm
+WORKDIR /workspace
all:
+ ARG VERSION=dev
+ BUILD +generate
COPY (+build/airgapify --GOARCH=amd64) ./dist/airgapify-linux-amd64
COPY (+build/airgapify --GOARCH=arm64) ./dist/airgapify-linux-arm64
+ COPY (+build/airgapify --GOARCH=riscv64) ./dist/airgapify-linux-riscv64
COPY (+build/airgapify --GOOS=darwin --GOARCH=amd64) ./dist/airgapify-darwin-amd64
COPY (+build/airgapify --GOOS=darwin --GOARCH=arm64) ./dist/airgapify-darwin-arm64
- RUN cd dist && find . -type f -exec sha256sum {} \; >> ../checksums.txt
- SAVE ARTIFACT ./dist/airgapify-linux-amd64 AS LOCAL dist/airgapify-linux-amd64
- SAVE ARTIFACT ./dist/airgapify-linux-arm64 AS LOCAL dist/airgapify-linux-arm64
- SAVE ARTIFACT ./dist/airgapify-darwin-amd64 AS LOCAL dist/airgapify-darwin-amd64
- SAVE ARTIFACT ./dist/airgapify-darwin-arm64 AS LOCAL dist/airgapify-darwin-arm64
- SAVE ARTIFACT ./checksums.txt AS LOCAL dist/checksums.txt
+ COPY (+build/airgapify --GOOS=windows --GOARCH=amd64) ./dist/airgapify-windows-amd64.exe
+ COPY (+package/*.deb --GOARCH=amd64) ./dist/
+ COPY (+package/*.deb --GOARCH=arm64) ./dist/
+ COPY (+package/*.deb --GOARCH=riscv64) ./dist/
+ RUN cd dist && find . -type f | sort | xargs sha256sum >> ../sha256sums.txt
+ SAVE ARTIFACT ./dist/* AS LOCAL dist/
+ SAVE ARTIFACT ./sha256sums.txt AS LOCAL dist/sha256sums.txt
build:
ARG GOOS=linux
@@ -20,11 +24,12 @@ build:
COPY go.mod go.sum ./
RUN go mod download
COPY . .
- RUN CGO_ENABLED=0 go build --ldflags '-s' -o airgapify cmd/main.go
+ ARG VERSION=dev
+ RUN CGO_ENABLED=0 go build --ldflags "-s -X 'github.com/dpeckett/airgapify/internal/constants.Version=${VERSION}'" -o airgapify main.go
SAVE ARTIFACT ./airgapify AS LOCAL dist/airgapify-${GOOS}-${GOARCH}
generate:
- ARG CONTROLLER_TOOLS_VERSION=v0.12.0
+ ARG CONTROLLER_TOOLS_VERSION=v0.16.2
RUN go install sigs.k8s.io/controller-tools/cmd/controller-gen@${CONTROLLER_TOOLS_VERSION}
COPY . ./
RUN controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./api/..."
@@ -34,16 +39,57 @@ generate:
tidy:
LOCALLY
+ ENV GOTOOLCHAIN=go1.22.1
RUN go mod tidy
RUN go fmt ./...
lint:
- FROM golangci/golangci-lint:v1.55.2
- WORKDIR /app
+ FROM golangci/golangci-lint:v1.59.1
+ WORKDIR /workspace
COPY . ./
RUN golangci-lint run --timeout 5m ./...
test:
- COPY . ./
+ COPY go.mod go.sum ./
+ RUN go mod download
+ COPY . .
RUN go test -coverprofile=coverage.out -v ./...
- SAVE ARTIFACT ./coverage.out AS LOCAL coverage.out
\ No newline at end of file
+ SAVE ARTIFACT ./coverage.out AS LOCAL coverage.out
+
+package:
+ FROM debian:bookworm
+ # Use bookworm-backports for newer golang versions
+ RUN echo "deb http://deb.debian.org/debian bookworm-backports main" > /etc/apt/sources.list.d/backports.list
+ RUN apt update
+ # Tooling
+ RUN apt install -y git curl devscripts dpkg-dev debhelper-compat git-buildpackage libfaketime dh-sequence-golang \
+ golang-any=2:1.22~3~bpo12+1 golang-go=2:1.22~3~bpo12+1 golang-src=2:1.22~3~bpo12+1 \
+ gcc-aarch64-linux-gnu gcc-riscv64-linux-gnu
+ RUN curl -fsL -o /etc/apt/keyrings/apt-pecke-tt-keyring.asc https://apt.pecke.tt/signing_key.asc \
+ && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/apt-pecke-tt-keyring.asc] http://apt.pecke.tt $(. /etc/os-release && echo $VERSION_CODENAME) stable" > /etc/apt/sources.list.d/apt-pecke-tt.list \
+ && apt update
+ # Build Dependencies
+ RUN apt install -y \
+ golang-github-dpeckett-archivefs-dev \
+ golang-github-dpeckett-telemetry-dev \
+ golang-github-dpeckett-uncompr-dev \
+ golang-github-google-go-containerregistry-dev \
+ golang-github-pierrec-lz4-dev=4.1.18-1~bpo12+1 \
+ golang-github-stretchr-testify-dev \
+ golang-github-urfave-cli-v2-dev \
+ golang-k8s-apimachinery-dev
+ RUN mkdir -p /workspace/airgapify
+ WORKDIR /workspace/airgapify
+ COPY . .
+ RUN if [ -n "$(git status --porcelain)" ]; then echo "Please commit your changes."; exit 1; fi
+ RUN if [ -z "$(git describe --tags --exact-match 2>/dev/null)" ]; then echo "Current commit is not tagged."; exit 1; fi
+ COPY debian/scripts/generate-changelog.sh /usr/local/bin/generate-changelog.sh
+ RUN chmod +x /usr/local/bin/generate-changelog.sh
+ ENV DEBEMAIL="damian@pecke.tt"
+ ENV DEBFULLNAME="Damian Peckett"
+ RUN /usr/local/bin/generate-changelog.sh
+ RUN VERSION=$(git describe --tags --abbrev=0 | tr -d 'v') \
+ && tar -czf ../airgapify_${VERSION}.orig.tar.gz --exclude-vcs .
+ ARG GOARCH
+ RUN dpkg-buildpackage -d -us -uc --host-arch=${GOARCH}
+ SAVE ARTIFACT /workspace/*.deb AS LOCAL dist/
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index 261eeb9..0ad25db 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,201 +1,661 @@
- 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 [yyyy] [name of copyright owner]
-
- 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.
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
diff --git a/README.md b/README.md
index 9f33542..f884020 100644
--- a/README.md
+++ b/README.md
@@ -2,22 +2,55 @@
A little tool that will construct an OCI image archive from a set of Kubernetes manifests.
+## Installation
+
+### From APT
+
+Add my apt repository to your system:
+
+*Currently packages are only published for Debian 12 (Bookworm).*
+
+```shell
+curl -fsL https://apt.pecke.tt/signing_key.asc | sudo tee /etc/apt/keyrings/apt-pecke-tt-keyring.asc > /dev/null
+echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/apt-pecke-tt-keyring.asc] http://apt.pecke.tt $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/apt-pecke-tt.list > /dev/null
+```
+
+Then install airgapify:
+
+```shell
+sudo apt update
+sudo apt install airgapify
+```
+
+### GitHub Releases
+
+Download statically linked binaries from the GitHub releases page:
+
+[Latest Release](https://github.com/dpeckett/airgapify/releases/latest)
+
## Usage
To create an OCI image archive from a directory containing Kubernetes manifests:
```shell
-airgapify -f manifests/ -o images.tar.zst
+airgapify -f manifests/ -o images.tar
```
-You can then load the image archive into Docker:
+You can then load the image archive into containerd:
```shell
-docker load -i images.tar.zst
+ctr image import images.tar
```
## Configuration
Airgapify will look in the manifests for a Config YAML resource. An example is provided in [examples/config.yaml](examples/config.yaml).
-The config resource allows you to specify additional images to include in the archive, and allows configuring image reference extraction for custom resources.
\ No newline at end of file
+The config resource allows you to specify additional images to include in the archive, and allows configuring image reference extraction for custom resources.
+
+## Telemetry
+
+By default airgapify gathers anonymous crash and usage statistics. This anonymized
+data is processed on our servers within the EU and is not shared with third
+parties. You can opt out of telemetry by setting the `DO_NOT_TRACK=1`
+environment variable.
\ No newline at end of file
diff --git a/THIRD-PARTY-LICENSES b/THIRD-PARTY-LICENSES
new file mode 100644
index 0000000..f51d0c2
--- /dev/null
+++ b/THIRD-PARTY-LICENSES
@@ -0,0 +1,32 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: airgapify
+Upstream-Contact: Damian Peckett
+Source: https://github.com/damian/airgapify
+
+Files:
+ internal/util/jsonpath/jsonpath.go
+ internal/util/jsonpath/jsonpath_test.go
+ internal/util/jsonpath/node.go
+ internal/util/jsonpath/parser.go
+ internal/util/jsonpath/parser_test.go
+ internal/util/jsonpath/template/exec.go
+ internal/util/jsonpath/template/funcs.go
+Copyright:
+ Copyright 2015 The Kubernetes Authors.
+License: Apache-2.0
+
+License: Apache-2.0
+ 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.
+ .
+ On Debian systems, the full text of the Apache License, Version 2.0 can be
+ found in the file `/usr/share/common-licenses/Apache-2.0'.
\ No newline at end of file
diff --git a/api/v1alpha1/config_types.go b/api/v1alpha1/config_types.go
index 9b1022d..ff5506d 100644
--- a/api/v1alpha1/config_types.go
+++ b/api/v1alpha1/config_types.go
@@ -1,18 +1,19 @@
-/* SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: AGPL-3.0-or-later
+/*
+ * Copyright (C) 2024 Damian Peckett .
*
- * Copyright 2024 Damian Peckett .
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*
- * 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
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
*
- * 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.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
*/
package v1alpha1
@@ -51,7 +52,3 @@ type ConfigList struct {
metav1.ListMeta `json:"metadata,omitempty"`
Items []Config `json:"items"`
}
-
-func init() {
- SchemeBuilder.Register(&Config{}, &ConfigList{})
-}
diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go
index a295d81..12ff64e 100644
--- a/api/v1alpha1/groupversion_info.go
+++ b/api/v1alpha1/groupversion_info.go
@@ -1,18 +1,19 @@
-/* SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: AGPL-3.0-or-later
+/*
+ * Copyright (C) 2024 Damian Peckett .
*
- * Copyright 2024 Damian Peckett .
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*
- * 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
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
*
- * 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.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
*/
// +kubebuilder:object:generate=true
@@ -21,16 +22,9 @@ package v1alpha1
import (
"k8s.io/apimachinery/pkg/runtime/schema"
- "sigs.k8s.io/controller-runtime/pkg/scheme"
)
var (
// GroupVersion is group version used to register these objects
GroupVersion = schema.GroupVersion{Group: "airgapify.pecke.tt", Version: "v1alpha1"}
-
- // SchemeBuilder is used to add go types to the GroupVersionKind scheme
- SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
-
- // AddToScheme adds the types in this group-version to the given scheme.
- AddToScheme = SchemeBuilder.AddToScheme
)
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 79a52ca..401a7dc 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -1,21 +1,21 @@
//go:build !ignore_autogenerated
-// +build !ignore_autogenerated
-/* SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: AGPL-3.0-or-later
+/*
+ * Copyright (C) 2024 Damian Peckett .
*
- * Copyright 2024 Damian Peckett .
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*
- * 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
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
*
- * 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.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
*/
// Code generated by controller-gen. DO NOT EDIT.
diff --git a/cmd/main.go b/cmd/main.go
deleted file mode 100644
index e670a8e..0000000
--- a/cmd/main.go
+++ /dev/null
@@ -1,162 +0,0 @@
-/* SPDX-License-Identifier: Apache-2.0
- *
- * Copyright 2024 Damian Peckett .
- *
- * 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.
- */
-
-package main
-
-import (
- "fmt"
- "log/slog"
- "os"
-
- airgapifyv1alpha1 "github.com/dpeckett/airgapify/api/v1alpha1"
- "github.com/dpeckett/airgapify/internal/archive"
- "github.com/dpeckett/airgapify/internal/extractor"
- "github.com/dpeckett/airgapify/internal/loader"
- v1 "github.com/google/go-containerregistry/pkg/v1"
- "github.com/urfave/cli/v2"
- "k8s.io/apimachinery/pkg/runtime"
- "k8s.io/client-go/kubernetes/scheme"
-)
-
-func init() {
- _ = airgapifyv1alpha1.AddToScheme(scheme.Scheme)
-}
-
-func main() {
- logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
-
- app := &cli.App{
- Name: "airgapify",
- Usage: "A little tool that will construct an OCI image archive from a set of Kubernetes manifests.",
- Flags: []cli.Flag{
- &cli.GenericFlag{
- Name: "log-level",
- Aliases: []string{"l"},
- Usage: "Set the log level",
- Value: fromLogLevel(slog.LevelInfo),
- },
- &cli.StringSliceFlag{
- Name: "file",
- Aliases: []string{"f"},
- Usage: "Path to one or more Kubernetes manifests.",
- Required: true,
- },
- &cli.StringFlag{
- Name: "output",
- Aliases: []string{"o"},
- Usage: "Where to write the oci image archive (will be a tar.zst archive).",
- Value: "images.tar.zst",
- },
- &cli.StringFlag{
- Name: "platform",
- Aliases: []string{"p"},
- Usage: "The target platform for the image archive.",
- },
- &cli.BoolFlag{
- Name: "no-progress",
- Usage: "Disable progress output.",
- },
- },
- Before: func(c *cli.Context) error {
- logger = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
- Level: (*slog.Level)(c.Generic("log-level").(*logLevelFlag)),
- }))
-
- return nil
- },
- Action: func(c *cli.Context) error {
- objects, err := loader.LoadObjectsFromFiles(c.StringSlice("file"))
- if err != nil {
- return fmt.Errorf("failed to load objects: %w", err)
- }
-
- logger.Info("Loaded objects", "count", len(objects))
-
- rules := extractor.DefaultRules
-
- for _, obj := range objects {
- if obj.GetAPIVersion() == "airgapify.pecke.tt/v1alpha1" && obj.GetKind() == "Config" {
- logger.Info("Found airgapify config")
-
- var config airgapifyv1alpha1.Config
- err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &config)
- if err != nil {
- return fmt.Errorf("failed to convert config: %w", err)
- }
-
- for _, rule := range config.Spec.Rules {
- rules = append(rules, extractor.ImageReferenceExtractionRule{
- TypeMeta: rule.TypeMeta,
- Paths: rule.Paths,
- })
- }
- }
- }
-
- e := extractor.NewImageReferenceExtractor(rules)
- images, err := e.ExtractImageReferences(objects)
- if err != nil {
- return fmt.Errorf("failed to extract image references: %w", err)
- }
-
- if images.Len() > 0 {
- logger.Info("Found image references", "count", images.Len())
- }
-
- options := &archive.Options{}
-
- if c.IsSet("no-progress") && c.Bool("no-progress") {
- options.DisableProgress = true
- }
-
- if c.IsSet("platform") {
- options.Platform, err = v1.ParsePlatform(c.String("platform"))
- if err != nil {
- return fmt.Errorf("failed to parse platform: %w", err)
- }
- }
-
- outputPath := c.String("output")
-
- if err := archive.Create(c.Context, logger, outputPath, images, options); err != nil {
- return fmt.Errorf("failed to create image archive: %w", err)
- }
-
- return nil
- },
- }
-
- if err := app.Run(os.Args); err != nil {
- logger.Error("Failed to run application", "error", err)
- os.Exit(1)
- }
-}
-
-type logLevelFlag slog.Level
-
-func fromLogLevel(l slog.Level) *logLevelFlag {
- f := logLevelFlag(l)
- return &f
-}
-
-func (f *logLevelFlag) Set(value string) error {
- return (*slog.Level)(f).UnmarshalText([]byte(value))
-}
-
-func (f *logLevelFlag) String() string {
- return (*slog.Level)(f).String()
-}
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..1b932c7
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,30 @@
+Source: airgapify
+Section: golang
+Priority: optional
+Maintainer: Damian Peckett
+Uploaders: Damian Peckett
+Rules-Requires-Root: no
+Build-Depends: debhelper-compat (= 13),
+ dh-sequence-golang,
+ golang-any,
+ golang-github-dpeckett-archivefs-dev,
+ golang-github-dpeckett-telemetry-dev,
+ golang-github-dpeckett-uncompr-dev,
+ golang-github-google-go-containerregistry-dev,
+ golang-github-stretchr-testify-dev,
+ golang-github-urfave-cli-v2-dev,
+ golang-k8s-apimachinery-dev
+Testsuite: autopkgtest-pkg-go
+Standards-Version: 4.6.2
+Vcs-Browser: https://github.com/dpeckett/airgapify
+Vcs-Git: https://github.com/dpeckett/airgapify.git
+Homepage: https://github.com/dpeckett/airgapify
+XS-Go-Import-Path: github.com/dpeckett/airgapify
+
+Package: airgapify
+Section: utils
+Architecture: any
+Depends: ${misc:Depends},
+ ${shlibs:Depends}
+Built-Using: ${misc:Built-Using}
+Description: A little tool that will construct an OCI image archive from a set of Kubernetes manifests. (program)
\ No newline at end of file
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..f2b6f3d
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,22 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Source: https://github.com/dpeckett/airgapify
+Upstream-Name: airgapify
+Upstream-Contact: Damian Peckett
+
+Files: *
+Copyright: 2024 Damian Peckett
+License: AGPL-3.0-or-later
+
+License: AGPL-3.0-or-later
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ .
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+ .
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
\ No newline at end of file
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..1aa07e8
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,26 @@
+#!/usr/bin/make -f
+
+export DEB_HOST_MULTIARCH ?= $(shell dpkg-architecture -qDEB_HOST_MULTIARCH)
+
+export BUILD_ARCH ?= $(shell dpkg-architecture -qDEB_BUILD_ARCH)
+export HOST_ARCH ?= $(shell dpkg-architecture -qDEB_HOST_ARCH)
+export VERSION ?= $(shell git describe --tags --abbrev=0)
+
+%:
+ dh $@ --builddirectory=_build --buildsystem=golang
+
+override_dh_auto_build:
+ dh_auto_build -- -ldflags "-X 'github.com/dpeckett/airgapify/internal/constants.Version=$(VERSION)'"
+
+override_dh_auto_install:
+ dh_auto_install -- --no-source
+
+override_dh_auto_test:
+ifneq ($(BUILD_ARCH), $(HOST_ARCH))
+ @echo "Skipping tests for cross-compilation"
+else
+ dh_auto_test
+endif
+
+override_dh_shlibdeps:
+ dh_shlibdeps -l/usr/$(DEB_HOST_MULTIARCH)/lib
\ No newline at end of file
diff --git a/debian/scripts/generate-changelog.sh b/debian/scripts/generate-changelog.sh
new file mode 100755
index 0000000..e1dad3e
--- /dev/null
+++ b/debian/scripts/generate-changelog.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+set -e
+
+package=$(grep '^Source:' debian/control | awk '{print $2}')
+
+tags=$(git tag --sort=creatordate)
+
+prev_tag=""
+for tag in $tags; do
+ git checkout $tag > /dev/null 2>&1
+
+ new_version="$(echo $tag | tr -d 'v')-1"
+
+ export FAKETIME=$(git show -s --format=%aI $tag | sed 's/T/ /; s/.\{6\}$//')
+
+ if [ -n "$prev_tag" ]; then
+ LD_PRELOAD="/usr/lib/$(uname -m)-linux-gnu/faketime/libfaketime.so.1" \
+ gbp dch --ignore-branch --release --distribution=stable --new-version="$new_version" --since=$prev_tag --spawn-editor=never
+ else
+ mkdir -p debian
+ cat < debian/changelog
+$package (0.0.0-1) UNRELEASED; urgency=medium
+
+ -- $DEBFULLNAME <$DEBEMAIL> Thu, 01 Jan 1970 00:00:00 +0000
+EOF
+
+ LD_PRELOAD="/usr/lib/$(uname -m)-linux-gnu/faketime/libfaketime.so.1" \
+ gbp dch --ignore-branch --release --distribution=stable --new-version=$new_version --spawn-editor=never
+ fi
+
+ prev_tag=$tag
+done
\ No newline at end of file
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 0000000..163aaf8
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/go.mod b/go.mod
index 80344eb..443c1c8 100644
--- a/go.mod
+++ b/go.mod
@@ -1,19 +1,18 @@
module github.com/dpeckett/airgapify
-go 1.21.0
+go 1.22.0
require (
- github.com/google/go-containerregistry v0.16.1
- github.com/mholt/archiver/v3 v3.5.1
+ github.com/dpeckett/archivefs v0.11.0
+ github.com/dpeckett/telemetry v0.1.2
+ github.com/dpeckett/uncompr v0.5.0
+ github.com/google/go-containerregistry v0.14.0
github.com/stretchr/testify v1.8.4
- github.com/urfave/cli/v2 v2.25.7
- k8s.io/apimachinery v0.28.1
- k8s.io/client-go v0.28.1
- sigs.k8s.io/controller-runtime v0.16.0
+ github.com/urfave/cli/v2 v2.3.0
+ k8s.io/apimachinery v0.20.0
)
require (
- github.com/andybalholm/brotli v1.1.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
@@ -21,18 +20,15 @@ require (
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker v24.0.0+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.0 // indirect
- github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
- github.com/go-logr/logr v1.2.4 // indirect
+ github.com/go-logr/logr v1.3.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
- github.com/golang/snappy v0.0.4 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.4 // indirect
- github.com/klauspost/pgzip v1.2.6 // indirect
+ github.com/kr/text v0.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
- github.com/nwaples/rardecode v1.1.3 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc3 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
@@ -42,19 +38,14 @@ require (
github.com/sirupsen/logrus v1.9.1 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/vbatts/tar-split v0.11.3 // indirect
- github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
- github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
- golang.org/x/net v0.13.0 // indirect
- golang.org/x/sync v0.2.0 // indirect
- golang.org/x/sys v0.11.0 // indirect
- golang.org/x/text v0.11.0 // indirect
+ golang.org/x/net v0.17.0 // indirect
+ golang.org/x/sync v0.7.0 // indirect
+ golang.org/x/sys v0.13.0 // indirect
+ golang.org/x/text v0.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
- k8s.io/api v0.28.1 // indirect
- k8s.io/klog/v2 v2.100.1 // indirect
- k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
- sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
- sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
+ k8s.io/klog/v2 v2.110.1 // indirect
+ sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
diff --git a/go.sum b/go.sum
index a45878a..45c79ae 100644
--- a/go.sum
+++ b/go.sum
@@ -1,11 +1,18 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
-github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
-github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
-github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
+github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
+github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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=
@@ -17,148 +24,243 @@ github.com/docker/docker v24.0.0+incompatible h1:z4bf8HvONXX9Tde5lGBMQ7yCJgNahmJ
github.com/docker/docker v24.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
-github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
-github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
-github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
-github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
-github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
-github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
+github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
+github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
+github.com/dpeckett/archivefs v0.11.0 h1:izE9zDbHJsfhB8XIOYaOgcZeNN9Mq93VTAaxZ5HOc3w=
+github.com/dpeckett/archivefs v0.11.0/go.mod h1:Jcv0qViFG7BHxAsXPzfIIoeJURxmhmxUcAlv1xRvxn0=
+github.com/dpeckett/telemetry v0.1.2 h1:tYMsQ9FA5ibliZDL9DmGMYgvdhXUzDb1r82GuS2pEq8=
+github.com/dpeckett/telemetry v0.1.2/go.mod h1:GmesnU1JHOLPmferdqqpeWSYztf6/oCCwj9aOwcXWT4=
+github.com/dpeckett/uncompr v0.5.0 h1:nibMydzi7Pn0kbA1p38lI6H8cs4CGyn3LOFRXhPWKBU=
+github.com/dpeckett/uncompr v0.5.0/go.mod h1:Z5Kv7L7JDX8dyTWmd5tIG/TuBPkPgkD4dko7Ya2n3UI=
+github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
+github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
+github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
+github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
+github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
+github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
+github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
+github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
+github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
-github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/google/go-containerregistry v0.16.1 h1:rUEt426sR6nyrL3gt+18ibRcvYpKYdpsa5ZW7MA08dQ=
-github.com/google/go-containerregistry v0.16.1/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
+github.com/google/go-containerregistry v0.14.0 h1:z58vMqHxuwvAsVwvKEkmVBz2TlgBgH5k6koEXBtlYkw=
+github.com/google/go-containerregistry v0.14.0/go.mod h1:aiJ2fp/SXvkWgmYHioXnbMdlgB8eXiiYOY55gfN91Wk=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
-github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
-github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
-github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
-github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
-github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
-github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
-github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
-github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
-github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
-github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
-github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
-github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU=
-github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM=
-github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
-github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
+github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
+github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
+github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8=
github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
-github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
-github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.1 h1:Ou41VVR3nMWWmTiEUnj0OlsgOSCUFgsPAOl6jRIcVtQ=
github.com/sirupsen/logrus v1.9.1/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
-github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
-github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
-github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
+github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
+github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck=
github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY=
-github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
-github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
-github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
-github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY=
-golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
-golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
-golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
-golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM=
-golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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=
@@ -167,21 +269,20 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
-k8s.io/api v0.28.1 h1:i+0O8k2NPBCPYaMB+uCkseEbawEt/eFaiRqUx8aB108=
-k8s.io/api v0.28.1/go.mod h1:uBYwID+66wiL28Kn2tBjBYQdEU0Xk0z5qF8bIBqk/Dg=
-k8s.io/apimachinery v0.28.1 h1:EJD40og3GizBSV3mkIoXQBsws32okPOy+MkRyzh6nPY=
-k8s.io/apimachinery v0.28.1/go.mod h1:X0xh/chESs2hP9koe+SdIAcXWcQ+RM5hy0ZynB+yEvw=
-k8s.io/client-go v0.28.1 h1:pRhMzB8HyLfVwpngWKE8hDcXRqifh1ga2Z/PU9SXVK8=
-k8s.io/client-go v0.28.1/go.mod h1:pEZA3FqOsVkCc07pFVzK076R+P/eXqsgx5zuuRWukNE=
-k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
-k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
-k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
-k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
-sigs.k8s.io/controller-runtime v0.16.0 h1:5koYaaRVBHDr0LZAJjO5dWzUjMsh6cwa7q1Mmusrdvk=
-sigs.k8s.io/controller-runtime v0.16.0/go.mod h1:77DnuwA8+J7AO0njzv3wbNlMOnGuLrwFr8JPNwx3J7g=
-sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
-sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
-sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
-sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+k8s.io/apimachinery v0.20.0 h1:jjzbTJRXk0unNS71L7h3lxGDH/2HPxMPaQY+MjECKL8=
+k8s.io/apimachinery v0.20.0/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
+k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
+k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
+k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
+k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
+k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
+k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
+sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
+sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
+sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt
index a369e24..568cea1 100644
--- a/hack/boilerplate.go.txt
+++ b/hack/boilerplate.go.txt
@@ -1,16 +1,17 @@
-/* SPDX-License-Identifier: Apache-2.0
- *
- * Copyright 2024 Damian Peckett .
- *
- * 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
+// SPDX-License-Identifier: AGPL-3.0-or-later
+/*
+ * Copyright (C) 2024 Damian Peckett .
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*
- * 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.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
*/
\ No newline at end of file
diff --git a/internal/archive/archive.go b/internal/archive/archive.go
index 7626876..4943a85 100644
--- a/internal/archive/archive.go
+++ b/internal/archive/archive.go
@@ -1,18 +1,19 @@
-/* SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: AGPL-3.0-or-later
+/*
+ * Copyright (C) 2024 Damian Peckett .
*
- * Copyright 2024 Damian Peckett .
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*
- * 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
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
*
- * 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.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
*/
package archive
@@ -24,23 +25,19 @@ import (
"os"
"path/filepath"
+ "github.com/dpeckett/archivefs/tarfs"
+ "github.com/dpeckett/uncompr"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/google/go-containerregistry/pkg/v1/remote"
- "github.com/mholt/archiver/v3"
"k8s.io/apimachinery/pkg/util/sets"
)
-type Options struct {
- DisableProgress bool
- Platform *v1.Platform
-}
-
// Create creates an OCI image archive from a set of image references.
-func Create(ctx context.Context, logger *slog.Logger, outputPath string, images sets.Set[string], opts *Options) error {
+func Create(ctx context.Context, outputPath string, images sets.String, platform *v1.Platform) error {
ociLayoutDir, err := os.MkdirTemp("", "airgapify-archive-*")
if err != nil {
return fmt.Errorf("failed to create temporary archive directory: %w", err)
@@ -61,8 +58,8 @@ func Create(ctx context.Context, logger *slog.Logger, outputPath string, images
remote.WithAuthFromKeychain(authn.DefaultKeychain),
}
- if opts.Platform != nil {
- options = append(options, remote.WithPlatform(*opts.Platform))
+ if platform != nil {
+ options = append(options, remote.WithPlatform(*platform))
}
ref, err := name.ParseReference(image)
@@ -70,7 +67,7 @@ func Create(ctx context.Context, logger *slog.Logger, outputPath string, images
return fmt.Errorf("failed to parse image reference %q: %w", image, err)
}
- logger.Info("Fetching image", "image", image)
+ slog.Info("Fetching image", "image", image)
img, err := remote.Image(ref, options...)
if err != nil {
@@ -83,8 +80,8 @@ func Create(ctx context.Context, logger *slog.Logger, outputPath string, images
}),
}
- if opts.Platform != nil {
- layoutOpts = append(layoutOpts, layout.WithPlatform(*opts.Platform))
+ if platform != nil {
+ layoutOpts = append(layoutOpts, layout.WithPlatform(*platform))
}
if err = p.AppendImage(img, layoutOpts...); err != nil {
@@ -92,19 +89,22 @@ func Create(ctx context.Context, logger *slog.Logger, outputPath string, images
}
}
- format := archiver.TarZstd{
- Tar: &archiver.Tar{
- OverwriteExisting: true,
- },
+ slog.Info("Writing image archive", "path", outputPath)
+
+ outputFile, err := os.Create(outputPath)
+ if err != nil {
+ return fmt.Errorf("failed to create output file: %w", err)
}
+ defer outputFile.Close()
- logger.Info("Writing image archive", "path", outputPath)
+ // Optionally compress the output file based on the file extension.
+ w, err := uncompr.NewWriter(outputFile, filepath.Base(outputPath))
+ if err != nil {
+ return fmt.Errorf("failed to create compressor: %w", err)
+ }
+ defer w.Close()
- if err := format.Archive([]string{
- filepath.Join(ociLayoutDir, "blobs"),
- filepath.Join(ociLayoutDir, "index.json"),
- filepath.Join(ociLayoutDir, "oci-layout"),
- }, outputPath); err != nil {
+ if err := tarfs.Create(w, os.DirFS(ociLayoutDir)); err != nil {
return fmt.Errorf("failed to create oci image archive: %w", err)
}
diff --git a/internal/constants/constants.go b/internal/constants/constants.go
new file mode 100644
index 0000000..f92b385
--- /dev/null
+++ b/internal/constants/constants.go
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+/*
+ * Copyright (C) 2024 Damian Peckett .
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package constants
+
+var (
+ // TelemetryURL is the URL to send anonymized telemetry data to.
+ TelemetryURL = "https://telemetry.pecke.tt"
+ Version = "dev"
+)
diff --git a/internal/extractor/extractor.go b/internal/extractor/extractor.go
index 4d4b2f0..0a7e480 100644
--- a/internal/extractor/extractor.go
+++ b/internal/extractor/extractor.go
@@ -1,18 +1,19 @@
-/* SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: AGPL-3.0-or-later
+/*
+ * Copyright (C) 2024 Damian Peckett .
*
- * Copyright 2024 Damian Peckett .
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*
- * 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
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
*
- * 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.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
*/
package extractor
@@ -20,10 +21,10 @@ package extractor
import (
"fmt"
+ "github.com/dpeckett/airgapify/internal/util/jsonpath"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/sets"
- "k8s.io/client-go/util/jsonpath"
)
type ImageReferenceExtractionRule struct {
@@ -94,8 +95,8 @@ func NewImageReferenceExtractor(rules []ImageReferenceExtractionRule) *ImageRefe
}
}
-func (e *ImageReferenceExtractor) ExtractImageReferences(objects []unstructured.Unstructured) (sets.Set[string], error) {
- images := sets.New[string]()
+func (e *ImageReferenceExtractor) ExtractImageReferences(objects []unstructured.Unstructured) (sets.String, error) {
+ images := sets.NewString()
for _, object := range objects {
imagesForObject, err := e.extractImagesFromObject(object)
@@ -111,8 +112,8 @@ func (e *ImageReferenceExtractor) ExtractImageReferences(objects []unstructured.
return images, nil
}
-func (e *ImageReferenceExtractor) extractImagesFromObject(object unstructured.Unstructured) (sets.Set[string], error) {
- images := sets.New[string]()
+func (e *ImageReferenceExtractor) extractImagesFromObject(object unstructured.Unstructured) (sets.String, error) {
+ images := sets.NewString()
for _, rule := range e.rules {
if object.GroupVersionKind() == rule.GroupVersionKind() {
@@ -132,7 +133,7 @@ func (e *ImageReferenceExtractor) extractImagesFromObject(object unstructured.Un
return images, nil
}
-func extractValueUsingJSONPath(object unstructured.Unstructured, jsonPath string) (sets.Set[string], error) {
+func extractValueUsingJSONPath(object unstructured.Unstructured, jsonPath string) (sets.String, error) {
j := jsonpath.New("extractor").AllowMissingKeys(true)
if err := j.Parse("{ " + jsonPath + " }"); err != nil {
return nil, err
@@ -143,7 +144,7 @@ func extractValueUsingJSONPath(object unstructured.Unstructured, jsonPath string
return nil, err
}
- images := sets.New[string]()
+ images := sets.NewString()
for _, r := range results {
for _, v := range r {
images.Insert(fmt.Sprintf("%v", v.Interface()))
diff --git a/internal/extractor/extractor_test.go b/internal/extractor/extractor_test.go
index e2cdf6b..b1b453f 100644
--- a/internal/extractor/extractor_test.go
+++ b/internal/extractor/extractor_test.go
@@ -1,18 +1,19 @@
-/* SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: AGPL-3.0-or-later
+/*
+ * Copyright (C) 2024 Damian Peckett .
*
- * Copyright 2024 Damian Peckett .
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*
- * 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
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
*
- * 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.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
*/
package extractor_test
@@ -53,6 +54,6 @@ func TestImageReferenceExtractor(t *testing.T) {
result, err := e.ExtractImageReferences(objects)
require.NoError(t, err)
- expected := sets.New[string]("image1:v1", "image2:v2")
+ expected := sets.NewString("image1:v1", "image2:v2")
assert.True(t, expected.Equal(result))
}
diff --git a/internal/loader/loader.go b/internal/loader/loader.go
index 7636e83..4f0afbb 100644
--- a/internal/loader/loader.go
+++ b/internal/loader/loader.go
@@ -1,18 +1,19 @@
-/* SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: AGPL-3.0-or-later
+/*
+ * Copyright (C) 2024 Damian Peckett .
*
- * Copyright 2024 Damian Peckett .
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*
- * 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
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
*
- * 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.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
*/
package loader
diff --git a/internal/loader/loader_test.go b/internal/loader/loader_test.go
index 3c3cbd5..416cf66 100644
--- a/internal/loader/loader_test.go
+++ b/internal/loader/loader_test.go
@@ -1,18 +1,19 @@
-/* SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: AGPL-3.0-or-later
+/*
+ * Copyright (C) 2024 Damian Peckett .
*
- * Copyright 2024 Damian Peckett .
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*
- * 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
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
*
- * 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.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
*/
package loader_test
diff --git a/internal/util/cli.go b/internal/util/cli.go
new file mode 100644
index 0000000..1afb423
--- /dev/null
+++ b/internal/util/cli.go
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+/*
+ * Copyright (C) 2024 Damian Peckett .
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package util
+
+import (
+ "github.com/urfave/cli/v2"
+)
+
+// BeforeAll runs multiple BeforeFuncs in order
+func BeforeAll(fns ...cli.BeforeFunc) cli.BeforeFunc {
+ return func(c *cli.Context) error {
+ for _, fn := range fns {
+ if err := fn(c); err != nil {
+ return err
+ }
+ }
+
+ return nil
+ }
+}
diff --git a/internal/util/jsonpath/jsonpath.go b/internal/util/jsonpath/jsonpath.go
new file mode 100644
index 0000000..f1d56e6
--- /dev/null
+++ b/internal/util/jsonpath/jsonpath.go
@@ -0,0 +1,582 @@
+/*
+Copyright 2015 The Kubernetes Authors.
+
+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.
+*/
+
+package jsonpath
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "reflect"
+ "strings"
+
+ "github.com/dpeckett/airgapify/internal/util/jsonpath/template"
+)
+
+type JSONPath struct {
+ name string
+ parser *Parser
+ beginRange int
+ inRange int
+ endRange int
+
+ lastEndNode *Node
+
+ allowMissingKeys bool
+ outputJSON bool
+}
+
+// New creates a new JSONPath with the given name.
+func New(name string) *JSONPath {
+ return &JSONPath{
+ name: name,
+ beginRange: 0,
+ inRange: 0,
+ endRange: 0,
+ }
+}
+
+// AllowMissingKeys allows a caller to specify whether they want an error if a field or map key
+// cannot be located, or simply an empty result. The receiver is returned for chaining.
+func (j *JSONPath) AllowMissingKeys(allow bool) *JSONPath {
+ j.allowMissingKeys = allow
+ return j
+}
+
+// Parse parses the given template and returns an error.
+func (j *JSONPath) Parse(text string) error {
+ var err error
+ j.parser, err = Parse(j.name, text)
+ return err
+}
+
+// Execute bounds data into template and writes the result.
+func (j *JSONPath) Execute(wr io.Writer, data interface{}) error {
+ fullResults, err := j.FindResults(data)
+ if err != nil {
+ return err
+ }
+ for ix := range fullResults {
+ if err := j.PrintResults(wr, fullResults[ix]); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (j *JSONPath) FindResults(data interface{}) ([][]reflect.Value, error) {
+ if j.parser == nil {
+ return nil, fmt.Errorf("%s is an incomplete jsonpath template", j.name)
+ }
+
+ cur := []reflect.Value{reflect.ValueOf(data)}
+ nodes := j.parser.Root.Nodes
+ fullResult := [][]reflect.Value{}
+ for i := 0; i < len(nodes); i++ {
+ node := nodes[i]
+ results, err := j.walk(cur, node)
+ if err != nil {
+ return nil, err
+ }
+
+ // encounter an end node, break the current block
+ if j.endRange > 0 && j.endRange <= j.inRange {
+ j.endRange--
+ j.lastEndNode = &nodes[i]
+ break
+ }
+ // encounter a range node, start a range loop
+ if j.beginRange > 0 {
+ j.beginRange--
+ j.inRange++
+ if len(results) > 0 {
+ for _, value := range results {
+ j.parser.Root.Nodes = nodes[i+1:]
+ nextResults, err := j.FindResults(value.Interface())
+ if err != nil {
+ return nil, err
+ }
+ fullResult = append(fullResult, nextResults...)
+ }
+ } else {
+ // If the range has no results, we still need to process the nodes within the range
+ // so the position will advance to the end node
+ j.parser.Root.Nodes = nodes[i+1:]
+ _, err := j.FindResults(nil)
+ if err != nil {
+ return nil, err
+ }
+ }
+ j.inRange--
+
+ // Fast forward to resume processing after the most recent end node that was encountered
+ for k := i + 1; k < len(nodes); k++ {
+ if &nodes[k] == j.lastEndNode {
+ i = k
+ break
+ }
+ }
+ continue
+ }
+ fullResult = append(fullResult, results)
+ }
+ return fullResult, nil
+}
+
+// EnableJSONOutput changes the PrintResults behavior to return a JSON array of results
+func (j *JSONPath) EnableJSONOutput(v bool) {
+ j.outputJSON = v
+}
+
+// PrintResults writes the results into writer
+func (j *JSONPath) PrintResults(wr io.Writer, results []reflect.Value) error {
+ if j.outputJSON {
+ // convert the []reflect.Value to something that json
+ // will be able to marshal
+ r := make([]interface{}, 0, len(results))
+ for i := range results {
+ r = append(r, results[i].Interface())
+ }
+ results = []reflect.Value{reflect.ValueOf(r)}
+ }
+ for i, r := range results {
+ var text []byte
+ var err error
+ outputJSON := true
+ kind := r.Kind()
+ if kind == reflect.Interface {
+ kind = r.Elem().Kind()
+ }
+ switch kind {
+ case reflect.Map:
+ case reflect.Array:
+ case reflect.Slice:
+ case reflect.Struct:
+ default:
+ outputJSON = false
+ }
+ switch {
+ case outputJSON || j.outputJSON:
+ if j.outputJSON {
+ text, err = json.MarshalIndent(r.Interface(), "", " ")
+ text = append(text, '\n')
+ } else {
+ text, err = json.Marshal(r.Interface())
+ }
+ default:
+ text, err = j.evalToText(r)
+ }
+ if err != nil {
+ return err
+ }
+ if i != len(results)-1 {
+ text = append(text, ' ')
+ }
+ if _, err = wr.Write(text); err != nil {
+ return err
+ }
+ }
+
+ return nil
+
+}
+
+// walk visits tree rooted at the given node in DFS order
+func (j *JSONPath) walk(value []reflect.Value, node Node) ([]reflect.Value, error) {
+ switch node := node.(type) {
+ case *ListNode:
+ return j.evalList(value, node)
+ case *TextNode:
+ return []reflect.Value{reflect.ValueOf(node.Text)}, nil
+ case *FieldNode:
+ return j.evalField(value, node)
+ case *ArrayNode:
+ return j.evalArray(value, node)
+ case *FilterNode:
+ return j.evalFilter(value, node)
+ case *IntNode:
+ return j.evalInt(value, node)
+ case *BoolNode:
+ return j.evalBool(value, node)
+ case *FloatNode:
+ return j.evalFloat(value, node)
+ case *WildcardNode:
+ return j.evalWildcard(value, node)
+ case *RecursiveNode:
+ return j.evalRecursive(value, node)
+ case *UnionNode:
+ return j.evalUnion(value, node)
+ case *IdentifierNode:
+ return j.evalIdentifier(value, node)
+ default:
+ return value, fmt.Errorf("unexpected Node %v", node)
+ }
+}
+
+// evalInt evaluates IntNode
+func (j *JSONPath) evalInt(input []reflect.Value, node *IntNode) ([]reflect.Value, error) {
+ result := make([]reflect.Value, len(input))
+ for i := range input {
+ result[i] = reflect.ValueOf(node.Value)
+ }
+ return result, nil
+}
+
+// evalFloat evaluates FloatNode
+func (j *JSONPath) evalFloat(input []reflect.Value, node *FloatNode) ([]reflect.Value, error) {
+ result := make([]reflect.Value, len(input))
+ for i := range input {
+ result[i] = reflect.ValueOf(node.Value)
+ }
+ return result, nil
+}
+
+// evalBool evaluates BoolNode
+func (j *JSONPath) evalBool(input []reflect.Value, node *BoolNode) ([]reflect.Value, error) {
+ result := make([]reflect.Value, len(input))
+ for i := range input {
+ result[i] = reflect.ValueOf(node.Value)
+ }
+ return result, nil
+}
+
+// evalList evaluates ListNode
+func (j *JSONPath) evalList(value []reflect.Value, node *ListNode) ([]reflect.Value, error) {
+ var err error
+ curValue := value
+ for _, node := range node.Nodes {
+ curValue, err = j.walk(curValue, node)
+ if err != nil {
+ return curValue, err
+ }
+ }
+ return curValue, nil
+}
+
+// evalIdentifier evaluates IdentifierNode
+func (j *JSONPath) evalIdentifier(input []reflect.Value, node *IdentifierNode) ([]reflect.Value, error) {
+ results := []reflect.Value{}
+ switch node.Name {
+ case "range":
+ j.beginRange++
+ results = input
+ case "end":
+ if j.inRange > 0 {
+ j.endRange++
+ } else {
+ return results, fmt.Errorf("not in range, nothing to end")
+ }
+ default:
+ return input, fmt.Errorf("unrecognized identifier %v", node.Name)
+ }
+ return results, nil
+}
+
+// evalArray evaluates ArrayNode
+func (j *JSONPath) evalArray(input []reflect.Value, node *ArrayNode) ([]reflect.Value, error) {
+ result := []reflect.Value{}
+ for _, value := range input {
+
+ value, isNil := template.Indirect(value)
+ if isNil {
+ continue
+ }
+ if value.Kind() != reflect.Array && value.Kind() != reflect.Slice {
+ return input, fmt.Errorf("%v is not array or slice", value.Type())
+ }
+ params := node.Params
+ if !params[0].Known {
+ params[0].Value = 0
+ }
+ if params[0].Value < 0 {
+ params[0].Value += value.Len()
+ }
+ if !params[1].Known {
+ params[1].Value = value.Len()
+ }
+
+ if params[1].Value < 0 || (params[1].Value == 0 && params[1].Derived) {
+ params[1].Value += value.Len()
+ }
+ sliceLength := value.Len()
+ if params[1].Value != params[0].Value { // if you're requesting zero elements, allow it through.
+ if params[0].Value >= sliceLength || params[0].Value < 0 {
+ return input, fmt.Errorf("array index out of bounds: index %d, length %d", params[0].Value, sliceLength)
+ }
+ if params[1].Value > sliceLength || params[1].Value < 0 {
+ return input, fmt.Errorf("array index out of bounds: index %d, length %d", params[1].Value-1, sliceLength)
+ }
+ if params[0].Value > params[1].Value {
+ return input, fmt.Errorf("starting index %d is greater than ending index %d", params[0].Value, params[1].Value)
+ }
+ } else {
+ return result, nil
+ }
+
+ value = value.Slice(params[0].Value, params[1].Value)
+
+ step := 1
+ if params[2].Known {
+ if params[2].Value <= 0 {
+ return input, fmt.Errorf("step must be > 0")
+ }
+ step = params[2].Value
+ }
+ for i := 0; i < value.Len(); i += step {
+ result = append(result, value.Index(i))
+ }
+ }
+ return result, nil
+}
+
+// evalUnion evaluates UnionNode
+func (j *JSONPath) evalUnion(input []reflect.Value, node *UnionNode) ([]reflect.Value, error) {
+ result := []reflect.Value{}
+ for _, listNode := range node.Nodes {
+ temp, err := j.evalList(input, listNode)
+ if err != nil {
+ return input, err
+ }
+ result = append(result, temp...)
+ }
+ return result, nil
+}
+
+func (j *JSONPath) findFieldInValue(value *reflect.Value, node *FieldNode) (reflect.Value, error) {
+ t := value.Type()
+ var inlineValue *reflect.Value
+ for ix := 0; ix < t.NumField(); ix++ {
+ f := t.Field(ix)
+ jsonTag := f.Tag.Get("json")
+ parts := strings.Split(jsonTag, ",")
+ if len(parts) == 0 {
+ continue
+ }
+ if parts[0] == node.Value {
+ return value.Field(ix), nil
+ }
+ if len(parts[0]) == 0 {
+ val := value.Field(ix)
+ inlineValue = &val
+ }
+ }
+ if inlineValue != nil {
+ if inlineValue.Kind() == reflect.Struct {
+ // handle 'inline'
+ match, err := j.findFieldInValue(inlineValue, node)
+ if err != nil {
+ return reflect.Value{}, err
+ }
+ if match.IsValid() {
+ return match, nil
+ }
+ }
+ }
+ return value.FieldByName(node.Value), nil
+}
+
+// evalField evaluates field of struct or key of map.
+func (j *JSONPath) evalField(input []reflect.Value, node *FieldNode) ([]reflect.Value, error) {
+ results := []reflect.Value{}
+ // If there's no input, there's no output
+ if len(input) == 0 {
+ return results, nil
+ }
+ for _, value := range input {
+ var result reflect.Value
+ value, isNil := template.Indirect(value)
+ if isNil {
+ continue
+ }
+
+ if value.Kind() == reflect.Struct {
+ var err error
+ if result, err = j.findFieldInValue(&value, node); err != nil {
+ return nil, err
+ }
+ } else if value.Kind() == reflect.Map {
+ mapKeyType := value.Type().Key()
+ nodeValue := reflect.ValueOf(node.Value)
+ // node value type must be convertible to map key type
+ if !nodeValue.Type().ConvertibleTo(mapKeyType) {
+ return results, fmt.Errorf("%s is not convertible to %s", nodeValue, mapKeyType)
+ }
+ result = value.MapIndex(nodeValue.Convert(mapKeyType))
+ }
+ if result.IsValid() {
+ results = append(results, result)
+ }
+ }
+ if len(results) == 0 {
+ if j.allowMissingKeys {
+ return results, nil
+ }
+ return results, fmt.Errorf("%s is not found", node.Value)
+ }
+ return results, nil
+}
+
+// evalWildcard extracts all contents of the given value
+func (j *JSONPath) evalWildcard(input []reflect.Value, node *WildcardNode) ([]reflect.Value, error) {
+ results := []reflect.Value{}
+ for _, value := range input {
+ value, isNil := template.Indirect(value)
+ if isNil {
+ continue
+ }
+
+ kind := value.Kind()
+ if kind == reflect.Struct {
+ for i := 0; i < value.NumField(); i++ {
+ results = append(results, value.Field(i))
+ }
+ } else if kind == reflect.Map {
+ for _, key := range value.MapKeys() {
+ results = append(results, value.MapIndex(key))
+ }
+ } else if kind == reflect.Array || kind == reflect.Slice || kind == reflect.String {
+ for i := 0; i < value.Len(); i++ {
+ results = append(results, value.Index(i))
+ }
+ }
+ }
+ return results, nil
+}
+
+// evalRecursive visits the given value recursively and pushes all of them to result
+func (j *JSONPath) evalRecursive(input []reflect.Value, node *RecursiveNode) ([]reflect.Value, error) {
+ result := []reflect.Value{}
+ for _, value := range input {
+ results := []reflect.Value{}
+ value, isNil := template.Indirect(value)
+ if isNil {
+ continue
+ }
+
+ kind := value.Kind()
+ if kind == reflect.Struct {
+ for i := 0; i < value.NumField(); i++ {
+ results = append(results, value.Field(i))
+ }
+ } else if kind == reflect.Map {
+ for _, key := range value.MapKeys() {
+ results = append(results, value.MapIndex(key))
+ }
+ } else if kind == reflect.Array || kind == reflect.Slice || kind == reflect.String {
+ for i := 0; i < value.Len(); i++ {
+ results = append(results, value.Index(i))
+ }
+ }
+ if len(results) != 0 {
+ result = append(result, value)
+ output, err := j.evalRecursive(results, node)
+ if err != nil {
+ return result, err
+ }
+ result = append(result, output...)
+ }
+ }
+ return result, nil
+}
+
+// evalFilter filters array according to FilterNode
+func (j *JSONPath) evalFilter(input []reflect.Value, node *FilterNode) ([]reflect.Value, error) {
+ results := []reflect.Value{}
+ for _, value := range input {
+ value, _ = template.Indirect(value)
+
+ if value.Kind() != reflect.Array && value.Kind() != reflect.Slice {
+ return input, fmt.Errorf("%v is not array or slice and cannot be filtered", value)
+ }
+ for i := 0; i < value.Len(); i++ {
+ temp := []reflect.Value{value.Index(i)}
+ lefts, err := j.evalList(temp, node.Left)
+
+ //case exists
+ if node.Operator == "exists" {
+ if len(lefts) > 0 {
+ results = append(results, value.Index(i))
+ }
+ continue
+ }
+
+ if err != nil {
+ return input, err
+ }
+
+ var left, right interface{}
+ switch {
+ case len(lefts) == 0:
+ continue
+ case len(lefts) > 1:
+ return input, fmt.Errorf("can only compare one element at a time")
+ }
+ left = lefts[0].Interface()
+
+ rights, err := j.evalList(temp, node.Right)
+ if err != nil {
+ return input, err
+ }
+ switch {
+ case len(rights) == 0:
+ continue
+ case len(rights) > 1:
+ return input, fmt.Errorf("can only compare one element at a time")
+ }
+ right = rights[0].Interface()
+
+ pass := false
+ switch node.Operator {
+ case "<":
+ pass, err = template.Less(left, right)
+ case ">":
+ pass, err = template.Greater(left, right)
+ case "==":
+ pass, err = template.Equal(left, right)
+ case "!=":
+ pass, err = template.NotEqual(left, right)
+ case "<=":
+ pass, err = template.LessEqual(left, right)
+ case ">=":
+ pass, err = template.GreaterEqual(left, right)
+ default:
+ return results, fmt.Errorf("unrecognized filter operator %s", node.Operator)
+ }
+ if err != nil {
+ return results, err
+ }
+ if pass {
+ results = append(results, value.Index(i))
+ }
+ }
+ }
+ return results, nil
+}
+
+// evalToText translates reflect value to corresponding text
+func (j *JSONPath) evalToText(v reflect.Value) ([]byte, error) {
+ iface, ok := template.PrintableValue(v)
+ if !ok {
+ return nil, fmt.Errorf("can't print type %s", v.Type())
+ }
+ if iface == nil {
+ return []byte("null"), nil
+ }
+ var buffer bytes.Buffer
+ fmt.Fprint(&buffer, iface)
+ return buffer.Bytes(), nil
+}
diff --git a/internal/util/jsonpath/jsonpath_test.go b/internal/util/jsonpath/jsonpath_test.go
new file mode 100644
index 0000000..18fd8c8
--- /dev/null
+++ b/internal/util/jsonpath/jsonpath_test.go
@@ -0,0 +1,951 @@
+/*
+Copyright 2015 The Kubernetes Authors.
+
+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.
+*/
+
+package jsonpath
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "sort"
+ "strings"
+ "testing"
+)
+
+type jsonpathTest struct {
+ name string
+ template string
+ input interface{}
+ expect string
+ expectError bool
+}
+
+func testJSONPath(tests []jsonpathTest, allowMissingKeys bool, t *testing.T) {
+ for _, test := range tests {
+ j := New(test.name)
+ j.AllowMissingKeys(allowMissingKeys)
+ err := j.Parse(test.template)
+ if err != nil {
+ if !test.expectError {
+ t.Errorf("in %s, parse %s error %v", test.name, test.template, err)
+ }
+ continue
+ }
+ buf := new(bytes.Buffer)
+ err = j.Execute(buf, test.input)
+ if test.expectError {
+ if err == nil {
+ t.Errorf(`in %s, expected execute error, got %q`, test.name, buf)
+ }
+ continue
+ } else if err != nil {
+ t.Errorf("in %s, execute error %v", test.name, err)
+ }
+ out := buf.String()
+ if out != test.expect {
+ t.Errorf(`in %s, expect to get "%s", got "%s"`, test.name, test.expect, out)
+ }
+ }
+}
+
+// testJSONPathSortOutput test cases related to map, the results may print in random order
+func testJSONPathSortOutput(tests []jsonpathTest, t *testing.T) {
+ for _, test := range tests {
+ j := New(test.name)
+ err := j.Parse(test.template)
+ if err != nil {
+ t.Errorf("in %s, parse %s error %v", test.name, test.template, err)
+ }
+ buf := new(bytes.Buffer)
+ err = j.Execute(buf, test.input)
+ if err != nil {
+ t.Errorf("in %s, execute error %v", test.name, err)
+ }
+ out := buf.String()
+ //since map is visited in random order, we need to sort the results.
+ sortedOut := strings.Fields(out)
+ sort.Strings(sortedOut)
+ sortedExpect := strings.Fields(test.expect)
+ sort.Strings(sortedExpect)
+ if !reflect.DeepEqual(sortedOut, sortedExpect) {
+ t.Errorf(`in %s, expect to get "%s", got "%s"`, test.name, test.expect, out)
+ }
+ }
+}
+
+func testFailJSONPath(tests []jsonpathTest, t *testing.T) {
+ for _, test := range tests {
+ j := New(test.name)
+ err := j.Parse(test.template)
+ if err != nil {
+ t.Errorf("in %s, parse %s error %v", test.name, test.template, err)
+ }
+ buf := new(bytes.Buffer)
+ err = j.Execute(buf, test.input)
+ var out string
+ if err == nil {
+ out = "nil"
+ } else {
+ out = err.Error()
+ }
+ if out != test.expect {
+ t.Errorf("in %s, expect to get error %q, got %q", test.name, test.expect, out)
+ }
+ }
+}
+
+func TestTypesInput(t *testing.T) {
+ types := map[string]interface{}{
+ "bools": []bool{true, false, true, false},
+ "integers": []int{1, 2, 3, 4},
+ "floats": []float64{1.0, 2.2, 3.3, 4.0},
+ "strings": []string{"one", "two", "three", "four"},
+ "interfaces": []interface{}{true, "one", 1, 1.1},
+ "maps": []map[string]interface{}{
+ {"name": "one", "value": 1},
+ {"name": "two", "value": 2.02},
+ {"name": "three", "value": 3.03},
+ {"name": "four", "value": 4.04},
+ },
+ "structs": []struct {
+ Name string `json:"name"`
+ Value interface{} `json:"value"`
+ Type string `json:"type"`
+ }{
+ {Name: "one", Value: 1, Type: "integer"},
+ {Name: "two", Value: 2.002, Type: "float"},
+ {Name: "three", Value: 3, Type: "integer"},
+ {Name: "four", Value: 4.004, Type: "float"},
+ },
+ }
+
+ sliceTests := []jsonpathTest{
+ // boolean slice tests
+ {"boolSlice", `{ .bools }`, types, `[true,false,true,false]`, false},
+ {"boolSliceIndex", `{ .bools[0] }`, types, `true`, false},
+ {"boolSliceIndex", `{ .bools[-1] }`, types, `false`, false},
+ {"boolSubSlice", `{ .bools[0:2] }`, types, `true false`, false},
+ {"boolSubSliceFirst2", `{ .bools[:2] }`, types, `true false`, false},
+ {"boolSubSliceStep2", `{ .bools[:4:2] }`, types, `true true`, false},
+ // integer slice tests
+ {"integerSlice", `{ .integers }`, types, `[1,2,3,4]`, false},
+ {"integerSliceIndex", `{ .integers[0] }`, types, `1`, false},
+ {"integerSliceIndexReverse", `{ .integers[-2] }`, types, `3`, false},
+ {"integerSubSliceFirst2", `{ .integers[0:2] }`, types, `1 2`, false},
+ {"integerSubSliceFirst2Alt", `{ .integers[:2] }`, types, `1 2`, false},
+ {"integerSubSliceStep2", `{ .integers[:4:2] }`, types, `1 3`, false},
+ // float slice tests
+ {"floatSlice", `{ .floats }`, types, `[1,2.2,3.3,4]`, false},
+ {"floatSliceIndex", `{ .floats[0] }`, types, `1`, false},
+ {"floatSliceIndexReverse", `{ .floats[-2] }`, types, `3.3`, false},
+ {"floatSubSliceFirst2", `{ .floats[0:2] }`, types, `1 2.2`, false},
+ {"floatSubSliceFirst2Alt", `{ .floats[:2] }`, types, `1 2.2`, false},
+ {"floatSubSliceStep2", `{ .floats[:4:2] }`, types, `1 3.3`, false},
+ // strings slice tests
+ {"stringSlice", `{ .strings }`, types, `["one","two","three","four"]`, false},
+ {"stringSliceIndex", `{ .strings[0] }`, types, `one`, false},
+ {"stringSliceIndexReverse", `{ .strings[-2] }`, types, `three`, false},
+ {"stringSubSliceFirst2", `{ .strings[0:2] }`, types, `one two`, false},
+ {"stringSubSliceFirst2Alt", `{ .strings[:2] }`, types, `one two`, false},
+ {"stringSubSliceStep2", `{ .strings[:4:2] }`, types, `one three`, false},
+ // interfaces slice tests
+ {"interfaceSlice", `{ .interfaces }`, types, `[true,"one",1,1.1]`, false},
+ {"interfaceSliceIndex", `{ .interfaces[0] }`, types, `true`, false},
+ {"interfaceSliceIndexReverse", `{ .interfaces[-2] }`, types, `1`, false},
+ {"interfaceSubSliceFirst2", `{ .interfaces[0:2] }`, types, `true one`, false},
+ {"interfaceSubSliceFirst2Alt", `{ .interfaces[:2] }`, types, `true one`, false},
+ {"interfaceSubSliceStep2", `{ .interfaces[:4:2] }`, types, `true 1`, false},
+ // maps slice tests
+ {"mapSlice", `{ .maps }`, types,
+ `[{"name":"one","value":1},{"name":"two","value":2.02},{"name":"three","value":3.03},{"name":"four","value":4.04}]`, false},
+ {"mapSliceIndex", `{ .maps[0] }`, types, `{"name":"one","value":1}`, false},
+ {"mapSliceIndexReverse", `{ .maps[-2] }`, types, `{"name":"three","value":3.03}`, false},
+ {"mapSubSliceFirst2", `{ .maps[0:2] }`, types, `{"name":"one","value":1} {"name":"two","value":2.02}`, false},
+ {"mapSubSliceFirst2Alt", `{ .maps[:2] }`, types, `{"name":"one","value":1} {"name":"two","value":2.02}`, false},
+ {"mapSubSliceStepOdd", `{ .maps[::2] }`, types, `{"name":"one","value":1} {"name":"three","value":3.03}`, false},
+ {"mapSubSliceStepEven", `{ .maps[1::2] }`, types, `{"name":"two","value":2.02} {"name":"four","value":4.04}`, false},
+ // structs slice tests
+ {"structSlice", `{ .structs }`, types,
+ `[{"name":"one","value":1,"type":"integer"},{"name":"two","value":2.002,"type":"float"},{"name":"three","value":3,"type":"integer"},{"name":"four","value":4.004,"type":"float"}]`, false},
+ {"structSliceIndex", `{ .structs[0] }`, types, `{"name":"one","value":1,"type":"integer"}`, false},
+ {"structSliceIndexReverse", `{ .structs[-2] }`, types, `{"name":"three","value":3,"type":"integer"}`, false},
+ {"structSubSliceFirst2", `{ .structs[0:2] }`, types,
+ `{"name":"one","value":1,"type":"integer"} {"name":"two","value":2.002,"type":"float"}`, false},
+ {"structSubSliceFirst2Alt", `{ .structs[:2] }`, types,
+ `{"name":"one","value":1,"type":"integer"} {"name":"two","value":2.002,"type":"float"}`, false},
+ {"structSubSliceStepOdd", `{ .structs[::2] }`, types,
+ `{"name":"one","value":1,"type":"integer"} {"name":"three","value":3,"type":"integer"}`, false},
+ {"structSubSliceStepEven", `{ .structs[1::2] }`, types,
+ `{"name":"two","value":2.002,"type":"float"} {"name":"four","value":4.004,"type":"float"}`, false},
+ }
+
+ testJSONPath(sliceTests, false, t)
+}
+
+type book struct {
+ Category string
+ Author string
+ Title string
+ Price float32
+}
+
+func (b book) String() string {
+ return fmt.Sprintf("{Category: %s, Author: %s, Title: %s, Price: %v}", b.Category, b.Author, b.Title, b.Price)
+}
+
+type bicycle struct {
+ Color string
+ Price float32
+ IsNew bool
+}
+
+type empName string
+type job string
+type store struct {
+ Book []book
+ Bicycle []bicycle
+ Name string
+ Labels map[string]int
+ Employees map[empName]job
+}
+
+func TestStructInput(t *testing.T) {
+
+ storeData := store{
+ Name: "jsonpath",
+ Book: []book{
+ {"reference", "Nigel Rees", "Sayings of the Centurey", 8.95},
+ {"fiction", "Evelyn Waugh", "Sword of Honour", 12.99},
+ {"fiction", "Herman Melville", "Moby Dick", 8.99},
+ },
+ Bicycle: []bicycle{
+ {"red", 19.95, true},
+ {"green", 20.01, false},
+ },
+ Labels: map[string]int{
+ "engieer": 10,
+ "web/html": 15,
+ "k8s-app": 20,
+ },
+ Employees: map[empName]job{
+ "jason": "manager",
+ "dan": "clerk",
+ },
+ }
+
+ storeTests := []jsonpathTest{
+ {"plain", "hello jsonpath", nil, "hello jsonpath", false},
+ {"recursive", "{..}", []int{1, 2, 3}, "[1,2,3]", false},
+ {"filter", "{[?(@<5)]}", []int{2, 6, 3, 7}, "2 3", false},
+ {"quote", `{"{"}`, nil, "{", false},
+ {"union", "{[1,3,4]}", []int{0, 1, 2, 3, 4}, "1 3 4", false},
+ {"array", "{[0:2]}", []string{"Monday", "Tudesday"}, "Monday Tudesday", false},
+ {"variable", "hello {.Name}", storeData, "hello jsonpath", false},
+ {"dict/", "{$.Labels.web/html}", storeData, "15", false},
+ {"dict/", "{$.Employees.jason}", storeData, "manager", false},
+ {"dict/", "{$.Employees.dan}", storeData, "clerk", false},
+ {"dict-", "{.Labels.k8s-app}", storeData, "20", false},
+ {"nest", "{.Bicycle[*].Color}", storeData, "red green", false},
+ {"allarray", "{.Book[*].Author}", storeData, "Nigel Rees Evelyn Waugh Herman Melville", false},
+ {"allfields", `{range .Bicycle[*]}{ "{" }{ @.* }{ "} " }{end}`, storeData, "{red 19.95 true} {green 20.01 false} ", false},
+ {"recurfields", "{..Price}", storeData, "8.95 12.99 8.99 19.95 20.01", false},
+ {"recurdotfields", "{...Price}", storeData, "8.95 12.99 8.99 19.95 20.01", false},
+ {"superrecurfields", "{............................................................Price}", storeData, "", true},
+ {"allstructsSlice", "{.Bicycle}", storeData,
+ `[{"Color":"red","Price":19.95,"IsNew":true},{"Color":"green","Price":20.01,"IsNew":false}]`, false},
+ {"allstructs", `{range .Bicycle[*]}{ @ }{ " " }{end}`, storeData,
+ `{"Color":"red","Price":19.95,"IsNew":true} {"Color":"green","Price":20.01,"IsNew":false} `, false},
+ {"lastarray", "{.Book[-1:]}", storeData,
+ `{"Category":"fiction","Author":"Herman Melville","Title":"Moby Dick","Price":8.99}`, false},
+ {"recurarray", "{..Book[2]}", storeData,
+ `{"Category":"fiction","Author":"Herman Melville","Title":"Moby Dick","Price":8.99}`, false},
+ {"bool", "{.Bicycle[?(@.IsNew==true)]}", storeData, `{"Color":"red","Price":19.95,"IsNew":true}`, false},
+ }
+
+ testJSONPath(storeTests, false, t)
+
+ missingKeyTests := []jsonpathTest{
+ {"nonexistent field", "{.hello}", storeData, "", false},
+ {"nonexistent field 2", "before-{.hello}after", storeData, "before-after", false},
+ }
+ testJSONPath(missingKeyTests, true, t)
+
+ failStoreTests := []jsonpathTest{
+ {"invalid identifier", "{hello}", storeData, "unrecognized identifier hello", false},
+ {"nonexistent field", "{.hello}", storeData, "hello is not found", false},
+ {"invalid array", "{.Labels[0]}", storeData, "map[string]int is not array or slice", false},
+ {"invalid filter operator", "{.Book[?(@.Price<>10)]}", storeData, "unrecognized filter operator <>", false},
+ {"redundant end", "{range .Labels.*}{@}{end}{end}", storeData, "not in range, nothing to end", false},
+ }
+ testFailJSONPath(failStoreTests, t)
+}
+
+func TestJSONInput(t *testing.T) {
+ var pointsJSON = []byte(`[
+ {"id": "i1", "x":4, "y":-5},
+ {"id": "i2", "x":-2, "y":-5, "z":1},
+ {"id": "i3", "x": 8, "y": 3 },
+ {"id": "i4", "x": -6, "y": -1 },
+ {"id": "i5", "x": 0, "y": 2, "z": 1 },
+ {"id": "i6", "x": 1, "y": 4 },
+ {"id": "i7", "x": null, "y": 4 }
+ ]`)
+ var pointsData interface{}
+ err := json.Unmarshal(pointsJSON, &pointsData)
+ if err != nil {
+ t.Error(err)
+ }
+ pointsTests := []jsonpathTest{
+ {"exists filter", "{[?(@.z)].id}", pointsData, "i2 i5", false},
+ {"bracket key", "{[0]['id']}", pointsData, "i1", false},
+ {"nil value", "{[-1]['x']}", pointsData, "null", false},
+ }
+ testJSONPath(pointsTests, false, t)
+}
+
+// TestKubernetes tests some use cases from kubernetes
+func TestKubernetes(t *testing.T) {
+ var input = []byte(`{
+ "kind": "List",
+ "items":[
+ {
+ "kind":"None",
+ "metadata":{
+ "name":"127.0.0.1",
+ "labels":{
+ "kubernetes.io/hostname":"127.0.0.1"
+ }
+ },
+ "status":{
+ "capacity":{"cpu":"4"},
+ "ready": true,
+ "addresses":[{"type": "LegacyHostIP", "address":"127.0.0.1"}]
+ }
+ },
+ {
+ "kind":"None",
+ "metadata":{
+ "name":"127.0.0.2",
+ "labels":{
+ "kubernetes.io/hostname":"127.0.0.2"
+ }
+ },
+ "status":{
+ "capacity":{"cpu":"8"},
+ "ready": false,
+ "addresses":[
+ {"type": "LegacyHostIP", "address":"127.0.0.2"},
+ {"type": "another", "address":"127.0.0.3"}
+ ]
+ }
+ }
+ ],
+ "users":[
+ {
+ "name": "myself",
+ "user": {}
+ },
+ {
+ "name": "e2e",
+ "user": {"username": "admin", "password": "secret"}
+ }
+ ]
+ }`)
+ var nodesData interface{}
+ err := json.Unmarshal(input, &nodesData)
+ if err != nil {
+ t.Error(err)
+ }
+
+ nodesTests := []jsonpathTest{
+ {"range item", `{range .items[*]}{.metadata.name}, {end}{.kind}`, nodesData, "127.0.0.1, 127.0.0.2, List", false},
+ {"range item with quote", `{range .items[*]}{.metadata.name}{"\t"}{end}`, nodesData, "127.0.0.1\t127.0.0.2\t", false},
+ {"range addresss", `{.items[*].status.addresses[*].address}`, nodesData,
+ "127.0.0.1 127.0.0.2 127.0.0.3", false},
+ {"double range", `{range .items[*]}{range .status.addresses[*]}{.address}, {end}{end}`, nodesData,
+ "127.0.0.1, 127.0.0.2, 127.0.0.3, ", false},
+ {"item name", `{.items[*].metadata.name}`, nodesData, "127.0.0.1 127.0.0.2", false},
+ {"union nodes capacity", `{.items[*]['metadata.name', 'status.capacity']}`, nodesData,
+ `127.0.0.1 127.0.0.2 {"cpu":"4"} {"cpu":"8"}`, false},
+ {"range nodes capacity", `{range .items[*]}[{.metadata.name}, {.status.capacity}] {end}`, nodesData,
+ `[127.0.0.1, {"cpu":"4"}] [127.0.0.2, {"cpu":"8"}] `, false},
+ {"user password", `{.users[?(@.name=="e2e")].user.password}`, &nodesData, "secret", false},
+ {"hostname", `{.items[0].metadata.labels.kubernetes\.io/hostname}`, &nodesData, "127.0.0.1", false},
+ {"hostname filter", `{.items[?(@.metadata.labels.kubernetes\.io/hostname=="127.0.0.1")].kind}`, &nodesData, "None", false},
+ {"bool item", `{.items[?(@..ready==true)].metadata.name}`, &nodesData, "127.0.0.1", false},
+ }
+ testJSONPath(nodesTests, false, t)
+
+ randomPrintOrderTests := []jsonpathTest{
+ {"recursive name", "{..name}", nodesData, `127.0.0.1 127.0.0.2 myself e2e`, false},
+ }
+ testJSONPathSortOutput(randomPrintOrderTests, t)
+}
+
+func TestEmptyRange(t *testing.T) {
+ var input = []byte(`{"items":[]}`)
+ var emptyList interface{}
+ err := json.Unmarshal(input, &emptyList)
+ if err != nil {
+ t.Error(err)
+ }
+
+ tests := []jsonpathTest{
+ {"empty range", `{range .items[*]}{.metadata.name}{end}`, &emptyList, "", false},
+ {"empty nested range", `{range .items[*]}{.metadata.name}{":"}{range @.spec.containers[*]}{.name}{","}{end}{"+"}{end}`, &emptyList, "", false},
+ }
+ testJSONPath(tests, true, t)
+}
+
+func TestNestedRanges(t *testing.T) {
+ var input = []byte(`{
+ "items": [
+ {
+ "metadata": {
+ "name": "pod1"
+ },
+ "spec": {
+ "containers": [
+ {
+ "name": "foo",
+ "another": [
+ { "name": "value1" },
+ { "name": "value2" }
+ ]
+ },
+ {
+ "name": "bar",
+ "another": [
+ { "name": "value1" },
+ { "name": "value2" }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "metadata": {
+ "name": "pod2"
+ },
+ "spec": {
+ "containers": [
+ {
+ "name": "baz",
+ "another": [
+ { "name": "value1" },
+ { "name": "value2" }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ }`)
+ var data interface{}
+ err := json.Unmarshal(input, &data)
+ if err != nil {
+ t.Error(err)
+ }
+
+ testJSONPath(
+ []jsonpathTest{
+ {
+ "nested range with a trailing newline",
+ `{range .items[*]}` +
+ `{.metadata.name}` +
+ `{":"}` +
+ `{range @.spec.containers[*]}` +
+ `{.name}` +
+ `{","}` +
+ `{end}` +
+ `{"+"}` +
+ `{end}`,
+ data,
+ "pod1:foo,bar,+pod2:baz,+",
+ false,
+ },
+ },
+ false,
+ t,
+ )
+
+ testJSONPath(
+ []jsonpathTest{
+ {
+ "nested range with a trailing character within another nested range with a trailing newline",
+ `{range .items[*]}` +
+ `{.metadata.name}` +
+ `{"~"}` +
+ `{range @.spec.containers[*]}` +
+ `{.name}` +
+ `{":"}` +
+ `{range @.another[*]}` +
+ `{.name}` +
+ `{","}` +
+ `{end}` +
+ `{"+"}` +
+ `{end}` +
+ `{"#"}` +
+ `{end}`,
+ data,
+ "pod1~foo:value1,value2,+bar:value1,value2,+#pod2~baz:value1,value2,+#",
+ false,
+ },
+ },
+ false,
+ t,
+ )
+
+ testJSONPath(
+ []jsonpathTest{
+ {
+ "two nested ranges at the same level with a trailing newline",
+ `{range .items[*]}` +
+ `{.metadata.name}` +
+ `{"\t"}` +
+ `{range @.spec.containers[*]}` +
+ `{.name}` +
+ `{" "}` +
+ `{end}` +
+ `{"\t"}` +
+ `{range @.spec.containers[*]}` +
+ `{.name}` +
+ `{" "}` +
+ `{end}` +
+ `{"\n"}` +
+ `{end}`,
+ data,
+ "pod1\tfoo bar \tfoo bar \npod2\tbaz \tbaz \n",
+ false,
+ },
+ },
+ false,
+ t,
+ )
+}
+
+func TestFilterPartialMatchesSometimesMissingAnnotations(t *testing.T) {
+ // for https://issues.k8s.io/45546
+ var input = []byte(`{
+ "kind": "List",
+ "items": [
+ {
+ "kind": "Pod",
+ "metadata": {
+ "name": "pod1",
+ "annotations": {
+ "color": "blue"
+ }
+ }
+ },
+ {
+ "kind": "Pod",
+ "metadata": {
+ "name": "pod2"
+ }
+ },
+ {
+ "kind": "Pod",
+ "metadata": {
+ "name": "pod3",
+ "annotations": {
+ "color": "green"
+ }
+ }
+ },
+ {
+ "kind": "Pod",
+ "metadata": {
+ "name": "pod4",
+ "annotations": {
+ "color": "blue"
+ }
+ }
+ }
+ ]
+ }`)
+ var data interface{}
+ err := json.Unmarshal(input, &data)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ testJSONPath(
+ []jsonpathTest{
+ {
+ "filter, should only match a subset, some items don't have annotations, tolerate missing items",
+ `{.items[?(@.metadata.annotations.color=="blue")].metadata.name}`,
+ data,
+ "pod1 pod4",
+ false, // expect no error
+ },
+ },
+ true, // allow missing keys
+ t,
+ )
+
+ testJSONPath(
+ []jsonpathTest{
+ {
+ "filter, should only match a subset, some items don't have annotations, error on missing items",
+ `{.items[?(@.metadata.annotations.color=="blue")].metadata.name}`,
+ data,
+ "",
+ true, // expect an error
+ },
+ },
+ false, // don't allow missing keys
+ t,
+ )
+}
+
+func TestNegativeIndex(t *testing.T) {
+ var input = []byte(
+ `{
+ "apiVersion": "v1",
+ "kind": "Pod",
+ "spec": {
+ "containers": [
+ {
+ "image": "radial/busyboxplus:curl",
+ "name": "fake0"
+ },
+ {
+ "image": "radial/busyboxplus:curl",
+ "name": "fake1"
+ },
+ {
+ "image": "radial/busyboxplus:curl",
+ "name": "fake2"
+ },
+ {
+ "image": "radial/busyboxplus:curl",
+ "name": "fake3"
+ }]}}`)
+
+ var data interface{}
+ err := json.Unmarshal(input, &data)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ testJSONPath(
+ []jsonpathTest{
+ {
+ "test containers[0], it equals containers[0]",
+ `{.spec.containers[0].name}`,
+ data,
+ "fake0",
+ false,
+ },
+ {
+ "test containers[0:0], it equals the empty set",
+ `{.spec.containers[0:0].name}`,
+ data,
+ "",
+ false,
+ },
+ {
+ "test containers[0:-1], it equals containers[0:3]",
+ `{.spec.containers[0:-1].name}`,
+ data,
+ "fake0 fake1 fake2",
+ false,
+ },
+ {
+ "test containers[-1:0], expect error",
+ `{.spec.containers[-1:0].name}`,
+ data,
+ "",
+ true,
+ },
+ {
+ "test containers[-1], it equals containers[3]",
+ `{.spec.containers[-1].name}`,
+ data,
+ "fake3",
+ false,
+ },
+ {
+ "test containers[-1:], it equals containers[3:]",
+ `{.spec.containers[-1:].name}`,
+ data,
+ "fake3",
+ false,
+ },
+ {
+ "test containers[-2], it equals containers[2]",
+ `{.spec.containers[-2].name}`,
+ data,
+ "fake2",
+ false,
+ },
+ {
+ "test containers[-2:], it equals containers[2:]",
+ `{.spec.containers[-2:].name}`,
+ data,
+ "fake2 fake3",
+ false,
+ },
+ {
+ "test containers[-3], it equals containers[1]",
+ `{.spec.containers[-3].name}`,
+ data,
+ "fake1",
+ false,
+ },
+ {
+ "test containers[-4], it equals containers[0]",
+ `{.spec.containers[-4].name}`,
+ data,
+ "fake0",
+ false,
+ },
+ {
+ "test containers[-4:], it equals containers[0:]",
+ `{.spec.containers[-4:].name}`,
+ data,
+ "fake0 fake1 fake2 fake3",
+ false,
+ },
+ {
+ "test containers[-5], expect a error cause it out of bounds",
+ `{.spec.containers[-5].name}`,
+ data,
+ "",
+ true, // expect error
+ },
+ {
+ "test containers[5:5], expect empty set",
+ `{.spec.containers[5:5].name}`,
+ data,
+ "",
+ false,
+ },
+ {
+ "test containers[-5:-5], expect empty set",
+ `{.spec.containers[-5:-5].name}`,
+ data,
+ "",
+ false,
+ },
+ {
+ "test containers[3:1], expect a error cause start index is greater than end index",
+ `{.spec.containers[3:1].name}`,
+ data,
+ "",
+ true,
+ },
+ {
+ "test containers[-1:-2], it equals containers[3:2], expect a error cause start index is greater than end index",
+ `{.spec.containers[-1:-2].name}`,
+ data,
+ "",
+ true,
+ },
+ },
+ false,
+ t,
+ )
+}
+
+func TestRunningPodsJSONPathOutput(t *testing.T) {
+ var input = []byte(`{
+ "kind": "List",
+ "items": [
+ {
+ "kind": "Pod",
+ "metadata": {
+ "name": "pod1"
+ },
+ "status": {
+ "phase": "Running"
+ }
+ },
+ {
+ "kind": "Pod",
+ "metadata": {
+ "name": "pod2"
+ },
+ "status": {
+ "phase": "Running"
+ }
+ },
+ {
+ "kind": "Pod",
+ "metadata": {
+ "name": "pod3"
+ },
+ "status": {
+ "phase": "Running"
+ }
+ },
+ {
+ "resourceVersion": ""
+ }
+ ]
+ }`)
+ var data interface{}
+ err := json.Unmarshal(input, &data)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ testJSONPath(
+ []jsonpathTest{
+ {
+ "range over pods without selecting the last one",
+ `{range .items[?(.status.phase=="Running")]}{.metadata.name}{" is Running\n"}{end}`,
+ data,
+ "pod1 is Running\npod2 is Running\npod3 is Running\n",
+ false, // expect no error
+ },
+ },
+ true, // allow missing keys
+ t,
+ )
+}
+
+func TestStep(t *testing.T) {
+ var input = []byte(
+ `{
+ "apiVersion": "v1",
+ "kind": "Pod",
+ "spec": {
+ "containers": [
+ {
+ "image": "radial/busyboxplus:curl",
+ "name": "fake0"
+ },
+ {
+ "image": "radial/busyboxplus:curl",
+ "name": "fake1"
+ },
+ {
+ "image": "radial/busyboxplus:curl",
+ "name": "fake2"
+ },
+ {
+ "image": "radial/busyboxplus:curl",
+ "name": "fake3"
+ },
+ {
+ "image": "radial/busyboxplus:curl",
+ "name": "fake4"
+ },
+ {
+ "image": "radial/busyboxplus:curl",
+ "name": "fake5"
+ }]}}`)
+
+ var data interface{}
+ err := json.Unmarshal(input, &data)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ testJSONPath(
+ []jsonpathTest{
+ {
+ "test containers[0:], it equals containers[0:6:1]",
+ `{.spec.containers[0:].name}`,
+ data,
+ "fake0 fake1 fake2 fake3 fake4 fake5",
+ false,
+ },
+ {
+ "test containers[0:6:], it equals containers[0:6:1]",
+ `{.spec.containers[0:6:].name}`,
+ data,
+ "fake0 fake1 fake2 fake3 fake4 fake5",
+ false,
+ },
+ {
+ "test containers[0:6:1]",
+ `{.spec.containers[0:6:1].name}`,
+ data,
+ "fake0 fake1 fake2 fake3 fake4 fake5",
+ false,
+ },
+ {
+ "test containers[0:6:0], it errors",
+ `{.spec.containers[0:6:0].name}`,
+ data,
+ "",
+ true,
+ },
+ {
+ "test containers[0:6:-1], it errors",
+ `{.spec.containers[0:6:-1].name}`,
+ data,
+ "",
+ true,
+ },
+ {
+ "test containers[1:4:2]",
+ `{.spec.containers[1:4:2].name}`,
+ data,
+ "fake1 fake3",
+ false,
+ },
+ {
+ "test containers[1:4:3]",
+ `{.spec.containers[1:4:3].name}`,
+ data,
+ "fake1",
+ false,
+ },
+ {
+ "test containers[1:4:4]",
+ `{.spec.containers[1:4:4].name}`,
+ data,
+ "fake1",
+ false,
+ },
+ {
+ "test containers[0:6:2]",
+ `{.spec.containers[0:6:2].name}`,
+ data,
+ "fake0 fake2 fake4",
+ false,
+ },
+ {
+ "test containers[0:6:3]",
+ `{.spec.containers[0:6:3].name}`,
+ data,
+ "fake0 fake3",
+ false,
+ },
+ {
+ "test containers[0:6:5]",
+ `{.spec.containers[0:6:5].name}`,
+ data,
+ "fake0 fake5",
+ false,
+ },
+ {
+ "test containers[0:6:6]",
+ `{.spec.containers[0:6:6].name}`,
+ data,
+ "fake0",
+ false,
+ },
+ },
+ false,
+ t,
+ )
+}
diff --git a/internal/util/jsonpath/node.go b/internal/util/jsonpath/node.go
new file mode 100644
index 0000000..83abe8b
--- /dev/null
+++ b/internal/util/jsonpath/node.go
@@ -0,0 +1,256 @@
+/*
+Copyright 2015 The Kubernetes Authors.
+
+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.
+*/
+
+package jsonpath
+
+import "fmt"
+
+// NodeType identifies the type of a parse tree node.
+type NodeType int
+
+// Type returns itself and provides an easy default implementation
+func (t NodeType) Type() NodeType {
+ return t
+}
+
+func (t NodeType) String() string {
+ return NodeTypeName[t]
+}
+
+const (
+ NodeText NodeType = iota
+ NodeArray
+ NodeList
+ NodeField
+ NodeIdentifier
+ NodeFilter
+ NodeInt
+ NodeFloat
+ NodeWildcard
+ NodeRecursive
+ NodeUnion
+ NodeBool
+)
+
+var NodeTypeName = map[NodeType]string{
+ NodeText: "NodeText",
+ NodeArray: "NodeArray",
+ NodeList: "NodeList",
+ NodeField: "NodeField",
+ NodeIdentifier: "NodeIdentifier",
+ NodeFilter: "NodeFilter",
+ NodeInt: "NodeInt",
+ NodeFloat: "NodeFloat",
+ NodeWildcard: "NodeWildcard",
+ NodeRecursive: "NodeRecursive",
+ NodeUnion: "NodeUnion",
+ NodeBool: "NodeBool",
+}
+
+type Node interface {
+ Type() NodeType
+ String() string
+}
+
+// ListNode holds a sequence of nodes.
+type ListNode struct {
+ NodeType
+ Nodes []Node // The element nodes in lexical order.
+}
+
+func newList() *ListNode {
+ return &ListNode{NodeType: NodeList}
+}
+
+func (l *ListNode) append(n Node) {
+ l.Nodes = append(l.Nodes, n)
+}
+
+func (l *ListNode) String() string {
+ return l.Type().String()
+}
+
+// TextNode holds plain text.
+type TextNode struct {
+ NodeType
+ Text string // The text; may span newlines.
+}
+
+func newText(text string) *TextNode {
+ return &TextNode{NodeType: NodeText, Text: text}
+}
+
+func (t *TextNode) String() string {
+ return fmt.Sprintf("%s: %s", t.Type(), t.Text)
+}
+
+// FieldNode holds field of struct
+type FieldNode struct {
+ NodeType
+ Value string
+}
+
+func newField(value string) *FieldNode {
+ return &FieldNode{NodeType: NodeField, Value: value}
+}
+
+func (f *FieldNode) String() string {
+ return fmt.Sprintf("%s: %s", f.Type(), f.Value)
+}
+
+// IdentifierNode holds an identifier
+type IdentifierNode struct {
+ NodeType
+ Name string
+}
+
+func newIdentifier(value string) *IdentifierNode {
+ return &IdentifierNode{
+ NodeType: NodeIdentifier,
+ Name: value,
+ }
+}
+
+func (f *IdentifierNode) String() string {
+ return fmt.Sprintf("%s: %s", f.Type(), f.Name)
+}
+
+// ParamsEntry holds param information for ArrayNode
+type ParamsEntry struct {
+ Value int
+ Known bool // whether the value is known when parse it
+ Derived bool
+}
+
+// ArrayNode holds start, end, step information for array index selection
+type ArrayNode struct {
+ NodeType
+ Params [3]ParamsEntry // start, end, step
+}
+
+func newArray(params [3]ParamsEntry) *ArrayNode {
+ return &ArrayNode{
+ NodeType: NodeArray,
+ Params: params,
+ }
+}
+
+func (a *ArrayNode) String() string {
+ return fmt.Sprintf("%s: %v", a.Type(), a.Params)
+}
+
+// FilterNode holds operand and operator information for filter
+type FilterNode struct {
+ NodeType
+ Left *ListNode
+ Right *ListNode
+ Operator string
+}
+
+func newFilter(left, right *ListNode, operator string) *FilterNode {
+ return &FilterNode{
+ NodeType: NodeFilter,
+ Left: left,
+ Right: right,
+ Operator: operator,
+ }
+}
+
+func (f *FilterNode) String() string {
+ return fmt.Sprintf("%s: %s %s %s", f.Type(), f.Left, f.Operator, f.Right)
+}
+
+// IntNode holds integer value
+type IntNode struct {
+ NodeType
+ Value int
+}
+
+func newInt(num int) *IntNode {
+ return &IntNode{NodeType: NodeInt, Value: num}
+}
+
+func (i *IntNode) String() string {
+ return fmt.Sprintf("%s: %d", i.Type(), i.Value)
+}
+
+// FloatNode holds float value
+type FloatNode struct {
+ NodeType
+ Value float64
+}
+
+func newFloat(num float64) *FloatNode {
+ return &FloatNode{NodeType: NodeFloat, Value: num}
+}
+
+func (i *FloatNode) String() string {
+ return fmt.Sprintf("%s: %f", i.Type(), i.Value)
+}
+
+// WildcardNode means a wildcard
+type WildcardNode struct {
+ NodeType
+}
+
+func newWildcard() *WildcardNode {
+ return &WildcardNode{NodeType: NodeWildcard}
+}
+
+func (i *WildcardNode) String() string {
+ return i.Type().String()
+}
+
+// RecursiveNode means a recursive descent operator
+type RecursiveNode struct {
+ NodeType
+}
+
+func newRecursive() *RecursiveNode {
+ return &RecursiveNode{NodeType: NodeRecursive}
+}
+
+func (r *RecursiveNode) String() string {
+ return r.Type().String()
+}
+
+// UnionNode is union of ListNode
+type UnionNode struct {
+ NodeType
+ Nodes []*ListNode
+}
+
+func newUnion(nodes []*ListNode) *UnionNode {
+ return &UnionNode{NodeType: NodeUnion, Nodes: nodes}
+}
+
+func (u *UnionNode) String() string {
+ return u.Type().String()
+}
+
+// BoolNode holds bool value
+type BoolNode struct {
+ NodeType
+ Value bool
+}
+
+func newBool(value bool) *BoolNode {
+ return &BoolNode{NodeType: NodeBool, Value: value}
+}
+
+func (b *BoolNode) String() string {
+ return fmt.Sprintf("%s: %t", b.Type(), b.Value)
+}
diff --git a/internal/util/jsonpath/parser.go b/internal/util/jsonpath/parser.go
new file mode 100644
index 0000000..1cc9a8e
--- /dev/null
+++ b/internal/util/jsonpath/parser.go
@@ -0,0 +1,527 @@
+/*
+Copyright 2015 The Kubernetes Authors.
+
+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.
+*/
+
+package jsonpath
+
+import (
+ "errors"
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+ "unicode"
+ "unicode/utf8"
+)
+
+const eof = -1
+
+const (
+ leftDelim = "{"
+ rightDelim = "}"
+)
+
+type Parser struct {
+ Name string
+ Root *ListNode
+ input string
+ pos int
+ start int
+ width int
+}
+
+var (
+ ErrSyntax = errors.New("invalid syntax")
+ dictKeyRex = regexp.MustCompile(`^'([^']*)'$`)
+ sliceOperatorRex = regexp.MustCompile(`^(-?[\d]*)(:-?[\d]*)?(:-?[\d]*)?$`)
+)
+
+// Parse parsed the given text and return a node Parser.
+// If an error is encountered, parsing stops and an empty
+// Parser is returned with the error
+func Parse(name, text string) (*Parser, error) {
+ p := NewParser(name)
+ err := p.Parse(text)
+ if err != nil {
+ p = nil
+ }
+ return p, err
+}
+
+func NewParser(name string) *Parser {
+ return &Parser{
+ Name: name,
+ }
+}
+
+// parseAction parsed the expression inside delimiter
+func parseAction(name, text string) (*Parser, error) {
+ p, err := Parse(name, fmt.Sprintf("%s%s%s", leftDelim, text, rightDelim))
+ // when error happens, p will be nil, so we need to return here
+ if err != nil {
+ return p, err
+ }
+ p.Root = p.Root.Nodes[0].(*ListNode)
+ return p, nil
+}
+
+func (p *Parser) Parse(text string) error {
+ p.input = text
+ p.Root = newList()
+ p.pos = 0
+ return p.parseText(p.Root)
+}
+
+// consumeText return the parsed text since last cosumeText
+func (p *Parser) consumeText() string {
+ value := p.input[p.start:p.pos]
+ p.start = p.pos
+ return value
+}
+
+// next returns the next rune in the input.
+func (p *Parser) next() rune {
+ if p.pos >= len(p.input) {
+ p.width = 0
+ return eof
+ }
+ r, w := utf8.DecodeRuneInString(p.input[p.pos:])
+ p.width = w
+ p.pos += p.width
+ return r
+}
+
+// peek returns but does not consume the next rune in the input.
+func (p *Parser) peek() rune {
+ r := p.next()
+ p.backup()
+ return r
+}
+
+// backup steps back one rune. Can only be called once per call of next.
+func (p *Parser) backup() {
+ p.pos -= p.width
+}
+
+func (p *Parser) parseText(cur *ListNode) error {
+ for {
+ if strings.HasPrefix(p.input[p.pos:], leftDelim) {
+ if p.pos > p.start {
+ cur.append(newText(p.consumeText()))
+ }
+ return p.parseLeftDelim(cur)
+ }
+ if p.next() == eof {
+ break
+ }
+ }
+ // Correctly reached EOF.
+ if p.pos > p.start {
+ cur.append(newText(p.consumeText()))
+ }
+ return nil
+}
+
+// parseLeftDelim scans the left delimiter, which is known to be present.
+func (p *Parser) parseLeftDelim(cur *ListNode) error {
+ p.pos += len(leftDelim)
+ p.consumeText()
+ newNode := newList()
+ cur.append(newNode)
+ cur = newNode
+ return p.parseInsideAction(cur)
+}
+
+func (p *Parser) parseInsideAction(cur *ListNode) error {
+ prefixMap := map[string]func(*ListNode) error{
+ rightDelim: p.parseRightDelim,
+ "[?(": p.parseFilter,
+ "..": p.parseRecursive,
+ }
+ for prefix, parseFunc := range prefixMap {
+ if strings.HasPrefix(p.input[p.pos:], prefix) {
+ return parseFunc(cur)
+ }
+ }
+
+ switch r := p.next(); {
+ case r == eof || isEndOfLine(r):
+ return fmt.Errorf("unclosed action")
+ case r == ' ':
+ p.consumeText()
+ case r == '@' || r == '$': //the current object, just pass it
+ p.consumeText()
+ case r == '[':
+ return p.parseArray(cur)
+ case r == '"' || r == '\'':
+ return p.parseQuote(cur, r)
+ case r == '.':
+ return p.parseField(cur)
+ case r == '+' || r == '-' || unicode.IsDigit(r):
+ p.backup()
+ return p.parseNumber(cur)
+ case isAlphaNumeric(r):
+ p.backup()
+ return p.parseIdentifier(cur)
+ default:
+ return fmt.Errorf("unrecognized character in action: %#U", r)
+ }
+ return p.parseInsideAction(cur)
+}
+
+// parseRightDelim scans the right delimiter, which is known to be present.
+func (p *Parser) parseRightDelim(cur *ListNode) error {
+ p.pos += len(rightDelim)
+ p.consumeText()
+ return p.parseText(p.Root)
+}
+
+// parseIdentifier scans build-in keywords, like "range" "end"
+func (p *Parser) parseIdentifier(cur *ListNode) error {
+ var r rune
+ for {
+ r = p.next()
+ if isTerminator(r) {
+ p.backup()
+ break
+ }
+ }
+ value := p.consumeText()
+
+ if isBool(value) {
+ v, err := strconv.ParseBool(value)
+ if err != nil {
+ return fmt.Errorf("can not parse bool '%s': %s", value, err.Error())
+ }
+
+ cur.append(newBool(v))
+ } else {
+ cur.append(newIdentifier(value))
+ }
+
+ return p.parseInsideAction(cur)
+}
+
+// parseRecursive scans the recursive descent operator ..
+func (p *Parser) parseRecursive(cur *ListNode) error {
+ if lastIndex := len(cur.Nodes) - 1; lastIndex >= 0 && cur.Nodes[lastIndex].Type() == NodeRecursive {
+ return fmt.Errorf("invalid multiple recursive descent")
+ }
+ p.pos += len("..")
+ p.consumeText()
+ cur.append(newRecursive())
+ if r := p.peek(); isAlphaNumeric(r) {
+ return p.parseField(cur)
+ }
+ return p.parseInsideAction(cur)
+}
+
+// parseNumber scans number
+func (p *Parser) parseNumber(cur *ListNode) error {
+ r := p.peek()
+ if r == '+' || r == '-' {
+ p.next()
+ }
+ for {
+ r = p.next()
+ if r != '.' && !unicode.IsDigit(r) {
+ p.backup()
+ break
+ }
+ }
+ value := p.consumeText()
+ i, err := strconv.Atoi(value)
+ if err == nil {
+ cur.append(newInt(i))
+ return p.parseInsideAction(cur)
+ }
+ d, err := strconv.ParseFloat(value, 64)
+ if err == nil {
+ cur.append(newFloat(d))
+ return p.parseInsideAction(cur)
+ }
+ return fmt.Errorf("cannot parse number %s", value)
+}
+
+// parseArray scans array index selection
+func (p *Parser) parseArray(cur *ListNode) error {
+Loop:
+ for {
+ switch p.next() {
+ case eof, '\n':
+ return fmt.Errorf("unterminated array")
+ case ']':
+ break Loop
+ }
+ }
+ text := p.consumeText()
+ text = text[1 : len(text)-1]
+ if text == "*" {
+ text = ":"
+ }
+
+ //union operator
+ strs := strings.Split(text, ",")
+ if len(strs) > 1 {
+ union := []*ListNode{}
+ for _, str := range strs {
+ parser, err := parseAction("union", fmt.Sprintf("[%s]", strings.Trim(str, " ")))
+ if err != nil {
+ return err
+ }
+ union = append(union, parser.Root)
+ }
+ cur.append(newUnion(union))
+ return p.parseInsideAction(cur)
+ }
+
+ // dict key
+ value := dictKeyRex.FindStringSubmatch(text)
+ if value != nil {
+ parser, err := parseAction("arraydict", fmt.Sprintf(".%s", value[1]))
+ if err != nil {
+ return err
+ }
+ for _, node := range parser.Root.Nodes {
+ cur.append(node)
+ }
+ return p.parseInsideAction(cur)
+ }
+
+ //slice operator
+ value = sliceOperatorRex.FindStringSubmatch(text)
+ if value == nil {
+ return fmt.Errorf("invalid array index %s", text)
+ }
+ value = value[1:]
+ params := [3]ParamsEntry{}
+ for i := 0; i < 3; i++ {
+ if value[i] != "" {
+ if i > 0 {
+ value[i] = value[i][1:]
+ }
+ if i > 0 && value[i] == "" {
+ params[i].Known = false
+ } else {
+ var err error
+ params[i].Known = true
+ params[i].Value, err = strconv.Atoi(value[i])
+ if err != nil {
+ return fmt.Errorf("array index %s is not a number", value[i])
+ }
+ }
+ } else {
+ if i == 1 {
+ params[i].Known = true
+ params[i].Value = params[0].Value + 1
+ params[i].Derived = true
+ } else {
+ params[i].Known = false
+ params[i].Value = 0
+ }
+ }
+ }
+ cur.append(newArray(params))
+ return p.parseInsideAction(cur)
+}
+
+// parseFilter scans filter inside array selection
+func (p *Parser) parseFilter(cur *ListNode) error {
+ p.pos += len("[?(")
+ p.consumeText()
+ begin := false
+ end := false
+ var pair rune
+
+Loop:
+ for {
+ r := p.next()
+ switch r {
+ case eof, '\n':
+ return fmt.Errorf("unterminated filter")
+ case '"', '\'':
+ if !begin {
+ //save the paired rune
+ begin = true
+ pair = r
+ continue
+ }
+ //only add when met paired rune
+ if p.input[p.pos-2] != '\\' && r == pair {
+ end = true
+ }
+ case ')':
+ //in rightParser below quotes only appear zero or once
+ //and must be paired at the beginning and end
+ if begin == end {
+ break Loop
+ }
+ }
+ }
+ if p.next() != ']' {
+ return fmt.Errorf("unclosed array expect ]")
+ }
+ reg := regexp.MustCompile(`^([^!<>=]+)([!<>=]+)(.+?)$`)
+ text := p.consumeText()
+ text = text[:len(text)-2]
+ value := reg.FindStringSubmatch(text)
+ if value == nil {
+ parser, err := parseAction("text", text)
+ if err != nil {
+ return err
+ }
+ cur.append(newFilter(parser.Root, newList(), "exists"))
+ } else {
+ leftParser, err := parseAction("left", value[1])
+ if err != nil {
+ return err
+ }
+ rightParser, err := parseAction("right", value[3])
+ if err != nil {
+ return err
+ }
+ cur.append(newFilter(leftParser.Root, rightParser.Root, value[2]))
+ }
+ return p.parseInsideAction(cur)
+}
+
+// parseQuote unquotes string inside double or single quote
+func (p *Parser) parseQuote(cur *ListNode, end rune) error {
+Loop:
+ for {
+ switch p.next() {
+ case eof, '\n':
+ return fmt.Errorf("unterminated quoted string")
+ case end:
+ //if it's not escape break the Loop
+ if p.input[p.pos-2] != '\\' {
+ break Loop
+ }
+ }
+ }
+ value := p.consumeText()
+ s, err := UnquoteExtend(value)
+ if err != nil {
+ return fmt.Errorf("unquote string %s error %v", value, err)
+ }
+ cur.append(newText(s))
+ return p.parseInsideAction(cur)
+}
+
+// parseField scans a field until a terminator
+func (p *Parser) parseField(cur *ListNode) error {
+ p.consumeText()
+ for p.advance() {
+ }
+ value := p.consumeText()
+ if value == "*" {
+ cur.append(newWildcard())
+ } else {
+ cur.append(newField(strings.Replace(value, "\\", "", -1)))
+ }
+ return p.parseInsideAction(cur)
+}
+
+// advance scans until next non-escaped terminator
+func (p *Parser) advance() bool {
+ r := p.next()
+ if r == '\\' {
+ p.next()
+ } else if isTerminator(r) {
+ p.backup()
+ return false
+ }
+ return true
+}
+
+// isTerminator reports whether the input is at valid termination character to appear after an identifier.
+func isTerminator(r rune) bool {
+ if isSpace(r) || isEndOfLine(r) {
+ return true
+ }
+ switch r {
+ case eof, '.', ',', '[', ']', '$', '@', '{', '}':
+ return true
+ }
+ return false
+}
+
+// isSpace reports whether r is a space character.
+func isSpace(r rune) bool {
+ return r == ' ' || r == '\t'
+}
+
+// isEndOfLine reports whether r is an end-of-line character.
+func isEndOfLine(r rune) bool {
+ return r == '\r' || r == '\n'
+}
+
+// isAlphaNumeric reports whether r is an alphabetic, digit, or underscore.
+func isAlphaNumeric(r rune) bool {
+ return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
+}
+
+// isBool reports whether s is a boolean value.
+func isBool(s string) bool {
+ return s == "true" || s == "false"
+}
+
+// UnquoteExtend is almost same as strconv.Unquote(), but it support parse single quotes as a string
+func UnquoteExtend(s string) (string, error) {
+ n := len(s)
+ if n < 2 {
+ return "", ErrSyntax
+ }
+ quote := s[0]
+ if quote != s[n-1] {
+ return "", ErrSyntax
+ }
+ s = s[1 : n-1]
+
+ if quote != '"' && quote != '\'' {
+ return "", ErrSyntax
+ }
+
+ // Is it trivial? Avoid allocation.
+ if !contains(s, '\\') && !contains(s, quote) {
+ return s, nil
+ }
+
+ var runeTmp [utf8.UTFMax]byte
+ buf := make([]byte, 0, 3*len(s)/2) // Try to avoid more allocations.
+ for len(s) > 0 {
+ c, multibyte, ss, err := strconv.UnquoteChar(s, quote)
+ if err != nil {
+ return "", err
+ }
+ s = ss
+ if c < utf8.RuneSelf || !multibyte {
+ buf = append(buf, byte(c))
+ } else {
+ n := utf8.EncodeRune(runeTmp[:], c)
+ buf = append(buf, runeTmp[:n]...)
+ }
+ }
+ return string(buf), nil
+}
+
+func contains(s string, c byte) bool {
+ for i := 0; i < len(s); i++ {
+ if s[i] == c {
+ return true
+ }
+ }
+ return false
+}
diff --git a/internal/util/jsonpath/parser_test.go b/internal/util/jsonpath/parser_test.go
new file mode 100644
index 0000000..96b1120
--- /dev/null
+++ b/internal/util/jsonpath/parser_test.go
@@ -0,0 +1,158 @@
+/*
+Copyright 2015 The Kubernetes Authors.
+
+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.
+*/
+
+package jsonpath
+
+import (
+ "testing"
+)
+
+type parserTest struct {
+ name string
+ text string
+ nodes []Node
+ shouldError bool
+}
+
+var parserTests = []parserTest{
+ {"plain", `hello jsonpath`, []Node{newText("hello jsonpath")}, false},
+ {"variable", `hello {.jsonpath}`,
+ []Node{newText("hello "), newList(), newField("jsonpath")}, false},
+ {"arrayfiled", `hello {['jsonpath']}`,
+ []Node{newText("hello "), newList(), newField("jsonpath")}, false},
+ {"quote", `{"{"}`, []Node{newList(), newText("{")}, false},
+ {"array", `{[1:3]}`, []Node{newList(),
+ newArray([3]ParamsEntry{{1, true, false}, {3, true, false}, {0, false, false}})}, false},
+ {"allarray", `{.book[*].author}`,
+ []Node{newList(), newField("book"),
+ newArray([3]ParamsEntry{{0, false, false}, {0, false, false}, {0, false, false}}), newField("author")}, false},
+ {"wildcard", `{.bicycle.*}`,
+ []Node{newList(), newField("bicycle"), newWildcard()}, false},
+ {"filter", `{[?(@.price<3)]}`,
+ []Node{newList(), newFilter(newList(), newList(), "<"),
+ newList(), newField("price"), newList(), newInt(3)}, false},
+ {"recursive", `{..}`, []Node{newList(), newRecursive()}, false},
+ {"recurField", `{..price}`,
+ []Node{newList(), newRecursive(), newField("price")}, false},
+ {"arraydict", `{['book.price']}`, []Node{newList(),
+ newField("book"), newField("price"),
+ }, false},
+ {"union", `{['bicycle.price', 3, 'book.price']}`, []Node{newList(), newUnion([]*ListNode{}),
+ newList(), newField("bicycle"), newField("price"),
+ newList(), newArray([3]ParamsEntry{{3, true, false}, {4, true, true}, {0, false, false}}),
+ newList(), newField("book"), newField("price"),
+ }, false},
+ {"range", `{range .items}{.name},{end}`, []Node{
+ newList(), newIdentifier("range"), newField("items"),
+ newList(), newField("name"), newText(","),
+ newList(), newIdentifier("end"),
+ }, false},
+ {"malformat input", `{\\\}`, []Node{}, true},
+ {"paired parentheses in quotes", `{[?(@.status.nodeInfo.osImage == "()")]}`,
+ []Node{newList(), newFilter(newList(), newList(), "=="), newList(), newField("status"), newField("nodeInfo"), newField("osImage"), newList(), newText("()")}, false},
+ {"paired parentheses in double quotes and with double quotes escape", `{[?(@.status.nodeInfo.osImage == "(\"\")")]}`,
+ []Node{newList(), newFilter(newList(), newList(), "=="), newList(), newField("status"), newField("nodeInfo"), newField("osImage"), newList(), newText("(\"\")")}, false},
+ {"unregular parentheses in double quotes", `{[?(@.test == "())(")]}`,
+ []Node{newList(), newFilter(newList(), newList(), "=="), newList(), newField("test"), newList(), newText("())(")}, false},
+ {"plain text in single quotes", `{[?(@.status.nodeInfo.osImage == 'Linux')]}`,
+ []Node{newList(), newFilter(newList(), newList(), "=="), newList(), newField("status"), newField("nodeInfo"), newField("osImage"), newList(), newText("Linux")}, false},
+ {"test filter suffix", `{[?(@.status.nodeInfo.osImage == "{[()]}")]}`,
+ []Node{newList(), newFilter(newList(), newList(), "=="), newList(), newField("status"), newField("nodeInfo"), newField("osImage"), newList(), newText("{[()]}")}, false},
+ {"double inside single", `{[?(@.status.nodeInfo.osImage == "''")]}`,
+ []Node{newList(), newFilter(newList(), newList(), "=="), newList(), newField("status"), newField("nodeInfo"), newField("osImage"), newList(), newText("''")}, false},
+ {"single inside double", `{[?(@.status.nodeInfo.osImage == '""')]}`,
+ []Node{newList(), newFilter(newList(), newList(), "=="), newList(), newField("status"), newField("nodeInfo"), newField("osImage"), newList(), newText("\"\"")}, false},
+ {"single containing escaped single", `{[?(@.status.nodeInfo.osImage == '\\\'')]}`,
+ []Node{newList(), newFilter(newList(), newList(), "=="), newList(), newField("status"), newField("nodeInfo"), newField("osImage"), newList(), newText("\\'")}, false},
+ {"negative index slice, equals a[len-5] to a[len-1]", `{[-5:]}`, []Node{newList(),
+ newArray([3]ParamsEntry{{-5, true, false}, {0, false, false}, {0, false, false}})}, false},
+ {"negative index slice, equals a[len-1]", `{[-1]}`, []Node{newList(),
+ newArray([3]ParamsEntry{{-1, true, false}, {0, true, true}, {0, false, false}})}, false},
+ {"negative index slice, equals a[1] to a[len-1]", `{[1:-1]}`, []Node{newList(),
+ newArray([3]ParamsEntry{{1, true, false}, {-1, true, false}, {0, false, false}})}, false},
+}
+
+func collectNode(nodes []Node, cur Node) []Node {
+ nodes = append(nodes, cur)
+ switch cur.Type() {
+ case NodeList:
+ for _, node := range cur.(*ListNode).Nodes {
+ nodes = collectNode(nodes, node)
+ }
+ case NodeFilter:
+ nodes = collectNode(nodes, cur.(*FilterNode).Left)
+ nodes = collectNode(nodes, cur.(*FilterNode).Right)
+ case NodeUnion:
+ for _, node := range cur.(*UnionNode).Nodes {
+ nodes = collectNode(nodes, node)
+ }
+ }
+ return nodes
+}
+
+func TestParser(t *testing.T) {
+ for _, test := range parserTests {
+ parser, err := Parse(test.name, test.text)
+ if test.shouldError {
+ if err == nil {
+ t.Errorf("unexpected non-error when parsing %s", test.name)
+ }
+ continue
+ }
+ if err != nil {
+ t.Errorf("parse %s error %v", test.name, err)
+ }
+ result := collectNode([]Node{}, parser.Root)[1:]
+ if len(result) != len(test.nodes) {
+ t.Errorf("in %s, expect to get %d nodes, got %d nodes", test.name, len(test.nodes), len(result))
+ t.Error(result)
+ }
+ for i, expect := range test.nodes {
+ if result[i].String() != expect.String() {
+ t.Errorf("in %s, %dth node, expect %v, got %v", test.name, i, expect, result[i])
+ }
+ }
+ }
+}
+
+type failParserTest struct {
+ name string
+ text string
+ err string
+}
+
+func TestFailParser(t *testing.T) {
+ failParserTests := []failParserTest{
+ {"unclosed action", "{.hello", "unclosed action"},
+ {"unrecognized character", "{*}", "unrecognized character in action: U+002A '*'"},
+ {"invalid number", "{+12.3.0}", "cannot parse number +12.3.0"},
+ {"unterminated array", "{[1}", "unterminated array"},
+ {"unterminated filter", "{[?(.price]}", "unterminated filter"},
+ {"invalid multiple recursive descent", "{........}", "invalid multiple recursive descent"},
+ }
+ for _, test := range failParserTests {
+ _, err := Parse(test.name, test.text)
+ var out string
+ if err == nil {
+ out = "nil"
+ } else {
+ out = err.Error()
+ }
+ if out != test.err {
+ t.Errorf("in %s, expect to get error %v, got %v", test.name, test.err, out)
+ }
+ }
+}
diff --git a/internal/util/jsonpath/template/exec.go b/internal/util/jsonpath/template/exec.go
new file mode 100644
index 0000000..ed66f84
--- /dev/null
+++ b/internal/util/jsonpath/template/exec.go
@@ -0,0 +1,52 @@
+// This package is copied from Go library text/template.
+// The original private functions indirect and printableValue
+// are exported as public functions.
+package template
+
+import (
+ "fmt"
+ "reflect"
+)
+
+var (
+ errorType = reflect.TypeOf((*error)(nil)).Elem()
+ fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
+)
+
+// Indirect returns the item at the end of indirection, and a bool to indicate if it's nil.
+// We indirect through pointers and empty interfaces (only) because
+// non-empty interfaces have methods we might need.
+func Indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
+ for ; v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface; v = v.Elem() {
+ if v.IsNil() {
+ return v, true
+ }
+ if v.Kind() == reflect.Interface && v.NumMethod() > 0 {
+ break
+ }
+ }
+ return v, false
+}
+
+// PrintableValue returns the, possibly indirected, interface value inside v that
+// is best for a call to formatted printer.
+func PrintableValue(v reflect.Value) (interface{}, bool) {
+ if v.Kind() == reflect.Pointer {
+ v, _ = Indirect(v) // fmt.Fprint handles nil.
+ }
+ if !v.IsValid() {
+ return "", true
+ }
+
+ if !v.Type().Implements(errorType) && !v.Type().Implements(fmtStringerType) {
+ if v.CanAddr() && (reflect.PointerTo(v.Type()).Implements(errorType) || reflect.PointerTo(v.Type()).Implements(fmtStringerType)) {
+ v = v.Addr()
+ } else {
+ switch v.Kind() {
+ case reflect.Chan, reflect.Func:
+ return nil, false
+ }
+ }
+ }
+ return v.Interface(), true
+}
diff --git a/internal/util/jsonpath/template/funcs.go b/internal/util/jsonpath/template/funcs.go
new file mode 100644
index 0000000..94c396c
--- /dev/null
+++ b/internal/util/jsonpath/template/funcs.go
@@ -0,0 +1,177 @@
+// This package is copied from Go library text/template.
+// The original private functions eq, ge, gt, le, lt, and ne
+// are exported as public functions.
+package template
+
+import (
+ "errors"
+ "reflect"
+)
+
+var (
+ errBadComparisonType = errors.New("invalid type for comparison")
+ errBadComparison = errors.New("incompatible types for comparison")
+ errNoComparison = errors.New("missing argument for comparison")
+)
+
+type kind int
+
+const (
+ invalidKind kind = iota
+ boolKind
+ complexKind
+ intKind
+ floatKind
+ integerKind
+ stringKind
+ uintKind
+)
+
+func basicKind(v reflect.Value) (kind, error) {
+ switch v.Kind() {
+ case reflect.Bool:
+ return boolKind, nil
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ return intKind, nil
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+ return uintKind, nil
+ case reflect.Float32, reflect.Float64:
+ return floatKind, nil
+ case reflect.Complex64, reflect.Complex128:
+ return complexKind, nil
+ case reflect.String:
+ return stringKind, nil
+ }
+ return invalidKind, errBadComparisonType
+}
+
+// Equal evaluates the comparison a == b || a == c || ...
+func Equal(arg1 interface{}, arg2 ...interface{}) (bool, error) {
+ v1 := reflect.ValueOf(arg1)
+ k1, err := basicKind(v1)
+ if err != nil {
+ return false, err
+ }
+ if len(arg2) == 0 {
+ return false, errNoComparison
+ }
+ for _, arg := range arg2 {
+ v2 := reflect.ValueOf(arg)
+ k2, err := basicKind(v2)
+ if err != nil {
+ return false, err
+ }
+ truth := false
+ if k1 != k2 {
+ // Special case: Can compare integer values regardless of type's sign.
+ switch {
+ case k1 == intKind && k2 == uintKind:
+ truth = v1.Int() >= 0 && uint64(v1.Int()) == v2.Uint()
+ case k1 == uintKind && k2 == intKind:
+ truth = v2.Int() >= 0 && v1.Uint() == uint64(v2.Int())
+ default:
+ return false, errBadComparison
+ }
+ } else {
+ switch k1 {
+ case boolKind:
+ truth = v1.Bool() == v2.Bool()
+ case complexKind:
+ truth = v1.Complex() == v2.Complex()
+ case floatKind:
+ truth = v1.Float() == v2.Float()
+ case intKind:
+ truth = v1.Int() == v2.Int()
+ case stringKind:
+ truth = v1.String() == v2.String()
+ case uintKind:
+ truth = v1.Uint() == v2.Uint()
+ default:
+ panic("invalid kind")
+ }
+ }
+ if truth {
+ return true, nil
+ }
+ }
+ return false, nil
+}
+
+// NotEqual evaluates the comparison a != b.
+func NotEqual(arg1, arg2 interface{}) (bool, error) {
+ // != is the inverse of ==.
+ equal, err := Equal(arg1, arg2)
+ return !equal, err
+}
+
+// Less evaluates the comparison a < b.
+func Less(arg1, arg2 interface{}) (bool, error) {
+ v1 := reflect.ValueOf(arg1)
+ k1, err := basicKind(v1)
+ if err != nil {
+ return false, err
+ }
+ v2 := reflect.ValueOf(arg2)
+ k2, err := basicKind(v2)
+ if err != nil {
+ return false, err
+ }
+ truth := false
+ if k1 != k2 {
+ // Special case: Can compare integer values regardless of type's sign.
+ switch {
+ case k1 == intKind && k2 == uintKind:
+ truth = v1.Int() < 0 || uint64(v1.Int()) < v2.Uint()
+ case k1 == uintKind && k2 == intKind:
+ truth = v2.Int() >= 0 && v1.Uint() < uint64(v2.Int())
+ default:
+ return false, errBadComparison
+ }
+ } else {
+ switch k1 {
+ case boolKind, complexKind:
+ return false, errBadComparisonType
+ case floatKind:
+ truth = v1.Float() < v2.Float()
+ case intKind:
+ truth = v1.Int() < v2.Int()
+ case stringKind:
+ truth = v1.String() < v2.String()
+ case uintKind:
+ truth = v1.Uint() < v2.Uint()
+ default:
+ panic("invalid kind")
+ }
+ }
+ return truth, nil
+}
+
+// LessEqual evaluates the comparison <= b.
+func LessEqual(arg1, arg2 interface{}) (bool, error) {
+ // <= is < or ==.
+ lessThan, err := Less(arg1, arg2)
+ if lessThan || err != nil {
+ return lessThan, err
+ }
+ return Equal(arg1, arg2)
+}
+
+// Greater evaluates the comparison a > b.
+func Greater(arg1, arg2 interface{}) (bool, error) {
+ // > is the inverse of <=.
+ lessOrEqual, err := LessEqual(arg1, arg2)
+ if err != nil {
+ return false, err
+ }
+ return !lessOrEqual, nil
+}
+
+// GreaterEqual evaluates the comparison a >= b.
+func GreaterEqual(arg1, arg2 interface{}) (bool, error) {
+ // >= is the inverse of <.
+ lessThan, err := Less(arg1, arg2)
+ if err != nil {
+ return false, err
+ }
+ return !lessThan, nil
+}
diff --git a/internal/util/logging.go b/internal/util/logging.go
new file mode 100644
index 0000000..9d1d4c4
--- /dev/null
+++ b/internal/util/logging.go
@@ -0,0 +1,40 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+/*
+ * Copyright (C) 2024 Damian Peckett .
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package util
+
+import (
+ "log/slog"
+ "strings"
+)
+
+// LevelFlag is a urfave/cli compatible flag for setting the log verbosity level.
+type LevelFlag slog.Level
+
+func FromSlogLevel(l slog.Level) *LevelFlag {
+ f := LevelFlag(l)
+ return &f
+}
+
+func (f *LevelFlag) Set(value string) error {
+ return (*slog.Level)(f).UnmarshalText([]byte(strings.ToUpper(value)))
+}
+
+func (f *LevelFlag) String() string {
+ return (*slog.Level)(f).String()
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..18245ae
--- /dev/null
+++ b/main.go
@@ -0,0 +1,191 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+/*
+ * Copyright (C) 2024 Damian Peckett .
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package main
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+ "os"
+ goruntime "runtime"
+ "time"
+
+ "github.com/dpeckett/airgapify/api/v1alpha1"
+ airgapifyv1alpha1 "github.com/dpeckett/airgapify/api/v1alpha1"
+ "github.com/dpeckett/airgapify/internal/archive"
+ "github.com/dpeckett/airgapify/internal/constants"
+ "github.com/dpeckett/airgapify/internal/extractor"
+ "github.com/dpeckett/airgapify/internal/loader"
+ "github.com/dpeckett/airgapify/internal/util"
+ "github.com/dpeckett/telemetry"
+ telemetryv1alpha1 "github.com/dpeckett/telemetry/v1alpha1"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/urfave/cli/v2"
+ "k8s.io/apimachinery/pkg/runtime"
+)
+
+func main() {
+ persistentFlags := []cli.Flag{
+ &cli.GenericFlag{
+ Name: "log-level",
+ Usage: "Set the log verbosity level",
+ Value: util.FromSlogLevel(slog.LevelInfo),
+ },
+ }
+
+ initLogger := func(c *cli.Context) error {
+ slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
+ Level: (*slog.Level)(c.Generic("log-level").(*util.LevelFlag)),
+ })))
+
+ return nil
+ }
+
+ // Collect anonymized usage statistics.
+ var telemetryReporter *telemetry.Reporter
+
+ initTelemetry := func(c *cli.Context) error {
+ telemetryReporter = telemetry.NewReporter(c.Context, slog.Default(), telemetry.Configuration{
+ BaseURL: constants.TelemetryURL,
+ Tags: []string{"airgapify"},
+ })
+
+ // Some basic system information.
+ info := map[string]string{
+ "os": goruntime.GOOS,
+ "arch": goruntime.GOARCH,
+ "num_cpu": fmt.Sprintf("%d", goruntime.NumCPU()),
+ "version": constants.Version,
+ }
+
+ telemetryReporter.ReportEvent(&telemetryv1alpha1.TelemetryEvent{
+ Kind: telemetryv1alpha1.TelemetryEventKindInfo,
+ Name: "ApplicationStart",
+ Values: info,
+ })
+
+ return nil
+ }
+
+ shutdownTelemetry := func(c *cli.Context) error {
+ if telemetryReporter == nil {
+ return nil
+ }
+
+ telemetryReporter.ReportEvent(&telemetryv1alpha1.TelemetryEvent{
+ Kind: telemetryv1alpha1.TelemetryEventKindInfo,
+ Name: "ApplicationStop",
+ })
+
+ // Don't want to block the shutdown of the application for too long.
+ ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
+ defer cancel()
+
+ if err := telemetryReporter.Shutdown(ctx); err != nil {
+ slog.Error("Failed to close telemetry reporter", slog.Any("error", err))
+ }
+
+ return nil
+ }
+
+ app := &cli.App{
+ Name: "airgapify",
+ Usage: "A little tool that will construct an OCI image archive from a set of Kubernetes manifests.",
+ Flags: append([]cli.Flag{
+ &cli.StringSliceFlag{
+ Name: "file",
+ Aliases: []string{"f"},
+ Usage: "Path to one or more Kubernetes manifests.",
+ Required: true,
+ },
+ &cli.StringFlag{
+ Name: "output",
+ Aliases: []string{"o"},
+ Usage: "Where to write the OCI image archive (optionally compressed).",
+ Value: "images.tar",
+ },
+ &cli.StringFlag{
+ Name: "platform",
+ Aliases: []string{"p"},
+ Usage: "The target platform for the image archive.",
+ },
+ }, persistentFlags...),
+ Before: util.BeforeAll(initLogger, initTelemetry),
+ After: shutdownTelemetry,
+ Action: func(c *cli.Context) error {
+ objects, err := loader.LoadObjectsFromFiles(c.StringSlice("file"))
+ if err != nil {
+ return fmt.Errorf("failed to load objects: %w", err)
+ }
+
+ slog.Info("Loaded objects", "count", len(objects))
+
+ rules := extractor.DefaultRules
+
+ for _, obj := range objects {
+ if obj.GetAPIVersion() == v1alpha1.GroupVersion.String() && obj.GetKind() == "Config" {
+ slog.Info("Found airgapify config")
+
+ var config airgapifyv1alpha1.Config
+ err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &config)
+ if err != nil {
+ return fmt.Errorf("failed to convert config: %w", err)
+ }
+
+ for _, rule := range config.Spec.Rules {
+ rules = append(rules, extractor.ImageReferenceExtractionRule{
+ TypeMeta: rule.TypeMeta,
+ Paths: rule.Paths,
+ })
+ }
+ }
+ }
+
+ e := extractor.NewImageReferenceExtractor(rules)
+ images, err := e.ExtractImageReferences(objects)
+ if err != nil {
+ return fmt.Errorf("failed to extract image references: %w", err)
+ }
+
+ if images.Len() > 0 {
+ slog.Info("Found image references", "count", images.Len())
+ }
+
+ var platform *v1.Platform
+ if c.IsSet("platform") {
+ platform, err = v1.ParsePlatform(c.String("platform"))
+ if err != nil {
+ return fmt.Errorf("failed to parse platform: %w", err)
+ }
+ }
+
+ outputPath := c.String("output")
+ if err := archive.Create(c.Context, outputPath, images, platform); err != nil {
+ return fmt.Errorf("failed to create image archive: %w", err)
+ }
+
+ return nil
+ },
+ }
+
+ if err := app.Run(os.Args); err != nil {
+ slog.Error("Error", slog.Any("error", err))
+ os.Exit(1)
+ }
+}