Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"io/ioutil"
"os"
"os/signal"
"path/filepath"
Expand Down Expand Up @@ -47,6 +48,7 @@ var (
verbose bool
serverThreads int
allPortOffsetsUnique bool
jwtSecretFile string
dockerEndpoint string
dockerImage string
dockerUser string
Expand Down Expand Up @@ -79,6 +81,7 @@ func init() {
f.BoolVar(&dockerNetHost, "dockerNetHost", false, "Run containers with --net=host")
f.BoolVar(&dockerPrivileged, "dockerPrivileged", false, "Run containers with --privileged")
f.BoolVar(&allPortOffsetsUnique, "uniquePortOffsets", false, "If set, all peers will get a unique port offset. If false (default) only portOffset+peerAddress pairs will be unique.")
f.StringVar(&jwtSecretFile, "jwtSecretFile", "", "name of a plain text file containing a JWT secret used for server authentication")
}

// handleSignal listens for termination signals and stops this process onup termination.
Expand Down Expand Up @@ -191,6 +194,16 @@ func cmdMainRun(cmd *cobra.Command, args []string) {
log.Fatalf("Cannot create data directory %s because %v, giving up.", dataDir, err)
}

// Read jwtSecret (if any)
var jwtSecret string
if jwtSecretFile != "" {
content, err := ioutil.ReadFile(jwtSecretFile)
if err != nil {
log.Fatalf("Failed to read JWT secret file '%s': %v", jwtSecretFile, err)
}
jwtSecret = strings.TrimSpace(string(content))
}

// Interrupt signal:
sigChannel := make(chan os.Signal)
rootCtx, cancel := context.WithCancel(context.Background())
Expand All @@ -213,6 +226,7 @@ func cmdMainRun(cmd *cobra.Command, args []string) {
Verbose: verbose,
ServerThreads: serverThreads,
AllPortOffsetsUnique: allPortOffsetsUnique,
JwtSecret: jwtSecret,
RunningInDocker: os.Getenv("RUNNING_IN_DOCKER") == "true",
DockerContainer: dockerContainer,
DockerEndpoint: dockerEndpoint,
Expand Down
99 changes: 68 additions & 31 deletions service/arangodb.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type ServiceConfig struct {
Verbose bool
ServerThreads int // If set to something other than 0, this will be added to the commandline of each server with `--server.threads`...
AllPortOffsetsUnique bool // If set, all peers will get a unique port offset. If false (default) only portOffset+peerAddress pairs will be unique.
JwtSecret string

DockerContainer string // Name of the container running this process
DockerEndpoint string // Where to reach the docker daemon
Expand Down Expand Up @@ -130,15 +131,32 @@ func slasher(s string) string {
return strings.Replace(s, "\\", "/", -1)
}

func testInstance(ctx context.Context, address string, port int) (up, cancelled bool) {
func (s *Service) testInstance(ctx context.Context, address string, port int) (up, cancelled bool) {
instanceUp := make(chan bool)
go func() {
client := &http.Client{Timeout: time.Second * 10}
for i := 0; i < 300; i++ {
makeRequest := func() error {
addr := net.JoinHostPort(address, strconv.Itoa(port))
url := fmt.Sprintf("http://%s/_api/version", addr)
r, e := client.Get(url)
if e == nil && r != nil && r.StatusCode == 200 {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return maskAny(err)
}
if err := addJwtHeader(req, s.JwtSecret); err != nil {
return maskAny(err)
}
resp, err := client.Do(req)
if err != nil {
return maskAny(err)
}
if resp.StatusCode != 200 {
return maskAny(fmt.Errorf("Invalid status %d", resp.StatusCode))
}
return nil
}

for i := 0; i < 300; i++ {
if err := makeRequest(); err == nil {
instanceUp <- true
break
}
Expand All @@ -154,23 +172,6 @@ func testInstance(ctx context.Context, address string, port int) (up, cancelled
}
}

var confFileTemplate = `# ArangoDB configuration file
#
# Documentation:
# https://docs.arangodb.com/Manual/Administration/Configuration/
#

[server]
endpoint = tcp://[::]:%s
threads = %d

[log]
level = %s

[javascript]
v8-contexts = %d
`

func (s *Service) makeBaseArgs(myHostDir, myContainerDir string, myAddress string, myPort string, mode string) (args []string, configVolumes []Volume) {
hostConfFileName := filepath.Join(myHostDir, "arangod.conf")
containerConfFileName := filepath.Join(myContainerDir, "arangod.conf")
Expand All @@ -184,20 +185,57 @@ func (s *Service) makeBaseArgs(myHostDir, myContainerDir string, myAddress strin
}

if _, err := os.Stat(hostConfFileName); os.IsNotExist(err) {
out, e := os.Create(hostConfFileName)
if e != nil {
s.log.Fatalf("Could not create configuration file %s, error: %#v", hostConfFileName, e)
}
var threads, v8Contexts string
logLevel := "INFO"
switch mode {
// Parameters are: port, server threads, log level, v8-contexts
case "agent":
fmt.Fprintf(out, confFileTemplate, myPort, 8, "INFO", 1)
threads = "8"
v8Contexts = "1"
case "dbserver":
fmt.Fprintf(out, confFileTemplate, myPort, 4, "INFO", 4)
threads = "4"
v8Contexts = "4"
case "coordinator":
fmt.Fprintf(out, confFileTemplate, myPort, 16, "INFO", 4)
threads = "16"
v8Contexts = "4"
}
serverSection := &configSection{
Name: "server",
Settings: map[string]string{
"endpoint": fmt.Sprintf("tcp://[::]:%s", myPort),
"threads": threads,
"authentication": "false",
},
}
if s.JwtSecret != "" {
serverSection.Settings["authentication"] = "true"
serverSection.Settings["jwt-secret"] = s.JwtSecret
}
config := configFile{
serverSection,
&configSection{
Name: "log",
Settings: map[string]string{
"level": logLevel,
},
},
&configSection{
Name: "javascript",
Settings: map[string]string{
"v8-contexts": v8Contexts,
},
},
}

out, e := os.Create(hostConfFileName)
if e != nil {
s.log.Fatalf("Could not create configuration file %s, error: %#v", hostConfFileName, e)
}
_, err := config.WriteTo(out)
out.Close()
if err != nil {
s.log.Fatalf("Cannot create config file: %v", err)
}
}
args = make([]string, 0, 40)
executable := s.ArangodExecutable
Expand All @@ -213,7 +251,6 @@ func (s *Service) makeBaseArgs(myHostDir, myContainerDir string, myAddress strin
"--javascript.app-path", slasher(filepath.Join(myContainerDir, "apps")),
"--log.file", slasher(filepath.Join(myContainerDir, "arangod.log")),
"--log.force-direct", "false",
"--server.authentication", "false",
)
if s.ServerThreads != 0 {
args = append(args, "--server.threads", strconv.Itoa(s.ServerThreads))
Expand Down Expand Up @@ -319,7 +356,7 @@ func (s *Service) startRunning(runner Runner) {
if p != nil {
s.log.Infof("%s seems to be running already, checking port %d...", mode, myPort)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
up, _ := testInstance(ctx, myHost, myPort)
up, _ := s.testInstance(ctx, myHost, myPort)
cancel()
if up {
s.log.Infof("%s is already running on %d. No need to start anything.", mode, myPort)
Expand Down Expand Up @@ -360,7 +397,7 @@ func (s *Service) startRunning(runner Runner) {
*processVar = p
ctx, cancel := context.WithCancel(s.ctx)
go func() {
if up, cancelled := testInstance(ctx, myHost, s.MasterPort+portOffset+serverPortOffset); !cancelled {
if up, cancelled := s.testInstance(ctx, myHost, s.MasterPort+portOffset+serverPortOffset); !cancelled {
if up {
s.log.Infof("%s up and running.", mode)
} else {
Expand Down
51 changes: 51 additions & 0 deletions service/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package service

import (
"fmt"
"io"
"strings"
)

var confHeader = `# ArangoDB configuration file
#
# Documentation:
# https://docs.arangodb.com/Manual/Administration/Configuration/
#

`

type configFile []*configSection

// WriteTo writes the configuration sections to the given writer.
func (cf configFile) WriteTo(w io.Writer) (int64, error) {
x := int64(0)
n, err := w.Write([]byte(confHeader))
if err != nil {
return x, maskAny(err)
}
x += int64(n)
for _, section := range cf {
n, err := section.WriteTo(w)
if err != nil {
return x, maskAny(err)
}
x += int64(n)
}
return x, nil
}

type configSection struct {
Name string
Settings map[string]string
}

// WriteTo writes the configuration section to the given writer.
func (s *configSection) WriteTo(w io.Writer) (int64, error) {
lines := []string{"[" + s.Name + "]"}
for k, v := range s.Settings {
lines = append(lines, fmt.Sprintf("%s = %s", k, v))
}
lines = append(lines, "")
n, err := w.Write([]byte(strings.Join(lines, "\n")))
return int64(n), maskAny(err)
}
31 changes: 31 additions & 0 deletions service/jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package service

import (
"net/http"

jwt "github.com/dgrijalva/jwt-go"
)

// addJwtHeader calculates a JWT authorization header based on the given secret
// and adds it to the given request.
// If the secret is empty, nothing is done.
func addJwtHeader(req *http.Request, jwtSecret string) error {
if jwtSecret == "" {
return nil
}
// Create a new token object, specifying signing method and the claims
// you would like it to contain.
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"iss": "arangodb",
"server_id": "foo",
})

// Sign and get the complete encoded token as a string using the secret
signedToken, err := token.SignedString([]byte(jwtSecret))
if err != nil {
return maskAny(err)
}

req.Header.Set("Authorization", "bearer "+signedToken)
return nil
}
4 changes: 4 additions & 0 deletions vendor/github.com/dgrijalva/jwt-go/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions vendor/github.com/dgrijalva/jwt-go/.travis.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions vendor/github.com/dgrijalva/jwt-go/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading