-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.go
More file actions
206 lines (169 loc) · 5.44 KB
/
main.go
File metadata and controls
206 lines (169 loc) · 5.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
package main
import (
"context"
"crypto/rsa"
"fmt"
"log"
"os"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/google/go-github/v33/github"
"github.com/spf13/cobra"
"golang.org/x/oauth2"
)
var (
appID string
privateKeyPath string
repoURL string
cloneDir string
)
func main() {
rootCmd := &cobra.Command{
Use: "tokenclone",
Short: "A small Golang utility to clone a GitHub repository using Github app credentials.",
Run: func(cmd *cobra.Command, args []string) {
if appID == "" || privateKeyPath == "" || repoURL == "" || cloneDir == "" {
log.Fatalf("All flags --app_id, --pem_path, --repo_url, and --clone_dir are required")
}
privateKey, err := os.ReadFile(privateKeyPath)
if err != nil {
log.Fatalf("Error reading private key: %v", err)
}
signKey, err := jwt.ParseRSAPrivateKeyFromPEM(privateKey)
if err != nil {
log.Fatalf("Error parsing private key: %v", err)
}
// Generate JWT
jwtToken, err := generateJWT(appID, signKey)
if err != nil {
log.Fatalf("Error generating JWT: %v", err)
}
// Get GitHub client using JWT
client := getGitHubClient(jwtToken)
// Get installation ID
installationID, err := getInstallationID(client)
if err != nil {
log.Fatalf("Error fetching installation ID: %v", err)
}
// Generate installation token
installationToken, err := getInstallationToken(client, installationID)
if err != nil {
log.Fatalf("Error generating installation token: %v", err)
}
// Check repository access
repo, err := checkRepoAccess(client, installationToken, repoURL)
if err != nil {
log.Fatalf("Error accessing repository: %v", err)
}
printRepoDetails(repo)
// Clone the repository
err = cloneRepo(repoURL, cloneDir, installationToken)
if err != nil {
log.Fatalf("Error cloning repository: %v", err)
}
fmt.Println("Repository cloned successfully")
},
}
rootCmd.Flags().StringVar(&appID, "app_id", "", "GitHub App ID")
rootCmd.Flags().StringVar(&privateKeyPath, "pem_path", "", "Path to the GitHub App private key PEM file")
rootCmd.Flags().StringVar(&repoURL, "repo_url", "", "URL of the repository to clone")
rootCmd.Flags().StringVar(&cloneDir, "clone_dir", "", "Directory to clone the repository into")
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func generateJWT(appID string, key *rsa.PrivateKey) (string, error) {
now := time.Now()
// Create the JWT claims, which includes the registered claims
claims := jwt.StandardClaims{
Issuer: appID,
IssuedAt: now.Unix(),
ExpiresAt: now.Add(time.Minute * 10).Unix(),
}
// Create the token
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
// Sign the token with our private key
jwtToken, err := token.SignedString(key)
if err != nil {
return "", err
}
return jwtToken, nil
}
func getGitHubClient(jwtToken string) *github.Client {
// Create a new OAuth2 token source using the JWT token
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: jwtToken},
)
// Create a new HTTP client using the OAuth2 token source
tc := oauth2.NewClient(context.Background(), ts)
// Return a new GitHub client using the HTTP client
return github.NewClient(tc)
}
func getInstallationID(client *github.Client) (int64, error) {
ctx := context.Background()
installations, _, err := client.Apps.ListInstallations(ctx, nil)
if err != nil {
return 0, err
}
if len(installations) == 0 {
return 0, fmt.Errorf("no installations found")
}
return installations[0].GetID(), nil
}
func getInstallationToken(client *github.Client, installationID int64) (string, error) {
// Create a new context
ctx := context.Background()
// Generate a new installation token
token, _, err := client.Apps.CreateInstallationToken(ctx, installationID, nil)
if err != nil {
return "", err
}
return token.GetToken(), nil
}
func checkRepoAccess(client *github.Client, token, repoURL string) (*github.Repository, error) {
ctx := context.Background()
// Extract owner and repo name from URL
repoParts := strings.Split(strings.TrimPrefix(repoURL, "https://github.com/"), "/")
if len(repoParts) != 2 {
return nil, fmt.Errorf("invalid repository URL format: %s", repoURL)
}
owner, repo := repoParts[0], strings.TrimSuffix(repoParts[1], ".git")
// Create a new OAuth2 token source using the installation token
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
)
// Create a new HTTP client using the OAuth2 token source
tc := oauth2.NewClient(ctx, ts)
client = github.NewClient(tc)
// Check if the repository is accessible
repository, resp, err := client.Repositories.Get(ctx, owner, repo)
if err != nil {
if resp != nil && resp.StatusCode == 404 {
log.Printf("Repository %s/%s not found", owner, repo)
} else {
log.Printf("Error accessing repository: %v", err)
}
return nil, err
}
return repository, nil
}
func printRepoDetails(repo *github.Repository) {
fmt.Printf("Repository Details:\n")
fmt.Printf("Name: %s\n", repo.GetName())
fmt.Printf("Full Name: %s\n", repo.GetFullName())
fmt.Printf("Clone URL: %s\n", repo.GetCloneURL())
}
func cloneRepo(repoURL, cloneDir, token string) error {
_, err := git.PlainClone(cloneDir, false, &git.CloneOptions{
URL: repoURL,
Auth: &http.BasicAuth{
Username: "x-access-token", // Username is ignored, but must be non-empty
Password: token,
},
})
return err
}