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 13 commits into
base: main
Choose a base branch
from
Draft
331 changes: 201 additions & 130 deletions pkg/patch/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,32 @@
defaultTag = "latest"
)

type TrivyOpts struct {
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 BkClient struct {
BkClient *client.Client
SolveOpt *client.SolveOpt
}

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 +100,34 @@
}
}

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
// reportFile is a vulnerability scan passed in by the user
// userSuppliedPatchTag is a tag set by the user to use for the patched image tag
// workingFolder is the folder used by copa, defaults to system temp folder
// scanner used to generate reportFile, defaults to Trivy
// format is the output format, defaults to openvex
// output is the desired output filepath
// ignoreError defines whether Copa should ignore errors
// bkOpts contains buildkitd options for addresses, CA certs, client certs, and client keys.
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 113 in pkg/patch/patch.go

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L112-L113

Added lines #L112 - L113 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)
}
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)
}

if reference.IsNameOnly(dockerNormalizedImageName) {
log.Warnf("Image name %s has no tag or digest, defaulting to %s:latest", image, dockerNormalizedImageName)
dockerNormalizedImageName = reference.TagNameOnly(dockerNormalizedImageName)

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L118-L120

Added lines #L118 - L120 were not covered by tests
}
_, err = reference.WithTag(imageName, patchedTag)

patchedTag := generatePatchedTag(dockerNormalizedImageName, userSuppliedPatchTag)

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L123

Added line #L123 was not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L125

Added line #L125 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 130 in pkg/patch/patch.go

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L130

Added line #L130 was not covered by tests

// Ensure working folder exists for call to InstallUpdates
if workingFolder == "" {
Expand Down Expand Up @@ -166,113 +191,17 @@
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
image, ch,
reportFile, workingFolder, updates, ignoreError,
output, dockerNormalizedImageName, patchedImageName, format,
},
BkClient{
bkClient, &solveOpt,
},
BuildStatus{buildChannel})

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L194-L204

Added lines #L194 - L204 were not covered by tests
return err
})

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

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

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L224-L225

Added lines #L224 - L225 were not covered by tests
return err
}
return pipeR.Close()
Expand All @@ -301,6 +231,147 @@
return eg.Wait()
}

// buildkitBuild submits a build request to BuildKit with the given information.
func buildkitBuild(buildContext BuildContext, trivyOpts *TrivyOpts, bkClient BkClient, buildStatus BuildStatus) error {
_, err := bkClient.BkClient.Build(buildContext.Ctx, *bkClient.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 239 in pkg/patch/patch.go

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L235-L239

Added lines #L235 - L239 were not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L242-L244

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

return buildReport(buildContext, trivyOpts, bkConfig, manager)

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L247

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

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L249

Added line #L249 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 257 in pkg/patch/patch.go

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L252-L257

Added lines #L252 - L257 were not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L260-L262

Added lines #L260 - L262 were not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L265-L267

Added lines #L265 - L267 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 272 in pkg/patch/patch.go

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L270-L272

Added lines #L270 - L272 were not covered by tests
}
} else {

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L274

Added line #L274 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 279 in pkg/patch/patch.go

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L276-L279

Added lines #L276 - L279 were not covered by tests
}
}
return manager, nil

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L282

Added line #L282 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 295 in pkg/patch/patch.go

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L292-L295

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

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L297-L299

Added lines #L297 - L299 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 303 in pkg/patch/patch.go

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L301-L303

Added lines #L301 - L303 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 310 in pkg/patch/patch.go

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L305-L310

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

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L312

Added line #L312 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 315 in pkg/patch/patch.go

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L314-L315

Added lines #L314 - L315 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 320 in pkg/patch/patch.go

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L317-L320

Added lines #L317 - L320 were not covered by tests
}
}
}
return res, nil

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

View check run for this annotation

Codecov / codecov/patch

pkg/patch/patch.go#L324

Added line #L324 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