Skip to content

Commit 525297f

Browse files
committed
Add copa generate command
Signed-off-by: robert-cronin <robert.owen.cronin@gmail.com>
1 parent 4f51759 commit 525297f

File tree

20 files changed

+1618
-60
lines changed

20 files changed

+1618
-60
lines changed

.github/workflows/build.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,48 @@ jobs:
449449
with:
450450
paths: "test-results.xml"
451451

452+
test-generate:
453+
needs: build
454+
name: Test generate command ${{ matrix.buildkit_mode }}
455+
runs-on: ubuntu-latest
456+
timeout-minutes: 15
457+
strategy:
458+
fail-fast: false
459+
matrix:
460+
buildkit_mode: ${{fromJson(needs.build.outputs.buildkitenvs)}}
461+
steps:
462+
- name: Download copa from build artifacts
463+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
464+
with:
465+
name: copa_edge_linux_amd64.tar.gz
466+
- run: docker system prune -a -f --volumes
467+
- name: Check out code
468+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
469+
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
470+
with:
471+
go-version: "1.24"
472+
check-latest: true
473+
- name: Install required tools
474+
shell: bash
475+
run: .github/workflows/scripts/download-tooling.sh
476+
- name: Download copa from build artifacts
477+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
478+
with:
479+
name: copa_edge_linux_amd64.tar.gz
480+
- name: Extract copa
481+
shell: bash
482+
run: |
483+
tar xzf copa_edge_linux_amd64.tar.gz
484+
./copa --version
485+
- name: Set up QEMU
486+
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
487+
- name: Run e2e tests
488+
shell: bash
489+
run: |
490+
set -eu -o pipefail
491+
. .github/workflows/scripts/buildkitenvs/${{ matrix.buildkit_mode}}
492+
go test -v ./test/e2e/generate --addr="${COPA_BUILDKIT_ADDR}" --copa="$(pwd)/copa" -timeout 0
493+
452494
test-patch-multiplatform-plugin:
453495
needs: build
454496
name: Test multiplatform with plugin

go.mod

Lines changed: 3 additions & 2 deletions
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.6
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-20250620022241-b7579e27df2b
3132
golang.org/x/sync v0.17.0
33+
golang.org/x/term v0.35.0
3234
google.golang.org/grpc v1.75.0
3335
k8s.io/apimachinery v0.34.1
3436
)
@@ -104,7 +106,6 @@ require (
104106
github.com/package-url/packageurl-go v0.1.3 // indirect
105107
github.com/pelletier/go-toml v1.9.5 // indirect
106108
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
107-
github.com/pkg/errors v0.9.1 // indirect
108109
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
109110
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
110111
github.com/quay/claircore/toolkit v1.2.4 // indirect
@@ -145,7 +146,7 @@ require (
145146
golang.org/x/crypto v0.41.0 // indirect
146147
golang.org/x/mod v0.27.0 // indirect
147148
golang.org/x/net v0.43.0 // indirect
148-
golang.org/x/sys v0.35.0 // indirect
149+
golang.org/x/sys v0.36.0 // indirect
149150
golang.org/x/text v0.28.0 // indirect
150151
golang.org/x/time v0.12.0 // indirect
151152
golang.org/x/tools v0.35.0 // indirect

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -405,10 +405,10 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
405405
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
406406
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
407407
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
408-
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
409-
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
410-
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
411-
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
408+
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
409+
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
410+
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
411+
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
412412
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
413413
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
414414
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=

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/common/registry.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package common
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
// GetRepoNameWithDigest extracts repo name with digest from image name and digest.
9+
// e.g. "docker.io/library/nginx:1.21.6-patched" -> "nginx@sha256:...".
10+
func GetRepoNameWithDigest(patchedImageName, imageDigest string) string {
11+
parts := strings.Split(patchedImageName, "/")
12+
last := parts[len(parts)-1]
13+
if idx := strings.IndexRune(last, ':'); idx >= 0 {
14+
last = last[:idx]
15+
}
16+
nameWithDigest := fmt.Sprintf("%s@%s", last, imageDigest)
17+
return nameWithDigest
18+
}

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)