11980: debian9 as a new target
[arvados-dev.git] / arvados-version-server / arvados-version-server.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package main
6
7 import (
8         "encoding/json"
9         "flag"
10         "fmt"
11         "git.curoverse.com/arvados.git/sdk/go/config"
12         "io"
13         "io/ioutil"
14         "log"
15         "net"
16         "net/http"
17         "os"
18         "os/exec"
19         "os/signal"
20         "regexp"
21         "strings"
22         "syscall"
23         "time"
24 )
25
26 var listener net.Listener
27
28 type report struct {
29         Type string
30         Msg  string
31 }
32
33 type bundle struct {
34         sourceDir     string
35         name          string
36         packageType   string
37         versionType   string
38         versionPrefix string
39 }
40
41 type result struct {
42         RequestHash string
43         GitHash     string
44         Versions    map[string]map[string]string
45         Cached      bool
46         Elapsed     string
47 }
48
49 type about struct {
50         Name    string
51         Version string
52         URL     string
53 }
54
55 type help struct {
56         Usage string
57 }
58
59 // Config structure
60 type Config struct {
61         DirPath           string
62         CacheDirPath      string
63         GitExecutablePath string
64         ListenPort        string
65
66         Packages []bundle
67 }
68
69 var theConfig Config
70
71 const defaultConfigPath = "/etc/arvados/version-server/version-server.yml"
72
73 func loadPackages() (packages []bundle) {
74         packages = []bundle{
75                 {
76                         sourceDir:     ".",
77                         name:          "arvados-src",
78                         packageType:   "distribution",
79                         versionType:   "git",
80                         versionPrefix: "0.1",
81                 },
82                 {
83                         sourceDir:     "apps/workbench",
84                         name:          "arvados-workbench",
85                         packageType:   "distribution",
86                         versionType:   "git",
87                         versionPrefix: "0.1",
88                 },
89                 {
90                         sourceDir:     "sdk/cwl",
91                         name:          "python-arvados-cwl-runner",
92                         packageType:   "distribution",
93                         versionType:   "python",
94                         versionPrefix: "1.0",
95                 },
96                 {
97                         sourceDir:     "sdk/cwl",
98                         name:          "arvados-cwl-runner",
99                         packageType:   "python",
100                         versionType:   "python",
101                         versionPrefix: "1.0",
102                 },
103                 {
104                         sourceDir:     "sdk/cwl",
105                         name:          "arvados/jobs",
106                         packageType:   "docker",
107                         versionType:   "docker",
108                         versionPrefix: "",
109                 },
110                 {
111                         sourceDir:     "sdk/go/crunchrunner",
112                         name:          "crunchrunner",
113                         packageType:   "distribution",
114                         versionType:   "go",
115                         versionPrefix: "0.1",
116                 },
117                 {
118                         sourceDir:     "sdk/pam",
119                         name:          "libpam-arvados",
120                         packageType:   "distribution",
121                         versionType:   "python",
122                         versionPrefix: "0.1",
123                 },
124                 {
125                         sourceDir:     "sdk/pam",
126                         name:          "arvados-pam",
127                         packageType:   "python",
128                         versionType:   "python",
129                         versionPrefix: "0.1",
130                 },
131                 {
132                         sourceDir:     "sdk/python",
133                         name:          "python-arvados-python-client",
134                         packageType:   "distribution",
135                         versionType:   "python",
136                         versionPrefix: "0.1",
137                 },
138                 {
139                         sourceDir:     "sdk/python",
140                         name:          "arvados-python-client",
141                         packageType:   "python",
142                         versionType:   "python",
143                         versionPrefix: "0.1",
144                 },
145                 {
146                         sourceDir:     "services/api",
147                         name:          "arvados-api-server",
148                         packageType:   "distribution",
149                         versionType:   "git",
150                         versionPrefix: "0.1",
151                 },
152                 {
153                         sourceDir:     "services/arv-git-httpd",
154                         name:          "arvados-git-httpd",
155                         packageType:   "distribution",
156                         versionType:   "go",
157                         versionPrefix: "0.1",
158                 },
159                 {
160                         sourceDir:     "services/crunch-dispatch-local",
161                         name:          "crunch-dispatch-local",
162                         packageType:   "distribution",
163                         versionType:   "go",
164                         versionPrefix: "0.1",
165                 },
166                 {
167                         sourceDir:     "services/crunch-dispatch-slurm",
168                         name:          "crunch-dispatch-slurm",
169                         packageType:   "distribution",
170                         versionType:   "go",
171                         versionPrefix: "0.1",
172                 },
173                 {
174                         sourceDir:     "services/crunch-run",
175                         name:          "crunch-run",
176                         packageType:   "distribution",
177                         versionType:   "go",
178                         versionPrefix: "0.1",
179                 },
180                 {
181                         sourceDir:     "services/crunchstat",
182                         name:          "crunchstat",
183                         packageType:   "distribution",
184                         versionType:   "git",
185                         versionPrefix: "0.1",
186                 },
187                 {
188                         sourceDir:     "services/dockercleaner",
189                         name:          "arvados-docker-cleaner",
190                         packageType:   "distribution",
191                         versionType:   "python",
192                         versionPrefix: "0.1",
193                 },
194                 {
195                         sourceDir:     "services/fuse",
196                         name:          "python-arvados-fuse",
197                         packageType:   "distribution",
198                         versionType:   "python",
199                         versionPrefix: "0.1",
200                 },
201                 {
202                         sourceDir:     "services/fuse",
203                         name:          "arvados_fuse",
204                         packageType:   "python",
205                         versionType:   "python",
206                         versionPrefix: "0.1",
207                 },
208                 {
209                         sourceDir:     "services/keep-balance",
210                         name:          "keep-balance",
211                         packageType:   "distribution",
212                         versionType:   "go",
213                         versionPrefix: "0.1",
214                 },
215                 {
216                         sourceDir:     "services/keepproxy",
217                         name:          "keepproxy",
218                         packageType:   "distribution",
219                         versionType:   "go",
220                         versionPrefix: "0.1",
221                 },
222                 {
223                         sourceDir:     "services/keepstore",
224                         name:          "keepstore",
225                         packageType:   "distribution",
226                         versionType:   "go",
227                         versionPrefix: "0.1",
228                 },
229                 {
230                         sourceDir:     "services/keep-web",
231                         name:          "keep-web",
232                         packageType:   "distribution",
233                         versionType:   "go",
234                         versionPrefix: "0.1",
235                 },
236                 {
237                         sourceDir:     "services/nodemanager",
238                         name:          "arvados-node-manager",
239                         packageType:   "distribution",
240                         versionType:   "python",
241                         versionPrefix: "0.1",
242                 },
243                 {
244                         sourceDir:     "services/nodemanager",
245                         name:          "arvados-node-manager",
246                         packageType:   "python",
247                         versionType:   "python",
248                         versionPrefix: "0.1",
249                 },
250                 {
251                         sourceDir:     "services/ws",
252                         name:          "arvados-ws",
253                         packageType:   "distribution",
254                         versionType:   "go",
255                         versionPrefix: "0.1",
256                 },
257                 {
258                         sourceDir:     "tools/crunchstat-summary",
259                         name:          "crunchstat-summary",
260                         packageType:   "distribution",
261                         versionType:   "go",
262                         versionPrefix: "0.1",
263                 },
264                 {
265                         sourceDir:     "tools/keep-block-check",
266                         name:          "keep-block-check",
267                         packageType:   "distribution",
268                         versionType:   "go",
269                         versionPrefix: "0.1",
270                 },
271                 {
272                         sourceDir:     "tools/keep-exercise",
273                         name:          "keep-exercise",
274                         packageType:   "distribution",
275                         versionType:   "go",
276                         versionPrefix: "0.1",
277                 },
278                 {
279                         sourceDir:     "tools/keep-rsync",
280                         name:          "keep-rsync",
281                         packageType:   "distribution",
282                         versionType:   "go",
283                         versionPrefix: "0.1",
284                 },
285                 {
286                         sourceDir:     "sdk/ruby",
287                         name:          "arvados",
288                         packageType:   "gem",
289                         versionType:   "ruby",
290                         versionPrefix: "0.1",
291                 },
292                 {
293                         sourceDir:     "sdk/cli",
294                         name:          "arvados-cli",
295                         packageType:   "gem",
296                         versionType:   "ruby",
297                         versionPrefix: "0.1",
298                 },
299                 {
300                         sourceDir:     "services/login-sync",
301                         name:          "arvados-login-sync",
302                         packageType:   "gem",
303                         versionType:   "ruby",
304                         versionPrefix: "0.1",
305                 },
306         }
307         return
308 }
309
310 func lookupInCache(hash string) (result, error) {
311         statData, err := os.Stat(theConfig.CacheDirPath)
312         if os.IsNotExist(err) {
313                 err = os.MkdirAll(theConfig.CacheDirPath, 0700)
314                 if err != nil {
315                         logError([]string{"Error creating directory", theConfig.CacheDirPath, ":", err.Error()})
316                 }
317         } else {
318                 if !statData.IsDir() {
319                         logError([]string{"The path", theConfig.CacheDirPath, "is not a directory"})
320                         return result{}, fmt.Errorf("The path %s is not a directory", theConfig.CacheDirPath)
321                 }
322         }
323         file, e := ioutil.ReadFile(theConfig.CacheDirPath + "/" + hash)
324         if e != nil {
325                 return result{}, fmt.Errorf("File error: %v\n", e)
326         }
327         var m result
328         err = json.Unmarshal(file, &m)
329         return m, err
330 }
331
332 func writeToCache(hash string, data result) (err error) {
333         statData, err := os.Stat(theConfig.CacheDirPath)
334         if os.IsNotExist(err) {
335                 err = os.MkdirAll(theConfig.CacheDirPath, 0700)
336                 if err != nil {
337                         logError([]string{"Error creating directory", theConfig.CacheDirPath, ":", err.Error()})
338                 }
339         } else {
340                 if !statData.IsDir() {
341                         logError([]string{"The path", theConfig.CacheDirPath, "is not a directory"})
342                         return fmt.Errorf("The path %s is not a directory", theConfig.CacheDirPath)
343                 }
344         }
345
346         jsonData, err := json.Marshal(data)
347         if err != nil {
348                 return
349         }
350         err = ioutil.WriteFile(theConfig.CacheDirPath+"/"+hash, jsonData, 0644)
351         return
352 }
353
354 func prepareGitPath(hash string) error {
355         statData, err := os.Stat(theConfig.DirPath)
356         if os.IsNotExist(err) {
357                 err = os.MkdirAll(theConfig.DirPath, 0700)
358                 if err != nil {
359                         logError([]string{"Error creating directory", theConfig.DirPath, ":", err.Error()})
360                         return fmt.Errorf("Error creating directory %s", theConfig.DirPath)
361                 }
362                 cmdArgs := []string{"clone", "https://github.com/curoverse/arvados.git", theConfig.DirPath}
363                 if _, err = exec.Command(theConfig.GitExecutablePath, cmdArgs...).Output(); err != nil {
364                         logError([]string{"There was an error running the command ", theConfig.GitExecutablePath, strings.Join(cmdArgs, " "), err.Error()})
365                         return fmt.Errorf("There was an error cloning the repository")
366                 }
367         } else {
368                 if !statData.IsDir() {
369                         logError([]string{"The path", theConfig.DirPath, "is not a directory"})
370                         return fmt.Errorf("The path %s is not a directory", theConfig.DirPath)
371                 }
372         }
373         return nil
374 }
375
376 func prepareGitCheckout(hash string) (string, error) {
377         err := prepareGitPath(hash)
378         if err != nil {
379                 return "", err
380         }
381         err = os.Chdir(theConfig.DirPath)
382         if err != nil {
383                 logError([]string{"Error changing directory to", theConfig.DirPath})
384                 return "", fmt.Errorf("Error changing directory to %s", theConfig.DirPath)
385         }
386         cmdArgs := []string{"fetch", "--all"}
387         if _, err := exec.Command(theConfig.GitExecutablePath, cmdArgs...).Output(); err != nil {
388                 logError([]string{"There was an error running the command ", theConfig.GitExecutablePath, strings.Join(cmdArgs, " "), err.Error()})
389                 return "", fmt.Errorf("There was an error fetching all remotes")
390         }
391         if hash == "" {
392                 hash = "master"
393         }
394         cmdArgs = []string{"checkout", hash}
395         if _, err := exec.Command(theConfig.GitExecutablePath, cmdArgs...).Output(); err != nil {
396                 logError([]string{"There was an error running the command ", theConfig.GitExecutablePath, strings.Join(cmdArgs, " "), err.Error()})
397                 return "", fmt.Errorf("There was an error checking out the requested revision")
398         }
399         if hash == "master" {
400                 cmdArgs := []string{"reset", "--hard", "origin/master"}
401                 if _, err := exec.Command(theConfig.GitExecutablePath, cmdArgs...).Output(); err != nil {
402                         logError([]string{"There was an error running the command ", theConfig.GitExecutablePath, strings.Join(cmdArgs, " "), err.Error()})
403                         return "", fmt.Errorf("There was an error fetching all remotes")
404                 }
405         }
406         return "", nil
407 }
408
409 // Generates the hash for the latest git commit for the current working directory
410 func gitHashFull() (string, error) {
411         cmdArgs := []string{"log", "-n1", "--first-parent", "--max-count=1", "--format=format:%H", "."}
412         cmdOut, err := exec.Command(theConfig.GitExecutablePath, cmdArgs...).Output()
413         if err != nil {
414                 logError([]string{"There was an error running the command ", theConfig.GitExecutablePath, strings.Join(cmdArgs, " "), err.Error()})
415                 return "", fmt.Errorf("There was an error getting the git hash for this revision")
416         }
417         return string(cmdOut), nil
418 }
419
420 // Generates a version number from the git log for the current working directory
421 func versionFromGit(prefix string) (string, error) {
422         gitTs, err := getGitTs()
423         if err != nil {
424                 return "", err
425         }
426         cmdArgs := []string{"log", "-n1", "--first-parent", "--max-count=1", "--format=format:%h", "."}
427         gitHash, err := exec.Command(theConfig.GitExecutablePath, cmdArgs...).Output()
428         if err != nil {
429                 logError([]string{"There was an error running the command ", theConfig.GitExecutablePath, strings.Join(cmdArgs, " "), err.Error()})
430                 return "", fmt.Errorf("There was an error getting the git hash for this revision")
431         }
432         cmdName := "/bin/date"
433         cmdArgs = []string{"-ud", "@" + string(gitTs), "+%Y%m%d%H%M%S"}
434         date, err := exec.Command(cmdName, cmdArgs...).Output()
435         if err != nil {
436                 logError([]string{"There was an error running the command ", cmdName, strings.Join(cmdArgs, " "), err.Error()})
437                 return "", fmt.Errorf("There was an error converting the datestamp for this revision")
438         }
439
440         return fmt.Sprintf("%s.%s.%s", strings.TrimSpace(prefix), strings.TrimSpace(string(date)), strings.TrimSpace(string(gitHash))), nil
441 }
442
443 // Generates a python package version number from the git log for the current working directory
444 func rubyVersionFromGit(prefix string) (string, error) {
445         gitTs, err := getGitTs()
446         if err != nil {
447                 return "", err
448         }
449         cmdName := "/bin/date"
450         cmdArgs := []string{"-ud", "@" + string(gitTs), "+%Y%m%d%H%M%S"}
451         date, err := exec.Command(cmdName, cmdArgs...).Output()
452         if err != nil {
453                 logError([]string{"There was an error running the command ", cmdName, strings.Join(cmdArgs, " "), err.Error()})
454                 return "", fmt.Errorf("There was an error converting the datestamp for this revision")
455         }
456
457         return fmt.Sprintf("%s.%s", strings.TrimSpace(prefix), strings.TrimSpace(string(date))), nil
458 }
459
460 // Generates a python package version number from the git log for the current working directory
461 func pythonVersionFromGit(prefix string) (string, error) {
462         rv, err := rubyVersionFromGit(prefix)
463         if err != nil {
464                 return "", err
465         }
466         return rv, nil
467 }
468
469 // Generates a docker image version number from the git log for the current working directory
470 func dockerVersionFromGit() (string, error) {
471         rv, err := gitHashFull()
472         if err != nil {
473                 return "", err
474         }
475         return rv, nil
476 }
477
478 func getGitTs() (gitTs []byte, err error) {
479         cmdArgs := []string{"log", "-n1", "--first-parent", "--max-count=1", "--format=format:%ct", "."}
480         gitTs, err = exec.Command(theConfig.GitExecutablePath, cmdArgs...).Output()
481         if err != nil {
482                 logError([]string{"There was an error running the command ", theConfig.GitExecutablePath, strings.Join(cmdArgs, " "), err.Error()})
483                 return nil, fmt.Errorf("There was an error getting the git hash for this revision")
484         }
485         return
486 }
487
488 // Generates a timestamp from the git log for the current working directory
489 func timestampFromGit() (string, error) {
490         gitTs, err := getGitTs()
491         if err != nil {
492                 return "", err
493         }
494         return fmt.Sprintf("%s", strings.TrimSpace(string(gitTs))), nil
495 }
496
497 func normalizeRequestedHash(hash string) (string, error) {
498         _, err := prepareGitCheckout(hash)
499         if err != nil {
500                 return "", err
501         }
502
503         // Get the git hash for the tree
504         var gitHash string
505         gitHash, err = gitHashFull()
506         if err != nil {
507                 return "", err
508         }
509
510         return gitHash, nil
511 }
512
513 func getPackageVersionsWorker(hash string) (gitHash string, goSDKTimestamp string, goSDKVersionWithoutPrefix string, pythonSDKTimestamp string, err error) {
514         gitHash = ""
515         goSDKTimestamp = ""
516         goSDKVersionWithoutPrefix = ""
517         pythonSDKTimestamp = ""
518
519         _, err = prepareGitCheckout(hash)
520         if err != nil {
521                 return
522         }
523
524         // Get the git hash for the tree
525         gitHash, err = gitHashFull()
526         if err != nil {
527                 return
528         }
529
530         // Get the git timestamp and version string for the sdk/go directory
531         err = os.Chdir(theConfig.DirPath + "/sdk/go")
532         if err != nil {
533                 err = nil
534         } else {
535                 goSDKTimestamp, err = timestampFromGit()
536                 if err != nil {
537                         return
538                 }
539                 goSDKVersionWithoutPrefix, err = versionFromGit("")
540                 if err != nil {
541                         return
542                 }
543         }
544
545         // Get the git timestamp and version string for the sdk/python directory
546         err = os.Chdir(theConfig.DirPath + "/sdk/python")
547         if err != nil {
548                 err = nil
549         } else {
550                 pythonSDKTimestamp, err = timestampFromGit()
551                 if err != nil {
552                         return
553                 }
554         }
555
556         return
557 }
558
559 func pythonSDKVersionCheck(pythonSDKTimestamp string) (err error) {
560         var packageTimestamp string
561         packageTimestamp, err = timestampFromGit()
562         if err != nil {
563                 return
564         }
565
566         if pythonSDKTimestamp > packageTimestamp {
567                 err = os.Chdir(theConfig.DirPath + "/sdk/python")
568                 if err != nil {
569                         return
570                 }
571         }
572         return
573 }
574
575 func getPackageVersions(hash string) (versions map[string]map[string]string, gitHash string, err error) {
576         versions = make(map[string]map[string]string)
577
578         gitHash, goSDKTimestamp, goSDKVersionWithoutPrefix, pythonSDKTimestamp, err := getPackageVersionsWorker(hash)
579         if err != nil {
580                 return nil, "", err
581         }
582
583         for _, p := range theConfig.Packages {
584                 err = os.Chdir(theConfig.DirPath + "/" + p.sourceDir)
585                 if err != nil {
586                         // Skip those packages for which the source directory doesn't exist
587                         // in this revision of the source tree.
588                         err = nil
589                         continue
590                 }
591                 name := p.name
592
593                 var packageVersion string
594
595                 if (p.versionType == "git") || (p.versionType == "go") {
596                         packageVersion, err = versionFromGit(p.versionPrefix)
597                         if err != nil {
598                                 return nil, "", err
599                         }
600                 }
601                 if p.versionType == "go" {
602                         var packageTimestamp string
603                         packageTimestamp, err = timestampFromGit()
604                         if err != nil {
605                                 return nil, "", err
606                         }
607
608                         if goSDKTimestamp > packageTimestamp {
609                                 packageVersion = p.versionPrefix + goSDKVersionWithoutPrefix
610                         }
611                 } else if p.versionType == "python" {
612                         // Not all of our packages that use our python sdk are automatically
613                         // getting rebuilt when sdk/python changes. Yet.
614                         if p.name == "python-arvados-cwl-runner" {
615                                 err = pythonSDKVersionCheck(pythonSDKTimestamp)
616                                 if err != nil {
617                                         return nil, "", err
618                                 }
619                         }
620
621                         packageVersion, err = pythonVersionFromGit(p.versionPrefix)
622                         if err != nil {
623                                 return nil, "", err
624                         }
625                 } else if p.versionType == "ruby" {
626                         packageVersion, err = rubyVersionFromGit(p.versionPrefix)
627                         if err != nil {
628                                 return nil, "", err
629                         }
630                 } else if p.versionType == "docker" {
631                         // the arvados/jobs image version is always the latest of the
632                         // sdk/python and the sdk/cwl version
633                         if p.name == "arvados/jobs" {
634                                 err = pythonSDKVersionCheck(pythonSDKTimestamp)
635                                 if err != nil {
636                                         return nil, "", err
637                                 }
638                         }
639                         packageVersion, err = dockerVersionFromGit()
640                         if err != nil {
641                                 return nil, "", err
642                         }
643                 }
644
645                 if versions[strings.Title(p.packageType)] == nil {
646                         versions[strings.Title(p.packageType)] = make(map[string]string)
647                 }
648                 versions[strings.Title(p.packageType)][name] = packageVersion
649         }
650
651         return
652 }
653
654 func logError(m []string) {
655         log.Printf(string(marshal(report{"Error", strings.Join(m, " ")})))
656 }
657
658 func logNotice(m []string) {
659         log.Printf(string(marshal(report{"Notice", strings.Join(m, " ")})))
660 }
661
662 func marshal(message interface{}) (encoded []byte) {
663         encoded, err := json.Marshal(message)
664         if err != nil {
665                 // do not call logError here because that would create an infinite loop
666                 fmt.Fprintln(os.Stderr, "{\"Error\": \"Unable to marshal message into json:", message, "\"}")
667                 return nil
668         }
669         return
670 }
671
672 func marshalAndWrite(w io.Writer, message interface{}) {
673         b := marshal(message)
674         if b == nil {
675                 errorMessage := "{\n\"Error\": \"Unspecified error\"\n}"
676                 _, err := io.WriteString(w, errorMessage)
677                 if err != nil {
678                         // do not call logError (it calls marshal and that function has already failed at this point)
679                         fmt.Fprintln(os.Stderr, "{\"Error\": \"Unable to write message to client\"}")
680                 }
681         } else {
682                 _, err := w.Write(b)
683                 if err != nil {
684                         logError([]string{"Unable to write message to client:", string(b)})
685                 }
686         }
687 }
688
689 func packageVersionHandler(w http.ResponseWriter, r *http.Request) {
690         start := time.Now()
691         w.Header().Set("Content-Type", "application/json; charset=utf-8")
692
693         var packageVersions map[string]map[string]string
694         var cached bool
695
696         // Sanity check the input RequestHash
697         match, err := regexp.MatchString("^([a-z0-9]+|)$", r.URL.Path[11:])
698         if err != nil {
699                 m := report{"Error", "Error matching RequestHash"}
700                 marshalAndWrite(w, m)
701                 return
702         }
703         if !match {
704                 m := report{"Error", "Invalid RequestHash"}
705                 marshalAndWrite(w, m)
706                 return
707         }
708
709         hash := r.URL.Path[11:]
710
711         // Empty hash or non-standard hash length? Normalize it.
712         if len(hash) != 7 && len(hash) != 40 {
713                 hash, err = normalizeRequestedHash(hash)
714                 if err != nil {
715                         m := report{"Error", err.Error()}
716                         marshalAndWrite(w, m)
717                         return
718                 }
719         }
720
721         var gitHash string
722         rs, err := lookupInCache(hash)
723         if err == nil {
724                 packageVersions = rs.Versions
725                 gitHash = rs.GitHash
726                 cached = true
727         } else {
728                 packageVersions, gitHash, err = getPackageVersions(hash)
729                 if err != nil {
730                         m := report{"Error", err.Error()}
731                         marshalAndWrite(w, m)
732                         return
733                 }
734                 m := result{"", gitHash, packageVersions, true, ""}
735                 err = writeToCache(hash, m)
736                 if err != nil {
737                         logError([]string{"Unable to save entry in cache directory", theConfig.CacheDirPath})
738                 }
739                 cached = false
740         }
741
742         m := result{hash, gitHash, packageVersions, cached, fmt.Sprintf("%v", time.Since(start))}
743         marshalAndWrite(w, m)
744 }
745
746 func aboutHandler(w http.ResponseWriter, r *http.Request) {
747         w.Header().Set("Content-Type", "application/json; charset=utf-8")
748         m := about{"Arvados Version Server", "0.1", "https://arvados.org"}
749         marshalAndWrite(w, m)
750 }
751
752 func helpHandler(w http.ResponseWriter, r *http.Request) {
753         w.Header().Set("Content-Type", "application/json; charset=utf-8")
754         m := help{"GET /v1/commit/ or GET /v1/commit/git-commit or GET /v1/about or GET /v1/help"}
755         marshalAndWrite(w, m)
756 }
757
758 func parseFlags() (configPath *string) {
759
760         flags := flag.NewFlagSet("arvados-version-server", flag.ExitOnError)
761         flags.Usage = func() { usage(flags) }
762
763         configPath = flags.String(
764                 "config",
765                 defaultConfigPath,
766                 "`path` to YAML configuration file")
767
768         // Parse args; omit the first arg which is the command name
769         err := flags.Parse(os.Args[1:])
770         if err != nil {
771                 logError([]string{"Unable to parse command line arguments:", err.Error()})
772                 os.Exit(1)
773         }
774
775         return
776 }
777
778 func main() {
779         err := os.Setenv("TZ", "UTC")
780         if err != nil {
781                 logError([]string{"Error setting environment variable:", err.Error()})
782                 os.Exit(1)
783         }
784
785         configPath := parseFlags()
786
787         err = readConfig(&theConfig, *configPath, defaultConfigPath)
788         if err != nil {
789                 logError([]string{"Unable to start Arvados Version Server:", err.Error()})
790                 os.Exit(1)
791         }
792
793         theConfig.Packages = loadPackages()
794
795         if theConfig.DirPath == "" {
796                 theConfig.DirPath = "/tmp/arvados-version-server-checkout"
797         }
798
799         if theConfig.CacheDirPath == "" {
800                 theConfig.CacheDirPath = "/tmp/arvados-version-server-cache"
801         }
802
803         if theConfig.GitExecutablePath == "" {
804                 theConfig.GitExecutablePath = "/usr/bin/git"
805         }
806
807         if theConfig.ListenPort == "" {
808                 theConfig.ListenPort = "80"
809         }
810
811         http.HandleFunc("/v1/commit/", packageVersionHandler)
812         http.HandleFunc("/v1/about", aboutHandler)
813         http.HandleFunc("/v1/help", helpHandler)
814         http.HandleFunc("/v1", helpHandler)
815         http.HandleFunc("/", helpHandler)
816         logNotice([]string{"Arvados Version Server listening on port", theConfig.ListenPort})
817
818         listener, err = net.Listen("tcp", ":"+theConfig.ListenPort)
819
820         if err != nil {
821                 logError([]string{"Unable to start Arvados Version Server:", err.Error()})
822                 os.Exit(1)
823         }
824
825         // Shut down the server gracefully (by closing the listener)
826         // if SIGTERM is received.
827         term := make(chan os.Signal, 1)
828         go func(sig <-chan os.Signal) {
829                 <-sig
830                 logError([]string{"caught signal"})
831                 _ = listener.Close()
832         }(term)
833         signal.Notify(term, syscall.SIGTERM)
834         signal.Notify(term, syscall.SIGINT)
835
836         // Start serving requests.
837         _ = http.Serve(listener, nil)
838         // http.Serve returns an error when it gets the term or int signal
839
840         logNotice([]string{"Arvados Version Server shutting down"})
841
842 }
843
844 func readConfig(cfg interface{}, path string, defaultConfigPath string) error {
845         err := config.LoadFile(cfg, path)
846         if err != nil && os.IsNotExist(err) && path == defaultConfigPath {
847                 logNotice([]string{"Config not specified. Continue with default configuration."})
848                 err = nil
849         }
850         return err
851 }