Skip to content

Commit 7c76166

Browse files
kasparasgdhui
authored andcommitted
Github Enterprise support (denisenkom#234)
* exported Github struct fields and ReadDirectory method * github ee implementation, tests and docs * build fixes * Github Enterprise API endpoint based on docs * addressing PR comments * code review * make linter happy * parseBool() takes fallback * pr comments * tweaks to Config{}
1 parent 0d13e79 commit 7c76166

File tree

10 files changed

+238
-22
lines changed

10 files changed

+238
-22
lines changed

Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ COPY . ./
99

1010
ENV GO111MODULE=on
1111
ENV DATABASES="postgres mysql redshift cassandra spanner cockroachdb clickhouse mongodb sqlserver"
12-
ENV SOURCES="file go_bindata github aws_s3 google_cloud_storage godoc_vfs gitlab"
12+
ENV SOURCES="file go_bindata github github_ee aws_s3 google_cloud_storage godoc_vfs gitlab"
1313

1414
RUN go build -a -o build/migrate.linux-386 -ldflags="-X main.Version=${VERSION}" -tags "$DATABASES $SOURCES" ./cmd/migrate
1515

Makefile

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
SOURCE ?= file go_bindata github aws_s3 google_cloud_storage godoc_vfs gitlab
1+
SOURCE ?= file go_bindata github github_ee aws_s3 google_cloud_storage godoc_vfs gitlab
22
DATABASE ?= postgres mysql redshift cassandra spanner cockroachdb clickhouse mongodb sqlserver
33
VERSION ?= $(shell git describe --tags 2>/dev/null | cut -c 2-)
44
TEST_FLAGS ?=
@@ -33,7 +33,7 @@ test:
3333

3434

3535
test-with-flags:
36-
@echo SOURCE: $(SOURCE)
36+
@echo SOURCE: $(SOURCE)
3737
@echo DATABASE: $(DATABASE)
3838

3939
@go test $(TEST_FLAGS) .
@@ -84,7 +84,7 @@ rewrite-import-paths:
8484
docs:
8585
-make kill-docs
8686
nohup godoc -play -http=127.0.0.1:6064 </dev/null >/dev/null 2>&1 & echo $$! > .godoc.pid
87-
cat .godoc.pid
87+
cat .godoc.pid
8888

8989

9090
kill-docs:

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Source drivers read migrations from local or remote sources. [Add a new source?]
6666
* [Filesystem](source/file) - read from filesystem
6767
* [Go-Bindata](source/go_bindata) - read from embedded binary data ([jteeuwen/go-bindata](https://github.com/jteeuwen/go-bindata))
6868
* [Github](source/github) - read from remote Github repositories
69+
* [Github Enterprise](source/github_ee) - read from remote Github Enterprise repositories
6970
* [Gitlab](source/gitlab) - read from remote Gitlab repositories
7071
* [AWS S3](source/aws_s3) - read from Amazon Web Services S3
7172
* [Google Cloud Storage](source/google_cloud_storage) - read from Google Cloud Platform Storage

internal/cli/build_github_ee.go

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// +build github
2+
3+
package cli
4+
5+
import (
6+
_ "github.com/golang-migrate/migrate/v4/source/github_ee"
7+
)

source/github/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# github
22

3+
This driver is catered for those that want to source migrations from [github.com](https://github.com). The URL scheme doesn't require a hostname, as it just simply defaults to `github.com`.
4+
35
`github://user:personal-access-token@owner/repo/path#ref`
46

57
| URL Query | WithInstance Config | Description |

source/github/github.go

+62-18
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,17 @@ var (
2929
)
3030

3131
type Github struct {
32-
client *github.Client
33-
url string
34-
35-
pathOwner string
36-
pathRepo string
37-
path string
32+
config *Config
33+
client *github.Client
3834
options *github.RepositoryContentGetOptions
3935
migrations *source.Migrations
4036
}
4137

4238
type Config struct {
39+
Owner string
40+
Repo string
41+
Path string
42+
Ref string
4343
}
4444

4545
func (g *Github) Open(url string) (source.Driver, error) {
@@ -64,20 +64,21 @@ func (g *Github) Open(url string) (source.Driver, error) {
6464

6565
gn := &Github{
6666
client: github.NewClient(tr.Client()),
67-
url: url,
6867
migrations: source.NewMigrations(),
6968
options: &github.RepositoryContentGetOptions{Ref: u.Fragment},
7069
}
7170

71+
gn.ensureFields()
72+
7273
// set owner, repo and path in repo
73-
gn.pathOwner = u.Host
74+
gn.config.Owner = u.Host
7475
pe := strings.Split(strings.Trim(u.Path, "/"), "/")
7576
if len(pe) < 1 {
7677
return nil, ErrInvalidRepo
7778
}
78-
gn.pathRepo = pe[0]
79+
gn.config.Repo = pe[0]
7980
if len(pe) > 1 {
80-
gn.path = strings.Join(pe[1:], "/")
81+
gn.config.Path = strings.Join(pe[1:], "/")
8182
}
8283

8384
if err := gn.readDirectory(); err != nil {
@@ -90,16 +91,29 @@ func (g *Github) Open(url string) (source.Driver, error) {
9091
func WithInstance(client *github.Client, config *Config) (source.Driver, error) {
9192
gn := &Github{
9293
client: client,
94+
config: config,
9395
migrations: source.NewMigrations(),
96+
options: &github.RepositoryContentGetOptions{Ref: config.Ref},
9497
}
98+
9599
if err := gn.readDirectory(); err != nil {
96100
return nil, err
97101
}
102+
98103
return gn, nil
99104
}
100105

101106
func (g *Github) readDirectory() error {
102-
fileContent, dirContents, _, err := g.client.Repositories.GetContents(context.Background(), g.pathOwner, g.pathRepo, g.path, g.options)
107+
g.ensureFields()
108+
109+
fileContent, dirContents, _, err := g.client.Repositories.GetContents(
110+
context.Background(),
111+
g.config.Owner,
112+
g.config.Repo,
113+
g.config.Path,
114+
g.options,
115+
)
116+
103117
if err != nil {
104118
return err
105119
}
@@ -120,37 +134,58 @@ func (g *Github) readDirectory() error {
120134
return nil
121135
}
122136

137+
func (g *Github) ensureFields() {
138+
if g.config == nil {
139+
g.config = &Config{}
140+
}
141+
}
142+
123143
func (g *Github) Close() error {
124144
return nil
125145
}
126146

127147
func (g *Github) First() (version uint, er error) {
148+
g.ensureFields()
149+
128150
if v, ok := g.migrations.First(); !ok {
129-
return 0, &os.PathError{Op: "first", Path: g.path, Err: os.ErrNotExist}
151+
return 0, &os.PathError{Op: "first", Path: g.config.Path, Err: os.ErrNotExist}
130152
} else {
131153
return v, nil
132154
}
133155
}
134156

135157
func (g *Github) Prev(version uint) (prevVersion uint, err error) {
158+
g.ensureFields()
159+
136160
if v, ok := g.migrations.Prev(version); !ok {
137-
return 0, &os.PathError{Op: fmt.Sprintf("prev for version %v", version), Path: g.path, Err: os.ErrNotExist}
161+
return 0, &os.PathError{Op: fmt.Sprintf("prev for version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
138162
} else {
139163
return v, nil
140164
}
141165
}
142166

143167
func (g *Github) Next(version uint) (nextVersion uint, err error) {
168+
g.ensureFields()
169+
144170
if v, ok := g.migrations.Next(version); !ok {
145-
return 0, &os.PathError{Op: fmt.Sprintf("next for version %v", version), Path: g.path, Err: os.ErrNotExist}
171+
return 0, &os.PathError{Op: fmt.Sprintf("next for version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
146172
} else {
147173
return v, nil
148174
}
149175
}
150176

151177
func (g *Github) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
178+
g.ensureFields()
179+
152180
if m, ok := g.migrations.Up(version); ok {
153-
file, _, _, err := g.client.Repositories.GetContents(context.Background(), g.pathOwner, g.pathRepo, path.Join(g.path, m.Raw), g.options)
181+
file, _, _, err := g.client.Repositories.GetContents(
182+
context.Background(),
183+
g.config.Owner,
184+
g.config.Repo,
185+
path.Join(g.config.Path, m.Raw),
186+
g.options,
187+
)
188+
154189
if err != nil {
155190
return nil, "", err
156191
}
@@ -162,12 +197,21 @@ func (g *Github) ReadUp(version uint) (r io.ReadCloser, identifier string, err e
162197
return ioutil.NopCloser(strings.NewReader(r)), m.Identifier, nil
163198
}
164199
}
165-
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.path, Err: os.ErrNotExist}
200+
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
166201
}
167202

168203
func (g *Github) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
204+
g.ensureFields()
205+
169206
if m, ok := g.migrations.Down(version); ok {
170-
file, _, _, err := g.client.Repositories.GetContents(context.Background(), g.pathOwner, g.pathRepo, path.Join(g.path, m.Raw), g.options)
207+
file, _, _, err := g.client.Repositories.GetContents(
208+
context.Background(),
209+
g.config.Owner,
210+
g.config.Repo,
211+
path.Join(g.config.Path, m.Raw),
212+
g.options,
213+
)
214+
171215
if err != nil {
172216
return nil, "", err
173217
}
@@ -179,5 +223,5 @@ func (g *Github) ReadDown(version uint) (r io.ReadCloser, identifier string, err
179223
return ioutil.NopCloser(strings.NewReader(r)), m.Identifier, nil
180224
}
181225
}
182-
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.path, Err: os.ErrNotExist}
226+
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
183227
}

source/github_ee/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.github_test_secrets

source/github_ee/README.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# github ee
2+
3+
## Github Enterprise Edition
4+
5+
This driver is catered for those who run Github Enterprise under private infrastructure.
6+
7+
The below URL scheme illustrates how to source migration files from Github Enterprise.
8+
9+
Github client for Go requires API and Uploads endpoint hosts in order to create an instance of Github Enterprise Client. We're making an assumption that the API and Uploads are available under `https://api.*` and `https://uploads.*` respectively. [Github Enterprise Installation Guide](https://help.github.com/en/enterprise/2.15/admin/installation/enabling-subdomain-isolation) recommends that you enable Subdomain isolation feature.
10+
11+
`github-ee://user:personal-access-token@host/owner/repo/path?verify-tls=true#ref`
12+
13+
| URL Query | WithInstance Config | Description |
14+
|------------|---------------------|-------------|
15+
| user | | The username of the user connecting |
16+
| personal-access-token | | Personal access token from your Github Enterprise instance |
17+
| owner | | the repo owner |
18+
| repo | | the name of the repository |
19+
| path | | path in repo to migrations |
20+
| ref | | (optional) can be a SHA, branch, or tag |
21+
| verify-tls | | (optional) defaults to `true`. This option sets `tls.Config.InsecureSkipVerify` accordingly |

source/github_ee/github_ee.go

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package github_ee
2+
3+
import (
4+
"crypto/tls"
5+
"fmt"
6+
"net/http"
7+
nurl "net/url"
8+
"strconv"
9+
"strings"
10+
11+
"github.com/golang-migrate/migrate/v4/source"
12+
gh "github.com/golang-migrate/migrate/v4/source/github"
13+
"github.com/google/go-github/github"
14+
)
15+
16+
func init() {
17+
source.Register("github-ee", &GithubEE{})
18+
}
19+
20+
type GithubEE struct {
21+
source.Driver
22+
}
23+
24+
func (g *GithubEE) Open(url string) (source.Driver, error) {
25+
verifyTLS := true
26+
27+
u, err := nurl.Parse(url)
28+
if err != nil {
29+
return nil, err
30+
}
31+
32+
if o := u.Query().Get("verify-tls"); o != "" {
33+
verifyTLS = parseBool(o, verifyTLS)
34+
}
35+
36+
if u.User == nil {
37+
return nil, gh.ErrNoUserInfo
38+
}
39+
40+
password, ok := u.User.Password()
41+
if !ok {
42+
return nil, gh.ErrNoUserInfo
43+
}
44+
45+
ghc, err := g.createGithubClient(u.Host, u.User.Username(), password, verifyTLS)
46+
if err != nil {
47+
return nil, err
48+
}
49+
50+
pe := strings.Split(strings.Trim(u.Path, "/"), "/")
51+
52+
if len(pe) < 1 {
53+
return nil, gh.ErrInvalidRepo
54+
}
55+
56+
cfg := &gh.Config{
57+
Owner: pe[0],
58+
Repo: pe[1],
59+
Ref: u.Fragment,
60+
}
61+
62+
if len(pe) > 2 {
63+
cfg.Path = strings.Join(pe[2:], "/")
64+
}
65+
66+
i, err := gh.WithInstance(ghc, cfg)
67+
if err != nil {
68+
return nil, err
69+
}
70+
71+
return &GithubEE{Driver: i}, nil
72+
}
73+
74+
func (g *GithubEE) createGithubClient(host, username, password string, verifyTLS bool) (*github.Client, error) {
75+
tr := &github.BasicAuthTransport{
76+
Username: username,
77+
Password: password,
78+
Transport: &http.Transport{
79+
TLSClientConfig: &tls.Config{InsecureSkipVerify: !verifyTLS},
80+
},
81+
}
82+
83+
apiHost := fmt.Sprintf("https://%s/api/v3", host)
84+
uploadHost := fmt.Sprintf("https://uploads.%s", host)
85+
86+
return github.NewEnterpriseClient(apiHost, uploadHost, tr.Client())
87+
}
88+
89+
func parseBool(val string, fallback bool) bool {
90+
b, err := strconv.ParseBool(val)
91+
if err != nil {
92+
return fallback
93+
}
94+
95+
return b
96+
}

source/github_ee/github_ee_test.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package github_ee
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
nurl "net/url"
7+
"testing"
8+
)
9+
10+
func Test(t *testing.T) {
11+
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
12+
if r.URL.Path != "/api/v3/repos/mattes/migrate_test_tmp/contents/test" {
13+
w.WriteHeader(http.StatusNotFound)
14+
return
15+
}
16+
17+
if ref := r.URL.Query().Get("ref"); ref != "452b8003e7" {
18+
w.WriteHeader(http.StatusNotFound)
19+
return
20+
}
21+
22+
w.Header().Set("Content-Type", "application/json")
23+
w.WriteHeader(http.StatusOK)
24+
25+
_, err := w.Write([]byte("[]"))
26+
if err != nil {
27+
w.WriteHeader(http.StatusInternalServerError)
28+
return
29+
}
30+
}))
31+
defer ts.Close()
32+
33+
u, err := nurl.Parse(ts.URL)
34+
if err != nil {
35+
t.Fatal(err)
36+
}
37+
38+
g := &GithubEE{}
39+
_, err = g.Open("github-ee://foo:bar@" + u.Host + "/mattes/migrate_test_tmp/test?verify-tls=false#452b8003e7")
40+
41+
if err != nil {
42+
t.Fatal(err)
43+
}
44+
}

0 commit comments

Comments
 (0)