mirror of
https://github.com/hauler-dev/hauler.git
synced 2026-03-01 09:10:23 +00:00
Compare commits
20 Commits
v1.0.3
...
v0.2.2-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a313e2f77d | ||
|
|
a3a2329a03 | ||
|
|
c360a193f4 | ||
|
|
0df5f85d44 | ||
|
|
a105782aa4 | ||
|
|
11806972b4 | ||
|
|
d682c03170 | ||
|
|
41e4d25969 | ||
|
|
cec4d8474c | ||
|
|
5cb6a5ef60 | ||
|
|
2165c54508 | ||
|
|
d90bd6152b | ||
|
|
90cc646ff3 | ||
|
|
5127843a0b | ||
|
|
c7886132f8 | ||
|
|
2ecc5a8b14 | ||
|
|
a836650d62 | ||
|
|
c76dd705e5 | ||
|
|
77560b1442 | ||
|
|
a22455c6df |
@@ -1,8 +0,0 @@
|
||||
*
|
||||
!cmd
|
||||
!go.mod
|
||||
!go.sum
|
||||
!internal
|
||||
!Makefile
|
||||
!pkg
|
||||
!static
|
||||
56
.github/ISSUE_TEMPLATE/bug_report.md
vendored
56
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,51 +1,31 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Create a report to help us improve!
|
||||
title: '[BUG]'
|
||||
labels: 'bug'
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Thank you for helping us to improve Hauler! We welcome all bug reports. Please fill out each area of the template so we can better assist you. Comments like this will be hidden when you submit, but you can delete them if you wish. -->
|
||||
<!-- Thanks for helping us to improve Hauler! We welcome all bug reports. Please fill out each area of the template so we can better help you. Comments like this will be hidden when you post but you can delete them if you wish. -->
|
||||
|
||||
**Environmental Info:**
|
||||
|
||||
<!-- Provide the output of "uname -a" -->
|
||||
|
||||
-
|
||||
**Environmental Info:**
|
||||
|
||||
**Hauler Version:**
|
||||
|
||||
<!-- Provide the output of "hauler version" -->
|
||||
**System CPU architecture, OS, and Version:**
|
||||
<!-- Provide the output from "uname -a" on the system where Hauler is installed -->
|
||||
|
||||
-
|
||||
**Describe the bug:**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**Describe the Bug:**
|
||||
**Steps To Reproduce:**
|
||||
|
||||
<!-- Provide a clear and concise description of the bug -->
|
||||
**Expected behavior:**
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
-
|
||||
**Actual behavior:**
|
||||
<!-- A clear and concise description of what actually happened. -->
|
||||
|
||||
**Steps to Reproduce:**
|
||||
|
||||
<!-- Provide a clear and concise way to reproduce the bug -->
|
||||
|
||||
-
|
||||
|
||||
**Expected Behavior:**
|
||||
|
||||
<!-- Provide a clear and concise description of what you expected to happen -->
|
||||
|
||||
-
|
||||
|
||||
**Actual Behavior:**
|
||||
|
||||
<!-- Provide a clear and concise description of what actually happens -->
|
||||
|
||||
-
|
||||
|
||||
**Additional Context:**
|
||||
|
||||
<!-- Provide any other context and/or logs about the bug -->
|
||||
|
||||
-
|
||||
**Additional context / logs:**
|
||||
<!-- Add any other context and/or logs about the problem here. -->
|
||||
|
||||
33
.github/ISSUE_TEMPLATE/feature_request.md
vendored
33
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,33 +0,0 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Submit a request for us to improve!
|
||||
title: '[RFE]'
|
||||
labels: 'enhancement'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
<!-- Thank you for helping us to improve Hauler! We welcome all requests for enhancements (RFEs). Please fill out each area of the template so we can better assist you. Comments like this will be hidden when you submit, but you can delete them if you wish. -->
|
||||
|
||||
**Is this RFE related to an Existing Problem? If so, please describe:**
|
||||
|
||||
<!-- Provide a clear and concise description of the problem -->
|
||||
|
||||
-
|
||||
|
||||
**Describe Proposed Solution(s):**
|
||||
|
||||
<!-- Provide a clear and concise description of what you want to happen -->
|
||||
|
||||
-
|
||||
|
||||
**Describe Possible Alternatives:**
|
||||
|
||||
<!-- Provide a clear and concise description of any alternative solutions or features you've considered -->
|
||||
|
||||
-
|
||||
|
||||
**Additional Context:**
|
||||
|
||||
<!-- Provide a clear and concise description of the problem -->
|
||||
|
||||
-
|
||||
31
.github/PULL_REQUEST_TEMPLATE.md
vendored
31
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,36 +1,23 @@
|
||||
**Please check below, if the PR fulfills these requirements:**
|
||||
- [ ] Commit(s) and code follow the repositories guidelines.
|
||||
- [ ] Test(s) have been added or updated to support these change(s).
|
||||
- [ ] Doc(s) have been added or updated to support these change(s).
|
||||
* **Please check if the PR fulfills these requirements**
|
||||
- [ ] The commit message follows our guidelines
|
||||
- [ ] Tests for the changes have been added (for bug fixes / features)
|
||||
- [ ] Docs have been added / updated (for bug fixes / features)
|
||||
|
||||
<!-- Comments like this will be hidden when you submit, but you can delete them if you wish. -->
|
||||
|
||||
**Associated Links:**
|
||||
* **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...)
|
||||
|
||||
<!-- Provide any associated or linked related to these change(s) -->
|
||||
|
||||
-
|
||||
|
||||
**Types of Changes:**
|
||||
* **What is the current behavior?** (You can also link to an open issue here)
|
||||
|
||||
<!-- What is the type of change? Bugfix, Feature, Breaking Change, etc... -->
|
||||
|
||||
-
|
||||
|
||||
**Proposed Changes:**
|
||||
* **What is the new behavior (if this is a feature change)?**
|
||||
|
||||
<!-- Provide the high level and low level description of your change(s) so we can better understand these change(s) -->
|
||||
|
||||
-
|
||||
|
||||
**Verification/Testing of Changes:**
|
||||
* **Does this PR introduce a breaking change?** (What changes might users need to make in their application due to this PR?)
|
||||
|
||||
<!-- How can the changes be verified? Provide the steps necessary to reproduce and verify the proposed change(s) -->
|
||||
|
||||
-
|
||||
|
||||
**Additional Context:**
|
||||
|
||||
<!-- Provide any additional information, such as if this is a small or large or complex change. Feel free to kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... -->
|
||||
|
||||
-
|
||||
* **Other information**:
|
||||
32
.github/workflows/ci.yaml
vendored
Normal file
32
.github/workflows/ci.yaml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.17.x
|
||||
-
|
||||
name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
|
||||
64
.github/workflows/release.yaml
vendored
64
.github/workflows/release.yaml
vendored
@@ -1,64 +0,0 @@
|
||||
name: Release Workflow
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
go-release:
|
||||
name: Go Release Job
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set Up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.x
|
||||
|
||||
- name: Run Go Releaser
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean -p 1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
|
||||
|
||||
container-release:
|
||||
name: Container Release Job
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set Up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push to GitHub Container Registry
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
|
||||
37
.github/workflows/unittest.yaml
vendored
37
.github/workflows/unittest.yaml
vendored
@@ -1,37 +0,0 @@
|
||||
name: Unit Test Workflow
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
unit-test:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set Up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.x
|
||||
|
||||
- name: Run Unit Tests
|
||||
run: |
|
||||
mkdir -p cmd/hauler/binaries
|
||||
touch cmd/hauler/binaries/dummy.txt
|
||||
go test -race -covermode=atomic -coverprofile=coverage.out ./pkg/... ./internal/... ./cmd/...
|
||||
|
||||
- name: On Failure, Launch Debug Session
|
||||
if: ${{ failure() }}
|
||||
uses: mxschmitt/action-tmate@v3
|
||||
timeout-minutes: 10
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -18,10 +18,13 @@ artifacts
|
||||
local-artifacts
|
||||
airgap-scp.sh
|
||||
|
||||
# test artifacts
|
||||
*.tar*
|
||||
|
||||
# generated
|
||||
dist/
|
||||
./bundle/
|
||||
tmp/
|
||||
bin/
|
||||
/store/
|
||||
/registry/
|
||||
cmd/hauler/binaries
|
||||
pkg.yaml
|
||||
haul/
|
||||
@@ -3,14 +3,9 @@ before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
- go mod download
|
||||
- rm -rf cmd/hauler/binaries
|
||||
|
||||
release:
|
||||
prerelease: auto
|
||||
|
||||
env:
|
||||
- vpkg=github.com/rancherfederal/hauler/internal/version
|
||||
- cosign_version=v2.2.3+carbide.2
|
||||
- vpkg=github.com/rancherfederal/hauler/pkg/version
|
||||
|
||||
builds:
|
||||
- main: cmd/hauler/main.go
|
||||
@@ -22,18 +17,12 @@ builds:
|
||||
- amd64
|
||||
- arm64
|
||||
ldflags:
|
||||
- -s -w -X {{ .Env.vpkg }}.gitVersion={{ .Version }} -X {{ .Env.vpkg }}.gitCommit={{ .ShortCommit }} -X {{ .Env.vpkg }}.gitTreeState={{if .IsGitDirty}}dirty{{else}}clean{{end}} -X {{ .Env.vpkg }}.buildDate={{ .Date }}
|
||||
hooks:
|
||||
pre:
|
||||
- mkdir -p cmd/hauler/binaries
|
||||
- wget -P cmd/hauler/binaries/ https://github.com/rancher-government-carbide/cosign/releases/download/{{ .Env.cosign_version }}/cosign-{{ .Os }}-{{ .Arch }}{{ if eq .Os "windows" }}.exe{{ end }}
|
||||
post:
|
||||
- rm -rf cmd/hauler/binaries
|
||||
- -s -w -X {{ .Env.vpkg }}.GitVersion={{ .Version }} -X {{ .Env.vpkg }}.commit={{ .ShortCommit }} -X {{ .Env.vpkg }}.buildDate={{ .Date }}
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
|
||||
universal_binaries:
|
||||
- replace: false
|
||||
- replace: true
|
||||
|
||||
changelog:
|
||||
skip: false
|
||||
|
||||
25
Dockerfile
25
Dockerfile
@@ -1,25 +0,0 @@
|
||||
FROM registry.suse.com/bci/golang:1.21 AS builder
|
||||
RUN zypper --non-interactive install make bash wget ca-certificates
|
||||
|
||||
COPY . /build
|
||||
WORKDIR /build
|
||||
RUN make build
|
||||
|
||||
RUN echo "hauler:x:1001:1001::/home:" > /etc/passwd \
|
||||
&& echo "hauler:x:1001:hauler" > /etc/group \
|
||||
&& mkdir /store \
|
||||
&& mkdir /fileserver \
|
||||
&& mkdir /registry
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /var/lib/ca-certificates/ca-bundle.pem /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=builder /etc/passwd /etc/passwd
|
||||
COPY --from=builder /etc/group /etc/group
|
||||
COPY --from=builder --chown=hauler:hauler /home/. /home
|
||||
COPY --from=builder --chown=hauler:hauler /tmp/. /tmp
|
||||
COPY --from=builder --chown=hauler:hauler /store/. /store
|
||||
COPY --from=builder --chown=hauler:hauler /registry/. /registry
|
||||
COPY --from=builder --chown=hauler:hauler /fileserver/. /fileserver
|
||||
COPY --from=builder --chown=hauler:hauler /build/bin/hauler /
|
||||
USER hauler
|
||||
ENTRYPOINT [ "/hauler" ]
|
||||
177
LICENSE
177
LICENSE
@@ -1,177 +0,0 @@
|
||||
|
||||
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
|
||||
14
Makefile
14
Makefile
@@ -1,27 +1,23 @@
|
||||
SHELL:=/bin/bash
|
||||
GO_BUILD_ENV=GOOS=linux GOARCH=amd64
|
||||
GO_FILES=$(shell go list ./... | grep -v /vendor/)
|
||||
|
||||
COSIGN_VERSION=v2.2.3+carbide.2
|
||||
BUILD_VERSION=$(shell cat VERSION)
|
||||
BUILD_TAG=$(BUILD_VERSION)
|
||||
|
||||
.SILENT:
|
||||
|
||||
all: fmt vet install test
|
||||
|
||||
build:
|
||||
rm -rf cmd/hauler/binaries;\
|
||||
mkdir -p cmd/hauler/binaries;\
|
||||
wget -P cmd/hauler/binaries/ https://github.com/rancher-government-carbide/cosign/releases/download/$(COSIGN_VERSION)/cosign-$(shell go env GOOS)-$(shell go env GOARCH);\
|
||||
mkdir bin;\
|
||||
CGO_ENABLED=0 go build -o bin ./cmd/...;\
|
||||
$(GO_BUILD_ENV) go build -o bin ./cmd/...;\
|
||||
|
||||
build-all: fmt vet
|
||||
goreleaser build --rm-dist --snapshot
|
||||
|
||||
install:
|
||||
rm -rf cmd/hauler/binaries;\
|
||||
mkdir -p cmd/hauler/binaries;\
|
||||
wget -P cmd/hauler/binaries/ https://github.com/rancher-government-carbide/cosign/releases/download/$(COSIGN_VERSION)/cosign-$(shell go env GOOS)-$(shell go env GOARCH);\
|
||||
CGO_ENABLED=0 go install ./cmd/...;\
|
||||
$(GO_BUILD_ENV) go install
|
||||
|
||||
vet:
|
||||
go vet $(GO_FILES)
|
||||
|
||||
46
README.md
46
README.md
@@ -1,44 +1,28 @@
|
||||
# Rancher Government Hauler
|
||||
# Hauler: Airgap Assistant
|
||||
|
||||

