mirror of
https://github.com/hauler-dev/hauler.git
synced 2026-02-15 02:19:55 +00:00
Compare commits
158 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c68654424 | ||
|
|
8ecd87d944 | ||
|
|
a355898171 | ||
|
|
3440b1a641 | ||
|
|
9081ac257b | ||
|
|
a01895bfff | ||
|
|
e8a5f82b7d | ||
|
|
dffcb8254c | ||
|
|
4a2b7b13a7 | ||
|
|
cf22fa8551 | ||
|
|
28432fc057 | ||
|
|
ac7d82b55f | ||
|
|
ded947d609 | ||
|
|
ff3cece87f | ||
|
|
c54065f316 | ||
|
|
382dea42a5 | ||
|
|
3c073688f3 | ||
|
|
96bab7b81f | ||
|
|
5ea9b29b8f | ||
|
|
15867e84ad | ||
|
|
c5da018450 | ||
|
|
5edc8802ee | ||
|
|
a3d62b204f | ||
|
|
d85a1b0775 | ||
|
|
ea10bc0256 | ||
|
|
1aea670588 | ||
|
|
f1a632a207 | ||
|
|
802e062f47 | ||
|
|
d227e1f18f | ||
|
|
33a9bb3f78 | ||
|
|
344c008607 | ||
|
|
09a149dab6 | ||
|
|
f7f1e2db8f | ||
|
|
0fafca87f9 | ||
|
|
38e676e934 | ||
|
|
369c85bab9 | ||
|
|
acbd1f1b6a | ||
|
|
3e44c53b75 | ||
|
|
062bb3ff2c | ||
|
|
c8b4e80371 | ||
|
|
d86957bf20 | ||
|
|
4a6fc8cec2 | ||
|
|
e089c31879 | ||
|
|
b7b599e6ed | ||
|
|
ea53002f3a | ||
|
|
4d0f779ae6 | ||
|
|
4d0b407452 | ||
|
|
3b96a95a94 | ||
|
|
f9a188259f | ||
|
|
5021f3ab6b | ||
|
|
db065a1088 | ||
|
|
01bf58de03 | ||
|
|
38b979d0c5 | ||
|
|
7de20a1f15 | ||
|
|
088fde5aa9 | ||
|
|
eb275b9690 | ||
|
|
7d28df1949 | ||
|
|
08f566fb28 | ||
|
|
c465d2c143 | ||
|
|
39325585eb | ||
|
|
535a82c1b5 | ||
|
|
53cf953750 | ||
|
|
ff144b1180 | ||
|
|
938914ba5c | ||
|
|
603249dea9 | ||
|
|
37032f5379 | ||
|
|
ec9ac48476 | ||
|
|
5f5cd64c2f | ||
|
|
882713b725 | ||
|
|
a20d7bf950 | ||
|
|
e97adcdfed | ||
|
|
cc17b030a9 | ||
|
|
090f4dc905 | ||
|
|
74aa40c69b | ||
|
|
f5e3b38a6d | ||
|
|
01faf396bb | ||
|
|
235218cfff | ||
|
|
4270a27819 | ||
|
|
1b77295438 | ||
|
|
38c7d1b17a | ||
|
|
2fa6c36208 | ||
|
|
dd50ed9dba | ||
|
|
fb100a27ac | ||
|
|
3406d5453d | ||
|
|
991f5b6bc1 | ||
|
|
0595ab043a | ||
|
|
73e5c1ec8b | ||
|
|
25d8cb83b2 | ||
|
|
9f7229a36b | ||
|
|
b294b6f026 | ||
|
|
ebd3fd66c8 | ||
|
|
6373a476b5 | ||
|
|
2c7aacd105 | ||
|
|
bbcbe0239a | ||
|
|
8a53a26a58 | ||
|
|
41d88954c6 | ||
|
|
caaed30297 | ||
|
|
aee296d48d | ||
|
|
407ed94a0b | ||
|
|
15a9e1a3c4 | ||
|
|
6510947bb9 | ||
|
|
01eebd54af | ||
|
|
5aa55e9eda | ||
|
|
6f8cd04a32 | ||
|
|
02231d716f | ||
|
|
16fa03fec8 | ||
|
|
51fe531c64 | ||
|
|
1a6ce4290f | ||
|
|
e4ec7bed76 | ||
|
|
cb81823487 | ||
|
|
2d930b5653 | ||
|
|
bd0cd8f428 | ||
|
|
d6b3c94920 | ||
|
|
20958826ef | ||
|
|
d633eeffcc | ||
|
|
c592551a37 | ||
|
|
ef3eb05fce | ||
|
|
3f64914097 | ||
|
|
6a74668e2c | ||
|
|
0c5cf20e87 | ||
|
|
513719bc9e | ||
|
|
047b7a7003 | ||
|
|
a4685169c6 | ||
|
|
47549615c4 | ||
|
|
2d725026dc | ||
|
|
60667b7116 | ||
|
|
7d62a1c98e | ||
|
|
894ffb1533 | ||
|
|
78b3442d23 | ||
|
|
cd46febb6b | ||
|
|
0957a930dd | ||
|
|
a6bc6308d9 | ||
|
|
1304cf6c76 | ||
|
|
f2e02c80c0 | ||
|
|
25806e993e | ||
|
|
05e67bc750 | ||
|
|
b43ed0503a | ||
|
|
27e2fc9de0 | ||
|
|
d32d75b93e | ||
|
|
ceb77601d0 | ||
|
|
d90545a9e4 | ||
|
|
bef141ab67 | ||
|
|
385d767c2a | ||
|
|
22edc77506 | ||
|
|
9058797bbc | ||
|
|
35e2f655da | ||
|
|
f5c0f6f0ae | ||
|
|
0ec77b4168 | ||
|
|
7a7906b8ea | ||
|
|
f4774445f6 | ||
|
|
d59b29bfce | ||
|
|
fd702202ac | ||
|
|
9e9565717b | ||
|
|
bfe47ae141 | ||
|
|
ebab7f38a0 | ||
|
|
f0cba3c2c6 | ||
|
|
286120da50 | ||
|
|
dcdeb93518 |
@@ -1,8 +0,0 @@
|
||||
*
|
||||
!cmd
|
||||
!go.mod
|
||||
!go.sum
|
||||
!internal
|
||||
!Makefile
|
||||
!pkg
|
||||
!static
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Create a report to help us improve!
|
||||
about: Submit a bug report to help us improve!
|
||||
title: '[BUG]'
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,14 +1,14 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Submit a request for us to improve!
|
||||
title: '[RFE]'
|
||||
about: Submit a feature request for us to improve!
|
||||
title: '[feature]'
|
||||
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:**
|
||||
**Is this Feature/Enhancement related to an Existing Problem? If so, please describe:**
|
||||
|
||||
<!-- Provide a clear and concise description of the problem -->
|
||||
|
||||
|
||||
51
.github/workflows/pages.yaml
vendored
Normal file
51
.github/workflows/pages.yaml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Pages Workflow
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy-pages:
|
||||
name: Deploy GitHub Pages
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
steps:
|
||||
- name: Clean Up Actions Tools Cache
|
||||
run: rm -rf /opt/hostedtoolcache
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Upload Pages Artifacts
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: './static'
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
72
.github/workflows/release.yaml
vendored
72
.github/workflows/release.yaml
vendored
@@ -7,58 +7,58 @@ on:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
go-release:
|
||||
name: Go Release Job
|
||||
goreleaser:
|
||||
name: GoReleaser Job
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Clean Up Actions Tools Cache
|
||||
run: rm -rf /opt/hostedtoolcache
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Set Up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6.1.0
|
||||
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
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
- name: Set Up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v3.7.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v3.11.1
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
- name: Authenticate to GitHub Container Registry
|
||||
uses: docker/login-action@v3.6.0
|
||||
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
|
||||
- name: Authenticate to DockerHub Container Registry
|
||||
uses: docker/login-action@v3.6.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6.4.0
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: "~> v2"
|
||||
args: "release --clean --timeout 60m"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
|
||||
DOCKER_CLI_EXPERIMENTAL: "enabled"
|
||||
|
||||
41
.github/workflows/testdata.yaml
vendored
Normal file
41
.github/workflows/testdata.yaml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Refresh Hauler Testdata
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
refresh-testdata:
|
||||
name: Refresh Hauler Testdata
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Clean Up Actions Tools Cache
|
||||
run: rm -rf /opt/hostedtoolcache
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6.0.1
|
||||
|
||||
- name: Fetch Hauler Binary
|
||||
run: curl -sfL https://get.hauler.dev | bash
|
||||
|
||||
- name: Login to GitHub Container Registry and Docker Hub Container Registry
|
||||
run: |
|
||||
hauler login ghcr.io --username ${{ github.repository_owner }} --password ${{ secrets.GITHUB_TOKEN }}
|
||||
hauler login docker.io --username ${{ secrets.DOCKERHUB_USERNAME }} --password ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Process Images for Tests
|
||||
run: |
|
||||
hauler store add image nginx:1.25-alpine
|
||||
hauler store add image nginx:1.26-alpine
|
||||
hauler store add image busybox
|
||||
hauler store add image busybox:stable
|
||||
hauler store add image gcr.io/distroless/base
|
||||
hauler store add image gcr.io/distroless/base@sha256:7fa7445dfbebae4f4b7ab0e6ef99276e96075ae42584af6286ba080750d6dfe5
|
||||
|
||||
- name: Push Store Contents to Hauler-Dev GitHub Container Registry
|
||||
run: |
|
||||
hauler store copy registry://ghcr.io/${{ github.repository_owner }}
|
||||
|
||||
- name: Verify Hauler Store Contents
|
||||
run: hauler store info
|
||||
480
.github/workflows/tests.yaml
vendored
Normal file
480
.github/workflows/tests.yaml
vendored
Normal file
@@ -0,0 +1,480 @@
|
||||
name: Tests Workflow
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
unit-tests:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Clean Up Actions Tools Cache
|
||||
run: rm -rf /opt/hostedtoolcache
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Set Up Go
|
||||
uses: actions/setup-go@v6.1.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
- name: Install Go Releaser
|
||||
uses: goreleaser/goreleaser-action@v6.4.0
|
||||
with:
|
||||
install-only: true
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y make
|
||||
sudo apt-get install -y build-essential
|
||||
|
||||
- name: Run Makefile Targets
|
||||
run: |
|
||||
make build-all
|
||||
|
||||
- name: Upload Hauler Binaries
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: hauler-binaries
|
||||
path: dist/*
|
||||
|
||||
- name: Upload Coverage Report
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: coverage-report
|
||||
path: coverage.out
|
||||
|
||||
integration-tests:
|
||||
name: Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: [unit-tests]
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Clean Up Actions Tools Cache
|
||||
run: rm -rf /opt/hostedtoolcache
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y unzip
|
||||
sudo apt-get install -y tree
|
||||
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v6.0.0
|
||||
with:
|
||||
name: hauler-binaries
|
||||
path: dist
|
||||
|
||||
- name: Prepare Hauler for Tests
|
||||
run: |
|
||||
pwd
|
||||
ls -la
|
||||
ls -la dist/
|
||||
chmod -R 755 dist/ testdata/certificate-script.sh
|
||||
sudo mv dist/hauler_linux_amd64_v1/hauler /usr/local/bin/hauler
|
||||
./testdata/certificate-script.sh && sudo chown -R $(whoami) testdata/certs/
|
||||
|
||||
- name: Verify - hauler version
|
||||
run: |
|
||||
hauler version
|
||||
|
||||
- name: Verify - hauler completion
|
||||
run: |
|
||||
hauler completion
|
||||
hauler completion bash
|
||||
hauler completion fish
|
||||
hauler completion powershell
|
||||
hauler completion zsh
|
||||
|
||||
- name: Verify - hauler help
|
||||
run: |
|
||||
hauler help
|
||||
|
||||
- name: Verify - hauler login
|
||||
run: |
|
||||
hauler login --help
|
||||
hauler login docker.io --username ${{ secrets.DOCKERHUB_USERNAME }} --password ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
echo ${{ secrets.GITHUB_TOKEN }} | hauler login ghcr.io --username ${{ github.repository_owner }} --password-stdin
|
||||
|
||||
- name: Verify - hauler store
|
||||
run: |
|
||||
hauler store --help
|
||||
|
||||
- name: Verify - hauler store add
|
||||
run: |
|
||||
hauler store add --help
|
||||
|
||||
- name: Verify - hauler store add chart
|
||||
run: |
|
||||
hauler store add chart --help
|
||||
# verify via helm repository
|
||||
hauler store add chart rancher --repo https://releases.rancher.com/server-charts/stable
|
||||
hauler store add chart rancher --repo https://releases.rancher.com/server-charts/stable --version 2.8.4
|
||||
hauler store add chart rancher --repo https://releases.rancher.com/server-charts/stable --version 2.8.3 --verify
|
||||
# verify via oci helm repository
|
||||
hauler store add chart hauler-helm --repo oci://ghcr.io/hauler-dev
|
||||
hauler store add chart hauler-helm --repo oci://ghcr.io/hauler-dev --version 1.0.6
|
||||
hauler store add chart hauler-helm --repo oci://ghcr.io/hauler-dev --version 1.0.4 --verify
|
||||
# verify via local helm repository
|
||||
curl -sfOL https://github.com/rancherfederal/rancher-cluster-templates/releases/download/rancher-cluster-templates-0.5.2/rancher-cluster-templates-0.5.2.tgz
|
||||
hauler store add chart rancher-cluster-templates-0.5.2.tgz --repo .
|
||||
curl -sfOL https://github.com/rancherfederal/rancher-cluster-templates/releases/download/rancher-cluster-templates-0.5.1/rancher-cluster-templates-0.5.1.tgz
|
||||
hauler store add chart rancher-cluster-templates-0.5.1.tgz --repo . --version 0.5.1
|
||||
curl -sfOL https://github.com/rancherfederal/rancher-cluster-templates/releases/download/rancher-cluster-templates-0.5.0/rancher-cluster-templates-0.5.0.tgz
|
||||
hauler store add chart rancher-cluster-templates-0.5.0.tgz --repo . --version 0.5.0 --verify
|
||||
# verify via the hauler store contents
|
||||
hauler store info
|
||||
|
||||
- name: Verify - hauler store add chart --rewrite
|
||||
run: |
|
||||
# add chart with rewrite flag
|
||||
hauler store add chart rancher --repo https://releases.rancher.com/server-charts/stable --version 2.8.4 --rewrite custom-path/rancher:2.8.4
|
||||
# verify new ref in store
|
||||
hauler store info | grep 'custom-path/rancher:2.8.4'
|
||||
# confrim leading slash trimmed from rewrite
|
||||
hauler store add chart rancher --repo https://releases.rancher.com/server-charts/stable --version 2.8.4 --rewrite /custom-path/rancher:2.8.4
|
||||
# verify no leading slash
|
||||
! hauler store info | grep '/custom-path/rancher:2.8.4'
|
||||
# confirm old tag used if not specified
|
||||
hauler store add chart rancher --repo https://releases.rancher.com/server-charts/stable --version 2.8.4 --rewrite /custom-path/rancher
|
||||
# confirm tag
|
||||
hauler store info | grep '2.8.4'
|
||||
|
||||
- name: Verify - hauler store add file
|
||||
run: |
|
||||
hauler store add file --help
|
||||
# verify via remote file
|
||||
hauler store add file https://get.rke2.io/install.sh
|
||||
hauler store add file https://get.rke2.io/install.sh --name rke2-install.sh
|
||||
# verify via local file
|
||||
hauler store add file testdata/hauler-manifest.yaml
|
||||
hauler store add file testdata/hauler-manifest.yaml --name hauler-manifest-local.yaml
|
||||
# verify via the hauler store contents
|
||||
hauler store info
|
||||
|
||||
- name: Verify - hauler store add image
|
||||
run: |
|
||||
hauler store add image --help
|
||||
# verify via image reference
|
||||
hauler store add image ghcr.io/hauler-dev/library/busybox
|
||||
# verify via image reference with version and platform
|
||||
hauler store add image ghcr.io/hauler-dev/library/busybox:stable --platform linux/amd64
|
||||
# verify via image reference with full reference
|
||||
hauler store add image ghcr.io/hauler-dev/distroless/base@sha256:7fa7445dfbebae4f4b7ab0e6ef99276e96075ae42584af6286ba080750d6dfe5
|
||||
# verify via the hauler store contents
|
||||
hauler store info
|
||||
|
||||
- name: Verify - hauler store add image --rewrite
|
||||
run: |
|
||||
# add image with rewrite flag
|
||||
hauler store add image ghcr.io/hauler-dev/library/busybox --rewrite custom-registry.io/custom-path/busybox:latest
|
||||
# verify new ref in store
|
||||
hauler store info | grep 'custom-registry.io/custom-path/busybox:latest'
|
||||
# confrim leading slash trimmed from rewrite
|
||||
hauler store add image ghcr.io/hauler-dev/library/busybox --rewrite /custom-path/busybox:latest
|
||||
# verify no leading slash
|
||||
! hauler store info | grep '/custom-path/busybox:latest'
|
||||
# confirm old tag used if not specified
|
||||
hauler store add image ghcr.io/hauler-dev/library/busybox:stable --rewrite /custom-path/busybox
|
||||
# confirm tag
|
||||
hauler store info | grep ':stable'
|
||||
|
||||
- name: Verify - hauler store copy
|
||||
run: |
|
||||
hauler store copy --help
|
||||
# need more tests here
|
||||
|
||||
- name: Verify - hauler store extract
|
||||
run: |
|
||||
hauler store extract --help
|
||||
# verify via extracting hauler store content
|
||||
hauler store extract hauler/hauler-manifest-local.yaml:latest
|
||||
# view extracted content from store
|
||||
cat hauler-manifest-local.yaml
|
||||
|
||||
- name: Verify - hauler store info
|
||||
run: |
|
||||
hauler store info --help
|
||||
# verify via table output
|
||||
hauler store info --output table
|
||||
# verify via json output
|
||||
hauler store info --output json
|
||||
# verify via filtered output (chart)
|
||||
hauler store info --type chart
|
||||
# verify via filtered output (file)
|
||||
hauler store info --type file
|
||||
# verify via filtered output (image)
|
||||
hauler store info --type image
|
||||
# verify store directory structure
|
||||
tree -hC store
|
||||
|
||||
- name: Verify - hauler store save
|
||||
run: |
|
||||
hauler store save --help
|
||||
# verify via save
|
||||
hauler store save
|
||||
# verify via save with filename
|
||||
hauler store save --filename store.tar.zst
|
||||
# verify via save with filename and platform (amd64)
|
||||
hauler store save --filename store-amd64.tar.zst --platform linux/amd64
|
||||
|
||||
- name: Remove Hauler Store Contents
|
||||
run: |
|
||||
rm -rf store
|
||||
hauler store info
|
||||
|
||||
- name: Verify - hauler store load
|
||||
run: |
|
||||
hauler store load --help
|
||||
# verify via load
|
||||
hauler store load
|
||||
# verify via load with multiple files
|
||||
hauler store load --filename haul.tar.zst --filename store.tar.zst
|
||||
# confirm store contents
|
||||
tar -xOf store.tar.zst index.json
|
||||
# verify via load with filename and temp directory
|
||||
hauler store load --filename store.tar.zst --tempdir /opt
|
||||
# verify via load with filename and platform (amd64)
|
||||
hauler store load --filename store-amd64.tar.zst
|
||||
|
||||
- name: Verify Hauler Store Contents
|
||||
run: |
|
||||
# verify store
|
||||
hauler store info
|
||||
# verify store directory structure
|
||||
tree -hC store
|
||||
|
||||
- name: Verify - docker load
|
||||
run: |
|
||||
docker load --help
|
||||
# verify via load
|
||||
docker load --input store-amd64.tar.zst
|
||||
|
||||
- name: Verify Docker Images Contents
|
||||
run: |
|
||||
docker images --help
|
||||
# verify images
|
||||
docker images --all
|
||||
|
||||
- name: Remove Hauler Store Contents
|
||||
run: |
|
||||
rm -rf store haul.tar.zst store.tar.zst store-amd64.tar.zst
|
||||
hauler store info
|
||||
|
||||
- name: Verify - hauler store sync
|
||||
run: |
|
||||
hauler store sync --help
|
||||
# verify via sync
|
||||
hauler store sync --filename testdata/hauler-manifest-pipeline.yaml
|
||||
# verify via sync with multiple files
|
||||
hauler store sync --filename testdata/hauler-manifest-pipeline.yaml --filename testdata/hauler-manifest.yaml
|
||||
# need more tests here
|
||||
|
||||
- name: Verify - hauler store serve
|
||||
run: |
|
||||
hauler store serve --help
|
||||
|
||||
- name: Verify - hauler store serve registry
|
||||
run: |
|
||||
hauler store serve registry --help
|
||||
# verify via registry
|
||||
hauler store serve registry &
|
||||
until curl -sf http://localhost:5000/v2/_catalog; do : ; done
|
||||
pkill -f "hauler store serve registry"
|
||||
# verify via registry with different port
|
||||
hauler store serve registry --port 5001 &
|
||||
until curl -sf http://localhost:5001/v2/_catalog; do : ; done
|
||||
pkill -f "hauler store serve registry --port 5001"
|
||||
# verify via registry with different port and readonly
|
||||
hauler store serve registry --port 5001 --readonly &
|
||||
until curl -sf http://localhost:5001/v2/_catalog; do : ; done
|
||||
pkill -f "hauler store serve registry --port 5001 --readonly"
|
||||
# verify via registry with different port with readonly with tls
|
||||
# hauler store serve registry --port 5001 --readonly --tls-cert testdata/certs/server-cert.crt --tls-key testdata/certs/server-cert.key &
|
||||
# until curl -sf --cacert testdata/certs/cacerts.pem https://localhost:5001/v2/_catalog; do : ; done
|
||||
# pkill -f "hauler store serve registry --port 5001 --readonly --tls-cert testdata/certs/server-cert.crt --tls-key testdata/certs/server-cert.key"
|
||||
|
||||
- name: Verify - hauler store serve fileserver
|
||||
run: |
|
||||
hauler store serve fileserver --help
|
||||
# verify via fileserver
|
||||
hauler store serve fileserver &
|
||||
until curl -sf http://localhost:8080; do : ; done
|
||||
pkill -f "hauler store serve fileserver"
|
||||
# verify via fileserver with different port
|
||||
hauler store serve fileserver --port 8000 &
|
||||
until curl -sf http://localhost:8000; do : ; done
|
||||
pkill -f "hauler store serve fileserver --port 8000"
|
||||
# verify via fileserver with different port and timeout
|
||||
hauler store serve fileserver --port 8000 --timeout 120 &
|
||||
until curl -sf http://localhost:8000; do : ; done
|
||||
pkill -f "hauler store serve fileserver --port 8000 --timeout 120"
|
||||
# verify via fileserver with different port with timeout and tls
|
||||
# hauler store serve fileserver --port 8000 --timeout 120 --tls-cert testdata/certs/server-cert.crt --tls-key testdata/certs/server-cert.key &
|
||||
# until curl -sf --cacert testdata/certs/cacerts.pem https://localhost:8000; do : ; done
|
||||
# pkill -f "hauler store serve fileserver --port 8000 --timeout 120 --tls-cert testdata/certs/server-cert.crt --tls-key testdata/certs/server-cert.key"
|
||||
|
||||
- name: Verify Hauler Store Contents
|
||||
run: |
|
||||
# verify store
|
||||
hauler store info
|
||||
# verify store directory structure
|
||||
tree -hC store
|
||||
# verify registry directory structure
|
||||
tree -hC registry
|
||||
# verify fileserver directory structure
|
||||
tree -hC fileserver
|
||||
|
||||
- name: Verify - hauler store remove (image)
|
||||
run: |
|
||||
hauler store remove --help
|
||||
# add test images
|
||||
hauler store add image ghcr.io/hauler-dev/library/nginx:1.25-alpine
|
||||
hauler store add image ghcr.io/hauler-dev/library/nginx:1.26-alpine
|
||||
# confirm artifacts
|
||||
hauler store info | grep 'nginx:1.25'
|
||||
hauler store info | grep 'nginx:1.26'
|
||||
# count blobs before delete
|
||||
BLOBS_BEFORE=$(find store/blobs/sha256 -type f | wc -l | xargs)
|
||||
echo "blobs before deletion: $BLOBS_BEFORE"
|
||||
# delete one artifact
|
||||
hauler store remove nginx:1.25 --force
|
||||
# verify artifact removed
|
||||
! hauler store info | grep -q "nginx:1.25"
|
||||
# non-deleted artifact exists
|
||||
hauler store info | grep -q "nginx:1.26"
|
||||
# count blobs after delete
|
||||
BLOBS_AFTER=$(find store/blobs/sha256 -type f | wc -l | xargs)
|
||||
echo "blobs after deletion: $BLOBS_AFTER"
|
||||
# verify only unreferenced blobs removed
|
||||
if [ "$BLOBS_AFTER" -ge "$BLOBS_BEFORE" ]; then
|
||||
echo "ERROR: No blobs were cleaned up"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$BLOBS_AFTER" -eq 0 ]; then
|
||||
echo "ERROR: All blobs deleted (shared layers removed)"
|
||||
exit 1
|
||||
fi
|
||||
# verify remaining image not missing layers
|
||||
hauler store extract ghcr.io/hauler-dev/library/nginx:1.26-alpine
|
||||
|
||||
- name: Verify - hauler store remove (chart)
|
||||
run: |
|
||||
hauler store remove --help
|
||||
# add test images
|
||||
hauler store add chart rancher --repo https://releases.rancher.com/server-charts/stable --version 2.8.4
|
||||
hauler store add chart rancher --repo https://releases.rancher.com/server-charts/stable --version 2.8.5
|
||||
# confirm artifacts
|
||||
hauler store info | grep '2.8.4'
|
||||
hauler store info | grep '2.8.5'
|
||||
# count blobs before delete
|
||||
BLOBS_BEFORE=$(find store/blobs/sha256 -type f | wc -l | xargs)
|
||||
echo "blobs before deletion: $BLOBS_BEFORE"
|
||||
# delete one artifact
|
||||
hauler store remove 2.8.4 --force
|
||||
# verify artifact removed
|
||||
! hauler store info | grep -q "2.8.4"
|
||||
# non-deleted artifact exists
|
||||
hauler store info | grep -q "2.8.5"
|
||||
# count blobs after delete
|
||||
BLOBS_AFTER=$(find store/blobs/sha256 -type f | wc -l | xargs)
|
||||
echo "blobs after deletion: $BLOBS_AFTER"
|
||||
# verify only unreferenced blobs removed
|
||||
if [ "$BLOBS_AFTER" -ge "$BLOBS_BEFORE" ]; then
|
||||
echo "ERROR: No blobs were cleaned up"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$BLOBS_AFTER" -eq 0 ]; then
|
||||
echo "ERROR: All blobs deleted (shared layers removed)"
|
||||
exit 1
|
||||
fi
|
||||
# verify remaining image not missing layers
|
||||
hauler store extract hauler/rancher:2.8.5
|
||||
|
||||
- name: Verify - hauler store remove (file)
|
||||
run: |
|
||||
hauler store remove --help
|
||||
# add test images
|
||||
hauler store add file https://get.hauler.dev
|
||||
hauler store add file https://get.rke2.io/install.sh
|
||||
# confirm artifacts
|
||||
hauler store info | grep 'get.hauler.dev'
|
||||
hauler store info | grep 'install.sh'
|
||||
# count blobs before delete
|
||||
BLOBS_BEFORE=$(find store/blobs/sha256 -type f | wc -l | xargs)
|
||||
echo "blobs before deletion: $BLOBS_BEFORE"
|
||||
# delete one artifact
|
||||
hauler store remove get.hauler.dev --force
|
||||
# verify artifact removed
|
||||
! hauler store info | grep -q "get.hauler.dev"
|
||||
# non-deleted artifact exists
|
||||
hauler store info | grep -q "install.sh"
|
||||
# count blobs after delete
|
||||
BLOBS_AFTER=$(find store/blobs/sha256 -type f | wc -l | xargs)
|
||||
echo "blobs after deletion: $BLOBS_AFTER"
|
||||
# verify only unreferenced blobs removed
|
||||
if [ "$BLOBS_AFTER" -ge "$BLOBS_BEFORE" ]; then
|
||||
echo "ERROR: No blobs were cleaned up"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$BLOBS_AFTER" -eq 0 ]; then
|
||||
echo "ERROR: All blobs deleted (shared layers removed)"
|
||||
exit 1
|
||||
fi
|
||||
# verify remaining image not missing layers
|
||||
hauler store extract hauler/install.sh:latest
|
||||
|
||||
- name: Create Hauler Report
|
||||
run: |
|
||||
hauler version >> hauler-report.txt
|
||||
hauler store info --output table >> hauler-report.txt
|
||||
|
||||
- name: Remove Hauler Store Contents
|
||||
run: |
|
||||
rm -rf store registry fileserver
|
||||
hauler store info
|
||||
|
||||
- name: Upload Hauler Report
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: hauler-report
|
||||
path: hauler-report.txt
|
||||
|
||||
- name: Verify - hauler logout
|
||||
run: |
|
||||
hauler logout --help
|
||||
hauler logout docker.io
|
||||
hauler logout ghcr.io
|
||||
|
||||
- name: Remove Hauler Store Credentials
|
||||
run: |
|
||||
rm -rf ~/.docker/config.json
|
||||
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
|
||||
19
.gitignore
vendored
19
.gitignore
vendored
@@ -1,9 +1,4 @@
|
||||
.DS_Store
|
||||
|
||||
# Vagrant
|
||||
.vagrant
|
||||
|
||||
# Editor directories and files
|
||||
**/.DS_Store
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
@@ -12,16 +7,12 @@
|
||||
*.sln
|
||||
*.sw?
|
||||
*.dir-locals.el
|
||||
|
||||
# old, ad-hoc ignores
|
||||
artifacts
|
||||
local-artifacts
|
||||
airgap-scp.sh
|
||||
|
||||
# generated
|
||||
dist/
|
||||
tmp/
|
||||
bin/
|
||||
/store/
|
||||
/registry/
|
||||
registry/
|
||||
fileserver/
|
||||
cmd/hauler/binaries
|
||||
testdata/certs/
|
||||
coverage.out
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
version: 2
|
||||
|
||||
project_name: hauler
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
- go mod download
|
||||
- rm -rf cmd/hauler/binaries
|
||||
- go fmt ./...
|
||||
- go vet ./...
|
||||
- go test ./... -cover -race -covermode=atomic -coverprofile=coverage.out
|
||||
|
||||
release:
|
||||
prerelease: auto
|
||||
make_latest: false
|
||||
|
||||
env:
|
||||
- vpkg=github.com/rancherfederal/hauler/internal/version
|
||||
- cosign_version=v2.2.3+carbide.2
|
||||
- vpkg=hauler.dev/go/hauler/internal/version
|
||||
- cosign_version=v2.2.3+carbide.3
|
||||
|
||||
builds:
|
||||
- main: cmd/hauler/main.go
|
||||
- dir: ./cmd/hauler/.
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
@@ -23,27 +28,64 @@ builds:
|
||||
- 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
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOEXPERIMENT=boringcrypto
|
||||
|
||||
universal_binaries:
|
||||
- replace: false
|
||||
|
||||
changelog:
|
||||
skip: false
|
||||
disable: false
|
||||
use: git
|
||||
|
||||
brews:
|
||||
homebrew_casks:
|
||||
- name: hauler
|
||||
tap:
|
||||
owner: rancherfederal
|
||||
repository:
|
||||
owner: hauler-dev
|
||||
name: homebrew-tap
|
||||
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
|
||||
folder: Formula
|
||||
description: "Hauler CLI"
|
||||
description: "Hauler: Airgap Swiss Army Knife"
|
||||
|
||||
dockers_v2:
|
||||
- id: hauler
|
||||
dockerfile: Dockerfile
|
||||
flags:
|
||||
- "--target=release"
|
||||
images:
|
||||
- docker.io/hauler/hauler
|
||||
- ghcr.io/hauler-dev/hauler
|
||||
tags:
|
||||
- "{{ .Version }}"
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
labels:
|
||||
"classification": "UNCLASSIFIED"
|
||||
"org.opencontainers.image.created": "{{.Date}}"
|
||||
"org.opencontainers.image.description": "Hauler: Airgap Swiss Army Knife"
|
||||
"org.opencontainers.image.name": "{{.ProjectName}}-debug"
|
||||
"org.opencontainers.image.revision": "{{.FullCommit}}"
|
||||
"org.opencontainers.image.source": "{{.GitURL}}"
|
||||
"org.opencontainers.image.version": "{{.Version}}"
|
||||
|
||||
- id: hauler-debug
|
||||
dockerfile: Dockerfile
|
||||
flags:
|
||||
- "--target=debug"
|
||||
images:
|
||||
- docker.io/hauler/hauler-debug
|
||||
- ghcr.io/hauler-dev/hauler-debug
|
||||
tags:
|
||||
- "{{ .Version }}"
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
labels:
|
||||
"classification": "UNCLASSIFIED"
|
||||
"org.opencontainers.image.created": "{{.Date}}"
|
||||
"org.opencontainers.image.description": "Hauler: Airgap Swiss Army Knife"
|
||||
"org.opencontainers.image.name": "{{.ProjectName}}-debug"
|
||||
"org.opencontainers.image.revision": "{{.FullCommit}}"
|
||||
"org.opencontainers.image.source": "{{.GitURL}}"
|
||||
"org.opencontainers.image.version": "{{.Version}}"
|
||||
36
Dockerfile
36
Dockerfile
@@ -1,25 +1,43 @@
|
||||
FROM registry.suse.com/bci/golang:1.21 AS builder
|
||||
RUN zypper --non-interactive install make bash wget ca-certificates
|
||||
# builder stage
|
||||
FROM registry.suse.com/bci/bci-base:15.7 AS builder
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
COPY . /build
|
||||
WORKDIR /build
|
||||
RUN make build
|
||||
# fetched from goreleaser build process
|
||||
COPY $TARGETPLATFORM/hauler /hauler
|
||||
|
||||
RUN echo "hauler:x:1001:1001::/home:" > /etc/passwd \
|
||||
RUN echo "hauler:x:1001:1001::/home/hauler:" > /etc/passwd \
|
||||
&& echo "hauler:x:1001:hauler" > /etc/group \
|
||||
&& mkdir /home/hauler \
|
||||
&& mkdir /store \
|
||||
&& mkdir /fileserver \
|
||||
&& mkdir /registry
|
||||
|
||||
FROM scratch
|
||||
# release stage
|
||||
FROM scratch AS release
|
||||
|
||||
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 /home/hauler/. /home/hauler
|
||||
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 /
|
||||
COPY --from=builder --chown=hauler:hauler /hauler /hauler
|
||||
|
||||
USER hauler
|
||||
ENTRYPOINT [ "/hauler" ]
|
||||
|
||||
# debug stage
|
||||
FROM alpine AS debug
|
||||
|
||||
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/hauler/. /home/hauler
|
||||
COPY --from=builder --chown=hauler:hauler /hauler /usr/local/bin/hauler
|
||||
|
||||
RUN apk --no-cache add curl
|
||||
|
||||
USER hauler
|
||||
WORKDIR /home/hauler
|
||||
|
||||
60
Makefile
60
Makefile
@@ -1,39 +1,49 @@
|
||||
SHELL:=/bin/bash
|
||||
GO_FILES=$(shell go list ./... | grep -v /vendor/)
|
||||
# Makefile for hauler
|
||||
|
||||
COSIGN_VERSION=v2.2.3+carbide.2
|
||||
# set shell
|
||||
SHELL=/bin/bash
|
||||
|
||||
.SILENT:
|
||||
# set go variables
|
||||
GO_FILES=./...
|
||||
GO_COVERPROFILE=coverage.out
|
||||
|
||||
all: fmt vet install test
|
||||
# set build variables
|
||||
BIN_DIRECTORY=bin
|
||||
DIST_DIRECTORY=dist
|
||||
|
||||
# local build of hauler for current platform
|
||||
# references/configuration from .goreleaser.yaml
|
||||
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/...;\
|
||||
goreleaser build --clean --snapshot --timeout 60m --single-target
|
||||
|
||||
build-all: fmt vet
|
||||
goreleaser build --rm-dist --snapshot
|
||||
|
||||
# local build of hauler for all platforms
|
||||
# references/configuration from .goreleaser.yaml
|
||||
build-all:
|
||||
goreleaser build --clean --snapshot --timeout 60m
|
||||
|
||||
# local release of hauler for all platforms
|
||||
# references/configuration from .goreleaser.yaml
|
||||
release:
|
||||
goreleaser release --clean --snapshot --timeout 60m
|
||||
|
||||
# install depedencies
|
||||
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/...;\
|
||||
|
||||
vet:
|
||||
go vet $(GO_FILES)
|
||||
go mod tidy
|
||||
go mod download
|
||||
CGO_ENABLED=0 go install ./cmd/...
|
||||
|
||||
# format go code
|
||||
fmt:
|
||||
go fmt $(GO_FILES)
|
||||
|
||||
# vet go code
|
||||
vet:
|
||||
go vet $(GO_FILES)
|
||||
|
||||
# test go code
|
||||
test:
|
||||
go test $(GO_FILES) -cover
|
||||
|
||||
integration_test:
|
||||
go test -tags=integration $(GO_FILES)
|
||||
go test $(GO_FILES) -cover -race -covermode=atomic -coverprofile=$(GO_COVERPROFILE)
|
||||
|
||||
# cleanup artifacts
|
||||
clean:
|
||||
rm -rf bin 2> /dev/null
|
||||
rm -rf $(BIN_DIRECTORY) $(DIST_DIRECTORY) $(GO_COVERPROFILE)
|
||||
|
||||
34
README.md
34
README.md
@@ -4,13 +4,39 @@
|
||||
|
||||
## Airgap Swiss Army Knife
|
||||
|
||||
> ⚠️ **Please Note:** Hauler and the Hauler Documentation are recently Generally Available (GA).
|
||||
|
||||
`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.
|
||||
|
||||
For more information, please review the **[Hauler Documentation](https://rancherfederal.github.io/hauler-docs)!**
|
||||
For more information, please review the **[Hauler Documentation](https://hauler.dev)!**
|
||||
|
||||
## Recent Changes
|
||||
|
||||
### In Hauler v1.4.0...
|
||||
|
||||
- Added a notice to `hauler store sync --products/--product-registry` to warn users the default registry will be updated in a future release.
|
||||
- Users will see logging notices when using the `--products/--product-registry` such as...
|
||||
- `!!! WARNING !!! [--products] will be updating its default registry in a future release...`
|
||||
- `!!! WARNING !!! [--product-registry] will be updating its default registry in a future release...`
|
||||
|
||||
### In Hauler v1.2.0...
|
||||
|
||||
- Upgraded the `apiVersion` to `v1` from `v1alpha1`
|
||||
- Users are able to use `v1` and `v1alpha1`, but `v1alpha1` is now deprecated and will be removed in a future release. We will update the community when we fully deprecate and remove the functionality of `v1alpha1`
|
||||
- Users will see logging notices when using the old `apiVersion` such as...
|
||||
- `!!! DEPRECATION WARNING !!! apiVersion [v1alpha1] will be removed in a future release...`
|
||||
---
|
||||
- Updated the behavior of `hauler store load` to default to loading a `haul` with the name of `haul.tar.zst` and requires the flag of `--filename/-f` to load a `haul` with a different name
|
||||
- Users can load multiple `hauls` by specifying multiple flags of `--filename/-f`
|
||||
- updated command usage: `hauler store load --filename hauling-hauls.tar.zst`
|
||||
- previous command usage (do not use): `hauler store load hauling-hauls.tar.zst`
|
||||
---
|
||||
- Updated the behavior of `hauler store sync` to default to syncing a `manifest` with the name of `hauler-manifest.yaml` and requires the flag of `--filename/-f` to sync a `manifest` with a different name
|
||||
- Users can sync multiple `manifests` by specifying multiple flags of `--filename/-f`
|
||||
- updated command usage: `hauler store sync --filename hauling-hauls-manifest.yaml`
|
||||
- previous command usage (do not use): `hauler store sync --files hauling-hauls-manifest.yaml`
|
||||
---
|
||||
Please review the documentation for any additional [Known Limits, Issues, and Notices](https://docs.hauler.dev/docs/known-limits)!
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -25,7 +51,7 @@ curl -sfL https://get.hauler.dev | bash
|
||||
|
||||
```bash
|
||||
# installs latest release
|
||||
brew tap rancherfederal/homebrew-tap
|
||||
brew tap hauler-dev/homebrew-tap
|
||||
brew install hauler
|
||||
```
|
||||
|
||||
|
||||
6
cmd/hauler/boringcrypto.go
Normal file
6
cmd/hauler/boringcrypto.go
Normal file
@@ -0,0 +1,6 @@
|
||||
//go:build boringcrypto
|
||||
// +build boringcrypto
|
||||
|
||||
package main
|
||||
|
||||
import _ "crypto/tls/fipsonly"
|
||||
@@ -1,25 +1,25 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"context"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/log"
|
||||
cranecmd "github.com/google/go-containerregistry/cmd/crane/cmd"
|
||||
"github.com/spf13/cobra"
|
||||
"hauler.dev/go/hauler/internal/flags"
|
||||
"hauler.dev/go/hauler/pkg/consts"
|
||||
"hauler.dev/go/hauler/pkg/log"
|
||||
)
|
||||
|
||||
type rootOpts struct {
|
||||
logLevel string
|
||||
}
|
||||
|
||||
var ro = &rootOpts{}
|
||||
|
||||
func New() *cobra.Command {
|
||||
func New(ctx context.Context, ro *flags.CliRootOpts) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "hauler",
|
||||
Short: "Airgap Swiss Army Knife",
|
||||
Use: "hauler",
|
||||
Short: "Airgap Swiss Army Knife",
|
||||
Example: " View the Docs: https://docs.hauler.dev\n Environment Variables: " + consts.HaulerDir + " | " + consts.HaulerTempDir + " | " + consts.HaulerStoreDir + " | " + consts.HaulerIgnoreErrors + "\n Warnings: Hauler commands and flags marked with (EXPERIMENTAL) are not yet stable and may change in the future.",
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
l := log.FromContext(cmd.Context())
|
||||
l.SetLevel(ro.logLevel)
|
||||
l := log.FromContext(ctx)
|
||||
l.SetLevel(ro.LogLevel)
|
||||
l.Debugf("running cli command [%s]", cmd.CommandPath())
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -27,14 +27,13 @@ func New() *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
pf := cmd.PersistentFlags()
|
||||
pf.StringVarP(&ro.logLevel, "log-level", "l", "info", "")
|
||||
flags.AddRootFlags(cmd, ro)
|
||||
|
||||
// Add subcommands
|
||||
addLogin(cmd)
|
||||
addStore(cmd)
|
||||
addVersion(cmd)
|
||||
addCompletion(cmd)
|
||||
cmd.AddCommand(cranecmd.NewCmdAuthLogin("hauler"))
|
||||
cmd.AddCommand(cranecmd.NewCmdAuthLogout("hauler"))
|
||||
addStore(cmd, ro)
|
||||
addVersion(cmd, ro)
|
||||
addCompletion(cmd, ro)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -3,54 +3,50 @@ package cli
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"hauler.dev/go/hauler/internal/flags"
|
||||
)
|
||||
|
||||
func addCompletion(parent *cobra.Command) {
|
||||
func addCompletion(parent *cobra.Command, ro *flags.CliRootOpts) {
|
||||
cmd := &cobra.Command{
|
||||
Use: "completion",
|
||||
Short: "Generates completion scripts for various shells",
|
||||
Long: `The completion sub-command generates completion scripts for various shells.`,
|
||||
Use: "completion",
|
||||
Short: "Generate auto-completion scripts for various shells",
|
||||
}
|
||||
|
||||
|
||||
cmd.AddCommand(
|
||||
addCompletionZsh(),
|
||||
addCompletionBash(),
|
||||
addCompletionFish(),
|
||||
addCompletionPowershell(),
|
||||
addCompletionZsh(ro),
|
||||
addCompletionBash(ro),
|
||||
addCompletionFish(ro),
|
||||
addCompletionPowershell(ro),
|
||||
)
|
||||
|
||||
|
||||
parent.AddCommand(cmd)
|
||||
}
|
||||
|
||||
func completionError(err error) ([]string, cobra.ShellCompDirective) {
|
||||
cobra.CompError(err.Error())
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
|
||||
func addCompletionZsh() *cobra.Command {
|
||||
func addCompletionZsh(ro *flags.CliRootOpts) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "zsh",
|
||||
Short: "Generates zsh completion scripts",
|
||||
Long: `The completion sub-command generates completion scripts for zsh.`,
|
||||
Short: "Generates auto-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)
|
||||
cmd.Root().GenZshCompletion(os.Stdout)
|
||||
// Cobra doesn't source zsh completion file, explicitly doing it here
|
||||
fmt.Println("compdef _hauler hauler")
|
||||
},
|
||||
@@ -58,66 +54,63 @@ func addCompletionZsh() *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addCompletionBash() *cobra.Command {
|
||||
func addCompletionBash(ro *flags.CliRootOpts) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "bash",
|
||||
Short: "Generates bash completion scripts",
|
||||
Long: `The completion sub-command generates completion scripts for bash.`,
|
||||
Short: "Generates auto-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)
|
||||
cmd.Root().GenBashCompletion(os.Stdout)
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addCompletionFish() *cobra.Command {
|
||||
func addCompletionFish(ro *flags.CliRootOpts) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "fish",
|
||||
Short: "Generates fish completion scripts",
|
||||
Long: `The completion sub-command generates completion scripts for fish.`,
|
||||
Short: "Generates auto-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)
|
||||
cmd.Root().GenFishCompletion(os.Stdout, true)
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addCompletionPowershell() *cobra.Command {
|
||||
func addCompletionPowershell(ro *flags.CliRootOpts) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "powershell",
|
||||
Short: "Generates powershell completion scripts",
|
||||
Long: `The completion sub-command generates completion scripts for powershell.`,
|
||||
Short: "Generates auto-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)
|
||||
cmd.Root().GenPowerShellCompletion(os.Stdout)
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,48 +1,50 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"fmt"
|
||||
|
||||
"github.com/rancherfederal/hauler/cmd/hauler/cli/store"
|
||||
"github.com/spf13/cobra"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
|
||||
"hauler.dev/go/hauler/cmd/hauler/cli/store"
|
||||
"hauler.dev/go/hauler/internal/flags"
|
||||
"hauler.dev/go/hauler/pkg/log"
|
||||
)
|
||||
|
||||
var rootStoreOpts = &store.RootOpts{}
|
||||
func addStore(parent *cobra.Command, ro *flags.CliRootOpts) {
|
||||
rso := &flags.StoreRootOpts{}
|
||||
|
||||
func addStore(parent *cobra.Command) {
|
||||
cmd := &cobra.Command{
|
||||
Use: "store",
|
||||
Aliases: []string{"s"},
|
||||
Short: "Interact with hauler's embedded content store",
|
||||
Short: "Interact with the content store",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cmd.Help()
|
||||
},
|
||||
}
|
||||
rootStoreOpts.AddArgs(cmd)
|
||||
rso.AddFlags(cmd)
|
||||
|
||||
cmd.AddCommand(
|
||||
addStoreSync(),
|
||||
addStoreExtract(),
|
||||
addStoreLoad(),
|
||||
addStoreSave(),
|
||||
addStoreServe(),
|
||||
addStoreInfo(),
|
||||
addStoreCopy(),
|
||||
|
||||
// TODO: Remove this in favor of sync?
|
||||
addStoreAdd(),
|
||||
addStoreSync(rso, ro),
|
||||
addStoreExtract(rso, ro),
|
||||
addStoreLoad(rso, ro),
|
||||
addStoreSave(rso, ro),
|
||||
addStoreServe(rso, ro),
|
||||
addStoreInfo(rso, ro),
|
||||
addStoreCopy(rso, ro),
|
||||
addStoreAdd(rso, ro),
|
||||
addStoreRemove(rso, ro),
|
||||
)
|
||||
|
||||
parent.AddCommand(cmd)
|
||||
}
|
||||
|
||||
func addStoreExtract() *cobra.Command {
|
||||
o := &store.ExtractOpts{RootOpts: rootStoreOpts}
|
||||
func addStoreExtract(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Command {
|
||||
o := &flags.ExtractOpts{StoreRootOpts: rso}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "extract",
|
||||
Short: "Extract content from the store to disk",
|
||||
Short: "Extract artifacts from the content store to disk",
|
||||
Aliases: []string{"x"},
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -56,17 +58,35 @@ func addStoreExtract() *cobra.Command {
|
||||
return store.ExtractCmd(ctx, o, s, args[0])
|
||||
},
|
||||
}
|
||||
o.AddArgs(cmd)
|
||||
o.AddFlags(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addStoreSync() *cobra.Command {
|
||||
o := &store.SyncOpts{RootOpts: rootStoreOpts}
|
||||
func addStoreSync(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Command {
|
||||
o := &flags.SyncOpts{StoreRootOpts: rso}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "sync",
|
||||
Short: "Sync content to the embedded content store",
|
||||
Short: "Sync content to the content store",
|
||||
Args: cobra.ExactArgs(0),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// warn if products or product-registry flag is used by the user
|
||||
if cmd.Flags().Changed("products") {
|
||||
log.FromContext(cmd.Context()).Warnf("!!! WARNING !!! [--products] will be updating its default registry in a future release.")
|
||||
}
|
||||
if cmd.Flags().Changed("product-registry") {
|
||||
log.FromContext(cmd.Context()).Warnf("!!! WARNING !!! [--product-registry] will be updating its default registry in a future release.")
|
||||
}
|
||||
// check if the products flag was passed
|
||||
if len(o.Products) > 0 {
|
||||
// only clear the default if the user did not explicitly set it
|
||||
if !cmd.Flags().Changed("filename") {
|
||||
o.FileName = []string{}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
@@ -75,7 +95,7 @@ func addStoreSync() *cobra.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
return store.SyncCmd(ctx, o, s)
|
||||
return store.SyncCmd(ctx, o, s, rso, ro)
|
||||
},
|
||||
}
|
||||
o.AddFlags(cmd)
|
||||
@@ -83,13 +103,13 @@ func addStoreSync() *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addStoreLoad() *cobra.Command {
|
||||
o := &store.LoadOpts{RootOpts: rootStoreOpts}
|
||||
func addStoreLoad(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Command {
|
||||
o := &flags.LoadOpts{StoreRootOpts: rso}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "load",
|
||||
Short: "Load a content store from a store archive",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
@@ -99,7 +119,7 @@ func addStoreLoad() *cobra.Command {
|
||||
}
|
||||
_ = s
|
||||
|
||||
return store.LoadCmd(ctx, o, args...)
|
||||
return store.LoadCmd(ctx, o, rso, ro)
|
||||
},
|
||||
}
|
||||
o.AddFlags(cmd)
|
||||
@@ -107,29 +127,29 @@ func addStoreLoad() *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addStoreServe() *cobra.Command {
|
||||
func addStoreServe(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Expose the content of a local store through an OCI compliant registry or file server",
|
||||
Short: "Serve the content store via an OCI Compliant Registry or Fileserver",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cmd.Help()
|
||||
},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
addStoreServeRegistry(),
|
||||
addStoreServeFiles(),
|
||||
addStoreServeRegistry(rso, ro),
|
||||
addStoreServeFiles(rso, ro),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RegistryCmd serves the embedded registry
|
||||
func addStoreServeRegistry() *cobra.Command {
|
||||
o := &store.ServeRegistryOpts{RootOpts: rootStoreOpts}
|
||||
func addStoreServeRegistry(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Command {
|
||||
o := &flags.ServeRegistryOpts{StoreRootOpts: rso}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "registry",
|
||||
Short: "Serve the embedded registry",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
Use: "registry",
|
||||
Short: "Serve the OCI Compliant Registry",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
s, err := o.Store(ctx)
|
||||
@@ -137,22 +157,22 @@ func addStoreServeRegistry() *cobra.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
return store.ServeRegistryCmd(ctx, o, s)
|
||||
},
|
||||
}
|
||||
return store.ServeRegistryCmd(ctx, o, s, rso, ro)
|
||||
},
|
||||
}
|
||||
|
||||
o.AddFlags(cmd)
|
||||
o.AddFlags(cmd)
|
||||
|
||||
return cmd
|
||||
return cmd
|
||||
}
|
||||
|
||||
// FileServerCmd serves the file server
|
||||
func addStoreServeFiles() *cobra.Command {
|
||||
o := &store.ServeFilesOpts{RootOpts: rootStoreOpts}
|
||||
func addStoreServeFiles(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Command {
|
||||
o := &flags.ServeFilesOpts{StoreRootOpts: rso}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "fileserver",
|
||||
Short: "Serve the file server",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
Use: "fileserver",
|
||||
Short: "Serve the Fileserver",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
s, err := o.Store(ctx)
|
||||
@@ -160,17 +180,17 @@ func addStoreServeFiles() *cobra.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
return store.ServeFilesCmd(ctx, o, s)
|
||||
},
|
||||
}
|
||||
return store.ServeFilesCmd(ctx, o, s, ro)
|
||||
},
|
||||
}
|
||||
|
||||
o.AddFlags(cmd)
|
||||
o.AddFlags(cmd)
|
||||
|
||||
return cmd
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addStoreSave() *cobra.Command {
|
||||
o := &store.SaveOpts{RootOpts: rootStoreOpts}
|
||||
func addStoreSave(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Command {
|
||||
o := &flags.SaveOpts{StoreRootOpts: rso}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "save",
|
||||
@@ -185,16 +205,16 @@ func addStoreSave() *cobra.Command {
|
||||
}
|
||||
_ = s
|
||||
|
||||
return store.SaveCmd(ctx, o, o.FileName)
|
||||
return store.SaveCmd(ctx, o, rso, ro)
|
||||
},
|
||||
}
|
||||
o.AddArgs(cmd)
|
||||
o.AddFlags(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addStoreInfo() *cobra.Command {
|
||||
o := &store.InfoOpts{RootOpts: rootStoreOpts}
|
||||
func addStoreInfo(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Command {
|
||||
o := &flags.InfoOpts{StoreRootOpts: rso}
|
||||
|
||||
var allowedValues = []string{"image", "chart", "file", "sigs", "atts", "sbom", "all"}
|
||||
|
||||
@@ -210,7 +230,7 @@ func addStoreInfo() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
for _, allowed := range allowedValues {
|
||||
if o.TypeFilter == allowed {
|
||||
return store.InfoCmd(ctx, o, s)
|
||||
@@ -224,12 +244,12 @@ func addStoreInfo() *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addStoreCopy() *cobra.Command {
|
||||
o := &store.CopyOpts{RootOpts: rootStoreOpts}
|
||||
func addStoreCopy(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Command {
|
||||
o := &flags.CopyOpts{StoreRootOpts: rso}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "copy",
|
||||
Short: "Copy all store contents to another OCI registry",
|
||||
Short: "Copy all store content to another location",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
@@ -239,7 +259,7 @@ func addStoreCopy() *cobra.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
return store.CopyCmd(ctx, o, s, args[0])
|
||||
return store.CopyCmd(ctx, o, s, args[0], ro)
|
||||
},
|
||||
}
|
||||
o.AddFlags(cmd)
|
||||
@@ -247,31 +267,39 @@ func addStoreCopy() *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addStoreAdd() *cobra.Command {
|
||||
func addStoreAdd(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "add",
|
||||
Short: "Add content to store",
|
||||
Short: "Add content to the store",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cmd.Help()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
addStoreAddFile(),
|
||||
addStoreAddImage(),
|
||||
addStoreAddChart(),
|
||||
addStoreAddFile(rso, ro),
|
||||
addStoreAddImage(rso, ro),
|
||||
addStoreAddChart(rso, ro),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addStoreAddFile() *cobra.Command {
|
||||
o := &store.AddFileOpts{RootOpts: rootStoreOpts}
|
||||
func addStoreAddFile(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Command {
|
||||
o := &flags.AddFileOpts{StoreRootOpts: rso}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "file",
|
||||
Short: "Add a file to the content store",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Short: "Add a file to the store",
|
||||
Example: `# fetch local file
|
||||
hauler store add file file.txt
|
||||
|
||||
# fetch remote file
|
||||
hauler store add file https://get.rke2.io/install.sh
|
||||
|
||||
# fetch remote file and assign new name
|
||||
hauler store add file https://get.hauler.dev --name hauler-install.sh`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
@@ -288,51 +316,30 @@ func addStoreAddFile() *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addStoreAddImage() *cobra.Command {
|
||||
o := &store.AddImageOpts{RootOpts: rootStoreOpts}
|
||||
func addStoreAddImage(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Command {
|
||||
o := &flags.AddImageOpts{StoreRootOpts: rso}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "image",
|
||||
Short: "Add an image to the content store",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
Short: "Add a image to the store",
|
||||
Example: `# fetch image
|
||||
hauler store add image busybox
|
||||
|
||||
s, err := o.Store(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
# fetch image with repository and tag
|
||||
hauler store add image library/busybox:stable
|
||||
|
||||
return store.AddImageCmd(ctx, o, s, args[0])
|
||||
},
|
||||
}
|
||||
o.AddFlags(cmd)
|
||||
# fetch image with full image reference and specific platform
|
||||
hauler store add image ghcr.io/hauler-dev/hauler-debug:v1.2.0 --platform linux/amd64
|
||||
|
||||
return cmd
|
||||
}
|
||||
# fetch image with full image reference via digest
|
||||
hauler store add image gcr.io/distroless/base@sha256:7fa7445dfbebae4f4b7ab0e6ef99276e96075ae42584af6286ba080750d6dfe5
|
||||
|
||||
func addStoreAddChart() *cobra.Command {
|
||||
o := &store.AddChartOpts{
|
||||
RootOpts: rootStoreOpts,
|
||||
ChartOpts: &action.ChartPathOptions{},
|
||||
}
|
||||
# fetch image with full image reference, specific platform, and signature verification
|
||||
curl -sfOL https://raw.githubusercontent.com/rancherfederal/carbide-releases/main/carbide-key.pub
|
||||
hauler store add image rgcrprod.azurecr.us/rancher/rke2-runtime:v1.31.5-rke2r1 --platform linux/amd64 --key carbide-key.pub
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "chart",
|
||||
Short: "Add a local or remote 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
|
||||
hauler store add chart longhorn --repo "https://charts.longhorn.io"
|
||||
|
||||
# add a specific version of a chart
|
||||
hauler store add chart rancher --repo "https://releases.rancher.com/server-charts/latest" --version "2.6.2"
|
||||
`,
|
||||
# fetch image and rewrite path
|
||||
hauler store add image busybox --rewrite custom-path/busybox:latest`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
@@ -342,7 +349,92 @@ hauler store add chart rancher --repo "https://releases.rancher.com/server-chart
|
||||
return err
|
||||
}
|
||||
|
||||
return store.AddChartCmd(ctx, o, s, args[0])
|
||||
return store.AddImageCmd(ctx, o, s, args[0], rso, ro)
|
||||
},
|
||||
}
|
||||
o.AddFlags(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addStoreAddChart(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Command {
|
||||
o := &flags.AddChartOpts{StoreRootOpts: rso, ChartOpts: &action.ChartPathOptions{}}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "chart",
|
||||
Short: "Add a helm chart to the store",
|
||||
Example: `# fetch local helm chart
|
||||
hauler store add chart path/to/chart/directory --repo .
|
||||
|
||||
# fetch local compressed helm chart
|
||||
hauler store add chart path/to/chart.tar.gz --repo .
|
||||
|
||||
# fetch remote oci helm chart
|
||||
hauler store add chart hauler-helm --repo oci://ghcr.io/hauler-dev
|
||||
|
||||
# fetch remote oci helm chart with version
|
||||
hauler store add chart hauler-helm --repo oci://ghcr.io/hauler-dev --version 1.2.0
|
||||
|
||||
# fetch remote helm chart
|
||||
hauler store add chart rancher --repo https://releases.rancher.com/server-charts/stable
|
||||
|
||||
# fetch remote helm chart with specific version
|
||||
hauler store add chart rancher --repo https://releases.rancher.com/server-charts/latest --version 2.10.1
|
||||
|
||||
# fetch remote helm chart and rewrite path
|
||||
hauler store add chart hauler-helm --repo oci://ghcr.io/hauler-dev --rewrite custom-path/hauler-chart:latest`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
s, err := o.Store(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return store.AddChartCmd(ctx, o, s, args[0], rso, ro)
|
||||
},
|
||||
}
|
||||
o.AddFlags(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addStoreRemove(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Command {
|
||||
o := &flags.RemoveOpts{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "remove <artifact-ref>",
|
||||
Short: "(EXPERIMENTAL) Remove an artifact from the content store",
|
||||
Example: `# remove an image using full store reference
|
||||
hauler store info
|
||||
hauler store remove index.docker.io/library/busybox:stable
|
||||
|
||||
# remove a chart using full store reference
|
||||
hauler store info
|
||||
hauler store remove hauler/rancher:2.8.4
|
||||
|
||||
# remove a file using full store reference
|
||||
hauler store info
|
||||
hauler store remove hauler/rke2-install.sh
|
||||
|
||||
# remove any artifact with the latest tag
|
||||
hauler store remove :latest
|
||||
|
||||
# remove any artifact with 'busybox' in the reference
|
||||
hauler store remove busybox
|
||||
|
||||
# force remove without verification
|
||||
hauler store remove busybox:latest --force`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
s, err := rso.Store(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return store.RemoveCmd(ctx, o, s, args[0])
|
||||
},
|
||||
}
|
||||
o.AddFlags(cmd)
|
||||
|
||||
@@ -2,35 +2,34 @@ package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/file/getter"
|
||||
"github.com/spf13/cobra"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
helmchart "helm.sh/helm/v3/pkg/chart"
|
||||
"helm.sh/helm/v3/pkg/chartutil"
|
||||
"helm.sh/helm/v3/pkg/engine"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
|
||||
"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/log"
|
||||
"github.com/rancherfederal/hauler/pkg/reference"
|
||||
"hauler.dev/go/hauler/internal/flags"
|
||||
v1 "hauler.dev/go/hauler/pkg/apis/hauler.cattle.io/v1"
|
||||
"hauler.dev/go/hauler/pkg/artifacts/file"
|
||||
"hauler.dev/go/hauler/pkg/consts"
|
||||
"hauler.dev/go/hauler/pkg/content/chart"
|
||||
"hauler.dev/go/hauler/pkg/cosign"
|
||||
"hauler.dev/go/hauler/pkg/getter"
|
||||
"hauler.dev/go/hauler/pkg/log"
|
||||
"hauler.dev/go/hauler/pkg/reference"
|
||||
"hauler.dev/go/hauler/pkg/store"
|
||||
)
|
||||
|
||||
type AddFileOpts struct {
|
||||
*RootOpts
|
||||
Name string
|
||||
}
|
||||
|
||||
func (o *AddFileOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
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 {
|
||||
cfg := v1alpha1.File{
|
||||
func AddFileCmd(ctx context.Context, o *flags.AddFileOpts, s *store.Layout, reference string) error {
|
||||
cfg := v1.File{
|
||||
Path: reference,
|
||||
}
|
||||
if len(o.Name) > 0 {
|
||||
@@ -39,7 +38,7 @@ func AddFileCmd(ctx context.Context, o *AddFileOpts, s *store.Layout, reference
|
||||
return storeFile(ctx, s, cfg)
|
||||
}
|
||||
|
||||
func storeFile(ctx context.Context, s *store.Layout, fi v1alpha1.File) error {
|
||||
func storeFile(ctx context.Context, s *store.Layout, fi v1.File) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
copts := getter.ClientOptions{
|
||||
@@ -47,112 +46,293 @@ func storeFile(ctx context.Context, s *store.Layout, fi v1alpha1.File) error {
|
||||
}
|
||||
|
||||
f := file.NewFile(fi.Path, file.WithClient(getter.NewClient(copts)))
|
||||
ref, err := reference.NewTagged(f.Name(fi.Path), reference.DefaultTag)
|
||||
ref, err := reference.NewTagged(f.Name(fi.Path), consts.DefaultTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Infof("adding 'file' [%s] to the store as [%s]", fi.Path, ref.Name())
|
||||
l.Infof("adding file [%s] to the store as [%s]", fi.Path, ref.Name())
|
||||
_, err = s.AddOCI(ctx, f, ref.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Infof("successfully added 'file' [%s]", ref.Name())
|
||||
l.Infof("successfully added file [%s]", ref.Name())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type AddImageOpts struct {
|
||||
*RootOpts
|
||||
Name string
|
||||
Key string
|
||||
Platform 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.")
|
||||
}
|
||||
|
||||
func AddImageCmd(ctx context.Context, o *AddImageOpts, s *store.Layout, reference string) error {
|
||||
func AddImageCmd(ctx context.Context, o *flags.AddImageOpts, s *store.Layout, reference string, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error {
|
||||
l := log.FromContext(ctx)
|
||||
cfg := v1alpha1.Image{
|
||||
Name: reference,
|
||||
|
||||
cfg := v1.Image{
|
||||
Name: reference,
|
||||
Rewrite: o.Rewrite,
|
||||
}
|
||||
|
||||
// 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)
|
||||
err := cosign.VerifySignature(ctx, s, o.Key, o.Tlog, cfg.Name, rso, ro)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.Infof("signature verified for image [%s]", cfg.Name)
|
||||
} else if o.CertIdentityRegexp != "" || o.CertIdentity != "" {
|
||||
// verify signature using keyless details
|
||||
l.Infof("verifying keyless signature for [%s]", cfg.Name)
|
||||
err := cosign.VerifyKeylessSignature(ctx, s, o.CertIdentity, o.CertIdentityRegexp, o.CertOidcIssuer, o.CertOidcIssuerRegexp, o.CertGithubWorkflowRepository, o.Tlog, cfg.Name, rso, ro)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.Infof("keyless signature verified for image [%s]", cfg.Name)
|
||||
}
|
||||
|
||||
return storeImage(ctx, s, cfg, o.Platform)
|
||||
return storeImage(ctx, s, cfg, o.Platform, rso, ro, o.Rewrite)
|
||||
}
|
||||
|
||||
func storeImage(ctx context.Context, s *store.Layout, i v1alpha1.Image, platform string) error {
|
||||
func storeImage(ctx context.Context, s *store.Layout, i v1.Image, platform string, rso *flags.StoreRootOpts, ro *flags.CliRootOpts, rewrite string) error {
|
||||
l := log.FromContext(ctx)
|
||||
l.Infof("adding 'image' [%s] to the store", i.Name)
|
||||
|
||||
if !ro.IgnoreErrors {
|
||||
envVar := os.Getenv(consts.HaulerIgnoreErrors)
|
||||
if envVar == "true" {
|
||||
ro.IgnoreErrors = true
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
if ro.IgnoreErrors {
|
||||
l.Warnf("unable to parse image [%s]: %v... skipping...", i.Name, err)
|
||||
return nil
|
||||
} else {
|
||||
l.Errorf("unable to parse image [%s]: %v", i.Name, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
l.Infof("successfully added 'image' [%s]", r.Name())
|
||||
// copy and sig verification
|
||||
err = cosign.SaveImage(ctx, s, r.Name(), platform, rso, ro)
|
||||
if err != nil {
|
||||
if ro.IgnoreErrors {
|
||||
l.Warnf("unable to add image [%s] to store: %v... skipping...", r.Name(), err)
|
||||
return nil
|
||||
} else {
|
||||
l.Errorf("unable to add image [%s] to store: %v", r.Name(), err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if rewrite != "" {
|
||||
rewrite = strings.TrimPrefix(rewrite, "/")
|
||||
if !strings.Contains(rewrite, ":") {
|
||||
rewrite = strings.Join([]string{rewrite, r.(name.Tag).TagStr()}, ":")
|
||||
}
|
||||
// rename image name in store
|
||||
newRef, err := name.ParseReference(rewrite)
|
||||
if err != nil {
|
||||
l.Errorf("unable to parse rewrite name: %w", err)
|
||||
}
|
||||
rewriteReference(ctx, s, r, newRef)
|
||||
}
|
||||
|
||||
l.Infof("successfully added image [%s]", r.Name())
|
||||
return nil
|
||||
}
|
||||
|
||||
type AddChartOpts struct {
|
||||
*RootOpts
|
||||
func rewriteReference(ctx context.Context, s *store.Layout, oldRef name.Reference, newRef name.Reference) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
l.Infof("rewriting [%s] to [%s]", oldRef.Name(), newRef.Name())
|
||||
|
||||
s.OCI.LoadIndex()
|
||||
|
||||
//TODO: improve string manipulation
|
||||
oldRefContext := oldRef.Context()
|
||||
newRefContext := newRef.Context()
|
||||
|
||||
oldRepo := oldRefContext.RepositoryStr()
|
||||
newRepo := newRefContext.RepositoryStr()
|
||||
oldTag := oldRef.(name.Tag).TagStr()
|
||||
newTag := newRef.(name.Tag).TagStr()
|
||||
oldRegistry := strings.TrimPrefix(oldRefContext.RegistryStr(), "index.")
|
||||
newRegistry := strings.TrimPrefix(newRefContext.RegistryStr(), "index.")
|
||||
|
||||
oldTotal := oldRepo + ":" + oldTag
|
||||
newTotal := newRepo + ":" + newTag
|
||||
oldTotalReg := oldRegistry + "/" + oldTotal
|
||||
newTotalReg := newRegistry + "/" + newTotal
|
||||
|
||||
//find and update reference
|
||||
found := false
|
||||
if err := s.OCI.Walk(func(k string, d ocispec.Descriptor) error {
|
||||
if d.Annotations[ocispec.AnnotationRefName] == oldTotal && d.Annotations[consts.ContainerdImageNameKey] == oldTotalReg {
|
||||
d.Annotations[ocispec.AnnotationRefName] = newTotal
|
||||
d.Annotations[consts.ContainerdImageNameKey] = newTotalReg
|
||||
found = true
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("could not find image [%s] in store", oldRef.Name())
|
||||
}
|
||||
|
||||
return s.OCI.SaveIndex()
|
||||
|
||||
ChartOpts *action.ChartPathOptions
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
func AddChartCmd(ctx context.Context, o *AddChartOpts, s *store.Layout, chartName string) error {
|
||||
// TODO: Reduce duplicates between api chart and upstream helm opts
|
||||
cfg := v1alpha1.Chart{
|
||||
func AddChartCmd(ctx context.Context, o *flags.AddChartOpts, s *store.Layout, chartName string, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error {
|
||||
cfg := v1.Chart{
|
||||
Name: chartName,
|
||||
RepoURL: o.ChartOpts.RepoURL,
|
||||
Version: o.ChartOpts.Version,
|
||||
}
|
||||
|
||||
return storeChart(ctx, s, cfg, o.ChartOpts)
|
||||
rewrite := ""
|
||||
if o.Rewrite != "" {
|
||||
rewrite = o.Rewrite
|
||||
}
|
||||
return storeChart(ctx, s, cfg, o, rso, ro, rewrite)
|
||||
}
|
||||
|
||||
func storeChart(ctx context.Context, s *store.Layout, cfg v1alpha1.Chart, opts *action.ChartPathOptions) 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
|
||||
// unexported type for the context key to avoid collisions
|
||||
type isSubchartKey struct{}
|
||||
|
||||
chrt, err := chart.NewChart(cfg.Name, opts)
|
||||
// imageregex parses image references starting with "image:" and with optional spaces or optional quotes
|
||||
var imageRegex = regexp.MustCompile(`(?m)^[ \t]*image:[ \t]*['"]?([^\s'"#]+)`)
|
||||
|
||||
// helmAnnotatedImage parses images references from helm chart annotations
|
||||
type helmAnnotatedImage struct {
|
||||
Image string `yaml:"image"`
|
||||
Name string `yaml:"name,omitempty"`
|
||||
}
|
||||
|
||||
// imagesFromChartAnnotations parses image references from helm chart annotations
|
||||
func imagesFromChartAnnotations(c *helmchart.Chart) ([]string, error) {
|
||||
if c == nil || c.Metadata == nil || c.Metadata.Annotations == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// support multiple annotations
|
||||
keys := []string{
|
||||
"helm.sh/images",
|
||||
"images",
|
||||
}
|
||||
|
||||
var out []string
|
||||
for _, k := range keys {
|
||||
raw, ok := c.Metadata.Annotations[k]
|
||||
if !ok || strings.TrimSpace(raw) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var items []helmAnnotatedImage
|
||||
if err := yaml.Unmarshal([]byte(raw), &items); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse helm chart annotation %q: %w", k, err)
|
||||
}
|
||||
|
||||
for _, it := range items {
|
||||
img := strings.TrimSpace(it.Image)
|
||||
if img == "" {
|
||||
continue
|
||||
}
|
||||
img = strings.TrimPrefix(img, "/")
|
||||
out = append(out, img)
|
||||
}
|
||||
}
|
||||
|
||||
slices.Sort(out)
|
||||
out = slices.Compact(out)
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// imagesFromImagesLock parses image references from images lock files in the chart directory
|
||||
func imagesFromImagesLock(chartDir string) ([]string, error) {
|
||||
var out []string
|
||||
|
||||
for _, name := range []string{
|
||||
"images.lock",
|
||||
"images-lock.yaml",
|
||||
"images.lock.yaml",
|
||||
".images.lock.yaml",
|
||||
} {
|
||||
p := filepath.Join(chartDir, name)
|
||||
b, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
matches := imageRegex.FindAllSubmatch(b, -1)
|
||||
for _, m := range matches {
|
||||
if len(m) > 1 {
|
||||
out = append(out, string(m[1]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(out) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for i := range out {
|
||||
out[i] = strings.TrimPrefix(out[i], "/")
|
||||
}
|
||||
slices.Sort(out)
|
||||
out = slices.Compact(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func applyDefaultRegistry(img string, defaultRegistry string) (string, error) {
|
||||
img = strings.TrimSpace(strings.TrimPrefix(img, "/"))
|
||||
if img == "" || defaultRegistry == "" {
|
||||
return img, nil
|
||||
}
|
||||
|
||||
ref, err := reference.Parse(img)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if ref.Context().RegistryStr() != "" {
|
||||
return img, nil
|
||||
}
|
||||
|
||||
newRef, err := reference.Relocate(img, defaultRegistry)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return newRef.Name(), nil
|
||||
}
|
||||
|
||||
func storeChart(ctx context.Context, s *store.Layout, cfg v1.Chart, opts *flags.AddChartOpts, rso *flags.StoreRootOpts, ro *flags.CliRootOpts, rewrite string) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
// subchart logging prefix
|
||||
isSubchart := ctx.Value(isSubchartKey{}) == true
|
||||
prefix := ""
|
||||
if isSubchart {
|
||||
prefix = " ↳ "
|
||||
}
|
||||
|
||||
// normalize chart name for logging
|
||||
displayName := cfg.Name
|
||||
if strings.Contains(cfg.Name, string(os.PathSeparator)) {
|
||||
displayName = filepath.Base(cfg.Name)
|
||||
}
|
||||
l.Infof("%sadding chart [%s] to the store", prefix, displayName)
|
||||
|
||||
opts.ChartOpts.RepoURL = cfg.RepoURL
|
||||
opts.ChartOpts.Version = cfg.Version
|
||||
|
||||
chrt, err := chart.NewChart(cfg.Name, opts.ChartOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -166,11 +346,257 @@ func storeChart(ctx context.Context, s *store.Layout, cfg v1alpha1.Chart, opts *
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.AddOCI(ctx, chrt, ref.Name())
|
||||
if err != nil {
|
||||
|
||||
if _, err := s.AddOCI(ctx, chrt, ref.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.OCI.SaveIndex(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Infof("successfully added 'chart' [%s]", ref.Name())
|
||||
l.Infof("%ssuccessfully added chart [%s:%s]", prefix, c.Name(), c.Metadata.Version)
|
||||
|
||||
tempOverride := rso.TempOverride
|
||||
if tempOverride == "" {
|
||||
tempOverride = os.Getenv(consts.HaulerTempDir)
|
||||
}
|
||||
tempDir, err := os.MkdirTemp(tempOverride, consts.DefaultHaulerTempDirName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp dir: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
chartPath := chrt.Path()
|
||||
if strings.HasSuffix(chartPath, ".tgz") {
|
||||
l.Debugf("%sextracting chart archive [%s]", prefix, filepath.Base(chartPath))
|
||||
if err := chartutil.ExpandFile(tempDir, chartPath); err != nil {
|
||||
return fmt.Errorf("failed to extract chart: %w", err)
|
||||
}
|
||||
|
||||
// expanded chart should be in a directory matching the chart name
|
||||
expectedChartDir := filepath.Join(tempDir, c.Name())
|
||||
if _, err := os.Stat(expectedChartDir); err != nil {
|
||||
return fmt.Errorf("chart archive did not expand into expected directory '%s': %w", c.Name(), err)
|
||||
}
|
||||
chartPath = expectedChartDir
|
||||
}
|
||||
|
||||
// add-images
|
||||
if opts.AddImages {
|
||||
userValues := chartutil.Values{}
|
||||
if opts.HelmValues != "" {
|
||||
userValues, err = chartutil.ReadValuesFile(opts.HelmValues)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read helm values file [%s]: %w", opts.HelmValues, err)
|
||||
}
|
||||
}
|
||||
|
||||
// set helm default capabilities
|
||||
caps := chartutil.DefaultCapabilities.Copy()
|
||||
|
||||
// only parse and override if provided kube version
|
||||
if opts.KubeVersion != "" {
|
||||
kubeVersion, err := chartutil.ParseKubeVersion(opts.KubeVersion)
|
||||
if err != nil {
|
||||
l.Warnf("%sinvalid kube-version [%s], using default kubernetes version", prefix, opts.KubeVersion)
|
||||
} else {
|
||||
caps.KubeVersion = *kubeVersion
|
||||
}
|
||||
}
|
||||
|
||||
values, err := chartutil.ToRenderValues(c, userValues, chartutil.ReleaseOptions{Namespace: "hauler"}, caps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// helper for normalization and deduping slices
|
||||
normalizeUniq := func(in []string) []string {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
for i := range in {
|
||||
in[i] = strings.TrimPrefix(in[i], "/")
|
||||
}
|
||||
slices.Sort(in)
|
||||
return slices.Compact(in)
|
||||
}
|
||||
|
||||
// Collect images by method so we can debug counts
|
||||
var (
|
||||
templateImages []string
|
||||
annotationImages []string
|
||||
lockImages []string
|
||||
)
|
||||
|
||||
// parse helm chart templates and values for images
|
||||
rendered, err := engine.Render(c, values)
|
||||
if err != nil {
|
||||
// charts may fail due to values so still try helm chart annotations and lock
|
||||
l.Warnf("%sfailed to render chart [%s]: %v", prefix, c.Name(), err)
|
||||
rendered = map[string]string{}
|
||||
}
|
||||
|
||||
for _, manifest := range rendered {
|
||||
matches := imageRegex.FindAllStringSubmatch(manifest, -1)
|
||||
for _, match := range matches {
|
||||
if len(match) > 1 {
|
||||
templateImages = append(templateImages, match[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse helm chart annotations for images
|
||||
annotationImages, err = imagesFromChartAnnotations(c)
|
||||
if err != nil {
|
||||
l.Warnf("%sfailed to parse helm chart annotation for [%s:%s]: %v", prefix, c.Name(), c.Metadata.Version, err)
|
||||
annotationImages = nil
|
||||
}
|
||||
|
||||
// parse images lock files for images
|
||||
lockImages, err = imagesFromImagesLock(chartPath)
|
||||
if err != nil {
|
||||
l.Warnf("%sfailed to parse images lock: %v", prefix, err)
|
||||
lockImages = nil
|
||||
}
|
||||
|
||||
// normalization and deduping the slices
|
||||
templateImages = normalizeUniq(templateImages)
|
||||
annotationImages = normalizeUniq(annotationImages)
|
||||
lockImages = normalizeUniq(lockImages)
|
||||
|
||||
// merge all sources then final dedupe
|
||||
images := append(append(templateImages, annotationImages...), lockImages...)
|
||||
images = normalizeUniq(images)
|
||||
|
||||
l.Debugf("%simage references identified for helm template: [%d] image(s)", prefix, len(templateImages))
|
||||
|
||||
l.Debugf("%simage references identified for helm chart annotations: [%d] image(s)", prefix, len(annotationImages))
|
||||
|
||||
l.Debugf("%simage references identified for helm image lock file: [%d] image(s)", prefix, len(lockImages))
|
||||
l.Debugf("%ssuccessfully parsed and deduped image references: [%d] image(s)", prefix, len(images))
|
||||
|
||||
l.Debugf("%ssuccessfully parsed image references %v", prefix, images)
|
||||
|
||||
if len(images) > 0 {
|
||||
l.Infof("%s ↳ identified [%d] image(s) in [%s:%s]", prefix, len(images), c.Name(), c.Metadata.Version)
|
||||
}
|
||||
|
||||
for _, image := range images {
|
||||
image, err := applyDefaultRegistry(image, opts.Registry)
|
||||
if err != nil {
|
||||
if ro.IgnoreErrors {
|
||||
l.Warnf("%s ↳ unable to apply registry to image [%s]: %v... skipping...", prefix, image, err)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("unable to apply registry to image [%s]: %w", image, err)
|
||||
}
|
||||
|
||||
imgCfg := v1.Image{Name: image}
|
||||
if err := storeImage(ctx, s, imgCfg, opts.Platform, rso, ro, ""); err != nil {
|
||||
if ro.IgnoreErrors {
|
||||
l.Warnf("%s ↳ failed to store image [%s]: %v... skipping...", prefix, image, err)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("failed to store image [%s]: %w", image, err)
|
||||
}
|
||||
s.OCI.LoadIndex()
|
||||
if err := s.OCI.SaveIndex(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add-dependencies
|
||||
if opts.AddDependencies && len(c.Metadata.Dependencies) > 0 {
|
||||
for _, dep := range c.Metadata.Dependencies {
|
||||
l.Infof("%sadding dependent chart [%s:%s]", prefix, dep.Name, dep.Version)
|
||||
|
||||
depOpts := *opts
|
||||
depOpts.AddDependencies = true
|
||||
depOpts.AddImages = true
|
||||
subCtx := context.WithValue(ctx, isSubchartKey{}, true)
|
||||
|
||||
var depCfg v1.Chart
|
||||
var err error
|
||||
|
||||
if strings.HasPrefix(dep.Repository, "file://") {
|
||||
subchartPath := filepath.Join(chartPath, "charts", dep.Name)
|
||||
|
||||
depCfg = v1.Chart{Name: subchartPath, RepoURL: "", Version: ""}
|
||||
depOpts.ChartOpts.RepoURL = ""
|
||||
depOpts.ChartOpts.Version = ""
|
||||
|
||||
err = storeChart(subCtx, s, depCfg, &depOpts, rso, ro, "")
|
||||
} else {
|
||||
depCfg = v1.Chart{Name: dep.Name, RepoURL: dep.Repository, Version: dep.Version}
|
||||
depOpts.ChartOpts.RepoURL = dep.Repository
|
||||
depOpts.ChartOpts.Version = dep.Version
|
||||
|
||||
err = storeChart(subCtx, s, depCfg, &depOpts, rso, ro, "")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if ro.IgnoreErrors {
|
||||
l.Warnf("%s ↳ failed to add dependent chart [%s]: %v... skipping...", prefix, dep.Name, err)
|
||||
} else {
|
||||
l.Errorf("%s ↳ failed to add dependent chart [%s]: %v", prefix, dep.Name, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// chart rewrite functionality
|
||||
if rewrite != "" {
|
||||
rewrite = strings.TrimPrefix(rewrite, "/")
|
||||
newRef, err := name.ParseReference(rewrite)
|
||||
if err != nil {
|
||||
// error... don't continue with a bad reference
|
||||
return fmt.Errorf("unable to parse rewrite name [%s]: %w", rewrite, err)
|
||||
}
|
||||
|
||||
// if rewrite omits a tag... keep the existing tag
|
||||
oldTag := ref.(name.Tag).TagStr()
|
||||
if !strings.Contains(rewrite, ":") {
|
||||
rewrite = strings.Join([]string{rewrite, oldTag}, ":")
|
||||
newRef, err = name.ParseReference(rewrite)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse rewrite name [%s]: %w", rewrite, err)
|
||||
}
|
||||
}
|
||||
|
||||
// rename chart name in store
|
||||
s.OCI.LoadIndex()
|
||||
|
||||
oldRefContext := ref.Context()
|
||||
newRefContext := newRef.Context()
|
||||
|
||||
oldRepo := oldRefContext.RepositoryStr()
|
||||
newRepo := newRefContext.RepositoryStr()
|
||||
newTag := newRef.(name.Tag).TagStr()
|
||||
|
||||
oldTotal := oldRepo + ":" + oldTag
|
||||
newTotal := newRepo + ":" + newTag
|
||||
|
||||
found := false
|
||||
if err := s.OCI.Walk(func(k string, d ocispec.Descriptor) error {
|
||||
if d.Annotations[ocispec.AnnotationRefName] == oldTotal {
|
||||
d.Annotations[ocispec.AnnotationRefName] = newTotal
|
||||
found = true
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("could not find chart [%s] in store", ref.Name())
|
||||
}
|
||||
|
||||
if err := s.OCI.SaveIndex(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,40 +5,25 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
"hauler.dev/go/hauler/internal/flags"
|
||||
"hauler.dev/go/hauler/pkg/cosign"
|
||||
"hauler.dev/go/hauler/pkg/log"
|
||||
"hauler.dev/go/hauler/pkg/store"
|
||||
)
|
||||
|
||||
type CopyOpts struct {
|
||||
*RootOpts
|
||||
|
||||
Username string
|
||||
Password string
|
||||
Insecure bool
|
||||
PlainHTTP bool
|
||||
}
|
||||
|
||||
func (o *CopyOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
|
||||
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 CopyCmd(ctx context.Context, o *CopyOpts, s *store.Layout, targetRef string) error {
|
||||
func CopyCmd(ctx context.Context, o *flags.CopyOpts, s *store.Layout, targetRef string, ro *flags.CliRootOpts) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
if o.Username != "" || o.Password != "" {
|
||||
return fmt.Errorf("--username/--password have been deprecated, please use 'hauler login'")
|
||||
}
|
||||
|
||||
components := strings.SplitN(targetRef, "://", 2)
|
||||
switch components[0] {
|
||||
case "dir":
|
||||
l.Debugf("identified directory target reference")
|
||||
l.Debugf("identified directory target reference of [%s]", components[1])
|
||||
fs := content.NewFile(components[1])
|
||||
defer fs.Close()
|
||||
|
||||
@@ -48,22 +33,13 @@ func CopyCmd(ctx context.Context, o *CopyOpts, s *store.Layout, targetRef string
|
||||
}
|
||||
|
||||
case "registry":
|
||||
l.Debugf("identified registry target reference")
|
||||
l.Debugf("identified registry target reference of [%s]", components[1])
|
||||
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)
|
||||
err := cosign.LoadImages(ctx, s, components[1], o.Only, ropts, ro)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,32 +2,20 @@ package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/store"
|
||||
|
||||
"github.com/rancherfederal/hauler/internal/mapper"
|
||||
"github.com/rancherfederal/hauler/pkg/log"
|
||||
"github.com/rancherfederal/hauler/pkg/reference"
|
||||
"hauler.dev/go/hauler/internal/flags"
|
||||
"hauler.dev/go/hauler/internal/mapper"
|
||||
"hauler.dev/go/hauler/pkg/log"
|
||||
"hauler.dev/go/hauler/pkg/reference"
|
||||
"hauler.dev/go/hauler/pkg/store"
|
||||
)
|
||||
|
||||
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)")
|
||||
}
|
||||
|
||||
func ExtractCmd(ctx context.Context, o *ExtractOpts, s *store.Layout, ref string) error {
|
||||
func ExtractCmd(ctx context.Context, o *flags.ExtractOpts, s *store.Layout, ref string) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
r, err := reference.Parse(ref)
|
||||
@@ -35,10 +23,12 @@ func ExtractCmd(ctx context.Context, o *ExtractOpts, s *store.Layout, ref string
|
||||
return err
|
||||
}
|
||||
|
||||
// use the repository from the context and the identifier from the reference
|
||||
repo := r.Context().RepositoryStr() + ":" + r.Identifier()
|
||||
|
||||
found := false
|
||||
if err := s.Walk(func(reference string, desc ocispec.Descriptor) error {
|
||||
|
||||
if !strings.Contains(reference, r.Name()) {
|
||||
if !strings.Contains(reference, repo) {
|
||||
return nil
|
||||
}
|
||||
found = true
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -4,38 +4,21 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
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"
|
||||
"hauler.dev/go/hauler/internal/flags"
|
||||
"hauler.dev/go/hauler/pkg/consts"
|
||||
"hauler.dev/go/hauler/pkg/reference"
|
||||
"hauler.dev/go/hauler/pkg/store"
|
||||
)
|
||||
|
||||
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 {
|
||||
func InfoCmd(ctx context.Context, o *flags.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 {
|
||||
@@ -47,7 +30,7 @@ func InfoCmd(ctx context.Context, o *InfoOpts, s *store.Layout) error {
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
// handle multi-arch images
|
||||
// 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 {
|
||||
@@ -66,33 +49,42 @@ func InfoCmd(ctx context.Context, o *InfoOpts, s *store.Layout) error {
|
||||
return err
|
||||
}
|
||||
|
||||
i := newItem(s, desc, internalManifest, fmt.Sprintf("%s/%s", internalDesc.Platform.OS, internalDesc.Platform.Architecture), o)
|
||||
i := newItemWithDigest(
|
||||
s,
|
||||
internalDesc.Digest.String(),
|
||||
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
|
||||
|
||||
// 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
|
||||
// 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)
|
||||
i := newItem(s, desc, m,
|
||||
fmt.Sprintf("%s/%s", internalManifest.OS, internalManifest.Architecture), o)
|
||||
var emptyItem item
|
||||
if i != emptyItem {
|
||||
items = append(items, i)
|
||||
@@ -104,8 +96,9 @@ func InfoCmd(ctx context.Context, o *InfoOpts, s *store.Layout) error {
|
||||
items = append(items, i)
|
||||
}
|
||||
}
|
||||
// handle the rest
|
||||
} else {
|
||||
|
||||
// handle everything else (charts, files, sigs, etc.)
|
||||
} else {
|
||||
var m ocispec.Manifest
|
||||
if err := json.NewDecoder(rc).Decode(&m); err != nil {
|
||||
return err
|
||||
@@ -123,6 +116,11 @@ func InfoCmd(ctx context.Context, o *InfoOpts, s *store.Layout) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if o.ListRepos {
|
||||
buildListRepos(items...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// sort items by ref and arch
|
||||
sort.Sort(byReferenceAndArch(items))
|
||||
|
||||
@@ -132,36 +130,111 @@ func InfoCmd(ctx context.Context, o *InfoOpts, s *store.Layout) error {
|
||||
msg = buildJson(items...)
|
||||
fmt.Println(msg)
|
||||
default:
|
||||
buildTable(items...)
|
||||
if err := buildTable(o.ShowDigests, items...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
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)
|
||||
func buildListRepos(items ...item) {
|
||||
// create map to track unique repository names
|
||||
repos := make(map[string]bool)
|
||||
|
||||
for _, i := range items {
|
||||
if i.Type != "" {
|
||||
row := []string{
|
||||
i.Reference,
|
||||
repoName := ""
|
||||
for j := 0; j < len(i.Reference); j++ {
|
||||
if i.Reference[j] == '/' {
|
||||
repoName = i.Reference[:j]
|
||||
break
|
||||
}
|
||||
}
|
||||
if repoName == "" {
|
||||
repoName = i.Reference
|
||||
}
|
||||
repos[repoName] = true
|
||||
}
|
||||
|
||||
// collect and print unique repository names
|
||||
for repoName := range repos {
|
||||
fmt.Println(repoName)
|
||||
}
|
||||
}
|
||||
|
||||
func buildTable(showDigests bool, items ...item) error {
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Configure(func(cfg *tablewriter.Config) {
|
||||
cfg.Header.Alignment.Global = tw.AlignLeft
|
||||
cfg.Row.Merging.Mode = tw.MergeVertical
|
||||
cfg.Row.Merging.ByColumnIndex = tw.NewBoolMapper(0)
|
||||
})
|
||||
|
||||
if showDigests {
|
||||
table.Header("Reference", "Type", "Platform", "Digest", "# Layers", "Size")
|
||||
} else {
|
||||
table.Header("Reference", "Type", "Platform", "# Layers", "Size")
|
||||
}
|
||||
|
||||
totalSize := int64(0)
|
||||
|
||||
for _, i := range items {
|
||||
if i.Type == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
ref := truncateReference(i.Reference)
|
||||
var row []string
|
||||
|
||||
if showDigests {
|
||||
digest := i.Digest
|
||||
if digest == "" {
|
||||
digest = "-"
|
||||
}
|
||||
row = []string{
|
||||
ref,
|
||||
i.Type,
|
||||
i.Platform,
|
||||
digest,
|
||||
fmt.Sprintf("%d", i.Layers),
|
||||
byteCountSI(i.Size),
|
||||
}
|
||||
} else {
|
||||
row = []string{
|
||||
ref,
|
||||
i.Type,
|
||||
i.Platform,
|
||||
fmt.Sprintf("%d", i.Layers),
|
||||
byteCountSI(i.Size),
|
||||
}
|
||||
totalSize += i.Size
|
||||
table.Append(row)
|
||||
}
|
||||
|
||||
totalSize += i.Size
|
||||
if err := table.Append(row); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
table.SetFooter([]string{"", "", "", "Total", byteCountSI(totalSize)})
|
||||
|
||||
table.Render()
|
||||
// align total column based on digest visibility
|
||||
if showDigests {
|
||||
table.Footer("", "", "", "", "Total", byteCountSI(totalSize))
|
||||
} else {
|
||||
table.Footer("", "", "", "Total", byteCountSI(totalSize))
|
||||
}
|
||||
|
||||
return table.Render()
|
||||
}
|
||||
|
||||
// truncateReference shortens the digest of a reference
|
||||
func truncateReference(ref string) string {
|
||||
const prefix = "@sha256:"
|
||||
idx := strings.Index(ref, prefix)
|
||||
if idx == -1 {
|
||||
return ref
|
||||
}
|
||||
if len(ref) > idx+len(prefix)+12 {
|
||||
return ref[:idx+len(prefix)+12] + "…"
|
||||
}
|
||||
return ref
|
||||
}
|
||||
|
||||
func buildJson(item ...item) string {
|
||||
@@ -173,11 +246,12 @@ func buildJson(item ...item) string {
|
||||
}
|
||||
|
||||
type item struct {
|
||||
Reference string
|
||||
Type string
|
||||
Platform string
|
||||
Layers int
|
||||
Size int64
|
||||
Reference string
|
||||
Type string
|
||||
Platform string
|
||||
Digest string
|
||||
Layers int
|
||||
Size int64
|
||||
}
|
||||
|
||||
type byReferenceAndArch []item
|
||||
@@ -200,7 +274,14 @@ func (a byReferenceAndArch) Less(i, j int) bool {
|
||||
return a[i].Reference < a[j].Reference
|
||||
}
|
||||
|
||||
func newItem(s *store.Layout, desc ocispec.Descriptor, m ocispec.Manifest, plat string, o *InfoOpts) item {
|
||||
// overrides the digest with a specific per platform digest
|
||||
func newItemWithDigest(s *store.Layout, digestStr string, desc ocispec.Descriptor, m ocispec.Manifest, plat string, o *flags.InfoOpts) item {
|
||||
item := newItem(s, desc, m, plat, o)
|
||||
item.Digest = digestStr
|
||||
return item
|
||||
}
|
||||
|
||||
func newItem(s *store.Layout, desc ocispec.Descriptor, m ocispec.Manifest, plat string, o *flags.InfoOpts) item {
|
||||
var size int64 = 0
|
||||
for _, l := range m.Layers {
|
||||
size += l.Size
|
||||
@@ -227,8 +308,12 @@ func newItem(s *store.Layout, desc ocispec.Descriptor, m ocispec.Manifest, plat
|
||||
case "dev.cosignproject.cosign/sboms":
|
||||
ctype = "sbom"
|
||||
}
|
||||
|
||||
ref, err := reference.Parse(desc.Annotations[ocispec.AnnotationRefName])
|
||||
|
||||
refName := desc.Annotations["io.containerd.image.name"]
|
||||
if refName == "" {
|
||||
refName = desc.Annotations[ocispec.AnnotationRefName]
|
||||
}
|
||||
ref, err := reference.Parse(refName)
|
||||
if err != nil {
|
||||
return item{}
|
||||
}
|
||||
@@ -238,11 +323,12 @@ func newItem(s *store.Layout, desc ocispec.Descriptor, m ocispec.Manifest, plat
|
||||
}
|
||||
|
||||
return item{
|
||||
Reference: ref.Name(),
|
||||
Type: ctype,
|
||||
Platform: plat,
|
||||
Layers: len(m.Layers),
|
||||
Size: size,
|
||||
Reference: ref.Name(),
|
||||
Type: ctype,
|
||||
Platform: plat,
|
||||
Digest: desc.Digest.String(),
|
||||
Layers: len(m.Layers),
|
||||
Size: size,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,60 +2,130 @@ package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/archiver/v3"
|
||||
"github.com/rancherfederal/hauler/pkg/content"
|
||||
"github.com/rancherfederal/hauler/pkg/store"
|
||||
"github.com/spf13/cobra"
|
||||
"hauler.dev/go/hauler/internal/flags"
|
||||
"hauler.dev/go/hauler/pkg/archives"
|
||||
"hauler.dev/go/hauler/pkg/consts"
|
||||
"hauler.dev/go/hauler/pkg/content"
|
||||
"hauler.dev/go/hauler/pkg/getter"
|
||||
"hauler.dev/go/hauler/pkg/log"
|
||||
"hauler.dev/go/hauler/pkg/store"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/log"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
type LoadOpts struct {
|
||||
*RootOpts
|
||||
TempOverride 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.")
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// extracts the contents of an archived oci layout to an existing oci layout
|
||||
func LoadCmd(ctx context.Context, o *flags.LoadOpts, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
for _, archiveRef := range archiveRefs {
|
||||
l.Infof("loading content from [%s] to [%s]", archiveRef, o.StoreDir)
|
||||
err := unarchiveLayoutTo(ctx, archiveRef, o.StoreDir, o.TempOverride)
|
||||
tempOverride := rso.TempOverride
|
||||
|
||||
if tempOverride == "" {
|
||||
tempOverride = os.Getenv(consts.HaulerTempDir)
|
||||
}
|
||||
|
||||
tempDir, err := os.MkdirTemp(tempOverride, consts.DefaultHaulerTempDirName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
l.Debugf("using temporary directory at [%s]", tempDir)
|
||||
|
||||
for _, fileName := range o.FileName {
|
||||
l.Infof("loading haul [%s] to [%s]", fileName, o.StoreDir)
|
||||
err := unarchiveLayoutTo(ctx, fileName, o.StoreDir, tempDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clearDir(tempDir)
|
||||
}
|
||||
|
||||
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")
|
||||
// accepts an archived OCI layout, extracts the contents to an existing OCI layout, and preserves the index
|
||||
func unarchiveLayoutTo(ctx context.Context, haulPath string, dest string, tempDir string) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
if strings.HasPrefix(haulPath, "http://") || strings.HasPrefix(haulPath, "https://") {
|
||||
l.Debugf("detected remote archive... starting download... [%s]", haulPath)
|
||||
|
||||
h := getter.NewHttp()
|
||||
parsedURL, err := url.Parse(haulPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rc, err := h.Open(ctx, parsedURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
fileName := h.Name(parsedURL)
|
||||
if fileName == "" {
|
||||
fileName = filepath.Base(parsedURL.Path)
|
||||
}
|
||||
haulPath = filepath.Join(tempDir, fileName)
|
||||
|
||||
out, err := os.Create(haulPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
if _, err = io.Copy(out, rc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := archives.Unarchive(ctx, haulPath, tempDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ensure the incoming index.json has the correct annotations.
|
||||
data, err := os.ReadFile(tempDir + "/index.json")
|
||||
if err != nil {
|
||||
return (err)
|
||||
}
|
||||
|
||||
var idx ocispec.Index
|
||||
if err := json.Unmarshal(data, &idx); err != nil {
|
||||
return (err)
|
||||
}
|
||||
|
||||
for i := range idx.Manifests {
|
||||
if idx.Manifests[i].Annotations == nil {
|
||||
idx.Manifests[i].Annotations = make(map[string]string)
|
||||
}
|
||||
if _, exists := idx.Manifests[i].Annotations[consts.KindAnnotationName]; !exists {
|
||||
idx.Manifests[i].Annotations[consts.KindAnnotationName] = consts.KindAnnotationImage
|
||||
}
|
||||
if ref, ok := idx.Manifests[i].Annotations[consts.ContainerdImageNameKey]; ok {
|
||||
if slash := strings.Index(ref, "/"); slash != -1 {
|
||||
ref = ref[slash+1:]
|
||||
}
|
||||
if idx.Manifests[i].Annotations[consts.ImageRefKey] != ref {
|
||||
idx.Manifests[i].Annotations[consts.ImageRefKey] = ref
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out, err := json.MarshalIndent(idx, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
if err := archiver.Unarchive(archivePath, tmpdir); err != nil {
|
||||
if err := os.WriteFile(tempDir+"/index.json", out, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s, err := store.NewLayout(tmpdir)
|
||||
s, err := store.NewLayout(tempDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -68,3 +138,19 @@ func unarchiveLayoutTo(ctx context.Context, archivePath string, dest string, tem
|
||||
_, err = s.CopyAll(ctx, ts, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func clearDir(path string) error {
|
||||
entries, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
err = os.RemoveAll(filepath.Join(path, entry.Name()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
122
cmd/hauler/cli/store/remove.go
Normal file
122
cmd/hauler/cli/store/remove.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
"hauler.dev/go/hauler/internal/flags"
|
||||
"hauler.dev/go/hauler/pkg/log"
|
||||
"hauler.dev/go/hauler/pkg/store"
|
||||
)
|
||||
|
||||
func formatReference(ref string) string {
|
||||
tagIdx := strings.LastIndex(ref, ":")
|
||||
if tagIdx == -1 {
|
||||
return ref
|
||||
}
|
||||
|
||||
dashIdx := strings.Index(ref[tagIdx+1:], "-")
|
||||
if dashIdx == -1 {
|
||||
return ref
|
||||
}
|
||||
|
||||
dashIdx = tagIdx + 1 + dashIdx
|
||||
|
||||
base := ref[:dashIdx]
|
||||
suffix := ref[dashIdx+1:]
|
||||
|
||||
if base == "" || suffix == "" {
|
||||
return ref
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s [%s]", base, suffix)
|
||||
}
|
||||
|
||||
func RemoveCmd(ctx context.Context, o *flags.RemoveOpts, s *store.Layout, ref string) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
// collect matching artifacts
|
||||
type match struct {
|
||||
reference string
|
||||
desc ocispec.Descriptor
|
||||
}
|
||||
var matches []match
|
||||
|
||||
if err := s.Walk(func(reference string, desc ocispec.Descriptor) error {
|
||||
if !strings.Contains(reference, ref) {
|
||||
return nil
|
||||
}
|
||||
|
||||
matches = append(matches, match{
|
||||
reference: reference,
|
||||
desc: desc,
|
||||
})
|
||||
|
||||
return nil // continue walking
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(matches) == 0 {
|
||||
return fmt.Errorf("reference [%s] not found in store (use `hauler store info` to list store contents)", ref)
|
||||
}
|
||||
|
||||
if len(matches) >= 1 {
|
||||
l.Infof("found %d matching references:", len(matches))
|
||||
for _, m := range matches {
|
||||
l.Infof(" - [%s]", formatReference(m.reference))
|
||||
}
|
||||
}
|
||||
|
||||
if !o.Force {
|
||||
fmt.Printf(" ↳ are you sure you want to remove [%d] artifact(s) from the store? (yes/no) ", len(matches))
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return fmt.Errorf("failed to read response: [%w]... please answer 'yes' or 'no'", err)
|
||||
}
|
||||
|
||||
response := strings.ToLower(strings.TrimSpace(line))
|
||||
|
||||
switch response {
|
||||
case "yes", "y":
|
||||
l.Infof("starting to remove artifacts from store...")
|
||||
case "no", "n":
|
||||
l.Infof("successfully cancelled removal of artifacts from store")
|
||||
return nil
|
||||
case "":
|
||||
return fmt.Errorf("failed to read response... please answer 'yes' or 'no'")
|
||||
default:
|
||||
return fmt.Errorf("invalid response [%s]... please answer 'yes' or 'no'", response)
|
||||
}
|
||||
}
|
||||
|
||||
// remove artifact(s)
|
||||
for _, m := range matches {
|
||||
if err := s.RemoveArtifact(ctx, m.reference, m.desc); err != nil {
|
||||
return fmt.Errorf("failed to remove artifact [%s]: %w", formatReference(m.reference), err)
|
||||
}
|
||||
|
||||
l.Infof("successfully removed [%s] of type [%s] with digest [%s]", formatReference(m.reference), m.desc.MediaType, m.desc.Digest.String())
|
||||
}
|
||||
|
||||
// clean up unreferenced blobs
|
||||
l.Infof("cleaning up all unreferenced blobs...")
|
||||
removedCount, removedSize, err := s.CleanUp(ctx)
|
||||
if err != nil {
|
||||
l.Warnf("garbage collection failed: [%v]", err)
|
||||
} else if removedCount > 0 {
|
||||
l.Infof("successfully removed [%d] unreferenced blobs [freed %d bytes]", removedCount, removedSize)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,37 +1,41 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"github.com/mholt/archiver/v3"
|
||||
"github.com/spf13/cobra"
|
||||
referencev3 "github.com/distribution/distribution/v3/reference"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
libv1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||
"github.com/google/go-containerregistry/pkg/v1/tarball"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/log"
|
||||
"hauler.dev/go/hauler/internal/flags"
|
||||
"hauler.dev/go/hauler/pkg/archives"
|
||||
"hauler.dev/go/hauler/pkg/consts"
|
||||
"hauler.dev/go/hauler/pkg/log"
|
||||
)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// saves a content store to store archives
|
||||
func SaveCmd(ctx context.Context, o *flags.SaveOpts, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
// TODO: Support more formats?
|
||||
a := archiver.NewTarZstd()
|
||||
a.OverwriteExisting = true
|
||||
// maps to handle compression and archival types
|
||||
compressionMap := archives.CompressionMap
|
||||
archivalMap := archives.ArchivalMap
|
||||
|
||||
absOutputfile, err := filepath.Abs(outputFile)
|
||||
// select the compression and archival type based parsed filename extension
|
||||
compression := compressionMap["zst"]
|
||||
archival := archivalMap["tar"]
|
||||
|
||||
absOutputfile, err := filepath.Abs(o.FileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -45,11 +49,207 @@ func SaveCmd(ctx context.Context, o *SaveOpts, outputFile string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = a.Archive([]string{"."}, absOutputfile)
|
||||
// create the manifest.json file
|
||||
if err := writeExportsManifest(ctx, ".", o.Platform); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// strip out the oci-layout file from the haul
|
||||
// required for containerd to be able to interpret the haul correctly for all mediatypes and artifactypes
|
||||
if o.ContainerdCompatibility {
|
||||
if err := os.Remove(filepath.Join(".", ocispec.ImageLayoutFile)); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
l.Warnf("compatibility warning... containerd... removing 'oci-layout' file to support containerd importing of images")
|
||||
}
|
||||
}
|
||||
|
||||
// create the archive
|
||||
err = archives.Archive(ctx, ".", absOutputfile, compression, archival)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Infof("saved store [%s] -> [%s]", o.StoreDir, absOutputfile)
|
||||
l.Infof("saving store [%s] to archive [%s]", o.StoreDir, o.FileName)
|
||||
return nil
|
||||
}
|
||||
|
||||
type exports struct {
|
||||
digests []string
|
||||
records map[string]tarball.Descriptor
|
||||
}
|
||||
|
||||
func writeExportsManifest(ctx context.Context, dir string, platformStr string) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
// validate platform format
|
||||
platform, err := libv1.ParsePlatform(platformStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oci, err := layout.FromPath(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
idx, err := oci.ImageIndex()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imx, err := idx.IndexManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
x := &exports{
|
||||
digests: []string{},
|
||||
records: map[string]tarball.Descriptor{},
|
||||
}
|
||||
|
||||
for _, desc := range imx.Manifests {
|
||||
l.Debugf("descriptor [%s] = [%s]", desc.Digest.String(), desc.MediaType)
|
||||
if artifactType := types.MediaType(desc.ArtifactType); artifactType != "" && !artifactType.IsImage() && !artifactType.IsIndex() {
|
||||
l.Debugf("descriptor [%s] <<< SKIPPING ARTIFACT [%q]", desc.Digest.String(), desc.ArtifactType)
|
||||
continue
|
||||
}
|
||||
if desc.Annotations != nil {
|
||||
// we only care about images that cosign has added to the layout index
|
||||
if kind, hasKind := desc.Annotations[consts.KindAnnotationName]; hasKind {
|
||||
if refName, hasRefName := desc.Annotations["io.containerd.image.name"]; hasRefName {
|
||||
// branch on image (aka image manifest) or image index
|
||||
switch kind {
|
||||
case consts.KindAnnotationImage:
|
||||
if err := x.record(ctx, idx, desc, refName); err != nil {
|
||||
return err
|
||||
}
|
||||
case consts.KindAnnotationIndex:
|
||||
l.Debugf("index [%s]: digest=[%s]... type=[%s]... size=[%d]", refName, desc.Digest.String(), desc.MediaType, desc.Size)
|
||||
|
||||
// when no platform is inputted... warn the user of potential mismatch on import for docker
|
||||
// required for docker to be able to interpret and load the image correctly
|
||||
if platform.String() == "" {
|
||||
l.Warnf("compatibility warning... docker... specify platform to prevent potential mismatch on import of index [%s]", refName)
|
||||
}
|
||||
|
||||
iix, err := idx.ImageIndex(desc.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ixm, err := iix.IndexManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ixd := range ixm.Manifests {
|
||||
if ixd.MediaType.IsImage() {
|
||||
if platform.String() != "" {
|
||||
if ixd.Platform.Architecture != platform.Architecture || ixd.Platform.OS != platform.OS {
|
||||
l.Debugf("index [%s]: digest=[%s], platform=[%s/%s]: does not match the supplied platform... skipping...", refName, desc.Digest.String(), ixd.Platform.OS, ixd.Platform.Architecture)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// skip any platforms of 'unknown/unknown'... docker hates
|
||||
// required for docker to be able to interpret and load the image correctly
|
||||
if ixd.Platform.Architecture == "unknown" && ixd.Platform.OS == "unknown" {
|
||||
l.Debugf("index [%s]: digest=[%s], platform=[%s/%s]: matches unknown platform... skipping...", refName, desc.Digest.String(), ixd.Platform.OS, ixd.Platform.Architecture)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := x.record(ctx, iix, ixd, refName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
l.Debugf("descriptor [%s] <<< SKIPPING KIND [%q]", desc.Digest.String(), kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
mnf := x.describe()
|
||||
err = json.NewEncoder(&buf).Encode(mnf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return oci.WriteFile(consts.ImageManifestFile, buf.Bytes(), 0666)
|
||||
}
|
||||
|
||||
func (x *exports) describe() tarball.Manifest {
|
||||
m := make(tarball.Manifest, len(x.digests))
|
||||
for i, d := range x.digests {
|
||||
m[i] = x.records[d]
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (x *exports) record(ctx context.Context, index libv1.ImageIndex, desc libv1.Descriptor, refname string) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
digest := desc.Digest.String()
|
||||
image, err := index.Image(desc.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config, err := image.ConfigName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
xd, recorded := x.records[digest]
|
||||
if !recorded {
|
||||
// record one export record per digest
|
||||
x.digests = append(x.digests, digest)
|
||||
xd = tarball.Descriptor{
|
||||
Config: path.Join(ocispec.ImageBlobsDir, config.Algorithm, config.Hex),
|
||||
RepoTags: []string{},
|
||||
Layers: []string{},
|
||||
}
|
||||
|
||||
layers, err := image.Layers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, layer := range layers {
|
||||
xl, err := layer.Digest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
xd.Layers = append(xd.Layers[:], path.Join(ocispec.ImageBlobsDir, xl.Algorithm, xl.Hex))
|
||||
}
|
||||
}
|
||||
|
||||
ref, err := name.ParseReference(refname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// record tags for the digest, eliminating dupes
|
||||
switch tag := ref.(type) {
|
||||
case name.Tag:
|
||||
named, err := referencev3.ParseNormalizedNamed(refname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
named = referencev3.TagNameOnly(named)
|
||||
repotag := referencev3.FamiliarString(named)
|
||||
xd.RepoTags = append(xd.RepoTags[:], repotag)
|
||||
slices.Sort(xd.RepoTags)
|
||||
xd.RepoTags = slices.Compact(xd.RepoTags)
|
||||
ref = tag.Digest(digest)
|
||||
}
|
||||
|
||||
l.Debugf("image [%s]: type=%s, size=%d", ref.Name(), desc.MediaType, desc.Size)
|
||||
// record export descriptor for the digest
|
||||
x.records[digest] = xd
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,9 +2,13 @@ package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/distribution/distribution/v3/configuration"
|
||||
dcontext "github.com/distribution/distribution/v3/context"
|
||||
@@ -12,49 +16,94 @@ import (
|
||||
_ "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/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/store"
|
||||
|
||||
"github.com/rancherfederal/hauler/internal/server"
|
||||
"github.com/rancherfederal/hauler/pkg/log"
|
||||
"hauler.dev/go/hauler/internal/flags"
|
||||
"hauler.dev/go/hauler/internal/server"
|
||||
"hauler.dev/go/hauler/pkg/log"
|
||||
"hauler.dev/go/hauler/pkg/store"
|
||||
)
|
||||
|
||||
type ServeRegistryOpts struct {
|
||||
*RootOpts
|
||||
func validateStoreExists(s *store.Layout) error {
|
||||
indexPath := filepath.Join(s.Root, "index.json")
|
||||
|
||||
Port int
|
||||
RootDir string
|
||||
ConfigFile string
|
||||
_, err := os.Stat(indexPath)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
storedir string
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return fmt.Errorf(
|
||||
"no store found at [%s]\n ↳ does the hauler store exist? (verify with `hauler store info`)",
|
||||
s.Root,
|
||||
)
|
||||
}
|
||||
|
||||
return fmt.Errorf(
|
||||
"unable to access store at [%s]: %w",
|
||||
s.Root,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
func (o *ServeRegistryOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
func loadConfig(filename string) (*configuration.Configuration, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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.StringVarP(&o.ConfigFile, "config", "c", "", "Path to a config file, will override all other configs")
|
||||
return configuration.Parse(f)
|
||||
}
|
||||
|
||||
func ServeRegistryCmd(ctx context.Context, o *ServeRegistryOpts, s *store.Layout) error {
|
||||
func DefaultRegistryConfig(o *flags.ServeRegistryOpts, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *configuration.Configuration {
|
||||
cfg := &configuration.Configuration{
|
||||
Version: "0.1",
|
||||
Storage: configuration.Storage{
|
||||
"cache": configuration.Parameters{"blobdescriptor": "inmemory"},
|
||||
"filesystem": configuration.Parameters{"rootdirectory": o.RootDir},
|
||||
"maintenance": configuration.Parameters{
|
||||
"readonly": map[any]any{"enabled": o.ReadOnly},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if o.TLSCert != "" && o.TLSKey != "" {
|
||||
cfg.HTTP.TLS.Certificate = o.TLSCert
|
||||
cfg.HTTP.TLS.Key = o.TLSKey
|
||||
}
|
||||
|
||||
cfg.HTTP.Addr = fmt.Sprintf(":%d", o.Port)
|
||||
cfg.HTTP.Headers = http.Header{
|
||||
"X-Content-Type-Options": []string{"nosniff"},
|
||||
}
|
||||
|
||||
cfg.Log.Level = configuration.Loglevel(ro.LogLevel)
|
||||
cfg.Validation.Manifests.URLs.Allow = []string{".+"}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func ServeRegistryCmd(ctx context.Context, o *flags.ServeRegistryOpts, s *store.Layout, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error {
|
||||
l := log.FromContext(ctx)
|
||||
ctx = dcontext.WithVersion(ctx, version.Version)
|
||||
|
||||
if err := validateStoreExists(s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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 {
|
||||
opts := &flags.CopyOpts{}
|
||||
if err := CopyCmd(ctx, opts, s, "registry://"+tr.Registry(), ro); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tr.Close()
|
||||
|
||||
cfg := o.defaultRegistryConfig()
|
||||
cfg := DefaultRegistryConfig(o, rso, ro)
|
||||
if o.ConfigFile != "" {
|
||||
ucfg, err := loadConfig(o.ConfigFile)
|
||||
if err != nil {
|
||||
@@ -64,6 +113,16 @@ func ServeRegistryCmd(ctx context.Context, o *ServeRegistryOpts, s *store.Layout
|
||||
}
|
||||
|
||||
l.Infof("starting registry on port [%d]", o.Port)
|
||||
|
||||
yamlConfig, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
l.Errorf("failed to validate/output registry configuration: %v", err)
|
||||
} else {
|
||||
l.Infof("using registry configuration... \n%s", strings.TrimSpace(string(yamlConfig)))
|
||||
}
|
||||
|
||||
l.Debugf("detailed registry configuration: %+v", cfg)
|
||||
|
||||
r, err := server.NewRegistry(ctx, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -76,78 +135,35 @@ 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 {
|
||||
func ServeFilesCmd(ctx context.Context, o *flags.ServeFilesOpts, s *store.Layout, ro *flags.CliRootOpts) error {
|
||||
l := log.FromContext(ctx)
|
||||
ctx = dcontext.WithVersion(ctx, version.Version)
|
||||
|
||||
opts := &CopyOpts{}
|
||||
if err := CopyCmd(ctx, opts, s, "dir://"+o.RootDir); err != nil {
|
||||
if err := validateStoreExists(s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg := server.FileConfig{
|
||||
Root: o.RootDir,
|
||||
Port: o.Port,
|
||||
opts := &flags.CopyOpts{}
|
||||
if err := CopyCmd(ctx, opts, s, "dir://"+o.RootDir, ro); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := server.NewFile(ctx, cfg)
|
||||
f, err := server.NewFile(ctx, *o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Infof("starting file server on port [%d]", o.Port)
|
||||
if err := f.ListenAndServe(); err != nil {
|
||||
return err
|
||||
if o.TLSCert != "" && o.TLSKey != "" {
|
||||
l.Infof("starting file server with tls on port [%d]", o.Port)
|
||||
if err := f.ListenAndServeTLS(o.TLSCert, o.TLSKey); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return configuration.Parse(f)
|
||||
}
|
||||
|
||||
func (o *ServeRegistryOpts) defaultRegistryConfig() *configuration.Configuration {
|
||||
cfg := &configuration.Configuration{
|
||||
Version: "0.1",
|
||||
Storage: configuration.Storage{
|
||||
"cache": configuration.Parameters{"blobdescriptor": "inmemory"},
|
||||
"filesystem": configuration.Parameters{"rootdirectory": o.RootDir},
|
||||
|
||||
// TODO: Ensure this is toggleable via cli arg if necessary
|
||||
// "maintenance": configuration.Parameters{"readonly.enabled": false},
|
||||
},
|
||||
}
|
||||
|
||||
// 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{
|
||||
"X-Content-Type-Options": []string{"nosniff"},
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
@@ -5,54 +5,51 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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"
|
||||
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"
|
||||
"hauler.dev/go/hauler/internal/flags"
|
||||
convert "hauler.dev/go/hauler/pkg/apis/hauler.cattle.io/convert"
|
||||
v1 "hauler.dev/go/hauler/pkg/apis/hauler.cattle.io/v1"
|
||||
v1alpha1 "hauler.dev/go/hauler/pkg/apis/hauler.cattle.io/v1alpha1"
|
||||
tchart "hauler.dev/go/hauler/pkg/collection/chart"
|
||||
"hauler.dev/go/hauler/pkg/collection/imagetxt"
|
||||
"hauler.dev/go/hauler/pkg/consts"
|
||||
"hauler.dev/go/hauler/pkg/content"
|
||||
"hauler.dev/go/hauler/pkg/cosign"
|
||||
"hauler.dev/go/hauler/pkg/getter"
|
||||
"hauler.dev/go/hauler/pkg/log"
|
||||
"hauler.dev/go/hauler/pkg/reference"
|
||||
"hauler.dev/go/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 *flags.SyncOpts, s *store.Layout, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) 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, "=")
|
||||
tempOverride := rso.TempOverride
|
||||
|
||||
if tempOverride == "" {
|
||||
tempOverride = os.Getenv(consts.HaulerTempDir)
|
||||
}
|
||||
|
||||
tempDir, err := os.MkdirTemp(tempOverride, consts.DefaultHaulerTempDirName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
l.Debugf("using temporary directory at [%s]", tempDir)
|
||||
|
||||
// if passed products, check for a remote manifest to retrieve and use
|
||||
for _, productName := range o.Products {
|
||||
l.Infof("processing product manifest for [%s] to store [%s]", productName, o.StoreDir)
|
||||
parts := strings.Split(productName, "=")
|
||||
tag := strings.ReplaceAll(parts[1], "+", "-")
|
||||
|
||||
ProductRegistry := o.ProductRegistry // cli flag
|
||||
@@ -62,47 +59,85 @@ func SyncCmd(ctx context.Context, o *SyncOpts, s *store.Layout) error {
|
||||
}
|
||||
|
||||
manifestLoc := fmt.Sprintf("%s/hauler/%s-manifest.yaml:%s", ProductRegistry, parts[0], tag)
|
||||
l.Infof("retrieving product manifest from: '%s'", manifestLoc)
|
||||
img := v1alpha1.Image{
|
||||
l.Infof("fetching product manifest from [%s]", manifestLoc)
|
||||
img := v1.Image{
|
||||
Name: manifestLoc,
|
||||
}
|
||||
err := storeImage(ctx, s, img, o.Platform)
|
||||
err := storeImage(ctx, s, img, o.Platform, rso, ro, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ExtractCmd(ctx, &ExtractOpts{RootOpts: o.RootOpts}, s, fmt.Sprintf("hauler/%s-manifest.yaml:%s", parts[0],tag))
|
||||
err = ExtractCmd(ctx, &flags.ExtractOpts{StoreRootOpts: o.StoreRootOpts}, s, fmt.Sprintf("hauler/%s-manifest.yaml:%s", parts[0], tag))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filename := fmt.Sprintf("%s-manifest.yaml", parts[0])
|
||||
fileName := fmt.Sprintf("%s-manifest.yaml", parts[0])
|
||||
|
||||
fi, err := os.Open(filename)
|
||||
fi, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = processContent(ctx, fi, o, s)
|
||||
err = processContent(ctx, fi, o, s, rso, ro)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.Infof("processing completed successfully")
|
||||
}
|
||||
|
||||
// if passed a local manifest, process it
|
||||
for _, filename := range o.ContentFiles {
|
||||
l.Debugf("processing content file: '%s'", filename)
|
||||
fi, err := os.Open(filename)
|
||||
// If passed a local manifest, process it
|
||||
for _, fileName := range o.FileName {
|
||||
l.Infof("processing manifest [%s] to store [%s]", fileName, o.StoreDir)
|
||||
|
||||
haulPath := fileName
|
||||
if strings.HasPrefix(haulPath, "http://") || strings.HasPrefix(haulPath, "https://") {
|
||||
l.Debugf("detected remote manifest... starting download... [%s]", haulPath)
|
||||
|
||||
h := getter.NewHttp()
|
||||
parsedURL, err := url.Parse(haulPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rc, err := h.Open(ctx, parsedURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
fileName := h.Name(parsedURL)
|
||||
if fileName == "" {
|
||||
fileName = filepath.Base(parsedURL.Path)
|
||||
}
|
||||
haulPath = filepath.Join(tempDir, fileName)
|
||||
|
||||
out, err := os.Create(haulPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
if _, err = io.Copy(out, rc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fi, err := os.Open(haulPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = processContent(ctx, fi, o, s)
|
||||
defer fi.Close()
|
||||
|
||||
err = processContent(ctx, fi, o, s, rso, ro)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Infof("processing completed successfully")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func processContent(ctx context.Context, fi *os.File, o *SyncOpts, s *store.Layout) error {
|
||||
func processContent(ctx context.Context, fi *os.File, o *flags.SyncOpts, s *store.Layout, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
reader := yaml.NewYAMLReader(bufio.NewReader(fi))
|
||||
@@ -116,179 +151,510 @@ func processContent(ctx context.Context, fi *os.File, o *SyncOpts, s *store.Layo
|
||||
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")
|
||||
l.Warnf("skipping syncing due to %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
l.Infof("syncing [%s] to store", obj.GroupVersionKind().String())
|
||||
gvk := obj.GroupVersionKind()
|
||||
l.Infof("syncing content [%s] with [kind=%s] to store [%s]", gvk.GroupVersion(), gvk.Kind, o.StoreDir)
|
||||
|
||||
// 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
|
||||
}
|
||||
switch gvk.Kind {
|
||||
|
||||
for _, f := range cfg.Spec.Files {
|
||||
err := storeFile(ctx, s, f)
|
||||
if err != nil {
|
||||
case consts.FilesContentKind:
|
||||
switch gvk.Version {
|
||||
case "v1alpha1":
|
||||
l.Warnf("!!! DEPRECATION WARNING !!! apiVersion [%s] will be removed in a future release...", gvk.Version)
|
||||
|
||||
var alphaCfg v1alpha1.Files
|
||||
if err := yaml.Unmarshal(doc, &alphaCfg); 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]
|
||||
var v1Cfg v1.Files
|
||||
if err := convert.ConvertFiles(&alphaCfg, &v1Cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range v1Cfg.Spec.Files {
|
||||
if err := storeFile(ctx, s, f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
case "v1":
|
||||
var cfg v1.Files
|
||||
if err := yaml.Unmarshal(doc, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range cfg.Spec.Files {
|
||||
if err := storeFile(ctx, s, f); err != nil {
|
||||
return err
|
||||
}
|
||||
// the individual image key trumps all
|
||||
if i.Key != "" {
|
||||
key, err = homedir.Expand(i.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported version [%s] for kind [%s]... valid versions are [v1 and v1alpha1]", gvk.Version, gvk.Kind)
|
||||
}
|
||||
|
||||
case consts.ImagesContentKind:
|
||||
switch gvk.Version {
|
||||
case "v1alpha1":
|
||||
l.Warnf("!!! DEPRECATION WARNING !!! apiVersion [%s] will be removed in a future release...", gvk.Version)
|
||||
|
||||
var alphaCfg v1alpha1.Images
|
||||
if err := yaml.Unmarshal(doc, &alphaCfg); err != nil {
|
||||
return err
|
||||
}
|
||||
var v1Cfg v1.Images
|
||||
if err := convert.ConvertImages(&alphaCfg, &v1Cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a := v1Cfg.GetAnnotations()
|
||||
for _, i := range v1Cfg.Spec.Images {
|
||||
|
||||
if a[consts.ImageAnnotationRegistry] != "" || o.Registry != "" {
|
||||
newRef, _ := reference.Parse(i.Name)
|
||||
newReg := o.Registry
|
||||
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()
|
||||
}
|
||||
l.Debugf("key for image [%s]", key)
|
||||
|
||||
// verify signature using the provided key.
|
||||
err := cosign.VerifySignature(ctx, s, key, i.Name)
|
||||
|
||||
hasAnnotationIdentityOptions := a[consts.ImageAnnotationCertIdentityRegexp] != "" || a[consts.ImageAnnotationCertIdentity] != ""
|
||||
hasCliIdentityOptions := o.CertIdentityRegexp != "" || o.CertIdentity != ""
|
||||
hasImageIdentityOptions := i.CertIdentityRegexp != "" || i.CertIdentity != ""
|
||||
|
||||
needsKeylessVerificaton := hasAnnotationIdentityOptions || hasCliIdentityOptions || hasImageIdentityOptions
|
||||
needsPubKeyVerification := a[consts.ImageAnnotationKey] != "" || o.Key != "" || i.Key != ""
|
||||
if needsPubKeyVerification {
|
||||
key := o.Key
|
||||
if o.Key == "" && a[consts.ImageAnnotationKey] != "" {
|
||||
key, err = homedir.Expand(a[consts.ImageAnnotationKey])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if i.Key != "" {
|
||||
key, err = homedir.Expand(i.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
l.Debugf("key for image [%s]", key)
|
||||
|
||||
tlog := o.Tlog
|
||||
if !o.Tlog && a[consts.ImageAnnotationTlog] == "true" {
|
||||
tlog = true
|
||||
}
|
||||
if i.Tlog {
|
||||
tlog = i.Tlog
|
||||
}
|
||||
l.Debugf("transparency log for verification [%b]", tlog)
|
||||
|
||||
if err := cosign.VerifySignature(ctx, s, key, tlog, i.Name, rso, ro); err != nil {
|
||||
l.Errorf("signature verification failed for image [%s]... skipping...\n%v", i.Name, err)
|
||||
continue
|
||||
}
|
||||
l.Infof("signature verified for image [%s]", i.Name)
|
||||
} else if needsKeylessVerificaton { //Keyless signature verification
|
||||
certIdentityRegexp := o.CertIdentityRegexp
|
||||
if o.CertIdentityRegexp == "" && a[consts.ImageAnnotationCertIdentityRegexp] != "" {
|
||||
certIdentityRegexp = a[consts.ImageAnnotationCertIdentityRegexp]
|
||||
}
|
||||
if i.CertIdentityRegexp != "" {
|
||||
certIdentityRegexp = i.CertIdentityRegexp
|
||||
}
|
||||
l.Debugf("certIdentityRegexp for image [%s]", certIdentityRegexp)
|
||||
|
||||
certIdentity := o.CertIdentity
|
||||
if o.CertIdentity == "" && a[consts.ImageAnnotationCertIdentity] != "" {
|
||||
certIdentity = a[consts.ImageAnnotationCertIdentity]
|
||||
}
|
||||
if i.CertIdentity != "" {
|
||||
certIdentity = i.CertIdentity
|
||||
}
|
||||
l.Debugf("certIdentity for image [%s]", certIdentity)
|
||||
|
||||
certOidcIssuer := o.CertOidcIssuer
|
||||
if o.CertOidcIssuer == "" && a[consts.ImageAnnotationCertOidcIssuer] != "" {
|
||||
certOidcIssuer = a[consts.ImageAnnotationCertOidcIssuer]
|
||||
}
|
||||
if i.CertOidcIssuer != "" {
|
||||
certOidcIssuer = i.CertOidcIssuer
|
||||
}
|
||||
l.Debugf("certOidcIssuer for image [%s]", certOidcIssuer)
|
||||
|
||||
certOidcIssuerRegexp := o.CertOidcIssuerRegexp
|
||||
if o.CertOidcIssuerRegexp == "" && a[consts.ImageAnnotationCertOidcIssuerRegexp] != "" {
|
||||
certOidcIssuerRegexp = a[consts.ImageAnnotationCertOidcIssuerRegexp]
|
||||
}
|
||||
if i.CertOidcIssuerRegexp != "" {
|
||||
certOidcIssuerRegexp = i.CertOidcIssuerRegexp
|
||||
}
|
||||
l.Debugf("certOidcIssuerRegexp for image [%s]", certOidcIssuerRegexp)
|
||||
|
||||
certGithubWorkflowRepository := o.CertGithubWorkflowRepository
|
||||
if o.CertGithubWorkflowRepository == "" && a[consts.ImageAnnotationCertGithubWorkflowRepository] != "" {
|
||||
certGithubWorkflowRepository = a[consts.ImageAnnotationCertGithubWorkflowRepository]
|
||||
}
|
||||
if i.CertGithubWorkflowRepository != "" {
|
||||
certGithubWorkflowRepository = i.CertGithubWorkflowRepository
|
||||
}
|
||||
l.Debugf("certGithubWorkflowRepository for image [%s]", certGithubWorkflowRepository)
|
||||
|
||||
tlog := o.Tlog
|
||||
if !o.Tlog && a[consts.ImageAnnotationTlog] == "true" {
|
||||
tlog = true
|
||||
}
|
||||
if i.Tlog {
|
||||
tlog = i.Tlog
|
||||
}
|
||||
l.Debugf("transparency log for verification [%b]", tlog)
|
||||
|
||||
if err := cosign.VerifyKeylessSignature(ctx, s, certIdentity, certIdentityRegexp, certOidcIssuer, certOidcIssuerRegexp, certGithubWorkflowRepository, tlog, i.Name, rso, ro); err != nil {
|
||||
l.Errorf("keyless signature verification failed for image [%s]... skipping...\n%v", i.Name, err)
|
||||
continue
|
||||
}
|
||||
l.Infof("keyless signature verified for image [%s]", i.Name)
|
||||
}
|
||||
|
||||
platform := o.Platform
|
||||
if o.Platform == "" && a[consts.ImageAnnotationPlatform] != "" {
|
||||
platform = a[consts.ImageAnnotationPlatform]
|
||||
}
|
||||
if i.Platform != "" {
|
||||
platform = i.Platform
|
||||
}
|
||||
|
||||
rewrite := ""
|
||||
if i.Rewrite != "" {
|
||||
rewrite = i.Rewrite
|
||||
}
|
||||
|
||||
if err := storeImage(ctx, s, i, platform, rso, ro, rewrite); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.CopyAll(ctx, s.OCI, nil)
|
||||
|
||||
case "v1":
|
||||
var cfg v1.Images
|
||||
if err := yaml.Unmarshal(doc, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a := cfg.GetAnnotations()
|
||||
for _, i := range cfg.Spec.Images {
|
||||
|
||||
if a[consts.ImageAnnotationRegistry] != "" || o.Registry != "" {
|
||||
newRef, _ := reference.Parse(i.Name)
|
||||
newReg := o.Registry
|
||||
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()
|
||||
}
|
||||
|
||||
hasAnnotationIdentityOptions := a[consts.ImageAnnotationCertIdentityRegexp] != "" || a[consts.ImageAnnotationCertIdentity] != ""
|
||||
hasCliIdentityOptions := o.CertIdentityRegexp != "" || o.CertIdentity != ""
|
||||
hasImageIdentityOptions := i.CertIdentityRegexp != "" || i.CertIdentity != ""
|
||||
|
||||
needsKeylessVerificaton := hasAnnotationIdentityOptions || hasCliIdentityOptions || hasImageIdentityOptions
|
||||
needsPubKeyVerification := a[consts.ImageAnnotationKey] != "" || o.Key != "" || i.Key != ""
|
||||
if needsPubKeyVerification {
|
||||
key := o.Key
|
||||
if o.Key == "" && a[consts.ImageAnnotationKey] != "" {
|
||||
key, err = homedir.Expand(a[consts.ImageAnnotationKey])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if i.Key != "" {
|
||||
key, err = homedir.Expand(i.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
l.Debugf("key for image [%s]", key)
|
||||
|
||||
tlog := o.Tlog
|
||||
if !o.Tlog && a[consts.ImageAnnotationTlog] == "true" {
|
||||
tlog = true
|
||||
}
|
||||
if i.Tlog {
|
||||
tlog = i.Tlog
|
||||
}
|
||||
l.Debugf("transparency log for verification [%b]", tlog)
|
||||
|
||||
if err := cosign.VerifySignature(ctx, s, key, tlog, i.Name, rso, ro); err != nil {
|
||||
l.Errorf("signature verification failed for image [%s]... skipping...\n%v", i.Name, err)
|
||||
continue
|
||||
}
|
||||
l.Infof("signature verified for image [%s]", i.Name)
|
||||
} else if needsKeylessVerificaton { //Keyless signature verification
|
||||
certIdentityRegexp := o.CertIdentityRegexp
|
||||
if o.CertIdentityRegexp == "" && a[consts.ImageAnnotationCertIdentityRegexp] != "" {
|
||||
certIdentityRegexp = a[consts.ImageAnnotationCertIdentityRegexp]
|
||||
}
|
||||
if i.CertIdentityRegexp != "" {
|
||||
certIdentityRegexp = i.CertIdentityRegexp
|
||||
}
|
||||
l.Debugf("certIdentityRegexp for image [%s]", certIdentityRegexp)
|
||||
|
||||
certIdentity := o.CertIdentity
|
||||
if o.CertIdentity == "" && a[consts.ImageAnnotationCertIdentity] != "" {
|
||||
certIdentity = a[consts.ImageAnnotationCertIdentity]
|
||||
}
|
||||
if i.CertIdentity != "" {
|
||||
certIdentity = i.CertIdentity
|
||||
}
|
||||
l.Debugf("certIdentity for image [%s]", certIdentity)
|
||||
|
||||
certOidcIssuer := o.CertOidcIssuer
|
||||
if o.CertOidcIssuer == "" && a[consts.ImageAnnotationCertOidcIssuer] != "" {
|
||||
certOidcIssuer = a[consts.ImageAnnotationCertOidcIssuer]
|
||||
}
|
||||
if i.CertOidcIssuer != "" {
|
||||
certOidcIssuer = i.CertOidcIssuer
|
||||
}
|
||||
l.Debugf("certOidcIssuer for image [%s]", certOidcIssuer)
|
||||
|
||||
certOidcIssuerRegexp := o.CertOidcIssuerRegexp
|
||||
if o.CertOidcIssuerRegexp == "" && a[consts.ImageAnnotationCertOidcIssuerRegexp] != "" {
|
||||
certOidcIssuerRegexp = a[consts.ImageAnnotationCertOidcIssuerRegexp]
|
||||
}
|
||||
if i.CertOidcIssuerRegexp != "" {
|
||||
certOidcIssuerRegexp = i.CertOidcIssuerRegexp
|
||||
}
|
||||
l.Debugf("certOidcIssuerRegexp for image [%s]", certOidcIssuerRegexp)
|
||||
|
||||
certGithubWorkflowRepository := o.CertGithubWorkflowRepository
|
||||
if o.CertGithubWorkflowRepository == "" && a[consts.ImageAnnotationCertGithubWorkflowRepository] != "" {
|
||||
certGithubWorkflowRepository = a[consts.ImageAnnotationCertGithubWorkflowRepository]
|
||||
}
|
||||
if i.CertGithubWorkflowRepository != "" {
|
||||
certGithubWorkflowRepository = i.CertGithubWorkflowRepository
|
||||
}
|
||||
l.Debugf("certGithubWorkflowRepository for image [%s]", certGithubWorkflowRepository)
|
||||
|
||||
tlog := o.Tlog
|
||||
if !o.Tlog && a[consts.ImageAnnotationTlog] == "true" {
|
||||
tlog = true
|
||||
}
|
||||
if i.Tlog {
|
||||
tlog = i.Tlog
|
||||
}
|
||||
l.Debugf("transparency log for verification [%b]", tlog)
|
||||
|
||||
if err := cosign.VerifyKeylessSignature(ctx, s, certIdentity, certIdentityRegexp, certOidcIssuer, certOidcIssuerRegexp, certGithubWorkflowRepository, tlog, i.Name, rso, ro); err != nil {
|
||||
l.Errorf("keyless signature verification failed for image [%s]... skipping...\n%v", i.Name, err)
|
||||
continue
|
||||
}
|
||||
l.Infof("keyless signature verified for image [%s]", i.Name)
|
||||
}
|
||||
platform := o.Platform
|
||||
if o.Platform == "" && a[consts.ImageAnnotationPlatform] != "" {
|
||||
platform = a[consts.ImageAnnotationPlatform]
|
||||
}
|
||||
if i.Platform != "" {
|
||||
platform = i.Platform
|
||||
}
|
||||
|
||||
rewrite := ""
|
||||
if i.Rewrite != "" {
|
||||
rewrite = i.Rewrite
|
||||
}
|
||||
|
||||
if err := storeImage(ctx, s, i, platform, rso, ro, rewrite); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.CopyAll(ctx, s.OCI, nil)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported version [%s] for kind [%s]... valid versions are [v1 and v1alpha1]", gvk.Version, gvk.Kind)
|
||||
}
|
||||
|
||||
case consts.ChartsContentKind:
|
||||
switch gvk.Version {
|
||||
case "v1alpha1":
|
||||
l.Warnf("!!! DEPRECATION WARNING !!! apiVersion [%s] will be removed in a future release...", gvk.Version)
|
||||
|
||||
var alphaCfg v1alpha1.Charts
|
||||
if err := yaml.Unmarshal(doc, &alphaCfg); err != nil {
|
||||
return err
|
||||
}
|
||||
var v1Cfg v1.Charts
|
||||
if err := convert.ConvertCharts(&alphaCfg, &v1Cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ch := range v1Cfg.Spec.Charts {
|
||||
if err := storeChart(ctx, s, ch,
|
||||
&flags.AddChartOpts{
|
||||
ChartOpts: &action.ChartPathOptions{
|
||||
RepoURL: ch.RepoURL,
|
||||
Version: ch.Version,
|
||||
},
|
||||
},
|
||||
rso, ro,
|
||||
"",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case "v1":
|
||||
var cfg v1.Charts
|
||||
if err := yaml.Unmarshal(doc, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
registry := o.Registry
|
||||
if registry == "" {
|
||||
annotation := cfg.GetAnnotations()
|
||||
if annotation != nil {
|
||||
registry = annotation[consts.ImageAnnotationRegistry]
|
||||
}
|
||||
}
|
||||
|
||||
for i, ch := range cfg.Spec.Charts {
|
||||
if err := storeChart(ctx, s, ch,
|
||||
&flags.AddChartOpts{
|
||||
ChartOpts: &action.ChartPathOptions{
|
||||
RepoURL: ch.RepoURL,
|
||||
Version: ch.Version,
|
||||
},
|
||||
AddImages: ch.AddImages,
|
||||
AddDependencies: ch.AddDependencies,
|
||||
Registry: registry,
|
||||
Platform: o.Platform,
|
||||
},
|
||||
rso, ro,
|
||||
cfg.Spec.Charts[i].Rewrite,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported version [%s] for kind [%s]... valid versions are [v1 and v1alpha1]", gvk.Version, gvk.Kind)
|
||||
}
|
||||
|
||||
case consts.ChartsCollectionKind:
|
||||
switch gvk.Version {
|
||||
case "v1alpha1":
|
||||
l.Warnf("!!! DEPRECATION WARNING !!! apiVersion [%s] will be removed in a future release...", gvk.Version)
|
||||
|
||||
var alphaCfg v1alpha1.ThickCharts
|
||||
if err := yaml.Unmarshal(doc, &alphaCfg); err != nil {
|
||||
return err
|
||||
}
|
||||
var v1Cfg v1.ThickCharts
|
||||
if err := convert.ConvertThickCharts(&alphaCfg, &v1Cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, chObj := range v1Cfg.Spec.Charts {
|
||||
tc, err := tchart.NewThickChart(chObj, &action.ChartPathOptions{
|
||||
RepoURL: chObj.RepoURL,
|
||||
Version: chObj.Version,
|
||||
})
|
||||
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
|
||||
return err
|
||||
}
|
||||
if _, err := s.AddOCICollection(ctx, tc); err != nil {
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
case "v1":
|
||||
var cfg v1.ThickCharts
|
||||
if err := yaml.Unmarshal(doc, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// sync with local index
|
||||
s.CopyAll(ctx, s.OCI, nil)
|
||||
for _, chObj := range cfg.Spec.Charts {
|
||||
tc, err := tchart.NewThickChart(chObj, &action.ChartPathOptions{
|
||||
RepoURL: chObj.RepoURL,
|
||||
Version: chObj.Version,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := s.AddOCICollection(ctx, tc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case v1alpha1.ChartsContentKind:
|
||||
var cfg v1alpha1.Charts
|
||||
if err := yaml.Unmarshal(doc, &cfg); err != nil {
|
||||
return err
|
||||
default:
|
||||
return fmt.Errorf("unsupported version [%s] for kind [%s]... valid versions are [v1 and v1alpha1]", gvk.Version, gvk.Kind)
|
||||
}
|
||||
|
||||
for _, ch := range cfg.Spec.Charts {
|
||||
// TODO: Provide a way to configure syncs
|
||||
err := storeChart(ctx, s, ch, &action.ChartPathOptions{})
|
||||
if err != nil {
|
||||
case consts.ImageTxtsContentKind:
|
||||
switch gvk.Version {
|
||||
case "v1alpha1":
|
||||
l.Warnf("!!! DEPRECATION WARNING !!! apiVersion [%s] will be removed in a future release...", gvk.Version)
|
||||
|
||||
var alphaCfg v1alpha1.ImageTxts
|
||||
if err := yaml.Unmarshal(doc, &alphaCfg); 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 {
|
||||
return err
|
||||
}
|
||||
|
||||
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.NewThickChart(cfg, &action.ChartPathOptions{
|
||||
RepoURL: cfg.RepoURL,
|
||||
Version: cfg.Version,
|
||||
})
|
||||
if err != nil {
|
||||
var v1Cfg v1.ImageTxts
|
||||
if err := convert.ConvertImageTxts(&alphaCfg, &v1Cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, cfgIt := range v1Cfg.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", v1Cfg.Name, err)
|
||||
}
|
||||
if _, err := s.AddOCICollection(ctx, it); err != nil {
|
||||
return fmt.Errorf("add ImageTxt %s to store: %v", v1Cfg.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := s.AddOCICollection(ctx, tc); err != nil {
|
||||
case "v1":
|
||||
var cfg v1.ImageTxts
|
||||
if err := yaml.Unmarshal(doc, &cfg); 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...),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert ImageTxt %s: %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.AddOCICollection(ctx, it); err != nil {
|
||||
return fmt.Errorf("add ImageTxt %s to store: %v", cfg.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := s.AddOCICollection(ctx, it); err != nil {
|
||||
return fmt.Errorf("add ImageTxt %s to store: %v", cfg.Name, err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported version [%s] for kind [%s]... valid versions are [v1 and v1alpha1]", gvk.Version, gvk.Kind)
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unrecognized content/collection type: %s", obj.GroupVersionKind().String())
|
||||
return fmt.Errorf("unsupported kind [%s]... valid kinds are [Files, Images, Charts, ThickCharts, ImageTxts]", gvk.Kind)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -5,11 +5,12 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/rancherfederal/hauler/internal/version"
|
||||
"hauler.dev/go/hauler/internal/flags"
|
||||
"hauler.dev/go/hauler/internal/version"
|
||||
)
|
||||
|
||||
func addVersion(parent *cobra.Command) {
|
||||
var json bool
|
||||
func addVersion(parent *cobra.Command, ro *flags.CliRootOpts) {
|
||||
o := &flags.VersionOpts{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "version",
|
||||
@@ -22,7 +23,7 @@ func addVersion(parent *cobra.Command) {
|
||||
v.FontName = "starwars"
|
||||
cmd.SetOut(cmd.OutOrStdout())
|
||||
|
||||
if json {
|
||||
if o.JSON {
|
||||
out, err := v.JSONString()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to generate JSON from version info: %w", err)
|
||||
@@ -34,7 +35,7 @@ func addVersion(parent *cobra.Command) {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&json, "json", false, "toggle output in JSON")
|
||||
o.AddFlags(cmd)
|
||||
|
||||
parent.AddCommand(cmd)
|
||||
}
|
||||
|
||||
@@ -2,17 +2,13 @@ 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"
|
||||
"hauler.dev/go/hauler/cmd/hauler/cli"
|
||||
"hauler.dev/go/hauler/internal/flags"
|
||||
"hauler.dev/go/hauler/pkg/log"
|
||||
)
|
||||
|
||||
//go:embed binaries/*
|
||||
var binaries embed.FS
|
||||
|
||||
func main() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
@@ -20,13 +16,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 {
|
||||
if err := cli.New(ctx, &flags.CliRootOpts{}).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
|
||||
419
go.mod
419
go.mod
@@ -1,175 +1,362 @@
|
||||
module github.com/rancherfederal/hauler
|
||||
module hauler.dev/go/hauler
|
||||
|
||||
go 1.21
|
||||
go 1.25.5
|
||||
|
||||
replace github.com/sigstore/cosign/v3 => github.com/hauler-dev/cosign/v3 v3.0.5-0.20260212234448-00b85d677dfc
|
||||
|
||||
replace github.com/distribution/distribution/v3 => github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2
|
||||
|
||||
replace github.com/docker/cli => github.com/docker/cli v28.5.1+incompatible // needed to keep oras v1.2.7 working, which depends on docker/cli v28.5.1+incompatible
|
||||
|
||||
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/containerd/containerd v1.7.29
|
||||
github.com/distribution/distribution/v3 v3.0.0
|
||||
github.com/google/go-containerregistry v0.20.7
|
||||
github.com/gorilla/handlers v1.5.2
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/mholt/archives v0.1.5
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/olekukonko/tablewriter v1.1.2
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.1.0-rc6
|
||||
github.com/opencontainers/image-spec v1.1.1
|
||||
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/rs/zerolog v1.34.0
|
||||
github.com/sigstore/cosign/v3 v3.0.2
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
golang.org/x/sync v0.19.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
helm.sh/helm/v3 v3.19.0
|
||||
k8s.io/apimachinery v0.35.0
|
||||
k8s.io/client-go v0.35.0
|
||||
oras.land/oras-go v1.2.7
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/auth v0.18.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
cuelabs.dev/go/oci/ociregistry v0.0.0-20250722084951-074d06050084 // indirect
|
||||
cuelang.org/go v0.15.3 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
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/AliyunContainerService/ack-ram-tool/pkg/credentials/provider v0.14.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest v0.11.29 // indirect
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.23 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/MakeNowJust/heredoc v1.0.0 // 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/semver/v3 v3.4.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||
github.com/Microsoft/hcsshim v0.11.4 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/STARRY-S/zip v0.2.3 // indirect
|
||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d // indirect
|
||||
github.com/andybalholm/brotli v1.0.1 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
|
||||
github.com/ThalesIgnite/crypto11 v1.2.5 // indirect
|
||||
github.com/agnivade/levenshtein v1.2.1 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect
|
||||
github.com/alibabacloud-go/cr-20160607 v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/cr-20181201 v1.0.10 // indirect
|
||||
github.com/alibabacloud-go/darabonba-openapi v0.2.1 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.0 // indirect
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.1 // indirect
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
|
||||
github.com/alibabacloud-go/tea v1.2.1 // indirect
|
||||
github.com/alibabacloud-go/tea-utils v1.4.5 // indirect
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
|
||||
github.com/aliyun/credentials-go v1.3.2 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.51.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
|
||||
github.com/aws/smithy-go v1.24.0 // indirect
|
||||
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.11.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||
github.com/bodgit/sevenzip v1.6.1 // indirect
|
||||
github.com/bodgit/windows 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/buildkite/agent/v3 v3.115.2 // indirect
|
||||
github.com/buildkite/go-pipeline v0.16.0 // indirect
|
||||
github.com/buildkite/interpolate v0.1.5 // indirect
|
||||
github.com/buildkite/roko v1.4.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chai2010/gettext-go v1.0.2 // indirect
|
||||
github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.6.0 // indirect
|
||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||
github.com/cockroachdb/apd/v3 v3.2.1 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // 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/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/containerd/platforms v1.0.0-rc.2 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
|
||||
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect
|
||||
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/cli v29.0.3+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/docker v28.5.2+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.4 // indirect
|
||||
github.com/docker/go-connections v0.6.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/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/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/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/emicklei/proto v1.14.2 // indirect
|
||||
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
|
||||
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.4 // 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-ini/ini v1.67.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // 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/go-openapi/analysis v0.24.1 // indirect
|
||||
github.com/go-openapi/errors v0.22.6 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.4 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.4 // indirect
|
||||
github.com/go-openapi/loads v0.23.2 // indirect
|
||||
github.com/go-openapi/runtime v0.29.2 // indirect
|
||||
github.com/go-openapi/spec v0.22.3 // indirect
|
||||
github.com/go-openapi/strfmt v0.25.0 // indirect
|
||||
github.com/go-openapi/swag v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/fileutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/loading v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/mangling v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/netutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
|
||||
github.com/go-openapi/validate v0.25.1 // indirect
|
||||
github.com/go-piv/piv-go/v2 v2.4.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // 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-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // 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/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/google/btree v1.1.3 // indirect
|
||||
github.com/google/certificate-transparency-go v1.3.2 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-github/v73 v73.0.0 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.9 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||
github.com/gosuri/uitable v0.0.4 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // 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/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/in-toto/attestation v1.1.2 // indirect
|
||||
github.com/in-toto/in-toto-golang v0.9.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 // indirect
|
||||
github.com/jmoiron/sqlx v1.4.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/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
||||
github.com/lestrrat-go/dsig v1.0.0 // indirect
|
||||
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.1 // indirect
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.12 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
|
||||
github.com/letsencrypt/boulder v0.20251110.0 // indirect
|
||||
github.com/lib/pq v1.10.9 // 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/manifoldco/promptui v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||
github.com/minio/minlz v1.0.1 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // 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/spdystream v0.5.0 // indirect
|
||||
github.com/moby/term v0.5.2 // 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.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
|
||||
github.com/mozillazg/docker-credential-acr-helper v0.4.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/nwaples/rardecode v1.1.0 // indirect
|
||||
github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.2.1 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/oleiade/reflections v1.1.0 // indirect
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||
github.com/olekukonko/errors v1.1.0 // indirect
|
||||
github.com/olekukonko/ll v0.1.3 // indirect
|
||||
github.com/open-policy-agent/opa v1.12.1 // indirect
|
||||
github.com/pborman/uuid v1.2.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // 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/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/protocolbuffers/txtpbfmt v0.0.0-20251016062345-16587c79cd91 // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/rubenv/sql-migrate v1.8.0 // 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/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/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||
github.com/sassoftware/relic v7.2.1+incompatible // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.10.0 // indirect
|
||||
github.com/segmentio/asm v1.2.1 // indirect
|
||||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sigstore/fulcio v1.8.5 // indirect
|
||||
github.com/sigstore/protobuf-specs v0.5.0 // indirect
|
||||
github.com/sigstore/rekor v1.5.0 // indirect
|
||||
github.com/sigstore/rekor-tiles/v2 v2.0.1 // indirect
|
||||
github.com/sigstore/sigstore v1.10.4 // indirect
|
||||
github.com/sigstore/sigstore-go v1.1.4 // indirect
|
||||
github.com/sigstore/timestamp-authority/v2 v2.0.4 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/spf13/viper v1.21.0 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
|
||||
github.com/tchap/go-patricia/v2 v2.3.3 // indirect
|
||||
github.com/thales-e-security/pool v0.0.2 // indirect
|
||||
github.com/theupdateframework/go-tuf v0.7.0 // indirect
|
||||
github.com/theupdateframework/go-tuf/v2 v2.3.1 // indirect
|
||||
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c // indirect
|
||||
github.com/transparency-dev/merkle v0.0.2 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/valyala/fastjson v1.6.4 // indirect
|
||||
github.com/vbatts/tar-split v0.12.2 // indirect
|
||||
github.com/vektah/gqlparser/v2 v2.5.31 // indirect
|
||||
github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
github.com/yashtewari/glob-intersection v0.2.0 // 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
|
||||
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
|
||||
gitlab.com/gitlab-org/api/client-go v1.11.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.6 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.1 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/term v0.38.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/api v0.260.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.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
|
||||
k8s.io/api v0.35.0 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.34.0 // indirect
|
||||
k8s.io/apiserver v0.34.0 // indirect
|
||||
k8s.io/cli-runtime v0.34.0 // indirect
|
||||
k8s.io/component-base v0.34.0 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
|
||||
k8s.io/kubectl v0.34.0 // indirect
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
||||
oras.land/oras-go/v2 v2.6.0 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.20.1 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/release-utils v0.12.3 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
200
install.sh
200
install.sh
@@ -13,15 +13,27 @@
|
||||
# - curl -sfL https://get.hauler.dev | HAULER_VERSION=1.0.0 bash
|
||||
# - HAULER_VERSION=1.0.0 ./install.sh
|
||||
#
|
||||
# Set Install Directory
|
||||
# - curl -sfL https://get.hauler.dev | HAULER_INSTALL_DIR=/usr/local/bin bash
|
||||
# - HAULER_INSTALL_DIR=/usr/local/bin ./install.sh
|
||||
#
|
||||
# Set Hauler Directory
|
||||
# - curl -sfL https://get.hauler.dev | HAULER_DIR=$HOME/.hauler bash
|
||||
# - HAULER_DIR=$HOME/.hauler ./install.sh
|
||||
#
|
||||
# Debug Usage:
|
||||
# - curl -sfL https://get.hauler.dev | HAULER_DEBUG=true bash
|
||||
# - HAULER_DEBUG=true ./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
|
||||
# - https://github.com/hauler-dev/hauler
|
||||
|
||||
# set functions for debugging/logging
|
||||
# set functions for logging
|
||||
function verbose {
|
||||
echo "$1"
|
||||
}
|
||||
@@ -36,146 +48,176 @@ function warn {
|
||||
|
||||
function fatal {
|
||||
echo && echo "[ERROR] Hauler: $1"
|
||||
exit 0
|
||||
exit 1
|
||||
}
|
||||
|
||||
# check for required dependencies
|
||||
for cmd in sudo rm curl grep mkdir sed awk openssl tar; do
|
||||
# debug hauler from argument or environment variable
|
||||
if [ "${HAULER_DEBUG}" = "true" ]; then
|
||||
set -x
|
||||
fi
|
||||
|
||||
# start hauler preflight checks
|
||||
info "Starting Preflight Checks..."
|
||||
|
||||
# check for required packages and dependencies
|
||||
for cmd in echo curl grep sed rm mkdir awk openssl tar install source; do
|
||||
if ! command -v "$cmd" &> /dev/null; then
|
||||
fatal "$cmd is not installed"
|
||||
fatal "$cmd is required to install Hauler"
|
||||
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}"
|
||||
# set install directory from argument or environment variable
|
||||
HAULER_INSTALL_DIR=${HAULER_INSTALL_DIR:-/usr/local/bin}
|
||||
|
||||
# ensure install directory exists and/or create it
|
||||
if [ ! -d "${HAULER_INSTALL_DIR}" ]; then
|
||||
mkdir -p "${HAULER_INSTALL_DIR}" || fatal "Failed to Create Install Directory: ${HAULER_INSTALL_DIR}"
|
||||
fi
|
||||
|
||||
# set uninstall environment variable from argument or environment
|
||||
# ensure install directory is writable (by user or root privileges)
|
||||
if [ ! -w "${HAULER_INSTALL_DIR}" ]; then
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
fatal "Root privileges are required to install Hauler to Directory: ${HAULER_INSTALL_DIR}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# uninstall hauler from argument or environment variable
|
||||
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"
|
||||
rm -rf "${HAULER_INSTALL_DIR}/hauler" || fatal "Failed to Remove Hauler from ${HAULER_INSTALL_DIR}"
|
||||
|
||||
# remove the installation directory
|
||||
rm -rf "$HOME/.hauler" || fatal "Failed to Remove Directory: $HOME/.hauler"
|
||||
# remove the hauler directory
|
||||
rm -rf "${HAULER_DIR}" || fatal "Failed to Remove Hauler Directory: ${HAULER_DIR}"
|
||||
|
||||
info "Hauler Uninstalled Successfully"
|
||||
info "Successfully Uninstalled Hauler" && echo
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# set version environment variable
|
||||
if [ -z "${HAULER_VERSION}" ]; then
|
||||
# attempt to retrieve the latest version from GitHub
|
||||
HAULER_VERSION=$(curl -sI https://github.com/hauler-dev/hauler/releases/latest | grep -i location | sed -e 's#.*tag/v##' -e 's/^[[:space:]]*//g' -e 's/[[:space:]]*$//g')
|
||||
|
||||
# exit if the version could not be detected
|
||||
if [ -z "${HAULER_VERSION}" ]; then
|
||||
fatal "HAULER_VERSION is unable to be detected and/or retrieved from GitHub. Please set: HAULER_VERSION"
|
||||
fi
|
||||
fi
|
||||
|
||||
# detect the operating system
|
||||
platform=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
case $platform in
|
||||
PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
case $PLATFORM in
|
||||
linux)
|
||||
platform="linux"
|
||||
PLATFORM="linux"
|
||||
;;
|
||||
darwin)
|
||||
platform="darwin"
|
||||
PLATFORM="darwin"
|
||||
;;
|
||||
*)
|
||||
fatal "Unsupported Platform: $platform"
|
||||
fatal "Unsupported Platform: ${PLATFORM}"
|
||||
;;
|
||||
esac
|
||||
|
||||
# detect the architecture
|
||||
arch=$(uname -m)
|
||||
case $arch in
|
||||
ARCH=$(uname -m)
|
||||
case $ARCH in
|
||||
x86_64 | x86-32 | x64 | x32 | amd64)
|
||||
arch="amd64"
|
||||
ARCH="amd64"
|
||||
;;
|
||||
aarch64 | arm64)
|
||||
arch="arm64"
|
||||
ARCH="arm64"
|
||||
;;
|
||||
*)
|
||||
fatal "Unsupported Architecture: $arch"
|
||||
fatal "Unsupported Architecture: ${ARCH}"
|
||||
;;
|
||||
esac
|
||||
|
||||
# set hauler directory from argument or environment variable
|
||||
HAULER_DIR=${HAULER_DIR:-$HOME/.hauler}
|
||||
|
||||
# start hauler installation
|
||||
info "Starting Installation..."
|
||||
|
||||
# display the version, platform, and architecture
|
||||
verbose "- Version: v$version"
|
||||
verbose "- Platform: $platform"
|
||||
verbose "- Architecture: $arch"
|
||||
verbose "- Version: v${HAULER_VERSION}"
|
||||
verbose "- Platform: ${PLATFORM}"
|
||||
verbose "- Architecture: ${ARCH}"
|
||||
verbose "- Install Directory: ${HAULER_INSTALL_DIR}"
|
||||
verbose "- Hauler Directory: ${HAULER_DIR}"
|
||||
|
||||
# check if install directory exists, create it if not
|
||||
if [ ! -d "$HOME/.hauler" ]; then
|
||||
mkdir -p "$HOME/.hauler" || fatal "Failed to Create Directory: ~/.hauler"
|
||||
# ensure hauler directory exists and/or create it
|
||||
if [ ! -d "${HAULER_DIR}" ]; then
|
||||
mkdir -p "${HAULER_DIR}" || fatal "Failed to Create Hauler Directory: ${HAULER_DIR}"
|
||||
fi
|
||||
|
||||
# change to install directory
|
||||
cd "$HOME/.hauler" || fatal "Failed to Change Directory: ~/.hauler"
|
||||
# ensure hauler directory is writable (by user or root privileges)
|
||||
chmod -R 777 "${HAULER_DIR}" || fatal "Failed to Update Permissions of Hauler Directory: ${HAULER_DIR}"
|
||||
|
||||
# change to hauler directory
|
||||
cd "${HAULER_DIR}" || fatal "Failed to Change Directory to Hauler Directory: ${HAULER_DIR}"
|
||||
|
||||
# start hauler artifacts download
|
||||
info "Starting Download..."
|
||||
|
||||
# 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"
|
||||
if ! curl -sfOL "https://github.com/hauler-dev/hauler/releases/download/v${HAULER_VERSION}/hauler_${HAULER_VERSION}_checksums.txt"; then
|
||||
fatal "Failed to Download: hauler_${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"
|
||||
if ! curl -sfOL "https://github.com/hauler-dev/hauler/releases/download/v${HAULER_VERSION}/hauler_${HAULER_VERSION}_${PLATFORM}_${ARCH}.tar.gz"; then
|
||||
fatal "Failed to Download: hauler_${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}')
|
||||
EXPECTED_CHECKSUM=$(awk -v HAULER_VERSION="${HAULER_VERSION}" -v PLATFORM="${PLATFORM}" -v ARCH="${ARCH}" '$2 == "hauler_"HAULER_VERSION"_"PLATFORM"_"ARCH".tar.gz" {print $1}' "hauler_${HAULER_VERSION}_checksums.txt")
|
||||
DETERMINED_CHECKSUM=$(openssl dgst -sha256 "hauler_${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"
|
||||
if [ -z "${EXPECTED_CHECKSUM}" ]; then
|
||||
fatal "Failed to Locate Checksum: hauler_${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_${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"
|
||||
verbose "- Expected: ${EXPECTED_CHECKSUM}"
|
||||
verbose "- Determined: ${DETERMINED_CHECKSUM}"
|
||||
fatal "Failed Checksum Verification: hauler_${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"
|
||||
# uncompress the hauler archive
|
||||
tar -xzf "hauler_${HAULER_VERSION}_${PLATFORM}_${ARCH}.tar.gz" || fatal "Failed to Extract: hauler_${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
|
||||
# install the hauler binary
|
||||
install -m 755 hauler "${HAULER_INSTALL_DIR}" || fatal "Failed to Install Hauler: ${HAULER_INSTALL_DIR}"
|
||||
|
||||
# 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"
|
||||
if [[ ":$PATH:" != *":${HAULER_INSTALL_DIR}:"* ]]; then
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
echo "export PATH=\$PATH:${HAULER_INSTALL_DIR}" >> "$HOME/.bashrc"
|
||||
source "$HOME/.bashrc"
|
||||
elif [ -f "$HOME/.bash_profile" ]; then
|
||||
echo "export PATH=\$PATH:${HAULER_INSTALL_DIR}" >> "$HOME/.bash_profile"
|
||||
source "$HOME/.bash_profile"
|
||||
elif [ -f "$HOME/.zshrc" ]; then
|
||||
echo "export PATH=\$PATH:${HAULER_INSTALL_DIR}" >> "$HOME/.zshrc"
|
||||
source "$HOME/.zshrc"
|
||||
elif [ -f "$HOME/.profile" ]; then
|
||||
echo "export PATH=\$PATH:${HAULER_INSTALL_DIR}" >> "$HOME/.profile"
|
||||
source "$HOME/.profile"
|
||||
else
|
||||
warn "Failed to add ${HAULER_INSTALL_DIR} to PATH: Unsupported Shell"
|
||||
fi
|
||||
fi
|
||||
|
||||
# display success message
|
||||
info "Successfully Installed at /usr/local/bin/hauler"
|
||||
info "Successfully Installed Hauler at ${HAULER_INSTALL_DIR}/hauler"
|
||||
|
||||
# display availability message
|
||||
verbose "- Hauler v${version} is now available for use!"
|
||||
info "Hauler v${HAULER_VERSION} is now available for use!"
|
||||
|
||||
# display hauler docs message
|
||||
verbose "- Documentation: https://hauler.dev" && echo
|
||||
|
||||
81
internal/flags/add.go
Normal file
81
internal/flags/add.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
)
|
||||
|
||||
type AddImageOpts struct {
|
||||
*StoreRootOpts
|
||||
Name string
|
||||
Key string
|
||||
CertOidcIssuer string
|
||||
CertOidcIssuerRegexp string
|
||||
CertIdentity string
|
||||
CertIdentityRegexp string
|
||||
CertGithubWorkflowRepository string
|
||||
Tlog bool
|
||||
Platform string
|
||||
Rewrite string
|
||||
}
|
||||
|
||||
func (o *AddImageOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
f.StringVarP(&o.Key, "key", "k", "", "(Optional) Location of public key to use for signature verification")
|
||||
f.StringVar(&o.CertIdentity, "certificate-identity", "", "(Optional) Cosign certificate-identity (either --certificate-identity or --certificate-identity-regexp required for keyless verification)")
|
||||
f.StringVar(&o.CertIdentityRegexp, "certificate-identity-regexp", "", "(Optional) Cosign certificate-identity-regexp (either --certificate-identity or --certificate-identity-regexp required for keyless verification)")
|
||||
f.StringVar(&o.CertOidcIssuer, "certificate-oidc-issuer", "", "(Optional) Cosign option to validate oidc issuer")
|
||||
f.StringVar(&o.CertOidcIssuerRegexp, "certificate-oidc-issuer-regexp", "", "(Optional) Cosign option to validate oidc issuer with regex")
|
||||
f.StringVar(&o.CertGithubWorkflowRepository, "certificate-github-workflow-repository", "", "(Optional) Cosign certificate-github-workflow-repository option")
|
||||
f.BoolVar(&o.Tlog, "use-tlog-verify", false, "(Optional) Allow transparency log verification (defaults to false)")
|
||||
f.StringVarP(&o.Platform, "platform", "p", "", "(Optional) Specify the platform of the image... i.e. linux/amd64 (defaults to all)")
|
||||
f.StringVar(&o.Rewrite, "rewrite", "", "(EXPERIMENTAL & Optional) Rewrite artifact path to specified string")
|
||||
}
|
||||
|
||||
type AddFileOpts struct {
|
||||
*StoreRootOpts
|
||||
Name string
|
||||
}
|
||||
|
||||
func (o *AddFileOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
f.StringVarP(&o.Name, "name", "n", "", "(Optional) Rewrite the name of the file")
|
||||
}
|
||||
|
||||
type AddChartOpts struct {
|
||||
*StoreRootOpts
|
||||
|
||||
ChartOpts *action.ChartPathOptions
|
||||
Rewrite string
|
||||
AddDependencies bool
|
||||
AddImages bool
|
||||
HelmValues string
|
||||
Platform string
|
||||
Registry string
|
||||
KubeVersion string
|
||||
}
|
||||
|
||||
func (o *AddChartOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
|
||||
f.StringVar(&o.ChartOpts.RepoURL, "repo", "", "Location of the chart (https:// | http:// | oci://)")
|
||||
f.StringVar(&o.ChartOpts.Version, "version", "", "(Optional) Specify the version of the chart (v1.0.0 | 2.0.0 | ^2.0.0)")
|
||||
f.BoolVar(&o.ChartOpts.Verify, "verify", false, "(Optional) Verify the chart before fetching it")
|
||||
f.StringVar(&o.ChartOpts.Username, "username", "", "(Optional) Username to use for authentication")
|
||||
f.StringVar(&o.ChartOpts.Password, "password", "", "(Optional) Password to use for authentication")
|
||||
f.StringVar(&o.ChartOpts.CertFile, "cert-file", "", "(Optional) Location of the TLS Certificate to use for client authentication")
|
||||
f.StringVar(&o.ChartOpts.KeyFile, "key-file", "", "(Optional) Location of the TLS Key to use for client authentication")
|
||||
f.BoolVar(&o.ChartOpts.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "(Optional) Skip TLS certificate verification")
|
||||
f.StringVar(&o.ChartOpts.CaFile, "ca-file", "", "(Optional) Location of CA Bundle to enable certification verification")
|
||||
f.StringVar(&o.Rewrite, "rewrite", "", "(EXPERIMENTAL & Optional) Rewrite artifact path to specified string")
|
||||
|
||||
cmd.MarkFlagsRequiredTogether("username", "password")
|
||||
cmd.MarkFlagsRequiredTogether("cert-file", "key-file", "ca-file")
|
||||
|
||||
cmd.Flags().BoolVar(&o.AddDependencies, "add-dependencies", false, "(EXPERIMENTAL & Optional) Fetch dependent helm charts")
|
||||
f.BoolVar(&o.AddImages, "add-images", false, "(EXPERIMENTAL & Optional) Fetch images referenced in helm charts")
|
||||
f.StringVar(&o.HelmValues, "values", "", "(EXPERIMENTAL & Optional) Specify helm chart values when fetching images")
|
||||
f.StringVarP(&o.Platform, "platform", "p", "", "(Optional) Specify the platform of the image, e.g. linux/amd64")
|
||||
f.StringVarP(&o.Registry, "registry", "g", "", "(Optional) Specify the registry of the image for images that do not alredy define one")
|
||||
f.StringVar(&o.KubeVersion, "kube-version", "v1.34.1", "(EXPERIMENTAL & Optional) Override the kubernetes version for helm template rendering")
|
||||
}
|
||||
17
internal/flags/cli.go
Normal file
17
internal/flags/cli.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package flags
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
type CliRootOpts struct {
|
||||
LogLevel string
|
||||
HaulerDir string
|
||||
IgnoreErrors bool
|
||||
}
|
||||
|
||||
func AddRootFlags(cmd *cobra.Command, ro *CliRootOpts) {
|
||||
pf := cmd.PersistentFlags()
|
||||
|
||||
pf.StringVarP(&ro.LogLevel, "log-level", "l", "info", "Set the logging level (i.e. info, debug, warn)")
|
||||
pf.StringVarP(&ro.HaulerDir, "haulerdir", "d", "", "Set the location of the hauler directory (default $HOME/.hauler)")
|
||||
pf.BoolVar(&ro.IgnoreErrors, "ignore-errors", false, "Ignore/Bypass errors (i.e. warn on error) (defaults false)")
|
||||
}
|
||||
38
internal/flags/copy.go
Normal file
38
internal/flags/copy.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package flags
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
type CopyOpts struct {
|
||||
*StoreRootOpts
|
||||
|
||||
Username string
|
||||
Password string
|
||||
Insecure bool
|
||||
PlainHTTP bool
|
||||
Only string
|
||||
}
|
||||
|
||||
func (o *CopyOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
|
||||
f.StringVarP(&o.Username, "username", "u", "", "(Deprecated) Please use 'hauler login'")
|
||||
f.StringVarP(&o.Password, "password", "p", "", "(Deprecated) Please use 'hauler login'")
|
||||
f.BoolVar(&o.Insecure, "insecure", false, "(Optional) Allow insecure connections")
|
||||
f.BoolVar(&o.PlainHTTP, "plain-http", false, "(Optional) Allow plain HTTP connections")
|
||||
f.StringVarP(&o.Only, "only", "o", "", "(Optional) Custom string array to only copy specific 'image' items")
|
||||
|
||||
cmd.MarkFlagsRequiredTogether("username", "password")
|
||||
|
||||
if err := f.MarkDeprecated("username", "please use 'hauler login'"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := f.MarkDeprecated("password", "please use 'hauler login'"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := f.MarkHidden("username"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := f.MarkHidden("password"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
14
internal/flags/extract.go
Normal file
14
internal/flags/extract.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package flags
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
type ExtractOpts struct {
|
||||
*StoreRootOpts
|
||||
DestinationDir string
|
||||
}
|
||||
|
||||
func (o *ExtractOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
|
||||
f.StringVarP(&o.DestinationDir, "output", "o", "", "(Optional) Set the directory to output (defaults to current directory)")
|
||||
}
|
||||
22
internal/flags/info.go
Normal file
22
internal/flags/info.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package flags
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
type InfoOpts struct {
|
||||
*StoreRootOpts
|
||||
|
||||
OutputFormat string
|
||||
TypeFilter string
|
||||
SizeUnit string
|
||||
ListRepos bool
|
||||
ShowDigests bool
|
||||
}
|
||||
|
||||
func (o *InfoOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
|
||||
f.StringVarP(&o.OutputFormat, "output", "o", "table", "(Optional) Specify the output format (table | json)")
|
||||
f.StringVar(&o.TypeFilter, "type", "all", "(Optional) Filter on content type (image | chart | file | sigs | atts | sbom)")
|
||||
f.BoolVar(&o.ListRepos, "list-repos", false, "(Optional) List all repository names")
|
||||
f.BoolVar(&o.ShowDigests, "digests", false, "(Optional) Show digests of each artifact in the output table")
|
||||
}
|
||||
17
internal/flags/load.go
Normal file
17
internal/flags/load.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"hauler.dev/go/hauler/pkg/consts"
|
||||
)
|
||||
|
||||
type LoadOpts struct {
|
||||
*StoreRootOpts
|
||||
FileName []string
|
||||
}
|
||||
|
||||
func (o *LoadOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
|
||||
f.StringSliceVarP(&o.FileName, "filename", "f", []string{consts.DefaultHaulerArchiveName}, "(Optional) Specify the name of inputted haul(s)")
|
||||
}
|
||||
11
internal/flags/remove.go
Normal file
11
internal/flags/remove.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package flags
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
type RemoveOpts struct {
|
||||
Force bool // skip remove confirmation
|
||||
}
|
||||
|
||||
func (o *RemoveOpts) AddFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().BoolVarP(&o.Force, "force", "f", false, "(Optional) Remove artifact(s) without confirmation")
|
||||
}
|
||||
22
internal/flags/save.go
Normal file
22
internal/flags/save.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"hauler.dev/go/hauler/pkg/consts"
|
||||
)
|
||||
|
||||
type SaveOpts struct {
|
||||
*StoreRootOpts
|
||||
FileName string
|
||||
Platform string
|
||||
ContainerdCompatibility bool
|
||||
}
|
||||
|
||||
func (o *SaveOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
|
||||
f.StringVarP(&o.FileName, "filename", "f", consts.DefaultHaulerArchiveName, "(Optional) Specify the name of outputted haul")
|
||||
f.StringVarP(&o.Platform, "platform", "p", "", "(Optional) Specify the platform for runtime imports... i.e. linux/amd64 (unspecified implies all)")
|
||||
f.BoolVar(&o.ContainerdCompatibility, "containerd", false, "(Optional) Enable import compatibility with containerd... removes oci-layout from the haul")
|
||||
|
||||
}
|
||||
56
internal/flags/serve.go
Normal file
56
internal/flags/serve.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"hauler.dev/go/hauler/pkg/consts"
|
||||
)
|
||||
|
||||
type ServeRegistryOpts struct {
|
||||
*StoreRootOpts
|
||||
|
||||
Port int
|
||||
RootDir string
|
||||
ConfigFile string
|
||||
ReadOnly bool
|
||||
|
||||
TLSCert string
|
||||
TLSKey string
|
||||
}
|
||||
|
||||
func (o *ServeRegistryOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
|
||||
f.IntVarP(&o.Port, "port", "p", consts.DefaultRegistryPort, "(Optional) Set the port to use for incoming connections")
|
||||
f.StringVar(&o.RootDir, "directory", consts.DefaultRegistryRootDir, "(Optional) Directory to use for backend. Defaults to $PWD/registry")
|
||||
f.StringVarP(&o.ConfigFile, "config", "c", "", "(Optional) Location of config file (overrides all flags)")
|
||||
f.BoolVar(&o.ReadOnly, "readonly", true, "(Optional) Run the registry as readonly")
|
||||
|
||||
f.StringVar(&o.TLSCert, "tls-cert", "", "(Optional) Location of the TLS Certificate to use for server authenication")
|
||||
f.StringVar(&o.TLSKey, "tls-key", "", "(Optional) Location of the TLS Key to use for server authenication")
|
||||
|
||||
cmd.MarkFlagsRequiredTogether("tls-cert", "tls-key")
|
||||
}
|
||||
|
||||
type ServeFilesOpts struct {
|
||||
*StoreRootOpts
|
||||
|
||||
Port int
|
||||
Timeout int
|
||||
RootDir string
|
||||
|
||||
TLSCert string
|
||||
TLSKey string
|
||||
}
|
||||
|
||||
func (o *ServeFilesOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
|
||||
f.IntVarP(&o.Port, "port", "p", consts.DefaultFileserverPort, "(Optional) Set the port to use for incoming connections")
|
||||
f.IntVar(&o.Timeout, "timeout", consts.DefaultFileserverTimeout, "(Optional) Timeout duration for HTTP Requests in seconds for both reads/writes")
|
||||
f.StringVar(&o.RootDir, "directory", consts.DefaultFileserverRootDir, "(Optional) Directory to use for backend. Defaults to $PWD/fileserver")
|
||||
|
||||
f.StringVar(&o.TLSCert, "tls-cert", "", "(Optional) Location of the TLS Certificate to use for server authenication")
|
||||
f.StringVar(&o.TLSKey, "tls-key", "", "(Optional) Location of the TLS Key to use for server authenication")
|
||||
|
||||
cmd.MarkFlagsRequiredTogether("tls-cert", "tls-key")
|
||||
}
|
||||
63
internal/flags/store.go
Normal file
63
internal/flags/store.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"hauler.dev/go/hauler/pkg/consts"
|
||||
"hauler.dev/go/hauler/pkg/log"
|
||||
"hauler.dev/go/hauler/pkg/store"
|
||||
)
|
||||
|
||||
type StoreRootOpts struct {
|
||||
StoreDir string
|
||||
Retries int
|
||||
TempOverride string
|
||||
}
|
||||
|
||||
func (o *StoreRootOpts) AddFlags(cmd *cobra.Command) {
|
||||
pf := cmd.PersistentFlags()
|
||||
pf.StringVarP(&o.StoreDir, "store", "s", "", "Set the directory to use for the content store")
|
||||
pf.IntVarP(&o.Retries, "retries", "r", consts.DefaultRetries, "Set the number of retries for operations")
|
||||
pf.StringVarP(&o.TempOverride, "tempdir", "t", "", "(Optional) Override the default temporary directory determined by the OS")
|
||||
}
|
||||
|
||||
func (o *StoreRootOpts) Store(ctx context.Context) (*store.Layout, error) {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
storeDir := o.StoreDir
|
||||
|
||||
if storeDir == "" {
|
||||
storeDir = os.Getenv(consts.HaulerStoreDir)
|
||||
}
|
||||
|
||||
if storeDir == "" {
|
||||
storeDir = consts.DefaultStoreName
|
||||
}
|
||||
|
||||
abs, err := filepath.Abs(storeDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
o.StoreDir = abs
|
||||
|
||||
l.Debugf("using store at [%s]", abs)
|
||||
|
||||
if _, err := os.Stat(abs); errors.Is(err, os.ErrNotExist) {
|
||||
if err := os.MkdirAll(abs, os.ModePerm); 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
|
||||
}
|
||||
41
internal/flags/sync.go
Normal file
41
internal/flags/sync.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"hauler.dev/go/hauler/pkg/consts"
|
||||
)
|
||||
|
||||
type SyncOpts struct {
|
||||
*StoreRootOpts
|
||||
FileName []string
|
||||
Key string
|
||||
CertOidcIssuer string
|
||||
CertOidcIssuerRegexp string
|
||||
CertIdentity string
|
||||
CertIdentityRegexp string
|
||||
CertGithubWorkflowRepository string
|
||||
Products []string
|
||||
Platform string
|
||||
Registry string
|
||||
ProductRegistry string
|
||||
Tlog bool
|
||||
Rewrite string
|
||||
}
|
||||
|
||||
func (o *SyncOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
|
||||
f.StringSliceVarP(&o.FileName, "filename", "f", []string{consts.DefaultHaulerManifestName}, "Specify the name of manifest(s) to sync")
|
||||
f.StringVarP(&o.Key, "key", "k", "", "(Optional) Location of public key to use for signature verification")
|
||||
f.StringVar(&o.CertIdentity, "certificate-identity", "", "(Optional) Cosign certificate-identity (either --certificate-identity or --certificate-identity-regexp required for keyless verification)")
|
||||
f.StringVar(&o.CertIdentityRegexp, "certificate-identity-regexp", "", "(Optional) Cosign certificate-identity-regexp (either --certificate-identity or --certificate-identity-regexp required for keyless verification)")
|
||||
f.StringVar(&o.CertOidcIssuer, "certificate-oidc-issuer", "", "(Optional) Cosign option to validate oidc issuer")
|
||||
f.StringVar(&o.CertOidcIssuerRegexp, "certificate-oidc-issuer-regexp", "", "(Optional) Cosign option to validate oidc issuer with regex")
|
||||
f.StringVar(&o.CertGithubWorkflowRepository, "certificate-github-workflow-repository", "", "(Optional) Cosign certificate-github-workflow-repository option")
|
||||
f.StringSliceVar(&o.Products, "products", []string{}, "(Optional) Specify the product name to fetch collections from the product registry i.e. rancher=v2.10.1,rke2=v1.31.5+rke2r1")
|
||||
f.StringVarP(&o.Platform, "platform", "p", "", "(Optional) Specify the platform of the image... i.e linux/amd64 (defaults to all)")
|
||||
f.StringVarP(&o.Registry, "registry", "g", "", "(Optional) Specify the registry of the image for images that do not alredy define one")
|
||||
f.StringVarP(&o.ProductRegistry, "product-registry", "c", "", "(Optional) Specify the product registry. Defaults to RGS Carbide Registry (rgcrprod.azurecr.us)")
|
||||
f.BoolVar(&o.Tlog, "use-tlog-verify", false, "(Optional) Allow transparency log verification (defaults to false)")
|
||||
f.StringVar(&o.Rewrite, "rewrite", "", "(EXPERIMENTAL & Optional) Rewrite artifact path to specified string")
|
||||
}
|
||||
12
internal/flags/version.go
Normal file
12
internal/flags/version.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package flags
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
type VersionOpts struct {
|
||||
JSON bool
|
||||
}
|
||||
|
||||
func (o *VersionOpts) AddFlags(cmd *cobra.Command) {
|
||||
f := cmd.Flags()
|
||||
f.BoolVar(&o.JSON, "json", false, "Set the output format to JSON")
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package mapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -15,7 +15,8 @@ import (
|
||||
)
|
||||
|
||||
// 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.
|
||||
//
|
||||
// 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{
|
||||
@@ -58,7 +59,7 @@ func (s *pusher) Push(ctx context.Context, desc ocispec.Descriptor) (ccontent.Wr
|
||||
|
||||
// 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
|
||||
return content.NewIoContentWriter(io.Discard, content.WithOutputHash(desc.Digest)), nil
|
||||
}
|
||||
|
||||
filename, err := s.mapper[desc.MediaType](desc)
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"oras.land/oras-go/pkg/target"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/consts"
|
||||
"hauler.dev/go/hauler/pkg/consts"
|
||||
)
|
||||
|
||||
type Fn func(desc ocispec.Descriptor) (string, error)
|
||||
@@ -36,7 +36,7 @@ func Images() map[string]Fn {
|
||||
m := make(map[string]Fn)
|
||||
|
||||
manifestMapperFn := Fn(func(desc ocispec.Descriptor) (string, error) {
|
||||
return "manifest.json", nil
|
||||
return consts.ImageManifestFile, nil
|
||||
})
|
||||
|
||||
for _, l := range []string{consts.DockerManifestSchema2, consts.DockerManifestListSchema2, consts.OCIManifestSchema1} {
|
||||
@@ -52,7 +52,7 @@ func Images() map[string]Fn {
|
||||
}
|
||||
|
||||
configMapperFn := Fn(func(desc ocispec.Descriptor) (string, error) {
|
||||
return "config.json", nil
|
||||
return consts.ImageConfigFile, nil
|
||||
})
|
||||
|
||||
for _, l := range []string{consts.DockerConfigJSON} {
|
||||
|
||||
@@ -9,32 +9,32 @@ import (
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
"hauler.dev/go/hauler/internal/flags"
|
||||
"hauler.dev/go/hauler/pkg/consts"
|
||||
)
|
||||
|
||||
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) {
|
||||
func NewFile(ctx context.Context, cfg flags.ServeFilesOpts) (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 = "."
|
||||
r.PathPrefix("/").Handler(handlers.LoggingHandler(os.Stdout, http.StripPrefix("/", http.FileServer(http.Dir(cfg.RootDir)))))
|
||||
if cfg.RootDir == "" {
|
||||
cfg.RootDir = "."
|
||||
}
|
||||
|
||||
if cfg.Port == 0 {
|
||||
cfg.Port = 8080
|
||||
cfg.Port = consts.DefaultFileserverPort
|
||||
}
|
||||
|
||||
if cfg.Timeout == 0 {
|
||||
cfg.Timeout = consts.DefaultFileserverTimeout
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Handler: r,
|
||||
Addr: fmt.Sprintf(":%d", cfg.Port),
|
||||
WriteTimeout: 15 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: time.Duration(cfg.Timeout) * time.Second,
|
||||
ReadTimeout: time.Duration(cfg.Timeout) * time.Second,
|
||||
}
|
||||
|
||||
return srv, nil
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"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"
|
||||
)
|
||||
@@ -22,14 +21,6 @@ func NewRegistry(ctx context.Context, cfg *configuration.Configuration) (*regist
|
||||
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
|
||||
}
|
||||
|
||||
@@ -45,9 +36,9 @@ func NewTempRegistry(ctx context.Context, root string) *tmpRegistryServer {
|
||||
"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"},
|
||||
|
||||
@@ -2,4 +2,5 @@ package server
|
||||
|
||||
type Server interface {
|
||||
ListenAndServe() error
|
||||
ListenAndServeTLS(string, string) error
|
||||
}
|
||||
|
||||
@@ -28,10 +28,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/common-nighthawk/go-figure"
|
||||
"hauler.dev/go/hauler/pkg/consts"
|
||||
)
|
||||
|
||||
const unknown = "unknown"
|
||||
|
||||
// Base version information.
|
||||
//
|
||||
// This is the fallback data used when version information from git is not
|
||||
@@ -41,19 +40,19 @@ var (
|
||||
// branch should be tagged using the correct versioning strategy.
|
||||
gitVersion = "devel"
|
||||
// SHA1 from git, output of $(git rev-parse HEAD)
|
||||
gitCommit = unknown
|
||||
gitCommit = consts.Unknown
|
||||
// State of git tree, either "clean" or "dirty"
|
||||
gitTreeState = unknown
|
||||
gitTreeState = consts.Unknown
|
||||
// Build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
buildDate = unknown
|
||||
buildDate = consts.Unknown
|
||||
// flag to print the ascii name banner
|
||||
asciiName = "true"
|
||||
// goVersion is the used golang version.
|
||||
goVersion = unknown
|
||||
goVersion = consts.Unknown
|
||||
// compiler is the used golang compiler.
|
||||
compiler = unknown
|
||||
compiler = consts.Unknown
|
||||
// platform is the used os/arch identifier.
|
||||
platform = unknown
|
||||
platform = consts.Unknown
|
||||
|
||||
once sync.Once
|
||||
info = Info{}
|
||||
@@ -84,7 +83,7 @@ func getBuildInfo() *debug.BuildInfo {
|
||||
|
||||
func getGitVersion(bi *debug.BuildInfo) string {
|
||||
if bi == nil {
|
||||
return unknown
|
||||
return consts.Unknown
|
||||
}
|
||||
|
||||
// TODO: remove this when the issue https://github.com/golang/go/issues/29228 is fixed
|
||||
@@ -107,28 +106,28 @@ func getDirty(bi *debug.BuildInfo) string {
|
||||
if modified == "false" {
|
||||
return "clean"
|
||||
}
|
||||
return unknown
|
||||
return consts.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 consts.Unknown
|
||||
}
|
||||
return t.Format("2006-01-02T15:04:05")
|
||||
}
|
||||
|
||||
func getKey(bi *debug.BuildInfo, key string) string {
|
||||
if bi == nil {
|
||||
return unknown
|
||||
return consts.Unknown
|
||||
}
|
||||
for _, iter := range bi.Settings {
|
||||
if iter.Key == key {
|
||||
return iter.Value
|
||||
}
|
||||
}
|
||||
return unknown
|
||||
return consts.Unknown
|
||||
}
|
||||
|
||||
// GetVersionInfo represents known information on how this binary was built.
|
||||
@@ -136,27 +135,27 @@ func GetVersionInfo() Info {
|
||||
once.Do(func() {
|
||||
buildInfo := getBuildInfo()
|
||||
gitVersion = getGitVersion(buildInfo)
|
||||
if gitCommit == unknown {
|
||||
if gitCommit == consts.Unknown {
|
||||
gitCommit = getCommit(buildInfo)
|
||||
}
|
||||
|
||||
if gitTreeState == unknown {
|
||||
if gitTreeState == consts.Unknown {
|
||||
gitTreeState = getDirty(buildInfo)
|
||||
}
|
||||
|
||||
if buildDate == unknown {
|
||||
if buildDate == consts.Unknown {
|
||||
buildDate = getBuildDate(buildInfo)
|
||||
}
|
||||
|
||||
if goVersion == unknown {
|
||||
if goVersion == consts.Unknown {
|
||||
goVersion = runtime.Version()
|
||||
}
|
||||
|
||||
if compiler == unknown {
|
||||
if compiler == consts.Unknown {
|
||||
compiler = runtime.Compiler
|
||||
}
|
||||
|
||||
if platform == unknown {
|
||||
if platform == consts.Unknown {
|
||||
platform = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
|
||||
@@ -226,4 +225,4 @@ func (i *Info) CheckFontName(fontName string) bool {
|
||||
|
||||
fmt.Fprintln(os.Stderr, "font not valid, using default")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
121
pkg/apis/hauler.cattle.io/convert/convert.go
Normal file
121
pkg/apis/hauler.cattle.io/convert/convert.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
v1 "hauler.dev/go/hauler/pkg/apis/hauler.cattle.io/v1"
|
||||
v1alpha1 "hauler.dev/go/hauler/pkg/apis/hauler.cattle.io/v1alpha1"
|
||||
)
|
||||
|
||||
// converts v1alpha1.Files -> v1.Files
|
||||
func ConvertFiles(in *v1alpha1.Files, out *v1.Files) error {
|
||||
out.TypeMeta = in.TypeMeta
|
||||
out.ObjectMeta = in.ObjectMeta
|
||||
out.Spec.Files = make([]v1.File, len(in.Spec.Files))
|
||||
for i := range in.Spec.Files {
|
||||
out.Spec.Files[i].Name = in.Spec.Files[i].Name
|
||||
out.Spec.Files[i].Path = in.Spec.Files[i].Path
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// converts v1alpha1.Images -> v1.Images
|
||||
func ConvertImages(in *v1alpha1.Images, out *v1.Images) error {
|
||||
out.TypeMeta = in.TypeMeta
|
||||
out.ObjectMeta = in.ObjectMeta
|
||||
out.Spec.Images = make([]v1.Image, len(in.Spec.Images))
|
||||
for i := range in.Spec.Images {
|
||||
out.Spec.Images[i].Name = in.Spec.Images[i].Name
|
||||
out.Spec.Images[i].Platform = in.Spec.Images[i].Platform
|
||||
out.Spec.Images[i].Key = in.Spec.Images[i].Key
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// converts v1alpha1.Charts -> v1.Charts
|
||||
func ConvertCharts(in *v1alpha1.Charts, out *v1.Charts) error {
|
||||
out.TypeMeta = in.TypeMeta
|
||||
out.ObjectMeta = in.ObjectMeta
|
||||
out.Spec.Charts = make([]v1.Chart, len(in.Spec.Charts))
|
||||
for i := range in.Spec.Charts {
|
||||
out.Spec.Charts[i].Name = in.Spec.Charts[i].Name
|
||||
out.Spec.Charts[i].RepoURL = in.Spec.Charts[i].RepoURL
|
||||
out.Spec.Charts[i].Version = in.Spec.Charts[i].Version
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// converts v1alpha1.ThickCharts -> v1.ThickCharts
|
||||
func ConvertThickCharts(in *v1alpha1.ThickCharts, out *v1.ThickCharts) error {
|
||||
out.TypeMeta = in.TypeMeta
|
||||
out.ObjectMeta = in.ObjectMeta
|
||||
out.Spec.Charts = make([]v1.ThickChart, len(in.Spec.Charts))
|
||||
for i := range in.Spec.Charts {
|
||||
out.Spec.Charts[i].Chart.Name = in.Spec.Charts[i].Chart.Name
|
||||
out.Spec.Charts[i].Chart.RepoURL = in.Spec.Charts[i].Chart.RepoURL
|
||||
out.Spec.Charts[i].Chart.Version = in.Spec.Charts[i].Chart.Version
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// converts v1alpha1.ImageTxts -> v1.ImageTxts
|
||||
func ConvertImageTxts(in *v1alpha1.ImageTxts, out *v1.ImageTxts) error {
|
||||
out.TypeMeta = in.TypeMeta
|
||||
out.ObjectMeta = in.ObjectMeta
|
||||
out.Spec.ImageTxts = make([]v1.ImageTxt, len(in.Spec.ImageTxts))
|
||||
for i := range in.Spec.ImageTxts {
|
||||
out.Spec.ImageTxts[i].Ref = in.Spec.ImageTxts[i].Ref
|
||||
out.Spec.ImageTxts[i].Sources.Include = append(
|
||||
out.Spec.ImageTxts[i].Sources.Include,
|
||||
in.Spec.ImageTxts[i].Sources.Include...,
|
||||
)
|
||||
out.Spec.ImageTxts[i].Sources.Exclude = append(
|
||||
out.Spec.ImageTxts[i].Sources.Exclude,
|
||||
in.Spec.ImageTxts[i].Sources.Exclude...,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// convert v1alpha1 object to v1 object
|
||||
func ConvertObject(in interface{}) (interface{}, error) {
|
||||
switch src := in.(type) {
|
||||
|
||||
case *v1alpha1.Files:
|
||||
dst := &v1.Files{}
|
||||
if err := ConvertFiles(src, dst); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dst, nil
|
||||
|
||||
case *v1alpha1.Images:
|
||||
dst := &v1.Images{}
|
||||
if err := ConvertImages(src, dst); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dst, nil
|
||||
|
||||
case *v1alpha1.Charts:
|
||||
dst := &v1.Charts{}
|
||||
if err := ConvertCharts(src, dst); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dst, nil
|
||||
|
||||
case *v1alpha1.ThickCharts:
|
||||
dst := &v1.ThickCharts{}
|
||||
if err := ConvertThickCharts(src, dst); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dst, nil
|
||||
|
||||
case *v1alpha1.ImageTxts:
|
||||
dst := &v1.ImageTxts{}
|
||||
if err := ConvertImageTxts(src, dst); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported object type [%T]", in)
|
||||
}
|
||||
46
pkg/apis/hauler.cattle.io/v1/chart.go
Normal file
46
pkg/apis/hauler.cattle.io/v1/chart.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
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,omitempty"`
|
||||
RepoURL string `json:"repoURL,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Rewrite string `json:"rewrite,omitempty"`
|
||||
|
||||
AddImages bool `json:"add-images,omitempty"`
|
||||
AddDependencies bool `json:"add-dependencies,omitempty"`
|
||||
}
|
||||
|
||||
type ThickCharts struct {
|
||||
*metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ThickChartSpec `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"`
|
||||
}
|
||||
17
pkg/apis/hauler.cattle.io/v1/driver.go
Normal file
17
pkg/apis/hauler.cattle.io/v1/driver.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
||||
25
pkg/apis/hauler.cattle.io/v1/file.go
Normal file
25
pkg/apis/hauler.cattle.io/v1/file.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
||||
12
pkg/apis/hauler.cattle.io/v1/groupversion_info.go
Normal file
12
pkg/apis/hauler.cattle.io/v1/groupversion_info.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"hauler.dev/go/hauler/pkg/consts"
|
||||
)
|
||||
|
||||
var (
|
||||
ContentGroupVersion = schema.GroupVersion{Group: consts.ContentGroup, Version: "v1"}
|
||||
CollectionGroupVersion = schema.GroupVersion{Group: consts.CollectionGroup, Version: "v1"}
|
||||
)
|
||||
41
pkg/apis/hauler.cattle.io/v1/image.go
Normal file
41
pkg/apis/hauler.cattle.io/v1/image.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
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 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"`
|
||||
|
||||
// Path is the path to the cosign public key used for verifying image signatures
|
||||
//Tlog string `json:"use-tlog-verify,omitempty"`
|
||||
Tlog bool `json:"use-tlog-verify"`
|
||||
|
||||
// cosign keyless validation options
|
||||
CertIdentity string `json:"certificate-identity"`
|
||||
CertIdentityRegexp string `json:"certificate-identity-regexp"`
|
||||
CertOidcIssuer string `json:"certificate-oidc-issuer"`
|
||||
CertOidcIssuerRegexp string `json:"certificate-oidc-issuer-regexp"`
|
||||
CertGithubWorkflowRepository string `json:"certificate-github-workflow-repository"`
|
||||
|
||||
// Platform of the image to be pulled. If not specified, all platforms will be pulled.
|
||||
//Platform string `json:"key,omitempty"`
|
||||
Platform string `json:"platform"`
|
||||
Rewrite string `json:"rewrite"`
|
||||
}
|
||||
26
pkg/apis/hauler.cattle.io/v1/imagetxt.go
Normal file
26
pkg/apis/hauler.cattle.io/v1/imagetxt.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
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 {
|
||||
Ref string `json:"ref,omitempty"`
|
||||
Sources ImageTxtSources `json:"sources,omitempty"`
|
||||
}
|
||||
|
||||
type ImageTxtSources struct {
|
||||
Include []string `json:"include,omitempty"`
|
||||
Exclude []string `json:"exclude,omitempty"`
|
||||
}
|
||||
@@ -4,11 +4,6 @@ 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"`
|
||||
|
||||
@@ -4,10 +4,6 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
DriverContentKind = "Driver"
|
||||
)
|
||||
|
||||
type Driver struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const FilesContentKind = "Files"
|
||||
|
||||
type Files struct {
|
||||
*metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
@@ -2,17 +2,11 @@ package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const (
|
||||
Version = "v1alpha1"
|
||||
ContentGroup = "content.hauler.cattle.io"
|
||||
CollectionGroup = "collection.hauler.cattle.io"
|
||||
"hauler.dev/go/hauler/pkg/consts"
|
||||
)
|
||||
|
||||
var (
|
||||
ContentGroupVersion = schema.GroupVersion{Group: ContentGroup, Version: Version}
|
||||
// SchemeBuilder = &scheme.Builder{GroupVersion: ContentGroupVersion}
|
||||
|
||||
CollectionGroupVersion = schema.GroupVersion{Group: CollectionGroup, Version: Version}
|
||||
ContentGroupVersion = schema.GroupVersion{Group: consts.ContentGroup, Version: "v1alpha1"}
|
||||
CollectionGroupVersion = schema.GroupVersion{Group: consts.CollectionGroup, Version: "v1alpha1"}
|
||||
)
|
||||
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const ImagesContentKind = "Images"
|
||||
|
||||
type Images struct {
|
||||
*metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
@@ -20,12 +18,24 @@ 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"`
|
||||
|
||||
// Path is the path to the cosign public key used for verifying image signatures
|
||||
//Tlog string `json:"use-tlog-verify,omitempty"`
|
||||
Tlog bool `json:"use-tlog-verify"`
|
||||
|
||||
// cosign keyless validation options
|
||||
CertIdentity string `json:"certificate-identity"`
|
||||
CertIdentityRegexp string `json:"certificate-identity-regexp"`
|
||||
CertOidcIssuer string `json:"certificate-oidc-issuer"`
|
||||
CertOidcIssuerRegexp string `json:"certificate-oidc-issuer-regexp"`
|
||||
CertGithubWorkflowRepository string `json:"certificate-github-workflow-repository"`
|
||||
|
||||
// Platform of the image to be pulled. If not specified, all platforms will be pulled.
|
||||
//Platform string `json:"key,omitempty"`
|
||||
Platform string `json:"platform"`
|
||||
Rewrite string `json:"rewrite"`
|
||||
}
|
||||
|
||||
@@ -4,10 +4,6 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
ImageTxtsContentKind = "ImageTxts"
|
||||
)
|
||||
|
||||
type ImageTxts struct {
|
||||
*metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package v1alpha1
|
||||
|
||||
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"`
|
||||
}
|
||||
104
pkg/archives/archiver.go
Normal file
104
pkg/archives/archiver.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package archives
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mholt/archives"
|
||||
"hauler.dev/go/hauler/pkg/log"
|
||||
)
|
||||
|
||||
// maps to handle compression types
|
||||
var CompressionMap = map[string]archives.Compression{
|
||||
"gz": archives.Gz{},
|
||||
"bz2": archives.Bz2{},
|
||||
"xz": archives.Xz{},
|
||||
"zst": archives.Zstd{},
|
||||
"lz4": archives.Lz4{},
|
||||
"br": archives.Brotli{},
|
||||
}
|
||||
|
||||
// maps to handle archival types
|
||||
var ArchivalMap = map[string]archives.Archival{
|
||||
"tar": archives.Tar{},
|
||||
"zip": archives.Zip{},
|
||||
}
|
||||
|
||||
// check if a path exists
|
||||
func isExist(path string) bool {
|
||||
_, statErr := os.Stat(path)
|
||||
return !os.IsNotExist(statErr)
|
||||
}
|
||||
|
||||
// archives the files in a directory
|
||||
// dir: the directory to Archive
|
||||
// outfile: the output file
|
||||
// compression: the compression to use (gzip, bzip2, etc.)
|
||||
// archival: the archival to use (tar, zip, etc.)
|
||||
func Archive(ctx context.Context, dir, outfile string, compression archives.Compression, archival archives.Archival) error {
|
||||
l := log.FromContext(ctx)
|
||||
l.Debugf("starting the archival process for [%s]", dir)
|
||||
|
||||
// remove outfile
|
||||
l.Debugf("removing existing output file: [%s]", outfile)
|
||||
if err := os.RemoveAll(outfile); err != nil {
|
||||
errMsg := fmt.Errorf("failed to remove existing output file [%s]: %w", outfile, err)
|
||||
l.Debugf(errMsg.Error())
|
||||
return errMsg
|
||||
}
|
||||
|
||||
if !isExist(dir) {
|
||||
errMsg := fmt.Errorf("directory [%s] does not exist, cannot proceed with archival", dir)
|
||||
l.Debugf(errMsg.Error())
|
||||
return errMsg
|
||||
}
|
||||
|
||||
// map files on disk to their paths in the archive
|
||||
l.Debugf("mapping files in directory [%s]", dir)
|
||||
archiveDirName := filepath.Base(filepath.Clean(dir))
|
||||
if dir == "." {
|
||||
archiveDirName = ""
|
||||
}
|
||||
files, err := archives.FilesFromDisk(context.Background(), nil, map[string]string{
|
||||
dir: archiveDirName,
|
||||
})
|
||||
if err != nil {
|
||||
errMsg := fmt.Errorf("error mapping files from directory [%s]: %w", dir, err)
|
||||
l.Debugf(errMsg.Error())
|
||||
return errMsg
|
||||
}
|
||||
l.Debugf("successfully mapped files for directory [%s]", dir)
|
||||
|
||||
// create the output file we'll write to
|
||||
l.Debugf("creating output file [%s]", outfile)
|
||||
outf, err := os.Create(outfile)
|
||||
if err != nil {
|
||||
errMsg := fmt.Errorf("error creating output file [%s]: %w", outfile, err)
|
||||
l.Debugf(errMsg.Error())
|
||||
return errMsg
|
||||
}
|
||||
defer func() {
|
||||
l.Debugf("closing output file [%s]", outfile)
|
||||
outf.Close()
|
||||
}()
|
||||
|
||||
// define the archive format
|
||||
l.Debugf("defining the archive format: [%T]/[%T]", archival, compression)
|
||||
format := archives.CompressedArchive{
|
||||
Compression: compression,
|
||||
Archival: archival,
|
||||
}
|
||||
|
||||
// create the archive
|
||||
l.Debugf("starting archive for [%s]", outfile)
|
||||
err = format.Archive(context.Background(), outf, files)
|
||||
if err != nil {
|
||||
errMsg := fmt.Errorf("error during archive creation for output file [%s]: %w", outfile, err)
|
||||
l.Debugf(errMsg.Error())
|
||||
return errMsg
|
||||
}
|
||||
l.Debugf("archive created successfully [%s]", outfile)
|
||||
return nil
|
||||
}
|
||||
158
pkg/archives/unarchiver.go
Normal file
158
pkg/archives/unarchiver.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package archives
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/archives"
|
||||
"hauler.dev/go/hauler/pkg/log"
|
||||
)
|
||||
|
||||
const (
|
||||
dirPermissions = 0o700 // default directory permissions
|
||||
filePermissions = 0o600 // default file permissions
|
||||
)
|
||||
|
||||
// ensures the path is safely relative to the target directory
|
||||
func securePath(basePath, relativePath string) (string, error) {
|
||||
relativePath = filepath.Clean("/" + relativePath)
|
||||
relativePath = strings.TrimPrefix(relativePath, string(os.PathSeparator))
|
||||
|
||||
dstPath := filepath.Join(basePath, relativePath)
|
||||
|
||||
if !strings.HasPrefix(filepath.Clean(dstPath)+string(os.PathSeparator), filepath.Clean(basePath)+string(os.PathSeparator)) {
|
||||
return "", fmt.Errorf("illegal file path: %s", dstPath)
|
||||
}
|
||||
return dstPath, nil
|
||||
}
|
||||
|
||||
// creates a directory with specified permissions
|
||||
func createDirWithPermissions(ctx context.Context, path string, mode os.FileMode) error {
|
||||
l := log.FromContext(ctx)
|
||||
l.Debugf("creating directory [%s]", path)
|
||||
if err := os.MkdirAll(path, mode); err != nil {
|
||||
return fmt.Errorf("failed to mkdir: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sets permissions to a file or directory
|
||||
func setPermissions(path string, mode os.FileMode) error {
|
||||
if err := os.Chmod(path, mode); err != nil {
|
||||
return fmt.Errorf("failed to chmod: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handles the extraction of a file from the archive.
|
||||
func handleFile(ctx context.Context, f archives.FileInfo, dst string) error {
|
||||
l := log.FromContext(ctx)
|
||||
l.Debugf("handling file [%s]", f.NameInArchive)
|
||||
|
||||
// validate and construct the destination path
|
||||
dstPath, pathErr := securePath(dst, f.NameInArchive)
|
||||
if pathErr != nil {
|
||||
return pathErr
|
||||
}
|
||||
|
||||
// ensure the parent directory exists
|
||||
parentDir := filepath.Dir(dstPath)
|
||||
if dirErr := createDirWithPermissions(ctx, parentDir, dirPermissions); dirErr != nil {
|
||||
return dirErr
|
||||
}
|
||||
|
||||
// handle directories
|
||||
if f.IsDir() {
|
||||
// create the directory with permissions from the archive
|
||||
if dirErr := createDirWithPermissions(ctx, dstPath, f.Mode()); dirErr != nil {
|
||||
return fmt.Errorf("failed to create directory: %w", dirErr)
|
||||
}
|
||||
l.Debugf("successfully created directory [%s]", dstPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ignore symlinks (or hardlinks)
|
||||
if f.LinkTarget != "" {
|
||||
l.Debugf("skipping symlink [%s] to [%s]", dstPath, f.LinkTarget)
|
||||
return nil
|
||||
}
|
||||
|
||||
// check and handle parent directory permissions
|
||||
originalMode, statErr := os.Stat(parentDir)
|
||||
if statErr != nil {
|
||||
return fmt.Errorf("failed to stat parent directory: %w", statErr)
|
||||
}
|
||||
|
||||
// if parent directory is read only, temporarily make it writable
|
||||
if originalMode.Mode().Perm()&0o200 == 0 {
|
||||
l.Debugf("parent directory is read only... temporarily making it writable [%s]", parentDir)
|
||||
if chmodErr := os.Chmod(parentDir, originalMode.Mode()|0o200); chmodErr != nil {
|
||||
return fmt.Errorf("failed to chmod parent directory: %w", chmodErr)
|
||||
}
|
||||
defer func() {
|
||||
// restore the original permissions after writing
|
||||
if chmodErr := os.Chmod(parentDir, originalMode.Mode()); chmodErr != nil {
|
||||
l.Debugf("failed to restore original permissions for [%s]: %v", parentDir, chmodErr)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// handle regular files
|
||||
reader, openErr := f.Open()
|
||||
if openErr != nil {
|
||||
return fmt.Errorf("failed to open file: %w", openErr)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
dstFile, createErr := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY, f.Mode())
|
||||
if createErr != nil {
|
||||
return fmt.Errorf("failed to create file: %w", createErr)
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
if _, copyErr := io.Copy(dstFile, reader); copyErr != nil {
|
||||
return fmt.Errorf("failed to copy: %w", copyErr)
|
||||
}
|
||||
l.Debugf("successfully extracted file [%s]", dstPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// unarchives a tarball to a directory, symlinks, and hardlinks are ignored
|
||||
func Unarchive(ctx context.Context, tarball, dst string) error {
|
||||
l := log.FromContext(ctx)
|
||||
l.Debugf("unarchiving temporary archive [%s] to temporary store [%s]", tarball, dst)
|
||||
archiveFile, openErr := os.Open(tarball)
|
||||
if openErr != nil {
|
||||
return fmt.Errorf("failed to open tarball %s: %w", tarball, openErr)
|
||||
}
|
||||
defer archiveFile.Close()
|
||||
|
||||
format, input, identifyErr := archives.Identify(context.Background(), tarball, archiveFile)
|
||||
if identifyErr != nil {
|
||||
return fmt.Errorf("failed to identify format: %w", identifyErr)
|
||||
}
|
||||
|
||||
extractor, ok := format.(archives.Extractor)
|
||||
if !ok {
|
||||
return fmt.Errorf("unsupported format for extraction")
|
||||
}
|
||||
|
||||
if dirErr := createDirWithPermissions(ctx, dst, dirPermissions); dirErr != nil {
|
||||
return fmt.Errorf("failed to create destination directory: %w", dirErr)
|
||||
}
|
||||
|
||||
handler := func(ctx context.Context, f archives.FileInfo) error {
|
||||
return handleFile(ctx, f, dst)
|
||||
}
|
||||
|
||||
if extractErr := extractor.Extract(context.Background(), input, handler); extractErr != nil {
|
||||
return fmt.Errorf("failed to extract: %w", extractErr)
|
||||
}
|
||||
|
||||
l.Infof("unarchiving completed successfully")
|
||||
return nil
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/consts"
|
||||
"hauler.dev/go/hauler/pkg/consts"
|
||||
)
|
||||
|
||||
var _ partial.Describable = (*marshallableConfig)(nil)
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"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"
|
||||
"hauler.dev/go/hauler/pkg/artifacts"
|
||||
"hauler.dev/go/hauler/pkg/consts"
|
||||
"hauler.dev/go/hauler/pkg/getter"
|
||||
)
|
||||
|
||||
// interface guard
|
||||
|
||||
@@ -13,9 +13,9 @@ import (
|
||||
|
||||
"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"
|
||||
"hauler.dev/go/hauler/pkg/artifacts/file"
|
||||
"hauler.dev/go/hauler/pkg/consts"
|
||||
"hauler.dev/go/hauler/pkg/getter"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts"
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/file/getter"
|
||||
"hauler.dev/go/hauler/pkg/artifacts"
|
||||
"hauler.dev/go/hauler/pkg/getter"
|
||||
)
|
||||
|
||||
type Option func(*File)
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
gv1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts"
|
||||
"hauler.dev/go/hauler/pkg/artifacts"
|
||||
)
|
||||
|
||||
var _ artifacts.OCI = (*Image)(nil)
|
||||
@@ -54,27 +54,27 @@ func NewImage(name string, opts ...remote.Option) (*Image, error) {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
_, 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
|
||||
}
|
||||
// If the descriptor could be converted to an image index, it's a multi-arch image
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"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"
|
||||
"hauler.dev/go/hauler/pkg/artifacts"
|
||||
"hauler.dev/go/hauler/pkg/consts"
|
||||
)
|
||||
|
||||
var _ artifacts.OCI = (*Memory)(nil)
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/opencontainers/go-digest"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/memory"
|
||||
"hauler.dev/go/hauler/pkg/artifacts/memory"
|
||||
)
|
||||
|
||||
func TestMemory_Layers(t *testing.T) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package memory
|
||||
|
||||
import "github.com/rancherfederal/hauler/pkg/artifacts"
|
||||
import "hauler.dev/go/hauler/pkg/artifacts"
|
||||
|
||||
type Option func(*Memory)
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ package artifacts
|
||||
import "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
|
||||
// This specific implementation fully encapsulates v1.Layer's within a more generic form
|
||||
//
|
||||
// At a high level, it is not constrained by an Image's config, manifests, and layer ordinality
|
||||
// This specific implementation fully encapsulates v1.Layer's within a more generic form
|
||||
type OCI interface {
|
||||
MediaType() string
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package chart
|
||||
|
||||
import (
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts"
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/image"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/apis/hauler.cattle.io/v1alpha1"
|
||||
"github.com/rancherfederal/hauler/pkg/content/chart"
|
||||
"github.com/rancherfederal/hauler/pkg/reference"
|
||||
"hauler.dev/go/hauler/pkg/apis/hauler.cattle.io/v1"
|
||||
"hauler.dev/go/hauler/pkg/artifacts"
|
||||
"hauler.dev/go/hauler/pkg/artifacts/image"
|
||||
"hauler.dev/go/hauler/pkg/content/chart"
|
||||
"hauler.dev/go/hauler/pkg/reference"
|
||||
)
|
||||
|
||||
var _ artifacts.OCICollection = (*tchart)(nil)
|
||||
@@ -15,13 +15,13 @@ var _ artifacts.OCICollection = (*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
|
||||
config v1.ThickChart
|
||||
|
||||
computed bool
|
||||
contents map[string]artifacts.OCI
|
||||
}
|
||||
|
||||
func NewThickChart(cfg v1alpha1.ThickChart, opts *action.ChartPathOptions) (artifacts.OCICollection, error) {
|
||||
func NewThickChart(cfg v1.ThickChart, opts *action.ChartPathOptions) (artifacts.OCICollection, error) {
|
||||
o, err := chart.NewChart(cfg.Chart.Name, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
"k8s.io/client-go/util/jsonpath"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/apis/hauler.cattle.io/v1alpha1"
|
||||
"hauler.dev/go/hauler/pkg/apis/hauler.cattle.io/v1"
|
||||
)
|
||||
|
||||
var defaultKnownImagePaths = []string{
|
||||
@@ -29,13 +29,13 @@ var defaultKnownImagePaths = []string{
|
||||
}
|
||||
|
||||
// ImagesInChart will render a chart and identify all dependent images from it
|
||||
func ImagesInChart(c *helmchart.Chart) (v1alpha1.Images, error) {
|
||||
func ImagesInChart(c *helmchart.Chart) (v1.Images, error) {
|
||||
docs, err := template(c)
|
||||
if err != nil {
|
||||
return v1alpha1.Images{}, err
|
||||
return v1.Images{}, err
|
||||
}
|
||||
|
||||
var images []v1alpha1.Image
|
||||
var images []v1.Image
|
||||
reader := yaml.NewYAMLReader(bufio.NewReader(strings.NewReader(docs)))
|
||||
for {
|
||||
raw, err := reader.Read()
|
||||
@@ -43,17 +43,17 @@ func ImagesInChart(c *helmchart.Chart) (v1alpha1.Images, error) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return v1alpha1.Images{}, err
|
||||
return v1.Images{}, err
|
||||
}
|
||||
|
||||
found := find(raw, defaultKnownImagePaths...)
|
||||
for _, f := range found {
|
||||
images = append(images, v1alpha1.Image{Name: f})
|
||||
images = append(images, v1.Image{Name: f})
|
||||
}
|
||||
}
|
||||
|
||||
ims := v1alpha1.Images{
|
||||
Spec: v1alpha1.ImageSpec{
|
||||
ims := v1.Images{
|
||||
Spec: v1.ImageSpec{
|
||||
Images: images,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"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"
|
||||
|
||||
artifact "hauler.dev/go/hauler/pkg/artifacts"
|
||||
"hauler.dev/go/hauler/pkg/artifacts/image"
|
||||
"hauler.dev/go/hauler/pkg/getter"
|
||||
"hauler.dev/go/hauler/pkg/log"
|
||||
)
|
||||
|
||||
type ImageTxt struct {
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts"
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/image"
|
||||
"hauler.dev/go/hauler/pkg/artifacts"
|
||||
"hauler.dev/go/hauler/pkg/artifacts/image"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
package k3s
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts"
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/image"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/file"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts/file/getter"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/reference"
|
||||
)
|
||||
|
||||
var _ artifacts.OCICollection = (*k3s)(nil)
|
||||
|
||||
const (
|
||||
releaseUrl = "https://github.com/k3s-io/k3s/releases/download"
|
||||
channelUrl = "https://update.k3s.io/v1-release/channels"
|
||||
bootstrapUrl = "https://get.k3s.io"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrImagesNotFound = errors.New("k3s dependent images not found")
|
||||
ErrFetchingImages = errors.New("failed to fetch k3s dependent images")
|
||||
ErrExecutableNotfound = errors.New("k3s executable not found")
|
||||
ErrChannelNotFound = errors.New("desired k3s channel not found")
|
||||
)
|
||||
|
||||
type k3s struct {
|
||||
version string
|
||||
arch string
|
||||
|
||||
computed bool
|
||||
contents map[string]artifacts.OCI
|
||||
channels map[string]string
|
||||
client *getter.Client
|
||||
}
|
||||
|
||||
func NewK3s(version string) (artifacts.OCICollection, error) {
|
||||
return &k3s{
|
||||
version: version,
|
||||
contents: make(map[string]artifacts.OCI),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (k *k3s) Contents() (map[string]artifacts.OCI, error) {
|
||||
if err := k.compute(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return k.contents, nil
|
||||
}
|
||||
|
||||
func (k *k3s) compute() error {
|
||||
if k.computed {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := k.fetchChannels(); err == nil {
|
||||
if version, ok := k.channels[k.version]; ok {
|
||||
k.version = version
|
||||
}
|
||||
}
|
||||
|
||||
if err := k.images(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := k.executable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := k.bootstrap(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k.computed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *k3s) executable() error {
|
||||
n := "k3s"
|
||||
if k.arch != "" && k.arch != "amd64" {
|
||||
n = fmt.Sprintf("name-%s", k.arch)
|
||||
}
|
||||
fref := k.releaseUrl(n)
|
||||
|
||||
resp, err := http.Head(fref)
|
||||
if resp.StatusCode != http.StatusOK || err != nil {
|
||||
return ErrExecutableNotfound
|
||||
}
|
||||
|
||||
f := file.NewFile(fref)
|
||||
|
||||
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))
|
||||
|
||||
ref := fmt.Sprintf("%s/k3s-init.sh:%s", reference.DefaultNamespace, reference.DefaultTag)
|
||||
k.contents[ref] = f
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *k3s) images() error {
|
||||
resp, err := http.Get(k.releaseUrl("k3s-images.txt"))
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return ErrFetchingImages
|
||||
} else if err != nil {
|
||||
return ErrImagesNotFound
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
for scanner.Scan() {
|
||||
reference := scanner.Text()
|
||||
o, err := image.NewImage(reference)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k.contents[reference] = o
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *k3s) releaseUrl(artifact string) string {
|
||||
u, _ := url.Parse(releaseUrl)
|
||||
complete := []string{u.Path}
|
||||
u.Path = path.Join(append(complete, []string{k.version, artifact}...)...)
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func (k *k3s) dnsCompliantVersion() string {
|
||||
return strings.ReplaceAll(k.version, "+", "-")
|
||||
}
|
||||
|
||||
func (k *k3s) fetchChannels() error {
|
||||
resp, err := http.Get(channelUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var c channel
|
||||
if err := json.NewDecoder(resp.Body).Decode(&c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
channels := make(map[string]string)
|
||||
for _, ch := range c.Data {
|
||||
channels[ch.Name] = ch.Latest
|
||||
}
|
||||
|
||||
k.channels = channels
|
||||
return nil
|
||||
}
|
||||
|
||||
type channel struct {
|
||||
Data []channelData `json:"data"`
|
||||
}
|
||||
|
||||
type channelData struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Latest string `json:"latest"`
|
||||
}
|
||||
@@ -1,57 +1,102 @@
|
||||
package consts
|
||||
|
||||
const (
|
||||
// container media types
|
||||
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"
|
||||
|
||||
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
|
||||
// helm chart media types
|
||||
ChartConfigMediaType = "application/vnd.cncf.helm.config.v1+json"
|
||||
ChartLayerMediaType = "application/vnd.cncf.helm.chart.content.v1.tar+gzip"
|
||||
ProvLayerMediaType = "application/vnd.cncf.helm.chart.provenance.v1.prov"
|
||||
|
||||
// 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
|
||||
// file media types
|
||||
FileLayerMediaType = "application/vnd.content.hauler.file.layer.v1"
|
||||
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
|
||||
// memory media types
|
||||
MemoryConfigMediaType = "application/vnd.content.hauler.memory.config.v1+json"
|
||||
|
||||
// WasmArtifactLayerMediaType is the reserved media type for WASM artifact layers
|
||||
// wasm media types
|
||||
WasmArtifactLayerMediaType = "application/vnd.wasm.content.layer.v1+wasm"
|
||||
WasmConfigMediaType = "application/vnd.wasm.config.v1+json"
|
||||
|
||||
// WasmConfigMediaType is the reserved media type for WASM configs
|
||||
WasmConfigMediaType = "application/vnd.wasm.config.v1+json"
|
||||
|
||||
// unknown media types
|
||||
UnknownManifest = "application/vnd.hauler.cattle.io.unknown.v1+json"
|
||||
UnknownLayer = "application/vnd.content.hauler.unknown.layer"
|
||||
Unknown = "unknown"
|
||||
|
||||
// vendor prefixes
|
||||
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"
|
||||
// annotation keys
|
||||
ContainerdImageNameKey = "io.containerd.image.name"
|
||||
KindAnnotationName = "kind"
|
||||
KindAnnotationImage = "dev.cosignproject.cosign/image"
|
||||
KindAnnotationIndex = "dev.cosignproject.cosign/imageIndex"
|
||||
ImageAnnotationKey = "hauler.dev/key"
|
||||
ImageAnnotationPlatform = "hauler.dev/platform"
|
||||
ImageAnnotationRegistry = "hauler.dev/registry"
|
||||
ImageAnnotationTlog = "hauler.dev/use-tlog-verify"
|
||||
ImageAnnotationRewrite = "hauler.dev/rewrite"
|
||||
ImageRefKey = "org.opencontainers.image.ref.name"
|
||||
|
||||
// cosign keyless validation options
|
||||
ImageAnnotationCertIdentity = "hauler.dev/certificate-identity"
|
||||
ImageAnnotationCertIdentityRegexp = "hauler.dev/certificate-identity-regexp"
|
||||
ImageAnnotationCertOidcIssuer = "hauler.dev/certificate-oidc-issuer"
|
||||
ImageAnnotationCertOidcIssuerRegexp = "hauler.dev/certificate-oidc-issuer-regexp"
|
||||
ImageAnnotationCertGithubWorkflowRepository = "hauler.dev/certificate-github-workflow-repository"
|
||||
|
||||
// content kinds
|
||||
ImagesContentKind = "Images"
|
||||
ChartsContentKind = "Charts"
|
||||
FilesContentKind = "Files"
|
||||
DriverContentKind = "Driver"
|
||||
ImageTxtsContentKind = "ImageTxts"
|
||||
ChartsCollectionKind = "ThickCharts"
|
||||
|
||||
// content groups
|
||||
ContentGroup = "content.hauler.cattle.io"
|
||||
CollectionGroup = "collection.hauler.cattle.io"
|
||||
|
||||
// environment variables
|
||||
HaulerDir = "HAULER_DIR"
|
||||
HaulerTempDir = "HAULER_TEMP_DIR"
|
||||
HaulerStoreDir = "HAULER_STORE_DIR"
|
||||
HaulerIgnoreErrors = "HAULER_IGNORE_ERRORS"
|
||||
|
||||
// container files and directories
|
||||
ImageManifestFile = "manifest.json"
|
||||
ImageConfigFile = "config.json"
|
||||
|
||||
// other constraints
|
||||
CarbideRegistry = "rgcrprod.azurecr.us"
|
||||
DefaultNamespace = "hauler"
|
||||
DefaultTag = "latest"
|
||||
DefaultStoreName = "store"
|
||||
DefaultHaulerDirName = ".hauler"
|
||||
DefaultHaulerTempDirName = "hauler"
|
||||
DefaultRegistryRootDir = "registry"
|
||||
DefaultRegistryPort = 5000
|
||||
DefaultFileserverRootDir = "fileserver"
|
||||
DefaultFileserverPort = 8080
|
||||
DefaultFileserverTimeout = 60
|
||||
DefaultHaulerArchiveName = "haul.tar.zst"
|
||||
DefaultHaulerManifestName = "hauler-manifest.yaml"
|
||||
DefaultRetries = 3
|
||||
RetriesInterval = 5
|
||||
CustomTimeFormat = "2006-01-02 15:04:05"
|
||||
)
|
||||
|
||||
@@ -5,8 +5,10 @@ import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
@@ -14,44 +16,74 @@ import (
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
gtypes "github.com/google/go-containerregistry/pkg/v1/types"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/rancherfederal/hauler/pkg/artifacts"
|
||||
"hauler.dev/go/hauler/pkg/artifacts"
|
||||
"hauler.dev/go/hauler/pkg/log"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"helm.sh/helm/v3/pkg/chart"
|
||||
"helm.sh/helm/v3/pkg/chart/loader"
|
||||
"helm.sh/helm/v3/pkg/cli"
|
||||
"helm.sh/helm/v3/pkg/registry"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/layer"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/consts"
|
||||
"hauler.dev/go/hauler/pkg/consts"
|
||||
"hauler.dev/go/hauler/pkg/layer"
|
||||
)
|
||||
|
||||
var _ artifacts.OCI = (*Chart)(nil)
|
||||
var (
|
||||
_ artifacts.OCI = (*Chart)(nil)
|
||||
settings = cli.New()
|
||||
)
|
||||
|
||||
// Chart implements the OCI interface for Chart API objects. API spec values are
|
||||
// stored into the Repo, Name, and Version fields.
|
||||
// chart implements the oci interface for chart api objects... api spec values are stored into the name, repo, and version fields
|
||||
type Chart struct {
|
||||
path string
|
||||
annotations map[string]string
|
||||
}
|
||||
|
||||
// NewChart is a helper method that returns NewLocalChart or NewRemoteChart depending on v1alpha1.Chart contents
|
||||
// newchart is a helper method that returns newlocalchart or newremotechart depending on chart contents
|
||||
func NewChart(name string, opts *action.ChartPathOptions) (*Chart, error) {
|
||||
cpo := action.ChartPathOptions{
|
||||
RepoURL: opts.RepoURL,
|
||||
Version: opts.Version,
|
||||
|
||||
CaFile: opts.CaFile,
|
||||
CertFile: opts.CertFile,
|
||||
KeyFile: opts.KeyFile,
|
||||
InsecureSkipTLSverify: opts.InsecureSkipTLSverify,
|
||||
Keyring: opts.Keyring,
|
||||
Password: opts.Password,
|
||||
PassCredentialsAll: opts.PassCredentialsAll,
|
||||
Username: opts.Username,
|
||||
Verify: opts.Verify,
|
||||
chartRef := name
|
||||
actionConfig := new(action.Configuration)
|
||||
if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), os.Getenv("HELM_DRIVER"), log.NewLogger(os.Stdout).Debugf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chartPath, err := cpo.LocateChart(name, cli.New())
|
||||
client := action.NewInstall(actionConfig)
|
||||
client.ChartPathOptions.Version = opts.Version
|
||||
|
||||
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
|
||||
client.InsecureSkipTLSverify, client.PlainHTTP)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("missing registry client: %w", err)
|
||||
}
|
||||
|
||||
client.SetRegistryClient(registryClient)
|
||||
if registry.IsOCI(opts.RepoURL) {
|
||||
chartRef = opts.RepoURL + "/" + name
|
||||
} else if isUrl(opts.RepoURL) { // oci protocol registers as a valid url
|
||||
client.ChartPathOptions.RepoURL = opts.RepoURL
|
||||
} else { // handles cases like grafana and loki
|
||||
chartRef = opts.RepoURL + "/" + name
|
||||
}
|
||||
|
||||
// suppress helm downloader oci logs (stdout/stderr)
|
||||
oldStdout := os.Stdout
|
||||
oldStderr := os.Stderr
|
||||
rOut, wOut, _ := os.Pipe()
|
||||
rErr, wErr, _ := os.Pipe()
|
||||
os.Stdout = wOut
|
||||
os.Stderr = wErr
|
||||
|
||||
chartPath, err := client.ChartPathOptions.LocateChart(chartRef, settings)
|
||||
|
||||
wOut.Close()
|
||||
wErr.Close()
|
||||
os.Stdout = oldStdout
|
||||
os.Stderr = oldStderr
|
||||
_, _ = io.Copy(io.Discard, rOut)
|
||||
_, _ = io.Copy(io.Discard, rErr)
|
||||
rOut.Close()
|
||||
rErr.Close()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -136,9 +168,8 @@ func (h *Chart) RawChartData() ([]byte, error) {
|
||||
return os.ReadFile(h.path)
|
||||
}
|
||||
|
||||
// chartData loads the chart contents into memory and returns a NopCloser for the contents
|
||||
//
|
||||
// Normally we avoid loading into memory, but charts sizes are strictly capped at ~1MB
|
||||
// chartdata loads the chart contents into memory and returns a NopCloser for the contents
|
||||
// normally we avoid loading into memory, but charts sizes are strictly capped at ~1MB
|
||||
func (h *Chart) chartData() (gv1.Layer, error) {
|
||||
info, err := os.Stat(h.path)
|
||||
if err != nil {
|
||||
@@ -217,3 +248,61 @@ func (h *Chart) chartData() (gv1.Layer, error) {
|
||||
|
||||
return chartDataLayer, err
|
||||
}
|
||||
func isUrl(name string) bool {
|
||||
_, err := url.ParseRequestURI(name)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func newRegistryClient(certFile, keyFile, caFile string, insecureSkipTLSverify, plainHTTP bool) (*registry.Client, error) {
|
||||
if certFile != "" && keyFile != "" || caFile != "" || insecureSkipTLSverify {
|
||||
registryClient, err := newRegistryClientWithTLS(certFile, keyFile, caFile, insecureSkipTLSverify)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return registryClient, nil
|
||||
}
|
||||
registryClient, err := newDefaultRegistryClient(plainHTTP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return registryClient, nil
|
||||
}
|
||||
|
||||
func newDefaultRegistryClient(plainHTTP bool) (*registry.Client, error) {
|
||||
opts := []registry.ClientOption{
|
||||
registry.ClientOptDebug(settings.Debug),
|
||||
registry.ClientOptEnableCache(true),
|
||||
registry.ClientOptWriter(io.Discard),
|
||||
registry.ClientOptCredentialsFile(settings.RegistryConfig),
|
||||
}
|
||||
if plainHTTP {
|
||||
opts = append(opts, registry.ClientOptPlainHTTP())
|
||||
}
|
||||
|
||||
// create a new registry client
|
||||
registryClient, err := registry.NewClient(opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return registryClient, nil
|
||||
}
|
||||
|
||||
func newRegistryClientWithTLS(certFile, keyFile, caFile string, insecureSkipTLSverify bool) (*registry.Client, error) {
|
||||
// create a new registry client
|
||||
registryClient, err := registry.NewRegistryClientWithTLS(
|
||||
io.Discard,
|
||||
certFile, keyFile, caFile,
|
||||
insecureSkipTLSverify,
|
||||
settings.RegistryConfig,
|
||||
settings.Debug,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return registryClient, nil
|
||||
}
|
||||
|
||||
// path returns the local filesystem path to the chart archive or directory
|
||||
func (h *Chart) Path() string {
|
||||
return h.path
|
||||
}
|
||||
|
||||
@@ -6,29 +6,19 @@ import (
|
||||
"testing"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/mholt/archiver/v3"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/consts"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/content/chart"
|
||||
)
|
||||
|
||||
var (
|
||||
chartpath = "../../../testdata/rancher-cluster-templates-0.4.4.tgz"
|
||||
"hauler.dev/go/hauler/pkg/consts"
|
||||
"hauler.dev/go/hauler/pkg/content/chart"
|
||||
)
|
||||
|
||||
func TestNewChart(t *testing.T) {
|
||||
tmpdir, err := os.MkdirTemp("", "hauler")
|
||||
tempDir, err := os.MkdirTemp("", "hauler")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
if err := archiver.Unarchive(chartpath, tmpdir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
type args struct {
|
||||
name string
|
||||
@@ -43,18 +33,18 @@ func TestNewChart(t *testing.T) {
|
||||
{
|
||||
name: "should create from a chart archive",
|
||||
args: args{
|
||||
name: chartpath,
|
||||
opts: &action.ChartPathOptions{},
|
||||
name: "rancher-cluster-templates-0.5.2.tgz",
|
||||
opts: &action.ChartPathOptions{RepoURL: "../../../testdata"},
|
||||
},
|
||||
want: v1.Descriptor{
|
||||
MediaType: consts.ChartLayerMediaType,
|
||||
Size: 13102,
|
||||
Size: 14970,
|
||||
Digest: v1.Hash{
|
||||
Algorithm: "sha256",
|
||||
Hex: "4b3bb4e474b54bf9057b298f8f11c239bb561396716d8cd5fc369c407fba2965",
|
||||
Hex: "0905de044a6e57cf3cd27bfc8482753049920050b10347ae2315599bd982a0e3",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
ocispec.AnnotationTitle: "rancher-cluster-templates-0.4.4.tgz",
|
||||
ocispec.AnnotationTitle: "rancher-cluster-templates-0.5.2.tgz",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
@@ -63,7 +53,7 @@ func TestNewChart(t *testing.T) {
|
||||
// {
|
||||
// name: "should create from a chart directory",
|
||||
// args: args{
|
||||
// path: filepath.Join(tmpdir, "podinfo"),
|
||||
// path: filepath.Join(tempDir, "podinfo"),
|
||||
// },
|
||||
// want: want,
|
||||
// wantErr: false,
|
||||
@@ -73,17 +63,17 @@ func TestNewChart(t *testing.T) {
|
||||
name: "should fetch a remote chart",
|
||||
args: args{
|
||||
name: "cert-manager",
|
||||
opts: &action.ChartPathOptions{RepoURL: "https://charts.jetstack.io", Version: "1.14.4"},
|
||||
opts: &action.ChartPathOptions{RepoURL: "https://charts.jetstack.io", Version: "1.15.3"},
|
||||
},
|
||||
want: v1.Descriptor{
|
||||
MediaType: consts.ChartLayerMediaType,
|
||||
Size: 80674,
|
||||
Size: 94751,
|
||||
Digest: v1.Hash{
|
||||
Algorithm: "sha256",
|
||||
Hex: "5775fdbc1881d6e510df76d38753af54b86bd14caa8edb28fdbb79527042dede",
|
||||
Hex: "016e68d9f7083d2c4fd302f951ee6490dbf4cb1ef44cfc06914c39cbfb01d858",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
ocispec.AnnotationTitle: "cert-manager-v1.14.4.tgz",
|
||||
ocispec.AnnotationTitle: "cert-manager-v1.15.3.tgz",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
|
||||
@@ -7,17 +7,31 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
|
||||
"github.com/rancherfederal/hauler/pkg/apis/hauler.cattle.io/v1alpha1"
|
||||
v1 "hauler.dev/go/hauler/pkg/apis/hauler.cattle.io/v1"
|
||||
v1alpha1 "hauler.dev/go/hauler/pkg/apis/hauler.cattle.io/v1alpha1"
|
||||
)
|
||||
|
||||
func Load(data []byte) (schema.ObjectKind, error) {
|
||||
var tm metav1.TypeMeta
|
||||
if err := yaml.Unmarshal(data, &tm); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to parse manifest: %w", err)
|
||||
}
|
||||
|
||||
if tm.GroupVersionKind().GroupVersion() != v1alpha1.ContentGroupVersion && tm.GroupVersionKind().GroupVersion() != v1alpha1.CollectionGroupVersion {
|
||||
return nil, fmt.Errorf("unrecognized content/collection type: %s", tm.GroupVersionKind().String())
|
||||
if tm.APIVersion == "" {
|
||||
return nil, fmt.Errorf("missing required manifest field [apiVersion]")
|
||||
}
|
||||
|
||||
if tm.Kind == "" {
|
||||
return nil, fmt.Errorf("missing required manifest field [kind]")
|
||||
}
|
||||
|
||||
gv := tm.GroupVersionKind().GroupVersion()
|
||||
// allow v1 and v1alpha1 content/collection
|
||||
if gv != v1.ContentGroupVersion &&
|
||||
gv != v1.CollectionGroupVersion &&
|
||||
gv != v1alpha1.ContentGroupVersion &&
|
||||
gv != v1alpha1.CollectionGroupVersion {
|
||||
return nil, fmt.Errorf("unrecognized content/collection [%s] with [kind=%s]", tm.APIVersion, tm.Kind)
|
||||
}
|
||||
|
||||
return &tm, nil
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user