github.com/kubecost/golang-migrate-duckdb/v4@v4.17.0-duckdb.1/source/gitlab/gitlab.go (about) 1 package gitlab 2 3 import ( 4 "encoding/base64" 5 "fmt" 6 "io" 7 "net/http" 8 nurl "net/url" 9 "os" 10 "strconv" 11 "strings" 12 13 "github.com/golang-migrate/migrate/v4/source" 14 "github.com/xanzy/go-gitlab" 15 ) 16 17 func init() { 18 source.Register("gitlab", &Gitlab{}) 19 } 20 21 const DefaultMaxItemsPerPage = 100 22 23 var ( 24 ErrNoUserInfo = fmt.Errorf("no username:token provided") 25 ErrNoAccessToken = fmt.Errorf("no access token") 26 ErrInvalidHost = fmt.Errorf("invalid host") 27 ErrInvalidProjectID = fmt.Errorf("invalid project id") 28 ErrInvalidResponse = fmt.Errorf("invalid response") 29 ) 30 31 type Gitlab struct { 32 client *gitlab.Client 33 url string 34 35 projectID string 36 path string 37 listOptions *gitlab.ListTreeOptions 38 getOptions *gitlab.GetFileOptions 39 migrations *source.Migrations 40 } 41 42 type Config struct { 43 } 44 45 func (g *Gitlab) Open(url string) (source.Driver, error) { 46 u, err := nurl.Parse(url) 47 if err != nil { 48 return nil, err 49 } 50 51 if u.User == nil { 52 return nil, ErrNoUserInfo 53 } 54 55 password, ok := u.User.Password() 56 if !ok { 57 return nil, ErrNoAccessToken 58 } 59 60 gn := &Gitlab{ 61 client: gitlab.NewClient(nil, password), 62 url: url, 63 migrations: source.NewMigrations(), 64 } 65 66 if u.Host != "" { 67 uri := nurl.URL{ 68 Scheme: "https", 69 Host: u.Host, 70 } 71 72 err = gn.client.SetBaseURL(uri.String()) 73 if err != nil { 74 return nil, ErrInvalidHost 75 } 76 } 77 78 pe := strings.Split(strings.Trim(u.Path, "/"), "/") 79 if len(pe) < 1 { 80 return nil, ErrInvalidProjectID 81 } 82 gn.projectID = pe[0] 83 if len(pe) > 1 { 84 gn.path = strings.Join(pe[1:], "/") 85 } 86 87 gn.listOptions = &gitlab.ListTreeOptions{ 88 Path: &gn.path, 89 Ref: &u.Fragment, 90 ListOptions: gitlab.ListOptions{ 91 PerPage: DefaultMaxItemsPerPage, 92 }, 93 } 94 95 gn.getOptions = &gitlab.GetFileOptions{ 96 Ref: &u.Fragment, 97 } 98 99 if err := gn.readDirectory(); err != nil { 100 return nil, err 101 } 102 103 return gn, nil 104 } 105 106 func WithInstance(client *gitlab.Client, config *Config) (source.Driver, error) { 107 gn := &Gitlab{ 108 client: client, 109 migrations: source.NewMigrations(), 110 } 111 if err := gn.readDirectory(); err != nil { 112 return nil, err 113 } 114 return gn, nil 115 } 116 117 func (g *Gitlab) readDirectory() error { 118 var nodes []*gitlab.TreeNode 119 for { 120 n, response, err := g.client.Repositories.ListTree(g.projectID, g.listOptions) 121 if err != nil { 122 return err 123 } 124 125 if response.StatusCode != http.StatusOK { 126 return ErrInvalidResponse 127 } 128 129 nodes = append(nodes, n...) 130 if response.CurrentPage >= response.TotalPages { 131 break 132 } 133 g.listOptions.ListOptions.Page = response.NextPage 134 } 135 136 for i := range nodes { 137 m, err := g.nodeToMigration(nodes[i]) 138 if err != nil { 139 continue 140 } 141 142 if !g.migrations.Append(m) { 143 return fmt.Errorf("unable to parse file %v", nodes[i].Name) 144 } 145 } 146 147 return nil 148 } 149 150 func (g *Gitlab) nodeToMigration(node *gitlab.TreeNode) (*source.Migration, error) { 151 m := source.Regex.FindStringSubmatch(node.Name) 152 if len(m) == 5 { 153 versionUint64, err := strconv.ParseUint(m[1], 10, 64) 154 if err != nil { 155 return nil, err 156 } 157 return &source.Migration{ 158 Version: uint(versionUint64), 159 Identifier: m[2], 160 Direction: source.Direction(m[3]), 161 Raw: g.path + "/" + node.Name, 162 }, nil 163 } 164 return nil, source.ErrParse 165 } 166 167 func (g *Gitlab) Close() error { 168 return nil 169 } 170 171 func (g *Gitlab) First() (version uint, er error) { 172 if v, ok := g.migrations.First(); !ok { 173 return 0, &os.PathError{Op: "first", Path: g.path, Err: os.ErrNotExist} 174 } else { 175 return v, nil 176 } 177 } 178 179 func (g *Gitlab) Prev(version uint) (prevVersion uint, err error) { 180 if v, ok := g.migrations.Prev(version); !ok { 181 return 0, &os.PathError{Op: fmt.Sprintf("prev for version %v", version), Path: g.path, Err: os.ErrNotExist} 182 } else { 183 return v, nil 184 } 185 } 186 187 func (g *Gitlab) Next(version uint) (nextVersion uint, err error) { 188 if v, ok := g.migrations.Next(version); !ok { 189 return 0, &os.PathError{Op: fmt.Sprintf("next for version %v", version), Path: g.path, Err: os.ErrNotExist} 190 } else { 191 return v, nil 192 } 193 } 194 195 func (g *Gitlab) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) { 196 if m, ok := g.migrations.Up(version); ok { 197 f, response, err := g.client.RepositoryFiles.GetFile(g.projectID, m.Raw, g.getOptions) 198 if err != nil { 199 return nil, "", err 200 } 201 202 if response.StatusCode != http.StatusOK { 203 return nil, "", ErrInvalidResponse 204 } 205 206 content, err := base64.StdEncoding.DecodeString(f.Content) 207 if err != nil { 208 return nil, "", err 209 } 210 211 return io.NopCloser(strings.NewReader(string(content))), m.Identifier, nil 212 } 213 214 return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.path, Err: os.ErrNotExist} 215 } 216 217 func (g *Gitlab) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) { 218 if m, ok := g.migrations.Down(version); ok { 219 f, response, err := g.client.RepositoryFiles.GetFile(g.projectID, m.Raw, g.getOptions) 220 if err != nil { 221 return nil, "", err 222 } 223 224 if response.StatusCode != http.StatusOK { 225 return nil, "", ErrInvalidResponse 226 } 227 228 content, err := base64.StdEncoding.DecodeString(f.Content) 229 if err != nil { 230 return nil, "", err 231 } 232 233 return io.NopCloser(strings.NewReader(string(content))), m.Identifier, nil 234 } 235 236 return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.path, Err: os.ErrNotExist} 237 }