diff --git a/cmd/krel/cmd/changelog_data_test.go b/cmd/krel/cmd/changelog_data_test.go index 80c792c27c4..b004cca4f01 100644 --- a/cmd/krel/cmd/changelog_data_test.go +++ b/cmd/krel/cmd/changelog_data_test.go @@ -52,7 +52,6 @@ const patchReleaseExpectedContent = `## Changes by Kind - Restores compatibility of kube-scheduler with clusters that do not enable the events.k8s.io/v1beta1 API ([#84465](https://github.com/kubernetes/kubernetes/pull/84465), [@yastij](https://github.com/yastij)) [SIG API Machinery and Scheduling] - Switched intstr.Type to sized integer to follow API guidelines and improve compatibility with proto libraries ([#83956](https://github.com/kubernetes/kubernetes/pull/83956), [@liggitt](https://github.com/liggitt)) [SIG API Machinery] - Update Cluster Autoscaler version to 1.16.2 (CA release docs: https://github.com/kubernetes/autoscaler/releases/tag/cluster-autoscaler-1.16.2) ([#84038](https://github.com/kubernetes/kubernetes/pull/84038), [@losipiuk](https://github.com/losipiuk)) [SIG Cluster Lifecycle] -- Update to use go1.12.12 ([#84064](https://github.com/kubernetes/kubernetes/pull/84064), [@cblecker](https://github.com/cblecker)) [SIG Release and Testing] - Upgrade to etcd client 3.3.17 to fix bug where etcd client does not parse IPv6 addresses correctly when members are joining, and to fix bug where failover on multi-member etcd cluster fails certificate check on DNS mismatch ([#83968](https://github.com/kubernetes/kubernetes/pull/83968), [@jpbetz](https://github.com/jpbetz)) [SIG API Machinery and Cloud Provider]` const patchReleaseDeps = `## Dependencies @@ -216,7 +215,6 @@ const patchReleaseExpectedHTML = `
  • Restores compatibility of kube-scheduler with clusters that do not enable the events.k8s.io/v1beta1 API (#84465, @yastij) [SIG API Machinery and Scheduling]
  • Switched intstr.Type to sized integer to follow API guidelines and improve compatibility with proto libraries (#83956, @liggitt) [SIG API Machinery]
  • Update Cluster Autoscaler version to 1.16.2 (CA release docs: https://github.com/kubernetes/autoscaler/releases/tag/cluster-autoscaler-1.16.2) (#84038, @losipiuk) [SIG Cluster Lifecycle]
  • -
  • Update to use go1.12.12 (#84064, @cblecker) [SIG Release and Testing]
  • Upgrade to etcd client 3.3.17 to fix bug where etcd client does not parse IPv6 addresses correctly when members are joining, and to fix bug where failover on multi-member etcd cluster fails certificate check on DNS mismatch (#83968, @jpbetz) [SIG API Machinery and Cloud Provider]
  • Dependencies

    diff --git a/cmd/krel/cmd/changelog_test.go b/cmd/krel/cmd/changelog_test.go index 5354b67ec14..650dfbe8a8c 100644 --- a/cmd/krel/cmd/changelog_test.go +++ b/cmd/krel/cmd/changelog_test.go @@ -30,11 +30,12 @@ import ( func (s *sut) getChangelogOptions(tag string) *changelog.Options { return &changelog.Options{ - RepoPath: s.repo.Dir(), - ReplayDir: filepath.Join(testDataDir, "changelog-"+tag), - Tag: tag, - Tars: ".", - Branch: git.DefaultBranch, + RepoPath: s.repo.Dir(), + ReplayDir: filepath.Join(testDataDir, "changelog-"+tag), + Tag: tag, + Tars: ".", + Branch: git.DefaultBranch, + CloneCVEMaps: false, } } diff --git a/pkg/anago/stage.go b/pkg/anago/stage.go index 91c7b53276c..067afff5ed8 100644 --- a/pkg/anago/stage.go +++ b/pkg/anago/stage.go @@ -491,6 +491,7 @@ func (d *DefaultStage) GenerateChangelog() error { HTMLFile: releaseNotesHTMLFile, JSONFile: releaseNotesJSONFile, Dependencies: true, + CloneCVEMaps: true, Tars: filepath.Join( gitRoot, fmt.Sprintf("%s-%s", release.BuildDir, d.state.versions.Prime()), diff --git a/pkg/changelog/changelog.go b/pkg/changelog/changelog.go index 91e4df1c968..3b1ff119c61 100644 --- a/pkg/changelog/changelog.go +++ b/pkg/changelog/changelog.go @@ -45,6 +45,8 @@ type Options struct { JSONFile string RecordDir string ReplayDir string + CVEDataDir string + CloneCVEMaps bool Dependencies bool } @@ -158,6 +160,14 @@ func (c *Changelog) Run() error { } } } else { + if c.options.CloneCVEMaps { + cveDir, err := c.impl.CloneCVEData() + if err != nil { + return errors.Wrap(err, "getting cve data maps") + } + c.options.CVEDataDir = cveDir + } + // A patch version, let’s just use the previous patch startTag := util.SemverToTagString(semver.Version{ Major: tag.Major, Minor: tag.Minor, Patch: tag.Patch - 1, @@ -244,6 +254,12 @@ func (c *Changelog) generateReleaseNotes( notesOptions.ReplayDir = c.options.ReplayDir notesOptions.Pull = false + if c.options.CVEDataDir != "" { + notesOptions.MapProviderStrings = append( + notesOptions.MapProviderStrings, c.options.CVEDataDir, + ) + } + if err := c.impl.ValidateAndFinish(notesOptions); err != nil { return "", "", errors.Wrap(err, "validating notes options") } diff --git a/pkg/changelog/changelog_test.go b/pkg/changelog/changelog_test.go index 06c9c187ede..59498475b3c 100644 --- a/pkg/changelog/changelog_test.go +++ b/pkg/changelog/changelog_test.go @@ -265,6 +265,32 @@ func TestRun(t *testing.T) { }, shouldErr: true, }, + { // CloneCVEData returns error + prepare: func(mock *changelogfakes.FakeImpl, o *changelog.Options) { + o.CloneCVEMaps = true + mock.TagStringToSemverReturns(semver.Version{ + Major: 1, + Minor: 19, + Patch: 3, + }, nil) + mock.CloneCVEDataReturns("", err) + }, + shouldErr: true, + }, + { // CloneCVEData returns empty string + prepare: func(mock *changelogfakes.FakeImpl, o *changelog.Options) { + o.CloneCVEMaps = true + mock.TagStringToSemverReturns(semver.Version{ + Major: 1, + Minor: 19, + Patch: 3, + }, nil) + mock.ReadFileReturns([]byte(changelog.TocEnd), nil) + mock.GatherReleaseNotesReturns(¬es.ReleaseNotes{}, nil) + mock.CloneCVEDataReturns("", nil) + }, + shouldErr: false, + }, } { options := &changelog.Options{} sut := changelog.New(options) diff --git a/pkg/changelog/changelogfakes/fake_impl.go b/pkg/changelog/changelogfakes/fake_impl.go index 39c26e4bf6f..6d9030b7224 100644 --- a/pkg/changelog/changelogfakes/fake_impl.go +++ b/pkg/changelog/changelogfakes/fake_impl.go @@ -1,5 +1,5 @@ /* -Copyright The Kubernetes Authors. +Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -71,6 +71,18 @@ type FakeImpl struct { checkoutReturnsOnCall map[int]struct { result1 error } + CloneCVEDataStub func() (string, error) + cloneCVEDataMutex sync.RWMutex + cloneCVEDataArgsForCall []struct { + } + cloneCVEDataReturns struct { + result1 string + result2 error + } + cloneCVEDataReturnsOnCall map[int]struct { + result1 string + result2 error + } CommitStub func(*git.Repo, string) error commitMutex sync.RWMutex commitArgsForCall []struct { @@ -567,6 +579,62 @@ func (fake *FakeImpl) CheckoutReturnsOnCall(i int, result1 error) { }{result1} } +func (fake *FakeImpl) CloneCVEData() (string, error) { + fake.cloneCVEDataMutex.Lock() + ret, specificReturn := fake.cloneCVEDataReturnsOnCall[len(fake.cloneCVEDataArgsForCall)] + fake.cloneCVEDataArgsForCall = append(fake.cloneCVEDataArgsForCall, struct { + }{}) + stub := fake.CloneCVEDataStub + fakeReturns := fake.cloneCVEDataReturns + fake.recordInvocation("CloneCVEData", []interface{}{}) + fake.cloneCVEDataMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImpl) CloneCVEDataCallCount() int { + fake.cloneCVEDataMutex.RLock() + defer fake.cloneCVEDataMutex.RUnlock() + return len(fake.cloneCVEDataArgsForCall) +} + +func (fake *FakeImpl) CloneCVEDataCalls(stub func() (string, error)) { + fake.cloneCVEDataMutex.Lock() + defer fake.cloneCVEDataMutex.Unlock() + fake.CloneCVEDataStub = stub +} + +func (fake *FakeImpl) CloneCVEDataReturns(result1 string, result2 error) { + fake.cloneCVEDataMutex.Lock() + defer fake.cloneCVEDataMutex.Unlock() + fake.CloneCVEDataStub = nil + fake.cloneCVEDataReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeImpl) CloneCVEDataReturnsOnCall(i int, result1 string, result2 error) { + fake.cloneCVEDataMutex.Lock() + defer fake.cloneCVEDataMutex.Unlock() + fake.CloneCVEDataStub = nil + if fake.cloneCVEDataReturnsOnCall == nil { + fake.cloneCVEDataReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.cloneCVEDataReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + func (fake *FakeImpl) Commit(arg1 *git.Repo, arg2 string) error { fake.commitMutex.Lock() ret, specificReturn := fake.commitReturnsOnCall[len(fake.commitArgsForCall)] @@ -2042,6 +2110,8 @@ func (fake *FakeImpl) Invocations() map[string][][]interface{} { defer fake.addMutex.RUnlock() fake.checkoutMutex.RLock() defer fake.checkoutMutex.RUnlock() + fake.cloneCVEDataMutex.RLock() + defer fake.cloneCVEDataMutex.RUnlock() fake.commitMutex.RLock() defer fake.commitMutex.RUnlock() fake.createDownloadsTableMutex.RLock() diff --git a/pkg/changelog/const.go b/pkg/changelog/const.go index 81ab8123991..c901f6e24c9 100644 --- a/pkg/changelog/const.go +++ b/pkg/changelog/const.go @@ -66,6 +66,24 @@ filename | sha512 hash {{- end -}} ## Changelog since {{$PreviousRevision}} +{{with .CVEList -}} +## Important Security Information + +This release contains changes that address the following vulnerabilities: +{{range .}} +### {{.ID}}: {{.Title}} + +{{.Description}} + +**CVSS Rating:** {{.CVSSRating}} ({{.CVSSScore}}) [{{.CVSSVector}}]({{.CalcLink}}) +{{- if .TrackingIssue -}} +
    +**Tracking Issue:** {{.TrackingIssue}} +{{- end }} + +{{ end }} +{{- end -}} + {{with .NotesWithActionRequired -}} ## Urgent Upgrade Notes diff --git a/pkg/changelog/impl.go b/pkg/changelog/impl.go index 312aff3216d..a241907836f 100644 --- a/pkg/changelog/impl.go +++ b/pkg/changelog/impl.go @@ -23,16 +23,20 @@ import ( "text/template" "github.com/blang/semver" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" "sigs.k8s.io/mdtoc/pkg/mdtoc" + "k8s.io/release/pkg/cve" "k8s.io/release/pkg/git" "k8s.io/release/pkg/github" "k8s.io/release/pkg/notes" "k8s.io/release/pkg/notes/document" "k8s.io/release/pkg/notes/options" + "k8s.io/release/pkg/object" "sigs.k8s.io/release-utils/http" "sigs.k8s.io/release-utils/util" ) @@ -88,6 +92,7 @@ type impl interface { Add(repo *git.Repo, filename string) error Commit(repo *git.Repo, msg string) error Rm(repo *git.Repo, force bool, files ...string) error + CloneCVEData() (cveDir string, err error) } type defaultImpl struct{} @@ -215,3 +220,34 @@ func (*defaultImpl) Commit(repo *git.Repo, msg string) error { func (*defaultImpl) Rm(repo *git.Repo, force bool, files ...string) error { return repo.Rm(force, files...) } + +// CloneCVEData copies the CVE data maps from the release bucket +func (*defaultImpl) CloneCVEData() (cveDir string, err error) { + tmpdir, err := os.MkdirTemp(os.TempDir(), "cve-maps-") + if err != nil { + return "", errors.Wrap(err, "creating temporary dir for CVE data") + } + + // Create a new GCS client + gcs := object.NewGCS() + remoteSrc, err := gcs.NormalizePath(object.GcsPrefix + filepath.Join(cve.Bucket, cve.Directory)) + if err != nil { + return "", errors.Wrap(err, "normalizing cve bucket path") + } + + bucketExists, err := gcs.PathExists(remoteSrc) + if err != nil { + return "", errors.Wrap(err, "checking if CVE directory exists") + } + if !bucketExists { + logrus.Warnf("CVE data maps not found in bucket location: %s", remoteSrc) + return "", nil + } + + // Rsync the CVE files to the temp dir + if err := gcs.RsyncRecursive(remoteSrc, tmpdir); err != nil { + return "", errors.Wrap(err, "copying release directory to bucket") + } + logrus.Infof("Successfully synchronized CVE data maps from %s", remoteSrc) + return tmpdir, nil +} diff --git a/pkg/notes/document/document.go b/pkg/notes/document/document.go index cfa4318360a..fc3bf6d460a 100644 --- a/pkg/notes/document/document.go +++ b/pkg/notes/document/document.go @@ -217,11 +217,6 @@ func New( for _, pr := range releaseNotes.History() { note := releaseNotes.Get(pr) - if note.DoNotPublish { - logrus.Debugf("skipping PR %d as (marked to not be published)", pr) - continue - } - if _, hasCVE := note.DataFields["cve"]; hasCVE { logrus.Infof("Release note for PR #%d has CVE vulnerability info", note.PrNumber) @@ -240,6 +235,11 @@ func New( doc.CVEList = append(doc.CVEList, newcve) } + if note.DoNotPublish { + logrus.Debugf("skipping PR %d as (marked to not be published)", pr) + continue + } + // TODO: Refactor the logic here and add testing. if note.DuplicateKind { kind := mapKind(highestPriorityKind(note.Kinds)) diff --git a/pkg/notes/notes.go b/pkg/notes/notes.go index 5274d9ae28c..2c5a97bf937 100644 --- a/pkg/notes/notes.go +++ b/pkg/notes/notes.go @@ -127,7 +127,7 @@ type ReleaseNote struct { DoNotPublish bool `json:"do_not_publish,omitempty"` // DataFields a key indexed map of data fields - DataFields map[string]ReleaseNotesDataField `json:"data_fields,omitempty"` + DataFields map[string]ReleaseNotesDataField `json:"-"` } type Documentation struct { @@ -269,11 +269,45 @@ func (g *Gatherer) ListReleaseNotes() (*ReleaseNotes, error) { return nil, errors.Wrap(err, "listing commits") } - results, err := g.gatherNotes(commits) + // Get the PRs into a temporary results set + resultsTemp, err := g.gatherNotes(commits) if err != nil { return nil, errors.Wrap(err, "gathering notes") } + // Cycle the results and add the complete notes, as well as those that + // have a map associated with it + results := []*Result{} + logrus.Info("Checking PRs for mapped data") + for _, res := range resultsTemp { + // If the PR has no release note, check if we have to add it + if MatchesExcludeFilter(*res.pullRequest.Body) { + for _, provider := range mapProviders { + noteMaps, err := provider.GetMapsForPR(res.pullRequest.GetNumber()) + if err != nil { + return nil, errors.Wrapf( + err, "checking if a map exists for PR %d", res.pullRequest.GetNumber(), + ) + } + if len(noteMaps) != 0 { + logrus.Infof( + "Artificially adding pr #%d because a map for it was found", + res.pullRequest.GetNumber(), + ) + results = append(results, res) + } else { + logrus.Debugf( + "Skipping PR #%d because it contains no release note", + res.pullRequest.GetNumber(), + ) + } + } + } else { + // Append the note as it is + results = append(results, res) + } + } + dedupeCache := map[string]struct{}{} notes := NewReleaseNotes() for _, result := range results { @@ -311,7 +345,6 @@ func (g *Gatherer) ListReleaseNotes() (*ReleaseNotes, error) { dedupeCache[note.Text] = struct{}{} } } - return notes, nil } @@ -676,14 +709,6 @@ func (g *Gatherer) notesForCommit(commit *gogithub.RepositoryCommit) (*Result, e "Got PR #%d for commit: %s", pr.GetNumber(), commit.GetSHA(), ) - if MatchesExcludeFilter(prBody) { - logrus.Debugf( - "Skipping PR #%d because it contains no release note", - pr.GetNumber(), - ) - continue - } - if MatchesIncludeFilter(prBody) { res := &Result{commit: commit, pullRequest: pr} logrus.Infof("PR #%d seems to contain a release note", pr.GetNumber()) diff --git a/pkg/notes/notes_gatherer_test.go b/pkg/notes/notes_gatherer_test.go index be5a5cd1b69..052d2ea911a 100644 --- a/pkg/notes/notes_gatherer_test.go +++ b/pkg/notes/notes_gatherer_test.go @@ -370,7 +370,7 @@ func TestGatherNotes(t *testing.T) { resultsChecker: func(t *testing.T, results []*Result) { // there is not much we can check on the Result, as all the fields are // unexported - expectedResultSize := 7 + expectedResultSize := 13 if e, a := expectedResultSize, len(results); e != a { t.Errorf("Expected the result to be of size %d, got %d", e, a) } diff --git a/pkg/notes/options/options.go b/pkg/notes/options/options.go index 3d0dfd50ad8..3455066bfda 100644 --- a/pkg/notes/options/options.go +++ b/pkg/notes/options/options.go @@ -146,13 +146,14 @@ const ( // New creates a new Options instance with the default values func New() *Options { return &Options{ - DiscoverMode: RevisionDiscoveryModeNONE, - GithubOrg: git.DefaultGithubOrg, - GithubRepo: git.DefaultGithubRepo, - Format: FormatMarkdown, - GoTemplate: GoTemplateDefault, - Pull: true, - gitCloneFn: git.CloneOrOpenGitHubRepo, + DiscoverMode: RevisionDiscoveryModeNONE, + GithubOrg: git.DefaultGithubOrg, + GithubRepo: git.DefaultGithubRepo, + Format: FormatMarkdown, + GoTemplate: GoTemplateDefault, + Pull: true, + gitCloneFn: git.CloneOrOpenGitHubRepo, + MapProviderStrings: []string{}, } }