blob: 4a5021edb580e72fb9ac4eec3ae7477e0ec700a5 [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)
31 GetRepoAddress(name string) string
32 AddRepository(name string) error
gio33059762024-07-05 13:19:07 +040033 UserExists(name string) (bool, error)
34 FindUser(pubKey string) (string, error)
gioe72b54f2024-04-22 10:44:41 +040035 AddUser(name, pubKey string) error
36 AddPublicKey(user string, pubKey string) error
37 RemovePublicKey(user string, pubKey string) error
38 MakeUserAdmin(name string) error
39 AddReadWriteCollaborator(repo, user string) error
40 AddReadOnlyCollaborator(repo, user string) error
gio0eaf2712024-04-14 13:08:46 +040041 AddWebhook(repo, url string, opts ...string) error
gioe72b54f2024-04-22 10:44:41 +040042}
43
44type realClient struct {
45 addr string
46 signer ssh.Signer
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040047 log *log.Logger
48 pemBytes []byte
giolekva8aa73e82022-07-09 11:34:39 +040049}
50
gioe72b54f2024-04-22 10:44:41 +040051func NewClient(addr string, clientPrivateKey []byte, log *log.Logger) (Client, error) {
giolekva8aa73e82022-07-09 11:34:39 +040052 signer, err := ssh.ParsePrivateKey(clientPrivateKey)
53 if err != nil {
54 return nil, err
55 }
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040056 log.SetPrefix("SOFT-SERVE: ")
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040057 log.Printf("Created signer")
gioe72b54f2024-04-22 10:44:41 +040058 return &realClient{
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040059 addr,
giolekva8aa73e82022-07-09 11:34:39 +040060 signer,
61 log,
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040062 clientPrivateKey,
giolekva8aa73e82022-07-09 11:34:39 +040063 }, nil
64}
65
gioe72b54f2024-04-22 10:44:41 +040066type ClientGetter interface {
67 Get(addr string, clientPrivateKey []byte, log *log.Logger) (Client, error)
68}
69
70type RealClientGetter struct{}
71
72func (c RealClientGetter) Get(addr string, clientPrivateKey []byte, log *log.Logger) (Client, error) {
73 var client Client
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040074 err := backoff.RetryNotify(func() error {
75 var err error
76 client, err = NewClient(addr, clientPrivateKey, log)
77 if err != nil {
78 return err
79 }
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +040080 if _, err := client.GetPublicKeys(); err != nil {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040081 return err
82 }
83 return nil
84 }, backoff.NewConstantBackOff(5*time.Second), func(err error, _ time.Duration) {
85 log.Printf("Failed to create client: %s\n", err.Error())
86 })
87 return client, err
88}
89
gioe72b54f2024-04-22 10:44:41 +040090func (ss *realClient) Address() string {
91 return ss.addr
92}
93
94func (ss *realClient) Signer() ssh.Signer {
95 return ss.signer
96}
97
gio33059762024-07-05 13:19:07 +040098func (ss *realClient) UserExists(name string) (bool, error) {
99 log.Printf("Adding user %s", name)
100 out, err := ss.RunCommand("user", "list")
101 if err != nil {
102 return false, err
103 }
104 return slices.Contains(strings.Fields(out), name), nil
105}
106
107func (ss *realClient) FindUser(pubKey string) (string, error) {
108 log.Printf("Finding user %s", pubKey)
109 pk := strings.Join(strings.Fields(pubKey)[:2], " ")
110 out, err := ss.RunCommand("user", "list")
111 if err != nil {
112 return "", err
113 }
114 for _, user := range strings.Fields(out) {
115 info, err := ss.RunCommand("user", "info", user)
116 if err != nil {
117 return "", err
118 }
119 if strings.Contains(info, pk) {
120 return user, nil
121 }
122 }
123 return "", nil
124}
125
gioe72b54f2024-04-22 10:44:41 +0400126func (ss *realClient) AddUser(name, pubKey string) error {
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400127 log.Printf("Adding user %s", name)
gio266c04f2024-07-03 14:18:45 +0400128 if _, err := ss.RunCommand("user", "create", name); err != nil {
giolekva8aa73e82022-07-09 11:34:39 +0400129 return err
130 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400131 return ss.AddPublicKey(name, pubKey)
giolekva8aa73e82022-07-09 11:34:39 +0400132}
133
gioe72b54f2024-04-22 10:44:41 +0400134func (ss *realClient) MakeUserAdmin(name string) error {
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400135 log.Printf("Making user %s admin", name)
gio266c04f2024-07-03 14:18:45 +0400136 _, err := ss.RunCommand("user", "set-admin", name, "true")
137 return err
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400138}
139
gioe72b54f2024-04-22 10:44:41 +0400140func (ss *realClient) AddPublicKey(user string, pubKey string) error {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400141 log.Printf("Adding public key: %s %s\n", user, pubKey)
gio266c04f2024-07-03 14:18:45 +0400142 _, err := ss.RunCommand("user", "add-pubkey", user, pubKey)
143 return err
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400144}
145
gioe72b54f2024-04-22 10:44:41 +0400146func (ss *realClient) RemovePublicKey(user string, pubKey string) error {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400147 log.Printf("Removing public key: %s %s\n", user, pubKey)
gio266c04f2024-07-03 14:18:45 +0400148 _, err := ss.RunCommand("user", "remove-pubkey", user, pubKey)
149 return err
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400150}
151
gio266c04f2024-07-03 14:18:45 +0400152func (ss *realClient) RunCommand(args ...string) (string, error) {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400153 cmd := strings.Join(args, " ")
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400154 log.Printf("Running command %s", cmd)
gioe72b54f2024-04-22 10:44:41 +0400155 client, err := ssh.Dial("tcp", ss.addr, ss.sshClientConfig())
giolekva8aa73e82022-07-09 11:34:39 +0400156 if err != nil {
gio266c04f2024-07-03 14:18:45 +0400157 return "", err
giolekva8aa73e82022-07-09 11:34:39 +0400158 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400159 defer client.Close()
giolekva8aa73e82022-07-09 11:34:39 +0400160 session, err := client.NewSession()
161 if err != nil {
gio266c04f2024-07-03 14:18:45 +0400162 return "", err
giolekva8aa73e82022-07-09 11:34:39 +0400163 }
164 defer session.Close()
gio266c04f2024-07-03 14:18:45 +0400165 var buf strings.Builder
166 session.Stdout = &buf
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400167 session.Stderr = os.Stderr
gio266c04f2024-07-03 14:18:45 +0400168 err = session.Run(cmd)
169 return buf.String(), err
170}
171
gio33059762024-07-05 13:19:07 +0400172func (ss *realClient) RepoExists(name string) (bool, error) {
gio266c04f2024-07-03 14:18:45 +0400173 out, err := ss.RunCommand("repo", "list")
174 if err != nil {
175 return false, err
176 }
177 return slices.Contains(strings.Fields(out), name), nil
giolekva8aa73e82022-07-09 11:34:39 +0400178}
179
gioe72b54f2024-04-22 10:44:41 +0400180func (ss *realClient) AddRepository(name string) error {
giolekva8aa73e82022-07-09 11:34:39 +0400181 log.Printf("Adding repository %s", name)
gio33059762024-07-05 13:19:07 +0400182 if ok, err := ss.RepoExists(name); ok {
gio0eaf2712024-04-14 13:08:46 +0400183 return ErrorAlreadyExists
gio266c04f2024-07-03 14:18:45 +0400184 } else if err != nil {
185 return err
gio0eaf2712024-04-14 13:08:46 +0400186 }
gio266c04f2024-07-03 14:18:45 +0400187 _, err := ss.RunCommand("repo", "create", name)
188 return err
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400189}
190
gioe72b54f2024-04-22 10:44:41 +0400191func (ss *realClient) AddReadWriteCollaborator(repo, user string) error {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400192 log.Printf("Adding read-write collaborator %s %s", repo, user)
gio266c04f2024-07-03 14:18:45 +0400193 _, err := ss.RunCommand("repo", "collab", "add", repo, user, "read-write")
194 return err
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400195}
196
gioe72b54f2024-04-22 10:44:41 +0400197func (ss *realClient) AddReadOnlyCollaborator(repo, user string) error {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400198 log.Printf("Adding read-only collaborator %s %s", repo, user)
gio266c04f2024-07-03 14:18:45 +0400199 _, err := ss.RunCommand("repo", "collab", "add", repo, user, "read-only")
200 return err
giolekva8aa73e82022-07-09 11:34:39 +0400201}
202
gio0eaf2712024-04-14 13:08:46 +0400203func (ss *realClient) AddWebhook(repo, url string, opts ...string) error {
204 log.Printf("Adding webhook %s %s", repo, url)
gio266c04f2024-07-03 14:18:45 +0400205 _, err := ss.RunCommand(append(
gio0eaf2712024-04-14 13:08:46 +0400206 []string{"repo", "webhook", "create", repo, url},
207 opts...,
208 )...)
gio266c04f2024-07-03 14:18:45 +0400209 return err
gio0eaf2712024-04-14 13:08:46 +0400210}
211
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400212type Repository struct {
213 *git.Repository
214 Addr RepositoryAddress
215}
216
gioe72b54f2024-04-22 10:44:41 +0400217func (ss *realClient) GetRepo(name string) (RepoIO, error) {
218 r, err := CloneRepository(RepositoryAddress{ss.addr, name}, ss.signer)
219 if err != nil {
220 return nil, err
221 }
222 return NewRepoIO(r, ss.signer)
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400223}
224
225type RepositoryAddress struct {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400226 Addr string
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400227 Name string
228}
229
230func ParseRepositoryAddress(addr string) (RepositoryAddress, error) {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400231 items := regexp.MustCompile(`ssh://(.*)/(.*)`).FindStringSubmatch(addr)
232 if len(items) != 3 {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400233 return RepositoryAddress{}, fmt.Errorf("Invalid address")
234 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400235 return RepositoryAddress{items[1], items[2]}, nil
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400236}
237
238func (r RepositoryAddress) FullAddress() string {
239 return fmt.Sprintf("ssh://%s/%s", r.Addr, r.Name)
240}
241
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400242func CloneRepository(addr RepositoryAddress, signer ssh.Signer) (*Repository, error) {
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400243 fmt.Printf("Cloning repository: %s %s\n", addr.Addr, addr.Name)
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400244 c, err := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
245 URL: addr.FullAddress(),
246 Auth: &gitssh.PublicKeys{
247 User: "git",
248 Signer: signer,
249 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
250 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
251 // TODO(giolekva): verify server public key
252 fmt.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
253 return nil
254 },
255 },
256 },
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400257 RemoteName: "origin",
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400258 ReferenceName: "refs/heads/master",
giof5ffedb2024-06-19 14:14:43 +0400259 SingleBranch: true,
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400260 Depth: 1,
261 InsecureSkipTLS: true,
262 Progress: os.Stdout,
263 })
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400264 if err != nil && !errors.Is(err, transport.ErrEmptyRemoteRepository) {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400265 return nil, err
266 }
giof5ffedb2024-06-19 14:14:43 +0400267 wt, err := c.Worktree()
268 if err != nil {
269 return nil, err
270 }
271 sb, err := wt.Submodules()
272 if err != nil {
273 return nil, err
274 }
275 if err := sb.Init(); err != nil {
276 return nil, err
277 }
278 if err := sb.Update(&git.SubmoduleUpdateOptions{
279 Depth: 1,
280 }); err != nil {
281 return nil, err
282 }
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400283 return &Repository{
284 Repository: c,
285 Addr: addr,
286 }, nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400287}
288
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400289// TODO(giolekva): dead code
gioe72b54f2024-04-22 10:44:41 +0400290func (ss *realClient) authSSH() gitssh.AuthMethod {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400291 a, err := gitssh.NewPublicKeys("git", ss.pemBytes, "")
giolekva8aa73e82022-07-09 11:34:39 +0400292 if err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400293 panic(err)
giolekva8aa73e82022-07-09 11:34:39 +0400294 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400295 a.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
296 // TODO(giolekva): verify server public key
297 ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
298 return nil
giolekva8aa73e82022-07-09 11:34:39 +0400299 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400300 return a
301 // return &gitssh.PublicKeys{
302 // User: "git",
303 // Signer: ss.Signer,
304 // HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
305 // 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
309 // },
310 // },
311 // }
giolekva8aa73e82022-07-09 11:34:39 +0400312}
313
gioe72b54f2024-04-22 10:44:41 +0400314func (ss *realClient) authGit() *gitssh.PublicKeys {
giolekva8aa73e82022-07-09 11:34:39 +0400315 return &gitssh.PublicKeys{
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400316 User: "git",
gioe72b54f2024-04-22 10:44:41 +0400317 Signer: ss.signer,
giolekva8aa73e82022-07-09 11:34:39 +0400318 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
319 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
320 // TODO(giolekva): verify server public key
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400321 ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
giolekva8aa73e82022-07-09 11:34:39 +0400322 return nil
323 },
324 },
325 }
326}
327
gioe72b54f2024-04-22 10:44:41 +0400328func (ss *realClient) GetPublicKeys() ([]string, error) {
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400329 var ret []string
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400330 config := &ssh.ClientConfig{
331 Auth: []ssh.AuthMethod{
gioe72b54f2024-04-22 10:44:41 +0400332 ssh.PublicKeys(ss.signer),
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400333 },
334 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400335 ret = append(ret, string(ssh.MarshalAuthorizedKey(key)))
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400336 return nil
337 },
338 }
gioe72b54f2024-04-22 10:44:41 +0400339 client, err := ssh.Dial("tcp", ss.addr, config)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400340 if err != nil {
341 return nil, err
342 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400343 defer client.Close()
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400344 return ret, nil
345}
346
gioe72b54f2024-04-22 10:44:41 +0400347func (ss *realClient) sshClientConfig() *ssh.ClientConfig {
giolekva8aa73e82022-07-09 11:34:39 +0400348 return &ssh.ClientConfig{
349 Auth: []ssh.AuthMethod{
gioe72b54f2024-04-22 10:44:41 +0400350 ssh.PublicKeys(ss.signer),
giolekva8aa73e82022-07-09 11:34:39 +0400351 },
352 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
353 // TODO(giolekva): verify server public key
354 // fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400355 fmt.Printf("%s %s %s", hostname, remote, ssh.MarshalAuthorizedKey(key))
giolekva8aa73e82022-07-09 11:34:39 +0400356 return nil
357 },
358 }
359}
360
gioe72b54f2024-04-22 10:44:41 +0400361func (ss *realClient) GetRepoAddress(name string) string {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400362 return fmt.Sprintf("%s/%s", ss.addressGit(), name)
363}
364
gioe72b54f2024-04-22 10:44:41 +0400365func (ss *realClient) addressGit() string {
366 return fmt.Sprintf("ssh://%s", ss.addr)
giolekva8aa73e82022-07-09 11:34:39 +0400367}