blob: 6aa487c517dbf09b412fddf8b611f89527e36623 [file] [log] [blame]
gio0eaf2712024-04-14 13:08:46 +04001package main
2
3import (
4 "encoding/json"
5 "flag"
6 "fmt"
gio5e4d1a72024-10-09 15:25:29 +04007 "log"
gio0eaf2712024-04-14 13:08:46 +04008 "net"
9 "os"
gio37fba252025-07-03 14:02:04 +040010 "os/signal"
gio24d6e9a2025-06-24 15:02:29 +040011 "path/filepath"
gioa421b062025-04-21 09:45:04 +040012 "strings"
gio37fba252025-07-03 14:02:04 +040013 "syscall"
gio5be6f782025-07-07 17:42:00 +040014 "time"
gio0eaf2712024-04-14 13:08:46 +040015
16 "golang.org/x/crypto/ssh"
17
18 "github.com/go-git/go-billy/v5/osfs"
19 "github.com/go-git/go-git/v5"
gio2b1157a2024-10-24 08:45:07 +040020 "github.com/go-git/go-git/v5/plumbing"
gio24d6e9a2025-06-24 15:02:29 +040021 "github.com/go-git/go-git/v5/plumbing/cache"
gio0eaf2712024-04-14 13:08:46 +040022 gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
gio24d6e9a2025-06-24 15:02:29 +040023 "github.com/go-git/go-git/v5/storage/filesystem"
gio0eaf2712024-04-14 13:08:46 +040024)
25
26var port = flag.Int("port", 3000, "Port to listen on")
gio266c04f2024-07-03 14:18:45 +040027var appId = flag.String("app-id", "", "Application ID")
giob87415c2025-05-08 22:32:11 +040028var service = flag.String("service", "", "Service name")
gioe65d9a92025-06-19 09:02:32 +040029var agentMode = flag.Bool("agent-mode", false, "Sketch agent mode")
gio0eaf2712024-04-14 13:08:46 +040030var repoAddr = flag.String("repo-addr", "", "Git repository address")
gio2b1157a2024-10-24 08:45:07 +040031var branch = flag.String("branch", "", "Name of the branch to process")
giofc441e32024-11-11 16:26:14 +040032var rootDir = flag.String("root-dir", "/", "Path to the app code")
gio0eaf2712024-04-14 13:08:46 +040033var sshKey = flag.String("ssh-key", "", "Private SSH key to access Git repository")
34var appDir = flag.String("app-dir", "", "Path to store application repository locally")
35var runCfg = flag.String("run-cfg", "", "Run configuration")
gioa60f0de2024-07-08 10:49:48 +040036var managerAddr = flag.String("manager-addr", "", "Address of the manager")
gio0eaf2712024-04-14 13:08:46 +040037
38type Command struct {
39 Bin string `json:"bin"`
40 Args []string `json:"args"`
gio1364e432024-06-29 11:39:18 +040041 Env []string `json:"env"`
gio0eaf2712024-04-14 13:08:46 +040042}
43
gio9635ccb2025-05-22 08:33:38 +040044type Commit struct {
45 Hash string `json:"hash"`
46 Message string `json:"message"`
47}
48
49func CloneRepositoryBranch(addr, branch, rootDir string, signer ssh.Signer, path string) (*Commit, error) {
gio2b1157a2024-10-24 08:45:07 +040050 ref := fmt.Sprintf("refs/heads/%s", branch)
giofc441e32024-11-11 16:26:14 +040051 opts := &git.CloneOptions{
52 URL: addr,
53 RemoteName: "origin",
54 ReferenceName: plumbing.ReferenceName(ref),
55 SingleBranch: true,
56 Depth: 1,
57 InsecureSkipTLS: true,
58 Progress: os.Stdout,
59 }
60 if signer != nil {
61 opts.Auth = &gitssh.PublicKeys{
gio0eaf2712024-04-14 13:08:46 +040062 User: "git",
63 Signer: signer,
64 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
65 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
66 // TODO(giolekva): verify server public key
67 fmt.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
68 return nil
69 },
70 },
giofc441e32024-11-11 16:26:14 +040071 }
72 }
gio24d6e9a2025-06-24 15:02:29 +040073 repo, err := git.Clone(
74 filesystem.NewStorage(
75 osfs.New(filepath.Join(path, ".git"), osfs.WithBoundOS()),
76 cache.NewObjectLRUDefault(),
77 ),
78 osfs.New(path, osfs.WithBoundOS()),
79 opts,
80 )
giof5ffedb2024-06-19 14:14:43 +040081 if err != nil {
gio9635ccb2025-05-22 08:33:38 +040082 return nil, err
giof5ffedb2024-06-19 14:14:43 +040083 }
gio9635ccb2025-05-22 08:33:38 +040084 head, err := repo.Head()
giof5ffedb2024-06-19 14:14:43 +040085 if err != nil {
gio9635ccb2025-05-22 08:33:38 +040086 return nil, err
giof5ffedb2024-06-19 14:14:43 +040087 }
gio9635ccb2025-05-22 08:33:38 +040088 commit, err := repo.CommitObject(head.Hash())
89 if err != nil {
90 return nil, err
91 }
92 return &Commit{
93 Hash: head.Hash().String(),
94 Message: commit.Message,
95 }, nil
gio0eaf2712024-04-14 13:08:46 +040096}
97
gio5be6f782025-07-07 17:42:00 +040098func readRunConfiguration(p string) []Command {
99 r, err := os.Open(p)
100 if err != nil {
101 panic(err)
102 }
103 defer r.Close()
104 var cmds []Command
105 if err := json.NewDecoder(r).Decode(&cmds); err != nil {
106 log.Fatal(err)
107 }
108 return cmds
109}
110
gio0eaf2712024-04-14 13:08:46 +0400111func main() {
112 flag.Parse()
113 self, ok := os.LookupEnv("SELF_IP")
114 if !ok {
115 panic("no SELF_IP")
116 }
gio5155c1a2025-05-21 15:36:21 +0400117 id, ok := os.LookupEnv("SELF_ID")
118 if !ok {
119 panic("no SELF_ID")
120 }
giofc441e32024-11-11 16:26:14 +0400121 var signer ssh.Signer
gioa421b062025-04-21 09:45:04 +0400122 // TODO(gio): revisit this logic
123 if *sshKey != "" && !(strings.HasPrefix(*repoAddr, "http://") || strings.HasPrefix(*repoAddr, "https://")) {
giofc441e32024-11-11 16:26:14 +0400124 key, err := os.ReadFile(*sshKey)
125 if err != nil {
126 panic(err)
127 }
128 signer, err = ssh.ParsePrivateKey(key)
129 if err != nil {
130 panic(err)
131 }
gio0eaf2712024-04-14 13:08:46 +0400132 }
gioe65d9a92025-06-19 09:02:32 +0400133 if !*agentMode {
134 if err := os.Mkdir(*appDir, os.ModePerm); err != nil {
135 panic(err)
136 }
gio0eaf2712024-04-14 13:08:46 +0400137 }
gio5be6f782025-07-07 17:42:00 +0400138 cmds := readRunConfiguration(*runCfg)
gioe65d9a92025-06-19 09:02:32 +0400139 s := NewServer(*agentMode, *port, *appId, *service, id, *repoAddr, *branch, *rootDir, signer, *appDir, cmds, self, *managerAddr)
gio37fba252025-07-03 14:02:04 +0400140 go func() {
141 if err := s.Start(); err != nil {
142 log.Fatal(err)
143 } else {
144 log.Println("Done")
145 os.Exit(0)
146 }
147 }()
gio5be6f782025-07-07 17:42:00 +0400148 go func() {
149 for {
150 time.Sleep(30 * time.Second)
151 newCmds := readRunConfiguration(*runCfg)
152 if commandsChanged(cmds, newCmds) {
153 s.UpdateRunCommands(newCmds)
154 cmds = newCmds
155 }
156 }
157 }()
gio37fba252025-07-03 14:02:04 +0400158 sigChan := make(chan os.Signal, 1)
159 signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
160 <-sigChan
161 s.Stop()
gio0eaf2712024-04-14 13:08:46 +0400162}
gio5be6f782025-07-07 17:42:00 +0400163
164func commandsChanged(a, b []Command) bool {
165 if len(a) != len(b) {
166 return true
167 }
168 for i, x := range a {
169 y := b[i]
170 if x.Bin != y.Bin {
171 return true
172 }
173 if len(x.Args) != len(y.Args) {
174 return true
175 }
176 for j, k := range x.Args {
177 l := y.Args[j]
178 if k != l {
179 return true
180 }
181 }
182 if !*agentMode {
183 if len(x.Env) != len(y.Env) {
184 return true
185 }
186 for j, k := range x.Env {
187 l := y.Env[j]
188 if k != l {
189 return true
190 }
191 }
192 }
193 }
194 return false
195}