2020-10-14 00:44:08 +00:00
/ *
Copyright 2020 The Kubernetes Authors All rights reserved .
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
package update
import (
"context"
"fmt"
"os"
"strings"
"time"
2020-10-16 23:07:25 +00:00
"golang.org/x/mod/semver"
2020-10-14 00:44:08 +00:00
"golang.org/x/oauth2"
2021-06-29 19:46:23 +00:00
"github.com/google/go-github/v36/github"
2020-10-14 00:44:08 +00:00
"k8s.io/klog/v2"
)
const (
2020-10-22 22:00:46 +00:00
// ghListPerPage uses max value (100) for PerPage to avoid hitting the rate limits.
2020-10-14 00:44:08 +00:00
// (ref: https://godoc.org/github.com/google/go-github/github#hdr-Rate_Limiting)
ghListPerPage = 100
2020-10-22 22:00:46 +00:00
// ghSearchLimit limits the number of searched items to be <= N * ghListPerPage.
2020-10-14 00:44:08 +00:00
ghSearchLimit = 100
)
var (
// GitHub repo data
ghToken = os . Getenv ( "GITHUB_TOKEN" )
ghOwner = "kubernetes"
ghRepo = "minikube"
2020-10-22 22:00:46 +00:00
ghBase = "master" // could be "main" in the near future?
2020-10-14 00:44:08 +00:00
)
2020-10-22 22:00:46 +00:00
// ghCreatePR returns PR created in the GitHub owner/repo, applying the changes to the base head commit fork, as defined by the schema and data.
// Returns any error occurred.
// PR branch will be named by the branch, sufixed by '_' and first 7 characters of the fork commit SHA.
// PR itself will be named by the title and will reference the issue.
2020-10-14 00:44:08 +00:00
func ghCreatePR ( ctx context . Context , owner , repo , base , branch , title string , issue int , token string , schema map [ string ] Item , data interface { } ) ( * github . PullRequest , error ) {
ghc := ghClient ( ctx , token )
// get base branch
baseBranch , _ , err := ghc . Repositories . GetBranch ( ctx , owner , repo , base )
if err != nil {
2020-10-22 22:00:46 +00:00
return nil , fmt . Errorf ( "unable to get base branch: %w" , err )
2020-10-14 00:44:08 +00:00
}
// get base commit
baseCommit , _ , err := ghc . Repositories . GetCommit ( ctx , owner , repo , * baseBranch . Commit . SHA )
if err != nil {
2020-10-22 22:00:46 +00:00
return nil , fmt . Errorf ( "unable to get base commit: %w" , err )
2020-10-14 00:44:08 +00:00
}
// get base tree
baseTree , _ , err := ghc . Git . GetTree ( ctx , owner , repo , baseCommit . GetSHA ( ) , true )
if err != nil {
2020-10-22 22:00:46 +00:00
return nil , fmt . Errorf ( "unable to get base tree: %w" , err )
2020-10-14 00:44:08 +00:00
}
// update files
2020-11-15 04:04:58 +00:00
changes , err := ghUpdate ( ctx , owner , repo , token , schema , data )
2020-10-14 00:44:08 +00:00
if err != nil {
2020-10-22 22:00:46 +00:00
return nil , fmt . Errorf ( "unable to update files: %w" , err )
2020-10-14 00:44:08 +00:00
}
if changes == nil {
return nil , nil
}
// create fork
fork , resp , err := ghc . Repositories . CreateFork ( ctx , owner , repo , nil )
2020-10-22 22:00:46 +00:00
// "This method might return an *AcceptedError and a status code of 202.
// This is because this is the status that GitHub returns to signify that it is now computing creating the fork in a background task.
// In this event, the Repository value will be returned, which includes the details about the pending fork.
// A follow up request, after a delay of a second or so, should result in a successful request."
// (ref: https://pkg.go.dev/github.com/google/go-github/v32@v32.1.0/github#RepositoriesService.CreateFork)
2020-10-14 00:44:08 +00:00
if resp . StatusCode == 202 { // *AcceptedError
time . Sleep ( time . Second * 5 )
} else if err != nil {
2020-10-22 22:00:46 +00:00
return nil , fmt . Errorf ( "unable to create fork: %w" , err )
2020-10-14 00:44:08 +00:00
}
// create fork tree from base and changed files
forkTree , _ , err := ghc . Git . CreateTree ( ctx , * fork . Owner . Login , * fork . Name , * baseTree . SHA , changes )
if err != nil {
2020-10-22 22:00:46 +00:00
return nil , fmt . Errorf ( "unable to create fork tree: %w" , err )
2020-10-14 00:44:08 +00:00
}
// create fork commit
forkCommit , _ , err := ghc . Git . CreateCommit ( ctx , * fork . Owner . Login , * fork . Name , & github . Commit {
Message : github . String ( title ) ,
Tree : & github . Tree { SHA : forkTree . SHA } ,
Parents : [ ] * github . Commit { { SHA : baseCommit . SHA } } ,
} )
if err != nil {
2020-10-22 22:00:46 +00:00
return nil , fmt . Errorf ( "unable to create fork commit: %w" , err )
2020-10-14 00:44:08 +00:00
}
2020-10-22 22:00:46 +00:00
klog . Infof ( "PR commit '%s' successfully created: %s" , forkCommit . GetSHA ( ) , forkCommit . GetHTMLURL ( ) )
2020-10-14 00:44:08 +00:00
// create PR branch
prBranch := branch + forkCommit . GetSHA ( ) [ : 7 ]
prRef , _ , err := ghc . Git . CreateRef ( ctx , * fork . Owner . Login , * fork . Name , & github . Reference {
Ref : github . String ( "refs/heads/" + prBranch ) ,
Object : & github . GitObject {
Type : github . String ( "commit" ) ,
SHA : forkCommit . SHA ,
} ,
} )
if err != nil {
2020-10-22 22:00:46 +00:00
return nil , fmt . Errorf ( "unable to create PR branch: %w" , err )
2020-10-14 00:44:08 +00:00
}
2020-10-22 22:00:46 +00:00
klog . Infof ( "PR branch '%s' successfully created: %s" , prBranch , prRef . GetURL ( ) )
2020-10-14 00:44:08 +00:00
// create PR
2020-11-15 04:04:58 +00:00
_ , pretty , err := GetPlan ( schema , data )
2020-10-14 00:44:08 +00:00
if err != nil {
2020-11-15 04:04:58 +00:00
klog . Fatalf ( "Unable to parse schema: %v\n%s" , err , pretty )
2020-10-14 00:44:08 +00:00
}
modifiable := true
pr , _ , err := ghc . PullRequests . Create ( ctx , owner , repo , & github . NewPullRequest {
Title : github . String ( title ) ,
Head : github . String ( * fork . Owner . Login + ":" + prBranch ) ,
Base : github . String ( base ) ,
2020-11-15 04:04:58 +00:00
Body : github . String ( fmt . Sprintf ( "fixes: #%d\n\nAutomatically created PR to update repo according to the Plan:\n\n```\n%s\n```" , issue , pretty ) ) ,
2020-10-14 00:44:08 +00:00
MaintainerCanModify : & modifiable ,
} )
if err != nil {
2020-10-22 22:00:46 +00:00
return nil , fmt . Errorf ( "unable to create PR: %w" , err )
2020-10-14 00:44:08 +00:00
}
return pr , nil
}
2020-10-22 22:00:46 +00:00
// ghFindPR returns URL of the PR if found in the given GitHub ower/repo base and any error occurred.
func ghFindPR ( ctx context . Context , title , owner , repo , base , token string ) ( url string , err error ) {
ghc := ghClient ( ctx , token )
// walk through the paginated list of up to ghSearchLimit newest pull requests
opts := & github . PullRequestListOptions { State : "all" , Base : base , ListOptions : github . ListOptions { PerPage : ghListPerPage } }
for ( opts . Page + 1 ) * ghListPerPage <= ghSearchLimit {
prs , resp , err := ghc . PullRequests . List ( ctx , owner , repo , opts )
if err != nil {
return "" , err
}
for _ , pr := range prs {
if pr . GetTitle ( ) == title {
return pr . GetHTMLURL ( ) , nil
}
}
if resp . NextPage == 0 {
break
}
opts . Page = resp . NextPage
}
return "" , nil
}
// ghUpdate updates remote GitHub owner/repo tree according to the given token, schema and data.
// Returns resulting changes, and any error occurred.
2020-11-15 04:04:58 +00:00
func ghUpdate ( ctx context . Context , owner , repo string , token string , schema map [ string ] Item , data interface { } ) ( changes [ ] * github . TreeEntry , err error ) {
2020-10-14 00:44:08 +00:00
ghc := ghClient ( ctx , token )
// load each schema item content and update it creating new GitHub TreeEntries
2020-11-15 04:04:58 +00:00
for path , item := range schema {
// if the item's content is already set, give it precedence over any current file content
var content string
if item . Content == nil {
file , _ , _ , err := ghc . Repositories . GetContents ( ctx , owner , repo , path , & github . RepositoryContentGetOptions { Ref : ghBase } )
if err != nil {
return nil , fmt . Errorf ( "unable to get file content: %w" , err )
2020-10-14 00:44:08 +00:00
}
2020-11-15 04:04:58 +00:00
content , err = file . GetContent ( )
if err != nil {
return nil , fmt . Errorf ( "unable to read file content: %w" , err )
}
item . Content = [ ] byte ( content )
}
2020-11-15 04:41:12 +00:00
if err := item . apply ( data ) ; err != nil {
2020-11-15 04:04:58 +00:00
return nil , fmt . Errorf ( "unable to update file: %w" , err )
}
if content != string ( item . Content ) {
// add github.TreeEntry that will replace original path content with the updated one or add new if one doesn't exist already
// ref: https://developer.github.com/v3/git/trees/#tree-object
rcPath := path // make sure to copy path variable as its reference (not value!) is passed to changes
rcMode := "100644"
rcType := "blob"
changes = append ( changes , & github . TreeEntry {
Path : & rcPath ,
Mode : & rcMode ,
Type : & rcType ,
Content : github . String ( string ( item . Content ) ) ,
} )
2020-10-14 00:44:08 +00:00
}
}
return changes , nil
}
2020-10-22 22:00:46 +00:00
// GHReleases returns greatest current stable release and greatest latest rc or beta pre-release from GitHub owner/repo repository, and any error occurred.
// If latest pre-release version is lower than the current stable release, then it will return current stable release for both.
func GHReleases ( ctx context . Context , owner , repo string ) ( stable , latest string , err error ) {
2020-10-14 00:44:08 +00:00
ghc := ghClient ( ctx , ghToken )
2020-10-22 22:00:46 +00:00
// walk through the paginated list of up to ghSearchLimit newest releases
2020-10-14 00:44:08 +00:00
opts := & github . ListOptions { PerPage : ghListPerPage }
2020-10-22 22:00:46 +00:00
for ( opts . Page + 1 ) * ghListPerPage <= ghSearchLimit {
2020-12-10 01:28:40 +00:00
rls , resp , err := ghc . Repositories . ListTags ( ctx , owner , repo , opts )
2020-10-14 00:44:08 +00:00
if err != nil {
return "" , "" , err
}
for _ , rl := range rls {
2020-12-10 01:28:40 +00:00
ver := * rl . Name
2020-10-16 23:07:25 +00:00
if ! semver . IsValid ( ver ) {
2020-10-14 00:44:08 +00:00
continue
}
2020-10-16 23:07:25 +00:00
// check if ver version is release (ie, 'v1.19.2') or pre-release (ie, 'v1.19.3-rc.0' or 'v1.19.0-beta.2')
prerls := semver . Prerelease ( ver )
if prerls == "" {
2021-02-22 17:22:13 +00:00
if semver . Compare ( ver , stable ) == 1 {
stable = ver
}
2020-10-16 23:07:25 +00:00
} else if strings . HasPrefix ( prerls , "-rc" ) || strings . HasPrefix ( prerls , "-beta" ) {
2021-02-22 17:22:13 +00:00
if semver . Compare ( ver , latest ) == 1 {
latest = ver
}
2020-10-14 00:44:08 +00:00
}
2020-10-16 23:07:25 +00:00
// make sure that latest >= stable
if semver . Compare ( latest , stable ) == - 1 {
latest = stable
2020-10-14 00:44:08 +00:00
}
}
if resp . NextPage == 0 {
break
}
opts . Page = resp . NextPage
}
return stable , latest , nil
}
2020-10-22 22:00:46 +00:00
// ghClient returns GitHub Client with a given context and optional token for authenticated requests.
func ghClient ( ctx context . Context , token string ) * github . Client {
if token == "" {
return github . NewClient ( nil )
}
ts := oauth2 . StaticTokenSource (
& oauth2 . Token { AccessToken : token } ,
)
tc := oauth2 . NewClient ( ctx , ts )
return github . NewClient ( tc )
}