Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add push flag to support pushing to registry directly #294

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 47 additions & 24 deletions pkg/buildkit/buildkit.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@
return nil
}

func SolveToDocker(ctx context.Context, c *client.Client, st *llb.State, configData []byte, tag string) error {
func Solve(ctx context.Context, c *client.Client, st *llb.State, configData []byte, tag string, push bool) error {

Check warning on line 185 in pkg/buildkit/buildkit.go

View check run for this annotation

Codecov / codecov/patch

pkg/buildkit/buildkit.go#L185

Added line #L185 was not covered by tests
def, err := st.Marshal(ctx)
if err != nil {
log.Errorf("st.Marshal failed with %s", err)
Expand All @@ -192,23 +192,7 @@
pipeR, pipeW := io.Pipe()
dockerConfig := config.LoadDefaultConfigFile(os.Stderr)
attachable := []session.Attachable{authprovider.NewDockerAuthProvider(dockerConfig)}
solveOpt := client.SolveOpt{
Exports: []client.ExportEntry{
{
Type: client.ExporterDocker,
Attrs: map[string]string{
"name": tag,
// Pass through resolved configData from original image
exptypes.ExporterImageConfigKey: string(configData),
},
Output: func(_ map[string]string) (io.WriteCloser, error) {
return pipeW, nil
},
},
},
Frontend: "", // i.e. we are passing in the llb.Definition directly
Session: attachable, // used for authprovider, sshagentprovider and secretprovider
}
solveOpt := generateSolveOpts(push, tag, configData, attachable, pipeW)

Check warning on line 195 in pkg/buildkit/buildkit.go

View check run for this annotation

Codecov / codecov/patch

pkg/buildkit/buildkit.go#L195

Added line #L195 was not covered by tests
solveOpt.SourcePolicy, err = build.ReadSourcePolicy()
if err != nil {
return err
Expand All @@ -229,11 +213,50 @@
_, err = progressui.DisplaySolveStatus(context.TODO(), c, os.Stdout, ch)
return err
})
eg.Go(func() error {
if err := dockerLoad(ctx, pipeR); err != nil {
return err
}
return pipeR.Close()
})
if !push {
eg.Go(func() error {
if err := dockerLoad(ctx, pipeR); err != nil {
return err
}
return pipeR.Close()

Check warning on line 221 in pkg/buildkit/buildkit.go

View check run for this annotation

Codecov / codecov/patch

pkg/buildkit/buildkit.go#L216-L221

Added lines #L216 - L221 were not covered by tests
})
}
return eg.Wait()
}

func generateSolveOpts(push bool, tag string, configData []byte, attachable []session.Attachable, pipeW *io.PipeWriter) client.SolveOpt {
if push {
return client.SolveOpt{
Exports: []client.ExportEntry{
{
Type: client.ExporterImage,
Attrs: map[string]string{
"name": tag,
"push": "true",
// Pass through resolved configData from original image
exptypes.ExporterImageConfigKey: string(configData),
},
},
},
Frontend: "", // i.e. we are passing in the llb.Definition directly
Session: attachable, // used for authprovider, sshagentprovider and secretprovider
}
}
return client.SolveOpt{
Exports: []client.ExportEntry{
{
Type: client.ExporterDocker,
Attrs: map[string]string{
"name": tag,
// Pass through resolved configData from original image
exptypes.ExporterImageConfigKey: string(configData),
},
Output: func(_ map[string]string) (io.WriteCloser, error) {
return pipeW, nil
},

Check warning on line 256 in pkg/buildkit/buildkit.go

View check run for this annotation

Codecov / codecov/patch

pkg/buildkit/buildkit.go#L227-L256

Added lines #L227 - L256 were not covered by tests
},
},
Frontend: "", // i.e. we are passing in the llb.Definition directly
Session: attachable, // used for authprovider, sshagentprovider and secretprovider
}
}
6 changes: 5 additions & 1 deletion pkg/patch/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
format string
output string
bkOpts buildkit.Opts
push bool
}

func NewPatchCmd() *cobra.Command {
Expand All @@ -51,7 +52,9 @@
ua.format,
ua.output,
ua.ignoreError,
bkopts)
ua.push,
bkopts,
)