|
||||
> ⚠️ This project is still in active development and _not_ GA. While a lot of the core features are ready, we're still adding a _ton_, and we may make breaking api and feature changes version to version.
|
||||
|
||||
## Airgap Swiss Army Knife
|
||||
`hauler` simplifies the airgap experience without forcing you to adopt a specific workflow for your infrastructure or application.
|
||||
|
||||
> ⚠️ **Please Note:** Hauler and the Hauler Documentation are recently Generally Available (GA).
|
||||
To accomplish this, it focuses strictly on two of the biggest airgap pain points:
|
||||
|
||||
`Rancher Government Hauler` simplifies the airgap experience without requiring operators to adopt a specific workflow. **Hauler** simplifies the airgapping process, by representing assets (images, charts, files, etc...) as content and collections to allow operators to easily fetch, store, package, and distribute these assets with declarative manifests or through the command line.
|
||||
* content collection
|
||||
* content distribution
|
||||
|
||||
`Hauler` does this by storing contents and collections as OCI Artifacts and allows operators to serve contents and collections with an embedded registry and fileserver. Additionally, `Hauler` has the ability to store and inspect various non-image OCI Artifacts.
|
||||
As OCI registries have become ubiquitous nowadays for storing and distributing containers. Their success and widespread adoption has led many projects to expand beyond containers.
|
||||
|
||||
For more information, please review the **[Hauler Documentation](https://rancherfederal.github.io/hauler-docs)!**
|
||||
`hauler` capitalizes on this, and leverages the [`oci`](https://github.com/opencontainers) spec to be a simple, zero dependency tool to collect, transport, and distribute your artifacts.
|
||||
|
||||
## Installation
|
||||
## Getting started
|
||||
|
||||
### Linux/Darwin
|
||||
See the [quickstart](docs/walkthrough.md#Quickstart) for a quick way to get started with some of `haulers` capabilities.
|
||||
|
||||
```bash
|
||||
# installs latest release
|
||||
curl -sfL https://get.hauler.dev | bash
|
||||
```
|
||||
|
||||
### Homebrew
|
||||
|
||||
```bash
|
||||
# installs latest release
|
||||
brew tap rancherfederal/homebrew-tap
|
||||
brew install hauler
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```bash
|
||||
# coming soon
|
||||
```
|
||||
For a guided example of all of `haulers` capabilities, check out the [guided example](docs/walkthrough.md#guided-examples).
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
`Hauler` wouldn't be possible without the open-source community, but there are a few projects that stand out:
|
||||
`hauler` wouldn't be possible without the open source community, but there are a few dependent projects that stand out:
|
||||
|
||||
- [oras cli](https://github.com/oras-project/oras)
|
||||
- [cosign](https://github.com/sigstore/cosign)
|
||||
- [go-containerregistry](https://github.com/google/go-containerregistry)
|
||||
* [go-containerregistry](https://github.com/google/go-containerregistry)
|
||||
* [oras](https://github.com/oras-project/oras)
|
||||
* [cosign](https://github.com/sigstore/cosign)
|
||||
50
ROADMAP.md
Normal file
50
ROADMAP.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Hauler Roadmap
|
||||
|
||||
## \> v0.2.0
|
||||
|
||||
- Leverage `referrers` api to robustly link content/collection
|
||||
- Support signing for all `artifact.OCI` contents
|
||||
- Support encryption for `artifact.OCI` layers
|
||||
- Support incremental updates to stores (some implementation of layer diffing)
|
||||
- Safely embed container runtime for user created `collections` creation and transformation
|
||||
- Better defaults/configuration/security around for long-lived embedded registry
|
||||
- Better support multi-platform content
|
||||
- Better leverage `oras` (`>=0.5.0`) for content relocation
|
||||
- Store git repos as CAS in OCI format
|
||||
|
||||
## v0.2.0 - MVP 2
|
||||
|
||||
- Re-focus on cli and framework for oci content fetching and delivery
|
||||
- Focus on initial key contents
|
||||
- Files (local/remote)
|
||||
- Charts (local/remote)
|
||||
- Images
|
||||
- Establish framework for `content` and `collections`
|
||||
- Define initial `content` types (`file`, `chart`, `image`)
|
||||
- Define initial `collection` types (`thickchart`, `k3s`)
|
||||
- Define framework for manipulating OCI content (`artifact.OCI`, `artifact.Collection`)
|
||||
|
||||
## v0.1.0 - MVP 1
|
||||
|
||||
- Install single-node k3s cluster
|
||||
- Support tarball and rpm installation methods
|
||||
- Target narrow set of known Operating Systems to have OS-specific code if needed
|
||||
- Serve container images
|
||||
- Collect images from image list file
|
||||
- Collect images from image archives
|
||||
- Deploy docker registry
|
||||
- Populate registry with all images
|
||||
- Serve git repositories
|
||||
- Collect repos
|
||||
- Deploy git server (Caddy? NGINX?)
|
||||
- Populate git server with repos
|
||||
- Serve files
|
||||
- Collect files from directory, including subdirectories
|
||||
- Deploy caddy file server
|
||||
- Populate file server with directory contents
|
||||
- NOTE: "generic" option - most other use cases can be satisfied by a specially crafted file
|
||||
server directory
|
||||
|
||||
## v0.0.x
|
||||
|
||||
- Install single-node k3s cluster into an Ubuntu machine using the tarball installation method
|
||||
49
VAGRANT.md
Normal file
49
VAGRANT.md
Normal file
@@ -0,0 +1,49 @@
|
||||
## Hauler Vagrant machine
|
||||
|
||||
A Vagrantfile is provided to allow easy provisioning of a local air-gapped CentOS environment. Some artifacts need to be collected from the internet; below are the steps required for successfully provisioning this machine, downloading all dependencies, and installing k3s (without hauler) into this machine.
|
||||
|
||||
### First-time setup
|
||||
|
||||
1. Install vagrant, if needed: <https://www.vagrantup.com/downloads>
|
||||
2. Install `vagrant-vbguest` plugin, as noted in the Vagrantfile:
|
||||
```shell
|
||||
vagrant plugin install vagrant-vbguest
|
||||
```
|
||||
3. Deploy Vagrant machine, disabling SELinux:
|
||||
```shell
|
||||
SELINUX=Disabled vagrant up
|
||||
```
|
||||
4. Access the Vagrant machine via SSH:
|
||||
```shell
|
||||
vagrant ssh
|
||||
```
|
||||
5. Run all prep scripts inside of the Vagrant machine:
|
||||
> This script temporarily enables internet access from within the VM to allow downloading all dependencies. Even so, the air-gapped network configuration IS restored before completion.
|
||||
```shell
|
||||
sudo /opt/hauler/vagrant-scripts/prep-all.sh
|
||||
```
|
||||
|
||||
All dependencies for all `vagrant-scripts/*-install.sh` scripts are now downloaded to the local
|
||||
repository under `local-artifacts`.
|
||||
|
||||
### Installing k3s manually
|
||||
|
||||
1. Access the Vagrant machine via SSH:
|
||||
```bash
|
||||
vagrant ssh
|
||||
```
|
||||
2. Run the k3s install script inside of the Vagrant machine:
|
||||
```shell
|
||||
sudo /opt/hauler/vagrant-scripts/k3s-install.sh
|
||||
```
|
||||
|
||||
### Installing RKE2 manually
|
||||
|
||||
1. Access the Vagrant machine via SSH:
|
||||
```shell
|
||||
vagrant ssh
|
||||
```
|
||||
2. Run the RKE2 install script inside of the Vagrant machine:
|
||||
```shell
|
||||
sudo /opt/hauler/vagrant-scripts/rke2-install.sh
|
||||
```
|
||||
65
Vagrantfile
vendored
Normal file
65
Vagrantfile
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
##################################
|
||||
# The vagrant-vbguest plugin is required for CentOS 7.
|
||||
# Run the following command to install/update this plugin:
|
||||
# vagrant plugin install vagrant-vbguest
|
||||
##################################
|
||||
|
||||
Vagrant.configure("2") do |config|
|
||||
config.vm.box = "centos/8"
|
||||
config.vm.hostname = "airgap"
|
||||
config.vm.network "private_network", type: "dhcp"
|
||||
|
||||
config.vm.synced_folder ".", "/vagrant"
|
||||
|
||||
config.vm.provider "virtualbox" do |vb|
|
||||
vb.memory = "2048"
|
||||
vb.cpus = "2"
|
||||
|
||||
config.vm.provision "airgap", type: "shell", run: "always",
|
||||
inline: "/vagrant/vagrant-scripts/airgap.sh airgap"
|
||||
end
|
||||
|
||||
# SELinux is Enforcing by default.
|
||||
# To set SELinux as Disabled on a VM that has already been provisioned:
|
||||
# SELINUX=Disabled vagrant up --provision-with=selinux
|
||||
# To set SELinux as Permissive on a VM that has already been provsioned
|
||||
# SELINUX=Permissive vagrant up --provision-with=selinux
|
||||
config.vm.provision "selinux", type: "shell", run: "once" do |sh|
|
||||
sh.upload_path = "/tmp/vagrant-selinux"
|
||||
sh.env = {
|
||||
'SELINUX': ENV['SELINUX'] || "Enforcing"
|
||||
}
|
||||
sh.inline = <<~SHELL
|
||||
#!/usr/bin/env bash
|
||||
set -eux -o pipefail
|
||||
|
||||
if ! type -p getenforce setenforce &>/dev/null; then
|
||||
echo SELinux is Disabled
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "${SELINUX}" in
|
||||
Disabled)
|
||||
if mountpoint -q /sys/fs/selinux; then
|
||||
setenforce 0
|
||||
umount -v /sys/fs/selinux
|
||||
fi
|
||||
;;
|
||||
Enforcing)
|
||||
mountpoint -q /sys/fs/selinux || mount -o rw,relatime -t selinuxfs selinuxfs /sys/fs/selinux
|
||||
setenforce 1
|
||||
;;
|
||||
Permissive)
|
||||
mountpoint -q /sys/fs/selinux || mount -o rw,relatime -t selinuxfs selinuxfs /sys/fs/selinux
|
||||
setenforce 0
|
||||
;;
|
||||
*)
|
||||
echo "SELinux mode not supported: ${SELINUX}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo SELinux is $(getenforce)
|
||||
SHELL
|
||||
end
|
||||
end
|
||||
@@ -1,21 +1,33 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/cache"
|
||||
"github.com/rancherfederal/hauler/pkg/log"
|
||||
"github.com/rancherfederal/hauler/pkg/store"
|
||||
)
|
||||
|
||||
type rootOpts struct {
|
||||
logLevel string
|
||||
cacheDir string
|
||||
storeDir string
|
||||
}
|
||||
|
||||
const defaultStoreLocation = "haul"
|
||||
|
||||
var ro = &rootOpts{}
|
||||
|
||||
func New() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "hauler",
|
||||
Short: "Airgap Swiss Army Knife",
|
||||
Short: "",
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
l := log.FromContext(cmd.Context())
|
||||
l.SetLevel(ro.logLevel)
|
||||
@@ -28,13 +40,69 @@ func New() *cobra.Command {
|
||||
}
|
||||
|
||||
pf := cmd.PersistentFlags()
|
||||
pf.StringVarP(&ro.logLevel, "log-level", "l", "info", "")
|
||||
pf.StringVarP(&ro.logLevel, "log-level", "l", "info", `Verbosity of logs ("debug", info", "warn", "error")`)
|
||||
pf.StringVar(&ro.cacheDir, "cache", "", "Location of where to store cache data (defaults to $XDG_CACHE_HOME/hauler)")
|
||||
pf.StringVarP(&ro.storeDir, "store", "s", "", "Location to create store at (defaults to $PWD/store)")
|
||||
|
||||
// Add subcommands
|
||||
addLogin(cmd)
|
||||
addDownload(cmd)
|
||||
addStore(cmd)
|
||||
addVersion(cmd)
|
||||
addCompletion(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *rootOpts) getStore(ctx context.Context) (*store.Store, error) {
|
||||
l := log.FromContext(ctx)
|
||||
dir := o.storeDir
|
||||
|
||||
if dir == "" {
|
||||
l.Debugf("no store path specified, defaulting to $PWD/store")
|
||||
dir = defaultStoreLocation
|
||||
}
|
||||
|
||||
abs, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve store absolute path: %w", err)
|
||||
}
|
||||
|
||||
l.Debugf("using store at %s", abs)
|
||||
if _, err := os.Stat(abs); errors.Is(err, os.ErrNotExist) {
|
||||
err := os.Mkdir(abs, os.ModePerm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create store directory %s: %w", abs, err)
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("stat %s: %w", abs, err)
|
||||
}
|
||||
|
||||
// TODO: Do we want this to be configurable?
|
||||
c, err := o.getCache(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get cache: %w", err)
|
||||
}
|
||||
|
||||
s := store.NewStore(ctx, abs, store.WithCache(c))
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (o *rootOpts) getCache(ctx context.Context) (cache.Cache, error) {
|
||||
dir := o.cacheDir
|
||||
|
||||
if dir == "" {
|
||||
// Default to $XDG_CACHE_HOME/hauler
|
||||
userCacheDir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get default cache directory: %w", err)
|
||||
}
|
||||
dir = filepath.Join(userCacheDir, "hauler")
|
||||
}
|
||||
|
||||
abs, _ := filepath.Abs(dir)
|
||||
if err := os.MkdirAll(abs, os.ModePerm); err != nil {
|
||||
return nil, fmt.Errorf("create cache directory %s: %w", abs, err)
|
||||
}
|
||||
|
||||
c := cache.NewFilesystem(dir)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func addCompletion(parent *cobra.Command) {
|
||||
cmd := &cobra.Command{
|
||||
Use: "completion",
|
||||
Short: "Generates completion scripts for various shells",
|
||||
Long: `The completion sub-command generates completion scripts for various shells.`,
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
addCompletionZsh(),
|
||||
addCompletionBash(),
|
||||
addCompletionFish(),
|
||||
addCompletionPowershell(),
|
||||
)
|
||||
|
||||
parent.AddCommand(cmd)
|
||||
}
|
||||
|
||||
func completionError(err error) ([]string, cobra.ShellCompDirective) {
|
||||
cobra.CompError(err.Error())
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
|
||||
func addCompletionZsh() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "zsh",
|
||||
Short: "Generates zsh completion scripts",
|
||||
Long: `The completion sub-command generates completion scripts for zsh.`,
|
||||
Example: `To load completion run
|
||||
|
||||
. <(hauler completion zsh)
|
||||
|
||||
To configure your zsh shell to load completions for each session add to your zshrc
|
||||
|
||||
# ~/.zshrc or ~/.profile
|
||||
command -v hauler >/dev/null && . <(hauler completion zsh)
|
||||
|
||||
or write a cached file in one of the completion directories in your ${fpath}:
|
||||
|
||||
echo "${fpath// /\n}" | grep -i completion
|
||||
hauler completion zsh > _hauler
|
||||
|
||||
mv _hauler ~/.oh-my-zsh/completions # oh-my-zsh
|
||||
mv _hauler ~/.zprezto/modules/completion/external/src/ # zprezto`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmd.GenZshCompletion(os.Stdout)
|
||||
// Cobra doesn't source zsh completion file, explicitly doing it here
|
||||
fmt.Println("compdef _hauler hauler")
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addCompletionBash() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "bash",
|
||||
Short: "Generates bash completion scripts",
|
||||
Long: `The completion sub-command generates completion scripts for bash.`,
|
||||
Example: `To load completion run
|
||||
|
||||
. <(hauler completion bash)
|
||||
|
||||
To configure your bash shell to load completions for each session add to your bashrc
|
||||
|
||||
# ~/.bashrc or ~/.profile
|
||||
command -v hauler >/dev/null && . <(hauler completion bash)`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmd.GenBashCompletion(os.Stdout)
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addCompletionFish() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "fish",
|
||||
Short: "Generates fish completion scripts",
|
||||
Long: `The completion sub-command generates completion scripts for fish.`,
|
||||
Example: `To configure your fish shell to load completions for each session write this script to your completions dir:
|
||||
|
||||
hauler completion fish > ~/.config/fish/completions/hauler.fish
|
||||
|
||||
See http://fishshell.com/docs/current/index.html#completion-own for more details`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmd.GenFishCompletion(os.Stdout, true)
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addCompletionPowershell() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "powershell",
|
||||
Short: "Generates powershell completion scripts",
|
||||
Long: `The completion sub-command generates completion scripts for powershell.`,
|
||||
Example: `To load completion run
|
||||
|
||||
. <(hauler completion powershell)
|
||||
|
||||
To configure your powershell shell to load completions for each session add to your powershell profile
|
||||
|
||||
Windows:
|
||||
|
||||
cd "$env:USERPROFILE\Documents\WindowsPowerShell\Modules"
|
||||
hauler completion powershell >> hauler-completion.ps1
|
||||
|
||||
Linux:
|
||||
|
||||
cd "${XDG_CONFIG_HOME:-"$HOME/.config/"}/powershell/modules"
|
||||
hauler completion powershell >> hauler-completions.ps1`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmd.GenPowerShellCompletion(os.Stdout)
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
42
cmd/hauler/cli/download.go
Normal file
42
cmd/hauler/cli/download.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/rancherfederal/hauler/cmd/hauler/cli/download"
|
||||
)
|
||||
|
||||
func addDownload(parent *cobra.Command) {
|
||||
o := &download.Opts{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "download",
|
||||
Short: "Download OCI content from a registry and populate it on disk",
|
||||
Long: `Locate OCI content based on it's reference in a compatible registry and download the contents to disk.
|
||||
|
||||
Note that the content type determines it's format on disk. Hauler's built in content types act as follows:
|
||||
|
||||
- File: as a file named after the pushed contents source name (ex: my-file.yaml:latest --> my-file.yaml)
|
||||
- Image: as a .tar named after the image (ex: alpine:latest --> alpine:latest.tar)
|
||||
- Chart: as a .tar.gz named after the chart (ex: loki:2.0.2 --> loki-2.0.2.tar.gz)`,
|
||||
Example: `
|
||||
# Download a file
|
||||
hauler dl my-file.yaml:latest
|
||||
|
||||
# Download an image
|
||||
hauler dl rancher/k3s:v1.22.2-k3s2
|
||||
|
||||
# Download a chart
|
||||
hauler dl longhorn:1.2.0`,
|
||||
Aliases: []string{"dl"},
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, arg []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
return download.Cmd(ctx, o, arg[0])
|
||||
},
|
||||
}
|
||||
o.AddArgs(cmd)
|
||||
|
||||
parent.AddCommand(cmd)
|
||||
}
|
||||
188
cmd/hauler/cli/download/download.go
Normal file
188
cmd/hauler/cli/download/download.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/containerd/containerd/remotes/docker"
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/google/go-containerregistry/pkg/v1/tarball"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/spf13/cobra"
|
||||
"oras.land/oras-go/pkg/content"
|
||||
"oras.land/oras-go/pkg/oras"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifact/types"
|
||||
"github.com/rancherfederal/hauler/pkg/log"
|
||||
"github.com/rancherfederal/hauler/pkg/version"
|
||||
)
|
||||
|
||||
type Opts struct {
|
||||
DestinationDir string
|
||||
|
||||
Username string
|
||||
Password string
|
||||
Insecure bool
|
||||
PlainHTTP bool
|
||||
}
|
||||
|
||||
func (o *Opts) AddArgs(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
|
||||
f.StringVarP(&o.DestinationDir, "output", "o", "", "Directory to save contents to (defaults to current directory)")
|
||||
f.StringVarP(&o.Username, "username", "u", "", "Username when copying to an authenticated remote registry")
|
||||
f.StringVarP(&o.Password, "password", "p", "", "Password when copying to an authenticated remote registry")
|
||||
f.BoolVar(&o.Insecure, "insecure", false, "Toggle allowing insecure connections when copying to a remote registry")
|
||||
f.BoolVar(&o.PlainHTTP, "plain-http", false, "Toggle allowing plain http connections when copying to a remote registry")
|
||||
}
|
||||
|
||||
func Cmd(ctx context.Context, o *Opts, reference string) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
cs := content.NewFileStore(o.DestinationDir)
|
||||
defer cs.Close()
|
||||
|
||||
// build + configure oras client
|
||||
var refOpts []name.Option
|
||||
remoteOpts := []remote.Option{
|
||||
remote.WithAuthFromKeychain(authn.DefaultKeychain),
|
||||
}
|
||||
|
||||
if o.PlainHTTP {
|
||||
refOpts = append(refOpts, name.Insecure)
|
||||
}
|
||||
|
||||
if o.Username != "" || o.Password != "" {
|
||||
basicAuth := &authn.Basic{
|
||||
Username: o.Username,
|
||||
Password: o.Password,
|
||||
}
|
||||
remoteOpts = append(remoteOpts, remote.WithAuth(basicAuth))
|
||||
}
|
||||
|
||||
if o.Insecure {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.TLSClientConfig.InsecureSkipVerify = true
|
||||
|
||||
remoteOpts = append(remoteOpts, remote.WithTransport(transport))
|
||||
}
|
||||
|
||||
// build + configure containerd client
|
||||
var registryOpts []docker.RegistryOpt
|
||||
|
||||
if o.PlainHTTP {
|
||||
registryOpts = append(registryOpts, docker.WithPlainHTTP(docker.MatchAllHosts))
|
||||
}
|
||||
|
||||
if o.Username != "" || o.Password != "" {
|
||||
creds := func(string) (string, string, error) {
|
||||
return o.Username, o.Password, nil
|
||||
}
|
||||
authorizer := docker.NewDockerAuthorizer(docker.WithAuthCreds(creds))
|
||||
registryOpts = append(registryOpts, docker.WithAuthorizer(authorizer))
|
||||
}
|
||||
|
||||
if o.Insecure {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.TLSClientConfig.InsecureSkipVerify = true
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
registryOpts = append(registryOpts, docker.WithClient(httpClient))
|
||||
}
|
||||
|
||||
resolverOpts := docker.ResolverOptions{
|
||||
Hosts: docker.ConfigureDefaultRegistries(registryOpts...),
|
||||
Headers: http.Header{},
|
||||
}
|
||||
resolverOpts.Headers.Set("User-Agent", "hauler/"+version.GitVersion)
|
||||
|
||||
resolver := docker.NewResolver(resolverOpts)
|
||||
|
||||
// begin dowloading target
|
||||
ref, err := name.ParseReference(reference, refOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc, err := remote.Get(ref, remoteOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifestData, err := desc.RawManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var manifest ocispec.Manifest
|
||||
if err := json.Unmarshal(manifestData, &manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: These need to be factored out into each of the contents own logic
|
||||
switch manifest.Config.MediaType {
|
||||
case types.DockerConfigJSON, types.OCIManifestSchema1:
|
||||
l.Debugf("identified [image] (%s) content", manifest.Config.MediaType)
|
||||
img, err := remote.Image(ref, remoteOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outputFile := fmt.Sprintf("%s_%s.tar", path.Base(ref.Context().RepositoryStr()), ref.Identifier())
|
||||
|
||||
if err := tarball.WriteToFile(outputFile, ref, img); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Infof("downloaded image [%s] to [%s]", ref.Name(), outputFile)
|
||||
|
||||
case types.FileConfigMediaType:
|
||||
l.Debugf("identified [file] (%s) content", manifest.Config.MediaType)
|
||||
|
||||
fs := content.NewFileStore(o.DestinationDir)
|
||||
|
||||
// TODO - additional accepted media types
|
||||
_, descs, err := oras.Pull(ctx, resolver, ref.Name(), fs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ldescs := len(descs)
|
||||
for i, desc := range descs {
|
||||
// NOTE: This is safe without a map key check b/c we're not allowing unnamed content from oras.Pull
|
||||
l.Infof("downloaded (%d/%d) files to [%s]", i+1, ldescs, desc.Annotations[ocispec.AnnotationTitle])
|
||||
}
|
||||
|
||||
case types.ChartLayerMediaType, types.ChartConfigMediaType:
|
||||
l.Debugf("identified [chart] (%s) content", manifest.Config.MediaType)
|
||||
|
||||
fs := content.NewFileStore(o.DestinationDir)
|
||||
|
||||
// TODO - additional accepted media types
|
||||
_, descs, err := oras.Pull(ctx, resolver, ref.Name(), fs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cn := path.Base(ref.Name())
|
||||
for _, d := range descs {
|
||||
if n, ok := d.Annotations[ocispec.AnnotationTitle]; ok {
|
||||
cn = n
|
||||
}
|
||||
}
|
||||
|
||||
l.Infof("downloaded chart [%s] to [%s]", ref.String(), cn)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unrecognized content type: %s", manifest.Config.MediaType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
38
cmd/hauler/cli/download/download_test.go
Normal file
38
cmd/hauler/cli/download/download_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCmd(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
o *Opts
|
||||
reference string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "should work",
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
o: &Opts{DestinationDir: ""},
|
||||
reference: "localhost:3000/hauler/file.txt:latest",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := Cmd(tt.args.ctx, tt.args.o, tt.args.reference); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Cmd() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"os"
|
||||
"io"
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"oras.land/oras-go/pkg/content"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/cosign"
|
||||
)
|
||||
|
||||
type Opts struct {
|
||||
Username string
|
||||
Password string
|
||||
PasswordStdin bool
|
||||
}
|
||||
|
||||
func (o *Opts) AddArgs(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
f.StringVarP(&o.Username, "username", "u", "", "Username")
|
||||
f.StringVarP(&o.Password, "password", "p", "", "Password")
|
||||
f.BoolVarP(&o.PasswordStdin, "password-stdin", "", false, "Take the password from stdin")
|
||||
}
|
||||
|
||||
func addLogin(parent *cobra.Command) {
|
||||
o := &Opts{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "login",
|
||||
Short: "Log in to a registry",
|
||||
Example: `
|
||||
# Log in to reg.example.com
|
||||
hauler login reg.example.com -u bob -p haulin`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, arg []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
if o.PasswordStdin {
|
||||
contents, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.Password = strings.TrimSuffix(string(contents), "\n")
|
||||
o.Password = strings.TrimSuffix(o.Password, "\r")
|
||||
}
|
||||
|
||||
if o.Username == "" && o.Password == "" {
|
||||
return fmt.Errorf("username and password required")
|
||||
}
|
||||
|
||||
return login(ctx, o, arg[0])
|
||||
},
|
||||
}
|
||||
o.AddArgs(cmd)
|
||||
|
||||
parent.AddCommand(cmd)
|
||||
}
|
||||
|
||||
func login(ctx context.Context, o *Opts, registry string) error {
|
||||
ropts := content.RegistryOptions{
|
||||
Username: o.Username,
|
||||
Password: o.Password,
|
||||
}
|
||||
|
||||
err := cosign.RegistryLogin(ctx, nil, registry, ropts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -2,14 +2,10 @@ package cli
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"fmt"
|
||||
|
||||
"github.com/rancherfederal/hauler/cmd/hauler/cli/store"
|
||||
)
|
||||
|
||||
var rootStoreOpts = &store.RootOpts{}
|
||||
|
||||
func addStore(parent *cobra.Command) {
|
||||
cmd := &cobra.Command{
|
||||
Use: "store",
|
||||
@@ -19,7 +15,6 @@ func addStore(parent *cobra.Command) {
|
||||
return cmd.Help()
|
||||
},
|
||||
}
|
||||
rootStoreOpts.AddArgs(cmd)
|
||||
|
||||
cmd.AddCommand(
|
||||
addStoreSync(),
|
||||
@@ -27,7 +22,7 @@ func addStore(parent *cobra.Command) {
|
||||
addStoreLoad(),
|
||||
addStoreSave(),
|
||||
addStoreServe(),
|
||||
addStoreInfo(),
|
||||
addStoreList(),
|
||||
addStoreCopy(),
|
||||
|
||||
// TODO: Remove this in favor of sync?
|
||||
@@ -38,7 +33,7 @@ func addStore(parent *cobra.Command) {
|
||||
}
|
||||
|
||||
func addStoreExtract() *cobra.Command {
|
||||
o := &store.ExtractOpts{RootOpts: rootStoreOpts}
|
||||
o := &store.ExtractOpts{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "extract",
|
||||
@@ -48,7 +43,7 @@ func addStoreExtract() *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
s, err := o.Store(ctx)
|
||||
s, err := ro.getStore(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -62,7 +57,7 @@ func addStoreExtract() *cobra.Command {
|
||||
}
|
||||
|
||||
func addStoreSync() *cobra.Command {
|
||||
o := &store.SyncOpts{RootOpts: rootStoreOpts}
|
||||
o := &store.SyncOpts{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "sync",
|
||||
@@ -70,7 +65,7 @@ func addStoreSync() *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
s, err := o.Store(ctx)
|
||||
s, err := ro.getStore(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -84,7 +79,7 @@ func addStoreSync() *cobra.Command {
|
||||
}
|
||||
|
||||
func addStoreLoad() *cobra.Command {
|
||||
o := &store.LoadOpts{RootOpts: rootStoreOpts}
|
||||
o := &store.LoadOpts{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "load",
|
||||
@@ -93,13 +88,12 @@ func addStoreLoad() *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
s, err := o.Store(ctx)
|
||||
s, err := ro.getStore(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s
|
||||
|
||||
return store.LoadCmd(ctx, o, args...)
|
||||
return store.LoadCmd(ctx, o, s.DataDir, args...)
|
||||
},
|
||||
}
|
||||
o.AddFlags(cmd)
|
||||
@@ -108,69 +102,29 @@ func addStoreLoad() *cobra.Command {
|
||||
}
|
||||
|
||||
func addStoreServe() *cobra.Command {
|
||||
o := &store.ServeOpts{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Expose the content of a local store through an OCI compliant registry or file server",
|
||||
Short: "Expose the content of a local store through an OCI compliant server",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cmd.Help()
|
||||
ctx := cmd.Context()
|
||||
|
||||
s, err := ro.getStore(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return store.ServeCmd(ctx, o, s)
|
||||
},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
addStoreServeRegistry(),
|
||||
addStoreServeFiles(),
|
||||
)
|
||||
o.AddFlags(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RegistryCmd serves the embedded registry
|
||||
func addStoreServeRegistry() *cobra.Command {
|
||||
o := &store.ServeRegistryOpts{RootOpts: rootStoreOpts}
|
||||
cmd := &cobra.Command{
|
||||
Use: "registry",
|
||||
Short: "Serve the embedded registry",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
s, err := o.Store(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return store.ServeRegistryCmd(ctx, o, s)
|
||||
},
|
||||
}
|
||||
|
||||
o.AddFlags(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// FileServerCmd serves the file server
|
||||
func addStoreServeFiles() *cobra.Command {
|
||||
o := &store.ServeFilesOpts{RootOpts: rootStoreOpts}
|
||||
cmd := &cobra.Command{
|
||||
Use: "fileserver",
|
||||
Short: "Serve the file server",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
s, err := o.Store(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return store.ServeFilesCmd(ctx, o, s)
|
||||
},
|
||||
}
|
||||
|
||||
o.AddFlags(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addStoreSave() *cobra.Command {
|
||||
o := &store.SaveOpts{RootOpts: rootStoreOpts}
|
||||
o := &store.SaveOpts{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "save",
|
||||
@@ -179,13 +133,12 @@ func addStoreSave() *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
s, err := o.Store(ctx)
|
||||
s, err := ro.getStore(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s
|
||||
|
||||
return store.SaveCmd(ctx, o, o.FileName)
|
||||
return store.SaveCmd(ctx, o, o.FileName, s.DataDir)
|
||||
},
|
||||
}
|
||||
o.AddArgs(cmd)
|
||||
@@ -193,30 +146,23 @@ func addStoreSave() *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addStoreInfo() *cobra.Command {
|
||||
o := &store.InfoOpts{RootOpts: rootStoreOpts}
|
||||
|
||||
var allowedValues = []string{"image", "chart", "file", "sigs", "atts", "sbom", "all"}
|
||||
func addStoreList() *cobra.Command {
|
||||
o := &store.ListOpts{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "info",
|
||||
Short: "Print out information about the store",
|
||||
Use: "list",
|
||||
Short: "List all content references in a store",
|
||||
Args: cobra.ExactArgs(0),
|
||||
Aliases: []string{"i", "list", "ls"},
|
||||
Aliases: []string{"ls"},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
s, err := o.Store(ctx)
|
||||
s, err := ro.getStore(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, allowed := range allowedValues {
|
||||
if o.TypeFilter == allowed {
|
||||
return store.InfoCmd(ctx, o, s)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("type must be one of %v", allowedValues)
|
||||
|
||||
return store.ListCmd(ctx, o, s)
|
||||
},
|
||||
}
|
||||
o.AddFlags(cmd)
|
||||
@@ -225,7 +171,7 @@ func addStoreInfo() *cobra.Command {
|
||||
}
|
||||
|
||||
func addStoreCopy() *cobra.Command {
|
||||
o := &store.CopyOpts{RootOpts: rootStoreOpts}
|
||||
o := &store.CopyOpts{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "copy",
|
||||
@@ -234,7 +180,7 @@ func addStoreCopy() *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
s, err := o.Store(ctx)
|
||||
s, err := ro.getStore(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -266,7 +212,7 @@ func addStoreAdd() *cobra.Command {
|
||||
}
|
||||
|
||||
func addStoreAddFile() *cobra.Command {
|
||||
o := &store.AddFileOpts{RootOpts: rootStoreOpts}
|
||||
o := &store.AddFileOpts{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "file",
|
||||
@@ -275,7 +221,7 @@ func addStoreAddFile() *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
s, err := o.Store(ctx)
|
||||
s, err := ro.getStore(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -289,7 +235,7 @@ func addStoreAddFile() *cobra.Command {
|
||||
}
|
||||
|
||||
func addStoreAddImage() *cobra.Command {
|
||||
o := &store.AddImageOpts{RootOpts: rootStoreOpts}
|
||||
o := &store.AddImageOpts{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "image",
|
||||
@@ -298,7 +244,7 @@ func addStoreAddImage() *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
s, err := o.Store(ctx)
|
||||
s, err := ro.getStore(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -312,22 +258,13 @@ func addStoreAddImage() *cobra.Command {
|
||||
}
|
||||
|
||||
func addStoreAddChart() *cobra.Command {
|
||||
o := &store.AddChartOpts{
|
||||
RootOpts: rootStoreOpts,
|
||||
ChartOpts: &action.ChartPathOptions{},
|
||||
}
|
||||
o := &store.AddChartOpts{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "chart",
|
||||
Short: "Add a local or remote chart to the content store",
|
||||
Short: "Add a chart to the content store",
|
||||
Example: `
|
||||
# add a local chart
|
||||
hauler store add chart path/to/chart/directory
|
||||
|
||||
# add a local compressed chart
|
||||
hauler store add chart path/to/chart.tar.gz
|
||||
|
||||
# add a remote chart
|
||||
# add a chart
|
||||
hauler store add chart longhorn --repo "https://charts.longhorn.io"
|
||||
|
||||
# add a specific version of a chart
|
||||
@@ -337,7 +274,7 @@ hauler store add chart rancher --repo "https://releases.rancher.com/server-chart
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
s, err := o.Store(ctx)
|
||||
s, err := ro.getStore(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,25 +2,21 @@ package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/file/getter"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/spf13/cobra"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/file"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/store"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/apis/hauler.cattle.io/v1alpha1"
|
||||
"github.com/rancherfederal/hauler/pkg/content/chart"
|
||||
"github.com/rancherfederal/hauler/pkg/cosign"
|
||||
"github.com/rancherfederal/hauler/pkg/content/file"
|
||||
"github.com/rancherfederal/hauler/pkg/content/image"
|
||||
"github.com/rancherfederal/hauler/pkg/log"
|
||||
"github.com/rancherfederal/hauler/pkg/reference"
|
||||
"github.com/rancherfederal/hauler/pkg/store"
|
||||
)
|
||||
|
||||
type AddFileOpts struct {
|
||||
*RootOpts
|
||||
Name string
|
||||
}
|
||||
|
||||
@@ -29,148 +25,147 @@ func (o *AddFileOpts) AddFlags(cmd *cobra.Command) {
|
||||
f.StringVarP(&o.Name, "name", "n", "", "(Optional) Name to assign to file in store")
|
||||
}
|
||||
|
||||
func AddFileCmd(ctx context.Context, o *AddFileOpts, s *store.Layout, reference string) error {
|
||||
func AddFileCmd(ctx context.Context, o *AddFileOpts, s *store.Store, reference string) error {
|
||||
s.Open()
|
||||
defer s.Close()
|
||||
|
||||
cfg := v1alpha1.File{
|
||||
Path: reference,
|
||||
}
|
||||
if len(o.Name) > 0 {
|
||||
cfg.Name = o.Name
|
||||
Ref: reference,
|
||||
Name: o.Name,
|
||||
}
|
||||
|
||||
return storeFile(ctx, s, cfg)
|
||||
}
|
||||
|
||||
func storeFile(ctx context.Context, s *store.Layout, fi v1alpha1.File) error {
|
||||
func storeFile(ctx context.Context, s *store.Store, fi v1alpha1.File) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
copts := getter.ClientOptions{
|
||||
NameOverride: fi.Name,
|
||||
if fi.Name == "" {
|
||||
base := filepath.Base(fi.Ref)
|
||||
fi.Name = filepath.Base(fi.Ref)
|
||||
l.Warnf("no name specified for file reference [%s], using base filepath: [%s]", fi.Ref, base)
|
||||
}
|
||||
|
||||
f := file.NewFile(fi.Path, file.WithClient(getter.NewClient(copts)))
|
||||
ref, err := reference.NewTagged(f.Name(fi.Path), reference.DefaultTag)
|
||||
oci, err := file.NewFile(fi.Ref, fi.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Infof("adding 'file' [%s] to the store as [%s]", fi.Path, ref.Name())
|
||||
_, err = s.AddOCI(ctx, f, ref.Name())
|
||||
ref, err := name.ParseReference(fi.Name, name.WithDefaultRegistry(""))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Infof("successfully added 'file' [%s]", ref.Name())
|
||||
desc, err := s.AddArtifact(ctx, oci, ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Infof("file [%s] added at: [%s]", ref.Name(), desc.Annotations[ocispec.AnnotationTitle])
|
||||
return nil
|
||||
}
|
||||
|
||||
type AddImageOpts struct {
|
||||
*RootOpts
|
||||
Name string
|
||||
Key string
|
||||
Platform string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (o *AddImageOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
f.StringVarP(&o.Key, "key", "k", "", "(Optional) Path to the key for digital signature verification")
|
||||
f.StringVarP(&o.Platform, "platform", "p", "", "(Optional) Specific platform to save. i.e. linux/amd64. Defaults to all if flag is omitted.")
|
||||
_ = f
|
||||
}
|
||||
|
||||
func AddImageCmd(ctx context.Context, o *AddImageOpts, s *store.Layout, reference string) error {
|
||||
l := log.FromContext(ctx)
|
||||
func AddImageCmd(ctx context.Context, o *AddImageOpts, s *store.Store, reference string) error {
|
||||
s.Open()
|
||||
defer s.Close()
|
||||
|
||||
cfg := v1alpha1.Image{
|
||||
Name: reference,
|
||||
Ref: reference,
|
||||
}
|
||||
|
||||
// Check if the user provided a key.
|
||||
if o.Key != "" {
|
||||
// verify signature using the provided key.
|
||||
err := cosign.VerifySignature(ctx, s, o.Key, cfg.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.Infof("signature verified for image [%s]", cfg.Name)
|
||||
}
|
||||
|
||||
return storeImage(ctx, s, cfg, o.Platform)
|
||||
return storeImage(ctx, s, cfg)
|
||||
}
|
||||
|
||||
func storeImage(ctx context.Context, s *store.Layout, i v1alpha1.Image, platform string) error {
|
||||
func storeImage(ctx context.Context, s *store.Store, i v1alpha1.Image) error {
|
||||
l := log.FromContext(ctx)
|
||||
l.Infof("adding 'image' [%s] to the store", i.Name)
|
||||
|
||||
r, err := name.ParseReference(i.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cosign.SaveImage(ctx, s, r.Name(), platform)
|
||||
oci, err := image.NewImage(i.Ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Infof("successfully added 'image' [%s]", r.Name())
|
||||
ref, err := name.ParseReference(i.Ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc, err := s.AddArtifact(ctx, oci, ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Infof("image [%s] added at: [%s]", ref.Name(), desc.Annotations[ocispec.AnnotationTitle])
|
||||
return nil
|
||||
}
|
||||
|
||||
type AddChartOpts struct {
|
||||
*RootOpts
|
||||
Version string
|
||||
RepoURL string
|
||||
|
||||
ChartOpts *action.ChartPathOptions
|
||||
// TODO: Support helm auth
|
||||
Username string
|
||||
Password string
|
||||
PassCredentialsAll bool
|
||||
CertFile string
|
||||
KeyFile string
|
||||
CaFile string
|
||||
InsecureSkipTLSverify bool
|
||||
RepositoryConfig string
|
||||
RepositoryCache string
|
||||
}
|
||||
|
||||
func (o *AddChartOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
|
||||
f.StringVar(&o.ChartOpts.RepoURL, "repo", "", "chart repository url where to locate the requested chart")
|
||||
f.StringVar(&o.ChartOpts.Version, "version", "", "specify a version constraint for the chart version to use. This constraint can be a specific tag (e.g. 1.1.1) or it may reference a valid range (e.g. ^2.0.0). If this is not specified, the latest version is used")
|
||||
f.BoolVar(&o.ChartOpts.Verify, "verify", false, "verify the package before using it")
|
||||
f.StringVar(&o.ChartOpts.Username, "username", "", "chart repository username where to locate the requested chart")
|
||||
f.StringVar(&o.ChartOpts.Password, "password", "", "chart repository password where to locate the requested chart")
|
||||
f.StringVar(&o.ChartOpts.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
|
||||
f.StringVar(&o.ChartOpts.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file")
|
||||
f.BoolVar(&o.ChartOpts.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download")
|
||||
f.StringVar(&o.ChartOpts.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
|
||||
f.StringVarP(&o.RepoURL, "repo", "r", "", "Chart repository URL")
|
||||
f.StringVar(&o.Version, "version", "", "(Optional) Version of the chart to download, defaults to latest if not specified")
|
||||
}
|
||||
|
||||
func AddChartCmd(ctx context.Context, o *AddChartOpts, s *store.Layout, chartName string) error {
|
||||
// TODO: Reduce duplicates between api chart and upstream helm opts
|
||||
func AddChartCmd(ctx context.Context, o *AddChartOpts, s *store.Store, chartName string) error {
|
||||
s.Open()
|
||||
defer s.Close()
|
||||
|
||||
cfg := v1alpha1.Chart{
|
||||
Name: chartName,
|
||||
RepoURL: o.ChartOpts.RepoURL,
|
||||
Version: o.ChartOpts.Version,
|
||||
RepoURL: o.RepoURL,
|
||||
Version: o.Version,
|
||||
}
|
||||
|
||||
return storeChart(ctx, s, cfg, o.ChartOpts)
|
||||
return storeChart(ctx, s, cfg)
|
||||
}
|
||||
|
||||
func storeChart(ctx context.Context, s *store.Layout, cfg v1alpha1.Chart, opts *action.ChartPathOptions) error {
|
||||
func storeChart(ctx context.Context, s *store.Store, ch v1alpha1.Chart) error {
|
||||
l := log.FromContext(ctx)
|
||||
l.Infof("adding 'chart' [%s] to the store", cfg.Name)
|
||||
|
||||
// TODO: This shouldn't be necessary
|
||||
opts.RepoURL = cfg.RepoURL
|
||||
opts.Version = cfg.Version
|
||||
|
||||
chrt, err := chart.NewChart(cfg.Name, opts)
|
||||
oci, err := chart.NewChart(ch.Name, ch.RepoURL, ch.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, err := chrt.Load()
|
||||
tag := ch.Version
|
||||
if tag == "" {
|
||||
tag = name.DefaultTag
|
||||
}
|
||||
|
||||
ref, err := name.ParseReference(ch.Name, name.WithDefaultRegistry(""), name.WithDefaultTag(tag))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ref, err := reference.NewTagged(c.Name(), c.Metadata.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.AddOCI(ctx, chrt, ref.Name())
|
||||
desc, err := s.AddArtifact(ctx, oci, ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Infof("successfully added 'chart' [%s]", ref.Name())
|
||||
l.Infof("chart [%s] added at: [%s]", ref.Name(), desc.Annotations[ocispec.AnnotationTitle])
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,76 +2,56 @@ package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/spf13/cobra"
|
||||
"oras.land/oras-go/pkg/content"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/cosign"
|
||||
"github.com/rancherfederal/hauler/pkg/store"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/log"
|
||||
"github.com/rancherfederal/hauler/pkg/store"
|
||||
)
|
||||
|
||||
type CopyOpts struct {
|
||||
*RootOpts
|
||||
|
||||
Username string
|
||||
Password string
|
||||
Insecure bool
|
||||
PlainHTTP bool
|
||||
}
|
||||
type CopyOpts struct{}
|
||||
|
||||
func (o *CopyOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
_ = f
|
||||
|
||||
f.StringVarP(&o.Username, "username", "u", "", "Username when copying to an authenticated remote registry")
|
||||
f.StringVarP(&o.Password, "password", "p", "", "Password when copying to an authenticated remote registry")
|
||||
f.BoolVar(&o.Insecure, "insecure", false, "Toggle allowing insecure connections when copying to a remote registry")
|
||||
f.BoolVar(&o.PlainHTTP, "plain-http", false, "Toggle allowing plain http connections when copying to a remote registry")
|
||||
// TODO: Regex matching
|
||||
}
|
||||
|
||||
func CopyCmd(ctx context.Context, o *CopyOpts, s *store.Layout, targetRef string) error {
|
||||
func CopyCmd(ctx context.Context, o *CopyOpts, s *store.Store, registry string) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
components := strings.SplitN(targetRef, "://", 2)
|
||||
switch components[0] {
|
||||
case "dir":
|
||||
l.Debugf("identified directory target reference")
|
||||
fs := content.NewFile(components[1])
|
||||
defer fs.Close()
|
||||
s.Open()
|
||||
defer s.Close()
|
||||
|
||||
_, err := s.CopyAll(ctx, fs, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case "registry":
|
||||
l.Debugf("identified registry target reference")
|
||||
ropts := content.RegistryOptions{
|
||||
Username: o.Username,
|
||||
Password: o.Password,
|
||||
Insecure: o.Insecure,
|
||||
PlainHTTP: o.PlainHTTP,
|
||||
}
|
||||
|
||||
if ropts.Username != "" {
|
||||
err := cosign.RegistryLogin(ctx, s, components[1], ropts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err := cosign.LoadImages(ctx, s, components[1], ropts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("detecting protocol from [%s]", targetRef)
|
||||
refs, err := s.List(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, r := range refs {
|
||||
ref, err := name.ParseReference(r, name.WithDefaultRegistry(s.Registry()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o, err := remote.Image(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rref, err := name.ParseReference(r, name.WithDefaultRegistry(registry))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Infof("copying [%s] -> [%s]", ref.Name(), rref.Name())
|
||||
if err := remote.Write(rref, o); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
l.Infof("copied artifacts to [%s]", components[1])
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,78 +2,36 @@ package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/rancherfederal/hauler/cmd/hauler/cli/download"
|
||||
"github.com/rancherfederal/hauler/pkg/layout"
|
||||
"github.com/rancherfederal/hauler/pkg/store"
|
||||
|
||||
"github.com/rancherfederal/hauler/internal/mapper"
|
||||
"github.com/rancherfederal/hauler/pkg/log"
|
||||
"github.com/rancherfederal/hauler/pkg/reference"
|
||||
)
|
||||
|
||||
type ExtractOpts struct {
|
||||
*RootOpts
|
||||
DestinationDir string
|
||||
}
|
||||
|
||||
func (o *ExtractOpts) AddArgs(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
|
||||
f.StringVarP(&o.DestinationDir, "output", "o", "", "Directory to save contents to (defaults to current directory)")
|
||||
f.StringVar(&o.DestinationDir, "dir", "", "Directory to save contents to (defaults to current directory)")
|
||||
}
|
||||
|
||||
func ExtractCmd(ctx context.Context, o *ExtractOpts, s *store.Layout, ref string) error {
|
||||
l := log.FromContext(ctx)
|
||||
func ExtractCmd(ctx context.Context, o *ExtractOpts, s *store.Store, reference string) error {
|
||||
s.Open()
|
||||
defer s.Close()
|
||||
|
||||
r, err := reference.Parse(ref)
|
||||
eref, err := layout.RelocateReference(reference, s.Registry())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
found := false
|
||||
if err := s.Walk(func(reference string, desc ocispec.Descriptor) error {
|
||||
|
||||
if !strings.Contains(reference, r.Name()) {
|
||||
return nil
|
||||
}
|
||||
found = true
|
||||
|
||||
rc, err := s.Fetch(ctx, desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
var m ocispec.Manifest
|
||||
if err := json.NewDecoder(rc).Decode(&m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mapperStore, err := mapper.FromManifest(m, o.DestinationDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pushedDesc, err := s.Copy(ctx, reference, mapperStore, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Infof("extracted [%s] from store with digest [%s]", pushedDesc.MediaType, pushedDesc.Digest.String())
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
gopts := &download.Opts{
|
||||
DestinationDir: o.DestinationDir,
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("reference [%s] not found in store (hint: use `hauler store info` to list store contents)", ref)
|
||||
}
|
||||
|
||||
return nil
|
||||
return download.Cmd(ctx, gopts, eref.Name())
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/store"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/log"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultStoreName = "store"
|
||||
)
|
||||
|
||||
type RootOpts struct {
|
||||
StoreDir string
|
||||
CacheDir string
|
||||
}
|
||||
|
||||
func (o *RootOpts) AddArgs(cmd *cobra.Command) {
|
||||
pf := cmd.PersistentFlags()
|
||||
pf.StringVarP(&o.StoreDir, "store", "s", DefaultStoreName, "Location to create store at")
|
||||
pf.StringVar(&o.CacheDir, "cache", "", "(deprecated flag and currently not used)")
|
||||
}
|
||||
|
||||
func (o *RootOpts) Store(ctx context.Context) (*store.Layout, error) {
|
||||
l := log.FromContext(ctx)
|
||||
dir := o.StoreDir
|
||||
|
||||
abs, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l.Debugf("using store at %s", abs)
|
||||
if _, err := os.Stat(abs); errors.Is(err, os.ErrNotExist) {
|
||||
err := os.Mkdir(abs, os.ModePerm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s, err := store.NewLayout(abs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/consts"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/store"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/reference"
|
||||
)
|
||||
|
||||
type InfoOpts struct {
|
||||
*RootOpts
|
||||
|
||||
OutputFormat string
|
||||
TypeFilter string
|
||||
SizeUnit string
|
||||
}
|
||||
|
||||
func (o *InfoOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
|
||||
f.StringVarP(&o.OutputFormat, "output", "o", "table", "Output format (table, json)")
|
||||
f.StringVarP(&o.TypeFilter, "type", "t", "all", "Filter on type (image, chart, file, sigs, atts, sbom)")
|
||||
|
||||
// TODO: Regex/globbing
|
||||
}
|
||||
|
||||
func InfoCmd(ctx context.Context, o *InfoOpts, s *store.Layout) error {
|
||||
var items []item
|
||||
if err := s.Walk(func(ref string, desc ocispec.Descriptor) error {
|
||||
if _, ok := desc.Annotations[ocispec.AnnotationRefName]; !ok {
|
||||
return nil
|
||||
}
|
||||
rc, err := s.Fetch(ctx, desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
// handle multi-arch images
|
||||
if desc.MediaType == consts.OCIImageIndexSchema || desc.MediaType == consts.DockerManifestListSchema2 {
|
||||
var idx ocispec.Index
|
||||
if err := json.NewDecoder(rc).Decode(&idx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, internalDesc := range idx.Manifests {
|
||||
rc, err := s.Fetch(ctx, internalDesc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
var internalManifest ocispec.Manifest
|
||||
if err := json.NewDecoder(rc).Decode(&internalManifest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i := newItem(s, desc, internalManifest, fmt.Sprintf("%s/%s", internalDesc.Platform.OS, internalDesc.Platform.Architecture), o)
|
||||
var emptyItem item
|
||||
if i != emptyItem {
|
||||
items = append(items, i)
|
||||
}
|
||||
}
|
||||
// handle "non" multi-arch images
|
||||
} else if desc.MediaType == consts.DockerManifestSchema2 || desc.MediaType == consts.OCIManifestSchema1 {
|
||||
var m ocispec.Manifest
|
||||
if err := json.NewDecoder(rc).Decode(&m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rc, err := s.FetchManifest(ctx, m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
// Unmarshal the OCI image content
|
||||
var internalManifest ocispec.Image
|
||||
if err := json.NewDecoder(rc).Decode(&internalManifest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if internalManifest.Architecture != "" {
|
||||
i := newItem(s, desc, m, fmt.Sprintf("%s/%s", internalManifest.OS, internalManifest.Architecture), o)
|
||||
var emptyItem item
|
||||
if i != emptyItem {
|
||||
items = append(items, i)
|
||||
}
|
||||
} else {
|
||||
i := newItem(s, desc, m, "-", o)
|
||||
var emptyItem item
|
||||
if i != emptyItem {
|
||||
items = append(items, i)
|
||||
}
|
||||
}
|
||||
// handle the rest
|
||||
} else {
|
||||
var m ocispec.Manifest
|
||||
if err := json.NewDecoder(rc).Decode(&m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i := newItem(s, desc, m, "-", o)
|
||||
var emptyItem item
|
||||
if i != emptyItem {
|
||||
items = append(items, i)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// sort items by ref and arch
|
||||
sort.Sort(byReferenceAndArch(items))
|
||||
|
||||
var msg string
|
||||
switch o.OutputFormat {
|
||||
case "json":
|
||||
msg = buildJson(items...)
|
||||
fmt.Println(msg)
|
||||
default:
|
||||
buildTable(items...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildTable(items ...item) {
|
||||
// Create a table for the results
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Reference", "Type", "Platform", "# Layers", "Size"})
|
||||
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
|
||||
table.SetRowLine(false)
|
||||
table.SetAutoMergeCellsByColumnIndex([]int{0})
|
||||
|
||||
totalSize := int64(0)
|
||||
for _, i := range items {
|
||||
if i.Type != "" {
|
||||
row := []string{
|
||||
i.Reference,
|
||||
i.Type,
|
||||
i.Platform,
|
||||
fmt.Sprintf("%d", i.Layers),
|
||||
byteCountSI(i.Size),
|
||||
}
|
||||
totalSize += i.Size
|
||||
table.Append(row)
|
||||
}
|
||||
}
|
||||
table.SetFooter([]string{"", "", "", "Total", byteCountSI(totalSize)})
|
||||
|
||||
table.Render()
|
||||
}
|
||||
|
||||
func buildJson(item ...item) string {
|
||||
data, err := json.MarshalIndent(item, "", " ")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
type item struct {
|
||||
Reference string
|
||||
Type string
|
||||
Platform string
|
||||
Layers int
|
||||
Size int64
|
||||
}
|
||||
|
||||
type byReferenceAndArch []item
|
||||
|
||||
func (a byReferenceAndArch) Len() int { return len(a) }
|
||||
func (a byReferenceAndArch) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byReferenceAndArch) Less(i, j int) bool {
|
||||
if a[i].Reference == a[j].Reference {
|
||||
if a[i].Type == "image" && a[j].Type == "image" {
|
||||
return a[i].Platform < a[j].Platform
|
||||
}
|
||||
if a[i].Type == "image" {
|
||||
return true
|
||||
}
|
||||
if a[j].Type == "image" {
|
||||
return false
|
||||
}
|
||||
return a[i].Type < a[j].Type
|
||||
}
|
||||
return a[i].Reference < a[j].Reference
|
||||
}
|
||||
|
||||
func newItem(s *store.Layout, desc ocispec.Descriptor, m ocispec.Manifest, plat string, o *InfoOpts) item {
|
||||
var size int64 = 0
|
||||
for _, l := range m.Layers {
|
||||
size += l.Size
|
||||
}
|
||||
|
||||
// Generate a human-readable content type
|
||||
var ctype string
|
||||
switch m.Config.MediaType {
|
||||
case consts.DockerConfigJSON:
|
||||
ctype = "image"
|
||||
case consts.ChartConfigMediaType:
|
||||
ctype = "chart"
|
||||
case consts.FileLocalConfigMediaType, consts.FileHttpConfigMediaType:
|
||||
ctype = "file"
|
||||
default:
|
||||
ctype = "image"
|
||||
}
|
||||
|
||||
switch desc.Annotations["kind"] {
|
||||
case "dev.cosignproject.cosign/sigs":
|
||||
ctype = "sigs"
|
||||
case "dev.cosignproject.cosign/atts":
|
||||
ctype = "atts"
|
||||
case "dev.cosignproject.cosign/sboms":
|
||||
ctype = "sbom"
|
||||
}
|
||||
|
||||
ref, err := reference.Parse(desc.Annotations[ocispec.AnnotationRefName])
|
||||
if err != nil {
|
||||
return item{}
|
||||
}
|
||||
|
||||
if o.TypeFilter != "all" && ctype != o.TypeFilter {
|
||||
return item{}
|
||||
}
|
||||
|
||||
return item{
|
||||
Reference: ref.Name(),
|
||||
Type: ctype,
|
||||
Platform: plat,
|
||||
Layers: len(m.Layers),
|
||||
Size: size,
|
||||
}
|
||||
}
|
||||
|
||||
func byteCountSI(b int64) string {
|
||||
const unit = 1000
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB",
|
||||
float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
||||
47
cmd/hauler/cli/store/list.go
Normal file
47
cmd/hauler/cli/store/list.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/store"
|
||||
)
|
||||
|
||||
type ListOpts struct{}
|
||||
|
||||
func (o *ListOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
_ = f
|
||||
|
||||
// TODO: Regex matching
|
||||
}
|
||||
|
||||
func ListCmd(ctx context.Context, o *ListOpts, s *store.Store) error {
|
||||
s.Open()
|
||||
defer s.Close()
|
||||
|
||||
refs, err := s.List(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 8, 0, '\t', 0)
|
||||
defer tw.Flush()
|
||||
|
||||
fmt.Fprintf(tw, "Reference\tTag/Digest\n")
|
||||
for _, r := range refs {
|
||||
ref, err := name.ParseReference(r, name.WithDefaultRegistry(""))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(tw, "%s\t%s\n", ref.Context().String(), ref.Identifier())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -2,39 +2,40 @@ package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/mholt/archiver/v3"
|
||||
"github.com/rancherfederal/hauler/pkg/content"
|
||||
"github.com/rancherfederal/hauler/pkg/store"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/log"
|
||||
)
|
||||
|
||||
type LoadOpts struct {
|
||||
*RootOpts
|
||||
TempOverride string
|
||||
OutputDir string
|
||||
}
|
||||
|
||||
func (o *LoadOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
|
||||
// On Unix systems, the default is $TMPDIR if non-empty, else /tmp.
|
||||
// On Windows, the default is GetTempPath, returning the first non-empty
|
||||
// value from %TMP%, %TEMP%, %USERPROFILE%, or the Windows directory.
|
||||
// On Plan 9, the default is /tmp.
|
||||
f.StringVarP(&o.TempOverride, "tempdir", "t", "", "overrides the default directory for temporary files, as returned by your OS.")
|
||||
f.StringVarP(&o.OutputDir, "output", "o", "", "Directory to unload archived contents to (defaults to $PWD/haul)")
|
||||
}
|
||||
|
||||
// LoadCmd
|
||||
// TODO: Just use mholt/archiver for now, even though we don't need most of it
|
||||
func LoadCmd(ctx context.Context, o *LoadOpts, archiveRefs ...string) error {
|
||||
func LoadCmd(ctx context.Context, o *LoadOpts, dir string, archiveRefs ...string) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
// TODO: Support more formats?
|
||||
a := archiver.NewTarZstd()
|
||||
a.OverwriteExisting = true
|
||||
|
||||
odir := dir
|
||||
if o.OutputDir != "" {
|
||||
odir = o.OutputDir
|
||||
}
|
||||
|
||||
for _, archiveRef := range archiveRefs {
|
||||
l.Infof("loading content from [%s] to [%s]", archiveRef, o.StoreDir)
|
||||
err := unarchiveLayoutTo(ctx, archiveRef, o.StoreDir, o.TempOverride)
|
||||
l.Infof("loading content from [%s] to [%s]", archiveRef, odir)
|
||||
err := a.Unarchive(archiveRef, odir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -42,29 +43,3 @@ func LoadCmd(ctx context.Context, o *LoadOpts, archiveRefs ...string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// unarchiveLayoutTo accepts an archived oci layout and extracts the contents to an existing oci layout, preserving the index
|
||||
func unarchiveLayoutTo(ctx context.Context, archivePath string, dest string, tempOverride string) error {
|
||||
tmpdir, err := os.MkdirTemp(tempOverride, "hauler")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
if err := archiver.Unarchive(archivePath, tmpdir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s, err := store.NewLayout(tmpdir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ts, err := content.NewOCI(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = s.CopyAll(ctx, ts, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -12,19 +12,18 @@ import (
|
||||
)
|
||||
|
||||
type SaveOpts struct {
|
||||
*RootOpts
|
||||
FileName string
|
||||
}
|
||||
|
||||
func (o *SaveOpts) AddArgs(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
|
||||
f.StringVarP(&o.FileName, "filename", "f", "haul.tar.zst", "Name of archive")
|
||||
f.StringVarP(&o.FileName, "filename", "f", "pkg.tar.zst", "Name of archive")
|
||||
}
|
||||
|
||||
// SaveCmd
|
||||
// TODO: Just use mholt/archiver for now, even though we don't need most of it
|
||||
func SaveCmd(ctx context.Context, o *SaveOpts, outputFile string) error {
|
||||
func SaveCmd(ctx context.Context, o *SaveOpts, outputFile string, dir string) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
// TODO: Support more formats?
|
||||
@@ -41,7 +40,7 @@ func SaveCmd(ctx context.Context, o *SaveOpts, outputFile string) error {
|
||||
return err
|
||||
}
|
||||
defer os.Chdir(cwd)
|
||||
if err := os.Chdir(o.StoreDir); err != nil {
|
||||
if err := os.Chdir(dir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -50,6 +49,6 @@ func SaveCmd(ctx context.Context, o *SaveOpts, outputFile string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Infof("saved store [%s] -> [%s]", o.StoreDir, absOutputfile)
|
||||
l.Infof("saved haul [%s] -> [%s]", dir, absOutputfile)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,54 +7,31 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/distribution/distribution/v3/configuration"
|
||||
dcontext "github.com/distribution/distribution/v3/context"
|
||||
_ "github.com/distribution/distribution/v3/registry/storage/driver/base"
|
||||
_ "github.com/distribution/distribution/v3/registry/storage/driver/filesystem"
|
||||
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
|
||||
"github.com/distribution/distribution/v3/version"
|
||||
"github.com/distribution/distribution/v3/registry"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/store"
|
||||
|
||||
"github.com/rancherfederal/hauler/internal/server"
|
||||
"github.com/rancherfederal/hauler/pkg/log"
|
||||
)
|
||||
|
||||
type ServeRegistryOpts struct {
|
||||
*RootOpts
|
||||
|
||||
type ServeOpts struct {
|
||||
Port int
|
||||
RootDir string
|
||||
ConfigFile string
|
||||
Daemon bool
|
||||
|
||||
storedir string
|
||||
}
|
||||
|
||||
func (o *ServeRegistryOpts) AddFlags(cmd *cobra.Command) {
|
||||
func (o *ServeOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
|
||||
f.IntVarP(&o.Port, "port", "p", 5000, "Port to listen on.")
|
||||
f.StringVar(&o.RootDir, "directory", "registry", "Directory to use for backend. Defaults to $PWD/registry")
|
||||
f.IntVarP(&o.Port, "port", "p", 5000, "Port to listen on")
|
||||
f.StringVarP(&o.ConfigFile, "config", "c", "", "Path to a config file, will override all other configs")
|
||||
f.BoolVarP(&o.Daemon, "daemon", "d", false, "Toggle serving as a daemon")
|
||||
}
|
||||
|
||||
func ServeRegistryCmd(ctx context.Context, o *ServeRegistryOpts, s *store.Layout) error {
|
||||
l := log.FromContext(ctx)
|
||||
ctx = dcontext.WithVersion(ctx, version.Version)
|
||||
|
||||
tr := server.NewTempRegistry(ctx, o.RootDir)
|
||||
if err := tr.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts := &CopyOpts{}
|
||||
if err := CopyCmd(ctx, opts, s, "registry://"+tr.Registry()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tr.Close()
|
||||
|
||||
cfg := o.defaultRegistryConfig()
|
||||
// ServeCmd does
|
||||
func ServeCmd(ctx context.Context, o *ServeOpts, s *store.Store) error {
|
||||
cfg := o.defaultConfig(s)
|
||||
if o.ConfigFile != "" {
|
||||
ucfg, err := loadConfig(o.ConfigFile)
|
||||
if err != nil {
|
||||
@@ -63,8 +40,7 @@ func ServeRegistryCmd(ctx context.Context, o *ServeRegistryOpts, s *store.Layout
|
||||
cfg = ucfg
|
||||
}
|
||||
|
||||
l.Infof("starting registry on port [%d]", o.Port)
|
||||
r, err := server.NewRegistry(ctx, cfg)
|
||||
r, err := registry.NewRegistry(ctx, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -76,49 +52,6 @@ func ServeRegistryCmd(ctx context.Context, o *ServeRegistryOpts, s *store.Layout
|
||||
return nil
|
||||
}
|
||||
|
||||
type ServeFilesOpts struct {
|
||||
*RootOpts
|
||||
|
||||
Port int
|
||||
RootDir string
|
||||
|
||||
storedir string
|
||||
}
|
||||
|
||||
func (o *ServeFilesOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
|
||||
f.IntVarP(&o.Port, "port", "p", 8080, "Port to listen on.")
|
||||
f.StringVar(&o.RootDir, "directory", "fileserver", "Directory to use for backend. Defaults to $PWD/fileserver")
|
||||
}
|
||||
|
||||
func ServeFilesCmd(ctx context.Context, o *ServeFilesOpts, s *store.Layout) error {
|
||||
l := log.FromContext(ctx)
|
||||
ctx = dcontext.WithVersion(ctx, version.Version)
|
||||
|
||||
opts := &CopyOpts{}
|
||||
if err := CopyCmd(ctx, opts, s, "dir://"+o.RootDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg := server.FileConfig{
|
||||
Root: o.RootDir,
|
||||
Port: o.Port,
|
||||
}
|
||||
|
||||
f, err := server.NewFile(ctx, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Infof("starting file server on port [%d]", o.Port)
|
||||
if err := f.ListenAndServe(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadConfig(filename string) (*configuration.Configuration, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
@@ -128,21 +61,17 @@ func loadConfig(filename string) (*configuration.Configuration, error) {
|
||||
return configuration.Parse(f)
|
||||
}
|
||||
|
||||
func (o *ServeRegistryOpts) defaultRegistryConfig() *configuration.Configuration {
|
||||
func (o *ServeOpts) defaultConfig(s *store.Store) *configuration.Configuration {
|
||||
cfg := &configuration.Configuration{
|
||||
Version: "0.1",
|
||||
Storage: configuration.Storage{
|
||||
"cache": configuration.Parameters{"blobdescriptor": "inmemory"},
|
||||
"filesystem": configuration.Parameters{"rootdirectory": o.RootDir},
|
||||
"filesystem": configuration.Parameters{"rootdirectory": s.DataDir},
|
||||
|
||||
// TODO: Ensure this is toggleable via cli arg if necessary
|
||||
// "maintenance": configuration.Parameters{"readonly.enabled": false},
|
||||
"maintenance": configuration.Parameters{"readonly.enabled": true},
|
||||
},
|
||||
}
|
||||
|
||||
// Add validation configuration
|
||||
cfg.Validation.Manifests.URLs.Allow = []string{".+"}
|
||||
|
||||
cfg.Log.Level = "info"
|
||||
cfg.HTTP.Addr = fmt.Sprintf(":%d", o.Port)
|
||||
cfg.HTTP.Headers = http.Header{
|
||||
|
||||
@@ -6,290 +6,192 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/cobra"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/apis/hauler.cattle.io/v1alpha1"
|
||||
"github.com/rancherfederal/hauler/pkg/apis/hauler.cattle.io/v1alpha2"
|
||||
tchart "github.com/rancherfederal/hauler/pkg/collection/chart"
|
||||
"github.com/rancherfederal/hauler/pkg/collection/imagetxt"
|
||||
"github.com/rancherfederal/hauler/pkg/collection/k3s"
|
||||
"github.com/rancherfederal/hauler/pkg/consts"
|
||||
"github.com/rancherfederal/hauler/pkg/content"
|
||||
"github.com/rancherfederal/hauler/pkg/cosign"
|
||||
"github.com/rancherfederal/hauler/pkg/log"
|
||||
"github.com/rancherfederal/hauler/pkg/reference"
|
||||
"github.com/rancherfederal/hauler/pkg/store"
|
||||
)
|
||||
|
||||
type SyncOpts struct {
|
||||
*RootOpts
|
||||
ContentFiles []string
|
||||
Key string
|
||||
Products []string
|
||||
Platform string
|
||||
Registry string
|
||||
ProductRegistry string
|
||||
}
|
||||
|
||||
func (o *SyncOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
|
||||
f.StringSliceVarP(&o.ContentFiles, "files", "f", []string{}, "Path to content files")
|
||||
f.StringVarP(&o.Key, "key", "k", "", "(Optional) Path to the key for signature verification")
|
||||
f.StringSliceVar(&o.Products, "products", []string{}, "Used for RGS Carbide customers to supply a product and version and Hauler will retrieve the images. i.e. '--product rancher=v2.7.6'")
|
||||
f.StringVarP(&o.Platform, "platform", "p", "", "(Optional) Specific platform to save. i.e. linux/amd64. Defaults to all if flag is omitted.")
|
||||
f.StringVarP(&o.Registry, "registry", "r", "", "(Optional) Default pull registry for image refs that are not specifying a registry name.")
|
||||
f.StringVarP(&o.ProductRegistry, "product-registry", "c", "", "(Optional) Specific Product Registry to use. Defaults to RGS Carbide Registry (rgcrprod.azurecr.us).")
|
||||
}
|
||||
|
||||
func SyncCmd(ctx context.Context, o *SyncOpts, s *store.Layout) error {
|
||||
func SyncCmd(ctx context.Context, o *SyncOpts, s *store.Store) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
// if passed products, check for a remote manifest to retrieve and use.
|
||||
for _, product := range o.Products {
|
||||
l.Infof("processing content file for product: '%s'", product)
|
||||
parts := strings.Split(product, "=")
|
||||
tag := strings.ReplaceAll(parts[1], "+", "-")
|
||||
|
||||
ProductRegistry := o.ProductRegistry // cli flag
|
||||
// if no cli flag use CarbideRegistry.
|
||||
if o.ProductRegistry == "" {
|
||||
ProductRegistry = consts.CarbideRegistry
|
||||
}
|
||||
|
||||
manifestLoc := fmt.Sprintf("%s/hauler/%s-manifest.yaml:%s", ProductRegistry, parts[0], tag)
|
||||
l.Infof("retrieving product manifest from: '%s'", manifestLoc)
|
||||
img := v1alpha1.Image{
|
||||
Name: manifestLoc,
|
||||
}
|
||||
err := storeImage(ctx, s, img, o.Platform)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ExtractCmd(ctx, &ExtractOpts{RootOpts: o.RootOpts}, s, fmt.Sprintf("hauler/%s-manifest.yaml:%s", parts[0],tag))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filename := fmt.Sprintf("%s-manifest.yaml", parts[0])
|
||||
|
||||
fi, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = processContent(ctx, fi, o, s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Start from an empty store (contents are cached elsewhere)
|
||||
l.Debugf("flushing any existing content in store: %s", s.DataDir)
|
||||
if err := s.Flush(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if passed a local manifest, process it
|
||||
s.Open()
|
||||
defer s.Close()
|
||||
|
||||
for _, filename := range o.ContentFiles {
|
||||
l.Debugf("processing content file: '%s'", filename)
|
||||
fi, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = processContent(ctx, fi, o, s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
reader := yaml.NewYAMLReader(bufio.NewReader(fi))
|
||||
|
||||
func processContent(ctx context.Context, fi *os.File, o *SyncOpts, s *store.Layout) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
reader := yaml.NewYAMLReader(bufio.NewReader(fi))
|
||||
|
||||
var docs [][]byte
|
||||
for {
|
||||
raw, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
docs = append(docs, raw)
|
||||
}
|
||||
|
||||
for _, doc := range docs {
|
||||
obj, err := content.Load(doc)
|
||||
if err != nil {
|
||||
l.Debugf("skipping sync of unknown content")
|
||||
continue
|
||||
}
|
||||
|
||||
l.Infof("syncing [%s] to store", obj.GroupVersionKind().String())
|
||||
|
||||
// TODO: Should type switch instead...
|
||||
switch obj.GroupVersionKind().Kind {
|
||||
case v1alpha1.FilesContentKind:
|
||||
var cfg v1alpha1.Files
|
||||
if err := yaml.Unmarshal(doc, &cfg); err != nil {
|
||||
return err
|
||||
var docs [][]byte
|
||||
for {
|
||||
raw, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
for _, f := range cfg.Spec.Files {
|
||||
err := storeFile(ctx, s, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case v1alpha1.ImagesContentKind:
|
||||
var cfg v1alpha1.Images
|
||||
if err := yaml.Unmarshal(doc, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
a := cfg.GetAnnotations()
|
||||
for _, i := range cfg.Spec.Images {
|
||||
|
||||
// Check if the user provided a registry. If a registry is provided in the annotation, use it for the images that don't have a registry in their ref name.
|
||||
if a[consts.ImageAnnotationRegistry] != "" || o.Registry != ""{
|
||||
newRef,_ := reference.Parse(i.Name)
|
||||
|
||||
newReg := o.Registry // cli flag
|
||||
// if no cli flag but there was an annotation, use the annotation.
|
||||
if o.Registry == "" && a[consts.ImageAnnotationRegistry] != "" {
|
||||
newReg = a[consts.ImageAnnotationRegistry]
|
||||
}
|
||||
|
||||
if newRef.Context().RegistryStr() == "" {
|
||||
newRef,err = reference.Relocate(i.Name, newReg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
i.Name = newRef.Name()
|
||||
}
|
||||
|
||||
// Check if the user provided a key. The flag from the CLI takes precedence over the annotation. The individual image key takes precedence over both.
|
||||
if a[consts.ImageAnnotationKey] != "" || o.Key != "" || i.Key != "" {
|
||||
key := o.Key // cli flag
|
||||
// if no cli flag but there was an annotation, use the annotation.
|
||||
if o.Key == "" && a[consts.ImageAnnotationKey] != "" {
|
||||
key, err = homedir.Expand(a[consts.ImageAnnotationKey])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// the individual image key trumps all
|
||||
if i.Key != "" {
|
||||
key, err = homedir.Expand(i.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
l.Debugf("key for image [%s]", key)
|
||||
|
||||
// verify signature using the provided key.
|
||||
err := cosign.VerifySignature(ctx, s, key, i.Name)
|
||||
if err != nil {
|
||||
l.Errorf("signature verification failed for image [%s]. ** hauler will skip adding this image to the store **:\n%v", i.Name, err)
|
||||
continue
|
||||
}
|
||||
l.Infof("signature verified for image [%s]", i.Name)
|
||||
}
|
||||
|
||||
// Check if the user provided a platform. The flag from the CLI takes precedence over the annotation. The individual image platform takes precedence over both.
|
||||
platform := o.Platform // cli flag
|
||||
// if no cli flag but there was an annotation, use the annotation.
|
||||
if o.Platform == "" && a[consts.ImageAnnotationPlatform] != "" {
|
||||
platform = a[consts.ImageAnnotationPlatform]
|
||||
}
|
||||
// the individual image platform trumps all
|
||||
if i.Platform != "" {
|
||||
platform = i.Platform
|
||||
}
|
||||
|
||||
err = storeImage(ctx, s, i, platform)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// sync with local index
|
||||
s.CopyAll(ctx, s.OCI, nil)
|
||||
|
||||
case v1alpha1.ChartsContentKind:
|
||||
var cfg v1alpha1.Charts
|
||||
if err := yaml.Unmarshal(doc, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, ch := range cfg.Spec.Charts {
|
||||
// TODO: Provide a way to configure syncs
|
||||
err := storeChart(ctx, s, ch, &action.ChartPathOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case v1alpha1.K3sCollectionKind:
|
||||
var cfg v1alpha1.K3s
|
||||
if err := yaml.Unmarshal(doc, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k, err := k3s.NewK3s(cfg.Spec.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := s.AddOCICollection(ctx, k); err != nil {
|
||||
docs = append(docs, raw)
|
||||
}
|
||||
|
||||
for _, doc := range docs {
|
||||
obj, err := content.Load(doc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case v1alpha1.ChartsCollectionKind:
|
||||
var cfg v1alpha1.ThickCharts
|
||||
if err := yaml.Unmarshal(doc, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
l.Infof("syncing [%s] to [%s]", obj.GroupVersionKind().String(), s.DataDir)
|
||||
|
||||
for _, cfg := range cfg.Spec.Charts {
|
||||
tc, err := tchart.NewThickChart(cfg, &action.ChartPathOptions{
|
||||
RepoURL: cfg.RepoURL,
|
||||
Version: cfg.Version,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gvk := obj.GroupVersionKind()
|
||||
|
||||
if _, err := s.AddOCICollection(ctx, tc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case v1alpha1.ImageTxtsContentKind:
|
||||
var cfg v1alpha1.ImageTxts
|
||||
if err := yaml.Unmarshal(doc, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, cfgIt := range cfg.Spec.ImageTxts {
|
||||
it, err := imagetxt.New(cfgIt.Ref,
|
||||
imagetxt.WithIncludeSources(cfgIt.Sources.Include...),
|
||||
imagetxt.WithExcludeSources(cfgIt.Sources.Exclude...),
|
||||
switch {
|
||||
// content.hauler.cattle.io/v1alpha1
|
||||
case gvk.GroupVersion() == v1alpha1.ContentGroupVersion:
|
||||
l.Warnf(
|
||||
"API version %s is deprecated in v0.3; ok to use in v0.2, use %s instead in v0.3",
|
||||
gvk.GroupVersion().String(),
|
||||
v1alpha2.ContentGroupVersion.String(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert ImageTxt %s: %v", cfg.Name, err)
|
||||
switch gvk.Kind {
|
||||
// content.hauler.cattle.io/v1alpha1 Files
|
||||
case v1alpha1.FilesContentKind:
|
||||
var cfg v1alpha1.Files
|
||||
if err := yaml.Unmarshal(doc, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range cfg.Spec.Files {
|
||||
err := storeFile(ctx, s, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// content.hauler.cattle.io/v1alpha1 Images
|
||||
case v1alpha1.ImagesContentKind:
|
||||
var cfg v1alpha1.Images
|
||||
if err := yaml.Unmarshal(doc, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, i := range cfg.Spec.Images {
|
||||
err := storeImage(ctx, s, i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// content.hauler.cattle.io/v1alpha1 Charts
|
||||
case v1alpha1.ChartsContentKind:
|
||||
var cfg v1alpha1.Charts
|
||||
if err := yaml.Unmarshal(doc, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ch := range cfg.Spec.Charts {
|
||||
err := storeChart(ctx, s, ch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// collection.hauler.cattle.io/v1alpha1 unknown
|
||||
default:
|
||||
return fmt.Errorf("unsupported Kind %s for %s", obj.GroupVersionKind().Kind, obj.GroupVersionKind().GroupVersion().String())
|
||||
}
|
||||
// collection.hauler.cattle.io/v1alpha1
|
||||
case gvk.GroupVersion() == v1alpha1.CollectionGroupVersion:
|
||||
l.Warnf(
|
||||
"API version %s is deprecated in v0.3; ok to use in v0.2, use %s instead in v0.3",
|
||||
gvk.GroupVersion().String(),
|
||||
v1alpha2.CollectionGroupVersion.String(),
|
||||
)
|
||||
switch gvk.Kind {
|
||||
// collection.hauler.cattle.io/v1alpha1 K3s
|
||||
case v1alpha1.K3sCollectionKind:
|
||||
var cfg v1alpha1.K3s
|
||||
if err := yaml.Unmarshal(doc, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
k, err := k3s.NewK3s(cfg.Spec.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := s.AddCollection(ctx, k); err != nil {
|
||||
return err
|
||||
}
|
||||
// collection.hauler.cattle.io/v1alpha1 ThickCharts
|
||||
case v1alpha1.ChartsCollectionKind:
|
||||
var cfg v1alpha1.ThickCharts
|
||||
if err := yaml.Unmarshal(doc, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, cfg := range cfg.Spec.Charts {
|
||||
tc, err := tchart.NewChart(cfg.Name, cfg.RepoURL, cfg.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := s.AddCollection(ctx, tc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// collection.hauler.cattle.io/v1alpha1 ImageTxts
|
||||
case v1alpha1.ImageTxtsContentKind:
|
||||
var cfg v1alpha1.ImageTxts
|
||||
if err := yaml.Unmarshal(doc, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := s.AddOCICollection(ctx, it); err != nil {
|
||||
return fmt.Errorf("add ImageTxt %s to store: %v", cfg.Name, err)
|
||||
for _, cfgIt := range cfg.Spec.ImageTxts {
|
||||
it, err := imagetxt.New(cfgIt.Ref,
|
||||
imagetxt.WithIncludeSources(cfgIt.Sources.Include...),
|
||||
imagetxt.WithExcludeSources(cfgIt.Sources.Exclude...),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert ImageTxt %s: %v", cfg.Name, err)
|
||||
}
|
||||
|
||||
if _, err := s.AddCollection(ctx, it); err != nil {
|
||||
return fmt.Errorf("add ImageTxt %s to store: %v", cfg.Name, err)
|
||||
}
|
||||
}
|
||||
// collection.hauler.cattle.io/v1alpha1 unknown
|
||||
default:
|
||||
return fmt.Errorf("unsupported Kind %s for %s", gvk.Kind, gvk.GroupVersion().String())
|
||||
}
|
||||
// content.hauler.cattle.io/v1alpha2 + collection.hauler.cattle.io/v1alpha2
|
||||
case gvk.GroupVersion() == v1alpha2.ContentGroupVersion || gvk.GroupVersion() == v1alpha2.CollectionGroupVersion:
|
||||
return fmt.Errorf("API group + version %s not yet supported", gvk.GroupVersion().String())
|
||||
// unknown
|
||||
default:
|
||||
return fmt.Errorf("unrecognized content/collection type: %s", obj.GroupVersionKind().String())
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unrecognized content/collection type: %s", obj.GroupVersionKind().String())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/rancherfederal/hauler/internal/version"
|
||||
"github.com/rancherfederal/hauler/pkg/version"
|
||||
)
|
||||
|
||||
func addVersion(parent *cobra.Command) {
|
||||
@@ -13,27 +13,24 @@ func addVersion(parent *cobra.Command) {
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the current version",
|
||||
Short: "Print current hauler version",
|
||||
Long: "Print current hauler version",
|
||||
Aliases: []string{"v"},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
v := version.GetVersionInfo()
|
||||
v.Name = cmd.Root().Name()
|
||||
v.Description = cmd.Root().Short
|
||||
v.FontName = "starwars"
|
||||
cmd.SetOut(cmd.OutOrStdout())
|
||||
|
||||
response := v.String()
|
||||
if json {
|
||||
out, err := v.JSONString()
|
||||
data, err := v.JSONString()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to generate JSON from version info: %w", err)
|
||||
return err
|
||||
}
|
||||
cmd.Println(out)
|
||||
} else {
|
||||
cmd.Println(v.String())
|
||||
response = data
|
||||
}
|
||||
fmt.Print(response)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&json, "json", false, "toggle output in JSON")
|
||||
|
||||
parent.AddCommand(cmd)
|
||||
|
||||
@@ -2,17 +2,12 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"os"
|
||||
|
||||
"github.com/rancherfederal/hauler/cmd/hauler/cli"
|
||||
"github.com/rancherfederal/hauler/pkg/cosign"
|
||||
"github.com/rancherfederal/hauler/pkg/log"
|
||||
)
|
||||
|
||||
//go:embed binaries/*
|
||||
var binaries embed.FS
|
||||
|
||||
func main() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
@@ -20,15 +15,7 @@ func main() {
|
||||
logger := log.NewLogger(os.Stdout)
|
||||
ctx = logger.WithContext(ctx)
|
||||
|
||||
// ensure cosign binary is available
|
||||
if err := cosign.EnsureBinaryExists(ctx, binaries); err != nil {
|
||||
logger.Errorf("%v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := cli.New().ExecuteContext(ctx); err != nil {
|
||||
logger.Errorf("%v", err)
|
||||
cancel()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# HELM IGNORE OPTIONS:
|
||||
# Patterns to ignore when building Helm packages.
|
||||
# Supports shell glob matching, relative path matching, and negation (prefixed with !)
|
||||
|
||||
.DS_Store
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
@@ -1,7 +0,0 @@
|
||||
apiVersion: v2
|
||||
name: hauler
|
||||
description: Hauler Helm Chart - Airgap Swiss Army Knife
|
||||
icon: https://raw.githubusercontent.com/rancherfederal/hauler/main/static/rgs-hauler-logo-icon.svg
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: 1.0.2
|
||||
@@ -1,33 +0,0 @@
|
||||
# Hauler Helm Chart
|
||||
|
||||
### Airgap Swiss Army Knife
|
||||
|
||||
`Rancher Government Hauler` simplifies the airgap experience without requiring operators to adopt a specific workflow. **Hauler** simplifies the airgapping process, by representing assets (images, charts, files, etc...) as content and collections to allow operators to easily fetch, store, package, and distribute these assets with declarative manifests or through the command line.
|
||||
|
||||
`Hauler` does this by storing contents and collections as OCI Artifacts and allows operators to serve contents and collections with an embedded registry and fileserver. Additionally, `Hauler` has the ability to store and inspect various non-image OCI Artifacts.
|
||||
|
||||
**GitHub Repostiory:** https://github.com/rancherfederal/hauler
|
||||
|
||||
**Documentation:** http://hauler.dev
|
||||
|
||||
---
|
||||
|
||||
| Type | Chart Version | App Version |
|
||||
| ----------- | ------------- | ----------- |
|
||||
| application | `0.1.0` | `1.0.2` |
|
||||
|
||||
## Installing the Chart
|
||||
|
||||
```bash
|
||||
helm install hauler hauler/hauler -n hauler-system -f values.yaml
|
||||
```
|
||||
|
||||
```bash
|
||||
helm status hauler -n hauler-system
|
||||
```
|
||||
|
||||
## Uninstalling the Chart
|
||||
|
||||
```bash
|
||||
helm uninstall hauler -n hauler-system
|
||||
```
|
||||
@@ -1,33 +0,0 @@
|
||||
# Hauler Helm Chart
|
||||
|
||||
### Airgap Swiss Army Knife
|
||||
|
||||
`Rancher Government Hauler` simplifies the airgap experience without requiring operators to adopt a specific workflow. **Hauler** simplifies the airgapping process, by representing assets (images, charts, files, etc...) as content and collections to allow operators to easily fetch, store, package, and distribute these assets with declarative manifests or through the command line.
|
||||
|
||||
`Hauler` does this by storing contents and collections as OCI Artifacts and allows operators to serve contents and collections with an embedded registry and fileserver. Additionally, `Hauler` has the ability to store and inspect various non-image OCI Artifacts.
|
||||
|
||||
**GitHub Repostiory:** https://github.com/rancherfederal/hauler
|
||||
|
||||
**Documentation:** http://hauler.dev
|
||||
|
||||
---
|
||||
|
||||
| Type | Chart Version | App Version |
|
||||
| ----------- | ------------- | ----------- |
|
||||
| application | `0.1.0` | `1.0.2` |
|
||||
|
||||
## Installing the Chart
|
||||
|
||||
```bash
|
||||
helm install hauler hauler/hauler -n hauler-system -f values.yaml
|
||||
```
|
||||
|
||||
```bash
|
||||
helm status hauler -n hauler-system
|
||||
```
|
||||
|
||||
## Uninstalling the Chart
|
||||
|
||||
```bash
|
||||
helm uninstall hauler -n hauler-system
|
||||
```
|
||||
@@ -1,62 +0,0 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "hauler.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "hauler.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "hauler.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "hauler.labels" -}}
|
||||
helm.sh/chart: {{ include "hauler.chart" . }}
|
||||
{{ include "hauler.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "hauler.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "hauler.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "hauler.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "hauler.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,76 +0,0 @@
|
||||
{{- if .Values.haulerFileserver.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: hauler-fileserver
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "hauler.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.haulerFileserver.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: hauler-fileserver
|
||||
{{- include "hauler.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: hauler-fileserver
|
||||
{{- include "hauler.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- if or .Values.haulerJobs.hauls.enabled .Values.haulerJobs.manifests.enabled }}
|
||||
initContainers:
|
||||
{{- if .Values.haulerJobs.hauls.enabled }}
|
||||
- name: wait-for-hauler-hauls-job
|
||||
image: {{ .Values.hauler.initContainers.image.repository }}:{{ .Values.hauler.initContainers.image.tag }}
|
||||
imagePullPolicy: {{ .Values.hauler.initContainers.imagePullPolicy }}
|
||||
args: ["wait", "--for=condition=complete", "job", "hauler-hauls-job", "--namespace", "{{ .Release.Namespace }}", "--timeout={{ .Values.hauler.initContainers.timeout }}"]
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
{{- end }}
|
||||
{{- if .Values.haulerJobs.manifests.enabled }}
|
||||
- name: wait-for-hauler-manifests-job
|
||||
image: {{ .Values.hauler.initContainers.image.repository }}:{{ .Values.hauler.initContainers.image.tag }}
|
||||
imagePullPolicy: {{ .Values.hauler.initContainers.imagePullPolicy }}
|
||||
args: ["wait", "--for=condition=complete", "job", "hauler-manifests-job", "--namespace", "{{ .Release.Namespace }}", "--timeout={{ .Values.hauler.initContainers.timeout }}"]
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: hauler-fileserver
|
||||
image: {{ .Values.hauler.image.repository }}:{{ .Values.hauler.image.tag }}
|
||||
imagePullPolicy: {{ .Values.hauler.imagePullPolicy }}
|
||||
args: ["store", "serve", "fileserver", "--port", "{{ .Values.haulerFileserver.port }}"]
|
||||
ports:
|
||||
- containerPort: {{ .Values.haulerFileserver.port }}
|
||||
volumeMounts:
|
||||
- name: hauler-data
|
||||
mountPath: /store
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
restartPolicy: Always
|
||||
serviceAccountName: hauler-service-account
|
||||
volumes:
|
||||
- name: hauler-data
|
||||
persistentVolumeClaim:
|
||||
claimName: hauler-data
|
||||
{{- end }}
|
||||
@@ -1,27 +0,0 @@
|
||||
{{- if and .Values.haulerFileserver.enabled .Values.haulerFileserver.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: hauler-fileserver
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "hauler.labels" . | nindent 4 }}
|
||||
spec:
|
||||
rules:
|
||||
- host: {{ .Values.haulerFileserver.ingress.hostname }}
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: hauler-fileserver
|
||||
port:
|
||||
number: {{ .Values.haulerFileserver.service.ports.targetPort }}
|
||||
path: /
|
||||
pathType: Prefix
|
||||
{{- if .Values.haulerFileserver.ingress.tls.enabled }}
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ .Values.haulerFileserver.ingress.hostname }}
|
||||
secretName: {{ .Values.haulerFileserver.ingress.tls.secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,18 +0,0 @@
|
||||
{{- if and .Values.haulerFileserver.enabled .Values.haulerFileserver.service.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: hauler-fileserver
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "hauler.labels" . | nindent 4 }}
|
||||
spec:
|
||||
selector:
|
||||
app: hauler-fileserver
|
||||
ports:
|
||||
- name: hauler-fileserver
|
||||
protocol: {{ .Values.haulerFileserver.service.ports.protocol }}
|
||||
port: {{ .Values.haulerFileserver.service.ports.port }}
|
||||
targetPort: {{ .Values.haulerFileserver.service.ports.targetPort }}
|
||||
type: {{ .Values.haulerFileserver.service.type }}
|
||||
{{- end }}
|
||||
@@ -1,76 +0,0 @@
|
||||
{{- if .Values.haulerRegistry.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: hauler-registry
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "hauler.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.haulerRegistry.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: hauler-registry
|
||||
{{- include "hauler.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: hauler-registry
|
||||
{{- include "hauler.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- if or .Values.haulerJobs.hauls.enabled .Values.haulerJobs.manifests.enabled }}
|
||||
initContainers:
|
||||
{{- if .Values.haulerJobs.hauls.enabled }}
|
||||
- name: wait-for-hauler-hauls-job
|
||||
image: {{ .Values.hauler.initContainers.image.repository }}:{{ .Values.hauler.initContainers.image.tag }}
|
||||
imagePullPolicy: {{ .Values.hauler.initContainers.imagePullPolicy }}
|
||||
args: ["wait", "--for=condition=complete", "job", "hauler-hauls-job", "--namespace", "{{ .Release.Namespace }}", "--timeout={{ .Values.hauler.initContainers.timeout }}"]
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
{{- end }}
|
||||
{{- if .Values.haulerJobs.manifests.enabled }}
|
||||
- name: wait-for-hauler-manifests-job
|
||||
image: {{ .Values.hauler.initContainers.image.repository }}:{{ .Values.hauler.initContainers.image.tag }}
|
||||
imagePullPolicy: {{ .Values.hauler.initContainers.imagePullPolicy }}
|
||||
args: ["wait", "--for=condition=complete", "job", "hauler-manifests-job", "--namespace", "{{ .Release.Namespace }}", "--timeout={{ .Values.hauler.initContainers.timeout }}"]
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: hauler-registry
|
||||
image: {{ .Values.hauler.image.repository }}:{{ .Values.hauler.image.tag }}
|
||||
imagePullPolicy: {{ .Values.hauler.imagePullPolicy }}
|
||||
args: ["store", "serve", "registry", "--port", "{{ .Values.haulerRegistry.port }}"]
|
||||
ports:
|
||||
- containerPort: {{ .Values.haulerRegistry.port }}
|
||||
volumeMounts:
|
||||
- name: hauler-data
|
||||
mountPath: /store
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
restartPolicy: Always
|
||||
serviceAccountName: hauler-service-account
|
||||
volumes:
|
||||
- name: hauler-data
|
||||
persistentVolumeClaim:
|
||||
claimName: hauler-data
|
||||
{{- end }}
|
||||
@@ -1,28 +0,0 @@
|
||||
{{- if and .Values.haulerRegistry.enabled .Values.haulerRegistry.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: hauler-registry
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "hauler.labels" . | nindent 4 }}
|
||||
spec:
|
||||
rules:
|
||||
- host: {{ .Values.haulerRegistry.ingress.hostname }}
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: hauler-registry
|
||||
port:
|
||||
number: {{ .Values.haulerRegistry.service.ports.targetPort }}
|
||||
path: /
|
||||
pathType: Prefix
|
||||
{{- if .Values.haulerRegistry.ingress.tls.enabled }}
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ .Values.haulerRegistry.ingress.hostname }}
|
||||
secretName: {{ .Values.haulerRegistry.ingress.tls.secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
{{- if and .Values.haulerRegistry.enabled .Values.haulerRegistry.service.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: hauler-registry
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "hauler.labels" . | nindent 4 }}
|
||||
spec:
|
||||
selector:
|
||||
app: hauler-registry
|
||||
ports:
|
||||
- name: hauler-registry
|
||||
protocol: {{ .Values.haulerRegistry.service.ports.protocol }}
|
||||
port: {{ .Values.haulerRegistry.service.ports.port }}
|
||||
targetPort: {{ .Values.haulerRegistry.service.ports.targetPort }}
|
||||
type: {{ .Values.haulerRegistry.service.type }}
|
||||
{{- end }}
|
||||
@@ -1,126 +0,0 @@
|
||||
{{- if .Values.haulerJobs.hauls.enabled }}
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: hauler-hauls-job
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "hauler.labels" . | nindent 4 }}
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
initContainers:
|
||||
- name: hauler-fetch-hauls
|
||||
image: {{ .Values.haulerJobs.image.repository }}:{{ .Values.haulerJobs.image.tag }}
|
||||
imagePullPolicy: {{ .Values.haulerJobs.imagePullPolicy }}
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
{{- range .Values.haulerJobs.hauls.artifacts }}
|
||||
curl -o /hauls/{{ .name }} {{ .path }} &&
|
||||
{{- end }}
|
||||
echo hauler fetch completed
|
||||
volumeMounts:
|
||||
- name: hauler-data
|
||||
mountPath: /hauls
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: hauler-load-hauls
|
||||
image: {{ .Values.hauler.image.repository }}:{{ .Values.hauler.image.tag }}
|
||||
imagePullPolicy: {{ .Values.hauler.imagePullPolicy }}
|
||||
args:
|
||||
- "store"
|
||||
- "load"
|
||||
{{- range .Values.haulerJobs.hauls.artifacts }}
|
||||
- "/hauls/{{ .name }}"
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: hauler-data
|
||||
mountPath: /hauls
|
||||
- name: hauler-data
|
||||
mountPath: /store
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
restartPolicy: OnFailure
|
||||
volumes:
|
||||
- name: hauler-data
|
||||
persistentVolumeClaim:
|
||||
claimName: hauler-data
|
||||
{{- end }}
|
||||
---
|
||||
{{- if .Values.haulerJobs.manifests.enabled }}
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: hauler-manifests-job
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "hauler.labels" . | nindent 4 }}
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
initContainers:
|
||||
- name: hauler-fetch-manifests
|
||||
image: {{ .Values.haulerJobs.image.repository }}:{{ .Values.haulerJobs.image.tag }}
|
||||
imagePullPolicy: {{ .Values.haulerJobs.imagePullPolicy }}
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
{{- range .Values.haulerJobs.manifests.artifacts }}
|
||||
curl -o /manifests/{{ .name }} {{ .path }} &&
|
||||
{{- end }}
|
||||
echo hauler fetch completed
|
||||
volumeMounts:
|
||||
- name: hauler-data
|
||||
mountPath: /manifests
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: hauler-load-manifests
|
||||
image: {{ .Values.hauler.image.repository }}:{{ .Values.hauler.image.tag }}
|
||||
imagePullPolicy: {{ .Values.hauler.imagePullPolicy }}
|
||||
args:
|
||||
{{- range .Values.haulerJobs.manifests.artifacts }}
|
||||
- "store"
|
||||
- "sync"
|
||||
- "--files"
|
||||
- "/manifests/{{ .name }}"
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: hauler-data
|
||||
mountPath: /manifests
|
||||
- name: hauler-data
|
||||
mountPath: /store
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
restartPolicy: OnFailure
|
||||
volumes:
|
||||
- name: hauler-data
|
||||
persistentVolumeClaim:
|
||||
claimName: hauler-data
|
||||
{{- end }}
|
||||
@@ -1,16 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: hauler-data
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "hauler.labels" . | nindent 4 }}
|
||||
spec:
|
||||
accessModes:
|
||||
- {{ .Values.hauler.data.pvc.accessModes }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.hauler.data.pvc.storageRequest }}
|
||||
{{- if .Values.hauler.data.pvc.storageClass }}
|
||||
storageClassName: {{ .Values.hauler.data.pvc.storageClass }}
|
||||
{{- end }}
|
||||
@@ -1,35 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: hauler-service-account
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "hauler.labels" . | nindent 4 }}
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: hauler-role
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "hauler.labels" . | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups: ["batch"]
|
||||
resources: ["jobs"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: hauler-role-binding
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "hauler.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: hauler-role
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: hauler-service-account
|
||||
namespace: {{ .Release.Namespace }}
|
||||
@@ -1,94 +0,0 @@
|
||||
# Helm Chart Values for Hauler
|
||||
# Docs: https://hauler.dev
|
||||
|
||||
hauler:
|
||||
image:
|
||||
repository: ghcr.io/rancherfederal/hauler
|
||||
tag: v1.0.2
|
||||
imagePullPolicy: Always
|
||||
|
||||
initContainers:
|
||||
image:
|
||||
repository: rancher/kubectl
|
||||
tag: v1.27.0 # update to your kubernetes version
|
||||
imagePullPolicy: Always
|
||||
timeout: 1h
|
||||
|
||||
data:
|
||||
pvc:
|
||||
accessModes: ReadWriteMany
|
||||
storageClass: longhorn # optional... will use default storage class
|
||||
storageRequest: 48Gi # recommended size of 3x the artifact(s)
|
||||
|
||||
# Helm Chart Values for the Hauler Jobs
|
||||
# Docs: https://rancherfederal.github.io/hauler-docs/docs/introduction/quickstart
|
||||
|
||||
haulerJobs:
|
||||
image:
|
||||
repository: rancher/shell
|
||||
tag: v0.1.22
|
||||
imagePullPolicy: Always
|
||||
|
||||
hauls:
|
||||
enabled: true
|
||||
artifacts:
|
||||
- path: https://raw.githubusercontent.com/rancherfederal/hauler/main/testdata/haul.tar.zst
|
||||
name: haul.tar.zst
|
||||
# - path: /path/to/additional-hauls.tar.zst
|
||||
# name: additional-hauls.tar.zst
|
||||
|
||||
manifests:
|
||||
enabled: true
|
||||
artifacts:
|
||||
- path: https://raw.githubusercontent.com/rancherfederal/hauler/main/testdata/hauler-manifest.yaml
|
||||
name: hauler-manifest.yaml
|
||||
# - path: /path/to/additional-manifests.yaml
|
||||
# name: additional-manifests.yaml
|
||||
|
||||
# Helm Chart Values for the Hauler Fileserver
|
||||
# Docs: https://rancherfederal.github.io/hauler-docs/docs/guides-references/command-line/hauler-store#hauler-store-serve-fileserver
|
||||
|
||||
haulerFileserver:
|
||||
enabled: true
|
||||
port: 8080 # default port for the fileserver
|
||||
replicas: 1
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
hostname: fileserver.ranchers.io
|
||||
tls:
|
||||
enabled: true
|
||||
source: secret # only supported source
|
||||
secretName: tls-certs # must be created outside of this chart
|
||||
|
||||
service:
|
||||
enabled: true
|
||||
type: ClusterIP
|
||||
ports:
|
||||
protocol: TCP
|
||||
port: 8080 # default port for the fileserver
|
||||
targetPort: 8080 # default port for the fileserver
|
||||
|
||||
# Helm Chart Values for the Hauler Registry
|
||||
# Docs: https://rancherfederal.github.io/hauler-docs/docs/guides-references/command-line/hauler-store#hauler-store-serve-registry
|
||||
|
||||
haulerRegistry:
|
||||
enabled: true
|
||||
port: 5000 # default port for the registry
|
||||
replicas: 1
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
hostname: registry.ranchers.io
|
||||
tls:
|
||||
enabled: true
|
||||
source: secret # only supported source
|
||||
secretName: tls-certs # must be created outside of this chart
|
||||
|
||||
service:
|
||||
enabled: true
|
||||
type: ClusterIP
|
||||
ports:
|
||||
protocol: TCP
|
||||
port: 5000 # default port for the registry
|
||||
targetPort: 5000 # default port for the registry
|
||||
177
docs/walkthrough.md
Normal file
177
docs/walkthrough.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Walkthrough
|
||||
|
||||
## Installation
|
||||
|
||||
The latest version of `hauler` is available as statically compiled binaries for most combinations of operating systems and architectures on the GitHub [releases](https://github.com/rancherfederal/hauler/releases) page.
|
||||
|
||||
## Quickstart
|
||||
|
||||
The tl;dr for how to use `hauler` to fetch, transport, and distribute `content`:
|
||||
|
||||
```bash
|
||||
# fetch some content
|
||||
hauler store add file "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
||||
hauler store add chart longhorn --repo "https://charts.longhorn.io"
|
||||
hauler store add image "rancher/cowsay"
|
||||
|
||||
# transport the content
|
||||
hauler store save
|
||||
|
||||
# <-airgap the haul.tar.zst file generated->
|
||||
|
||||
# load the content
|
||||
hauler store load
|
||||
|
||||
# serve the content
|
||||
hauler store serve
|
||||
```
|
||||
|
||||
While the example above fits into a quickstart, it falls short of demonstrating all the capabilities `hauler` has to offer, including taking advantage of its fully declarative nature. Keep reading the [Guided Examples](#Guided-Examples) below for a more thorough walkthrough of `haulers` full capabilities.
|
||||
|
||||
## Guided Examples
|
||||
|
||||
Since `hauler`'s primary objective is to simplify the content collection/distribution airgap process, a lot of the design revolves around the typical airgap workflow:
|
||||
|
||||
```bash
|
||||
fetch -> save - | <airgap> | -> validate/load -> distribute
|
||||
```
|
||||
|
||||
This is accomplished as follows:
|
||||
|
||||
```bash
|
||||
# fetch content
|
||||
hauler store add ...
|
||||
|
||||
# compress and archive content
|
||||
hauler store save
|
||||
|
||||
# <airgap>
|
||||
|
||||
# validate/load content
|
||||
hauler store load ...
|
||||
|
||||
# distribute content
|
||||
hauler store serve
|
||||
```
|
||||
|
||||
At this point you're probably wondering: what is `content`? In `hauler` land, there are a few important terms given to important resources:
|
||||
|
||||
* `artifact`: anything that can be represented as an [`oci artifact`](https://github.com/opencontainers/artifacts)
|
||||
* `content`: built in "primitive" types of `artifacts` that `hauler` understands
|
||||
|
||||
### Built in content
|
||||
|
||||
As of today, `hauler` understands three types of `content`, one with a strong legacy of community support and consensus ([`image-spec`]()), one with a finalized spec and experimental support ([`chart-spec`]()), and one generic type created just for `hauler`. These `content` types are outlined below:
|
||||
|
||||
__`files`__:
|
||||
|
||||
Generic content that can be represented as a file, either sourced locally or remotely.
|
||||
|
||||
```bash
|
||||
# local file
|
||||
hauler store add file path/to/local/file.txt
|
||||
|
||||
# remote file
|
||||
hauler store add file https://get.k3s.io
|
||||
```
|
||||
|
||||
__`images`__:
|
||||
|
||||
Any OCI compatible image can be fetched remotely.
|
||||
|
||||
```bash
|
||||
# "shorthand" image references
|
||||
hauler store add image rancher/k3s:v1.22.2-k3s1
|
||||
|
||||
# fully qualified image references
|
||||
hauler store add image ghcr.io/fluxcd/flux-cli@sha256:02aa820c3a9c57d67208afcfc4bce9661658c17d15940aea369da259d2b976dd
|
||||
```
|
||||
|
||||
__`charts`__:
|
||||
|
||||
Helm charts represented as OCI content.
|
||||
|
||||
```bash
|
||||
# add a helm chart (defaults to latest version)
|
||||
hauler store add chart loki --repo "https://grafana.github.io/helm-charts"
|
||||
|
||||
# add a specific version of a helm chart
|
||||
hauler store add chart loki --repo "https://grafana.github.io/helm-charts" --version 2.8.1
|
||||
|
||||
# install directly from the oci content
|
||||
HELM_EXPERIMENTAL_OCI=1 helm install loki oci://localhost:3000/library/loki --version 2.8.1
|
||||
```
|
||||
|
||||
> Note: `hauler` supports the currently experimental format of helm as OCI content, but can also be represented as the usual tarball if necessary
|
||||
|
||||
### Content API
|
||||
|
||||
While imperatively adding `content` to `hauler` is a simple way to get started, the recommended long term approach is to use the provided api that each `content` has, in conjunction with the `sync` command.
|
||||
|
||||
```bash
|
||||
# create a haul from declaratively defined content
|
||||
hauler store sync -f testdata/contents.yaml
|
||||
```
|
||||
|
||||
> For a commented view of the `contents` api, take a look at the `testdata` folder in the root of the project.
|
||||
|
||||
The API for each type of built-in `content` allows you to easily and declaratively define all the `content` that exist within a `haul`, and ensures a more gitops compatible workflow for managing the lifecycle of your `hauls`.
|
||||
|
||||
### Collections
|
||||
|
||||
Earlier we referred to `content` as "primitives". While the quotes justify the loose definition of that term, we call it that because they can be used to build groups of `content`, which we call `collections`.
|
||||
|
||||
`collections` are groups of 1 or more `contents` that collectively represent something desirable. Just like `content`, there are a handful that are built in to `hauler`.
|
||||
|
||||
Since `collections` usually contain more purposefully crafted `contents`, we restrict their use to the declarative commands (`sync`):
|
||||
|
||||
```bash
|
||||
# sync a collection
|
||||
hauler store sync -f my-collection.yaml
|
||||
|
||||
# sync sets of content/collection
|
||||
hauler store sync -f collection.yaml -f content.yaml
|
||||
```
|
||||
|
||||
__`thickcharts`__:
|
||||
|
||||
Thick Charts represent the combination of `charts` and `images`. When storing a thick chart, the chart _and_ the charts dependent images will be fetched and stored by `hauler`.
|
||||
|
||||
```yaml
|
||||
# thick-chart.yaml
|
||||
apiVersion: collection.hauler.cattle.io/v1alpha1
|
||||
kind: ThickCharts
|
||||
metadata:
|
||||
name: loki
|
||||
spec:
|
||||
charts:
|
||||
- name: loki
|
||||
repoURL: https://grafana.github.io/helm-charts
|
||||
```
|
||||
|
||||
When syncing the collection above, `hauler` will identify the images the chart depends on and store those too
|
||||
|
||||
> The method for identifying images is constantly changing, as of today, the chart is rendered and a configurable set of container defining json path's are processed. The most common paths are recognized by hauler, but this can be configured for the more niche CRDs out there.
|
||||
|
||||
__`k3s`__:
|
||||
|
||||
Combining `files` and `images`, full clusters can also be captured by `hauler` for further simplifying the already simple nature of `k3s`.
|
||||
|
||||
```yaml
|
||||
# k3s.yaml
|
||||
---
|
||||
apiVersion: collection.hauler.cattle.io/v1alpha1
|
||||
kind: K3s
|
||||
metadata:
|
||||
name: k3s
|
||||
spec:
|
||||
version: stable
|
||||
```
|
||||
|
||||
Using the collection above, the dependent files (`k3s` executable and `https://get.k3s.io` script) will be fetched, as well as all the dependent images.
|
||||
|
||||
> We know not everyone uses the get.k3s.io script to provision k3s, in the future this may change, but until then you're welcome to mix and match the `collection` with any of your own additional `content`
|
||||
|
||||
#### User defined `collections`
|
||||
|
||||
Although `content` and `collections` can only be used when they are baked in to `hauler`, the goal is to allow these to be securely user-defined, allowing you to define your own desirable `collection` types, and leave the heavy lifting to `hauler`. Check out our [roadmap](../ROADMAP.md) and [milestones]() for more info on that.
|
||||
236
go.mod
236
go.mod
@@ -1,175 +1,165 @@
|
||||
module github.com/rancherfederal/hauler
|
||||
|
||||
go 1.21
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be
|
||||
github.com/containerd/containerd v1.7.11
|
||||
github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2
|
||||
github.com/docker/go-metrics v0.0.1
|
||||
github.com/google/go-containerregistry v0.16.1
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/mholt/archiver/v3 v3.5.1
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/containerd/containerd v1.5.7
|
||||
github.com/distribution/distribution/v3 v3.0.0-20210926092439-1563384b69df
|
||||
github.com/google/go-containerregistry v0.6.0
|
||||
github.com/mholt/archiver/v3 v3.5.0
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.1.0-rc6
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/afero v1.10.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
golang.org/x/sync v0.6.0
|
||||
helm.sh/helm/v3 v3.14.2
|
||||
k8s.io/apimachinery v0.29.0
|
||||
k8s.io/client-go v0.29.0
|
||||
oras.land/oras-go v1.2.5
|
||||
github.com/opencontainers/image-spec v1.0.1
|
||||
github.com/rancher/wrangler v0.8.4
|
||||
github.com/rs/zerolog v1.26.0
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/spf13/cobra v1.2.1
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
helm.sh/helm/v3 v3.7.1
|
||||
k8s.io/apimachinery v0.22.2
|
||||
k8s.io/client-go v0.22.2
|
||||
oras.land/oras-go v0.4.0
|
||||
sigs.k8s.io/controller-runtime v0.10.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/MakeNowJust/heredoc v1.0.0 // indirect
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||
github.com/Microsoft/hcsshim v0.11.4 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.0 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d // indirect
|
||||
github.com/andybalholm/brotli v1.0.1 // indirect
|
||||
github.com/andybalholm/brotli v1.0.0 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 // indirect
|
||||
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd // indirect
|
||||
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b // indirect
|
||||
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chai2010/gettext-go v1.0.2 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.7.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.5.0 // indirect
|
||||
github.com/docker/cli v25.0.1+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker v25.0.5+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/cli v20.10.9+incompatible // indirect
|
||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||
github.com/docker/docker v20.10.9+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.6.4 // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
|
||||
github.com/dsnet/compress v0.0.1 // indirect
|
||||
github.com/evanphx/json-patch v4.11.0+incompatible // indirect
|
||||
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/go-errors/errors v1.4.2 // indirect
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
|
||||
github.com/go-logr/logr v1.3.0 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/fatih/color v1.9.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-errors/errors v1.0.1 // indirect
|
||||
github.com/go-logr/logr v0.4.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.5 // indirect
|
||||
github.com/go-openapi/swag v0.19.14 // indirect
|
||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.3 // indirect
|
||||
github.com/gomodule/redigo v1.8.2 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/go-cmp v0.5.6 // indirect
|
||||
github.com/google/gofuzz v1.1.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/googleapis/gnostic v0.5.5 // indirect
|
||||
github.com/gorilla/handlers v1.5.1 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/gosuri/uitable v0.0.4 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/imdario/mergo v0.3.13 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||
github.com/huandu/xstrings v1.3.2 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jmoiron/sqlx v1.3.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.16.5 // indirect
|
||||
github.com/klauspost/pgzip v1.2.5 // indirect
|
||||
github.com/json-iterator/go v1.1.11 // indirect
|
||||
github.com/klauspost/compress v1.13.6 // indirect
|
||||
github.com/klauspost/pgzip v1.2.4 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/lib/pq v1.10.0 // indirect
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-isatty v0.0.13 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/locker v1.0.1 // indirect
|
||||
github.com/moby/spdystream v0.2.0 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/nwaples/rardecode v1.1.0 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.2 // indirect
|
||||
github.com/prometheus/client_golang v1.16.0 // indirect
|
||||
github.com/prometheus/client_model v0.4.0 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/procfs v0.10.1 // indirect
|
||||
github.com/rubenv/sql-migrate v1.5.2 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.0.3 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.11.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.26.0 // indirect
|
||||
github.com/prometheus/procfs v0.6.0 // indirect
|
||||
github.com/rancher/lasso v0.0.0-20210616224652-fc3ebd901c08 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc // indirect
|
||||
github.com/russross/blackfriday v1.5.2 // indirect
|
||||
github.com/sergi/go-diff v1.2.0 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/ulikunitz/xz v0.5.9 // indirect
|
||||
github.com/vbatts/tar-split v0.11.3 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/stretchr/testify v1.7.0 // indirect
|
||||
github.com/ulikunitz/xz v0.5.7 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect
|
||||
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 // indirect
|
||||
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 // indirect
|
||||
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect
|
||||
go.opentelemetry.io/otel v1.19.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.19.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.19.0 // indirect
|
||||
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/net v0.23.0 // indirect
|
||||
golang.org/x/oauth2 v0.10.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/term v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
|
||||
golang.org/x/net v0.0.0-20210913180222-943fd674d43e // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c // indirect
|
||||
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||
google.golang.org/grpc v1.58.3 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20210719143636-1d5a45f8e492 // indirect
|
||||
google.golang.org/grpc v1.39.0 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/gorp.v1 v1.7.2 // 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.29.0 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.29.0 // indirect
|
||||
k8s.io/apiserver v0.29.0 // indirect
|
||||
k8s.io/cli-runtime v0.29.0 // indirect
|
||||
k8s.io/component-base v0.29.0 // indirect
|
||||
k8s.io/klog/v2 v2.110.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
|
||||
k8s.io/kubectl v0.29.0 // indirect
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
k8s.io/api v0.22.2 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.22.2 // indirect
|
||||
k8s.io/apiserver v0.22.2 // indirect
|
||||
k8s.io/cli-runtime v0.22.1 // indirect
|
||||
k8s.io/component-base v0.22.2 // indirect
|
||||
k8s.io/klog/v2 v2.9.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e // indirect
|
||||
k8s.io/kubectl v0.22.1 // indirect
|
||||
k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a // indirect
|
||||
sigs.k8s.io/kustomize/api v0.8.11 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.11.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect
|
||||
sigs.k8s.io/yaml v1.2.0 // indirect
|
||||
)
|
||||
|
||||
181
install.sh
181
install.sh
@@ -1,181 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Usage:
|
||||
# - curl -sfL... | ENV_VAR=... bash
|
||||
# - ENV_VAR=... ./install.sh
|
||||
#
|
||||
# Install Usage:
|
||||
# Install Latest Release
|
||||
# - curl -sfL https://get.hauler.dev | bash
|
||||
# - ./install.sh
|
||||
#
|
||||
# Install Specific Release
|
||||
# - curl -sfL https://get.hauler.dev | HAULER_VERSION=1.0.0 bash
|
||||
# - HAULER_VERSION=1.0.0 ./install.sh
|
||||
#
|
||||
# Uninstall Usage:
|
||||
# - curl -sfL https://get.hauler.dev | HAULER_UNINSTALL=true bash
|
||||
# - HAULER_UNINSTALL=true ./install.sh
|
||||
#
|
||||
# Documentation:
|
||||
# - https://hauler.dev
|
||||
# - https://github.com/rancherfederal/hauler
|
||||
|
||||
# set functions for debugging/logging
|
||||
function verbose {
|
||||
echo "$1"
|
||||
}
|
||||
|
||||
function info {
|
||||
echo && echo "[INFO] Hauler: $1"
|
||||
}
|
||||
|
||||
function warn {
|
||||
echo && echo "[WARN] Hauler: $1"
|
||||
}
|
||||
|
||||
function fatal {
|
||||
echo && echo "[ERROR] Hauler: $1"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# check for required dependencies
|
||||
for cmd in sudo rm curl grep mkdir sed awk openssl tar; do
|
||||
if ! command -v "$cmd" &> /dev/null; then
|
||||
fatal "$cmd is not installed"
|
||||
fi
|
||||
done
|
||||
|
||||
# set version environment variable
|
||||
if [ -z "${HAULER_VERSION}" ]; then
|
||||
version="${HAULER_VERSION:-$(curl -s https://api.github.com/repos/rancherfederal/hauler/releases/latest | grep '"tag_name":' | sed 's/.*"v\([^"]*\)".*/\1/')}"
|
||||
else
|
||||
version="${HAULER_VERSION}"
|
||||
fi
|
||||
|
||||
# set uninstall environment variable from argument or environment
|
||||
if [ "${HAULER_UNINSTALL}" = "true" ]; then
|
||||
# remove the hauler binary
|
||||
sudo rm -f /usr/local/bin/hauler || fatal "Failed to Remove Hauler from /usr/local/bin"
|
||||
|
||||
# remove the installation directory
|
||||
rm -rf "$HOME/.hauler" || fatal "Failed to Remove Directory: $HOME/.hauler"
|
||||
|
||||
info "Hauler Uninstalled Successfully"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# detect the operating system
|
||||
platform=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
case $platform in
|
||||
linux)
|
||||
platform="linux"
|
||||
;;
|
||||
darwin)
|
||||
platform="darwin"
|
||||
;;
|
||||
*)
|
||||
fatal "Unsupported Platform: $platform"
|
||||
;;
|
||||
esac
|
||||
|
||||
# detect the architecture
|
||||
arch=$(uname -m)
|
||||
case $arch in
|
||||
x86_64 | x86-32 | x64 | x32 | amd64)
|
||||
arch="amd64"
|
||||
;;
|
||||
aarch64 | arm64)
|
||||
arch="arm64"
|
||||
;;
|
||||
*)
|
||||
fatal "Unsupported Architecture: $arch"
|
||||
;;
|
||||
esac
|
||||
|
||||
# start hauler installation
|
||||
info "Starting Installation..."
|
||||
|
||||
# display the version, platform, and architecture
|
||||
verbose "- Version: v$version"
|
||||
verbose "- Platform: $platform"
|
||||
verbose "- Architecture: $arch"
|
||||
|
||||
# check if install directory exists, create it if not
|
||||
if [ ! -d "$HOME/.hauler" ]; then
|
||||
mkdir -p "$HOME/.hauler" || fatal "Failed to Create Directory: ~/.hauler"
|
||||
fi
|
||||
|
||||
# change to install directory
|
||||
cd "$HOME/.hauler" || fatal "Failed to Change Directory: ~/.hauler"
|
||||
|
||||
# download the checksum file
|
||||
if ! curl -sfOL "https://github.com/rancherfederal/hauler/releases/download/v${version}/hauler_${version}_checksums.txt"; then
|
||||
fatal "Failed to Download: hauler_${version}_checksums.txt"
|
||||
fi
|
||||
|
||||
# download the archive file
|
||||
if ! curl -sfOL "https://github.com/rancherfederal/hauler/releases/download/v${version}/hauler_${version}_${platform}_${arch}.tar.gz"; then
|
||||
fatal "Failed to Download: hauler_${version}_${platform}_${arch}.tar.gz"
|
||||
fi
|
||||
|
||||
# start hauler checksum verification
|
||||
info "Starting Checksum Verification..."
|
||||
|
||||
# verify the Hauler checksum
|
||||
expected_checksum=$(awk -v version="$version" -v platform="$platform" -v arch="$arch" '$2 == "hauler_"version"_"platform"_"arch".tar.gz" {print $1}' "hauler_${version}_checksums.txt")
|
||||
determined_checksum=$(openssl dgst -sha256 "hauler_${version}_${platform}_${arch}.tar.gz" | awk '{print $2}')
|
||||
|
||||
if [ -z "$expected_checksum" ]; then
|
||||
fatal "Failed to Locate Checksum: hauler_${version}_${platform}_${arch}.tar.gz"
|
||||
elif [ "$determined_checksum" = "$expected_checksum" ]; then
|
||||
verbose "- Expected Checksum: $expected_checksum"
|
||||
verbose "- Determined Checksum: $determined_checksum"
|
||||
verbose "- Successfully Verified Checksum: hauler_${version}_${platform}_${arch}.tar.gz"
|
||||
else
|
||||
verbose "- Expected: $expected_checksum"
|
||||
verbose "- Determined: $determined_checksum"
|
||||
fatal "Failed Checksum Verification: hauler_${version}_${platform}_${arch}.tar.gz"
|
||||
fi
|
||||
|
||||
# uncompress the archive
|
||||
tar -xzf "hauler_${version}_${platform}_${arch}.tar.gz" || fatal "Failed to Extract: hauler_${version}_${platform}_${arch}.tar.gz"
|
||||
|
||||
# install the binary
|
||||
case "$platform" in
|
||||
linux)
|
||||
sudo install -m 755 hauler /usr/local/bin || fatal "Failed to Install Hauler to /usr/local/bin"
|
||||
;;
|
||||
darwin)
|
||||
sudo install -m 755 hauler /usr/local/bin || fatal "Failed to Install Hauler to /usr/local/bin"
|
||||
;;
|
||||
*)
|
||||
fatal "Unsupported Platform or Architecture: $platform/$arch"
|
||||
;;
|
||||
esac
|
||||
|
||||
# add hauler to the path
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
echo "export PATH=$PATH:/usr/local/bin/" >> "$HOME/.bashrc"
|
||||
source "$HOME/.bashrc"
|
||||
elif [ -f "$HOME/.bash_profile" ]; then
|
||||
echo "export PATH=$PATH:/usr/local/bin/" >> "$HOME/.bash_profile"
|
||||
source "$HOME/.bash_profile"
|
||||
elif [ -f "$HOME/.zshrc" ]; then
|
||||
echo "export PATH=$PATH:/usr/local/bin/" >> "$HOME/.zshrc"
|
||||
source "$HOME/.zshrc"
|
||||
elif [ -f "$HOME/.profile" ]; then
|
||||
echo "export PATH=$PATH:/usr/local/bin/" >> "$HOME/.profile"
|
||||
source "$HOME/.profile"
|
||||
else
|
||||
echo "Failed to add /usr/local/bin to PATH: Unsupported Shell"
|
||||
fi
|
||||
|
||||
# display success message
|
||||
info "Successfully Installed at /usr/local/bin/hauler"
|
||||
|
||||
# display availability message
|
||||
verbose "- Hauler v${version} is now available for use!"
|
||||
|
||||
# display hauler docs message
|
||||
verbose "- Documentation: https://hauler.dev" && echo
|
||||
@@ -1,85 +0,0 @@
|
||||
package mapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
ccontent "github.com/containerd/containerd/content"
|
||||
"github.com/containerd/containerd/remotes"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"oras.land/oras-go/pkg/content"
|
||||
)
|
||||
|
||||
// NewMapperFileStore creates a new file store that uses mapper functions for each detected descriptor.
|
||||
// This extends content.File, and differs in that it allows much more functionality into how each descriptor is written.
|
||||
func NewMapperFileStore(root string, mapper map[string]Fn) *store {
|
||||
fs := content.NewFile(root)
|
||||
return &store{
|
||||
File: fs,
|
||||
mapper: mapper,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *store) Pusher(ctx context.Context, ref string) (remotes.Pusher, error) {
|
||||
var tag, hash string
|
||||
parts := strings.SplitN(ref, "@", 2)
|
||||
if len(parts) > 0 {
|
||||
tag = parts[0]
|
||||
}
|
||||
if len(parts) > 1 {
|
||||
hash = parts[1]
|
||||
}
|
||||
return &pusher{
|
||||
store: s.File,
|
||||
tag: tag,
|
||||
ref: hash,
|
||||
mapper: s.mapper,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type store struct {
|
||||
*content.File
|
||||
mapper map[string]Fn
|
||||
}
|
||||
|
||||
func (s *pusher) Push(ctx context.Context, desc ocispec.Descriptor) (ccontent.Writer, error) {
|
||||
// TODO: This is suuuuuper ugly... redo this when oras v2 is out
|
||||
if _, ok := content.ResolveName(desc); ok {
|
||||
p, err := s.store.Pusher(ctx, s.ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.Push(ctx, desc)
|
||||
}
|
||||
|
||||
// If no custom mapper found, fall back to content.File mapper
|
||||
if _, ok := s.mapper[desc.MediaType]; !ok {
|
||||
return content.NewIoContentWriter(ioutil.Discard, content.WithOutputHash(desc.Digest)), nil
|
||||
}
|
||||
|
||||
filename, err := s.mapper[desc.MediaType](desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fullFileName := filepath.Join(s.store.ResolvePath(""), filename)
|
||||
// TODO: Don't rewrite everytime, we can check the digest
|
||||
f, err := os.OpenFile(fullFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "pushing file")
|
||||
}
|
||||
|
||||
w := content.NewIoContentWriter(f, content.WithInputHash(desc.Digest), content.WithOutputHash(desc.Digest))
|
||||
return w, nil
|
||||
}
|
||||
|
||||
type pusher struct {
|
||||
store *content.File
|
||||
tag string
|
||||
ref string
|
||||
mapper map[string]Fn
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package mapper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"oras.land/oras-go/pkg/target"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/consts"
|
||||
)
|
||||
|
||||
type Fn func(desc ocispec.Descriptor) (string, error)
|
||||
|
||||
// FromManifest will return the appropriate content store given a reference and source type adequate for storing the results on disk
|
||||
func FromManifest(manifest ocispec.Manifest, root string) (target.Target, error) {
|
||||
// TODO: Don't rely solely on config mediatype
|
||||
switch manifest.Config.MediaType {
|
||||
case consts.DockerConfigJSON, consts.OCIManifestSchema1:
|
||||
s := NewMapperFileStore(root, Images())
|
||||
defer s.Close()
|
||||
return s, nil
|
||||
|
||||
case consts.ChartLayerMediaType, consts.ChartConfigMediaType:
|
||||
s := NewMapperFileStore(root, Chart())
|
||||
defer s.Close()
|
||||
return s, nil
|
||||
|
||||
default:
|
||||
s := NewMapperFileStore(root, nil)
|
||||
defer s.Close()
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
|
||||
func Images() map[string]Fn {
|
||||
m := make(map[string]Fn)
|
||||
|
||||
manifestMapperFn := Fn(func(desc ocispec.Descriptor) (string, error) {
|
||||
return "manifest.json", nil
|
||||
})
|
||||
|
||||
for _, l := range []string{consts.DockerManifestSchema2, consts.DockerManifestListSchema2, consts.OCIManifestSchema1} {
|
||||
m[l] = manifestMapperFn
|
||||
}
|
||||
|
||||
layerMapperFn := Fn(func(desc ocispec.Descriptor) (string, error) {
|
||||
return fmt.Sprintf("%s.tar.gz", desc.Digest.String()), nil
|
||||
})
|
||||
|
||||
for _, l := range []string{consts.OCILayer, consts.DockerLayer} {
|
||||
m[l] = layerMapperFn
|
||||
}
|
||||
|
||||
configMapperFn := Fn(func(desc ocispec.Descriptor) (string, error) {
|
||||
return "config.json", nil
|
||||
})
|
||||
|
||||
for _, l := range []string{consts.DockerConfigJSON} {
|
||||
m[l] = configMapperFn
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func Chart() map[string]Fn {
|
||||
m := make(map[string]Fn)
|
||||
|
||||
chartMapperFn := Fn(func(desc ocispec.Descriptor) (string, error) {
|
||||
f := "chart.tar.gz"
|
||||
if _, ok := desc.Annotations[ocispec.AnnotationTitle]; ok {
|
||||
f = desc.Annotations[ocispec.AnnotationTitle]
|
||||
}
|
||||
return f, nil
|
||||
})
|
||||
|
||||
provMapperFn := Fn(func(desc ocispec.Descriptor) (string, error) {
|
||||
return "prov.json", nil
|
||||
})
|
||||
|
||||
m[consts.ChartLayerMediaType] = chartMapperFn
|
||||
m[consts.ProvLayerMediaType] = provMapperFn
|
||||
return m
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type FileConfig struct {
|
||||
Root string
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
// NewFile returns a fileserver
|
||||
// TODO: Better configs
|
||||
func NewFile(ctx context.Context, cfg FileConfig) (Server, error) {
|
||||
r := mux.NewRouter()
|
||||
r.PathPrefix("/").Handler(handlers.LoggingHandler(os.Stdout, http.StripPrefix("/", http.FileServer(http.Dir(cfg.Root)))))
|
||||
if cfg.Root == "" {
|
||||
cfg.Root = "."
|
||||
}
|
||||
|
||||
if cfg.Port == 0 {
|
||||
cfg.Port = 8080
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Handler: r,
|
||||
Addr: fmt.Sprintf(":%d", cfg.Port),
|
||||
WriteTimeout: 15 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
}
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/distribution/distribution/v3/configuration"
|
||||
"github.com/distribution/distribution/v3/registry"
|
||||
"github.com/distribution/distribution/v3/registry/handlers"
|
||||
"github.com/docker/go-metrics"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func NewRegistry(ctx context.Context, cfg *configuration.Configuration) (*registry.Registry, error) {
|
||||
r, err := registry.NewRegistry(ctx, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cfg.HTTP.Debug.Prometheus.Enabled {
|
||||
path := cfg.HTTP.Debug.Prometheus.Path
|
||||
if path == "" {
|
||||
path = "/metrics"
|
||||
}
|
||||
http.Handle(path, metrics.Handler())
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
type tmpRegistryServer struct {
|
||||
*httptest.Server
|
||||
}
|
||||
|
||||
func NewTempRegistry(ctx context.Context, root string) *tmpRegistryServer {
|
||||
cfg := &configuration.Configuration{
|
||||
Version: "0.1",
|
||||
Storage: configuration.Storage{
|
||||
"cache": configuration.Parameters{"blobdescriptor": "inmemory"},
|
||||
"filesystem": configuration.Parameters{"rootdirectory": root},
|
||||
},
|
||||
}
|
||||
// Add validation configuration
|
||||
cfg.Validation.Manifests.URLs.Allow = []string{".+"}
|
||||
|
||||
cfg.Log.Level = "error"
|
||||
cfg.HTTP.Headers = http.Header{
|
||||
"X-Content-Type-Options": []string{"nosniff"},
|
||||
}
|
||||
|
||||
l, err := logrus.ParseLevel("panic")
|
||||
if err != nil {
|
||||
l = logrus.ErrorLevel
|
||||
}
|
||||
logrus.SetLevel(l)
|
||||
|
||||
app := handlers.NewApp(ctx, cfg)
|
||||
app.RegisterHealthChecks()
|
||||
handler := alive("/", app)
|
||||
|
||||
s := httptest.NewUnstartedServer(handler)
|
||||
return &tmpRegistryServer{
|
||||
Server: s,
|
||||
}
|
||||
}
|
||||
|
||||
// Registry returns the URL of the server without the protocol, suitable for content references
|
||||
func (t *tmpRegistryServer) Registry() string {
|
||||
return strings.Replace(t.Server.URL, "http://", "", 1)
|
||||
}
|
||||
|
||||
func (t *tmpRegistryServer) Start() error {
|
||||
t.Server.Start()
|
||||
|
||||
err := retry(5, 1*time.Second, func() (err error) {
|
||||
resp, err := http.Get(t.Server.URL + "/v2")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
return errors.New("to start temporary registry")
|
||||
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *tmpRegistryServer) Stop() {
|
||||
t.Server.Close()
|
||||
}
|
||||
|
||||
func alive(path string, handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == path {
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func retry(attempts int, sleep time.Duration, f func() error) (err error) {
|
||||
for i := 0; i < attempts; i++ {
|
||||
if i > 0 {
|
||||
time.Sleep(sleep)
|
||||
sleep *= 2
|
||||
}
|
||||
err = f()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("after %d attempts, last error: %s", attempts, err)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package server
|
||||
|
||||
type Server interface {
|
||||
ListenAndServe() error
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 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 version
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/common-nighthawk/go-figure"
|
||||
)
|
||||
|
||||
const unknown = "unknown"
|
||||
|
||||
// Base version information.
|
||||
//
|
||||
// This is the fallback data used when version information from git is not
|
||||
// provided via go ldflags.
|
||||
var (
|
||||
// Output of "git describe". The prerequisite is that the
|
||||
// branch should be tagged using the correct versioning strategy.
|
||||
gitVersion = "devel"
|
||||
// SHA1 from git, output of $(git rev-parse HEAD)
|
||||
gitCommit = unknown
|
||||
// State of git tree, either "clean" or "dirty"
|
||||
gitTreeState = unknown
|
||||
// Build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
buildDate = unknown
|
||||
// flag to print the ascii name banner
|
||||
asciiName = "true"
|
||||
// goVersion is the used golang version.
|
||||
goVersion = unknown
|
||||
// compiler is the used golang compiler.
|
||||
compiler = unknown
|
||||
// platform is the used os/arch identifier.
|
||||
platform = unknown
|
||||
|
||||
once sync.Once
|
||||
info = Info{}
|
||||
)
|
||||
|
||||
type Info struct {
|
||||
GitVersion string `json:"gitVersion"`
|
||||
GitCommit string `json:"gitCommit"`
|
||||
GitTreeState string `json:"gitTreeState"`
|
||||
BuildDate string `json:"buildDate"`
|
||||
GoVersion string `json:"goVersion"`
|
||||
Compiler string `json:"compiler"`
|
||||
Platform string `json:"platform"`
|
||||
|
||||
ASCIIName string `json:"-"`
|
||||
FontName string `json:"-"`
|
||||
Name string `json:"-"`
|
||||
Description string `json:"-"`
|
||||
}
|
||||
|
||||
func getBuildInfo() *debug.BuildInfo {
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return bi
|
||||
}
|
||||
|
||||
func getGitVersion(bi *debug.BuildInfo) string {
|
||||
if bi == nil {
|
||||
return unknown
|
||||
}
|
||||
|
||||
// TODO: remove this when the issue https://github.com/golang/go/issues/29228 is fixed
|
||||
if bi.Main.Version == "(devel)" || bi.Main.Version == "" {
|
||||
return gitVersion
|
||||
}
|
||||
|
||||
return bi.Main.Version
|
||||
}
|
||||
|
||||
func getCommit(bi *debug.BuildInfo) string {
|
||||
return getKey(bi, "vcs.revision")
|
||||
}
|
||||
|
||||
func getDirty(bi *debug.BuildInfo) string {
|
||||
modified := getKey(bi, "vcs.modified")
|
||||
if modified == "true" {
|
||||
return "dirty"
|
||||
}
|
||||
if modified == "false" {
|
||||
return "clean"
|
||||
}
|
||||
return unknown
|
||||
}
|
||||
|
||||
func getBuildDate(bi *debug.BuildInfo) string {
|
||||
buildTime := getKey(bi, "vcs.time")
|
||||
t, err := time.Parse("2006-01-02T15:04:05Z", buildTime)
|
||||
if err != nil {
|
||||
return unknown
|
||||
}
|
||||
return t.Format("2006-01-02T15:04:05")
|
||||
}
|
||||
|
||||
func getKey(bi *debug.BuildInfo, key string) string {
|
||||
if bi == nil {
|
||||
return unknown
|
||||
}
|
||||
for _, iter := range bi.Settings {
|
||||
if iter.Key == key {
|
||||
return iter.Value
|
||||
}
|
||||
}
|
||||
return unknown
|
||||
}
|
||||
|
||||
// GetVersionInfo represents known information on how this binary was built.
|
||||
func GetVersionInfo() Info {
|
||||
once.Do(func() {
|
||||
buildInfo := getBuildInfo()
|
||||
gitVersion = getGitVersion(buildInfo)
|
||||
if gitCommit == unknown {
|
||||
gitCommit = getCommit(buildInfo)
|
||||
}
|
||||
|
||||
if gitTreeState == unknown {
|
||||
gitTreeState = getDirty(buildInfo)
|
||||
}
|
||||
|
||||
if buildDate == unknown {
|
||||
buildDate = getBuildDate(buildInfo)
|
||||
}
|
||||
|
||||
if goVersion == unknown {
|
||||
goVersion = runtime.Version()
|
||||
}
|
||||
|
||||
if compiler == unknown {
|
||||
compiler = runtime.Compiler
|
||||
}
|
||||
|
||||
if platform == unknown {
|
||||
platform = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
|
||||
info = Info{
|
||||
ASCIIName: asciiName,
|
||||
GitVersion: gitVersion,
|
||||
GitCommit: gitCommit,
|
||||
GitTreeState: gitTreeState,
|
||||
BuildDate: buildDate,
|
||||
GoVersion: goVersion,
|
||||
Compiler: compiler,
|
||||
Platform: platform,
|
||||
}
|
||||
})
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
// String returns the string representation of the version info
|
||||
func (i *Info) String() string {
|
||||
b := strings.Builder{}
|
||||
w := tabwriter.NewWriter(&b, 0, 0, 2, ' ', 0)
|
||||
|
||||
// name and description are optional.
|
||||
if i.Name != "" {
|
||||
if i.ASCIIName == "true" {
|
||||
f := figure.NewFigure(strings.ToUpper(i.Name), i.FontName, true)
|
||||
_, _ = fmt.Fprint(w, f.String())
|
||||
}
|
||||
_, _ = fmt.Fprint(w, i.Name)
|
||||
if i.Description != "" {
|
||||
_, _ = fmt.Fprintf(w, ": %s", i.Description)
|
||||
}
|
||||
_, _ = fmt.Fprint(w, "\n\n")
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(w, "GitVersion:\t%s\n", i.GitVersion)
|
||||
_, _ = fmt.Fprintf(w, "GitCommit:\t%s\n", i.GitCommit)
|
||||
_, _ = fmt.Fprintf(w, "GitTreeState:\t%s\n", i.GitTreeState)
|
||||
_, _ = fmt.Fprintf(w, "BuildDate:\t%s\n", i.BuildDate)
|
||||
_, _ = fmt.Fprintf(w, "GoVersion:\t%s\n", i.GoVersion)
|
||||
_, _ = fmt.Fprintf(w, "Compiler:\t%s\n", i.Compiler)
|
||||
_, _ = fmt.Fprintf(w, "Platform:\t%s\n", i.Platform)
|
||||
|
||||
_ = w.Flush()
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// JSONString returns the JSON representation of the version info
|
||||
func (i *Info) JSONString() (string, error) {
|
||||
b, err := json.MarshalIndent(i, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func (i *Info) CheckFontName(fontName string) bool {
|
||||
assetNames := figure.AssetNames()
|
||||
|
||||
for _, font := range assetNames {
|
||||
if strings.Contains(font, fontName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "font not valid, using default")
|
||||
return false
|
||||
}
|
||||
@@ -21,27 +21,24 @@ type ChartSpec struct {
|
||||
}
|
||||
|
||||
type Chart struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
RepoURL string `json:"repoURL,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Name string `json:"name"`
|
||||
RepoURL string `json:"repoURL"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type ThickCharts struct {
|
||||
*metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ThickChartSpec `json:"spec,omitempty"`
|
||||
Spec ChartSpec `json:"spec,omitempty"`
|
||||
}
|
||||
|
||||
type ThickChartSpec struct {
|
||||
Charts []ThickChart `json:"charts,omitempty"`
|
||||
ThickCharts []ThickChart `json:"charts,omitempty"`
|
||||
}
|
||||
|
||||
type ThickChart struct {
|
||||
Chart `json:",inline,omitempty"`
|
||||
ExtraImages []ChartImage `json:"extraImages,omitempty"`
|
||||
}
|
||||
|
||||
type ChartImage struct {
|
||||
Reference string `json:"ref"`
|
||||
Name string `json:"name"`
|
||||
RepoURL string `json:"repoURL"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
@@ -18,10 +18,6 @@ type FileSpec struct {
|
||||
}
|
||||
|
||||
type File struct {
|
||||
// Path is the path to the file contents, can be a local or remote path
|
||||
Path string `json:"path"`
|
||||
|
||||
// Name is an optional field specifying the name of the file when specified,
|
||||
// it will override any dynamic name discovery from Path
|
||||
Ref string `json:"ref"`
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/controller-runtime/pkg/scheme"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -12,7 +13,7 @@ const (
|
||||
|
||||
var (
|
||||
ContentGroupVersion = schema.GroupVersion{Group: ContentGroup, Version: Version}
|
||||
// SchemeBuilder = &scheme.Builder{GroupVersion: ContentGroupVersion}
|
||||
SchemeBuilder = &scheme.Builder{GroupVersion: ContentGroupVersion}
|
||||
|
||||
CollectionGroupVersion = schema.GroupVersion{Group: CollectionGroup, Version: Version}
|
||||
)
|
||||
|
||||
@@ -18,14 +18,5 @@ type ImageSpec struct {
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
// Name is the full location for the image, can be referenced by tags or digests
|
||||
Name string `json:"name"`
|
||||
|
||||
// Path is the path to the cosign public key used for verifying image signatures
|
||||
//Key string `json:"key,omitempty"`
|
||||
Key string `json:"key"`
|
||||
|
||||
// Platform of the image to be pulled. If not specified, all platforms will be pulled.
|
||||
//Platform string `json:"key,omitempty"`
|
||||
Platform string `json:"platform"`
|
||||
Ref string `json:"ref"`
|
||||
}
|
||||
|
||||
47
pkg/apis/hauler.cattle.io/v1alpha2/chart.go
Normal file
47
pkg/apis/hauler.cattle.io/v1alpha2/chart.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package v1alpha2
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
ChartsContentKind = "Charts"
|
||||
ChartsCollectionKind = "ThickCharts"
|
||||
)
|
||||
|
||||
type Charts struct {
|
||||
*metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ChartSpec `json:"spec,omitempty"`
|
||||
}
|
||||
|
||||
type ChartSpec struct {
|
||||
Charts []Chart `json:"charts,omitempty"`
|
||||
}
|
||||
|
||||
type Chart struct {
|
||||
Name string `json:"name"`
|
||||
RepoURL string `json:"repoURL"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type ThickCharts struct {
|
||||
*metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ChartSpec `json:"spec,omitempty"`
|
||||
}
|
||||
|
||||
type ThickChartSpec struct {
|
||||
Charts []ThickChart `json:"charts,omitempty"`
|
||||
}
|
||||
|
||||
type ThickChart struct {
|
||||
Chart `json:",inline,omitempty"`
|
||||
ExtraImages []ChartImage `json:"extraImages,omitempty"`
|
||||
}
|
||||
|
||||
type ChartImage struct {
|
||||
Reference string `json:"ref"`
|
||||
}
|
||||
21
pkg/apis/hauler.cattle.io/v1alpha2/driver.go
Normal file
21
pkg/apis/hauler.cattle.io/v1alpha2/driver.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package v1alpha2
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
DriverContentKind = "Driver"
|
||||
)
|
||||
|
||||
type Driver struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec DriverSpec `json:"spec"`
|
||||
}
|
||||
|
||||
type DriverSpec struct {
|
||||
Type string `json:"type"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
27
pkg/apis/hauler.cattle.io/v1alpha2/file.go
Normal file
27
pkg/apis/hauler.cattle.io/v1alpha2/file.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package v1alpha2
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const FilesContentKind = "Files"
|
||||
|
||||
type Files struct {
|
||||
*metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec FileSpec `json:"spec,omitempty"`
|
||||
}
|
||||
|
||||
type FileSpec struct {
|
||||
Files []File `json:"files,omitempty"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
// Path is the path to the file contents, can be a local or remote path
|
||||
Path string `json:"path"`
|
||||
|
||||
// Name is an optional field specifying the name of the file. When specified,
|
||||
// it will override any dynamic name discovery from Path
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
19
pkg/apis/hauler.cattle.io/v1alpha2/groupversion_info.go
Normal file
19
pkg/apis/hauler.cattle.io/v1alpha2/groupversion_info.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package v1alpha2
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/controller-runtime/pkg/scheme"
|
||||
)
|
||||
|
||||
const (
|
||||
Version = "v1alpha2"
|
||||
ContentGroup = "content.hauler.cattle.io"
|
||||
CollectionGroup = "collection.hauler.cattle.io"
|
||||
)
|
||||
|
||||
var (
|
||||
ContentGroupVersion = schema.GroupVersion{Group: ContentGroup, Version: Version}
|
||||
SchemeBuilder = &scheme.Builder{GroupVersion: ContentGroupVersion}
|
||||
|
||||
CollectionGroupVersion = schema.GroupVersion{Group: CollectionGroup, Version: Version}
|
||||
)
|
||||
22
pkg/apis/hauler.cattle.io/v1alpha2/image.go
Normal file
22
pkg/apis/hauler.cattle.io/v1alpha2/image.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package v1alpha2
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const ImagesContentKind = "Images"
|
||||
|
||||
type Images struct {
|
||||
*metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ImageSpec `json:"spec,omitempty"`
|
||||
}
|
||||
|
||||
type ImageSpec struct {
|
||||
Images []Image `json:"images,omitempty"`
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
30
pkg/apis/hauler.cattle.io/v1alpha2/imagetxt.go
Normal file
30
pkg/apis/hauler.cattle.io/v1alpha2/imagetxt.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package v1alpha2
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
ImageTxtsContentKind = "ImageTxts"
|
||||
)
|
||||
|
||||
type ImageTxts struct {
|
||||
*metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ImageTxtsSpec `json:"spec,omitempty"`
|
||||
}
|
||||
|
||||
type ImageTxtsSpec struct {
|
||||
ImageTxts []ImageTxt `json:"imageTxts,omitempty"`
|
||||
}
|
||||
|
||||
type ImageTxt struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Sources ImageTxtSources `json:"sources,omitempty"`
|
||||
}
|
||||
|
||||
type ImageTxtSources struct {
|
||||
Include []string `json:"include,omitempty"`
|
||||
Exclude []string `json:"exclude,omitempty"`
|
||||
}
|
||||
17
pkg/apis/hauler.cattle.io/v1alpha2/k3s.go
Normal file
17
pkg/apis/hauler.cattle.io/v1alpha2/k3s.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package v1alpha2
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
const K3sCollectionKind = "K3s"
|
||||
|
||||
type K3s struct {
|
||||
*metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec K3sSpec `json:"spec,omitempty"`
|
||||
}
|
||||
|
||||
type K3sSpec struct {
|
||||
Version string `json:"version"`
|
||||
Arch string `json:"arch"`
|
||||
}
|
||||
10
pkg/artifact/config.go
Normal file
10
pkg/artifact/config.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package artifact
|
||||
|
||||
import v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
|
||||
type Config interface {
|
||||
// Raw returns the config bytes
|
||||
Raw() ([]byte, error)
|
||||
|
||||
Descriptor() (v1.Descriptor, error)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package layer
|
||||
package local
|
||||
|
||||
import (
|
||||
"io"
|
||||
@@ -6,16 +6,14 @@ import (
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
gtypes "github.com/google/go-containerregistry/pkg/v1/types"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/consts"
|
||||
"github.com/rancherfederal/hauler/pkg/artifact/types"
|
||||
)
|
||||
|
||||
type Opener func() (io.ReadCloser, error)
|
||||
|
||||
func FromOpener(opener Opener, opts ...Option) (v1.Layer, error) {
|
||||
func LayerFromOpener(opener Opener, opts ...LayerOption) (v1.Layer, error) {
|
||||
var err error
|
||||
|
||||
layer := &layer{
|
||||
mediaType: consts.UnknownLayer,
|
||||
mediaType: types.UnknownLayer,
|
||||
annotations: make(map[string]string, 1),
|
||||
}
|
||||
|
||||
@@ -25,7 +23,7 @@ func FromOpener(opener Opener, opts ...Option) (v1.Layer, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: actually compress this
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
@@ -53,15 +51,15 @@ func compute(opener Opener) (v1.Hash, int64, error) {
|
||||
return v1.SHA256(rc)
|
||||
}
|
||||
|
||||
type Option func(*layer)
|
||||
type LayerOption func(*layer)
|
||||
|
||||
func WithMediaType(mt string) Option {
|
||||
func WithMediaType(mt string) LayerOption {
|
||||
return func(l *layer) {
|
||||
l.mediaType = mt
|
||||
}
|
||||
}
|
||||
|
||||
func WithAnnotations(annotations map[string]string) Option {
|
||||
func WithAnnotations(annotations map[string]string) LayerOption {
|
||||
return func(l *layer) {
|
||||
if l.annotations == nil {
|
||||
l.annotations = make(map[string]string)
|
||||
25
pkg/artifact/local/opener.go
Normal file
25
pkg/artifact/local/opener.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Opener func() (io.ReadCloser, error)
|
||||
|
||||
func LocalOpener(path string) Opener {
|
||||
return func() (io.ReadCloser, error) {
|
||||
return os.Open(path)
|
||||
}
|
||||
}
|
||||
|
||||
func RemoteOpener(url string) Opener {
|
||||
return func() (io.ReadCloser, error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Body, nil
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package artifacts
|
||||
package artifact
|
||||
|
||||
import "github.com/google/go-containerregistry/pkg/v1"
|
||||
import (
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1"
|
||||
)
|
||||
|
||||
// OCI is the bare minimum we need to represent an artifact in an oci layout
|
||||
// At a high level, it is not constrained by an Image's config, manifests, and layer ordinality
|
||||
@@ -15,7 +18,7 @@ type OCI interface {
|
||||
Layers() ([]v1.Layer, error)
|
||||
}
|
||||
|
||||
type OCICollection interface {
|
||||
type Collection interface {
|
||||
// Contents returns the list of contents in the collection
|
||||
Contents() (map[string]OCI, error)
|
||||
Contents() (map[name.Reference]OCI, error)
|
||||
}
|
||||
37
pkg/artifact/types/types.go
Normal file
37
pkg/artifact/types/types.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package types
|
||||
|
||||
const (
|
||||
OCIManifestSchema1 = "application/vnd.oci.image.manifest.v1+json"
|
||||
DockerManifestSchema2 = "application/vnd.docker.distribution.manifest.v2+json"
|
||||
|
||||
DockerConfigJSON = "application/vnd.docker.container.image.v1+json"
|
||||
|
||||
// ChartConfigMediaType is the reserved media type for the Helm chart manifest config
|
||||
ChartConfigMediaType = "application/vnd.cncf.helm.config.v1+json"
|
||||
|
||||
// ChartLayerMediaType is the reserved media type for Helm chart package content
|
||||
ChartLayerMediaType = "application/vnd.cncf.helm.chart.content.v1.tar+gzip"
|
||||
|
||||
// ProvLayerMediaType is the reserved media type for Helm chart provenance files
|
||||
ProvLayerMediaType = "application/vnd.cncf.helm.chart.provenance.v1.prov"
|
||||
|
||||
// FileLayerMediaType is the reserved media type for File content layers
|
||||
FileLayerMediaType = "application/vnd.content.hauler.file.layer.v1"
|
||||
|
||||
// FileConfigMediaType is the reserved media type for File config
|
||||
FileConfigMediaType = "application/vnd.content.hauler.file.config.v1+json"
|
||||
|
||||
// WasmArtifactLayerMediaType is the reserved media type for WASM artifact layers
|
||||
WasmArtifactLayerMediaType = "application/vnd.wasm.content.layer.v1+wasm"
|
||||
|
||||
// WasmConfigMediaType is the reserved media type for WASM configs
|
||||
WasmConfigMediaType = "application/vnd.wasm.config.v1+json"
|
||||
|
||||
UnknownManifest = "application/vnd.hauler.cattle.io.unknown.v1+json"
|
||||
UnknownLayer = "application/vnd.content.hauler.unknown.layer"
|
||||
|
||||
OCIVendorPrefix = "vnd.oci"
|
||||
DockerVendorPrefix = "vnd.docker"
|
||||
HaulerVendorPrefix = "vnd.hauler"
|
||||
OCIImageIndexFile = "index.json"
|
||||
)
|
||||
@@ -1,92 +0,0 @@
|
||||
package artifacts
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/consts"
|
||||
)
|
||||
|
||||
var _ partial.Describable = (*marshallableConfig)(nil)
|
||||
|
||||
type Config interface {
|
||||
// Raw returns the config bytes
|
||||
Raw() ([]byte, error)
|
||||
|
||||
Digest() (v1.Hash, error)
|
||||
|
||||
MediaType() (types.MediaType, error)
|
||||
|
||||
Size() (int64, error)
|
||||
}
|
||||
|
||||
type Marshallable interface{}
|
||||
|
||||
type ConfigOption func(*marshallableConfig)
|
||||
|
||||
// ToConfig takes anything that is marshallabe and converts it into a Config
|
||||
func ToConfig(i Marshallable, opts ...ConfigOption) Config {
|
||||
mc := &marshallableConfig{Marshallable: i}
|
||||
for _, o := range opts {
|
||||
o(mc)
|
||||
}
|
||||
return mc
|
||||
}
|
||||
|
||||
func WithConfigMediaType(mediaType string) ConfigOption {
|
||||
return func(config *marshallableConfig) {
|
||||
config.mediaType = mediaType
|
||||
}
|
||||
}
|
||||
|
||||
// marshallableConfig implements Config using helper methods
|
||||
type marshallableConfig struct {
|
||||
Marshallable
|
||||
|
||||
mediaType string
|
||||
}
|
||||
|
||||
func (c *marshallableConfig) MediaType() (types.MediaType, error) {
|
||||
mt := c.mediaType
|
||||
if mt == "" {
|
||||
mt = consts.UnknownManifest
|
||||
}
|
||||
return types.MediaType(mt), nil
|
||||
}
|
||||
|
||||
func (c *marshallableConfig) Raw() ([]byte, error) {
|
||||
return json.Marshal(c.Marshallable)
|
||||
}
|
||||
|
||||
func (c *marshallableConfig) Digest() (v1.Hash, error) {
|
||||
return Digest(c)
|
||||
}
|
||||
|
||||
func (c *marshallableConfig) Size() (int64, error) {
|
||||
return Size(c)
|
||||
}
|
||||
|
||||
type WithRawConfig interface {
|
||||
Raw() ([]byte, error)
|
||||
}
|
||||
|
||||
func Digest(c WithRawConfig) (v1.Hash, error) {
|
||||
b, err := c.Raw()
|
||||
if err != nil {
|
||||
return v1.Hash{}, err
|
||||
}
|
||||
digest, _, err := v1.SHA256(bytes.NewReader(b))
|
||||
return digest, err
|
||||
}
|
||||
|
||||
func Size(c WithRawConfig) (int64, error) {
|
||||
b, err := c.Raw()
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return int64(len(b)), nil
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
gv1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
gtypes "github.com/google/go-containerregistry/pkg/v1/types"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts"
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/file/getter"
|
||||
"github.com/rancherfederal/hauler/pkg/consts"
|
||||
)
|
||||
|
||||
// interface guard
|
||||
var _ artifacts.OCI = (*File)(nil)
|
||||
|
||||
// File implements the OCI interface for File API objects. API spec information is
|
||||
// stored into the Path field.
|
||||
type File struct {
|
||||
Path string
|
||||
|
||||
computed bool
|
||||
client *getter.Client
|
||||
config artifacts.Config
|
||||
blob gv1.Layer
|
||||
manifest *gv1.Manifest
|
||||
annotations map[string]string
|
||||
}
|
||||
|
||||
func NewFile(path string, opts ...Option) *File {
|
||||
client := getter.NewClient(getter.ClientOptions{})
|
||||
|
||||
f := &File{
|
||||
client: client,
|
||||
Path: path,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(f)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// Name is the name of the file's reference
|
||||
func (f *File) Name(path string) string {
|
||||
return f.client.Name(path)
|
||||
}
|
||||
|
||||
func (f *File) MediaType() string {
|
||||
return consts.OCIManifestSchema1
|
||||
}
|
||||
|
||||
func (f *File) RawConfig() ([]byte, error) {
|
||||
if err := f.compute(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.config.Raw()
|
||||
}
|
||||
|
||||
func (f *File) Layers() ([]gv1.Layer, error) {
|
||||
if err := f.compute(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var layers []gv1.Layer
|
||||
layers = append(layers, f.blob)
|
||||
return layers, nil
|
||||
}
|
||||
|
||||
func (f *File) Manifest() (*gv1.Manifest, error) {
|
||||
if err := f.compute(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.manifest, nil
|
||||
}
|
||||
|
||||
func (f *File) compute() error {
|
||||
if f.computed {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
blob, err := f.client.LayerFrom(ctx, f.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
layer, err := partial.Descriptor(blob)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg := f.client.Config(f.Path)
|
||||
if cfg == nil {
|
||||
cfg = f.client.Config(f.Path)
|
||||
}
|
||||
|
||||
cfgDesc, err := partial.Descriptor(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m := &gv1.Manifest{
|
||||
SchemaVersion: 2,
|
||||
MediaType: gtypes.MediaType(f.MediaType()),
|
||||
Config: *cfgDesc,
|
||||
Layers: []gv1.Descriptor{*layer},
|
||||
Annotations: f.annotations,
|
||||
}
|
||||
|
||||
f.manifest = m
|
||||
f.config = cfg
|
||||
f.blob = blob
|
||||
f.computed = true
|
||||
return nil
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
package file_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/file"
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/file/getter"
|
||||
"github.com/rancherfederal/hauler/pkg/consts"
|
||||
)
|
||||
|
||||
var (
|
||||
filename = "myfile.yaml"
|
||||
data = []byte(`data`)
|
||||
|
||||
ts *httptest.Server
|
||||
tfs afero.Fs
|
||||
mc *getter.Client
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
teardown := setup()
|
||||
defer teardown()
|
||||
code := m.Run()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func Test_file_Config(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ref string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "should properly type local file",
|
||||
ref: filename,
|
||||
want: consts.FileLocalConfigMediaType,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should properly type remote file",
|
||||
ref: ts.URL + "/" + filename,
|
||||
want: consts.FileHttpConfigMediaType,
|
||||
wantErr: false,
|
||||
},
|
||||
// TODO: Add directory test
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f := file.NewFile(tt.ref, file.WithClient(mc))
|
||||
|
||||
f.MediaType()
|
||||
|
||||
m, err := f.Manifest()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got := string(m.Config.MediaType)
|
||||
if got != tt.want {
|
||||
t.Errorf("unxpected mediatype; got %s, want %s", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_file_Layers(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ref string
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "should load a local file and preserve contents",
|
||||
ref: filename,
|
||||
want: data,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should load a remote file and preserve contents",
|
||||
ref: ts.URL + "/" + filename,
|
||||
want: data,
|
||||
wantErr: false,
|
||||
},
|
||||
// TODO: Add directory test
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(it *testing.T) {
|
||||
f := file.NewFile(tt.ref, file.WithClient(mc))
|
||||
|
||||
layers, err := f.Layers()
|
||||
if (err != nil) != tt.wantErr {
|
||||
it.Fatalf("unexpected Layers() error: got %v, want %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
rc, err := layers[0].Compressed()
|
||||
if err != nil {
|
||||
it.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := io.ReadAll(rc)
|
||||
if err != nil {
|
||||
it.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(got, tt.want) {
|
||||
it.Fatalf("unexpected Layers(): got %v, want %v", layers, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setup() func() {
|
||||
tfs = afero.NewMemMapFs()
|
||||
afero.WriteFile(tfs, filename, data, 0644)
|
||||
|
||||
mf := &mockFile{File: getter.NewFile(), fs: tfs}
|
||||
|
||||
mockHttp := getter.NewHttp()
|
||||
mhttp := afero.NewHttpFs(tfs)
|
||||
fileserver := http.FileServer(mhttp.Dir("."))
|
||||
http.Handle("/", fileserver)
|
||||
ts = httptest.NewServer(fileserver)
|
||||
|
||||
mc = &getter.Client{
|
||||
Options: getter.ClientOptions{},
|
||||
Getters: map[string]getter.Getter{
|
||||
"file": mf,
|
||||
"http": mockHttp,
|
||||
},
|
||||
}
|
||||
|
||||
teardown := func() {
|
||||
defer ts.Close()
|
||||
}
|
||||
|
||||
return teardown
|
||||
}
|
||||
|
||||
type mockFile struct {
|
||||
*getter.File
|
||||
fs afero.Fs
|
||||
}
|
||||
|
||||
func (m mockFile) Open(ctx context.Context, u *url.URL) (io.ReadCloser, error) {
|
||||
return m.fs.Open(filepath.Join(u.Host, u.Path))
|
||||
}
|
||||
|
||||
func (m mockFile) Detect(u *url.URL) bool {
|
||||
fi, err := m.fs.Stat(filepath.Join(u.Host, u.Path))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !fi.IsDir()
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
package getter
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts"
|
||||
"github.com/rancherfederal/hauler/pkg/consts"
|
||||
)
|
||||
|
||||
type directory struct {
|
||||
*File
|
||||
}
|
||||
|
||||
func NewDirectory() *directory {
|
||||
return &directory{File: NewFile()}
|
||||
}
|
||||
|
||||
func (d directory) Open(ctx context.Context, u *url.URL) (io.ReadCloser, error) {
|
||||
tmpfile, err := os.CreateTemp("", "hauler")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
digester := digest.Canonical.Digester()
|
||||
zw := gzip.NewWriter(io.MultiWriter(tmpfile, digester.Hash()))
|
||||
defer zw.Close()
|
||||
|
||||
tarDigester := digest.Canonical.Digester()
|
||||
if err := tarDir(d.path(u), d.Name(u), io.MultiWriter(zw, tarDigester.Hash()), false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := zw.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tmpfile.Sync(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := os.Open(tmpfile.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// rc := &closer{
|
||||
// t: io.TeeReader(tmpfile, fi),
|
||||
// closes: []func() error{fi.Close, tmpfile.Close, zw.Close},
|
||||
// }
|
||||
return fi, nil
|
||||
}
|
||||
|
||||
func (d directory) Detect(u *url.URL) bool {
|
||||
if len(d.path(u)) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
fi, err := os.Stat(d.path(u))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return fi.IsDir()
|
||||
}
|
||||
|
||||
func (d directory) Config(u *url.URL) artifacts.Config {
|
||||
c := &directoryConfig{
|
||||
config{Reference: u.String()},
|
||||
}
|
||||
return artifacts.ToConfig(c, artifacts.WithConfigMediaType(consts.FileDirectoryConfigMediaType))
|
||||
}
|
||||
|
||||
type directoryConfig struct {
|
||||
config `json:",inline,omitempty"`
|
||||
}
|
||||
|
||||
func tarDir(root string, prefix string, w io.Writer, stripTimes bool) error {
|
||||
tw := tar.NewWriter(w)
|
||||
defer tw.Close()
|
||||
if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Rename path
|
||||
name, err := filepath.Rel(root, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name = filepath.Join(prefix, name)
|
||||
name = filepath.ToSlash(name)
|
||||
|
||||
// Generate header
|
||||
var link string
|
||||
mode := info.Mode()
|
||||
if mode&os.ModeSymlink != 0 {
|
||||
if link, err = os.Readlink(path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
header, err := tar.FileInfoHeader(info, link)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, path)
|
||||
}
|
||||
header.Name = name
|
||||
header.Uid = 0
|
||||
header.Gid = 0
|
||||
header.Uname = ""
|
||||
header.Gname = ""
|
||||
|
||||
if stripTimes {
|
||||
header.ModTime = time.Time{}
|
||||
header.AccessTime = time.Time{}
|
||||
header.ChangeTime = time.Time{}
|
||||
}
|
||||
|
||||
// Write file
|
||||
if err := tw.WriteHeader(header); err != nil {
|
||||
return errors.Wrap(err, "tar")
|
||||
}
|
||||
if mode.IsRegular() {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
if _, err := io.Copy(tw, file); err != nil {
|
||||
return errors.Wrap(err, path)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type closer struct {
|
||||
t io.Reader
|
||||
closes []func() error
|
||||
}
|
||||
|
||||
func (c *closer) Read(p []byte) (n int, err error) {
|
||||
return c.t.Read(p)
|
||||
}
|
||||
|
||||
func (c *closer) Close() error {
|
||||
var err error
|
||||
for _, c := range c.closes {
|
||||
lastErr := c()
|
||||
if err == nil {
|
||||
err = lastErr
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package getter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts"
|
||||
"github.com/rancherfederal/hauler/pkg/consts"
|
||||
)
|
||||
|
||||
type File struct{}
|
||||
|
||||
func NewFile() *File {
|
||||
return &File{}
|
||||
}
|
||||
|
||||
func (f File) Name(u *url.URL) string {
|
||||
return filepath.Base(f.path(u))
|
||||
}
|
||||
|
||||
func (f File) Open(ctx context.Context, u *url.URL) (io.ReadCloser, error) {
|
||||
return os.Open(f.path(u))
|
||||
}
|
||||
|
||||
func (f File) Detect(u *url.URL) bool {
|
||||
if len(f.path(u)) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
fi, err := os.Stat(f.path(u))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !fi.IsDir()
|
||||
}
|
||||
|
||||
func (f File) path(u *url.URL) string {
|
||||
return filepath.Join(u.Host, u.Path)
|
||||
}
|
||||
|
||||
func (f File) Config(u *url.URL) artifacts.Config {
|
||||
c := &fileConfig{
|
||||
config{Reference: u.String()},
|
||||
}
|
||||
return artifacts.ToConfig(c, artifacts.WithConfigMediaType(consts.FileLocalConfigMediaType))
|
||||
}
|
||||
|
||||
type fileConfig struct {
|
||||
config `json:",inline,omitempty"`
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
package getter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"oras.land/oras-go/pkg/content"
|
||||
|
||||
content2 "github.com/rancherfederal/hauler/pkg/artifacts"
|
||||
"github.com/rancherfederal/hauler/pkg/consts"
|
||||
"github.com/rancherfederal/hauler/pkg/layer"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Getters map[string]Getter
|
||||
Options ClientOptions
|
||||
}
|
||||
|
||||
// ClientOptions provides options for the client
|
||||
type ClientOptions struct {
|
||||
NameOverride string
|
||||
}
|
||||
|
||||
var (
|
||||
ErrGetterTypeUnknown = errors.New("no getter type found matching reference")
|
||||
)
|
||||
|
||||
type Getter interface {
|
||||
Open(context.Context, *url.URL) (io.ReadCloser, error)
|
||||
|
||||
Detect(*url.URL) bool
|
||||
|
||||
Name(*url.URL) string
|
||||
|
||||
Config(*url.URL) content2.Config
|
||||
}
|
||||
|
||||
func NewClient(opts ClientOptions) *Client {
|
||||
defaults := map[string]Getter{
|
||||
"file": NewFile(),
|
||||
"directory": NewDirectory(),
|
||||
"http": NewHttp(),
|
||||
}
|
||||
|
||||
c := &Client{
|
||||
Getters: defaults,
|
||||
Options: opts,
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Client) LayerFrom(ctx context.Context, source string) (v1.Layer, error) {
|
||||
u, err := url.Parse(source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g, err := c.getterFrom(u)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrGetterTypeUnknown) {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("create getter: %w", err)
|
||||
}
|
||||
|
||||
opener := func() (io.ReadCloser, error) {
|
||||
return g.Open(ctx, u)
|
||||
}
|
||||
|
||||
annotations := make(map[string]string)
|
||||
annotations[ocispec.AnnotationTitle] = c.Name(source)
|
||||
|
||||
switch g.(type) {
|
||||
case *directory:
|
||||
annotations[content.AnnotationUnpack] = "true"
|
||||
}
|
||||
|
||||
l, err := layer.FromOpener(opener,
|
||||
layer.WithMediaType(consts.FileLayerMediaType),
|
||||
layer.WithAnnotations(annotations))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (c *Client) ContentFrom(ctx context.Context, source string) (io.ReadCloser, error) {
|
||||
u, err := url.Parse(source)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse source %s: %w", source, err)
|
||||
}
|
||||
g, err := c.getterFrom(u)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrGetterTypeUnknown) {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("create getter: %w", err)
|
||||
}
|
||||
return g.Open(ctx, u)
|
||||
}
|
||||
|
||||
func (c *Client) getterFrom(srcUrl *url.URL) (Getter, error) {
|
||||
for _, g := range c.Getters {
|
||||
if g.Detect(srcUrl) {
|
||||
return g, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.Wrapf(ErrGetterTypeUnknown, "source %s", srcUrl.String())
|
||||
}
|
||||
|
||||
func (c *Client) Name(source string) string {
|
||||
if c.Options.NameOverride != "" {
|
||||
return c.Options.NameOverride
|
||||
}
|
||||
u, err := url.Parse(source)
|
||||
if err != nil {
|
||||
return source
|
||||
}
|
||||
for _, g := range c.Getters {
|
||||
if g.Detect(u) {
|
||||
return g.Name(u)
|
||||
}
|
||||
}
|
||||
return source
|
||||
}
|
||||
|
||||
func (c *Client) Config(source string) content2.Config {
|
||||
u, err := url.Parse(source)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for _, g := range c.Getters {
|
||||
if g.Detect(u) {
|
||||
return g.Config(u)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type config struct {
|
||||
Reference string `json:"reference"`
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package getter_test
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/file/getter"
|
||||
)
|
||||
|
||||
func TestClient_Detect(t *testing.T) {
|
||||
teardown := setup(t)
|
||||
defer teardown()
|
||||
|
||||
c := getter.NewClient(getter.ClientOptions{})
|
||||
|
||||
type args struct {
|
||||
source string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "should identify a file",
|
||||
args: args{
|
||||
source: fileWithExt,
|
||||
},
|
||||
want: "file",
|
||||
},
|
||||
{
|
||||
name: "should identify a directory",
|
||||
args: args{
|
||||
source: rootDir,
|
||||
},
|
||||
want: "directory",
|
||||
},
|
||||
{
|
||||
name: "should identify an http fqdn",
|
||||
args: args{
|
||||
source: "http://my.cool.website",
|
||||
},
|
||||
want: "http",
|
||||
},
|
||||
{
|
||||
name: "should identify an http fqdn",
|
||||
args: args{
|
||||
source: "https://my.cool.website",
|
||||
},
|
||||
want: "http",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := identify(c, tt.args.source); got != tt.want {
|
||||
t.Errorf("identify() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func identify(c *getter.Client, source string) string {
|
||||
u, _ := url.Parse(source)
|
||||
for t, g := range c.Getters {
|
||||
if g.Detect(u) {
|
||||
return t
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestClient_Name(t *testing.T) {
|
||||
teardown := setup(t)
|
||||
defer teardown()
|
||||
|
||||
type args struct {
|
||||
source string
|
||||
opts getter.ClientOptions
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "should correctly name a file with an extension",
|
||||
args: args{
|
||||
source: fileWithExt,
|
||||
opts: getter.ClientOptions{},
|
||||
},
|
||||
want: "file.yaml",
|
||||
},
|
||||
{
|
||||
name: "should correctly name a directory",
|
||||
args: args{
|
||||
source: rootDir,
|
||||
opts: getter.ClientOptions{},
|
||||
},
|
||||
want: rootDir,
|
||||
},
|
||||
{
|
||||
name: "should correctly override a files name",
|
||||
args: args{
|
||||
source: fileWithExt,
|
||||
opts: getter.ClientOptions{NameOverride: "myfile"},
|
||||
},
|
||||
want: "myfile",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := getter.NewClient(tt.args.opts)
|
||||
if got := c.Name(tt.args.source); got != tt.want {
|
||||
t.Errorf("Name() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
rootDir = "gettertests"
|
||||
fileWithExt = filepath.Join(rootDir, "file.yaml")
|
||||
)
|
||||
|
||||
func setup(t *testing.T) func() {
|
||||
if err := os.MkdirAll(rootDir, os.ModePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(fileWithExt, []byte(""), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return func() {
|
||||
os.RemoveAll(rootDir)
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package getter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts"
|
||||
"github.com/rancherfederal/hauler/pkg/consts"
|
||||
)
|
||||
|
||||
type Http struct{}
|
||||
|
||||
func NewHttp() *Http {
|
||||
return &Http{}
|
||||
}
|
||||
|
||||
func (h Http) Name(u *url.URL) string {
|
||||
resp, err := http.Head(u.String())
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
name, _ := url.PathUnescape(u.String())
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
for _, v := range strings.Split(contentType, ",") {
|
||||
t, _, err := mime.ParseMediaType(v)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
// TODO: Identify known mimetypes for hints at a filename
|
||||
_ = t
|
||||
}
|
||||
|
||||
// TODO: Not this
|
||||
return filepath.Base(name)
|
||||
}
|
||||
|
||||
func (h Http) Open(ctx context.Context, u *url.URL) (io.ReadCloser, error) {
|
||||
resp, err := http.Get(u.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
func (h Http) Detect(u *url.URL) bool {
|
||||
switch u.Scheme {
|
||||
case "http", "https":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *Http) Config(u *url.URL) artifacts.Config {
|
||||
c := &httpConfig{
|
||||
config{Reference: u.String()},
|
||||
}
|
||||
return artifacts.ToConfig(c, artifacts.WithConfigMediaType(consts.FileHttpConfigMediaType))
|
||||
}
|
||||
|
||||
type httpConfig struct {
|
||||
config `json:",inline,omitempty"`
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts"
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/file/getter"
|
||||
)
|
||||
|
||||
type Option func(*File)
|
||||
|
||||
func WithClient(c *getter.Client) Option {
|
||||
return func(f *File) {
|
||||
f.client = c
|
||||
}
|
||||
}
|
||||
|
||||
func WithConfig(obj interface{}, mediaType string) Option {
|
||||
return func(f *File) {
|
||||
f.config = artifacts.ToConfig(obj, artifacts.WithConfigMediaType(mediaType))
|
||||
}
|
||||
}
|
||||
|
||||
func WithAnnotations(m map[string]string) Option {
|
||||
return func(f *File) {
|
||||
f.annotations = m
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
gname "github.com/google/go-containerregistry/pkg/name"
|
||||
gv1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts"
|
||||
)
|
||||
|
||||
var _ artifacts.OCI = (*Image)(nil)
|
||||
|
||||
func (i *Image) MediaType() string {
|
||||
mt, err := i.Image.MediaType()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(mt)
|
||||
}
|
||||
|
||||
func (i *Image) RawConfig() ([]byte, error) {
|
||||
return i.RawConfigFile()
|
||||
}
|
||||
|
||||
// Image implements the OCI interface for Image API objects. API spec information
|
||||
// is stored into the Name field.
|
||||
type Image struct {
|
||||
Name string
|
||||
gv1.Image
|
||||
}
|
||||
|
||||
func NewImage(name string, opts ...remote.Option) (*Image, error) {
|
||||
r, err := gname.ParseReference(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defaultOpts := []remote.Option{
|
||||
remote.WithAuthFromKeychain(authn.DefaultKeychain),
|
||||
}
|
||||
opts = append(opts, defaultOpts...)
|
||||
|
||||
img, err := remote.Image(r, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Image{
|
||||
Name: name,
|
||||
Image: img,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func IsMultiArchImage(name string, opts ...remote.Option) (bool, error) {
|
||||
ref, err := gname.ParseReference(name)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("parsing reference %q: %v", name, err)
|
||||
}
|
||||
|
||||
defaultOpts := []remote.Option{
|
||||
remote.WithAuthFromKeychain(authn.DefaultKeychain),
|
||||
}
|
||||
opts = append(opts, defaultOpts...)
|
||||
|
||||
desc, err := remote.Get(ref, opts...)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("getting image %q: %v", name, err)
|
||||
}
|
||||
|
||||
_, err = desc.ImageIndex()
|
||||
if err != nil {
|
||||
// If the descriptor could not be converted to an image index, it's not a multi-arch image
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// If the descriptor could be converted to an image index, it's a multi-arch image
|
||||
return true, nil
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package image_test
|
||||
@@ -1,78 +0,0 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
"github.com/google/go-containerregistry/pkg/v1/static"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts"
|
||||
"github.com/rancherfederal/hauler/pkg/consts"
|
||||
)
|
||||
|
||||
var _ artifacts.OCI = (*Memory)(nil)
|
||||
|
||||
// Memory implements the OCI interface for a generic set of bytes stored in memory.
|
||||
type Memory struct {
|
||||
blob v1.Layer
|
||||
annotations map[string]string
|
||||
config artifacts.Config
|
||||
}
|
||||
|
||||
type defaultConfig struct {
|
||||
MediaType string `json:"mediaType,omitempty"`
|
||||
}
|
||||
|
||||
func NewMemory(data []byte, mt string, opts ...Option) *Memory {
|
||||
blob := static.NewLayer(data, types.MediaType(mt))
|
||||
|
||||
cfg := defaultConfig{MediaType: consts.MemoryConfigMediaType}
|
||||
m := &Memory{
|
||||
blob: blob,
|
||||
config: artifacts.ToConfig(cfg),
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(m)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Memory) MediaType() string {
|
||||
return consts.OCIManifestSchema1
|
||||
}
|
||||
|
||||
func (m *Memory) Manifest() (*v1.Manifest, error) {
|
||||
layer, err := partial.Descriptor(m.blob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfgDesc, err := partial.Descriptor(m.config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manifest := &v1.Manifest{
|
||||
SchemaVersion: 2,
|
||||
MediaType: types.MediaType(m.MediaType()),
|
||||
Config: *cfgDesc,
|
||||
Layers: []v1.Descriptor{*layer},
|
||||
Annotations: m.annotations,
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
func (m *Memory) RawConfig() ([]byte, error) {
|
||||
if m.config == nil {
|
||||
return []byte(`{}`), nil
|
||||
}
|
||||
return m.config.Raw()
|
||||
}
|
||||
|
||||
func (m *Memory) Layers() ([]v1.Layer, error) {
|
||||
var layers []v1.Layer
|
||||
layers = append(layers, m.blob)
|
||||
return layers, nil
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package memory_test
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/opencontainers/go-digest"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/memory"
|
||||
)
|
||||
|
||||
func TestMemory_Layers(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want *v1.Manifest
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "should preserve content",
|
||||
want: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data, m := setup(t)
|
||||
|
||||
layers, err := m.Layers()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(layers) != 1 {
|
||||
t.Fatalf("Expected 1 layer, got %d", len(layers))
|
||||
}
|
||||
|
||||
h, err := layers[0].Digest()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
d := digest.FromBytes(data)
|
||||
|
||||
if d.String() != h.String() {
|
||||
t.Fatalf("bytes do not match, got %s, expected %s", h.String(), d.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setup(t *testing.T) ([]byte, *memory.Memory) {
|
||||
block := make([]byte, 2048)
|
||||
_, err := rand.Read(block)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mem := memory.NewMemory(block, "random")
|
||||
return block, mem
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package memory
|
||||
|
||||
import "github.com/rancherfederal/hauler/pkg/artifacts"
|
||||
|
||||
type Option func(*Memory)
|
||||
|
||||
func WithConfig(obj interface{}, mediaType string) Option {
|
||||
return func(m *Memory) {
|
||||
m.config = artifacts.ToConfig(obj, artifacts.WithConfigMediaType(mediaType))
|
||||
}
|
||||
}
|
||||
|
||||
func WithAnnotations(annotations map[string]string) Option {
|
||||
return func(m *Memory) {
|
||||
m.annotations = annotations
|
||||
}
|
||||
}
|
||||
12
pkg/layer/cache.go → pkg/cache/cache.go
vendored
12
pkg/layer/cache.go → pkg/cache/cache.go
vendored
@@ -1,4 +1,4 @@
|
||||
package layer
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -7,13 +7,9 @@ import (
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts"
|
||||
"github.com/rancherfederal/hauler/pkg/artifact"
|
||||
)
|
||||
|
||||
/*
|
||||
This package is _heavily_ influenced by go-containerregistry and it's cache implementation: https://github.com/google/go-containerregistry/tree/main/pkg/v1/cache
|
||||
*/
|
||||
|
||||
type Cache interface {
|
||||
Put(v1.Layer) (v1.Layer, error)
|
||||
|
||||
@@ -23,12 +19,12 @@ type Cache interface {
|
||||
var ErrLayerNotFound = errors.New("layer not found")
|
||||
|
||||
type oci struct {
|
||||
artifacts.OCI
|
||||
artifact.OCI
|
||||
|
||||
c Cache
|
||||
}
|
||||
|
||||
func OCICache(o artifacts.OCI, c Cache) artifacts.OCI {
|
||||
func Oci(o artifact.OCI, c Cache) artifact.OCI {
|
||||
return &oci{
|
||||
OCI: o,
|
||||
c: c,
|
||||
5
pkg/cache/doc.go
vendored
Normal file
5
pkg/cache/doc.go
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
package cache
|
||||
|
||||
/*
|
||||
This package is _heavily_ influenced by go-containerregistry and it's cache implementation: https://github.com/google/go-containerregistry/tree/main/pkg/v1/cache
|
||||
*/
|
||||
@@ -1,4 +1,4 @@
|
||||
package layer
|
||||
package cache
|
||||
|
||||
import (
|
||||
"io"
|
||||
@@ -6,13 +6,15 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifact/local"
|
||||
)
|
||||
|
||||
type fs struct {
|
||||
root string
|
||||
}
|
||||
|
||||
func NewFilesystemCache(root string) Cache {
|
||||
func NewFilesystem(root string) Cache {
|
||||
return &fs{root: root}
|
||||
}
|
||||
|
||||
@@ -35,14 +37,14 @@ func (f *fs) Put(l v1.Layer) (v1.Layer, error) {
|
||||
|
||||
func (f *fs) Get(h v1.Hash) (v1.Layer, error) {
|
||||
opener := f.open(h)
|
||||
l, err := FromOpener(opener)
|
||||
l, err := local.LayerFromOpener(opener)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, ErrLayerNotFound
|
||||
}
|
||||
return l, err
|
||||
}
|
||||
|
||||
func (f *fs) open(h v1.Hash) Opener {
|
||||
func (f *fs) open(h v1.Hash) local.Opener {
|
||||
return func() (io.ReadCloser, error) {
|
||||
return os.Open(layerpath(f.root, h))
|
||||
}
|
||||
@@ -1,40 +1,42 @@
|
||||
package chart
|
||||
|
||||
import (
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts"
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/image"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
gname "github.com/google/go-containerregistry/pkg/name"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/apis/hauler.cattle.io/v1alpha1"
|
||||
"github.com/rancherfederal/hauler/pkg/artifact"
|
||||
"github.com/rancherfederal/hauler/pkg/content/chart"
|
||||
"github.com/rancherfederal/hauler/pkg/reference"
|
||||
"github.com/rancherfederal/hauler/pkg/content/image"
|
||||
)
|
||||
|
||||
var _ artifacts.OCICollection = (*tchart)(nil)
|
||||
var _ artifact.Collection = (*tchart)(nil)
|
||||
|
||||
// tchart is a thick chart that includes all the dependent images as well as the chart itself
|
||||
type tchart struct {
|
||||
chart *chart.Chart
|
||||
config v1alpha1.ThickChart
|
||||
name string
|
||||
repo string
|
||||
version string
|
||||
chart *chart.Chart
|
||||
|
||||
computed bool
|
||||
contents map[string]artifacts.OCI
|
||||
contents map[gname.Reference]artifact.OCI
|
||||
}
|
||||
|
||||
func NewThickChart(cfg v1alpha1.ThickChart, opts *action.ChartPathOptions) (artifacts.OCICollection, error) {
|
||||
o, err := chart.NewChart(cfg.Chart.Name, opts)
|
||||
func NewChart(name, repo, version string) (artifact.Collection, error) {
|
||||
o, err := chart.NewChart(name, repo, version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &tchart{
|
||||
name: name,
|
||||
repo: repo,
|
||||
version: version,
|
||||
chart: o,
|
||||
config: cfg,
|
||||
contents: make(map[string]artifacts.OCI),
|
||||
contents: make(map[gname.Reference]artifact.OCI),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *tchart) Contents() (map[string]artifacts.OCI, error) {
|
||||
func (c *tchart) Contents() (map[gname.Reference]artifact.OCI, error) {
|
||||
if err := c.compute(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -49,28 +51,32 @@ func (c *tchart) compute() error {
|
||||
if err := c.dependentImages(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.chartContents(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.extraImages(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.computed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *tchart) chartContents() error {
|
||||
ch, err := c.chart.Load()
|
||||
oci, err := chart.NewChart(c.name, c.repo, c.version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ref, err := reference.NewTagged(ch.Name(), ch.Metadata.Version)
|
||||
tag := c.version
|
||||
if tag == "" {
|
||||
tag = gname.DefaultTag
|
||||
}
|
||||
|
||||
ref, err := gname.ParseReference(c.name, gname.WithDefaultRegistry(""), gname.WithDefaultTag(tag))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.contents[ref.Name()] = c.chart
|
||||
|
||||
c.contents[ref] = oci
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -86,22 +92,17 @@ func (c *tchart) dependentImages() error {
|
||||
}
|
||||
|
||||
for _, img := range imgs.Spec.Images {
|
||||
i, err := image.NewImage(img.Name)
|
||||
ref, err := gname.ParseReference(img.Ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.contents[img.Name] = i
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *tchart) extraImages() error {
|
||||
for _, img := range c.config.ExtraImages {
|
||||
i, err := image.NewImage(img.Reference)
|
||||
i, err := image.NewImage(img.Ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.contents[img.Reference] = i
|
||||
c.contents[ref] = i
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
package chart
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/rancher/wrangler/pkg/yaml"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
helmchart "helm.sh/helm/v3/pkg/chart"
|
||||
"helm.sh/helm/v3/pkg/chartutil"
|
||||
"helm.sh/helm/v3/pkg/kube/fake"
|
||||
"helm.sh/helm/v3/pkg/storage"
|
||||
"helm.sh/helm/v3/pkg/storage/driver"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/util/jsonpath"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/apis/hauler.cattle.io/v1alpha1"
|
||||
@@ -30,37 +32,50 @@ var defaultKnownImagePaths = []string{
|
||||
|
||||
// ImagesInChart will render a chart and identify all dependent images from it
|
||||
func ImagesInChart(c *helmchart.Chart) (v1alpha1.Images, error) {
|
||||
docs, err := template(c)
|
||||
objs, err := template(c)
|
||||
if err != nil {
|
||||
return v1alpha1.Images{}, err
|
||||
}
|
||||
|
||||
var images []v1alpha1.Image
|
||||
reader := yaml.NewYAMLReader(bufio.NewReader(strings.NewReader(docs)))
|
||||
for {
|
||||
raw, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
var imageRefs []string
|
||||
for _, o := range objs {
|
||||
d, err := o.(*unstructured.Unstructured).MarshalJSON()
|
||||
if err != nil {
|
||||
return v1alpha1.Images{}, err
|
||||
// TODO: Should we actually capture these errors?
|
||||
continue
|
||||
}
|
||||
|
||||
found := find(raw, defaultKnownImagePaths...)
|
||||
for _, f := range found {
|
||||
images = append(images, v1alpha1.Image{Name: f})
|
||||
var obj interface{}
|
||||
if err := json.Unmarshal(d, &obj); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
j := jsonpath.New("")
|
||||
j.AllowMissingKeys(true)
|
||||
|
||||
for _, p := range defaultKnownImagePaths {
|
||||
r, err := parseJSONPath(obj, j, p)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
imageRefs = append(imageRefs, r...)
|
||||
}
|
||||
}
|
||||
|
||||
ims := v1alpha1.Images{
|
||||
Spec: v1alpha1.ImageSpec{
|
||||
Images: images,
|
||||
Images: []v1alpha1.Image{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, ref := range imageRefs {
|
||||
ims.Spec.Images = append(ims.Spec.Images, v1alpha1.Image{Ref: ref})
|
||||
}
|
||||
return ims, nil
|
||||
}
|
||||
|
||||
func template(c *helmchart.Chart) (string, error) {
|
||||
func template(c *helmchart.Chart) ([]runtime.Object, error) {
|
||||
s := storage.Init(driver.NewMemory())
|
||||
|
||||
templateCfg := &action.Configuration{
|
||||
@@ -84,33 +99,10 @@ func template(c *helmchart.Chart) (string, error) {
|
||||
|
||||
release, err := client.Run(c, vals)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return release.Manifest, nil
|
||||
}
|
||||
|
||||
func find(data []byte, paths ...string) []string {
|
||||
var (
|
||||
pathMatches []string
|
||||
obj interface{}
|
||||
)
|
||||
|
||||
if err := yaml.Unmarshal(data, &obj); err != nil {
|
||||
return nil
|
||||
}
|
||||
j := jsonpath.New("")
|
||||
j.AllowMissingKeys(true)
|
||||
|
||||
for _, p := range paths {
|
||||
r, err := parseJSONPath(obj, j, p)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
pathMatches = append(pathMatches, r...)
|
||||
}
|
||||
return pathMatches
|
||||
return yaml.ToObjects(bytes.NewBufferString(release.Manifest))
|
||||
}
|
||||
|
||||
func parseJSONPath(data interface{}, parser *jsonpath.JSONPath, template string) ([]string, error) {
|
||||
|
||||
@@ -2,19 +2,19 @@ package imagetxt
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifact"
|
||||
"github.com/rancherfederal/hauler/pkg/artifact/local"
|
||||
"github.com/rancherfederal/hauler/pkg/content/image"
|
||||
"github.com/rancherfederal/hauler/pkg/log"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
artifact "github.com/rancherfederal/hauler/pkg/artifacts"
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/file/getter"
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/image"
|
||||
)
|
||||
|
||||
type ImageTxt struct {
|
||||
@@ -23,12 +23,12 @@ type ImageTxt struct {
|
||||
ExcludeSources map[string]bool
|
||||
|
||||
lock *sync.Mutex
|
||||
client *getter.Client
|
||||
getter local.Opener
|
||||
computed bool
|
||||
contents map[string]artifact.OCI
|
||||
contents map[name.Reference]artifact.OCI
|
||||
}
|
||||
|
||||
var _ artifact.OCICollection = (*ImageTxt)(nil)
|
||||
var _ artifact.Collection = (*ImageTxt)(nil)
|
||||
|
||||
type Option interface {
|
||||
Apply(*ImageTxt) error
|
||||
@@ -70,8 +70,13 @@ func New(ref string, opts ...Option) (*ImageTxt, error) {
|
||||
it := &ImageTxt{
|
||||
Ref: ref,
|
||||
|
||||
client: getter.NewClient(getter.ClientOptions{}),
|
||||
lock: &sync.Mutex{},
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
if strings.HasPrefix(ref, "http") || strings.HasPrefix(ref, "https") {
|
||||
it.getter = local.RemoteOpener(ref)
|
||||
} else {
|
||||
it.getter = local.LocalOpener(ref)
|
||||
}
|
||||
|
||||
for i, o := range opts {
|
||||
@@ -83,7 +88,7 @@ func New(ref string, opts ...Option) (*ImageTxt, error) {
|
||||
return it, nil
|
||||
}
|
||||
|
||||
func (it *ImageTxt) Contents() (map[string]artifact.OCI, error) {
|
||||
func (it *ImageTxt) Contents() (map[name.Reference]artifact.OCI, error) {
|
||||
it.lock.Lock()
|
||||
defer it.lock.Unlock()
|
||||
if !it.computed {
|
||||
@@ -99,17 +104,20 @@ func (it *ImageTxt) compute() error {
|
||||
// TODO - pass in logger from context
|
||||
l := log.NewLogger(os.Stdout)
|
||||
|
||||
it.contents = make(map[string]artifact.OCI)
|
||||
it.contents = make(map[name.Reference]artifact.OCI)
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
rc, err := it.client.ContentFrom(ctx, it.Ref)
|
||||
r, err := it.getter()
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch image.txt ref %s: %w", it.Ref, err)
|
||||
return fmt.Errorf("fetch image.txt ref %s: %v", it.Ref, err)
|
||||
}
|
||||
defer rc.Close()
|
||||
defer r.Close()
|
||||
|
||||
entries, err := splitImagesTxt(rc)
|
||||
buf := &bytes.Buffer{}
|
||||
if _, err := io.Copy(buf, r); err != nil {
|
||||
return fmt.Errorf("read image.txt ref %s: %v", it.Ref, err)
|
||||
}
|
||||
|
||||
entries, err := splitImagesTxt(buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse image.txt ref %s: %v", it.Ref, err)
|
||||
}
|
||||
@@ -178,7 +186,7 @@ func (it *ImageTxt) compute() error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("pull image %s: %v", e.Reference, err)
|
||||
}
|
||||
it.contents[e.Reference.String()] = curImage
|
||||
it.contents[e.Reference] = curImage
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,14 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts"
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/image"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
|
||||
artifacts "github.com/rancherfederal/hauler/pkg/artifact"
|
||||
"github.com/rancherfederal/hauler/pkg/content/image"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidRef = errors.New("invalid reference")
|
||||
ErrRefNotFound = errors.New("ref not found")
|
||||
ErrRefNotImage = errors.New("ref is not image")
|
||||
ErrExtraRefsFound = errors.New("extra refs found in contents")
|
||||
@@ -185,20 +188,24 @@ func TestImageTxtCollection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func checkImages(content map[string]artifacts.OCI, refs []string) error {
|
||||
contentCopy := make(map[string]artifacts.OCI, len(content))
|
||||
func checkImages(content map[name.Reference]artifacts.OCI, refs []string) error {
|
||||
contentCopy := make(map[name.Reference]artifacts.OCI, len(content))
|
||||
for k, v := range content {
|
||||
contentCopy[k] = v
|
||||
}
|
||||
for _, ref := range refs {
|
||||
target, ok := content[ref]
|
||||
nameRef, err := name.ParseReference(ref)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ref %s: %w", ref, ErrInvalidRef)
|
||||
}
|
||||
target, ok := content[nameRef]
|
||||
if !ok {
|
||||
return fmt.Errorf("ref %s: %w", ref, ErrRefNotFound)
|
||||
}
|
||||
if _, ok := target.(*image.Image); !ok {
|
||||
return fmt.Errorf("got underlying type %T: %w", target, ErrRefNotImage)
|
||||
}
|
||||
delete(contentCopy, ref)
|
||||
delete(contentCopy, nameRef)
|
||||
}
|
||||
|
||||
if len(contentCopy) != 0 {
|
||||
|
||||
@@ -10,17 +10,14 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts"
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/image"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/file"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/file/getter"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/reference"
|
||||
"github.com/rancherfederal/hauler/pkg/artifact"
|
||||
"github.com/rancherfederal/hauler/pkg/content/file"
|
||||
"github.com/rancherfederal/hauler/pkg/content/image"
|
||||
)
|
||||
|
||||
var _ artifacts.OCICollection = (*k3s)(nil)
|
||||
var _ artifact.Collection = (*k3s)(nil)
|
||||
|
||||
const (
|
||||
releaseUrl = "https://github.com/k3s-io/k3s/releases/download"
|
||||
@@ -40,19 +37,18 @@ type k3s struct {
|
||||
arch string
|
||||
|
||||
computed bool
|
||||
contents map[string]artifacts.OCI
|
||||
contents map[name.Reference]artifact.OCI
|
||||
channels map[string]string
|
||||
client *getter.Client
|
||||
}
|
||||
|
||||
func NewK3s(version string) (artifacts.OCICollection, error) {
|
||||
func NewK3s(version string) (artifact.Collection, error) {
|
||||
return &k3s{
|
||||
version: version,
|
||||
contents: make(map[string]artifacts.OCI),
|
||||
contents: make(map[name.Reference]artifact.OCI),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (k *k3s) Contents() (map[string]artifacts.OCI, error) {
|
||||
func (k *k3s) Contents() (map[name.Reference]artifact.OCI, error) {
|
||||
if err := k.compute(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -98,18 +94,31 @@ func (k *k3s) executable() error {
|
||||
return ErrExecutableNotfound
|
||||
}
|
||||
|
||||
f := file.NewFile(fref)
|
||||
f, err := file.NewFile(fref, "k3s")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ref, err := name.ParseReference("hauler/k3s", name.WithDefaultTag(k.dnsCompliantVersion()), name.WithDefaultRegistry(""))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ref := fmt.Sprintf("%s/k3s:%s", reference.DefaultNamespace, k.dnsCompliantVersion())
|
||||
k.contents[ref] = f
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *k3s) bootstrap() error {
|
||||
c := getter.NewClient(getter.ClientOptions{NameOverride: "k3s-init.sh"})
|
||||
f := file.NewFile(bootstrapUrl, file.WithClient(c))
|
||||
f, err := file.NewFile(bootstrapUrl, "get-k3s.io")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ref, err := name.ParseReference("hauler/get-k3s.io", name.WithDefaultRegistry(""), name.WithDefaultTag("latest"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ref := fmt.Sprintf("%s/k3s-init.sh:%s", reference.DefaultNamespace, reference.DefaultTag)
|
||||
k.contents[ref] = f
|
||||
return nil
|
||||
}
|
||||
@@ -126,12 +135,16 @@ func (k *k3s) images() error {
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
for scanner.Scan() {
|
||||
reference := scanner.Text()
|
||||
ref, err := name.ParseReference(reference)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o, err := image.NewImage(reference)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k.contents[reference] = o
|
||||
k.contents[ref] = o
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
71
pkg/collection/k3s/k3s_test.go
Normal file
71
pkg/collection/k3s/k3s_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package k3s
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifact"
|
||||
"github.com/rancherfederal/hauler/pkg/log"
|
||||
"github.com/rancherfederal/hauler/pkg/store"
|
||||
)
|
||||
|
||||
// TODO: This is not at all a good test, we really just need to test the added collections functionality (like image scanning)
|
||||
func TestNewK3s(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
l := log.NewLogger(os.Stdout)
|
||||
ctx = l.WithContext(ctx)
|
||||
|
||||
tmpdir, err := os.MkdirTemp("", "hauler")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
defer os.Remove(tmpdir)
|
||||
|
||||
s := store.NewStore(ctx, tmpdir)
|
||||
s.Open()
|
||||
defer s.Close()
|
||||
|
||||
type args struct {
|
||||
version string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want artifact.Collection
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "should work",
|
||||
args: args{
|
||||
version: "v1.22.2+k3s2",
|
||||
},
|
||||
want: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := NewK3s(tt.args.version)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NewK3s() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
c, err := got.Contents()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for r, o := range c {
|
||||
if _, err := s.AddArtifact(ctx, o, r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// if !reflect.DeepEqual(got, tt.want) {
|
||||
// t.Errorf("NewK3s() got = %v, want %v", got, tt.want)
|
||||
// }
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package consts
|
||||
|
||||
const (
|
||||
OCIManifestSchema1 = "application/vnd.oci.image.manifest.v1+json"
|
||||
DockerManifestSchema2 = "application/vnd.docker.distribution.manifest.v2+json"
|
||||
DockerManifestListSchema2 = "application/vnd.docker.distribution.manifest.list.v2+json"
|
||||
OCIImageIndexSchema = "application/vnd.oci.image.index.v1+json"
|
||||
|
||||
DockerConfigJSON = "application/vnd.docker.container.image.v1+json"
|
||||
DockerLayer = "application/vnd.docker.image.rootfs.diff.tar.gzip"
|
||||
DockerForeignLayer = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"
|
||||
DockerUncompressedLayer = "application/vnd.docker.image.rootfs.diff.tar"
|
||||
OCILayer = "application/vnd.oci.image.layer.v1.tar+gzip"
|
||||
OCIArtifact = "application/vnd.oci.empty.v1+json"
|
||||
|
||||
// ChartConfigMediaType is the reserved media type for the Helm chart manifest config
|
||||
ChartConfigMediaType = "application/vnd.cncf.helm.config.v1+json"
|
||||
|
||||
// ChartLayerMediaType is the reserved media type for Helm chart package content
|
||||
ChartLayerMediaType = "application/vnd.cncf.helm.chart.content.v1.tar+gzip"
|
||||
|
||||
// ProvLayerMediaType is the reserved media type for Helm chart provenance files
|
||||
ProvLayerMediaType = "application/vnd.cncf.helm.chart.provenance.v1.prov"
|
||||
|
||||
// FileLayerMediaType is the reserved media type for File content layers
|
||||
FileLayerMediaType = "application/vnd.content.hauler.file.layer.v1"
|
||||
|
||||
// FileLocalConfigMediaType is the reserved media type for File config
|
||||
FileLocalConfigMediaType = "application/vnd.content.hauler.file.local.config.v1+json"
|
||||
FileDirectoryConfigMediaType = "application/vnd.content.hauler.file.directory.config.v1+json"
|
||||
FileHttpConfigMediaType = "application/vnd.content.hauler.file.http.config.v1+json"
|
||||
|
||||
// MemoryConfigMediaType is the reserved media type for Memory config for a generic set of bytes stored in memory
|
||||
MemoryConfigMediaType = "application/vnd.content.hauler.memory.config.v1+json"
|
||||
|
||||
// WasmArtifactLayerMediaType is the reserved media type for WASM artifact layers
|
||||
WasmArtifactLayerMediaType = "application/vnd.wasm.content.layer.v1+wasm"
|
||||
|
||||
// WasmConfigMediaType is the reserved media type for WASM configs
|
||||
WasmConfigMediaType = "application/vnd.wasm.config.v1+json"
|
||||
|
||||
UnknownManifest = "application/vnd.hauler.cattle.io.unknown.v1+json"
|
||||
UnknownLayer = "application/vnd.content.hauler.unknown.layer"
|
||||
|
||||
OCIVendorPrefix = "vnd.oci"
|
||||
DockerVendorPrefix = "vnd.docker"
|
||||
HaulerVendorPrefix = "vnd.hauler"
|
||||
OCIImageIndexFile = "index.json"
|
||||
|
||||
KindAnnotationName = "kind"
|
||||
KindAnnotation = "dev.cosignproject.cosign/image"
|
||||
|
||||
CarbideRegistry = "rgcrprod.azurecr.us"
|
||||
ImageAnnotationKey = "hauler.dev/key"
|
||||
ImageAnnotationPlatform = "hauler.dev/platform"
|
||||
ImageAnnotationRegistry = "hauler.dev/registry"
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user