blob: 3e5827994743f65a15d9609a018f419a05dc9d27 [file] [log] [blame]
giolekva8aa73e82022-07-09 11:34:39 +04001package soft
2
3import (
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +04004 "errors"
giolekva8aa73e82022-07-09 11:34:39 +04005 "fmt"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +04006 "golang.org/x/crypto/ssh"
giolekva8aa73e82022-07-09 11:34:39 +04007 "log"
8 "net"
9 "os"
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040010 "regexp"
gio266c04f2024-07-03 14:18:45 +040011 "slices"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040012 "strings"
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040013 "time"
giolekva8aa73e82022-07-09 11:34:39 +040014
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040015 "github.com/cenkalti/backoff/v4"
giolekva8aa73e82022-07-09 11:34:39 +040016 "github.com/go-git/go-billy/v5/memfs"
17 "github.com/go-git/go-git/v5"
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040018 "github.com/go-git/go-git/v5/plumbing/transport"
giolekva8aa73e82022-07-09 11:34:39 +040019 gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
20 "github.com/go-git/go-git/v5/storage/memory"
giolekva8aa73e82022-07-09 11:34:39 +040021)
22
gio0eaf2712024-04-14 13:08:46 +040023var ErrorAlreadyExists = errors.New("already exists")
24
gioe72b54f2024-04-22 10:44:41 +040025type Client interface {
26 Address() string
27 Signer() ssh.Signer
28 GetPublicKeys() ([]string, error)
gio33059762024-07-05 13:19:07 +040029 RepoExists(name string) (bool, error)
gioe72b54f2024-04-22 10:44:41 +040030 GetRepo(name string) (RepoIO, error)
giocafd4e62024-07-31 10:53:40 +040031 GetAllRepos() ([]string, error)
gioe72b54f2024-04-22 10:44:41 +040032 GetRepoAddress(name string) string
33 AddRepository(name string) error
gio33059762024-07-05 13:19:07 +040034 UserExists(name string) (bool, error)
35 FindUser(pubKey string) (string, error)
gioe72b54f2024-04-22 10:44:41 +040036 AddUser(name, pubKey string) error
37 AddPublicKey(user string, pubKey string) error
38 RemovePublicKey(user string, pubKey string) error
39 MakeUserAdmin(name string) error
40 AddReadWriteCollaborator(repo, user string) error
41 AddReadOnlyCollaborator(repo, user string) error
gio0eaf2712024-04-14 13:08:46 +040042 AddWebhook(repo, url string, opts ...string) error
gioe72b54f2024-04-22 10:44:41 +040043}
44
45type realClient struct {
46 addr string
47 signer ssh.Signer
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040048 log *log.Logger
49 pemBytes []byte
giolekva8aa73e82022-07-09 11:34:39 +040050}
51
gioe72b54f2024-04-22 10:44:41 +040052func NewClient(addr string, clientPrivateKey []byte, log *log.Logger) (Client, error) {
giolekva8aa73e82022-07-09 11:34:39 +040053 signer, err := ssh.ParsePrivateKey(clientPrivateKey)
54 if err != nil {
55 return nil, err
56 }
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040057 log.SetPrefix("SOFT-SERVE: ")
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040058 log.Printf("Created signer")
gioe72b54f2024-04-22 10:44:41 +040059 return &realClient{
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040060 addr,
giolekva8aa73e82022-07-09 11:34:39 +040061 signer,
62 log,
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040063 clientPrivateKey,
giolekva8aa73e82022-07-09 11:34:39 +040064 }, nil
65}
66
gioe72b54f2024-04-22 10:44:41 +040067type ClientGetter interface {
68 Get(addr string, clientPrivateKey []byte, log *log.Logger) (Client, error)
69}
70
71type RealClientGetter struct{}
72
73func (c RealClientGetter) Get(addr string, clientPrivateKey []byte, log *log.Logger) (Client, error) {
74 var client Client
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040075 err := backoff.RetryNotify(func() error {
76 var err error
77 client, err = NewClient(addr, clientPrivateKey, log)
78 if err != nil {
79 return err
80 }
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +040081 if _, err := client.GetPublicKeys(); err != nil {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040082 return err
83 }
84 return nil
85 }, backoff.NewConstantBackOff(5*time.Second), func(err error, _ time.Duration) {
86 log.Printf("Failed to create client: %s\n", err.Error())
87 })
88 return client, err
89}
90
gioe72b54f2024-04-22 10:44:41 +040091func (ss *realClient) Address() string {
92 return ss.addr
93}
94
95func (ss *realClient) Signer() ssh.Signer {
96 return ss.signer
97}
98
gio33059762024-07-05 13:19:07 +040099func (ss *realClient) UserExists(name string) (bool, error) {
gio11617ac2024-07-15 16:09:04 +0400100 log.Printf("Checking user exists %s", name)
gio33059762024-07-05 13:19:07 +0400101 out, err := ss.RunCommand("user", "list")
102 if err != nil {
103 return false, err
104 }
105 return slices.Contains(strings.Fields(out), name), nil
106}
107
108func (ss *realClient) FindUser(pubKey string) (string, error) {
109 log.Printf("Finding user %s", pubKey)
110 pk := strings.Join(strings.Fields(pubKey)[:2], " ")
111 out, err := ss.RunCommand("user", "list")
112 if err != nil {
113 return "", err
114 }
115 for _, user := range strings.Fields(out) {
116 info, err := ss.RunCommand("user", "info", user)
117 if err != nil {
118 return "", err
119 }
120 if strings.Contains(info, pk) {
121 return user, nil
122 }
123 }
124 return "", nil
125}
126
gioe72b54f2024-04-22 10:44:41 +0400127func (ss *realClient) AddUser(name, pubKey string) error {
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400128 log.Printf("Adding user %s", name)
gio266c04f2024-07-03 14:18:45 +0400129 if _, err := ss.RunCommand("user", "create", name); err != nil {
giolekva8aa73e82022-07-09 11:34:39 +0400130 return err
131 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400132 return ss.AddPublicKey(name, pubKey)
giolekva8aa73e82022-07-09 11:34:39 +0400133}
134
gioe72b54f2024-04-22 10:44:41 +0400135func (ss *realClient) MakeUserAdmin(name string) error {
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400136 log.Printf("Making user %s admin", name)
gio266c04f2024-07-03 14:18:45 +0400137 _, err := ss.RunCommand("user", "set-admin", name, "true")
138 return err
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400139}
140
gioe72b54f2024-04-22 10:44:41 +0400141func (ss *realClient) AddPublicKey(user string, pubKey string) error {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400142 log.Printf("Adding public key: %s %s\n", user, pubKey)
gio266c04f2024-07-03 14:18:45 +0400143 _, err := ss.RunCommand("user", "add-pubkey", user, pubKey)
144 return err
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400145}
146
gioe72b54f2024-04-22 10:44:41 +0400147func (ss *realClient) RemovePublicKey(user string, pubKey string) error {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400148 log.Printf("Removing public key: %s %s\n", user, pubKey)
gio266c04f2024-07-03 14:18:45 +0400149 _, err := ss.RunCommand("user", "remove-pubkey", user, pubKey)
150 return err
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400151}
152
gio266c04f2024-07-03 14:18:45 +0400153func (ss *realClient) RunCommand(args ...string) (string, error) {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400154 cmd := strings.Join(args, " ")
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400155 log.Printf("Running command %s", cmd)
gioe72b54f2024-04-22 10:44:41 +0400156 client, err := ssh.Dial("tcp", ss.addr, ss.sshClientConfig())
giolekva8aa73e82022-07-09 11:34:39 +0400157 if err != nil {
gio266c04f2024-07-03 14:18:45 +0400158 return "", err
giolekva8aa73e82022-07-09 11:34:39 +0400159 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400160 defer client.Close()
giolekva8aa73e82022-07-09 11:34:39 +0400161 session, err := client.NewSession()
162 if err != nil {
gio266c04f2024-07-03 14:18:45 +0400163 return "", err
giolekva8aa73e82022-07-09 11:34:39 +0400164 }
165 defer session.Close()
gio266c04f2024-07-03 14:18:45 +0400166 var buf strings.Builder
167 session.Stdout = &buf
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400168 session.Stderr = os.Stderr
gio266c04f2024-07-03 14:18:45 +0400169 err = session.Run(cmd)
170 return buf.String(), err
171}
172
gio33059762024-07-05 13:19:07 +0400173func (ss *realClient) RepoExists(name string) (bool, error) {
gio266c04f2024-07-03 14:18:45 +0400174 out, err := ss.RunCommand("repo", "list")
175 if err != nil {
176 return false, err
177 }
178 return slices.Contains(strings.Fields(out), name), nil
giolekva8aa73e82022-07-09 11:34:39 +0400179}
180
gioe72b54f2024-04-22 10:44:41 +0400181func (ss *realClient) AddRepository(name string) error {
giolekva8aa73e82022-07-09 11:34:39 +0400182 log.Printf("Adding repository %s", name)
gio33059762024-07-05 13:19:07 +0400183 if ok, err := ss.RepoExists(name); ok {
gio0eaf2712024-04-14 13:08:46 +0400184 return ErrorAlreadyExists
gio266c04f2024-07-03 14:18:45 +0400185 } else if err != nil {
186 return err
gio0eaf2712024-04-14 13:08:46 +0400187 }
gio266c04f2024-07-03 14:18:45 +0400188 _, err := ss.RunCommand("repo", "create", name)
189 return err
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400190}
191
gioe72b54f2024-04-22 10:44:41 +0400192func (ss *realClient) AddReadWriteCollaborator(repo, user string) error {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400193 log.Printf("Adding read-write collaborator %s %s", repo, user)
gio266c04f2024-07-03 14:18:45 +0400194 _, err := ss.RunCommand("repo", "collab", "add", repo, user, "read-write")
195 return err
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400196}
197
gioe72b54f2024-04-22 10:44:41 +0400198func (ss *realClient) AddReadOnlyCollaborator(repo, user string) error {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400199 log.Printf("Adding read-only collaborator %s %s", repo, user)
gio266c04f2024-07-03 14:18:45 +0400200 _, err := ss.RunCommand("repo", "collab", "add", repo, user, "read-only")
201 return err
giolekva8aa73e82022-07-09 11:34:39 +0400202}
203
gio0eaf2712024-04-14 13:08:46 +0400204func (ss *realClient) AddWebhook(repo, url string, opts ...string) error {
205 log.Printf("Adding webhook %s %s", repo, url)
gio266c04f2024-07-03 14:18:45 +0400206 _, err := ss.RunCommand(append(
gio0eaf2712024-04-14 13:08:46 +0400207 []string{"repo", "webhook", "create", repo, url},
208 opts...,
209 )...)
gio266c04f2024-07-03 14:18:45 +0400210 return err
gio0eaf2712024-04-14 13:08:46 +0400211}
212
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400213type Repository struct {
214 *git.Repository
215 Addr RepositoryAddress
216}
217
gioe72b54f2024-04-22 10:44:41 +0400218func (ss *realClient) GetRepo(name string) (RepoIO, error) {
219 r, err := CloneRepository(RepositoryAddress{ss.addr, name}, ss.signer)
220 if err != nil {
221 return nil, err
222 }
223 return NewRepoIO(r, ss.signer)
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400224}
225
giocafd4e62024-07-31 10:53:40 +0400226func (ss *realClient) GetAllRepos() ([]string, error) {
227 log.Printf("Getting all repos")
228 out, err := ss.RunCommand("repo", "list")
229 if err != nil {
230 return nil, err
231 }
232 return strings.Fields(out), nil
233}
234
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400235type RepositoryAddress struct {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400236 Addr string
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400237 Name string
238}
239
240func ParseRepositoryAddress(addr string) (RepositoryAddress, error) {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400241 items := regexp.MustCompile(`ssh://(.*)/(.*)`).FindStringSubmatch(addr)
242 if len(items) != 3 {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400243 return RepositoryAddress{}, fmt.Errorf("Invalid address")
244 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400245 return RepositoryAddress{items[1], items[2]}, nil
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400246}
247
248func (r RepositoryAddress) FullAddress() string {
249 return fmt.Sprintf("ssh://%s/%s", r.Addr, r.Name)
250}
251
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400252func CloneRepository(addr RepositoryAddress, signer ssh.Signer) (*Repository, error) {
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400253 fmt.Printf("Cloning repository: %s %s\n", addr.Addr, addr.Name)
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400254 c, err := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
255 URL: addr.FullAddress(),
256 Auth: &gitssh.PublicKeys{
257 User: "git",
258 Signer: signer,
259 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
260 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
261 // TODO(giolekva): verify server public key
262 fmt.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
263 return nil
264 },
265 },
266 },
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400267 RemoteName: "origin",
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400268 ReferenceName: "refs/heads/master",
giof5ffedb2024-06-19 14:14:43 +0400269 SingleBranch: true,
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400270 Depth: 1,
271 InsecureSkipTLS: true,
272 Progress: os.Stdout,
273 })
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400274 if err != nil && !errors.Is(err, transport.ErrEmptyRemoteRepository) {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400275 return nil, err
276 }
giof5ffedb2024-06-19 14:14:43 +0400277 wt, err := c.Worktree()
278 if err != nil {
279 return nil, err
280 }
281 sb, err := wt.Submodules()
282 if err != nil {
283 return nil, err
284 }
285 if err := sb.Init(); err != nil {
286 return nil, err
287 }
288 if err := sb.Update(&git.SubmoduleUpdateOptions{
289 Depth: 1,
290 }); err != nil {
291 return nil, err
292 }
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400293 return &Repository{
294 Repository: c,
295 Addr: addr,
296 }, nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400297}
298
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400299// TODO(giolekva): dead code
gioe72b54f2024-04-22 10:44:41 +0400300func (ss *realClient) authSSH() gitssh.AuthMethod {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400301 a, err := gitssh.NewPublicKeys("git", ss.pemBytes, "")
giolekva8aa73e82022-07-09 11:34:39 +0400302 if err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400303 panic(err)
giolekva8aa73e82022-07-09 11:34:39 +0400304 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400305 a.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
306 // TODO(giolekva): verify server public key
307 ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
308 return nil
giolekva8aa73e82022-07-09 11:34:39 +0400309 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400310 return a
311 // return &gitssh.PublicKeys{
312 // User: "git",
313 // Signer: ss.Signer,
314 // HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
315 // HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
316 // // TODO(giolekva): verify server public key
317 // ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
318 // return nil
319 // },
320 // },
321 // }
giolekva8aa73e82022-07-09 11:34:39 +0400322}
323
gioe72b54f2024-04-22 10:44:41 +0400324func (ss *realClient) authGit() *gitssh.PublicKeys {
giolekva8aa73e82022-07-09 11:34:39 +0400325 return &gitssh.PublicKeys{
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400326 User: "git",
gioe72b54f2024-04-22 10:44:41 +0400327 Signer: ss.signer,
giolekva8aa73e82022-07-09 11:34:39 +0400328 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
329 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
330 // TODO(giolekva): verify server public key
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400331 ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
giolekva8aa73e82022-07-09 11:34:39 +0400332 return nil
333 },
334 },
335 }
336}
337
gioe72b54f2024-04-22 10:44:41 +0400338func (ss *realClient) GetPublicKeys() ([]string, error) {
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400339 var ret []string
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400340 config := &ssh.ClientConfig{
341 Auth: []ssh.AuthMethod{
gioe72b54f2024-04-22 10:44:41 +0400342 ssh.PublicKeys(ss.signer),
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400343 },
344 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400345 ret = append(ret, string(ssh.MarshalAuthorizedKey(key)))
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400346 return nil
347 },
348 }
gioe72b54f2024-04-22 10:44:41 +0400349 client, err := ssh.Dial("tcp", ss.addr, config)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400350 if err != nil {
351 return nil, err
352 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400353 defer client.Close()
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400354 return ret, nil
355}
356
gioe72b54f2024-04-22 10:44:41 +0400357func (ss *realClient) sshClientConfig() *ssh.ClientConfig {
giolekva8aa73e82022-07-09 11:34:39 +0400358 return &ssh.ClientConfig{
359 Auth: []ssh.AuthMethod{
gioe72b54f2024-04-22 10:44:41 +0400360 ssh.PublicKeys(ss.signer),
giolekva8aa73e82022-07-09 11:34:39 +0400361 },
362 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
363 // TODO(giolekva): verify server public key
364 // fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400365 fmt.Printf("%s %s %s", hostname, remote, ssh.MarshalAuthorizedKey(key))
giolekva8aa73e82022-07-09 11:34:39 +0400366 return nil
367 },
368 }
369}
370
gioe72b54f2024-04-22 10:44:41 +0400371func (ss *realClient) GetRepoAddress(name string) string {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400372 return fmt.Sprintf("%s/%s", ss.addressGit(), name)
373}
374
gioe72b54f2024-04-22 10:44:41 +0400375func (ss *realClient) addressGit() string {
376 return fmt.Sprintf("ssh://%s", ss.addr)
giolekva8aa73e82022-07-09 11:34:39 +0400377}