Check warning on line 57 in pkg/patch/cmd.go

View check run for this annotation

Codecov / codecov/patch

pkg/patch/cmd.go#L55-L57

Added lines #L55 - L57 were not covered by tests
},
}
flags := patchCmd.Flags()
Expand All @@ -68,6 +71,7 @@
flags.BoolVar(&ua.ignoreError, "ignore-errors", false, "Ignore errors and continue patching")
flags.StringVarP(&ua.format, "format", "f", "openvex", "Output format, defaults to 'openvex'")
flags.StringVarP(&ua.output, "output", "o", "", "Output file path")
flags.BoolVarP(&ua.push, "push", "p", false, "Push patched image to destination registry")

if err := patchCmd.MarkFlagRequired("image"); err != nil {
panic(err)
Expand Down
76 changes: 49 additions & 27 deletions pkg/patch/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
"errors"
"fmt"
"os"
"strings"
"time"

ref "github.com/distribution/reference"
log "github.com/sirupsen/logrus"
"golang.org/x/exp/slices"

"github.com/distribution/reference"
"github.com/project-copacetic/copacetic/pkg/buildkit"
"github.com/project-copacetic/copacetic/pkg/pkgmgr"
"github.com/project-copacetic/copacetic/pkg/report"
Expand All @@ -24,13 +25,13 @@
)

