github.com/replicatedhq/ship@v0.55.0/pkg/specs/githubclient/client.go (about) 1 package githubclient 2 3 import ( 4 "archive/tar" 5 "compress/gzip" 6 "context" 7 "fmt" 8 "io" 9 "net/http" 10 "net/url" 11 "os" 12 "path" 13 "path/filepath" 14 "strings" 15 16 "github.com/go-kit/kit/log" 17 "github.com/go-kit/kit/log/level" 18 "github.com/google/go-github/v18/github" 19 "github.com/pkg/errors" 20 errors2 "github.com/replicatedhq/ship/pkg/util/errors" 21 22 "github.com/spf13/afero" 23 "golang.org/x/oauth2" 24 ) 25 26 type GitHubFetcher interface { 27 ResolveReleaseNotes(ctx context.Context, upstream string) (string, error) 28 ResolveLatestRelease(ctx context.Context, upstream string) (string, error) 29 } 30 31 var _ GitHubFetcher = &GithubClient{} 32 33 type GithubClient struct { 34 Logger log.Logger 35 Client *github.Client 36 Fs afero.Afero 37 } 38 39 func NewGithubClient(fs afero.Afero, logger log.Logger) *GithubClient { 40 var httpClient *http.Client 41 if accessToken := os.Getenv("GITHUB_TOKEN"); accessToken != "" { 42 level.Debug(logger).Log("msg", "using github access token from environment") 43 ts := oauth2.StaticTokenSource( 44 &oauth2.Token{AccessToken: accessToken}, 45 ) 46 httpClient = oauth2.NewClient(context.Background(), ts) 47 } 48 client := github.NewClient(httpClient) 49 return &GithubClient{ 50 Client: client, 51 Fs: fs, 52 Logger: logger, 53 } 54 } 55 56 func (g *GithubClient) GetFiles( 57 ctx context.Context, 58 upstream string, 59 destinationPath string, 60 ) (string, error) { 61 debug := level.Debug(log.With(g.Logger, "method", "getRepoContents")) 62 63 debug.Log("event", "validateGithubURL") 64 validatedUpstreamURL, err := validateGithubURL(upstream) 65 if err != nil { 66 return "", err 67 } 68 69 debug.Log("event", "decodeGithubURL") 70 owner, repo, branch, repoPath, err := decodeGitHubURL(validatedUpstreamURL.Path) 71 if err != nil { 72 return "", err 73 } 74 75 debug.Log("event", "removeAll", "destinationPath", destinationPath) 76 err = g.Fs.RemoveAll(destinationPath) 77 if err != nil { 78 return "", errors.Wrap(err, "remove chart clone destination") 79 } 80 81 downloadBasePath := "" 82 if filepath.Ext(repoPath) != "" { 83 downloadBasePath = repoPath 84 repoPath = "" 85 } 86 err = g.downloadAndExtractFiles(ctx, owner, repo, branch, downloadBasePath, destinationPath) 87 if err != nil { 88 return "", errors2.FetchFilesError{Message: err.Error()} 89 } 90 91 return filepath.Join(destinationPath, repoPath), nil 92 } 93 94 func (g *GithubClient) downloadAndExtractFiles( 95 ctx context.Context, 96 owner string, 97 repo string, 98 branch string, 99 basePath string, 100 filePath string, 101 ) error { 102 debug := level.Debug(log.With(g.Logger, "method", "downloadAndExtractFiles")) 103 104 debug.Log("event", "getContents", "path", basePath) 105 106 archiveOpts := &github.RepositoryContentGetOptions{ 107 Ref: branch, 108 } 109 archiveLink, _, err := g.Client.Repositories.GetArchiveLink(ctx, owner, repo, github.Tarball, archiveOpts) 110 if err != nil { 111 return errors.Wrapf(err, "get archive link for owner - %s repo - %s", owner, repo) 112 } 113 114 resp, err := http.Get(archiveLink.String()) 115 if err != nil { 116 return errors.Wrapf(err, "downloading archive") 117 } 118 defer resp.Body.Close() 119 120 uncompressedStream, err := gzip.NewReader(resp.Body) 121 if err != nil { 122 return errors.Wrapf(err, "create uncompressed stream") 123 } 124 125 tarReader := tar.NewReader(uncompressedStream) 126 127 basePathFound := false 128 for { 129 header, err := tarReader.Next() 130 if err == io.EOF { 131 if !basePathFound { 132 branchString := branch 133 if branchString == "" { 134 branchString = "master" 135 } 136 return errors.Errorf("Path %s in %s/%s on branch %s not found", basePath, owner, repo, branchString) 137 } 138 break 139 } 140 141 if err != nil { 142 return errors.Wrapf(err, "extract tar gz, next()") 143 } 144 145 switch header.Typeflag { 146 case tar.TypeReg: 147 // need this in a func because defer in a loop was leaking handles 148 err := func() error { 149 fileName := strings.Join(strings.Split(header.Name, "/")[1:], "/") 150 if !strings.HasPrefix(fileName, basePath) { 151 return nil 152 } 153 basePathFound = true 154 155 if fileName != basePath { 156 fileName = strings.TrimPrefix(fileName, basePath) 157 } 158 dirPath, _ := path.Split(fileName) 159 if err := g.Fs.MkdirAll(filepath.Join(filePath, dirPath), 0755); err != nil { 160 return errors.Wrapf(err, "extract tar gz, mkdir") 161 } 162 outFile, err := g.Fs.Create(filepath.Join(filePath, fileName)) 163 if err != nil { 164 return errors.Wrapf(err, "extract tar gz, create") 165 } 166 defer outFile.Close() 167 if _, err := io.Copy(outFile, tarReader); err != nil { 168 return errors.Wrapf(err, "extract tar gz, copy") 169 } 170 return nil 171 }() 172 if err != nil { 173 return err 174 } 175 } 176 } 177 178 return nil 179 } 180 181 func decodeGitHubURL(chartPath string) (owner string, repo string, branch string, path string, err error) { 182 splitPath := strings.Split(chartPath, "/") 183 184 if len(splitPath) < 3 { 185 return owner, repo, path, branch, errors.Wrapf(errors.New("unable to decode github url"), chartPath) 186 } 187 188 owner = splitPath[1] 189 repo = splitPath[2] 190 branch = "" 191 path = "" 192 if len(splitPath) > 3 { 193 if splitPath[3] == "tree" || splitPath[3] == "blob" { 194 branch = splitPath[4] 195 path = strings.Join(splitPath[5:], "/") 196 } else { 197 path = strings.Join(splitPath[3:], "/") 198 } 199 } 200 201 return owner, repo, branch, path, nil 202 } 203 204 func validateGithubURL(upstream string) (*url.URL, error) { 205 if !strings.HasPrefix(upstream, "http") { 206 207 upstream = fmt.Sprintf("http://%s", upstream) 208 } 209 210 upstreamURL, err := url.Parse(upstream) 211 if err != nil { 212 return nil, err 213 } 214 215 if !strings.Contains(upstreamURL.Host, "github.com") { 216 return nil, errors.Errorf("%s is not a Github URL", upstream) 217 } 218 219 return upstreamURL, nil 220 } 221 222 func (g *GithubClient) ResolveReleaseNotes(ctx context.Context, upstream string) (string, error) { 223 debug := level.Debug(log.With(g.Logger, "method", "ResolveReleaseNotes")) 224 225 debug.Log("event", "validateGithubURL") 226 validatedUpstreamURL, err := validateGithubURL(upstream) 227 if err != nil { 228 return "", errors.Wrap(err, "not a valid github url") 229 } 230 231 debug.Log("event", "decodeGithubURL") 232 owner, repo, branch, repoPath, err := decodeGitHubURL(validatedUpstreamURL.Path) 233 if err != nil { 234 return "", err 235 } 236 237 commitList, _, err := g.Client.Repositories.ListCommits(ctx, owner, repo, &github.CommitsListOptions{ 238 SHA: branch, 239 Path: repoPath, 240 }) 241 if err != nil { 242 return "", err 243 } 244 245 if len(commitList) > 0 { 246 latestRepoCommit := commitList[0] 247 if latestRepoCommit != nil { 248 commit := latestRepoCommit.GetCommit() 249 if commit != nil { 250 return commit.GetMessage(), nil 251 } 252 } 253 } 254 255 return "", errors.New("No commit available") 256 } 257 258 func (g *GithubClient) ResolveLatestRelease(ctx context.Context, upstream string) (string, error) { 259 validatedUpstreamURL, err := validateGithubURL(upstream) 260 if err != nil { 261 return "", errors.Wrap(err, "not a valid github url") 262 } 263 264 owner, repo, _, _, err := decodeGitHubURL(validatedUpstreamURL.Path) 265 if err != nil { 266 return "", err 267 } 268 269 latest, _, err := g.Client.Repositories.GetLatestRelease(ctx, owner, repo) 270 if err != nil { 271 return "", errors.Wrap(err, "get latest release") 272 } 273 274 return latest.GetTagName(), nil 275 }