Skip to content

Commit a497bd4

Browse files
committed
Add generate command
Signed-off-by: robert-cronin <robert.owen.cronin@gmail.com>
1 parent 5e6d3f3 commit a497bd4

File tree

8 files changed

+1182
-1
lines changed

8 files changed

+1182
-1
lines changed

.github/workflows/build.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,48 @@ jobs:
429429
430430
go test -v ./integration/multiarch --addr="${COPA_BUILDKIT_ADDR}" --copa="$(pwd)/copa" -timeout 0
431431
432+
test-generate:
433+
needs: build
434+
name: Test generate command ${{ matrix.buildkit_mode }}
435+
runs-on: ubuntu-latest
436+
timeout-minutes: 15
437+
strategy:
438+
fail-fast: false
439+
matrix:
440+
buildkit_mode: ${{fromJson(needs.build.outputs.buildkitenvs)}}
441+
steps:
442+
- name: Download copa from build artifacts
443+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
444+
with:
445+
name: copa_edge_linux_amd64.tar.gz
446+
- run: docker system prune -a -f --volumes
447+
- name: Check out code
448+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
449+
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
450+
with:
451+
go-version: "1.24"
452+
check-latest: true
453+
- name: Install required tools
454+
shell: bash
455+
run: .github/workflows/scripts/download-tooling.sh
456+
- name: Download copa from build artifacts
457+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
458+
with:
459+
name: copa_edge_linux_amd64.tar.gz
460+
- name: Extract copa
461+
shell: bash
462+
run: |
463+
tar xzf copa_edge_linux_amd64.tar.gz
464+
./copa --version
465+
- name: Set up QEMU
466+
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
467+
- name: Run e2e tests
468+
shell: bash
469+
run: |
470+
set -eu -o pipefail
471+
. .github/workflows/scripts/buildkitenvs/${{ matrix.buildkit_mode}}
472+
go test -v ./test/e2e/generate --addr="${COPA_BUILDKIT_ADDR}" --copa="$(pwd)/copa" -timeout 0
473+
432474
test-patch-multiplatform-plugin:
433475
needs: build
434476
name: Test multiplatform with plugin

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ require (
2121
github.com/opencontainers/go-digest v1.0.0
2222
github.com/opencontainers/image-spec v1.1.1
2323
github.com/openvex/go-vex v0.2.5
24+
github.com/pkg/errors v0.9.1
2425
github.com/quay/claircore v1.5.39
2526
github.com/sirupsen/logrus v1.9.3
2627
github.com/spf13/cobra v1.9.1
@@ -29,6 +30,7 @@ require (
2930
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f
3031
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
3132
golang.org/x/sync v0.16.0
33+
golang.org/x/term v0.32.0
3234
google.golang.org/grpc v1.73.0
3335
k8s.io/apimachinery v0.33.3
3436
)
@@ -103,7 +105,6 @@ require (
103105
github.com/package-url/packageurl-go v0.1.3 // indirect
104106
github.com/pelletier/go-toml v1.9.5 // indirect
105107
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
106-
github.com/pkg/errors v0.9.1 // indirect
107108
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
108109
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
109110
github.com/quay/claircore/toolkit v1.2.4 // indirect

main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"os"
55
"strings"
66

7+
"github.com/project-copacetic/copacetic/pkg/generate"
78
"github.com/project-copacetic/copacetic/pkg/patch"
89
log "github.com/sirupsen/logrus"
910
"github.com/spf13/cobra"
@@ -37,6 +38,7 @@ func newRootCmd() *cobra.Command {
3738
flags.BoolVar(&debug, "debug", false, "enable debug level logging")
3839

3940
rootCmd.AddCommand(patch.NewPatchCmd())
41+
rootCmd.AddCommand(generate.NewGenerateCmd())
4042
return rootCmd
4143
}
4244

pkg/generate/cmd.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package generate
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"time"
8+
9+
"github.com/project-copacetic/copacetic/pkg/buildkit"
10+
"github.com/project-copacetic/copacetic/pkg/types"
11+
"github.com/spf13/cobra"
12+
"golang.org/x/term"
13+
14+
// Register connection helpers for buildkit.
15+
_ "github.com/moby/buildkit/client/connhelper/dockercontainer"
16+
_ "github.com/moby/buildkit/client/connhelper/kubepod"
17+
_ "github.com/moby/buildkit/client/connhelper/nerdctlcontainer"
18+
_ "github.com/moby/buildkit/client/connhelper/podmancontainer"
19+
_ "github.com/moby/buildkit/client/connhelper/ssh"
20+
)
21+
22+
type generateArgs struct {
23+
appImage string
24+
report string
25+
patchedTag string
26+
suffix string
27+
workingFolder string
28+
timeout time.Duration
29+
scanner string
30+
ignoreError bool
31+
outputContext string
32+
format string
33+
output string
34+
bkOpts buildkit.Opts
35+
platform []string
36+
loader string
37+
}
38+
39+
func NewGenerateCmd() *cobra.Command {
40+
ga := generateArgs{}
41+
generateCmd := &cobra.Command{
42+
Use: "generate",
43+
Short: "Generate a Docker build context tar stream for patching container images",
44+
Long: `Generate creates a tar stream containing a Dockerfile and patch layer that can be piped to 'docker build'.
45+
This command produces a build context with the patch diff layer and a Dockerfile that applies the patch.`,
46+
Example: ` # Generate patch context and pipe to docker build
47+
copa generate -i ubuntu:22.04 -r trivy.json | docker build -t ubuntu:22.04-patched -
48+
49+
# Generate patch context without vulnerability report (update all packages)
50+
copa generate -i alpine:3.18 | docker build -t alpine:3.18-patched -
51+
52+
# Save context to file
53+
copa generate -i alpine:3.18 -r scan.json --output-context patch.tar`,
54+
RunE: func(_ *cobra.Command, _ []string) error {
55+
// Check if stdout is a TTY when not writing to file
56+
if ga.outputContext == "" && term.IsTerminal(int(os.Stdout.Fd())) {
57+
return fmt.Errorf("refusing to write tar stream to terminal. Use --output-context to save to file or redirect stdout")
58+
}
59+
60+
opts := &types.Options{
61+
Image: ga.appImage,
62+
Report: ga.report,
63+
PatchedTag: ga.patchedTag,
64+
Suffix: ga.suffix,
65+
WorkingFolder: ga.workingFolder,
66+
Timeout: ga.timeout,
67+
Scanner: ga.scanner,
68+
IgnoreError: ga.ignoreError,
69+
OutputContext: ga.outputContext,
70+
Format: ga.format,
71+
Output: ga.output,
72+
BkAddr: ga.bkOpts.Addr,
73+
BkCACertPath: ga.bkOpts.CACertPath,
74+
BkCertPath: ga.bkOpts.CertPath,
75+
BkKeyPath: ga.bkOpts.KeyPath,
76+
Platforms: ga.platform,
77+
Loader: ga.loader,
78+
}
79+
return Generate(context.Background(), opts)
80+
},
81+
}
82+
83+
flags := generateCmd.Flags()
84+
flags.StringVarP(&ga.appImage, "image", "i", "", "Application image name and tag to patch")
85+
flags.StringVarP(&ga.report, "report", "r", "", "Vulnerability report file path (optional)")
86+
flags.StringVarP(&ga.patchedTag, "tag", "t", "", "Tag for the patched image")
87+
flags.StringVarP(&ga.suffix, "tag-suffix", "", "patched", "Suffix for the patched image (if no explicit --tag provided)")
88+
flags.StringVarP(&ga.workingFolder, "working-folder", "w", "", "Working folder, defaults to system temp folder")
89+
flags.StringVarP(&ga.bkOpts.Addr, "addr", "a", "", "Address of buildkitd service, defaults to local docker daemon with fallback to "+buildkit.DefaultAddr)
90+
flags.StringVarP(&ga.bkOpts.CACertPath, "cacert", "", "", "Absolute path to buildkitd CA certificate")
91+
flags.StringVarP(&ga.bkOpts.CertPath, "cert", "", "", "Absolute path to buildkit client certificate")
92+
flags.StringVarP(&ga.bkOpts.KeyPath, "key", "", "", "Absolute path to buildkit client key")
93+
flags.DurationVar(&ga.timeout, "timeout", 5*time.Minute, "Timeout for the operation, defaults to '5m'")
94+
flags.StringVarP(&ga.scanner, "scanner", "s", "trivy", "Scanner that generated the report, defaults to 'trivy'")
95+
flags.BoolVar(&ga.ignoreError, "ignore-errors", false, "Ignore errors during patching")
96+
flags.StringVarP(&ga.format, "format", "f", "openvex", "Output format, defaults to 'openvex'")
97+
flags.StringVarP(&ga.output, "output", "o", "", "Output file path")
98+
flags.StringSliceVar(&ga.platform, "platform", nil,
99+
"Target platform(s) for multi-arch images when no report directory is provided (e.g., linux/amd64,linux/arm64). "+
100+
"Valid platforms: linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6. "+
101+
"If platform flag is used, only specified platforms are patched and the rest are preserved. If not specified, all platforms present in the image are patched.")
102+
flags.StringVarP(&ga.loader, "loader", "l", "", "Loader to use for loading images. Options: 'docker', 'podman', or empty for auto-detection based on buildkit address")
103+
flags.StringVar(&ga.outputContext, "output-context", "", "Path to save the generated tar context (instead of stdout)")
104+
105+
if err := generateCmd.MarkFlagRequired("image"); err != nil {
106+
panic(err)
107+
}
108+
109+
return generateCmd
110+
}

0 commit comments

Comments
 (0)