Compare commits

..

28 Commits

Author SHA1 Message Date
Adam Martin
d1499b7738 Merge pull request #164 from amartin120/cosign-updates
Add `--platform` flag to image processes and RGS flavored cosign setup improvement.
2024-01-29 14:46:18 -05:00
Adam Martin
27acb239e4 clean up makefile 2024-01-29 13:41:53 -05:00
Adam Martin
e8d084847d remove extra debug statement
Signed-off-by: Adam Martin <adam.martin@rancherfederal.com>
2024-01-28 21:15:27 -05:00
Adam Martin
e70379870f another fix for the unit test gh action
Signed-off-by: Adam Martin <adam.martin@rancherfederal.com>
2024-01-28 19:51:22 -05:00
Adam Martin
a05d21c052 add platform flag for image add and sync
Signed-off-by: Adam Martin <adam.martin@rancherfederal.com>
2024-01-28 19:48:16 -05:00
Adam Martin
8256aa55ce adjust unit test gh action for latest updates 2024-01-28 19:46:55 -05:00
Adam Martin
0e6c3690b1 bump cosign version to v2.2.2+carbide.2 2024-01-28 19:45:05 -05:00
Adam Martin
a977cec50c improve cosign setup
Signed-off-by: Adam Martin <adam.martin@rancherfederal.com>
2024-01-28 12:08:31 -05:00
Adam Martin
5edc96d152 Merge pull request #162 from zackbradys/main
updated archive default name
2024-01-24 09:19:48 -05:00
Zack Hodgson Brady
fbafa60da5 updated archive default name 2024-01-23 22:49:20 -05:00
Adam Martin
cc917af0f2 Merge pull request #159 from amartin120/store-fileserver
Store fileserver
2024-01-22 15:12:45 -05:00
Adam Martin
f76160d8be Merge pull request #160 from amartin120/add-license
add license file
2024-01-22 15:12:05 -05:00
Adam Martin
b24b25d557 add license file 2024-01-22 15:06:09 -05:00
Adam Martin
d9e298b725 adjust to make registry and fileserver subcommands 2024-01-22 13:40:58 -05:00
Adam Martin
e14453f730 add fileserver option for store serve 2024-01-22 11:31:46 -05:00
Zack Brady
990ade9cd0 merge pull request #152 from zackbradys/main
updated readme and hauler `install.sh`
2023-12-20 19:56:57 -05:00
Zack Hodgson Brady
aecd37d192 added homebrew install instructions 2023-12-20 19:46:55 -05:00
Zack Brady
02f4946ead Merge branch 'rancherfederal:main' into main 2023-12-20 00:31:44 -05:00
Zack Hodgson Brady
978dc659f8 updated hauler version and automated default version 2023-12-19 21:24:04 -05:00
Adam Martin
f982f51d57 Merge pull request #150 from amartin120/info-type-filter
add simple type filter to store info
2023-12-19 13:07:46 -05:00
Adam Martin
2174e96f0e add simple type filter to store info
Signed-off-by: Adam Martin <adam.martin@rancherfederal.com>
2023-12-19 09:59:06 -05:00
Adam Martin
8cfe4432fc Merge pull request #149 from amartin120/registry-serve-fix
fix for validating foreign blobs
2023-12-18 15:51:38 -05:00
Adam Martin
f129484224 Merge pull request #148 from amartin120/fix-chart-tags
fix for charts with a + in the version
2023-12-18 15:51:20 -05:00
Adam Martin
4dbff83459 fix for validating foreign blobs
Signed-off-by: Adam Martin <adam.martin@rancherfederal.com>
2023-12-18 15:27:32 -05:00
Adam Martin
e229c2a1da fix for chart tags with a +
Signed-off-by: Adam Martin <adam.martin@rancherfederal.com>
2023-12-15 16:17:34 -05:00
Zack Brady
2a93e74b62 merge pull request #147 from zackbradys/main
updated/fixed install.sh
2023-12-14 23:36:53 -05:00
Zack Hodgson Brady
4d5d9eda7b updated readme for hauler install 2023-12-14 23:05:01 -05:00
Zack Hodgson Brady
a7cbfcb042 updated/fixed hauler install.sh 2023-12-14 23:04:36 -05:00
20 changed files with 477 additions and 263 deletions

View File

@@ -24,7 +24,7 @@ jobs:
with:
distribution: goreleaser
version: latest
args: release --rm-dist
args: release --rm-dist -p 1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}

View File

