├── .dockerignore ├── Dockerfile ├── LICENSE ├── README.md ├── certs.go └── main.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .go 3 | LICENSE 4 | README.md 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | MAINTAINER Kelsey Hightower 3 | ADD contributors contributors 4 | ENV PORT 80 5 | EXPOSE 80 6 | ENTRYPOINT ["/contributors"] 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Kelsey Hightower 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Contributors App 2 | 3 | Display GitHub contributors for a specific repo. 4 | 5 | The Contributors App is designed to run in the scratch Docker image. The total size of the Docker image including the contributors binary is less than 6MB. 6 | 7 | - Avoid "x509: failed to load system roots and no roots provided" by bundling root certificates. 8 | - Avoid dynamic linking by using the pure Go net package (-tags netgo) 9 | - Avoid dynamic linking by disabling cgo (CGO_ENABLED=0) 10 | - Reduce binary size by omitting dwarf information (-ldflags '-w') 11 | 12 | 13 | ## Build 14 | 15 | ### Binary 16 | 17 | The following command will produce a statically linked Go binary without debugging (dwarf) information. 18 | 19 | ``` 20 | CGO_ENABLED=0 GOOS=linux go build -a -tags netgo -ldflags '-w' . 21 | ``` 22 | 23 | ### Docker Image 24 | 25 | ``` 26 | docker build -t kelseyhightower/contributors . 27 | ``` 28 | 29 | ## Run 30 | 31 | ``` 32 | docker run -d -P kelseyhightower/contributors 33 | ``` 34 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "net" 11 | "net/http" 12 | "os" 13 | "text/template" 14 | ) 15 | 16 | var ( 17 | client *http.Client 18 | pool *x509.CertPool 19 | ) 20 | 21 | var html = template.Must(template.New("html").Parse(` 22 | 23 | 24 | 25 | Contributors 26 | 27 | 28 |

Contributors

29 |
30 | 31 | 32 | 33 | 34 | 35 |
36 |

{{.ErrorMessage}}

37 | {{range .Contributors}} 38 | 39 | 40 | {{end}} 42 |
{{.Author.Login}} 41 |
43 | 44 | 45 | `)) 46 | 47 | type Result struct { 48 | Contributors Contributors 49 | ErrorMessage string 50 | } 51 | 52 | type Contributors []Contributor 53 | 54 | type Contributor struct { 55 | Author Author `json:"author"` 56 | } 57 | 58 | type Author struct { 59 | AvatarUrl string `json:"avatar_url"` 60 | Login string `json:"login"` 61 | } 62 | 63 | func init() { 64 | pool = x509.NewCertPool() 65 | pool.AppendCertsFromPEM(pemCerts) 66 | client = &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{RootCAs: pool}}} 67 | } 68 | 69 | func contributors(w http.ResponseWriter, req *http.Request) { 70 | var result Result 71 | owner := req.FormValue("owner") 72 | repo := req.FormValue("repo") 73 | if owner != "" && repo != "" { 74 | url := fmt.Sprintf("https://api.github.com/repos/%s/%s/stats/contributors", owner, repo) 75 | resp, err := client.Get(url) 76 | if err != nil { 77 | result.ErrorMessage = err.Error() 78 | goto L 79 | } 80 | defer resp.Body.Close() 81 | if resp.StatusCode == http.StatusOK { 82 | data, err := ioutil.ReadAll(resp.Body) 83 | if err != nil { 84 | result.ErrorMessage = err.Error() 85 | goto L 86 | } 87 | err = json.Unmarshal(data, &result.Contributors) 88 | if err != nil { 89 | result.ErrorMessage = err.Error() 90 | goto L 91 | } 92 | } 93 | if resp.StatusCode == http.StatusNotFound { 94 | result.ErrorMessage = fmt.Sprintf("%s/%s not found.", owner, repo) 95 | } 96 | } 97 | L: 98 | err := html.Execute(w, result) 99 | if err != nil { 100 | http.Error(w, err.Error(), http.StatusInternalServerError) 101 | return 102 | } 103 | } 104 | 105 | func main() { 106 | hostPort := net.JoinHostPort("0.0.0.0", os.Getenv("PORT")) 107 | http.HandleFunc("/", contributors) 108 | log.Println("Starting contributors app") 109 | log.Printf("Listening on %s\n", hostPort) 110 | if err := http.ListenAndServe(hostPort, nil); err != nil { 111 | log.Fatal("ListenAndServe: ", err) 112 | } 113 | } 114 | --------------------------------------------------------------------------------