2020-07-02 19:36:10 +01:00
package src
import (
"context"
"fmt"
2020-09-18 16:50:26 -04:00
"os"
2020-07-02 19:36:10 +01:00
"path"
2020-09-21 17:40:48 -04:00
"strings"
2020-07-02 19:36:10 +01:00
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/http"
2022-04-26 03:37:19 +00:00
"github.com/google/go-github/v43/github"
2020-07-02 19:36:10 +01:00
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/oauth2"
)
2022-09-09 09:32:08 +00:00
const enterpriseAegisVersionHeaderValue = "GitHub AE"
const enterpriseAPIPath = "/api/v3"
const enterpriseVersionHeaderKey = "X-GitHub-Enterprise-Version"
const xOAuthScopesHeader = "X-OAuth-Scopes"
2020-09-18 16:50:26 -04:00
type PushOnlyFlags struct {
2022-09-09 09:32:08 +00:00
BaseURL , Token , ActionsAdminUser string
DisableGitAuth bool
2020-07-02 19:36:10 +01:00
}
2020-09-18 16:50:26 -04:00
type PushFlags struct {
CommonFlags
PushOnlyFlags
}
2020-07-02 19:36:10 +01:00
func ( f * PushFlags ) Init ( cmd * cobra . Command ) {
2020-09-18 16:50:26 -04:00
f . CommonFlags . Init ( cmd )
f . PushOnlyFlags . Init ( cmd )
}
func ( f * PushOnlyFlags ) Init ( cmd * cobra . Command ) {
2020-07-02 19:36:10 +01:00
cmd . Flags ( ) . StringVar ( & f . BaseURL , "destination-url" , "" , "URL of GHES instance" )
2022-09-20 09:48:29 +00:00
cmd . Flags ( ) . StringVar ( & f . ActionsAdminUser , "actions-admin-user" , "" , "A user to impersonate for the push requests. To use the default name, pass 'actions-admin'." )
2020-07-02 19:36:10 +01:00
cmd . Flags ( ) . StringVar ( & f . Token , "destination-token" , "" , "Token to access API on GHES instance" )
cmd . Flags ( ) . BoolVar ( & f . DisableGitAuth , "disable-push-git-auth" , false , "Disables git authentication whilst pushing" )
}
func ( f * PushFlags ) Validate ( ) Validations {
2020-09-18 16:50:26 -04:00
return f . CommonFlags . Validate ( false ) . Join ( f . PushOnlyFlags . Validate ( ) )
}
func ( f * PushOnlyFlags ) Validate ( ) Validations {
2020-07-02 19:36:10 +01:00
var validations Validations
if f . BaseURL == "" {
2020-08-06 14:41:28 +01:00
validations = append ( validations , "--destination-url must be set" )
2020-07-02 19:36:10 +01:00
}
if f . Token == "" {
2020-08-06 14:41:28 +01:00
validations = append ( validations , "--destination-token must be set" )
2020-07-02 19:36:10 +01:00
}
return validations
}
2022-09-09 09:32:08 +00:00
func GetImpersonationToken ( ctx context . Context , flags * PushFlags ) ( string , error ) {
2022-09-20 09:48:29 +00:00
fmt . Printf ( "getting an impersonation token for `%s` ...\n" , flags . ActionsAdminUser )
2022-09-09 09:32:08 +00:00
ts := oauth2 . StaticTokenSource ( & oauth2 . Token { AccessToken : flags . Token } )
tc := oauth2 . NewClient ( ctx , ts )
ghClient , err := github . NewEnterpriseClient ( flags . BaseURL , flags . BaseURL , tc )
if err != nil {
return "" , errors . Wrap ( err , "error creating enterprise client" )
}
rootRequest , err := ghClient . NewRequest ( "GET" , enterpriseAPIPath , nil )
if err != nil {
2022-09-20 09:48:29 +00:00
return "" , errors . Wrap ( err , "error constructing request for GitHub Enterprise client." )
2022-09-09 09:32:08 +00:00
}
rootResponse , err := ghClient . Do ( ctx , rootRequest , nil )
if err != nil {
2022-09-20 09:48:29 +00:00
return "" , errors . Wrap ( err , "error checking connectivity for GitHub Enterprise client." )
2022-09-09 09:32:08 +00:00
}
scopesHeader := rootResponse . Header . Get ( xOAuthScopesHeader )
2022-09-20 09:48:29 +00:00
fmt . Printf ( "these are the scopes we have for the current token `%s` ...\n" , scopesHeader )
2022-09-09 09:32:08 +00:00
2022-09-21 11:59:16 +00:00
if ! strings . Contains ( scopesHeader , "site_admin" ) {
return "" , errors . Wrap ( err , "the current token doesn't have the `site_admin` scope, the impersonation function requires the `site_admin` permission to be able to impersonate." )
}
2022-09-09 09:32:08 +00:00
isAE := rootResponse . Header . Get ( enterpriseVersionHeaderKey ) == enterpriseAegisVersionHeaderValue
minimumRepositoryScope := "public_repo"
if isAE {
// the default repository scope for non-ae instances is 'public_repo'
// while it is `repo` for ae.
minimumRepositoryScope = "repo"
2022-09-20 09:48:29 +00:00
fmt . Printf ( "running against GitHub AE, changing the repository scope to '%s' ...\n" , minimumRepositoryScope )
2022-09-09 09:32:08 +00:00
}
impersonationToken , _ , err := ghClient . Admin . CreateUserImpersonation ( ctx , flags . ActionsAdminUser , & github . ImpersonateUserOptions { Scopes : [ ] string { minimumRepositoryScope , "workflow" } } )
if err != nil {
2022-09-21 11:59:16 +00:00
return "" , errors . Wrap ( err , "failed to impersonate Actions admin user." )
2022-09-09 09:32:08 +00:00
}
2022-09-20 09:48:29 +00:00
fmt . Printf ( "got the impersonation token for `%s` ...\n" , flags . ActionsAdminUser )
2022-09-09 09:32:08 +00:00
return impersonationToken . GetToken ( ) , nil
}
2020-09-18 16:50:26 -04:00
func Push ( ctx context . Context , flags * PushFlags ) error {
2022-09-20 09:48:29 +00:00
if flags . ActionsAdminUser != "" {
2022-09-09 09:32:08 +00:00
var token , err = GetImpersonationToken ( ctx , flags )
if err != nil {
return errors . Wrap ( err , "error obtaining the impersonation token" )
}
// Override the initial token with the one that we got in the exchange
flags . Token = token
2022-09-20 09:48:29 +00:00
} else {
fmt . Print ( "not using impersonation for the requests \n" )
2022-09-09 09:32:08 +00:00
}
2020-07-02 19:36:10 +01:00
ts := oauth2 . StaticTokenSource ( & oauth2 . Token { AccessToken : flags . Token } )
tc := oauth2 . NewClient ( ctx , ts )
ghClient , err := github . NewEnterpriseClient ( flags . BaseURL , flags . BaseURL , tc )
if err != nil {
return errors . Wrap ( err , "error creating enterprise client" )
}
2020-09-18 16:50:26 -04:00
repoNames , err := getRepoNamesFromRepoFlags ( & flags . CommonFlags )
2020-07-02 19:36:10 +01:00
if err != nil {
2020-09-18 16:50:26 -04:00
return err
2020-07-02 19:36:10 +01:00
}
2020-09-18 16:50:26 -04:00
if repoNames == nil {
repoNames , err = getRepoNamesFromCacheDir ( & flags . CommonFlags )
2020-07-02 19:36:10 +01:00
if err != nil {
2020-09-18 16:50:26 -04:00
return err
2020-07-02 19:36:10 +01:00
}
2020-09-18 16:50:26 -04:00
}
return PushManyWithGitImpl ( ctx , flags , repoNames , ghClient , gitImplementation { } )
}
func PushManyWithGitImpl ( ctx context . Context , flags * PushFlags , repoNames [ ] string , ghClient * github . Client , gitimpl GitImplementation ) error {
for _ , repoName := range repoNames {
if err := PushWithGitImpl ( ctx , flags , repoName , ghClient , gitimpl ) ; err != nil {
return err
2020-07-02 19:36:10 +01:00
}
}
return nil
}
2020-09-18 16:50:26 -04:00
func PushWithGitImpl ( ctx context . Context , flags * PushFlags , repoName string , ghClient * github . Client , gitimpl GitImplementation ) error {
_ , nwo , err := extractSourceDest ( repoName )
if err != nil {
return err
}
ownerName , bareRepoName , err := splitNwo ( nwo )
if err != nil {
return err
}
repoDirPath := path . Join ( flags . CacheDir , nwo )
_ , err = os . Stat ( repoDirPath )
if err != nil {
return err
}
fmt . Printf ( "syncing `%s`\n" , nwo )
2020-09-22 13:52:54 -04:00
ghRepo , err := getOrCreateGitHubRepo ( ctx , ghClient , bareRepoName , ownerName )
2020-09-18 16:50:26 -04:00
if err != nil {
return errors . Wrapf ( err , "error creating github repository `%s`" , nwo )
}
err = syncWithCachedRepository ( ctx , flags , ghRepo , repoDirPath , gitimpl )
if err != nil {
return errors . Wrapf ( err , "error syncing repository `%s`" , nwo )
}
fmt . Printf ( "successfully synced `%s`\n" , nwo )
return nil
}
2020-09-22 13:52:54 -04:00
func getOrCreateGitHubRepo ( ctx context . Context , client * github . Client , repoName , ownerName string ) ( * github . Repository , error ) {
2020-07-02 19:36:10 +01:00
repo := & github . Repository {
Name : github . String ( repoName ) ,
HasIssues : github . Bool ( false ) ,
HasWiki : github . Bool ( false ) ,
HasPages : github . Bool ( false ) ,
HasProjects : github . Bool ( false ) ,
}
2020-09-21 17:40:48 -04:00
2020-09-22 13:52:54 -04:00
currentUser , _ , err := client . Users . Get ( ctx , "" )
if err != nil {
return nil , errors . Wrap ( err , "error retrieving authenticated user" )
}
if currentUser == nil || currentUser . Login == nil {
return nil , errors . New ( "error retrieving authenticated user's login name" )
}
2020-09-21 17:40:48 -04:00
2020-09-22 13:52:54 -04:00
// check if the owner refers to the authenticated user or an organization.
var createRepoOrgName string
if strings . EqualFold ( * currentUser . Login , ownerName ) {
// we'll create the repo under the authenticated user's account.
createRepoOrgName = ""
} else {
// ensure the org exists.
createRepoOrgName = ownerName
_ , err := getOrCreateGitHubOrg ( ctx , client , ownerName , * currentUser . Login )
if err != nil {
return nil , err
2020-09-21 17:40:48 -04:00
}
}
2020-09-22 13:52:54 -04:00
ghRepo , resp , err := client . Repositories . Create ( ctx , createRepoOrgName , repo )
if err == nil {
fmt . Printf ( "Created repo `%s/%s`\n" , ownerName , repoName )
} else if resp != nil && resp . StatusCode == 422 {
2020-09-21 17:40:48 -04:00
ghRepo , _ , err = client . Repositories . Get ( ctx , ownerName , repoName )
2020-07-02 19:36:10 +01:00
}
if err != nil {
2020-09-22 13:52:54 -04:00
return nil , errors . Wrapf ( err , "error creating repository %s/%s" , ownerName , repoName )
2020-07-02 19:36:10 +01:00
}
if ghRepo == nil {
return nil , errors . New ( "error repository is nil" )
}
return ghRepo , nil
}
2020-09-22 13:52:54 -04:00
func getOrCreateGitHubOrg ( ctx context . Context , client * github . Client , orgName , admin string ) ( * github . Organization , error ) {
org := & github . Organization { Login : & orgName }
var getErr error
ghOrg , _ , createErr := client . Admin . CreateOrg ( ctx , org , admin )
if createErr == nil {
fmt . Printf ( "Created organization `%s` (admin: %s)\n" , orgName , admin )
} else {
// Regardless of why create failed, see if we can retrieve the org
ghOrg , _ , getErr = client . Organizations . Get ( ctx , orgName )
}
if createErr != nil && getErr != nil {
return nil , errors . Wrapf ( createErr , "error creating organization %s" , orgName )
}
if ghOrg == nil {
return nil , errors . New ( "error organization is nil" )
}
return ghOrg , nil
}
2020-09-18 16:50:26 -04:00
func syncWithCachedRepository ( ctx context . Context , flags * PushFlags , ghRepo * github . Repository , repoDir string , gitimpl GitImplementation ) error {
2020-07-02 19:36:10 +01:00
gitRepo , err := gitimpl . NewGitRepository ( repoDir )
if err != nil {
2020-09-21 17:40:48 -04:00
return errors . Wrapf ( err , "error opening git repository %s" , repoDir )
2020-07-02 19:36:10 +01:00
}
_ = gitRepo . DeleteRemote ( "ghes" )
remote , err := gitRepo . CreateRemote ( & config . RemoteConfig {
Name : "ghes" ,
URLs : [ ] string { ghRepo . GetCloneURL ( ) } ,
} )
if err != nil {
return errors . Wrap ( err , "error creating remote" )
}
var auth transport . AuthMethod
if ! flags . DisableGitAuth {
auth = & http . BasicAuth {
Username : "username" ,
Password : flags . Token ,
}
}
err = remote . PushContext ( ctx , & git . PushOptions {
RemoteName : remote . Config ( ) . Name ,
RefSpecs : [ ] config . RefSpec {
"+refs/heads/*:refs/heads/*" ,
"+refs/tags/*:refs/tags/*" ,
} ,
Auth : auth ,
} )
if errors . Cause ( err ) == git . NoErrAlreadyUpToDate {
return nil
}
return errors . Wrapf ( err , "failed to push to repo: %s" , ghRepo . GetCloneURL ( ) )
}