Skip to content

Commit 27cb198

Browse files
chore: add oci + release workflow (#34)
Co-authored-by: razzle <[email protected]>
1 parent 67c2cf7 commit 27cb198

File tree

20 files changed

+1510
-4
lines changed

20 files changed

+1510
-4
lines changed

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Set this repository to use unix style line endings
2+
* text eol=lf

.github/workflows/lint.yaml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Lint
1+
name: Lint and fmt
22
on:
33
pull_request:
44
paths-ignore:
@@ -24,6 +24,15 @@ jobs:
2424
- name: Checkout
2525
uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
2626

27+
- name: Install tools
28+
uses: ./.github/actions/install-tools
29+
30+
- name: ensure proper go formatting
31+
run: make check-fmt
32+
33+
- name: ensure all modules are on the same go version
34+
run: make check-go-version-consistency
35+
2736
- name: Run Revive Action by pulling pre-built image
2837
uses: docker://morphy/revive-action@sha256:087d4e61077087755711ab7e9fae3cc899b7bb07ff8f6a30c3dfb240b1620ae8 #v2.5.7
2938
with:

.github/workflows/release-oci.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Release OCI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- 'oci/**'
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
release:
15+
runs-on: ubuntu-latest
16+
permissions:
17+
contents: write
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
21+
with:
22+
fetch-depth: 0
23+
24+
- name: Release
25+
uses: ./.github/actions/release
26+
with:
27+
module: "oci"

.pre-commit-config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ repos:
2626
language: system
2727
pass_filenames: false
2828

29+
- id: fmt
30+
name: go fmt
31+
entry: make fmt
32+
language: system
33+
pass_filenames: false
34+
2935
- id: lint
3036
name: go lint
3137
entry: make lint

Makefile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ fmt:
1818
fmt-%:
1919
cd $(subst :,/,$*); go fmt ./...
2020

21+
check-fmt:
22+
$(MAKE) $(addprefix check-fmt-, $(MODULES))
23+
24+
check-fmt-%:
25+
cd $(subst :,/,$*); test -z "$$(gofmt -l .)"
26+
2127
vet:
2228
$(MAKE) $(addprefix vet-, $(MODULES))
2329

@@ -38,3 +44,6 @@ scan:
3844

3945
scan-%:
4046
cd $(subst :,/,$*); syft scan . -o json | grype --fail-on low
47+
48+
check-go-version-consistency:
49+
hack/check_go_version.sh

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
| Module | Import | Description |
1010
| --- | --- | --- |
1111
| [![GitHub Tag](https://img.shields.io/github/v/tag/defenseunicorns/pkg?sort=date&filter=helpers%2F*&label)](https://pkg.go.dev/github.com/defenseunicorns/pkg/helpers) | `go get github.com/defenseunicorns/pkg/helpers` | Common helper functions for Go. |
12+
| [![GitHub Tag](https://img.shields.io/github/v/tag/defenseunicorns/pkg?sort=date&filter=oci%2F*&label)](https://pkg.go.dev/github.com/defenseunicorns/pkg/oci) | `go get github.com/defenseunicorns/pkg/oci` | tools for interacting with artifacts stored in OCI registries. |
1213

1314
## Contributing
1415

hack/check_go_version.sh

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
first_version=""
6+
# Find and iterate over all go.mod files, excluding the vendor directory
7+
find . -name go.mod | while read -r mod; do
8+
current_version=$(grep '^go 1\.' "$mod" | cut -d ' ' -f 2)
9+
10+
if [[ -z "$first_version" ]]; then
11+
first_version=$current_version
12+
elif [[ "$current_version" != "$first_version" ]]; then
13+
echo "Inconsistency found: $mod uses Go version $current_version, this differs from another found version $first_version."
14+
exit 1
15+
fi
16+
done
17+
18+
echo "All modules use the same Go version: $first_version."

internal/release/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/defenseunicorns/pkg/internal/release
22

3-
go 1.22.1
3+
go 1.21.8
44

55
require (
66
github.com/Masterminds/semver v1.5.0

internal/release/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,14 +159,14 @@ func getCommitMessagesFromLastTag(r *git.Repository, lastTagVersion *semver.Vers
159159
var commitsAfterTagOnModule []string
160160
err = commitsOnModule.ForEach(func(c *object.Commit) error {
161161
for _, commitAfterTag := range commitsAfterTag {
162-
if commitAfterTag.Hash == c.Hash{
162+
if commitAfterTag.Hash == c.Hash {
163163
commitsAfterTagOnModule = append(commitsAfterTagOnModule, c.Message)
164164
}
165165
}
166166
return nil
167167
})
168168

169-
if err != nil{
169+
if err != nil {
170170
return nil, fmt.Errorf("could not iterate over commits %w", err)
171171
}
172172

oci/common.go

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: 2024-Present Defense Unicorns
3+
4+
// Package oci provides tools for interacting with artifacts stored in OCI registries
5+
package oci
6+
7+
import (
8+
"fmt"
9+
"log/slog"
10+
"net/http"
11+
"strings"
12+
13+
"github.com/defenseunicorns/pkg/helpers"
14+
"github.com/docker/cli/cli/config"
15+
"github.com/docker/cli/cli/config/configfile"
16+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
17+
"oras.land/oras-go/v2/registry"
18+
"oras.land/oras-go/v2/registry/remote"
19+
"oras.land/oras-go/v2/registry/remote/auth"
20+
)
21+
22+
const (
23+
// MultiOS is the OS used for multi-platform packages
24+
MultiOS = "multi"
25+
)
26+
27+
// OrasRemote is a wrapper around the Oras remote repository that includes a progress bar for interactive feedback.
28+
type OrasRemote struct {
29+
repo *remote.Repository
30+
root *Manifest
31+
progTransport *helpers.Transport
32+
targetPlatform *ocispec.Platform
33+
log *slog.Logger
34+
}
35+
36+
// Modifier is a function that modifies an OrasRemote
37+
type Modifier func(*OrasRemote)
38+
39+
// WithPlainHTTP sets the plain HTTP flag for the remote
40+
func WithPlainHTTP(plainHTTP bool) Modifier {
41+
return func(o *OrasRemote) {
42+
o.repo.PlainHTTP = plainHTTP
43+
}
44+
}
45+
46+
// WithInsecureSkipVerify sets the insecure TLS flag for the remote
47+
func WithInsecureSkipVerify(insecure bool) Modifier {
48+
return func(o *OrasRemote) {
49+
o.progTransport.Base.(*http.Transport).TLSClientConfig.InsecureSkipVerify = insecure
50+
}
51+
}
52+
53+
// PlatformForArch sets the target architecture for the remote
54+
func PlatformForArch(arch string) ocispec.Platform {
55+
return ocispec.Platform{
56+
OS: MultiOS,
57+
Architecture: arch,
58+
}
59+
}
60+
61+
// WithUserAgent sets the user agent for the remote
62+
func WithUserAgent(userAgent string) Modifier {
63+
return func(o *OrasRemote) {
64+
o.repo.Client.(*auth.Client).SetUserAgent(userAgent)
65+
}
66+
}
67+
68+
// WithLogger sets the logger for the remote
69+
func WithLogger(logger *slog.Logger) Modifier {
70+
return func(o *OrasRemote) {
71+
o.log = logger
72+
}
73+
}
74+
75+
// NewOrasRemote returns an oras remote repository client and context for the given url.
76+
//
77+
// Registry auth is handled by the Docker CLI's credential store and checked before returning the client
78+
func NewOrasRemote(url string, platform ocispec.Platform, mods ...Modifier) (*OrasRemote, error) {
79+
ref, err := registry.ParseReference(strings.TrimPrefix(url, helpers.OCIURLPrefix))
80+
if err != nil {
81+
return nil, fmt.Errorf("failed to parse OCI reference %q: %w", url, err)
82+
}
83+
transport := http.DefaultTransport.(*http.Transport).Clone()
84+
client := auth.DefaultClient
85+
client.Client.Transport = transport
86+
o := &OrasRemote{
87+
repo: &remote.Repository{Client: client},
88+
progTransport: helpers.NewTransport(transport, nil),
89+
targetPlatform: &platform,
90+
log: slog.Default(),
91+
}
92+
93+
for _, mod := range mods {
94+
mod(o)
95+
}
96+
97+
if err := o.setRepository(ref); err != nil {
98+
return nil, err
99+
}
100+
101+
return o, nil
102+
}
103+
104+
// SetProgressWriter sets the progress writer for the remote
105+
func (o *OrasRemote) SetProgressWriter(bar helpers.ProgressWriter) {
106+
o.progTransport.ProgressBar = bar
107+
o.repo.Client.(*auth.Client).Client.Transport = o.progTransport
108+
}
109+
110+
// ClearProgressWriter clears the progress writer for the remote
111+
func (o *OrasRemote) ClearProgressWriter() {
112+
o.progTransport.ProgressBar = nil
113+
o.repo.Client.(*auth.Client).Client.Transport = o.progTransport
114+
}
115+
116+
// Repo gives you access to the underlying remote repository
117+
func (o *OrasRemote) Repo() *remote.Repository {
118+
return o.repo
119+
}
120+
121+
// Log gives you access to the OrasRemote logger
122+
func (o *OrasRemote) Log() *slog.Logger {
123+
return o.log
124+
}
125+
126+
// setRepository sets the repository for the remote as well as the auth client.
127+
func (o *OrasRemote) setRepository(ref registry.Reference) error {
128+
o.root = nil
129+
130+
// patch docker.io to registry-1.docker.io
131+
// this allows end users to use docker.io as an alias for registry-1.docker.io
132+
if ref.Registry == "docker.io" {
133+
ref.Registry = "registry-1.docker.io"
134+
}
135+
if ref.Registry == "🦄" || ref.Registry == "defenseunicorns" {
136+
ref.Registry = "ghcr.io"
137+
ref.Repository = "defenseunicorns/packages/" + ref.Repository
138+
}
139+
client, err := o.createAuthClient(ref)
140+
if err != nil {
141+
return err
142+
}
143+
144+
o.repo.Reference = ref
145+
o.repo.Client = client
146+
147+
return nil
148+
}
149+
150+
// createAuthClient returns an auth client for the given reference.
151+
//
152+
// The credentials are pulled using Docker's default credential store.
153+
//
154+
// TODO: instead of using Docker's cred store, should use the new one from ORAS to remove that dep
155+
func (o *OrasRemote) createAuthClient(ref registry.Reference) (*auth.Client, error) {
156+
157+
client := o.repo.Client.(*auth.Client)
158+
o.log.Debug(fmt.Sprintf("Loading docker config file from default config location: %s for %s", config.Dir(), ref))
159+
cfg, err := config.Load(config.Dir())
160+
if err != nil {
161+
return nil, err
162+
}
163+
if !cfg.ContainsAuth() {
164+
o.log.Debug("no docker config file found")
165+
return client, nil
166+
}
167+
168+
configs := []*configfile.ConfigFile{cfg}
169+
170+
var key = ref.Registry
171+
if key == "registry-1.docker.io" {
172+
// Docker stores its credentials under the following key, otherwise credentials use the registry URL
173+
key = "https://index.docker.io/v1/"
174+
}
175+
176+
authConf, err := configs[0].GetCredentialsStore(key).Get(key)
177+
if err != nil {
178+
return nil, fmt.Errorf("unable to get credentials for %s: %w", key, err)
179+
}
180+
181+
cred := auth.Credential{
182+
Username: authConf.Username,
183+
Password: authConf.Password,
184+
AccessToken: authConf.RegistryToken,
185+
RefreshToken: authConf.IdentityToken,
186+
}
187+
188+
client.Credential = auth.StaticCredential(ref.Registry, cred)
189+
190+
return client, nil
191+
}

0 commit comments

Comments
 (0)