@@ -27,6 +27,8 @@ jobs:
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() }}

3
.gitignore vendored
View File

@@ -27,4 +27,5 @@ dist/
tmp/
bin/
/store/
/registry/
/registry/
cmd/hauler/binaries

View File

@@ -3,9 +3,11 @@ before:
hooks:
- go mod tidy
- go mod download
- rm -rf cmd/hauler/binaries
env:
- vpkg=github.com/rancherfederal/hauler/internal/version
- cosign_version=v2.2.2+carbide.2
builds:
- main: cmd/hauler/main.go
@@ -18,6 +20,12 @@ 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

177
LICENSE Normal file
View File

@@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@@ -1,23 +1,27 @@
SHELL:=/bin/bash
GO_BUILD_ENV=GOOS=linux GOARCH=amd64
GO_FILES=$(shell go list ./... | grep -v /vendor/)
BUILD_VERSION=$(shell cat VERSION)
BUILD_TAG=$(BUILD_VERSION)
COSIGN_VERSION=v2.2.2+carbide.2
.SILENT:
all: fmt vet install test
build:
rm -rf cmd/hauler/binaries;\
mkdir -p cmd/hauler/binaries;\
wget -P cmd/hauler/binaries/ https://github.com/rancher-government-carbide/cosign/releases/download/$(COSIGN_VERSION)/cosign-$(shell go env GOOS)-$(shell go env GOARCH);\
mkdir bin;\
GOENV=GOARCH=$(uname -m) CGO_ENABLED=0 go build -o bin ./cmd/...;\
CGO_ENABLED=0 go build -o bin ./cmd/...;\
build-all: fmt vet
goreleaser build --rm-dist --snapshot
install:
GOENV=GOARCH=$(uname -m) CGO_ENABLED=0 go install ./cmd/...;\
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)

View File

