blob: 473efeb552753cd168e6be9cc9ab261c1bc13da7 [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)
29 GetRepo(name string) (RepoIO, error)
30 GetRepoAddress(name string) string
31 AddRepository(name string) error
32 AddUser(name, pubKey string) error
33 AddPublicKey(user string, pubKey string) error
34 RemovePublicKey(user string, pubKey string) error
35 MakeUserAdmin(name string) error
36 AddReadWriteCollaborator(repo, user string) error
37 AddReadOnlyCollaborator(repo, user string) error
gio0eaf2712024-04-14 13:08:46 +040038 AddWebhook(repo, url string, opts ...string) error
gioe72b54f2024-04-22 10:44:41 +040039}
40
41type realClient struct {
42 addr string
43 signer ssh.Signer
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040044 log *log.Logger
45 pemBytes []byte
giolekva8aa73e82022-07-09 11:34:39 +040046}
47
gioe72b54f2024-04-22 10:44:41 +040048func NewClient(addr string, clientPrivateKey []byte, log *log.Logger) (Client, error) {
giolekva8aa73e82022-07-09 11:34:39 +040049 signer, err := ssh.ParsePrivateKey(clientPrivateKey)
50 if err != nil {
51 return nil, err
52 }
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040053 log.SetPrefix("SOFT-SERVE: ")
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040054 log.Printf("Created signer")
gioe72b54f2024-04-22 10:44:41 +040055 return &realClient{
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040056 addr,
giolekva8aa73e82022-07-09 11:34:39 +040057 signer,
58 log,
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040059 clientPrivateKey,
giolekva8aa73e82022-07-09 11:34:39 +040060 }, nil
61}
62
gioe72b54f2024-04-22 10:44:41 +040063type ClientGetter interface {
64 Get(addr string, clientPrivateKey []byte, log *log.Logger) (Client, error)
65}
66
67type RealClientGetter struct{}
68
69func (c RealClientGetter) Get(addr string, clientPrivateKey []byte, log *log.Logger) (Client, error) {
70 var client Client
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040071 err := backoff.RetryNotify(func() error {
72 var err error
73 client, err = NewClient(addr, clientPrivateKey, log)
74 if err != nil {
75 return err
76 }
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +040077 if _, err := client.GetPublicKeys(); err != nil {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040078 return err
79 }
80 return nil
81 }, backoff.NewConstantBackOff(5*time.Second), func(err error, _ time.Duration) {
82 log.Printf("Failed to create client: %s\n", err.Error())
83 })
84 return client, err
85}
86
gioe72b54f2024-04-22 10:44:41 +040087func (ss *realClient) Address() string {
88 return ss.addr
89}
90
91func (ss *realClient) Signer() ssh.Signer {
92 return ss.signer
93}
94
95func (ss *realClient) AddUser(name, pubKey string) error {
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040096 log.Printf("Adding user %s", name)
gio266c04f2024-07-03 14:18:45 +040097 if _, err := ss.RunCommand("user", "create", name); err != nil {
giolekva8aa73e82022-07-09 11:34:39 +040098 return err
99 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400100 return ss.AddPublicKey(name, pubKey)
giolekva8aa73e82022-07-09 11:34:39 +0400101}
102
gioe72b54f2024-04-22 10:44:41 +0400103func (ss *realClient) MakeUserAdmin(name string) error {
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400104 log.Printf("Making user %s admin", name)
gio266c04f2024-07-03 14:18:45 +0400105 _, err := ss.RunCommand("user", "set-admin", name, "true")
106 return err
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400107}
108
gioe72b54f2024-04-22 10:44:41 +0400109func (ss *realClient) AddPublicKey(user string, pubKey string) error {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400110 log.Printf("Adding public key: %s %s\n", user, pubKey)
gio266c04f2024-07-03 14:18:45 +0400111 _, err := ss.RunCommand("user", "add-pubkey", user, pubKey)
112 return err
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400113}
114
gioe72b54f2024-04-22 10:44:41 +0400115func (ss *realClient) RemovePublicKey(user string, pubKey string) error {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400116 log.Printf("Removing public key: %s %s\n", user, pubKey)
gio266c04f2024-07-03 14:18:45 +0400117 _, err := ss.RunCommand("user", "remove-pubkey", user, pubKey)
118 return err
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400119}
120
gio266c04f2024-07-03 14:18:45 +0400121func (ss *realClient) RunCommand(args ...string) (string, error) {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400122 cmd := strings.Join(args, " ")
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400123 log.Printf("Running command %s", cmd)
gioe72b54f2024-04-22 10:44:41 +0400124 client, err := ssh.Dial("tcp", ss.addr, ss.sshClientConfig())
giolekva8aa73e82022-07-09 11:34:39 +0400125 if err != nil {
gio266c04f2024-07-03 14:18:45 +0400126 return "", err
giolekva8aa73e82022-07-09 11:34:39 +0400127 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400128 defer client.Close()
giolekva8aa73e82022-07-09 11:34:39 +0400129 session, err := client.NewSession()
130 if err != nil {
gio266c04f2024-07-03 14:18:45 +0400131 return "", err
giolekva8aa73e82022-07-09 11:34:39 +0400132 }
133 defer session.Close()
gio266c04f2024-07-03 14:18:45 +0400134 var buf strings.Builder
135 session.Stdout = &buf
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400136 session.Stderr = os.Stderr
gio266c04f2024-07-03 14:18:45 +0400137 err = session.Run(cmd)
138 return buf.String(), err
139}
140
141func (ss *realClient) repoExists(name string) (bool, error) {
142 // if err := ss.RunCommand("repo", "info", name); err == nil {
143 // return ErrorAlreadyExists
144 // }
145 out, err := ss.RunCommand("repo", "list")
146 if err != nil {
147 return false, err
148 }
149 return slices.Contains(strings.Fields(out), name), nil
giolekva8aa73e82022-07-09 11:34:39 +0400150}
151
gioe72b54f2024-04-22 10:44:41 +0400152func (ss *realClient) AddRepository(name string) error {
giolekva8aa73e82022-07-09 11:34:39 +0400153 log.Printf("Adding repository %s", name)
gio266c04f2024-07-03 14:18:45 +0400154 if ok, err := ss.repoExists(name); ok {
gio0eaf2712024-04-14 13:08:46 +0400155 return ErrorAlreadyExists
gio266c04f2024-07-03 14:18:45 +0400156 } else if err != nil {
157 return err
gio0eaf2712024-04-14 13:08:46 +0400158 }
gio266c04f2024-07-03 14:18:45 +0400159 _, err := ss.RunCommand("repo", "create", name)
160 return err
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400161}
162
gioe72b54f2024-04-22 10:44:41 +0400163func (ss *realClient) AddReadWriteCollaborator(repo, user string) error {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400164 log.Printf("Adding read-write collaborator %s %s", repo, user)
gio266c04f2024-07-03 14:18:45 +0400165 _, err := ss.RunCommand("repo", "collab", "add", repo, user, "read-write")
166 return err
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400167}
168
gioe72b54f2024-04-22 10:44:41 +0400169func (ss *realClient) AddReadOnlyCollaborator(repo, user string) error {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400170 log.Printf("Adding read-only collaborator %s %s", repo, user)
gio266c04f2024-07-03 14:18:45 +0400171 _, err := ss.RunCommand("repo", "collab", "add", repo, user, "read-only")
172 return err
giolekva8aa73e82022-07-09 11:34:39 +0400173}
174
gio0eaf2712024-04-14 13:08:46 +0400175func (ss *realClient) AddWebhook(repo, url string, opts ...string) error {
176 log.Printf("Adding webhook %s %s", repo, url)
gio266c04f2024-07-03 14:18:45 +0400177 _, err := ss.RunCommand(append(
gio0eaf2712024-04-14 13:08:46 +0400178 []string{"repo", "webhook", "create", repo, url},
179 opts...,
180 )...)
gio266c04f2024-07-03 14:18:45 +0400181 return err
gio0eaf2712024-04-14 13:08:46 +0400182}
183
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400184type Repository struct {
185 *git.Repository
186 Addr RepositoryAddress
187}
188
gioe72b54f2024-04-22 10:44:41 +0400189func (ss *realClient) GetRepo(name string) (RepoIO, error) {
190 r, err := CloneRepository(RepositoryAddress{ss.addr, name}, ss.signer)
191 if err != nil {
192 return nil, err
193 }
194 return NewRepoIO(r, ss.signer)
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400195}
196
197type RepositoryAddress struct {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400198 Addr string
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400199 Name string
200}
201
202func ParseRepositoryAddress(addr string) (RepositoryAddress, error) {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400203 items := regexp.MustCompile(`ssh://(.*)/(.*)`).FindStringSubmatch(addr)
204 if len(items) != 3 {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400205 return RepositoryAddress{}, fmt.Errorf("Invalid address")
206 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400207 return RepositoryAddress{items[1], items[2]}, nil
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400208}
209
210func (r RepositoryAddress) FullAddress() string {
211 return fmt.Sprintf("ssh://%s/%s", r.Addr, r.Name)
212}
213
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400214func CloneRepository(addr RepositoryAddress, signer ssh.Signer) (*Repository, error) {
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400215 fmt.Printf("Cloning repository: %s %s\n", addr.Addr, addr.Name)
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400216 c, err := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
217 URL: addr.FullAddress(),
218 Auth: &gitssh.PublicKeys{
219 User: "git",
220 Signer: signer,
221 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
222 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
223 // TODO(giolekva): verify server public key
224 fmt.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
225 return nil
226 },
227 },
228 },
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400229 RemoteName: "origin",
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400230 ReferenceName: "refs/heads/master",
giof5ffedb2024-06-19 14:14:43 +0400231 SingleBranch: true,
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400232 Depth: 1,
233 InsecureSkipTLS: true,
234 Progress: os.Stdout,
235 })
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400236 if err != nil && !errors.Is(err, transport.ErrEmptyRemoteRepository) {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400237 return nil, err
238 }
giof5ffedb2024-06-19 14:14:43 +0400239 wt, err := c.Worktree()
240 if err != nil {
241 return nil, err
242 }
243 sb, err := wt.Submodules()
244 if err != nil {
245 return nil, err
246 }
247 if err := sb.Init(); err != nil {
248 return nil, err
249 }
250 if err := sb.Update(&git.SubmoduleUpdateOptions{
251 Depth: 1,
252 }); err != nil {
253 return nil, err
254 }
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400255 return &Repository{
256 Repository: c,
257 Addr: addr,
258 }, nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400259}
260
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400261// TODO(giolekva): dead code
gioe72b54f2024-04-22 10:44:41 +0400262func (ss *realClient) authSSH() gitssh.AuthMethod {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400263 a, err := gitssh.NewPublicKeys("git", ss.pemBytes, "")
giolekva8aa73e82022-07-09 11:34:39 +0400264 if err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400265 panic(err)
giolekva8aa73e82022-07-09 11:34:39 +0400266 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400267 a.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
268 // TODO(giolekva): verify server public key
269 ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
270 return nil
giolekva8aa73e82022-07-09 11:34:39 +0400271 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400272 return a
273 // return &gitssh.PublicKeys{
274 // User: "git",
275 // Signer: ss.Signer,
276 // HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
277 // HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
278 // // TODO(giolekva): verify server public key
279 // ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
280 // return nil
281 // },
282 // },
283 // }
giolekva8aa73e82022-07-09 11:34:39 +0400284}
285
gioe72b54f2024-04-22 10:44:41 +0400286func (ss *realClient) authGit() *gitssh.PublicKeys {
giolekva8aa73e82022-07-09 11:34:39 +0400287 return &gitssh.PublicKeys{
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400288 User: "git",
gioe72b54f2024-04-22 10:44:41 +0400289 Signer: ss.signer,
giolekva8aa73e82022-07-09 11:34:39 +0400290 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
291 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
292 // TODO(giolekva): verify server public key
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400293 ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
giolekva8aa73e82022-07-09 11:34:39 +0400294 return nil
295 },
296 },
297 }
298}
299
gioe72b54f2024-04-22 10:44:41 +0400300func (ss *realClient) GetPublicKeys() ([]string, error) {
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400301 var ret []string
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400302 config := &ssh.ClientConfig{
303 Auth: []ssh.AuthMethod{
gioe72b54f2024-04-22 10:44:41 +0400304 ssh.PublicKeys(ss.signer),
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400305 },
306 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400307 ret = append(ret, string(ssh.MarshalAuthorizedKey(key)))
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400308 return nil
309 },
310 }
gioe72b54f2024-04-22 10:44:41 +0400311 client, err := ssh.Dial("tcp", ss.addr, config)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400312 if err != nil {
313 return nil, err
314 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400315 defer client.Close()
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400316 return ret, nil
317}
318
gioe72b54f2024-04-22 10:44:41 +0400319func (ss *realClient) sshClientConfig() *ssh.ClientConfig {
giolekva8aa73e82022-07-09 11:34:39 +0400320 return &ssh.ClientConfig{
321 Auth: []ssh.AuthMethod{
gioe72b54f2024-04-22 10:44:41 +0400322 ssh.PublicKeys(ss.signer),
giolekva8aa73e82022-07-09 11:34:39 +0400323 },
324 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
325 // TODO(giolekva): verify server public key
326 // fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400327 fmt.Printf("%s %s %s", hostname, remote, ssh.MarshalAuthorizedKey(key))
giolekva8aa73e82022-07-09 11:34:39 +0400328 return nil
329 },
330 }
331}
332
gioe72b54f2024-04-22 10:44:41 +0400333func (ss *realClient) GetRepoAddress(name string) string {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400334 return fmt.Sprintf("%s/%s", ss.addressGit(), name)
335}
336
gioe72b54f2024-04-22 10:44:41 +0400337func (ss *realClient) addressGit() string {
338 return fmt.Sprintf("ssh://%s", ss.addr)
giolekva8aa73e82022-07-09 11:34:39 +0400339}