github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/plugins/testfreeze/checker/checker.go (about) 1 /* 2 Copyright 2022 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package checker 18 19 import ( 20 "encoding/json" 21 "errors" 22 "fmt" 23 "io" 24 "net/http" 25 "strings" 26 "time" 27 28 "github.com/blang/semver/v4" 29 git "github.com/go-git/go-git/v5" 30 gitconfig "github.com/go-git/go-git/v5/config" 31 "github.com/go-git/go-git/v5/plumbing" 32 gitmemory "github.com/go-git/go-git/v5/storage/memory" 33 "github.com/sirupsen/logrus" 34 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 35 v1 "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 36 ) 37 38 const ( 39 prowjobsURL = "https://prow.k8s.io/prowjobs.js?omit=annotations,labels,decoration_config,pod_spec" 40 jobName = "ci-fast-forward" 41 unknownTime = "unknown" 42 ) 43 44 // Checker is the main structure of checking if we're in Test Freeze. 45 type Checker struct { 46 checker checker 47 log *logrus.Entry 48 } 49 50 // Result is the result returned by `InTestFreeze`. 51 type Result struct { 52 // InTestFreeze is true if we're in Test Freeze. 53 InTestFreeze bool 54 55 // Branch is the found latest release branch. 56 Branch string 57 58 // Tag is the latest minor release tag to be expected. 59 Tag string 60 61 // LastFastForward specifies the latest point int time when a fast forward 62 // was successful. 63 LastFastForward string 64 } 65 66 //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate 67 //counterfeiter:generate . checker 68 type checker interface { 69 ListRefs(*git.Remote) ([]*plumbing.Reference, error) 70 HttpGet(string) (*http.Response, error) 71 CloseBody(*http.Response) error 72 ReadAllBody(*http.Response) ([]byte, error) 73 UnmarshalProwJobs([]byte) (*v1.ProwJobList, error) 74 } 75 76 type defaultChecker struct{} 77 78 // New creates a new Checker instance. 79 func New(log *logrus.Entry) *Checker { 80 return &Checker{ 81 checker: &defaultChecker{}, 82 log: log, 83 } 84 } 85 86 // InTestFreeze returns if we're in Test Freeze: 87 // https://github.com/kubernetes/sig-release/blob/2d8a1cc/releases/release_phases.md#test-freeze 88 // It errors in case of any issue. 89 func (c *Checker) InTestFreeze() (*Result, error) { 90 remote := git.NewRemote(gitmemory.NewStorage(), &gitconfig.RemoteConfig{ 91 Name: "origin", 92 URLs: []string{"https://github.com/kubernetes/kubernetes"}, 93 }) 94 95 refs, err := c.checker.ListRefs(remote) 96 if err != nil { 97 c.log.Errorf("Unable to list git remote: %v", err) 98 return nil, fmt.Errorf("list git remote: %w", err) 99 } 100 101 const releaseBranchPrefix = "release-" 102 var ( 103 latestSemver semver.Version 104 latestBranch string 105 ) 106 107 for _, ref := range refs { 108 if ref.Name().IsBranch() { 109 branch := ref.Name().Short() 110 111 // Filter for release branches 112 if !strings.HasPrefix(branch, releaseBranchPrefix) { 113 continue 114 } 115 116 // Try to parse the latest minor version 117 version := strings.TrimPrefix(branch, releaseBranchPrefix) + ".0" 118 119 parsed, err := semver.Parse(version) 120 if err != nil { 121 c.log.WithField("version", version).WithError(err).Debug("Unable to parse version.") 122 continue 123 } 124 125 if parsed.GT(latestSemver) { 126 latestSemver = parsed 127 latestBranch = branch 128 } 129 } 130 } 131 132 if latestBranch == "" { 133 return nil, errors.New("no latest release branch found") 134 } 135 136 for _, ref := range refs { 137 if ref.Name().IsTag() { 138 tag := strings.TrimPrefix(ref.Name().Short(), "v") 139 140 parsed, err := semver.Parse(tag) 141 if err != nil { 142 c.log.WithField("tag", tag).WithError(err).Debug("Unable to parse tag.") 143 continue 144 } 145 146 // Found the latest minor version on the latest release branch, 147 // which means we're not in Test Freeze. 148 if latestSemver.EQ(parsed) { 149 return &Result{ 150 InTestFreeze: false, 151 Branch: latestBranch, 152 Tag: "v" + tag, 153 }, nil 154 } 155 } 156 } 157 158 lastFastForward := unknownTime 159 last, err := c.lastFastForward() 160 if err != nil { 161 c.log.WithError(err).Error("Unable to get last fast forward result.") 162 } else { 163 lastFastForward = last.Format(time.UnixDate) 164 } 165 166 // Latest minor version not found in latest release branch, 167 // we're in Test Freeze. 168 return &Result{ 169 InTestFreeze: true, 170 Branch: latestBranch, 171 Tag: "v" + latestSemver.String(), 172 LastFastForward: lastFastForward, 173 }, nil 174 } 175 176 func (c *Checker) lastFastForward() (*metav1.Time, error) { 177 resp, err := c.checker.HttpGet(prowjobsURL) 178 if err != nil { 179 return nil, fmt.Errorf("get prow jobs: %w", err) 180 } 181 defer c.checker.CloseBody(resp) 182 183 body, err := c.checker.ReadAllBody(resp) 184 if err != nil { 185 return nil, fmt.Errorf("read response body: %w", err) 186 } 187 188 prowJobs, err := c.checker.UnmarshalProwJobs(body) 189 if err != nil { 190 return nil, fmt.Errorf("unmarshal prow jobs: %w", err) 191 } 192 193 for _, job := range prowJobs.Items { 194 if job.Spec.Job == jobName && job.Status.State == v1.SuccessState { 195 return job.Status.CompletionTime, nil 196 } 197 } 198 199 return nil, errors.New("unable to find successful run") 200 } 201 202 func (*defaultChecker) ListRefs(r *git.Remote) ([]*plumbing.Reference, error) { 203 return r.List(&git.ListOptions{}) 204 } 205 206 func (*defaultChecker) HttpGet(url string) (*http.Response, error) { 207 return http.Get(url) 208 } 209 210 func (*defaultChecker) CloseBody(resp *http.Response) error { 211 return resp.Body.Close() 212 } 213 214 func (*defaultChecker) ReadAllBody(resp *http.Response) ([]byte, error) { 215 return io.ReadAll(resp.Body) 216 } 217 218 func (*defaultChecker) UnmarshalProwJobs(data []byte) (*v1.ProwJobList, error) { 219 prowJobs := &v1.ProwJobList{} 220 if err := json.Unmarshal(data, prowJobs); err != nil { 221 return nil, err 222 } 223 return prowJobs, nil 224 }