| package main |
| |
| import ( |
| "encoding/json" |
| "flag" |
| "fmt" |
| "log" |
| "net" |
| "os" |
| "os/signal" |
| "path/filepath" |
| "strings" |
| "syscall" |
| "time" |
| |
| "golang.org/x/crypto/ssh" |
| |
| "github.com/go-git/go-billy/v5/osfs" |
| "github.com/go-git/go-git/v5" |
| "github.com/go-git/go-git/v5/plumbing" |
| "github.com/go-git/go-git/v5/plumbing/cache" |
| gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh" |
| "github.com/go-git/go-git/v5/storage/filesystem" |
| ) |
| |
| var port = flag.Int("port", 3000, "Port to listen on") |
| var appId = flag.String("app-id", "", "Application ID") |
| var service = flag.String("service", "", "Service name") |
| var agentMode = flag.Bool("agent-mode", false, "Sketch agent mode") |
| var repoAddr = flag.String("repo-addr", "", "Git repository address") |
| var branch = flag.String("branch", "", "Name of the branch to process") |
| var rootDir = flag.String("root-dir", "/", "Path to the app code") |
| var sshKey = flag.String("ssh-key", "", "Private SSH key to access Git repository") |
| var appDir = flag.String("app-dir", "", "Path to store application repository locally") |
| var runCfg = flag.String("run-cfg", "", "Run configuration") |
| var managerAddr = flag.String("manager-addr", "", "Address of the manager") |
| |
| type Command struct { |
| Bin string `json:"bin"` |
| Args []string `json:"args"` |
| Env []string `json:"env"` |
| } |
| |
| type Commit struct { |
| Hash string `json:"hash"` |
| Message string `json:"message"` |
| } |
| |
| func CloneRepositoryBranch(addr, branch, rootDir string, signer ssh.Signer, path string) (*Commit, error) { |
| ref := fmt.Sprintf("refs/heads/%s", branch) |
| opts := &git.CloneOptions{ |
| URL: addr, |
| RemoteName: "origin", |
| ReferenceName: plumbing.ReferenceName(ref), |
| SingleBranch: true, |
| Depth: 1, |
| InsecureSkipTLS: true, |
| Progress: os.Stdout, |
| } |
| if signer != nil { |
| opts.Auth = &gitssh.PublicKeys{ |
| User: "git", |
| Signer: signer, |
| HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{ |
| HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { |
| // TODO(giolekva): verify server public key |
| fmt.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key)) |
| return nil |
| }, |
| }, |
| } |
| } |
| repo, err := git.Clone( |
| filesystem.NewStorage( |
| osfs.New(filepath.Join(path, ".git"), osfs.WithBoundOS()), |
| cache.NewObjectLRUDefault(), |
| ), |
| osfs.New(path, osfs.WithBoundOS()), |
| opts, |
| ) |
| if err != nil { |
| return nil, err |
| } |
| head, err := repo.Head() |
| if err != nil { |
| return nil, err |
| } |
| commit, err := repo.CommitObject(head.Hash()) |
| if err != nil { |
| return nil, err |
| } |
| return &Commit{ |
| Hash: head.Hash().String(), |
| Message: commit.Message, |
| }, nil |
| } |
| |
| func readRunConfiguration(p string) []Command { |
| r, err := os.Open(p) |
| if err != nil { |
| panic(err) |
| } |
| defer r.Close() |
| var cmds []Command |
| if err := json.NewDecoder(r).Decode(&cmds); err != nil { |
| log.Fatal(err) |
| } |
| return cmds |
| } |
| |
| func main() { |
| flag.Parse() |
| self, ok := os.LookupEnv("SELF_IP") |
| if !ok { |
| panic("no SELF_IP") |
| } |
| id, ok := os.LookupEnv("SELF_ID") |
| if !ok { |
| panic("no SELF_ID") |
| } |
| var signer ssh.Signer |
| // TODO(gio): revisit this logic |
| if *sshKey != "" && !(strings.HasPrefix(*repoAddr, "http://") || strings.HasPrefix(*repoAddr, "https://")) { |
| key, err := os.ReadFile(*sshKey) |
| if err != nil { |
| panic(err) |
| } |
| signer, err = ssh.ParsePrivateKey(key) |
| if err != nil { |
| panic(err) |
| } |
| } |
| if !*agentMode { |
| if err := os.Mkdir(*appDir, os.ModePerm); err != nil { |
| panic(err) |
| } |
| } |
| cmds := readRunConfiguration(*runCfg) |
| s := NewServer(*agentMode, *port, *appId, *service, id, *repoAddr, *branch, *rootDir, signer, *appDir, cmds, self, *managerAddr) |
| go func() { |
| if err := s.Start(); err != nil { |
| log.Fatal(err) |
| } else { |
| log.Println("Done") |
| os.Exit(0) |
| } |
| }() |
| go func() { |
| for { |
| time.Sleep(30 * time.Second) |
| newCmds := readRunConfiguration(*runCfg) |
| if commandsChanged(cmds, newCmds) { |
| s.UpdateRunCommands(newCmds) |
| cmds = newCmds |
| } |
| } |
| }() |
| sigChan := make(chan os.Signal, 1) |
| signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) |
| <-sigChan |
| s.Stop() |
| } |
| |
| func commandsChanged(a, b []Command) bool { |
| if len(a) != len(b) { |
| return true |
| } |
| for i, x := range a { |
| y := b[i] |
| if x.Bin != y.Bin { |
| return true |
| } |
| if len(x.Args) != len(y.Args) { |
| return true |
| } |
| for j, k := range x.Args { |
| l := y.Args[j] |
| if k != l { |
| return true |
| } |
| } |
| if !*agentMode { |
| if len(x.Env) != len(y.Env) { |
| return true |
| } |
| for j, k := range x.Env { |
| l := y.Env[j] |
| if k != l { |
| return true |
| } |
| } |
| } |
| } |
| return false |
| } |