blob: 1c9366627241d5495e7beae152a4156f283869bf [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"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040011 "strings"
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040012 "time"
giolekva8aa73e82022-07-09 11:34:39 +040013
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040014 "github.com/cenkalti/backoff/v4"
giolekva8aa73e82022-07-09 11:34:39 +040015 "github.com/go-git/go-billy/v5/memfs"
16 "github.com/go-git/go-git/v5"
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040017 "github.com/go-git/go-git/v5/plumbing/transport"
giolekva8aa73e82022-07-09 11:34:39 +040018 gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
19 "github.com/go-git/go-git/v5/storage/memory"
giolekva8aa73e82022-07-09 11:34:39 +040020)
21
gio0eaf2712024-04-14 13:08:46 +040022var ErrorAlreadyExists = errors.New("already exists")
23
gioe72b54f2024-04-22 10:44:41 +040024type Client interface {
25 Address() string
26 Signer() ssh.Signer
27 GetPublicKeys() ([]string, error)
28 GetRepo(name string) (RepoIO, error)
29 GetRepoAddress(name string) string
30 AddRepository(name string) error
31 AddUser(name, pubKey string) error
32 AddPublicKey(user string, pubKey string) error
33 RemovePublicKey(user string, pubKey string) error
34 MakeUserAdmin(name string) error
35 AddReadWriteCollaborator(repo, user string) error
36 AddReadOnlyCollaborator(repo, user string) error
gio0eaf2712024-04-14 13:08:46 +040037 AddWebhook(repo, url string, opts ...string) error
gioe72b54f2024-04-22 10:44:41 +040038}
39
40type realClient struct {
41 addr string
42 signer ssh.Signer
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040043 log *log.Logger
44 pemBytes []byte
giolekva8aa73e82022-07-09 11:34:39 +040045}
46
gioe72b54f2024-04-22 10:44:41 +040047func NewClient(addr string, clientPrivateKey []byte, log *log.Logger) (Client, error) {
giolekva8aa73e82022-07-09 11:34:39 +040048 signer, err := ssh.ParsePrivateKey(clientPrivateKey)
49 if err != nil {
50 return nil, err
51 }
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040052 log.SetPrefix("SOFT-SERVE: ")
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040053 log.Printf("Created signer")
gioe72b54f2024-04-22 10:44:41 +040054 return &realClient{
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040055 addr,
giolekva8aa73e82022-07-09 11:34:39 +040056 signer,
57 log,
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040058 clientPrivateKey,
giolekva8aa73e82022-07-09 11:34:39 +040059 }, nil
60}
61
gioe72b54f2024-04-22 10:44:41 +040062type ClientGetter interface {
63 Get(addr string, clientPrivateKey []byte, log *log.Logger) (Client, error)
64}
65
66type RealClientGetter struct{}
67
68func (c RealClientGetter) Get(addr string, clientPrivateKey []byte, log *log.Logger) (Client, error) {
69 var client Client
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040070 err := backoff.RetryNotify(func() error {
71 var err error
72 client, err = NewClient(addr, clientPrivateKey, log)
73 if err != nil {
74 return err
75 }
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +040076 if _, err := client.GetPublicKeys(); err != nil {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040077 return err
78 }
79 return nil
80 }, backoff.NewConstantBackOff(5*time.Second), func(err error, _ time.Duration) {
81 log.Printf("Failed to create client: %s\n", err.Error())
82 })
83 return client, err
84}
85
gioe72b54f2024-04-22 10:44:41 +040086func (ss *realClient) Address() string {
87 return ss.addr
88}
89
90func (ss *realClient) Signer() ssh.Signer {
91 return ss.signer
92}
93
94func (ss *realClient) AddUser(name, pubKey string) error {
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040095 log.Printf("Adding user %s", name)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040096 if err := ss.RunCommand("user", "create", name); err != nil {
giolekva8aa73e82022-07-09 11:34:39 +040097 return err
98 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040099 return ss.AddPublicKey(name, pubKey)
giolekva8aa73e82022-07-09 11:34:39 +0400100}
101
gioe72b54f2024-04-22 10:44:41 +0400102func (ss *realClient) MakeUserAdmin(name string) error {
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400103 log.Printf("Making user %s admin", name)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400104 return ss.RunCommand("user", "set-admin", name, "true")
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400105}
106
gioe72b54f2024-04-22 10:44:41 +0400107func (ss *realClient) AddPublicKey(user string, pubKey string) error {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400108 log.Printf("Adding public key: %s %s\n", user, pubKey)
109 return ss.RunCommand("user", "add-pubkey", user, pubKey)
110}
111
gioe72b54f2024-04-22 10:44:41 +0400112func (ss *realClient) RemovePublicKey(user string, pubKey string) error {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400113 log.Printf("Removing public key: %s %s\n", user, pubKey)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400114 return ss.RunCommand("user", "remove-pubkey", user, pubKey)
115}
116
gioe72b54f2024-04-22 10:44:41 +0400117func (ss *realClient) RunCommand(args ...string) error {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400118 cmd := strings.Join(args, " ")
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400119 log.Printf("Running command %s", cmd)
gioe72b54f2024-04-22 10:44:41 +0400120 client, err := ssh.Dial("tcp", ss.addr, ss.sshClientConfig())
giolekva8aa73e82022-07-09 11:34:39 +0400121 if err != nil {
122 return err
123 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400124 defer client.Close()
giolekva8aa73e82022-07-09 11:34:39 +0400125 session, err := client.NewSession()
126 if err != nil {
127 return err
128 }
129 defer session.Close()
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400130 session.Stdout = os.Stdout
131 session.Stderr = os.Stderr
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400132 return session.Run(cmd)
giolekva8aa73e82022-07-09 11:34:39 +0400133}
134
gioe72b54f2024-04-22 10:44:41 +0400135func (ss *realClient) AddRepository(name string) error {
giolekva8aa73e82022-07-09 11:34:39 +0400136 log.Printf("Adding repository %s", name)
gio0eaf2712024-04-14 13:08:46 +0400137 if err := ss.RunCommand("repo", "info", name); err == nil {
138 return ErrorAlreadyExists
139 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400140 return ss.RunCommand("repo", "create", name)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400141}
142
gioe72b54f2024-04-22 10:44:41 +0400143func (ss *realClient) AddReadWriteCollaborator(repo, user string) error {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400144 log.Printf("Adding read-write collaborator %s %s", repo, user)
145 return ss.RunCommand("repo", "collab", "add", repo, user, "read-write")
146}
147
gioe72b54f2024-04-22 10:44:41 +0400148func (ss *realClient) AddReadOnlyCollaborator(repo, user string) error {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400149 log.Printf("Adding read-only collaborator %s %s", repo, user)
150 return ss.RunCommand("repo", "collab", "add", repo, user, "read-only")
giolekva8aa73e82022-07-09 11:34:39 +0400151}
152
gio0eaf2712024-04-14 13:08:46 +0400153func (ss *realClient) AddWebhook(repo, url string, opts ...string) error {
154 log.Printf("Adding webhook %s %s", repo, url)
155 return ss.RunCommand(append(
156 []string{"repo", "webhook", "create", repo, url},
157 opts...,
158 )...)
159}
160
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400161type Repository struct {
162 *git.Repository
163 Addr RepositoryAddress
164}
165
gioe72b54f2024-04-22 10:44:41 +0400166func (ss *realClient) GetRepo(name string) (RepoIO, error) {
167 r, err := CloneRepository(RepositoryAddress{ss.addr, name}, ss.signer)
168 if err != nil {
169 return nil, err
170 }
171 return NewRepoIO(r, ss.signer)
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400172}
173
174type RepositoryAddress struct {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400175 Addr string
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400176 Name string
177}
178
179func ParseRepositoryAddress(addr string) (RepositoryAddress, error) {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400180 items := regexp.MustCompile(`ssh://(.*)/(.*)`).FindStringSubmatch(addr)
181 if len(items) != 3 {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400182 return RepositoryAddress{}, fmt.Errorf("Invalid address")
183 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400184 return RepositoryAddress{items[1], items[2]}, nil
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400185}
186
187func (r RepositoryAddress) FullAddress() string {
188 return fmt.Sprintf("ssh://%s/%s", r.Addr, r.Name)
189}
190
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400191func CloneRepository(addr RepositoryAddress, signer ssh.Signer) (*Repository, error) {
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400192 fmt.Printf("Cloning repository: %s %s\n", addr.Addr, addr.Name)
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400193 c, err := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
194 URL: addr.FullAddress(),
195 Auth: &gitssh.PublicKeys{
196 User: "git",
197 Signer: signer,
198 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
199 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
200 // TODO(giolekva): verify server public key
201 fmt.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
202 return nil
203 },
204 },
205 },
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400206 RemoteName: "origin",
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400207 ReferenceName: "refs/heads/master",
giof5ffedb2024-06-19 14:14:43 +0400208 SingleBranch: true,
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400209 Depth: 1,
210 InsecureSkipTLS: true,
211 Progress: os.Stdout,
212 })
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400213 if err != nil && !errors.Is(err, transport.ErrEmptyRemoteRepository) {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400214 return nil, err
215 }
giof5ffedb2024-06-19 14:14:43 +0400216 wt, err := c.Worktree()
217 if err != nil {
218 return nil, err
219 }
220 sb, err := wt.Submodules()
221 if err != nil {
222 return nil, err
223 }
224 if err := sb.Init(); err != nil {
225 return nil, err
226 }
227 if err := sb.Update(&git.SubmoduleUpdateOptions{
228 Depth: 1,
229 }); err != nil {
230 return nil, err
231 }
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400232 return &Repository{
233 Repository: c,
234 Addr: addr,
235 }, nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400236}
237
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400238// TODO(giolekva): dead code
gioe72b54f2024-04-22 10:44:41 +0400239func (ss *realClient) authSSH() gitssh.AuthMethod {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400240 a, err := gitssh.NewPublicKeys("git", ss.pemBytes, "")
giolekva8aa73e82022-07-09 11:34:39 +0400241 if err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400242 panic(err)
giolekva8aa73e82022-07-09 11:34:39 +0400243 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400244 a.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
245 // TODO(giolekva): verify server public key
246 ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
247 return nil
giolekva8aa73e82022-07-09 11:34:39 +0400248 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400249 return a
250 // return &gitssh.PublicKeys{
251 // User: "git",
252 // Signer: ss.Signer,
253 // HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
254 // HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
255 // // TODO(giolekva): verify server public key
256 // ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
257 // return nil
258 // },
259 // },
260 // }
giolekva8aa73e82022-07-09 11:34:39 +0400261}
262
gioe72b54f2024-04-22 10:44:41 +0400263func (ss *realClient) authGit() *gitssh.PublicKeys {
giolekva8aa73e82022-07-09 11:34:39 +0400264 return &gitssh.PublicKeys{
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400265 User: "git",
gioe72b54f2024-04-22 10:44:41 +0400266 Signer: ss.signer,
giolekva8aa73e82022-07-09 11:34:39 +0400267 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
268 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
269 // TODO(giolekva): verify server public key
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400270 ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
giolekva8aa73e82022-07-09 11:34:39 +0400271 return nil
272 },
273 },
274 }
275}
276
gioe72b54f2024-04-22 10:44:41 +0400277func (ss *realClient) GetPublicKeys() ([]string, error) {
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400278 var ret []string
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400279 config := &ssh.ClientConfig{
280 Auth: []ssh.AuthMethod{
gioe72b54f2024-04-22 10:44:41 +0400281 ssh.PublicKeys(ss.signer),
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400282 },
283 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400284 ret = append(ret, string(ssh.MarshalAuthorizedKey(key)))
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400285 return nil
286 },
287 }
gioe72b54f2024-04-22 10:44:41 +0400288 client, err := ssh.Dial("tcp", ss.addr, config)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400289 if err != nil {
290 return nil, err
291 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400292 defer client.Close()
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400293 return ret, nil
294}
295
gioe72b54f2024-04-22 10:44:41 +0400296func (ss *realClient) sshClientConfig() *ssh.ClientConfig {
giolekva8aa73e82022-07-09 11:34:39 +0400297 return &ssh.ClientConfig{
298 Auth: []ssh.AuthMethod{
gioe72b54f2024-04-22 10:44:41 +0400299 ssh.PublicKeys(ss.signer),
giolekva8aa73e82022-07-09 11:34:39 +0400300 },
301 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
302 // TODO(giolekva): verify server public key
303 // fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400304 fmt.Printf("%s %s %s", hostname, remote, ssh.MarshalAuthorizedKey(key))
giolekva8aa73e82022-07-09 11:34:39 +0400305 return nil
306 },
307 }
308}
309
gioe72b54f2024-04-22 10:44:41 +0400310func (ss *realClient) GetRepoAddress(name string) string {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400311 return fmt.Sprintf("%s/%s", ss.addressGit(), name)
312}
313
gioe72b54f2024-04-22 10:44:41 +0400314func (ss *realClient) addressGit() string {
315 return fmt.Sprintf("ssh://%s", ss.addr)
giolekva8aa73e82022-07-09 11:34:39 +0400316}