[ARVADOS-ORG] updated: 35cd8f0f90daadc61b2bfe7bea0fe73b078c40da
Git user
git at public.curoverse.com
Mon May 15 17:45:01 EDT 2017
Summary of changes:
discards 484a4027dabcad4e17acddfedd5ecf5a7fa45d57 (commit)
via 35cd8f0f90daadc61b2bfe7bea0fe73b078c40da (commit)
This update added new revisions after undoing existing revisions. That is
to say, the old revision is not a strict subset of the new revision. This
situation occurs when you --force push a change and generate a repository
containing something like this:
* -- * -- B -- O -- O -- O (484a4027dabcad4e17acddfedd5ecf5a7fa45d57)
\
N -- N -- N (35cd8f0f90daadc61b2bfe7bea0fe73b078c40da)
When this happens we assume that you've already had alert emails for all
of the O revisions, and so we here report only the revisions in the N
branch from the common base, B.
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
commit 35cd8f0f90daadc61b2bfe7bea0fe73b078c40da
Author: Ward Vandewege <ward at curoverse.com>
Date: Mon May 15 17:38:22 2017 -0400
Initial commit.
refs #11406
Arvados-DCO-1.1-Signed-off-by: Ward Vandewege <ward at curoverse.com>
diff --git a/registry/cmd/arv-registry/.gitignore b/registry/cmd/arv-registry/.gitignore
new file mode 100644
index 0000000..6d09ab8
--- /dev/null
+++ b/registry/cmd/arv-registry/.gitignore
@@ -0,0 +1 @@
+arv-registry
diff --git a/registry/cmd/arv-registry/arv-registry.go b/registry/cmd/arv-registry/arv-registry.go
new file mode 100644
index 0000000..f360525
--- /dev/null
+++ b/registry/cmd/arv-registry/arv-registry.go
@@ -0,0 +1,155 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package main
+
+import (
+ "bufio"
+ "flag"
+ "fmt"
+ "git.curoverse.com/arvados-org.git/registry"
+ "os"
+)
+
+var theCluster registry.ClusterInfo
+var clusterID string
+var registryURL string
+var checkFlag bool
+
+func parseFlags() (config registry.ClusterInfo) {
+
+ flags := flag.NewFlagSet("arv-registry", flag.ExitOnError)
+ flags.Usage = func() { usage(flags) }
+
+ flags.BoolVar(&checkFlag, "check", false, "Instead of registering, check if a cluster identifier is available.")
+
+ flags.StringVar(&clusterID,
+ "clusterID",
+ "",
+ "(required) the ClusterID to verify or register. Format: 5 characters, all lowercase. Position 1: a character from the set a-w or 0-9. Positions 2-5: a character from the set a-z or 0-9")
+
+ flags.StringVar(&config.Name,
+ "name",
+ "",
+ "(required) your name")
+
+ flags.StringVar(&config.Email,
+ "email",
+ "",
+ "(required) your e-mail address")
+
+ flags.StringVar(&config.Organization,
+ "organization",
+ "",
+ "(required) your organization")
+
+ flags.StringVar(&config.ClientSecret,
+ "clientSecret",
+ "",
+ "(required) a client secret, minimum 16 characters long")
+
+ flags.StringVar(®istryURL,
+ "registryURL",
+ "https://registry.arvadosapi.com",
+ "(optional) an alternative Arvados Registry URL")
+
+ // Parse args; omit the first arg which is the command name
+ err := flags.Parse(os.Args[1:])
+ if err != nil {
+ flags.Usage()
+ os.Exit(1)
+ }
+ if clusterID == "" {
+ flags.Usage()
+ os.Exit(1)
+ }
+ return
+}
+
+func usage(fs *flag.FlagSet) {
+ fmt.Fprintf(os.Stderr, `
+arv-registry is a cli client to interact with the Arvados Registry Daemon
+
+Arguments:
+`)
+ fs.PrintDefaults()
+ fmt.Println()
+}
+
+func main() {
+ registryURL = "https://registry.arvadosapi.com"
+
+ theCluster := parseFlags()
+
+ err := registry.SanityCheckClusterID(clusterID)
+ if err != nil {
+ fmt.Println("\nError:", err.Error(), "\n")
+ os.Exit(1)
+ }
+
+ result, err := registry.Lookup(registryURL, clusterID, theCluster)
+ if err != nil {
+ fmt.Println("\nError:", err.Error(), "\n")
+ os.Exit(1)
+ }
+
+ if result.Available {
+ if checkFlag != true {
+ scanner := bufio.NewScanner(os.Stdin)
+ if theCluster.Name == "" {
+ fmt.Print("Your name: ")
+ scanner.Scan()
+ theCluster.Name = scanner.Text()
+ }
+ if theCluster.Email == "" {
+ fmt.Print("Your e-mail address: ")
+ scanner.Scan()
+ theCluster.Email = scanner.Text()
+ }
+ if theCluster.Organization == "" {
+ fmt.Print("Your organization: ")
+ scanner.Scan()
+ theCluster.Organization = scanner.Text()
+ }
+ if theCluster.ClientSecret == "" {
+ fmt.Print("A secret string, minimum 16 characters: ")
+ scanner.Scan()
+ theCluster.ClientSecret = scanner.Text()
+ }
+ response, err := registry.Register(registryURL, clusterID, theCluster)
+ if err != nil {
+ fmt.Println("\nError: registration failed:", err.Error(), "\n")
+ os.Exit(1)
+ } else {
+ fmt.Println("\nRegistration successful.\n")
+ fmt.Println("Please record the following information, it will be needed to manage your cluster identifier:\n")
+ fmt.Println("Cluster identifier: ", response.ClusterID)
+ fmt.Println("Contact name: ", response.Data["Name"])
+ fmt.Println("Contact e-mail: ", response.Data["Email"])
+ fmt.Println("Contact organization:", response.Data["Organization"])
+ fmt.Println("Registration Secret: ", theCluster.ClientSecret)
+ fmt.Println()
+ }
+ } else {
+ fmt.Println("\nResult:", clusterID, "is available for registration.\n")
+ os.Exit(0)
+ }
+ } else {
+ if checkFlag != true {
+ fmt.Println("\nError:", clusterID, "is not available for registration.\n")
+ os.Exit(2)
+ } else {
+ fmt.Println("\nResult:", clusterID, "is NOT available for registration.\n")
+ if len(result.Data) != 0 {
+ reply := "Additional Data for " + result.ClusterID + ":"
+ for k, v := range result.Data {
+ reply += "\n " + k + ": " + v
+ }
+ fmt.Println(reply)
+ fmt.Println()
+ }
+ os.Exit(2)
+ }
+ }
+}
diff --git a/registry/registry.go b/registry/registry.go
new file mode 100644
index 0000000..cfd35b3
--- /dev/null
+++ b/registry/registry.go
@@ -0,0 +1,177 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package registry
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "git.curoverse.com/arvados.git/sdk/go/config"
+ "io"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "os"
+ "regexp"
+ "strings"
+ "time"
+)
+
+// Client-level types and functions
+
+type Result struct {
+ ClusterID string
+ Available bool
+ Registered bool
+ RegisteredOn *time.Time `json:",omitempty"` // RegisteredOn is a pointer to make json's omitempty work
+ Data map[string]string `json:",omitempty"`
+ RequestAuthenticated bool
+ Elapsed string
+ Type string `json:",omitempty"`
+ Msg string `json:",omitempty"`
+}
+
+// ClusterInfo structure
+type ClusterInfo struct {
+ Name string
+ Email string
+ Organization string
+ ClientSecret string `json:",omitempty"` // I'd use json:"-" here but that also eats the field when doing the import (Decode)
+}
+
+func Lookup(registryURL string, clusterID string, cluster ClusterInfo) (response Result, err error) {
+ url := registryURL + "/v1/check/" + clusterID
+
+ jsonStr, err := json.Marshal(cluster)
+ if err != nil {
+ err = errors.New("unable to create JSON request object")
+ return
+ }
+
+ req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonStr))
+ req.Header.Set("Content-Type", "application/json")
+
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+ err = errors.New("error contacting Arvados Registry server")
+ return
+ }
+ defer resp.Body.Close()
+
+ //fmt.Println("response Status:", resp.Status)
+ //fmt.Println("response Headers:", resp.Header)
+ body, _ := ioutil.ReadAll(resp.Body)
+ //fmt.Println("response Body:", string(body))
+
+ err = json.Unmarshal([]byte(string(body)), &response)
+ if err != nil {
+ err = errors.New("invalid response from the Arvados Registry server")
+ }
+ return
+}
+
+func Register(registryURL string, clusterID string, cluster ClusterInfo) (response Result, err error) {
+ url := registryURL + "/v1/register/" + clusterID
+
+ jsonStr, err := json.Marshal(cluster)
+ if err != nil {
+ err = errors.New("unable to create JSON request object")
+ return
+ }
+ req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr))
+ req.Header.Set("Content-Type", "application/json")
+
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+ err = errors.New("error contacting Arvados Registry server")
+ return
+ }
+ defer resp.Body.Close()
+
+ //fmt.Println("response Status:", resp.Status)
+ //fmt.Println("response Headers:", resp.Header)
+ body, _ := ioutil.ReadAll(resp.Body)
+ //fmt.Println("response Body:", string(body))
+
+ err = json.Unmarshal([]byte(string(body)), &response)
+ if err != nil {
+ err = errors.New("invalid response from the Arvados Registry server")
+ return
+ }
+
+ if resp.Status != "200 OK" {
+ err = errors.New(response.Msg)
+ }
+
+ return
+}
+
+func SanityCheckClusterID(clusterIDToVerify string) (err error) {
+ // Sanity check the input RequestHash
+ // We do not permit 0-9 as the first character (often not permitted as the first character of an object name)
+ // We do not permit x-z as the first character (reserved for future use)
+ match, err := regexp.MatchString("^([a-w][a-z0-9]{4})$", clusterIDToVerify)
+ if err != nil {
+ return
+ }
+ if !match {
+ err = errors.New("invalid cluster identifier (must be 5 characters, lower case, a-z or 0-9 except for the first character, which must be a-w)")
+ }
+
+ return
+}
+
+// Server-level types and functions
+
+type Report struct {
+ Type string
+ Msg string
+}
+
+func LogError(m []string) {
+ log.Printf(string(marshal(Report{"Error", strings.Join(m, " ")})))
+}
+
+func LogNotice(m []string) {
+ log.Printf(string(marshal(Report{"Notice", strings.Join(m, " ")})))
+}
+
+func ReadConfig(cfg interface{}, path string, defaultConfigPath string) error {
+ err := config.LoadFile(cfg, path)
+ if err != nil && os.IsNotExist(err) && path == defaultConfigPath {
+ LogNotice([]string{"Config not specified. Aborting."})
+ }
+ return err
+}
+
+func MarshalAndWrite(w io.Writer, message interface{}) {
+ b := marshal(message)
+ if b == nil {
+ errorMessage := "{\n\"Error\": \"Unspecified error\"\n}"
+ _, err := io.WriteString(w, errorMessage)
+ if err != nil {
+ // do not call arvadosClusterRegistryServer.LogError (it calls marshal and that function has already failed at this point)
+ fmt.Fprintln(os.Stderr, "{\"Error\": \"Unable to write message to client\"}")
+ }
+ } else {
+ _, err := w.Write(b)
+ if err != nil {
+ LogError([]string{"Unable to write message to client:", string(b)})
+ }
+ }
+}
+
+func marshal(message interface{}) (encoded []byte) {
+ encoded, err := json.Marshal(message)
+ if err != nil {
+ // do not call logError here because that would create an infinite loop
+ fmt.Fprintln(os.Stderr, "{\"Error\": \"Unable to marshal message into json:", message, "\"}")
+ return nil
+ }
+ return
+}
diff --git a/registry/server/jabber-bot/.gitignore b/registry/server/jabber-bot/.gitignore
new file mode 100644
index 0000000..76d8f29
--- /dev/null
+++ b/registry/server/jabber-bot/.gitignore
@@ -0,0 +1 @@
+jabber-bot
diff --git a/registry/server/jabber-bot/jabber-bot.go b/registry/server/jabber-bot/jabber-bot.go
new file mode 100644
index 0000000..d8ae134
--- /dev/null
+++ b/registry/server/jabber-bot/jabber-bot.go
@@ -0,0 +1,189 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package main
+
+import (
+ "flag"
+ "fmt"
+
+ "git.curoverse.com/arvados-org.git/registry"
+
+ "github.com/gabeguz/gobot/bot"
+ "github.com/gabeguz/gobot/bot/xmpp"
+
+ // "encoding/json"
+ "os"
+ "regexp"
+ "strings"
+)
+
+// Config structure
+type Config struct {
+ registry.ClusterInfo
+ RegistryURL string
+ XmppHost string
+ XmppUser string
+ XmppPass string
+ XmppRoom string
+ XmppBotName string
+}
+
+var exampleConfigFile = []byte(`
+Name: "Curoverse Sysadmin"
+Email: "sysadmin at curoverse.com"
+Organization: "Curoverse, Inc."
+ClientSecret: "a-really-good-secret-minimum-16-characters"
+XmppHost: "jabber.your.domain:5222"
+XmppUser: "bot-jabber-username"
+XmppPass: "bot-jabber-password"
+XmppRoom: "#arvbottest at conference.your.domain"
+XmppBotName: "jabber-bot-nickname"
+`)
+
+func usage(fs *flag.FlagSet) {
+ fmt.Fprintf(os.Stderr, `
+Arvados Registry Bot is a Jabber bot that can interact with the Arvados Registry Daemon.
+
+Options:
+`)
+ fs.PrintDefaults()
+ fmt.Fprintf(os.Stderr, `
+Example config file:
+%s
+`, exampleConfigFile)
+}
+
+var theConfig Config
+
+const defaultConfigPath = "/etc/arvados/registry/jabber-bot.yml"
+
+type chatbot struct {
+ bot.Bot
+ plugins []bot.Plugin
+}
+
+func parseFlags() (configPath *string) {
+
+ flags := flag.NewFlagSet("arvados-registry-bot", flag.ExitOnError)
+ flags.Usage = func() { usage(flags) }
+
+ configPath = flags.String(
+ "config",
+ defaultConfigPath,
+ "`path` to YAML configuration file")
+
+ // Parse args; omit the first arg which is the command name
+ err := flags.Parse(os.Args[1:])
+ if err != nil {
+ registry.LogError([]string{"Unable to parse command line arguments:", err.Error()})
+ os.Exit(1)
+ }
+
+ return
+}
+
+func main() {
+ configPath := parseFlags()
+
+ err := registry.ReadConfig(&theConfig, *configPath, defaultConfigPath)
+ if err != nil {
+ registry.LogError([]string{"Unable to start Arvados Registry Bot:", err.Error()})
+ os.Exit(1)
+ }
+
+ if theConfig.RegistryURL == "" {
+ theConfig.RegistryURL = "https://registry.arvadosapi.com"
+ }
+
+ if theConfig.XmppHost == "" {
+ registry.LogError([]string{"Unable to start Arvados Registry Daemon: the configuration file needs to define an XMPP Host"})
+ os.Exit(1)
+ }
+
+ gobot := chatbot{
+ xmpp.New(theConfig.XmppHost, theConfig.XmppUser, theConfig.XmppPass, theConfig.XmppRoom, theConfig.XmppBotName),
+ []bot.Plugin{},
+ }
+ err = gobot.Connect()
+ if err != nil {
+ registry.LogError([]string{"Unable to start bot:", err.Error()})
+ os.Exit(1)
+ }
+
+ var msg bot.Message
+ for msg = range gobot.Listen() {
+ go execute(msg, gobot)
+ }
+
+}
+
+func execute(msg bot.Message, bot bot.Bot) {
+ if msg.From() == bot.FullName() {
+ return
+ }
+
+ if strings.HasPrefix(msg.Body(), "!a help") {
+ bot.Send("I am the Arvados Registry Bot. I know these commands:")
+ bot.Send("!a help")
+ bot.Send("!a ch(eck) ClusterID")
+ bot.Send("!a reg(ister) ClusterID")
+ }
+
+ if strings.HasPrefix(msg.Body(), "!a ch") {
+ re := regexp.MustCompile(`ch(eck|) ([a-z0-9]{5})$`)
+ matches := re.FindStringSubmatch(msg.Body())
+ if matches != nil {
+ clusterID := re.FindStringSubmatch(msg.Body())[2]
+ response, err := registry.Lookup(theConfig.RegistryURL, clusterID, theConfig.ClusterInfo)
+ if err != nil {
+ bot.Send("I'm sorry, I encountered an internal error trying to look up " + clusterID)
+ } else {
+ var reply string
+ if response.Available {
+ reply = response.ClusterID + " is available for registration"
+ } else if response.Registered {
+ reply = response.ClusterID + " was registered on " + response.RegisteredOn.String()
+ } else {
+ reply = response.ClusterID + " is not available"
+ }
+ bot.Send(reply)
+ if len(response.Data) != 0 {
+ reply = "Additional Data for " + response.ClusterID + ":"
+ for k, v := range response.Data {
+ reply += "\n" + k + ": " + v
+ }
+ bot.Send(reply)
+ }
+ // responseStr, err := json.Marshal(response)
+ // if err != nil {
+ // bot.Send("I'm sorry, I encountered an internal error trying to parse the response from the registration command for " + clusterID)
+ // } else {
+ // bot.Send(string(responseStr))
+ // }
+ }
+ } else {
+ bot.Send("Syntax: ch(eck) <cluster_id>")
+ }
+ }
+
+ if strings.HasPrefix(msg.Body(), "!a reg") {
+ re := regexp.MustCompile(`reg(ister|) ([a-z0-9]{5})$`)
+ matches := re.FindStringSubmatch(msg.Body())
+ if matches != nil {
+ clusterID := re.FindStringSubmatch(msg.Body())[2]
+ response, err := registry.Register(theConfig.RegistryURL, clusterID, theConfig.ClusterInfo)
+
+ if err != nil {
+ bot.Send("Error: " + response.Msg)
+ } else {
+ bot.Send("I have registered " + clusterID + " for " + response.Data["Name"] + " <" + response.Data["Email"] + "> at " + response.Data["Organization"])
+ }
+
+ } else {
+ bot.Send("Syntax: reg(ister) <cluster_id>")
+ }
+ }
+ return
+}
diff --git a/registry/server/registryd/.gitignore b/registry/server/registryd/.gitignore
new file mode 100644
index 0000000..ce4f068
--- /dev/null
+++ b/registry/server/registryd/.gitignore
@@ -0,0 +1 @@
+registryd
diff --git a/registry/server/registryd/arvados-registryd.service b/registry/server/registryd/arvados-registryd.service
new file mode 100644
index 0000000..77f866c
--- /dev/null
+++ b/registry/server/registryd/arvados-registryd.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=Arvados Registry Daemon
+Documentation=https://doc.arvados.org/
+After=network.target
+AssertPathExists=/etc/arvados/registry/registryd.yml
+
+[Service]
+Type=simple
+ExecStart=/usr/local/bin/arvados-registryd
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
diff --git a/registry/server/registryd/registryd.go b/registry/server/registryd/registryd.go
new file mode 100644
index 0000000..e997a89
--- /dev/null
+++ b/registry/server/registryd/registryd.go
@@ -0,0 +1,447 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package main
+
+import (
+ "encoding/json"
+ "flag"
+ "fmt"
+
+ "git.curoverse.com/arvados-org.git/registry"
+
+ "net"
+ "net/http"
+ "os"
+ "os/signal"
+ "crypto/hmac"
+ "crypto/sha256"
+ "database/sql"
+ "encoding/base64"
+ _ "github.com/mattn/go-sqlite3"
+ "github.com/rubenv/sql-migrate"
+ "regexp"
+ "strings"
+ "syscall"
+ "time"
+)
+
+var listener net.Listener
+var db *sql.DB
+
+type bundle struct {
+ sourceDir string
+ name string
+ packageType string
+ versionType string
+ versionPrefix string
+}
+
+type result struct {
+ ClusterID string
+ Available bool
+ Registered bool
+ RegisteredOn *time.Time `json:",omitempty"` // RegisteredOn is a pointer to make json's omitempty work
+ Data map[string]string `json:",omitempty"`
+ RequestAuthenticated bool
+ Elapsed string
+}
+
+type about struct {
+ Name string
+ Version string
+ URL string
+}
+
+type help struct {
+ Usage string
+}
+
+type registerCluster struct {
+ Name string
+ Email string
+ Organization string
+ ClientSecret string `json:",omitempty"` // I'd use json:"-" here but that also eats the field when doing the import (Decode)
+}
+
+// Config structure
+type Config struct {
+ DatabaseAdapter string
+ Database string
+ ServerSecret string
+ ListenPort string
+}
+
+var theConfig Config
+
+const defaultConfigPath = "/etc/arvados/registry/registryd.yml"
+
+func lookup(clusterID string) (available bool, registered bool, registeredOn time.Time, data map[string]string, secret string, err error) {
+ var dataStr string
+ err = db.QueryRow("SELECT registered, registered_on, data, secret FROM clusters WHERE identifier=?", clusterID).Scan(®istered, ®isteredOn, &dataStr, &secret)
+
+ switch {
+ case err == sql.ErrNoRows:
+ registry.LogNotice([]string{"No cluster found with ID", clusterID})
+ available = true
+ err = nil
+ case err != nil:
+ registry.LogError([]string{"Unable to query database:", err.Error()})
+ default:
+ err = json.Unmarshal([]byte(dataStr), &data)
+ if err != nil {
+ registry.LogError([]string{"Unable to unmarshal data field", err.Error()})
+ }
+ }
+ return
+}
+
+func computeHmac(payload string) string {
+ mac := hmac.New(sha256.New, []byte(theConfig.ServerSecret))
+ mac.Write([]byte(payload))
+ return base64.StdEncoding.EncodeToString(mac.Sum(nil))
+}
+
+func register(clusterID string, cluster registerCluster, ipAddress string) (err error) {
+ hmac := computeHmac(cluster.ClientSecret)
+
+ cluster.ClientSecret = "" // we don't want this in the db
+ dataStr, err := json.Marshal(cluster)
+ if err != nil {
+ return
+ }
+
+ _, err = db.Exec("INSERT INTO clusters (identifier,data,registered,registered_on,secret,ip_address) values (?,?,?,?,?,?)", clusterID, dataStr, true, time.Now(), hmac, ipAddress)
+ if err == nil {
+ registry.LogNotice([]string{"Registered ClusterID:", clusterID})
+ } else {
+ registry.LogError([]string{"Unable to register ClusterID:", clusterID, err.Error()})
+ }
+ return
+}
+
+func sanityCheckClusterID(clusterIDToVerify string) (clusterID string, errReport registry.Report) {
+ // Sanity check the input RequestHash
+ // We do not permit 0-9 as the first character (often not permitted as the first character of an object name)
+ // We do not permit x-z as the first character (reserved for future use)
+ match, err := regexp.MatchString("^([a-w][a-z0-9]{4})$", clusterIDToVerify)
+ if err != nil {
+ errReport = registry.Report{"Error", "Error matching ClusterID"}
+ return
+ }
+ if !match {
+ errReport = registry.Report{"Error", "Invalid ClusterID (must be 5 characters, lower case, a-z or 0-9 except for the first character, which must be a-w)"}
+ }
+ clusterID = clusterIDToVerify
+
+ return
+}
+
+func sanityCheckClientSecret(clientSecret string) (errReport registry.Report) {
+ // Sanity check the ClientSecret provided
+ match, err := regexp.MatchString("^.{16,}$", clientSecret)
+ if err != nil {
+ errReport = registry.Report{"Error", "Error verifying validity of ClientSecret"}
+ return
+ }
+ if !match {
+ errReport = registry.Report{"Error", "Invalid ClientSecret (min length: 16 characters)"}
+ }
+
+ return
+}
+
+func registerHandler(w http.ResponseWriter, r *http.Request) {
+ start := time.Now()
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+
+ if r.Body == nil {
+ w.WriteHeader(http.StatusBadRequest)
+ m := registry.Report{"Error", "Please send a request body as JSON with these keys: email, organization, clientSecret"}
+ registry.MarshalAndWrite(w, m)
+ return
+ }
+
+ clusterID, errReport := sanityCheckClusterID(r.URL.Path[13:])
+ if errReport != (registry.Report{}) {
+ w.WriteHeader(http.StatusBadRequest)
+ registry.MarshalAndWrite(w, errReport)
+ return
+ }
+
+ var cluster registerCluster
+ err := json.NewDecoder(r.Body).Decode(&cluster)
+ if err != nil {
+ w.WriteHeader(http.StatusBadRequest)
+ m := registry.Report{"Error", "Unable to decode request body as JSON: " + err.Error()}
+ registry.MarshalAndWrite(w, m)
+ return
+ }
+ errReport = sanityCheckClientSecret(cluster.ClientSecret)
+ if errReport != (registry.Report{}) {
+ w.WriteHeader(http.StatusBadRequest)
+ registry.MarshalAndWrite(w, errReport)
+ return
+ }
+
+ if cluster.Name == "" ||
+ cluster.Email == "" ||
+ cluster.Organization == "" {
+ errReport = registry.Report{"Error", "Missing fields: name, email, organization are required"}
+ w.WriteHeader(http.StatusBadRequest)
+ registry.MarshalAndWrite(w, errReport)
+ return
+ }
+
+ match, err := regexp.MatchString("^.+ at .+\\..+$", cluster.Email)
+ if err != nil {
+ errReport = registry.Report{"Error", "Internal error validating e-mail address format"}
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ if !match {
+ errReport = registry.Report{"Error", "Invalid e-mail address"}
+ w.WriteHeader(http.StatusBadRequest)
+ registry.MarshalAndWrite(w, errReport)
+ return
+ }
+
+ logCluster := cluster
+ logCluster.ClientSecret = "" // So as not to log it
+ encoded, _ := json.Marshal(logCluster)
+ registry.LogNotice([]string{"Received valid JSON request body:", string(encoded)})
+
+ var available, registered bool
+ var registeredOn time.Time
+ var data map[string]string
+
+ available, registered, registeredOn, data, _, err = lookup(clusterID)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ m := registry.Report{"Error", "Unspecified error"}
+ registry.MarshalAndWrite(w, m)
+ return
+ }
+
+ if available {
+ // Cf. https://en.wikipedia.org/wiki/X-Forwarded-For#Format
+ ipAddress := strings.Split(r.Header.Get("X-Forwarded-For"), ", ")[0]
+ if ipAddress == "" {
+ ipAddress, _, err = net.SplitHostPort(r.RemoteAddr)
+ if err != nil {
+ fmt.Fprintf(w, "userip: %q is not IP:port", r.RemoteAddr)
+ }
+ }
+ // register it
+ err := register(clusterID, cluster, ipAddress)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ m := registry.Report{"Error", "Unable to register ClusterID " + clusterID}
+ registry.MarshalAndWrite(w, m)
+ return
+ }
+ available, registered, registeredOn, data, _, err = lookup(clusterID)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ m := registry.Report{"Error", "Unspecified error registering ClusterID " + clusterID}
+ registry.MarshalAndWrite(w, m)
+ return
+ }
+ } else {
+ registry.LogNotice([]string{"Attempt to register ClusterID that is not available for registration:", clusterID})
+ w.WriteHeader(http.StatusBadRequest)
+ m := registry.Report{"Error", "This ClusterID is not available for registration."}
+ registry.MarshalAndWrite(w, m)
+ return
+ }
+
+ m := result{clusterID, available, registered, ®isteredOn, data, true, fmt.Sprintf("%v", time.Since(start))}
+ registry.MarshalAndWrite(w, m)
+}
+
+func checkHandler(w http.ResponseWriter, r *http.Request) {
+ start := time.Now()
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+
+ var available, registered, requestAuthenticated bool
+ var registeredOn time.Time
+ var data map[string]string
+
+ clusterID, errReport := sanityCheckClusterID(r.URL.Path[10:])
+
+ available, registered, registeredOn, realData, secret, err := lookup(clusterID)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ m := registry.Report{"Error", "Unspecified error"}
+ registry.MarshalAndWrite(w, m)
+ return
+ }
+
+ // This ClusterID did not pass the sanity check, it is invalid
+ if errReport != (registry.Report{}) {
+ available = false
+ }
+
+ // An optional request body is permitted to send a ClientSecret
+ // If it matches what is in the database, the Data object will be
+ // included in the response.
+ if r.Body != nil {
+ var cluster registerCluster
+ err := json.NewDecoder(r.Body).Decode(&cluster)
+ if err == nil {
+ if computeHmac(cluster.ClientSecret) != secret {
+ registry.LogNotice([]string{"Invalid ClientSecret supplied, not returning Data object"})
+ } else {
+ data = realData
+ requestAuthenticated = true
+ }
+ }
+ }
+ registeredOnPtr := ®isteredOn
+ if registered == false {
+ registeredOnPtr = nil
+ }
+
+ m := result{clusterID, available, registered, registeredOnPtr, data, requestAuthenticated, fmt.Sprintf("%v", time.Since(start))}
+ registry.MarshalAndWrite(w, m)
+}
+
+func aboutHandler(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ m := about{"Arvados Registry Daemon", "0.1", "https://arvados.org"}
+ registry.MarshalAndWrite(w, m)
+}
+
+func helpHandler(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ m := help{"GET /v1/check/cluster-id or GET /v1/about or GET /v1/help"}
+ registry.MarshalAndWrite(w, m)
+}
+
+func parseFlags() (configPath *string) {
+
+ flags := flag.NewFlagSet("arvados-cluster-registry", flag.ExitOnError)
+ flags.Usage = func() { usage(flags) }
+
+ configPath = flags.String(
+ "config",
+ defaultConfigPath,
+ "`path` to YAML configuration file")
+
+ // Parse args; omit the first arg which is the command name
+ err := flags.Parse(os.Args[1:])
+ if err != nil {
+ registry.LogError([]string{"Unable to parse command line arguments:", err.Error()})
+ os.Exit(1)
+ }
+
+ return
+}
+
+func loadDb() (db *sql.DB, err error) {
+ // Hardcoded migration strings in memory:
+ migrations := &migrate.MemoryMigrationSource{
+ Migrations: []*migrate.Migration{
+ {
+ Id: "0",
+ Up: []string{"CREATE TABLE clusters (identifier varchar(5), data text, registered boolean, registered_on timestamp, secret text, ip_address text, CONSTRAINT identifier_unique UNIQUE (identifier))"},
+ Down: []string{"DROP TABLE clusters"},
+ },
+ {
+ Id: "1",
+ Up: []string{"INSERT INTO clusters (identifier, data, registered, registered_on, secret) values ('qr1hi','{\"Name\":\"Ward Vandewege\",\"Email\":\"sysadmin at curoverse.com\",\"Organization\":\"Curoverse, Inc.\"}',1,'2013-09-01 00:00:00','')"},
+ Down: []string{"DELETE FROM clusters where identifier='qr1hi'"},
+ },
+ {
+ Id: "2",
+ Up: []string{"INSERT INTO clusters (identifier, data, registered, registered_on, secret) values ('su92l','{\"Name\":\"Ward Vandewege\",\"Email\":\"sysadmin at curoverse.com\",\"Organization\":\"Curoverse, Inc.\"}',1,'2013-09-01 00:00:00','')"},
+ Down: []string{"DELETE FROM clusters where identifier='su92l'"},
+ },
+ },
+ }
+ registry.LogNotice([]string{"Using", theConfig.DatabaseAdapter})
+ registry.LogNotice([]string{"Database", theConfig.Database})
+
+ db, err = sql.Open("sqlite3", theConfig.Database)
+ if err != nil {
+ return
+ }
+
+ n, err := migrate.Exec(db, "sqlite3", migrations, migrate.Up)
+ if err != nil {
+ return
+ }
+
+ if n > 0 {
+ registry.LogNotice([]string{"Applied", fmt.Sprintf("%d", n), "migration(s)"})
+ }
+ return
+}
+
+func main() {
+
+ configPath := parseFlags()
+
+ err := registry.ReadConfig(&theConfig, *configPath, defaultConfigPath)
+ if err != nil {
+ registry.LogError([]string{"Unable to start Arvados Registry Daemon:", err.Error()})
+ os.Exit(1)
+ }
+
+ if theConfig.DatabaseAdapter == "" {
+ theConfig.DatabaseAdapter = "sqlite3"
+ }
+
+ if theConfig.Database == "" {
+ registry.LogError([]string{"Unable to start Arvados Registry Daemon: the configuration file needs to define a database"})
+ os.Exit(1)
+ }
+
+ db, err = loadDb()
+ if err != nil {
+ registry.LogError([]string{"Unable to start Arvados Registry Daemon:", err.Error()})
+ os.Exit(1)
+ }
+
+ if theConfig.ServerSecret == "" {
+ registry.LogError([]string{"Unable to start Arvados Registry Daemon: the configuration file needs to define a serverSecret"})
+ os.Exit(1)
+ }
+
+ if theConfig.ListenPort == "" {
+ theConfig.ListenPort = "80"
+ }
+
+ http.HandleFunc("/v1/check/", checkHandler)
+ http.HandleFunc("/v1/register/", registerHandler)
+ http.HandleFunc("/v1/about", aboutHandler)
+ http.HandleFunc("/v1/help", helpHandler)
+ http.HandleFunc("/v1", helpHandler)
+ http.HandleFunc("/", helpHandler)
+ registry.LogNotice([]string{"Arvados Registry Daemon listening on port", theConfig.ListenPort})
+
+ listener, err = net.Listen("tcp", ":"+theConfig.ListenPort)
+
+ if err != nil {
+ registry.LogError([]string{"Unable to start Arvados Registry Daemon:", err.Error()})
+ os.Exit(1)
+ }
+
+ // Shut down the server gracefully (by closing the listener)
+ // if SIGTERM is received.
+ term := make(chan os.Signal, 1)
+ go func(sig <-chan os.Signal) {
+ <-sig
+ registry.LogError([]string{"caught signal"})
+ _ = listener.Close()
+ }(term)
+ signal.Notify(term, syscall.SIGTERM)
+ signal.Notify(term, syscall.SIGINT)
+
+ // Start serving requests.
+ _ = http.Serve(listener, nil)
+ // http.Serve returns an error when it gets the term or int signal
+
+ registry.LogNotice([]string{"Arvados Registry Daemon shutting down"})
+}
diff --git a/registry/server/registryd/registryd_test.go b/registry/server/registryd/registryd_test.go
new file mode 100644
index 0000000..d7b11db
--- /dev/null
+++ b/registry/server/registryd/registryd_test.go
@@ -0,0 +1,319 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "git.curoverse.com/arvados-org.git/registry"
+ . "gopkg.in/check.v1"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "os"
+ "testing"
+ "time"
+)
+
+// Hook gocheck into the "go test" runner.
+func Test(t *testing.T) { TestingT(t) }
+
+// Gocheck boilerplate
+var _ = Suite(&ServerRequiredSuite{})
+var _ = Suite(&ServerNotRequiredSuite{})
+
+type ServerRequiredSuite struct{}
+type ServerNotRequiredSuite struct{}
+
+var tmpConfigFileName string
+var tmpDBFileName string
+
+func closeListener() {
+ if listener != nil {
+ listener.Close()
+ }
+}
+
+func (s *ServerNotRequiredSuite) TestConfig(c *C) {
+ var config Config
+
+ // A specified but non-existing config path needs to result in an error
+ err := registry.ReadConfig(&config, "/nosuchdir89j7879/8hjwr7ojgyy7", defaultConfigPath)
+ c.Assert(err, NotNil)
+
+ // No configuration file and default configuration path specified, must error
+ err = registry.ReadConfig(&config, "/nosuchdir89j7879/8hjwr7ojgyy7", "/nosuchdir89j7879/8hjwr7ojgyy7")
+ c.Assert(err, NotNil)
+
+ // Test parsing of config data
+ tmpfile, err := ioutil.TempFile(os.TempDir(), "config")
+ c.Check(err, IsNil)
+ defer os.Remove(tmpfile.Name())
+
+ tmpdb, err := ioutil.TempFile(os.TempDir(), "db")
+ c.Check(err, IsNil)
+ defer os.Remove(tmpdb.Name())
+
+ argsS := `{"ServerSecret": "super_secret_sauce", "ListenPort": "12345", "Database": "` + tmpdb.Name() + `"}`
+ _, err = tmpfile.Write([]byte(argsS))
+ c.Check(err, IsNil)
+
+ err = registry.ReadConfig(&config, tmpfile.Name(), defaultConfigPath)
+ c.Assert(err, IsNil)
+
+ c.Check(config.ServerSecret, Equals, "super_secret_sauce")
+ c.Check(config.ListenPort, Equals, "12345")
+ c.Check(config.Database, Equals, tmpdb.Name())
+}
+
+func (s *ServerNotRequiredSuite) TestFlags(c *C) {
+ args := []string{"arvados-cluster-registry"}
+ os.Args = append(args)
+}
+
+func runServer(c *C) {
+ tmpfile, err := ioutil.TempFile(os.TempDir(), "config")
+ c.Check(err, IsNil)
+
+ tmpConfigFileName = tmpfile.Name()
+
+ tmpdb, err := ioutil.TempFile(os.TempDir(), "db")
+ c.Check(err, IsNil)
+
+ tmpDBFileName = tmpdb.Name()
+
+ argsS := `{"ServerSecret": "a_very_good_secret_this_is_not", "ListenPort": "12345", "Database": ` + tmpDBFileName + `}`
+ _, err = tmpfile.Write([]byte(argsS))
+ c.Check(err, IsNil)
+
+ args := []string{"arvados-cluster-registry"}
+ os.Args = append(args, "-config", tmpfile.Name())
+ listener = nil
+ go main()
+ waitForListener()
+}
+
+func waitForListener() {
+ const (
+ ms = 5
+ )
+ for i := 0; listener == nil && i < 10000; i += ms {
+ time.Sleep(ms * time.Millisecond)
+ }
+ if listener == nil {
+ log.Fatalf("Timed out waiting for listener to start")
+ }
+}
+
+func (s *ServerNotRequiredSuite) SetUpTest(c *C) {
+ // Discard standard log output
+ log.SetOutput(ioutil.Discard)
+}
+
+func (s *ServerRequiredSuite) SetUpTest(c *C) {
+ // Discard standard log output
+ log.SetOutput(ioutil.Discard)
+}
+
+func (s *ServerRequiredSuite) TearDownSuite(c *C) {
+ log.SetOutput(os.Stderr)
+}
+
+func (s *ServerNotRequiredSuite) TearDownSuite(c *C) {
+ log.SetOutput(os.Stderr)
+}
+
+func (s *ServerRequiredSuite) TestResults(c *C) {
+ runServer(c)
+ defer closeListener()
+ defer os.Remove(tmpDBFileName)
+ defer os.Remove(tmpConfigFileName)
+
+ // Test the about handler
+ {
+ client := http.Client{}
+ req, err := http.NewRequest("GET",
+ fmt.Sprintf("http://%s/%s", listener.Addr().String(), "v1/about"),
+ nil)
+ resp, err := client.Do(req)
+ c.Check(err, Equals, nil)
+ c.Check(resp.StatusCode, Equals, 200)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Check(string(body), Matches, ".*\"Name\":\"Arvados Registry Daemon\".*")
+ }
+
+ // Test the help handler
+ {
+ client := http.Client{}
+ req, err := http.NewRequest("GET",
+ fmt.Sprintf("http://%s/%s", listener.Addr().String(), "v1/help"),
+ nil)
+ resp, err := client.Do(req)
+ c.Check(err, Equals, nil)
+ c.Check(resp.StatusCode, Equals, 200)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Check(string(body), Matches, ".*GET /v1/about or GET /v1/help.*")
+ }
+
+ // Registration of a valid ClusterID with proper JSON body (must pass)
+ {
+ client := http.Client{}
+ var jsonStr = []byte(`{"clientSecret":"this_is_long_enough","name":"Ward Vandewege","email":"sysadmin at curoverse.com","organization":"Curoverse, Inc."}`)
+ req, err := http.NewRequest("POST",
+ fmt.Sprintf("http://%s/%s", listener.Addr().String(), "v1/register/abcde"),
+ bytes.NewBuffer(jsonStr))
+ resp, err := client.Do(req)
+ c.Check(err, Equals, nil)
+ c.Check(resp.StatusCode, Equals, 200)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Check(string(body), Matches, ".*\"ClusterID\":\"abcde\",\"Available\":false,\"Registered\":true.*")
+ }
+
+ // Check existence of a valid ClusterID (must pass)
+ {
+ client := http.Client{}
+ req, err := http.NewRequest("GET",
+ fmt.Sprintf("http://%s/%s", listener.Addr().String(), "v1/check/abcde"),
+ nil)
+ resp, err := client.Do(req)
+ c.Check(err, Equals, nil)
+ c.Check(resp.StatusCode, Equals, 200)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Check(string(body), Matches, ".*\"ClusterID\":\"abcde\",\"Available\":false,\"Registered\":true.*")
+ }
+
+ // Check existence of an invalid ClusterID (must pass)
+ {
+ client := http.Client{}
+ req, err := http.NewRequest("GET",
+ fmt.Sprintf("http://%s/%s", listener.Addr().String(), "v1/check/1bcde"),
+ nil)
+ resp, err := client.Do(req)
+ c.Check(err, Equals, nil)
+ c.Check(resp.StatusCode, Equals, 200)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Check(string(body), Matches, ".*\"ClusterID\":\"1bcde\",\"Available\":false,\"Registered\":false.*")
+ }
+
+ // Registration of a ClusterID with proper JSON body missing fields (must fail)
+ {
+ client := http.Client{}
+ var jsonStr = []byte(`{"clientSecret":"this_is_long_enough"}`)
+ req, err := http.NewRequest("POST",
+ fmt.Sprintf("http://%s/%s", listener.Addr().String(), "v1/register/abcde"),
+ bytes.NewBuffer(jsonStr))
+ resp, err := client.Do(req)
+ c.Check(err, Equals, nil)
+ c.Check(resp.StatusCode, Equals, 400)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Check(string(body), Matches, ".*Missing fields.*")
+ }
+
+ // Registration of an existing ClusterID with proper JSON body (must fail)
+ {
+ client := http.Client{}
+ var jsonStr = []byte(`{"clientSecret":"this_is_long_enough","name":"Ward Vandewege","email":"sysadmin at curoverse.com","organization":"Curoverse, Inc."}`)
+ req, err := http.NewRequest("POST",
+ fmt.Sprintf("http://%s/%s", listener.Addr().String(), "v1/register/abcde"),
+ bytes.NewBuffer(jsonStr))
+ resp, err := client.Do(req)
+ c.Check(err, Equals, nil)
+ c.Check(resp.StatusCode, Equals, 400)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Check(string(body), Matches, ".*This ClusterID is not available for registration.*")
+ }
+
+ // Registration of a valid ClusterID with proper JSON body but clientSecret that is too short (must fail)
+ {
+ client := http.Client{}
+ var jsonStr = []byte(`{"clientSecret":"not_long_enough"}`)
+ req, err := http.NewRequest("POST",
+ fmt.Sprintf("http://%s/%s", listener.Addr().String(), "v1/register/abcde"),
+ bytes.NewBuffer(jsonStr))
+ resp, err := client.Do(req)
+ c.Check(err, Equals, nil)
+ c.Check(resp.StatusCode, Equals, 400)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Check(string(body), Matches, ".*min length: 16 characters.*")
+ }
+
+ // Registration of a valid ClusterID but without JSON body (must fail)
+ {
+ client := http.Client{}
+ req, err := http.NewRequest("POST",
+ fmt.Sprintf("http://%s/%s", listener.Addr().String(), "v1/register/abcde"),
+ nil)
+ resp, err := client.Do(req)
+ c.Check(err, Equals, nil)
+ c.Check(resp.StatusCode, Equals, 400)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Check(string(body), Matches, ".*Unable to decode request body as JSON.*")
+ }
+
+ // Registration of an invalid ClusterID (must fail)
+ {
+ client := http.Client{}
+ req, err := http.NewRequest("POST",
+ fmt.Sprintf("http://%s/%s", listener.Addr().String(), "v1/register/1abcd"),
+ nil)
+ resp, err := client.Do(req)
+ c.Check(err, Equals, nil)
+ c.Check(resp.StatusCode, Equals, 400)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Check(string(body), Matches, ".*Invalid ClusterID.*")
+ }
+
+ // Registration of an invalid ClusterID (must fail)
+ {
+ client := http.Client{}
+ req, err := http.NewRequest("POST",
+ fmt.Sprintf("http://%s/%s", listener.Addr().String(), "v1/register/xabcd"),
+ nil)
+ resp, err := client.Do(req)
+ c.Check(err, Equals, nil)
+ c.Check(resp.StatusCode, Equals, 400)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Check(string(body), Matches, ".*Invalid ClusterID.*")
+ }
+
+ // Registration of an invalid ClusterID (must fail)
+ {
+ client := http.Client{}
+ req, err := http.NewRequest("POST",
+ fmt.Sprintf("http://%s/%s", listener.Addr().String(), "v1/register/yabcd"),
+ nil)
+ resp, err := client.Do(req)
+ c.Check(err, Equals, nil)
+ c.Check(resp.StatusCode, Equals, 400)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Check(string(body), Matches, ".*Invalid ClusterID.*")
+ }
+
+ // Registration of an invalid ClusterID (must fail)
+ {
+ client := http.Client{}
+ req, err := http.NewRequest("POST",
+ fmt.Sprintf("http://%s/%s", listener.Addr().String(), "v1/register/zabcd"),
+ nil)
+ resp, err := client.Do(req)
+ c.Check(err, Equals, nil)
+ c.Check(resp.StatusCode, Equals, 400)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Check(string(body), Matches, ".*Invalid ClusterID.*")
+ }
+
+ // Registration of an invalid ClusterID (must fail)
+ {
+ client := http.Client{}
+ req, err := http.NewRequest("POST",
+ fmt.Sprintf("http://%s/%s", listener.Addr().String(), "v1/register/ABCDE"),
+ nil)
+ resp, err := client.Do(req)
+ c.Check(err, Equals, nil)
+ c.Check(resp.StatusCode, Equals, 400)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Check(string(body), Matches, ".*Invalid ClusterID.*")
+ }
+}
diff --git a/registry/server/registryd/usage.go b/registry/server/registryd/usage.go
new file mode 100644
index 0000000..4a4521c
--- /dev/null
+++ b/registry/server/registryd/usage.go
@@ -0,0 +1,30 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+)
+
+var exampleConfigFile = []byte(`
+listenPort: 8080
+serverSecret: a_long_secret_string
+`)
+
+func usage(fs *flag.FlagSet) {
+ fmt.Fprintf(os.Stderr, `
+Arvados Registry Daemon is a JSON REST service that manages the global namespace
+of Arvados cluster identifiers.
+
+Options:
+`)
+ fs.PrintDefaults()
+ fmt.Fprintf(os.Stderr, `
+Example config file:
+%s
+`, exampleConfigFile)
+}
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list