@@ -14,20 +14,19 @@ For more information, please review the **[Hauler Documentation](https://rancher
### Linux/Darwin
```bash
# install latest release
curl -sfL https://get.hauler.dev | sh
# install specific release
curl -sfL https://get.hauler.dev | HAULER_VERSION=0.4.0 sh
```
### Windows
```bash
# coming soon
# installs latest release
curl -sfL https://get.hauler.dev | bash
```
### Homebrew
```bash
# installs latest release
brew tap rancherfederal/homebrew-tap
brew install hauler
```
### Windows
```bash
# coming soon
```

View File

@@ -3,6 +3,7 @@ package cli
import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/pkg/action"
"fmt"
"github.com/rancherfederal/hauler/cmd/hauler/cli/store"
)
@@ -107,12 +108,28 @@ func addStoreLoad() *cobra.Command {
}
func addStoreServe() *cobra.Command {
o := &store.ServeOpts{RootOpts: rootStoreOpts}
cmd := &cobra.Command{
Use: "serve",
Short: "Expose the content of a local store through an OCI compliant server",
Short: "Expose the content of a local store through an OCI compliant registry or file server",
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
cmd.AddCommand(
addStoreServeRegistry(),
addStoreServeFiles(),
)
return cmd
}
// RegistryCmd serves the embedded registry
func addStoreServeRegistry() *cobra.Command {
o := &store.ServeRegistryOpts{RootOpts: rootStoreOpts}
cmd := &cobra.Command{
Use: "registry",
Short: "Serve the embedded registry",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
s, err := o.Store(ctx)
@@ -120,12 +137,36 @@ func addStoreServe() *cobra.Command {
return err
}
return store.ServeCmd(ctx, o, s)
},
}
o.AddFlags(cmd)
return store.ServeRegistryCmd(ctx, o, s)
},
}
return cmd
o.AddFlags(cmd)
return cmd
}
// FileServerCmd serves the file server
func addStoreServeFiles() *cobra.Command {
o := &store.ServeFilesOpts{RootOpts: rootStoreOpts}
cmd := &cobra.Command{
Use: "fileserver",
Short: "Serve the file server",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
s, err := o.Store(ctx)
if err != nil {
return err
}
return store.ServeFilesCmd(ctx, o, s)
},
}
o.AddFlags(cmd)
return cmd
}
func addStoreSave() *cobra.Command {
@@ -155,6 +196,8 @@ func addStoreSave() *cobra.Command {
func addStoreInfo() *cobra.Command {
o := &store.InfoOpts{RootOpts: rootStoreOpts}
var allowedValues = []string{"image", "chart", "file", "all"}
cmd := &cobra.Command{
Use: "info",
Short: "Print out information about the store",
@@ -167,8 +210,13 @@ func addStoreInfo() *cobra.Command {
if err != nil {
return err
}
return store.InfoCmd(ctx, o, s)
for _, allowed := range allowedValues {
if o.TypeFilter == allowed {
return store.InfoCmd(ctx, o, s)
}
}
return fmt.Errorf("type must be one of %v", allowedValues)
},
}
o.AddFlags(cmd)

View File

@@ -61,13 +61,15 @@ func storeFile(ctx context.Context, s *store.Layout, fi v1alpha1.File) error {
type AddImageOpts struct {
*RootOpts
Name string
Key string
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 {
@@ -86,10 +88,10 @@ func AddImageCmd(ctx context.Context, o *AddImageOpts, s *store.Layout, referenc
l.Infof("signature verified for image [%s]", cfg.Name)
}
return storeImage(ctx, s, cfg)
return storeImage(ctx, s, cfg, o.Platform)
}
func storeImage(ctx context.Context, s *store.Layout, i v1alpha1.Image) error {
func storeImage(ctx context.Context, s *store.Layout, i v1alpha1.Image, platform string) error {
l := log.FromContext(ctx)
r, err := name.ParseReference(i.Name)
@@ -97,7 +99,7 @@ func storeImage(ctx context.Context, s *store.Layout, i v1alpha1.Image) error {
return err
}
err = cosign.SaveImage(ctx, s, r.Name())
err = cosign.SaveImage(ctx, s, r.Name(), platform)
if err != nil {
return err
}

View File

@@ -22,6 +22,7 @@ type InfoOpts struct {
*RootOpts
OutputFormat string
TypeFilter string
SizeUnit string
}
@@ -29,6 +30,7 @@ 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)")
// TODO: Regex/globbing
}
@@ -64,14 +66,14 @@ func InfoCmd(ctx context.Context, o *InfoOpts, s *store.Layout) error {
return err
}
i := newItem(s, desc, internalManifest, internalDesc.Platform.Architecture)
i := newItem(s, desc, internalManifest, fmt.Sprintf("%s/%s", internalDesc.Platform.OS, internalDesc.Platform.Architecture), o)
var emptyItem item
if i != emptyItem {
items = append(items, i)
}
}
// handle single arch docker images
} else if desc.MediaType == consts.DockerManifestSchema2 {
// 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
@@ -88,11 +90,19 @@ func InfoCmd(ctx context.Context, o *InfoOpts, s *store.Layout) error {
if err := json.NewDecoder(rc).Decode(&internalManifest); err != nil {
return err
}
i := newItem(s, desc, m, internalManifest.Architecture)
var emptyItem item
if i != emptyItem {
items = append(items, i)
if internalManifest.Architecture != "" {
i := newItem(s, desc, m, fmt.Sprintf("%s/%s", internalManifest.OS, internalManifest.Architecture), o)
var emptyItem item
if i != emptyItem {
items = append(items, i)
}
} else {
i := newItem(s, desc, m, "-", o)
var emptyItem item
if i != emptyItem {
items = append(items, i)
}
}
// handle the rest
} else {
@@ -101,7 +111,7 @@ func InfoCmd(ctx context.Context, o *InfoOpts, s *store.Layout) error {
return err
}
i := newItem(s, desc, m, "-")
i := newItem(s, desc, m, "-", o)
var emptyItem item
if i != emptyItem {
items = append(items, i)
@@ -130,7 +140,7 @@ func InfoCmd(ctx context.Context, o *InfoOpts, s *store.Layout) error {
func buildTable(items ...item) {
// Create a table for the results
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Reference", "Type", "Arch", "# Layers", "Size"})
table.SetHeader([]string{"Reference", "Type", "Platform", "# Layers", "Size"})
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetRowLine(false)
table.SetAutoMergeCellsByColumnIndex([]int{0})
@@ -140,7 +150,7 @@ func buildTable(items ...item) {
row := []string{
i.Reference,
i.Type,
i.Architecture,
i.Platform,
fmt.Sprintf("%d", i.Layers),
i.Size,
}
@@ -161,7 +171,7 @@ func buildJson(item ...item) string {
type item struct {
Reference string
Type string
Architecture string
Platform string
Layers int
Size string
}
@@ -172,12 +182,12 @@ func (a byReferenceAndArch) Len() int { return len(a) }
func (a byReferenceAndArch) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byReferenceAndArch) Less(i, j int) bool {
if a[i].Reference == a[j].Reference {
return a[i].Architecture < a[j].Architecture
return a[i].Platform < a[j].Platform
}
return a[i].Reference < a[j].Reference
}
func newItem(s *store.Layout, desc ocispec.Descriptor, m ocispec.Manifest, arch string) item {
func newItem(s *store.Layout, desc ocispec.Descriptor, m ocispec.Manifest, plat string, o *InfoOpts) item {
// skip listing cosign items
if desc.Annotations["kind"] == "dev.cosignproject.cosign/atts" ||
desc.Annotations["kind"] == "dev.cosignproject.cosign/sigs" ||
@@ -208,10 +218,14 @@ func newItem(s *store.Layout, desc ocispec.Descriptor, m ocispec.Manifest, arch
return item{}
}
if o.TypeFilter != "all" && ctype != o.TypeFilter {
return item{}
}
return item{
Reference: ref.Name(),
Type: ctype,
Architecture: arch,
Platform: plat,
Layers: len(m.Layers),
Size: byteCountSI(size),
}

View File

@@ -19,7 +19,7 @@ type SaveOpts struct {
func (o *SaveOpts) AddArgs(cmd *cobra.Command) {
f := cmd.Flags()
f.StringVarP(&o.FileName, "filename", "f", "pkg.tar.zst", "Name of archive")
f.StringVarP(&o.FileName, "filename", "f", "haul.tar.zst", "Name of archive")
}
// SaveCmd

View File

@@ -17,30 +17,29 @@ import (
"github.com/rancherfederal/hauler/pkg/store"
"github.com/rancherfederal/hauler/internal/server"
"github.com/rancherfederal/hauler/pkg/log"
)
type ServeOpts struct {
type ServeRegistryOpts struct {
*RootOpts
Port int
RootDir string
ConfigFile string
Daemon bool
storedir string
}
func (o *ServeOpts) AddFlags(cmd *cobra.Command) {
func (o *ServeRegistryOpts) AddFlags(cmd *cobra.Command) {
f := cmd.Flags()
f.IntVarP(&o.Port, "port", "p", 5000, "Port to listen on")
f.StringVar(&o.RootDir, "directory", "registry", "Directory to use for registry backend (defaults to '$PWD/registry')")
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")
f.BoolVarP(&o.Daemon, "daemon", "d", false, "Toggle serving as a daemon")
}
// ServeCmd serves the embedded registry almost identically to how distribution/v3 does it
func ServeCmd(ctx context.Context, o *ServeOpts, s *store.Layout) error {
func ServeRegistryCmd(ctx context.Context, o *ServeRegistryOpts, s *store.Layout) error {
l := log.FromContext(ctx)
ctx = dcontext.WithVersion(ctx, version.Version)
tr := server.NewTempRegistry(ctx, o.RootDir)
@@ -55,7 +54,7 @@ func ServeCmd(ctx context.Context, o *ServeOpts, s *store.Layout) error {
tr.Close()
cfg := o.defaultConfig()
cfg := o.defaultRegistryConfig()
if o.ConfigFile != "" {
ucfg, err := loadConfig(o.ConfigFile)
if err != nil {
@@ -64,14 +63,59 @@ func ServeCmd(ctx context.Context, o *ServeOpts, s *store.Layout) error {
cfg = ucfg
}
l.Infof("starting registry on port [%d]", o.Port)
r, err := server.NewRegistry(ctx, cfg)
if err != nil {
return err
}
if err = r.ListenAndServe(); err != nil {
return err
}
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", "store-files", "Directory to use for backend. Defaults to $PWD/store-files")
}
func ServeFilesCmd(ctx context.Context, o *ServeFilesOpts, s *store.Layout) error {
l := log.FromContext(ctx)
ctx = dcontext.WithVersion(ctx, version.Version)
opts := &CopyOpts{}
if err := CopyCmd(ctx, opts, s, "dir://"+o.RootDir); err != nil {
return err
}
cfg := server.FileConfig{
Root: o.RootDir,
Port: o.Port,
}
f, err := server.NewFile(ctx, cfg)
if err != nil {
return err
}
l.Infof("starting file server on port [%d]", o.Port)
if err := f.ListenAndServe(); err != nil {
return err
}
return nil
}
@@ -84,7 +128,7 @@ func loadConfig(filename string) (*configuration.Configuration, error) {
return configuration.Parse(f)
}
func (o *ServeOpts) defaultConfig() *configuration.Configuration {
func (o *ServeRegistryOpts) defaultRegistryConfig() *configuration.Configuration {
cfg := &configuration.Configuration{
Version: "0.1",
Storage: configuration.Storage{
@@ -95,6 +139,10 @@ func (o *ServeOpts) defaultConfig() *configuration.Configuration {
// "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{

View File

@@ -29,6 +29,7 @@ type SyncOpts struct {
ContentFiles []string
Key string
Products []string
Platform string
}
func (o *SyncOpts) AddFlags(cmd *cobra.Command) {
@@ -37,6 +38,7 @@ func (o *SyncOpts) AddFlags(cmd *cobra.Command) {
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.")
}
func SyncCmd(ctx context.Context, o *SyncOpts, s *store.Layout) error {
@@ -52,7 +54,7 @@ func SyncCmd(ctx context.Context, o *SyncOpts, s *store.Layout) error {
img := v1alpha1.Image{
Name: manifestLoc,
}
err := storeImage(ctx, s, img)
err := storeImage(ctx, s, img, o.Platform)
if err != nil {
return err
}
@@ -154,8 +156,14 @@ func processContent(ctx context.Context, fi *os.File, o *SyncOpts, s *store.Layo
}
l.Infof("signature verified for image [%s]", i.Name)
}
err = storeImage(ctx, s, i)
// Check if the user provided a platform.
platform := o.Platform
if i.Platform != "" {
platform = i.Platform
}
err = storeImage(ctx, s, i, platform)
if err != nil {
return err
}

View File

@@ -3,11 +3,16 @@ package main
import (
"context"
"os"
"embed"
"github.com/rancherfederal/hauler/cmd/hauler/cli"
"github.com/rancherfederal/hauler/pkg/cosign"
"github.com/rancherfederal/hauler/pkg/log"
)
//go:embed binaries/*
var binaries embed.FS
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -15,6 +20,11 @@ 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)
}
if err := cli.New().ExecuteContext(ctx); err != nil {
logger.Errorf("%v", err)
}

45
install.sh Normal file → Executable file
View File

@@ -1,15 +1,15 @@
#!/bin/bash
# Usage:
# - curl -sfL... | ENV_VAR=... sh
# - ENV_VAR=... sh ./install.sh
# - curl -sfL... | ENV_VAR=... bash
# - ENV_VAR=... bash ./install.sh
# - ./install.sh ENV_VAR=...
# Example:
# Install Latest Release
# - curl -sfL https://get.hauler.dev | sh
# - curl -sfL https://get.hauler.dev | bash
# Install Specific Release
# - curl -sfL https://get.hauler.dev | HAULER_VERSION=0.4.0 sh
# - curl -sfL https://get.hauler.dev | HAULER_VERSION=0.4.2 bash
# Documentation:
# - https://hauler.dev
@@ -34,9 +34,7 @@ function fatal {
}
# check for required dependencies
dependencies=("curl" "awk" "openssl" "tar" "sudo" "mv" "rm")
for cmd in "${dependencies[@]}"; do
for cmd in curl sed awk openssl tar rm; do
if ! command -v "$cmd" &> /dev/null; then
fatal "$cmd is not installed"
fi
@@ -46,7 +44,7 @@ done
info "Starting Installation..."
# set version with an environment variable
version=${HAULER_VERSION:-0.4.0}
version=${HAULER_VERSION:-$(curl -s https://api.github.com/repos/rancherfederal/hauler/releases/latest | grep '"tag_name":' | sed 's/.*"v\([^"]*\)".*/\1/')}
# set verision with an argument
while [[ $# -gt 0 ]]; do
@@ -109,36 +107,41 @@ info "Starting Checksum Verification..."
# Verify the Hauler checksum
expected_checksum=$(awk -v version="$version" -v platform="$platform" -v arch="$arch" '$2 == "hauler_"version"_"platform"_"arch".tar.gz" {print $1}' "hauler_${version}_checksums.txt")
determined_checksum=$(openssl dgst -sha256 "hauler_${version}_${platform}_${arch}.tar.gz" | awk '{print $2}')
if [ -z "$expected_checksum" ]; then
fatal "Failed to Locate Checksum: hauler_${version}_${platform}_${arch}.tar.gz"
elif [ "$determined_checksum" = "$expected_checksum" ]; then
verbose "- Expected Checksum: $expected_checksum"
verbose "- Determined Checksum: $determined_checksum"
verbose "- Successfully Verified Checksum: hauler_${version}_${platform}_${arch}.tar.gz"
else
verbose "- Expected: $expected_checksum"
verbose "- Determined: $determined_checksum"
fatal "Failed Checksum Verification: hauler_${version}_${platform}_${arch}.tar.gz"
fi
determined_checksum=$(openssl dgst -sha256 "hauler_${version}_${platform}_${arch}.tar.gz" | awk '{print $2}')
if [ "$determined_checksum" != "$expected_checksum" ]; then
fatal "Failed to Verify Checksum: Expected: $expected_checksum - Determined: $determined_checksum"
fi
# hauler checksum verified
verbose "- Successfully Verified Checksum"
# uncompress the archive
tar -xzf "hauler_${version}_${platform}_${arch}.tar.gz" || fatal "Failed to Extract: hauler_${version}_${platform}_${arch}.tar.gz"
# install the binary
case "$platform" in
linux)
sudo mv hauler /usr/local/bin || fatal "Failed to Move: hauler to /usr/local/bin"
install hauler /usr/local/bin || fatal "Failed to Install Hauler to /usr/local/bin"
;;
darwin)
sudo mv hauler /usr/local/bin || fatal "Failed to Move: hauler to /usr/local/bin"
install hauler /usr/local/bin || fatal "Failed to Install Hauler to /usr/local/bin"
;;
*)
fatal "Unsupported Platform/Architecture: $platform/$arch"
fatal "Unsupported Platform or Architecture: $platform/$arch"
;;
esac
# clean up the files
rm "hauler_${version}_checksums.txt" "hauler_${version}_${platform}_${arch}.tar.gz" || warn "Failed to Remove: hauler_${version}_checksums.txt hauler_${version}_${platform}_${arch}.tar.gz"
# clean up checksum(s)
rm -rf "hauler_${version}_checksums.txt" || warn "Failed to Remove: hauler_${version}_checksums.txt"
# clean up archive file(s)
rm -rf "hauler_${version}_${platform}_${arch}.tar.gz" || warn "Failed to Remove: hauler_${version}_${platform}_${arch}.tar.gz"
# display success message
info "Successfully Installed at /usr/local/bin/hauler"

View File

@@ -21,8 +21,7 @@ type FileConfig struct {
// TODO: Better configs
func NewFile(ctx context.Context, cfg FileConfig) (Server, error) {
r := mux.NewRouter()
r.Handle("/", handlers.LoggingHandler(os.Stdout, http.FileServer(http.Dir(cfg.Root))))
r.PathPrefix("/").Handler(handlers.LoggingHandler(os.Stdout, http.StripPrefix("/", http.FileServer(http.Dir(cfg.Root)))))
if cfg.Root == "" {
cfg.Root = "."
}

View File

@@ -45,6 +45,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"},

View File

@@ -24,4 +24,8 @@ type Image struct {
// Path is the path to the cosign public key used for verifying image signatures
//Key string `json:"key,omitempty"`
Key string `json:"key"`
// Platform of the image to be pulled. If not specified, all platforms will be pulled.
//Platform string `json:"key,omitempty"`
Platform string `json:"platform"`
}

View File

@@ -2,28 +2,20 @@ package cosign
import (
"fmt"
"io"
"net/http"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"context"
"strings"
"encoding/json"
"time"
"bufio"
"embed"
"strings"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/pkg/content"
"github.com/rancherfederal/hauler/pkg/store"
"github.com/rancherfederal/hauler/pkg/log"
"github.com/rancherfederal/hauler/internal/mapper"
"github.com/rancherfederal/hauler/pkg/reference"
"github.com/rancherfederal/hauler/pkg/artifacts/file"
"github.com/rancherfederal/hauler/pkg/artifacts/file/getter"
"github.com/rancherfederal/hauler/pkg/apis/hauler.cattle.io/v1alpha1"
)
const maxRetries = 3
@@ -32,7 +24,7 @@ const retryDelay = time.Second * 5
// VerifyFileSignature verifies the digital signature of a file using Sigstore/Cosign.
func VerifySignature(ctx context.Context, s *store.Layout, keyPath string, ref string) error {
operation := func() error {
cosignBinaryPath, err := ensureCosignBinary(ctx, s)
cosignBinaryPath, err := getCosignPath(ctx)
if err != nil {
return err
}
@@ -50,17 +42,31 @@ func VerifySignature(ctx context.Context, s *store.Layout, keyPath string, ref s
}
// SaveImage saves image and any signatures/attestations to the store.
func SaveImage(ctx context.Context, s *store.Layout, ref string) error {
func SaveImage(ctx context.Context, s *store.Layout, ref string, platform string) error {
operation := func() error {
cosignBinaryPath, err := ensureCosignBinary(ctx, s)
cosignBinaryPath, err := getCosignPath(ctx)
if err != nil {
return err
}
cmd := exec.Command(cosignBinaryPath, "save", ref, "--dir", s.Root)
// Conditionally add platform.
if platform != "" {
cmd.Args = append(cmd.Args, "--platform", platform)
}
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("error adding image to store: %v, output: %s", err, output)
if strings.Contains(string(output), "specified reference is not a multiarch image") {
// Rerun the command without the platform flag
cmd = exec.Command(cosignBinaryPath, "save", ref, "--dir", s.Root)
output, err = cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("error adding image to store: %v, output: %s", err, output)
}
} else {
return fmt.Errorf("error adding image to store: %v, output: %s", err, output)
}
}
return nil
@@ -73,7 +79,7 @@ func SaveImage(ctx context.Context, s *store.Layout, ref string) error {
func LoadImages(ctx context.Context, s *store.Layout, registry string, ropts content.RegistryOptions) error {
l := log.FromContext(ctx)
cosignBinaryPath, err := ensureCosignBinary(ctx, s)
cosignBinaryPath, err := getCosignPath(ctx)
if err != nil {
return err
}
@@ -132,7 +138,7 @@ func LoadImages(ctx context.Context, s *store.Layout, registry string, ropts con
// RegistryLogin - performs cosign login
func RegistryLogin(ctx context.Context, s *store.Layout, registry string, ropts content.RegistryOptions) error {
cosignBinaryPath, err := ensureCosignBinary(ctx, s)
cosignBinaryPath, err := getCosignPath(ctx)
if err != nil {
return err
}
@@ -170,10 +176,41 @@ func RetryOperation(ctx context.Context, operation func() error) error {
}
// ensureCosignBinary checks if the cosign binary exists in the specified directory and installs it if not.
func ensureCosignBinary(ctx context.Context, s *store.Layout) (string, error) {
l := log.FromContext(ctx)
func EnsureBinaryExists(ctx context.Context, bin embed.FS) (error) {
// Set up a path for the binary to be copied.
binaryPath, err := getCosignPath(ctx)
if err != nil {
return fmt.Errorf("Error: %v\n", err)
}
// Determine the architecture so that we pull the correct embedded binary.
arch := runtime.GOARCH
rOS := runtime.GOOS
binaryName := "cosign"
if rOS == "windows" {
binaryName = fmt.Sprintf("cosign-%s-%s.exe", rOS, arch)
} else {
binaryName = fmt.Sprintf("cosign-%s-%s", rOS, arch)
}
// retrieve the embedded binary
f, err := bin.ReadFile(fmt.Sprintf("binaries/%s", binaryName))
if err != nil {
return fmt.Errorf("Error: %v\n", err)
}
// write the binary to the filesystem
err = os.WriteFile(binaryPath, f, 0755)
if err != nil {
return fmt.Errorf("Error: %v\n", err)
}
return nil
}
// getCosignPath returns the binary path
func getCosignPath(ctx context.Context) (string, error) {
// Get the current user's information
currentUser, err := user.Current()
if err != nil {
@@ -192,170 +229,17 @@ func ensureCosignBinary(ctx context.Context, s *store.Layout) (string, error) {
if err := os.MkdirAll(haulerDir, 0755); err != nil {
return "", fmt.Errorf("Error creating .hauler directory: %v\n", err)
}
l.Infof("Created .hauler directory at: %s", haulerDir)
}
// Check if the cosign binary exists in the specified directory.
binaryPath := filepath.Join(haulerDir, "cosign")
_, err = os.Stat(binaryPath)
if err == nil {
// Cosign binary is already installed in the specified directory.
return binaryPath, nil
}
// Cosign binary is not found.
l.Infof("Cosign binary not found. Checking to see if it exists in the store...")
// grab binary from store if it exists, otherwise try to download it from GitHub.
// if the binary has to be downloaded, then automatically add it to the store afterwards.
err = copyCosignFromStore(ctx, s, haulerDir)
if err != nil {
l.Warnf("%s", err)
err = downloadCosign(ctx, haulerDir)
if err != nil {
return "", err
}
err = addCosignToStore(ctx, s, binaryPath)
if err != nil {
return "", err
}
}
// Make the binary executable.
if err := os.Chmod(filepath.Join(haulerDir, "cosign"), 0755); err != nil {
return "", fmt.Errorf("error setting executable permission: %v", err)
}
return binaryPath, nil
}
// used to check if the cosign binary is in the store and if so copy it to the .hauler directory
func copyCosignFromStore(ctx context.Context, s *store.Layout, destDir string) error {
l := log.FromContext(ctx)
ref := "hauler/cosign:latest"
r, err := reference.Parse(ref)
if err != nil {
return err
}
found := false
if err := s.Walk(func(reference string, desc ocispec.Descriptor) error {
if !strings.Contains(reference, r.Name()) {
return nil
}
found = true
rc, err := s.Fetch(ctx, desc)
if err != nil {
return err
}
defer rc.Close()
var m ocispec.Manifest
if err := json.NewDecoder(rc).Decode(&m); err != nil {
return err
}
mapperStore, err := mapper.FromManifest(m, destDir)
if err != nil {
return err
}
pushedDesc, err := s.Copy(ctx, reference, mapperStore, "")
if err != nil {
return err
}
l.Infof("extracted [%s] from store with digest [%s]", ref, pushedDesc.Digest.String())
return nil
}); err != nil {
return err
}
if !found {
return fmt.Errorf("Reference [%s] not found in store. Hauler will attempt to download it from Github.", ref)
}
return nil
}
// adds the cosign binary to the store.
// this is to help with airgapped situations where you cannot access the internet.
func addCosignToStore(ctx context.Context, s *store.Layout, binaryPath string) error {
l := log.FromContext(ctx)
fi := v1alpha1.File{
Path: binaryPath,
}
copts := getter.ClientOptions{
NameOverride: fi.Name,
}
f := file.NewFile(fi.Path, file.WithClient(getter.NewClient(copts)))
ref, err := reference.NewTagged(f.Name(fi.Path), reference.DefaultTag)
if err != nil {
return err
}
desc, err := s.AddOCI(ctx, f, ref.Name())
if err != nil {
return err
}
l.Infof("added 'file' to store at [%s], with digest [%s]", ref.Name(), desc.Digest.String())
return nil
}
// used to check if the cosign binary is in the store and if so copy it to the .hauler directory
func downloadCosign(ctx context.Context, haulerDir string) error {
l := log.FromContext(ctx)
// Define the GitHub release URL and architecture-specific binary name.
releaseURL := "https://github.com/rancher-government-carbide/cosign/releases/latest/download"
// Determine the architecture and add it to the binary name.
arch := runtime.GOARCH
// Determine the binary name.
rOS := runtime.GOOS
binaryName := "cosign"
if rOS == "windows" {
binaryName = fmt.Sprintf("cosign-%s-%s.exe", rOS, arch)
} else {
binaryName = fmt.Sprintf("cosign-%s-%s", rOS, arch)
binaryName = "cosign.exe"
}
// Download the binary.
downloadURL := fmt.Sprintf("%s/%s", releaseURL, binaryName)
resp, err := http.Get(downloadURL)
if err != nil {
return fmt.Errorf("error downloading cosign binary: %v", err)
}
defer resp.Body.Close()
// Create the cosign binary file in the specified directory.
binaryFile, err := os.Create(filepath.Join(haulerDir, binaryName))
if err != nil {
return fmt.Errorf("error creating cosign binary: %v", err)
}
defer binaryFile.Close()
// construct path to binary
binaryPath := filepath.Join(haulerDir, binaryName)
// Copy the downloaded binary to the file.
_, err = io.Copy(binaryFile, resp.Body)
if err != nil {
return fmt.Errorf("error saving cosign binary: %v", err)
}
// Rename the binary to "cosign"
oldBinaryPath := filepath.Join(haulerDir, binaryName)
newBinaryPath := filepath.Join(haulerDir, "cosign")
if err := os.Rename(oldBinaryPath, newBinaryPath); err != nil {
return fmt.Errorf("error renaming cosign binary: %v", err)
}
l.Infof("Cosign binary downloaded and installed to %s", haulerDir)
return nil
return binaryPath, nil
}

View File

@@ -29,7 +29,7 @@ func NewTagged(n string, tag string) (gname.Reference, error) {
if err != nil {
return nil, err
}
tag = strings.Replace(tag, "+", "-", -1)
return repo.Context().Tag(tag), nil
}