// Patch command applies package updates to an OCI image given a vulnerability report.
func Patch(ctx context.Context, timeout time.Duration, image, reportFile, patchedTag, workingFolder, scanner, format, output string, ignoreError bool, bkOpts buildkit.Opts) error {
func Patch(ctx context.Context, timeout time.Duration, image, reportFile, patchedTag, workingFolder, scanner, format, output string, ignoreError, push bool, bkOpts buildkit.Opts) error {

Check warning on line 28 in pkg/patch/patch.go

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L28

Added line #L28 was not covered by tests
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

ch := make(chan error)
go func() {
ch <- patchWithContext(timeoutCtx, image, reportFile, patchedTag, workingFolder, scanner, format, output, ignoreError, bkOpts)
ch <- patchWithContext(timeoutCtx, image, reportFile, patchedTag, workingFolder, scanner, format, output, ignoreError, push, bkOpts)

Check warning on line 34 in pkg/patch/patch.go

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L34

Added line #L34 was not covered by tests
}()

select {
Expand All @@ -55,31 +56,11 @@
}
}

func patchWithContext(ctx context.Context, image, reportFile, patchedTag, workingFolder, scanner, format, output string, ignoreError bool, bkOpts buildkit.Opts) error {
imageName, err := reference.ParseNamed(image)
func patchWithContext(ctx context.Context, image, reportFile, patchedTag, workingFolder, scanner, format, output string, ignoreError, push bool, bkOpts buildkit.Opts) error {
patchedImageName, err := patchedImageTarget(image, patchedTag)

Check warning on line 60 in pkg/patch/patch.go

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L59-L60

Added lines #L59 - L60 were not covered by tests
if err != nil {
return err
}
if reference.IsNameOnly(imageName) {
log.Warnf("Image name has no tag or digest, using latest as tag")
imageName = reference.TagNameOnly(imageName)
}
taggedName, ok := imageName.(reference.Tagged)
if !ok {
err := errors.New("unexpected: TagNameOnly did create Tagged ref")
log.Error(err)
return err
}
tag := taggedName.Tag()
if patchedTag == "" {
if tag == "" {
log.Warnf("No output tag specified for digest-referenced image, defaulting to `%s`", defaultPatchedTagSuffix)
patchedTag = defaultPatchedTagSuffix
} else {
patchedTag = fmt.Sprintf("%s-%s", tag, defaultPatchedTagSuffix)
}
}
patchedImageName := fmt.Sprintf("%s:%s", imageName.Name(), patchedTag)

// Ensure working folder exists for call to InstallUpdates
if workingFolder == "" {
Expand Down Expand Up @@ -133,7 +114,7 @@
return err
}

if err = buildkit.SolveToDocker(ctx, config.Client, patchedImageState, config.ConfigData, patchedImageName); err != nil {
if err = buildkit.Solve(ctx, config.Client, patchedImageState, config.ConfigData, *patchedImageName, push); err != nil {

Check warning on line 117 in pkg/patch/patch.go

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L117

Added line #L117 was not covered by tests
return err
}

Expand All @@ -157,7 +138,48 @@
}
// vex document must contain at least one statement
if output != "" && len(validatedManifest.Updates) > 0 {
return vex.TryOutputVexDocument(validatedManifest, pkgmgr, patchedImageName, format, output)
return vex.TryOutputVexDocument(validatedManifest, pkgmgr, *patchedImageName, format, output)

Check warning on line 141 in pkg/patch/patch.go

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L141

Added line #L141 was not covered by tests
}
return nil
}

func patchedImageTarget(image, patchedTag string) (*string, error) {
imageName, err := ref.ParseNamed(image)
if err != nil {
return nil, err
}

Check warning on line 150 in pkg/patch/patch.go

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L149-L150

Added lines #L149 - L150 were not covered by tests
if ref.IsNameOnly(imageName) {
log.Warn("Image name has no tag or digest, using latest as tag")
imageName = ref.TagNameOnly(imageName)
}

Check warning on line 154 in pkg/patch/patch.go

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L152-L154

Added lines #L152 - L154 were not covered by tests
taggedName, ok := imageName.(ref.Tagged)
if !ok {
err := errors.New("unexpected: TagNameOnly did create Tagged ref")
log.Error(err)
return nil, err
}

Check warning on line 160 in pkg/patch/patch.go

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L157-L160

Added lines #L157 - L160 were not covered by tests
tag := taggedName.Tag()
var patchedImageName string
if patchedTag == "" {
if tag == "" {
log.Warnf("No output tag specified for digest-referenced image, defaulting to `%s`", defaultPatchedTagSuffix)
patchedTag = defaultPatchedTagSuffix

Check warning on line 166 in pkg/patch/patch.go

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L165-L166

Added lines #L165 - L166 were not covered by tests
} else {
patchedTag = fmt.Sprintf("%s-%s", tag, defaultPatchedTagSuffix)
}
}

slashCount := strings.Count(patchedTag, "/")
if slashCount > 0 {
if slashCount < 2 {
err := fmt.Errorf("invalid tag %s, must be in the form <registry>/<image>:<tag>", patchedTag)
return nil, err
}
// this implies user has passed a destination image name, not just a tag
patchedImageName = patchedTag
} else {
patchedImageName = fmt.Sprintf("%s:%s", imageName.Name(), patchedTag)
}

return &patchedImageName, nil
}
55 changes: 55 additions & 0 deletions pkg/patch/patch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,58 @@ func TestRemoveIfNotDebug(t *testing.T) {
os.RemoveAll(workingFolder)
})
}

func TestPatchedImageTarget(t *testing.T) {
tests := []struct {
name string
image string
patchedTag string
want string
wantErr bool
}{
{
name: "tag passed is empty",
image: "docker.io/library/nginx:1.21.3",
patchedTag: "",
want: "docker.io/library/nginx:1.21.3-patched",
wantErr: false,
},

{
name: "tag passed with value",
image: "docker.io/library/nginx:1.21.3",
patchedTag: "custom",
want: "docker.io/library/nginx:custom",
wantErr: false,
},
{
name: "tag passed but without registry or repo",
image: "docker.io/library/nginx:1.21.3",
patchedTag: "myregistry.azurecr.io/nginx:1.21.3-patched",
want: "",
wantErr: true,
},
{
name: "tag passed contains registry, repo and image",
image: "docker.io/library/nginx:1.21.3",
patchedTag: "myregistry.azurecr.io/myrepo/nginx:1.21.3-patched",
want: "myregistry.azurecr.io/myrepo/nginx:1.21.3-patched",
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := patchedImageTarget(tt.image, tt.patchedTag)
if (err != nil) != tt.wantErr {
t.Errorf("patchedImageTarget() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != nil {
if *got != tt.want {
t.Errorf("patchedImageTarget() = %v, want %v", *got, tt.want)
}
}
})
}
}
Loading