-
Notifications
You must be signed in to change notification settings - Fork 210
Expand file tree
/
Copy pathfetcher_github.go
More file actions
145 lines (140 loc) · 4.02 KB
/
Copy pathfetcher_github.go
File metadata and controls
145 lines (140 loc) · 4.02 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
package fetcher
import (
"compress/gzip"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"runtime"
"strings"
"time"
)
//Github uses the Github V3 API to retrieve the latest release
//of a given repository and enumerate its assets. If a release
//contains a matching asset, it will fetch
//and return its io.Reader stream.
type Github struct {
//Github username and repository name
User, Repo string
//Interval between fetches
Interval time.Duration
//Asset is used to find matching release asset.
//By default a file will match if it contains
//both GOOS and GOARCH.
Asset func(filename string) bool
//internal state
releaseURL string
delay bool
lastETag string
latestRelease struct {
TagName string `json:"tag_name"`
Assets []struct {
Name string `json:"name"`
URL string `json:"browser_download_url"`
} `json:"assets"`
}
}
func (h *Github) defaultAsset(filename string) bool {
return strings.Contains(filename, runtime.GOOS) && strings.Contains(filename, runtime.GOARCH)
}
// Init validates the provided config
func (h *Github) Init() error {
//apply defaults
if h.User == "" {
return errors.New("user required")
}
if h.Repo == "" {
return errors.New("repo required")
}
if h.Asset == nil {
h.Asset = h.defaultAsset
}
h.releaseURL = "https://api.github.com/repos/" + h.User + "/" + h.Repo + "/releases/latest"
if h.Interval == 0 {
h.Interval = 5 * time.Minute
} else if h.Interval < 1*time.Minute {
log.Printf("[overseer.github] warning: intervals less than 1 minute will surpass the public rate limit")
}
return nil
}
// Fetch the binary from the provided Repository
func (h *Github) Fetch() (io.Reader, error) {
//delay fetches after first
if h.delay {
time.Sleep(h.Interval)
}
h.delay = true
//check release status
resp, err := http.Get(h.releaseURL)
if err != nil {
return nil, fmt.Errorf("release info request failed (%w)", err)
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("release info request failed (status code %d)", resp.StatusCode)
}
//clear assets
h.latestRelease.Assets = nil
if err := json.NewDecoder(resp.Body).Decode(&h.latestRelease); err != nil {
return nil, fmt.Errorf("invalid request info (%w)", err)
}
resp.Body.Close()
//find appropriate asset
assetURL := ""
for _, a := range h.latestRelease.Assets {
if h.Asset(a.Name) {
assetURL = a.URL
break
}
}
if assetURL == "" {
return nil, fmt.Errorf("no matching assets in this release (%s)", h.latestRelease.TagName)
}
//fetch location
req, _ := http.NewRequest("HEAD", assetURL, nil)
resp, err = http.DefaultTransport.RoundTrip(req)
if err != nil {
return nil, fmt.Errorf("release location request failed (%w)", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusFound {
return nil, fmt.Errorf("release location request failed (status code %d)", resp.StatusCode)
}
s3URL := resp.Header.Get("Location")
//pseudo-HEAD request
req, err = http.NewRequest("GET", s3URL, nil)
if err != nil {
return nil, fmt.Errorf("release location url error (%w)", err)
}
req.Header.Set("Range", "bytes=0-0") // HEAD not allowed so we request for 1 byte
resp, err = http.DefaultTransport.RoundTrip(req)
if err != nil {
return nil, fmt.Errorf("release location request failed (%w)", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusPartialContent {
return nil, fmt.Errorf("release location request failed (status code %d)", resp.StatusCode)
}
etag := resp.Header.Get("ETag")
if etag != "" && h.lastETag == etag {
return nil, nil //skip, hash match
}
//get binary request
resp, err = http.Get(s3URL)
if err != nil {
return nil, fmt.Errorf("release binary request failed (%w)", err)
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("release binary request failed (status code %d)", resp.StatusCode)
}
h.lastETag = etag
//success!
//extract gz files
if strings.HasSuffix(assetURL, ".gz") && resp.Header.Get("Content-Encoding") != "gzip" {
return gzip.NewReader(resp.Body)
}
return resp.Body, nil
}