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  }