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

refactor: patch with context #732

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 10 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
314 changes: 185 additions & 129 deletions pkg/patch/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,29 @@
defaultTag = "latest"
)

type TrivyOpts struct {
BkClient *client.Client
SolveOpt *client.SolveOpt
Image string
Ch chan error
MiahaCybersec marked this conversation as resolved.
Show resolved Hide resolved
ReportFile string
WorkingFolder string
Updates *unversioned.UpdateManifest
IgnoreError bool
Output string
DockerNormalizedImageName reference.Named
PatchedImageName string
Format string
}

type BuildStatus struct {
BuildChannel chan *client.SolveStatus
}

type BuildContext struct {
Ctx context.Context
}

// 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 {
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
Expand Down Expand Up @@ -74,35 +97,26 @@
}
}

func patchWithContext(ctx context.Context, ch chan error, image, reportFile, patchedTag, workingFolder, scanner, format, output string, ignoreError bool, bkOpts buildkit.Opts) error {
imageName, err := reference.ParseNormalizedNamed(image)
// patchWithContext patches the user-supplied image, image.
MiahaCybersec marked this conversation as resolved.
Show resolved Hide resolved
func patchWithContext(ctx context.Context, ch chan error, image, reportFile, userSuppliedPatchTag, workingFolder, scanner, format, output string, ignoreError bool, bkOpts buildkit.Opts) error {
dockerNormalizedImageName, err := reference.ParseNormalizedNamed(image)

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L101-L102

Added lines #L101 - L102 were not covered by tests
if err != nil {
return err
}
if reference.IsNameOnly(imageName) {

if reference.IsNameOnly(dockerNormalizedImageName) {

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L107

Added line #L107 was not covered by tests
log.Warnf("Image name has no tag or digest, using latest as tag")
ashnamehrotra marked this conversation as resolved.
Show resolved Hide resolved
imageName = reference.TagNameOnly(imageName)
}
var tag string
taggedName, ok := imageName.(reference.Tagged)
if ok {
tag = taggedName.Tag()
} else {
log.Warnf("Image name has no 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)
}
dockerNormalizedImageName = reference.TagNameOnly(dockerNormalizedImageName)

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L109

Added line #L109 was not covered by tests
}
_, err = reference.WithTag(imageName, patchedTag)

patchedTag := generatePatchedTag(dockerNormalizedImageName, userSuppliedPatchTag)

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L112

Added line #L112 was not covered by tests

_, err = reference.WithTag(dockerNormalizedImageName, patchedTag)

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L114

Added line #L114 was not covered by tests
if err != nil {
return fmt.Errorf("%w with patched tag %s", err, patchedTag)
ashnamehrotra marked this conversation as resolved.
Show resolved Hide resolved
}
patchedImageName := fmt.Sprintf("%s:%s", imageName.Name(), patchedTag)

patchedImageName := fmt.Sprintf("%s:%s", dockerNormalizedImageName.Name(), patchedTag)

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L119

Added line #L119 was not covered by tests

// Ensure working folder exists for call to InstallUpdates
if workingFolder == "" {
Expand Down Expand Up @@ -166,113 +180,14 @@
buildChannel := make(chan *client.SolveStatus)
eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() error {
_, err := bkClient.Build(ctx, solveOpt, copaProduct, func(ctx context.Context, c gwclient.Client) (*gwclient.Result, error) {
// Configure buildctl/client for use by package manager
config, err := buildkit.InitializeBuildkitConfig(ctx, c, imageName.String())
if err != nil {
ch <- err
return nil, err
}

// Create package manager helper
var manager pkgmgr.PackageManager
if reportFile == "" {
// determine OS family
fileBytes, err := buildkit.ExtractFileFromState(ctx, c, &config.ImageState, "/etc/os-release")
if err != nil {
ch <- err
return nil, fmt.Errorf("unable to extract /etc/os-release file from state %w", err)
}

osType, err := getOSType(ctx, fileBytes)
if err != nil {
ch <- err
return nil, err
}

osVersion, err := getOSVersion(ctx, fileBytes)
if err != nil {
ch <- err
return nil, err
}

// get package manager based on os family type
manager, err = pkgmgr.GetPackageManager(osType, osVersion, config, workingFolder)
if err != nil {
ch <- err
return nil, err
}
} else {
// get package manager based on os family type
manager, err = pkgmgr.GetPackageManager(updates.Metadata.OS.Type, updates.Metadata.OS.Version, config, workingFolder)
if err != nil {
ch <- err
return nil, err
}
}

// Export the patched image state to Docker
// TODO: Add support for other output modes as buildctl does.
patchedImageState, errPkgs, err := manager.InstallUpdates(ctx, updates, ignoreError)
if err != nil {
ch <- err
return nil, err
}

platform := platforms.Normalize(platforms.DefaultSpec())
if platform.OS != "linux" {
platform.OS = "linux"
}

def, err := patchedImageState.Marshal(ctx, llb.Platform(platform))
if err != nil {
ch <- err
return nil, fmt.Errorf("unable to get platform from ImageState %w", err)
}

res, err := c.Solve(ctx, gwclient.SolveRequest{
Definition: def.ToPB(),
Evaluate: true,
})
if err != nil {
ch <- err
return nil, err
}

res.AddMeta(exptypes.ExporterImageConfigKey, config.ConfigData)

// Currently can only validate updates if updating via scanner
if reportFile != "" {
// create a new manifest with the successfully patched packages
validatedManifest := &unversioned.UpdateManifest{
Metadata: unversioned.Metadata{
OS: unversioned.OS{
Type: updates.Metadata.OS.Type,
Version: updates.Metadata.OS.Version,
},
Config: unversioned.Config{
Arch: updates.Metadata.Config.Arch,
},
},
Updates: []unversioned.UpdatePackage{},
}
for _, update := range updates.Updates {
if !slices.Contains(errPkgs, update.Name) {
validatedManifest.Updates = append(validatedManifest.Updates, update)
}
}
// vex document must contain at least one statement
if output != "" && len(validatedManifest.Updates) > 0 {
if err := vex.TryOutputVexDocument(validatedManifest, manager, patchedImageName, format, output); err != nil {
ch <- err
return nil, err
}
}
}

return res, nil
}, buildChannel)

err = buildkitBuild(
BuildContext{ctx},
&TrivyOpts{
MiahaCybersec marked this conversation as resolved.
Show resolved Hide resolved
bkClient, &solveOpt, image, ch,
reportFile, workingFolder, updates, ignoreError,
output, dockerNormalizedImageName, patchedImageName, format,
},
BuildStatus{buildChannel})

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L183-L190

Added lines #L183 - L190 were not covered by tests
return err
})

Expand All @@ -292,7 +207,8 @@
})

eg.Go(func() error {
if err := dockerLoad(ctx, pipeR); err != nil {
err = dockerLoad(ctx, pipeR)
if err != nil {

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L210-L211

Added lines #L210 - L211 were not covered by tests
return err
}
return pipeR.Close()
Expand All @@ -301,6 +217,146 @@
return eg.Wait()
}

func buildkitBuild(buildContext BuildContext, trivyOpts *TrivyOpts, buildStatus BuildStatus) error {
_, err := trivyOpts.BkClient.Build(buildContext.Ctx, *trivyOpts.SolveOpt, copaProduct, func(ctx context.Context, c gwclient.Client) (*gwclient.Result, error) {
bkConfig, err := buildkit.InitializeBuildkitConfig(ctx, c, trivyOpts.DockerNormalizedImageName.String())
if err != nil {
return handleError(trivyOpts.Ch, err)

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L220-L224

Added lines #L220 - L224 were not covered by tests
}

manager, err := resolvePackageManager(buildContext, trivyOpts, c, bkConfig)
if err != nil {
return handleError(trivyOpts.Ch, err)

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L227-L229

Added lines #L227 - L229 were not covered by tests
}

return buildReport(buildContext, trivyOpts, bkConfig, manager)

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L232

Added line #L232 was not covered by tests
}, buildStatus.BuildChannel)
return err

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L234

Added line #L234 was not covered by tests
}

func resolvePackageManager(buildContext BuildContext, trivyOpts *TrivyOpts, client gwclient.Client, config *buildkit.Config) (pkgmgr.PackageManager, error) {
var manager pkgmgr.PackageManager
if trivyOpts.ReportFile == "" {
fileBytes, err := buildkit.ExtractFileFromState(buildContext.Ctx, client, &config.ImageState, "/etc/os-release")
if err != nil {
return nil, err

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L237-L242

Added lines #L237 - L242 were not covered by tests
}

osType, err := getOSType(buildContext.Ctx, fileBytes)
if err != nil {
return nil, err

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L245-L247

Added lines #L245 - L247 were not covered by tests
}

osVersion, err := getOSVersion(buildContext.Ctx, fileBytes)
if err != nil {
return nil, err

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L250-L252

Added lines #L250 - L252 were not covered by tests
}
// get package manager based on os family type
manager, err = pkgmgr.GetPackageManager(osType, osVersion, config, trivyOpts.WorkingFolder)
if err != nil {
return nil, err

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L255-L257

Added lines #L255 - L257 were not covered by tests
}
} else {

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L259

Added line #L259 was not covered by tests
// get package manager based on os family type
var err error
manager, err = pkgmgr.GetPackageManager(trivyOpts.Updates.Metadata.OS.Type, trivyOpts.Updates.Metadata.OS.Version, config, trivyOpts.WorkingFolder)
if err != nil {
return nil, err

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L261-L264

Added lines #L261 - L264 were not covered by tests
}
}
return manager, nil

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L267

Added line #L267 was not covered by tests
}

// handleError streamlines error forwarding to error channel and returns the error again for further propagation.
func handleError(ch chan error, err error) (*gwclient.Result, error) {
ch <- err
return nil, err
}

// buildReport is an extracted method containing logic to manage the updates and build report.
func buildReport(buildContext BuildContext, trivyOpts *TrivyOpts, config *buildkit.Config, manager pkgmgr.PackageManager) (*gwclient.Result, error) {
patchedImageState, errPkgs, err := manager.InstallUpdates(buildContext.Ctx, trivyOpts.Updates, trivyOpts.IgnoreError)
if err != nil {
return handleError(trivyOpts.Ch, err)

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L277-L280

Added lines #L277 - L280 were not covered by tests
}
platform := platforms.Normalize(platforms.DefaultSpec())
if platform.OS != "linux" {
platform.OS = "linux"

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L282-L284

Added lines #L282 - L284 were not covered by tests
}
def, err := patchedImageState.Marshal(buildContext.Ctx, llb.Platform(platform))
if err != nil {
return handleError(trivyOpts.Ch, fmt.Errorf("unable to get platform from ImageState %w", err))

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L286-L288

Added lines #L286 - L288 were not covered by tests
}
res, err := config.Client.Solve(buildContext.Ctx, gwclient.SolveRequest{
Definition: def.ToPB(),
Evaluate: true,
})
if err != nil {
return handleError(trivyOpts.Ch, err)

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L290-L295

Added lines #L290 - L295 were not covered by tests
}
res.AddMeta(exptypes.ExporterImageConfigKey, config.ConfigData)

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L297

Added line #L297 was not covered by tests
// Currently can only validate updates if updating via scanner
if trivyOpts.ReportFile != "" {
validatedManifest := updateManifest(trivyOpts.Updates, errPkgs)

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L299-L300

Added lines #L299 - L300 were not covered by tests
// vex document must contain at least one statement
if trivyOpts.Output != "" && len(validatedManifest.Updates) > 0 {
err = vex.TryOutputVexDocument(validatedManifest, manager, trivyOpts.PatchedImageName, trivyOpts.Format, trivyOpts.Output)
if err != nil {
return handleError(trivyOpts.Ch, err)

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L302-L305

Added lines #L302 - L305 were not covered by tests
}
}
}
return res, nil

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L309

Added line #L309 was not covered by tests
}

// updateManifest creates a new manifest with the successfully patched packages.
func updateManifest(updates *unversioned.UpdateManifest, errPkgs []string) *unversioned.UpdateManifest {
validatedManifest := &unversioned.UpdateManifest{
Metadata: unversioned.Metadata{
OS: unversioned.OS{
Type: updates.Metadata.OS.Type,
Version: updates.Metadata.OS.Version,
},
Config: unversioned.Config{
Arch: updates.Metadata.Config.Arch,
},
},
Updates: []unversioned.UpdatePackage{},
}
for _, update := range updates.Updates {
if !slices.Contains(errPkgs, update.Name) {
validatedManifest.Updates = append(validatedManifest.Updates, update)
}
}
return validatedManifest
}

func generatePatchedTag(dockerNormalizedImageName reference.Named, userSuppliedPatchTag string) string {
// officialTag is typically the versioning tag of the image as published in a container registry
var officialTag string
MiahaCybersec marked this conversation as resolved.
Show resolved Hide resolved
var copaTag string

taggedName, ok := dockerNormalizedImageName.(reference.Tagged)

if ok {
officialTag = taggedName.Tag()
} else {
log.Warnf("Image name has no tag")
}

if userSuppliedPatchTag != "" {
copaTag = userSuppliedPatchTag
return copaTag
} else if officialTag == "" {
log.Warnf("No output tag specified for digest-referenced image, defaulting to `%s`", defaultPatchedTagSuffix)
copaTag = defaultPatchedTagSuffix
return copaTag
}

copaTag = fmt.Sprintf("%s-%s", officialTag, defaultPatchedTagSuffix)
return copaTag
}

func getOSType(ctx context.Context, osreleaseBytes []byte) (string, error) {
r := bytes.NewReader(osreleaseBytes)
osData, err := osrelease.Parse(ctx, r)
Expand Down
Loading
Loading