github.com/unigraph-dev/dgraph@v1.1.1-0.20200923154953-8b52b426f765/compose/compose.go (about) 1 /* 2 * Copyright 2019 Dgraph Labs, Inc. and Contributors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package main 18 19 import ( 20 "fmt" 21 "io/ioutil" 22 "os" 23 "os/user" 24 25 "github.com/pkg/errors" 26 "github.com/spf13/cobra" 27 "github.com/spf13/pflag" 28 yaml "gopkg.in/yaml.v2" 29 30 "github.com/dgraph-io/dgraph/x" 31 ) 32 33 type stringMap map[string]string 34 35 type volume struct { 36 Type string 37 Source string 38 Target string 39 ReadOnly bool `yaml:"read_only"` 40 } 41 42 type service struct { 43 name string // not exported 44 Image string 45 ContainerName string `yaml:"container_name"` 46 Hostname string `yaml:",omitempty"` 47 Pid string `yaml:",omitempty"` 48 WorkingDir string `yaml:"working_dir,omitempty"` 49 DependsOn []string `yaml:"depends_on,omitempty"` 50 Labels stringMap `yaml:",omitempty"` 51 Environment []string `yaml:",omitempty"` 52 Ports []string `yaml:",omitempty"` 53 Volumes []volume `yaml:",omitempty"` 54 TmpFS []string `yaml:",omitempty"` 55 User string `yaml:",omitempty"` 56 Command string `yaml:",omitempty"` 57 } 58 59 type composeConfig struct { 60 Version string 61 Services map[string]service 62 Volumes map[string]stringMap 63 } 64 65 type options struct { 66 NumZeros int 67 NumAlphas int 68 NumReplicas int 69 LruSizeMB int 70 AclSecret string 71 DataDir string 72 DataVol bool 73 TmpFS bool 74 UserOwnership bool 75 Jaeger bool 76 Metrics bool 77 PortOffset int 78 Verbosity int 79 OutFile string 80 LocalBin bool 81 Tag string 82 WhiteList bool 83 Ratel bool 84 RatelPort int 85 } 86 87 var opts options 88 89 const ( 90 zeroBasePort int = 5080 // HTTP=6080 91 alphaBasePort int = 7080 // HTTP=8080, GRPC=9080 92 ) 93 94 func name(prefix string, idx int) string { 95 return fmt.Sprintf("%s%d", prefix, idx) 96 } 97 98 func toExposedPort(i int) string { 99 return fmt.Sprintf("%d:%d", i, i) 100 } 101 102 func getOffset(idx int) int { 103 if idx == 1 { 104 return 0 105 } 106 return idx 107 } 108 109 func initService(basename string, idx, grpcPort int) service { 110 var svc service 111 112 svc.name = name(basename, idx) 113 svc.Image = "dgraph/dgraph:" + opts.Tag 114 svc.ContainerName = svc.name 115 svc.WorkingDir = fmt.Sprintf("/data/%s", svc.name) 116 if idx > 1 { 117 svc.DependsOn = append(svc.DependsOn, name(basename, idx-1)) 118 } 119 svc.Labels = map[string]string{"cluster": "test"} 120 121 svc.Ports = []string{ 122 toExposedPort(grpcPort), 123 toExposedPort(grpcPort + 1000), // http port 124 } 125 126 svc.Volumes = append(svc.Volumes, volume{ 127 Type: "bind", 128 Source: "$GOPATH/bin", 129 Target: "/gobin", 130 ReadOnly: true, 131 }) 132 133 switch { 134 case opts.DataVol: 135 svc.Volumes = append(svc.Volumes, volume{ 136 Type: "volume", 137 Source: "data", 138 Target: "/data", 139 }) 140 case opts.DataDir != "": 141 svc.Volumes = append(svc.Volumes, volume{ 142 Type: "bind", 143 Source: opts.DataDir, 144 Target: "/data", 145 }) 146 default: 147 // no data volume 148 } 149 150 svc.Command = "dgraph" 151 if opts.LocalBin { 152 svc.Command = "/gobin/dgraph" 153 } 154 if opts.UserOwnership { 155 user, err := user.Current() 156 if err != nil { 157 x.CheckfNoTrace(errors.Wrap(err, "unable to get current user")) 158 } 159 svc.User = fmt.Sprintf("${UID:-%s}", user.Uid) 160 svc.WorkingDir = fmt.Sprintf("/working/%s", svc.name) 161 svc.Command += fmt.Sprintf(" --cwd=/data/%s", svc.name) 162 } 163 svc.Command += " " + basename 164 if opts.Jaeger { 165 svc.Command += " --jaeger.collector=http://jaeger:14268" 166 } 167 168 return svc 169 } 170 171 func getZero(idx int) service { 172 basename := "zero" 173 basePort := zeroBasePort + opts.PortOffset 174 grpcPort := basePort + getOffset(idx) 175 176 svc := initService(basename, idx, grpcPort) 177 178 if opts.TmpFS { 179 svc.TmpFS = append(svc.TmpFS, fmt.Sprintf("/data/%s/zw", svc.name)) 180 } 181 182 svc.Command += fmt.Sprintf(" -o %d --idx=%d", opts.PortOffset+getOffset(idx), idx) 183 svc.Command += fmt.Sprintf(" --my=%s:%d", svc.name, grpcPort) 184 if opts.NumAlphas > 1 { 185 svc.Command += fmt.Sprintf(" --replicas=%d", opts.NumReplicas) 186 } 187 svc.Command += fmt.Sprintf(" --logtostderr -v=%d", opts.Verbosity) 188 if idx == 1 { 189 svc.Command += fmt.Sprintf(" --bindall") 190 } else { 191 svc.Command += fmt.Sprintf(" --peer=%s:%d", name(basename, 1), basePort) 192 } 193 194 return svc 195 } 196 197 func getAlpha(idx int) service { 198 basename := "alpha" 199 internalPort := alphaBasePort + opts.PortOffset + getOffset(idx) 200 grpcPort := internalPort + 1000 201 202 svc := initService(basename, idx, grpcPort) 203 204 if opts.TmpFS { 205 svc.TmpFS = append(svc.TmpFS, fmt.Sprintf("/data/%s/w", svc.name)) 206 } 207 208 svc.Command += fmt.Sprintf(" -o %d", opts.PortOffset+getOffset(idx)) 209 svc.Command += fmt.Sprintf(" --my=%s:%d", svc.name, internalPort) 210 svc.Command += fmt.Sprintf(" --lru_mb=%d", opts.LruSizeMB) 211 svc.Command += fmt.Sprintf(" --zero=zero1:%d", zeroBasePort+opts.PortOffset) 212 svc.Command += fmt.Sprintf(" --logtostderr -v=%d", opts.Verbosity) 213 svc.Command += fmt.Sprintf(" --idx=%d", idx) 214 if opts.WhiteList { 215 svc.Command += " --whitelist=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16" 216 } 217 if opts.AclSecret != "" { 218 svc.Command += " --acl_secret_file=/secret/hmac --acl_access_ttl 3s --acl_cache_ttl 5s" 219 svc.Volumes = append(svc.Volumes, volume{ 220 Type: "bind", 221 Source: opts.AclSecret, 222 Target: "/secret/hmac", 223 ReadOnly: true, 224 }) 225 } 226 227 return svc 228 } 229 230 func getJaeger() service { 231 svc := service{ 232 Image: "jaegertracing/all-in-one:latest", 233 ContainerName: "jaeger", 234 WorkingDir: "/working/jaeger", 235 Ports: []string{ 236 toExposedPort(14268), 237 toExposedPort(16686), 238 }, 239 Environment: []string{ 240 "SPAN_STORAGE_TYPE=badger", 241 }, 242 Command: "--badger.ephemeral=false" + 243 " --badger.directory-key /working/jaeger" + 244 " --badger.directory-value /working/jaeger", 245 } 246 return svc 247 } 248 249 func getRatel() service { 250 portFlag := "" 251 if opts.RatelPort != 8000 { 252 portFlag = fmt.Sprintf(" -port=%d", opts.RatelPort) 253 } 254 svc := service{ 255 Image: "dgraph/dgraph:" + opts.Tag, 256 ContainerName: "ratel", 257 Ports: []string{ 258 toExposedPort(opts.RatelPort), 259 }, 260 Command: "dgraph-ratel" + portFlag, 261 } 262 return svc 263 } 264 265 func addMetrics(cfg *composeConfig) { 266 cfg.Volumes["prometheus-volume"] = stringMap{} 267 cfg.Volumes["grafana-volume"] = stringMap{} 268 269 cfg.Services["node-exporter"] = service{ 270 Image: "quay.io/prometheus/node-exporter", 271 ContainerName: "node-exporter", 272 Pid: "host", 273 WorkingDir: "/working/jaeger", 274 Volumes: []volume{{ 275 Type: "bind", 276 Source: "/", 277 Target: "/host", 278 ReadOnly: true, 279 }}, 280 } 281 282 cfg.Services["prometheus"] = service{ 283 Image: "prom/prometheus", 284 ContainerName: "prometheus", 285 Hostname: "prometheus", 286 Ports: []string{ 287 toExposedPort(9090), 288 }, 289 Volumes: []volume{ 290 { 291 Type: "volume", 292 Source: "prometheus-volume", 293 Target: "/prometheus", 294 }, 295 { 296 Type: "bind", 297 Source: "$GOPATH/src/github.com/dgraph-io/dgraph/compose/prometheus.yml", 298 Target: "/etc/prometheus/prometheus.yml", 299 ReadOnly: true, 300 }, 301 }, 302 } 303 304 cfg.Services["grafana"] = service{ 305 Image: "grafana/grafana", 306 ContainerName: "grafana", 307 Hostname: "grafana", 308 Ports: []string{ 309 toExposedPort(3000), 310 }, 311 Environment: []string{ 312 // Skip login 313 "GF_AUTH_ANONYMOUS_ENABLED=true", 314 "GF_AUTH_ANONYMOUS_ORG_ROLE=Admin", 315 }, 316 Volumes: []volume{{ 317 Type: "volume", 318 Source: "grafana-volume", 319 Target: "/var/lib/grafana", 320 }}, 321 } 322 } 323 324 func warning(str string) { 325 fmt.Fprintf(os.Stderr, "compose: %v\n", str) 326 } 327 328 func fatal(err error) { 329 fmt.Fprintf(os.Stderr, "compose: %v\n", err) 330 os.Exit(1) 331 } 332 333 func main() { 334 var cmd = &cobra.Command{ 335 Use: "compose", 336 Short: "docker-compose config file generator for dgraph", 337 Long: "Dynamically generate a docker-compose.yml file for running a dgraph cluster.", 338 Example: "$ compose -z=3 -a=3", 339 Run: func(cmd *cobra.Command, args []string) { 340 // dummy to get "Usage:" template in Usage() output. 341 }, 342 } 343 344 cmd.PersistentFlags().IntVarP(&opts.NumZeros, "num_zeros", "z", 3, 345 "number of zeros in dgraph cluster") 346 cmd.PersistentFlags().IntVarP(&opts.NumAlphas, "num_alphas", "a", 3, 347 "number of alphas in dgraph cluster") 348 cmd.PersistentFlags().IntVarP(&opts.NumReplicas, "num_replicas", "r", 3, 349 "number of alpha replicas in dgraph cluster") 350 cmd.PersistentFlags().IntVar(&opts.LruSizeMB, "lru_mb", 1024, 351 "approximate size of LRU cache") 352 cmd.PersistentFlags().BoolVar(&opts.DataVol, "data_vol", false, 353 "mount a docker volume as /data in containers") 354 cmd.PersistentFlags().StringVarP(&opts.DataDir, "data_dir", "d", "", 355 "mount a host directory as /data in containers") 356 cmd.PersistentFlags().StringVar(&opts.AclSecret, "acl_secret", "", 357 "enable ACL feature with specified HMAC secret file") 358 cmd.PersistentFlags().BoolVarP(&opts.UserOwnership, "user", "u", false, 359 "run as the current user rather than root") 360 cmd.PersistentFlags().BoolVar(&opts.TmpFS, "tmpfs", false, 361 "store w and zw directories on a tmpfs filesystem") 362 cmd.PersistentFlags().BoolVarP(&opts.Jaeger, "jaeger", "j", false, 363 "include jaeger service") 364 cmd.PersistentFlags().BoolVarP(&opts.Metrics, "metrics", "m", false, 365 "include metrics (prometheus, grafana) services") 366 cmd.PersistentFlags().IntVarP(&opts.PortOffset, "port_offset", "o", 100, 367 "port offset for alpha and, if not 100, zero as well") 368 cmd.PersistentFlags().IntVarP(&opts.Verbosity, "verbosity", "v", 2, 369 "glog verbosity level") 370 cmd.PersistentFlags().StringVarP(&opts.OutFile, "out", "O", 371 "./docker-compose.yml", "name of output file") 372 cmd.PersistentFlags().BoolVarP(&opts.LocalBin, "local", "l", true, 373 "use locally-compiled binary if true, otherwise use binary from docker container") 374 cmd.PersistentFlags().StringVarP(&opts.Tag, "tag", "t", "latest", 375 "Docker tag for dgraph/dgraph image. Requires -l=false to use binary from docker container.") 376 cmd.PersistentFlags().BoolVarP(&opts.WhiteList, "whitelist", "w", false, 377 "include a whitelist if true") 378 cmd.PersistentFlags().BoolVar(&opts.Ratel, "ratel", false, 379 "include ratel service") 380 cmd.PersistentFlags().IntVar(&opts.RatelPort, "ratel_port", 8000, 381 "Port to expose Ratel service") 382 383 err := cmd.ParseFlags(os.Args) 384 if err != nil { 385 if err == pflag.ErrHelp { 386 _ = cmd.Usage() 387 os.Exit(0) 388 } 389 fatal(err) 390 } 391 392 // Do some sanity checks. 393 if opts.NumZeros < 1 || opts.NumZeros > 99 { 394 fatal(errors.Errorf("number of zeros must be 1-99")) 395 } 396 if opts.NumAlphas < 1 || opts.NumAlphas > 99 { 397 fatal(errors.Errorf("number of alphas must be 1-99")) 398 } 399 if opts.NumReplicas%2 == 0 { 400 fatal(errors.Errorf("number of replicas must be odd")) 401 } 402 if opts.LruSizeMB < 1024 { 403 fatal(errors.Errorf("LRU cache size must be >= 1024 MB")) 404 } 405 if opts.DataVol && opts.DataDir != "" { 406 fatal(errors.Errorf("only one of --data_vol and --data_dir may be used at a time")) 407 } 408 if opts.UserOwnership && opts.DataDir == "" { 409 fatal(errors.Errorf("--user option requires --data_dir=<path>")) 410 } 411 if cmd.Flags().Changed("ratel_port") && !opts.Ratel { 412 fatal(errors.Errorf("--ratel_port option requires --ratel")) 413 } 414 415 services := make(map[string]service) 416 417 for i := 1; i <= opts.NumZeros; i++ { 418 svc := getZero(i) 419 services[svc.name] = svc 420 } 421 422 for i := 1; i <= opts.NumAlphas; i++ { 423 svc := getAlpha(i) 424 services[svc.name] = svc 425 } 426 427 cfg := composeConfig{ 428 Version: "3.5", 429 Services: services, 430 Volumes: make(map[string]stringMap), 431 } 432 433 if opts.DataVol { 434 cfg.Volumes["data"] = stringMap{} 435 } 436 437 if opts.Jaeger { 438 services["jaeger"] = getJaeger() 439 } 440 441 if opts.Ratel { 442 services["ratel"] = getRatel() 443 } 444 445 if opts.Metrics { 446 addMetrics(&cfg) 447 } 448 449 yml, err := yaml.Marshal(cfg) 450 x.CheckfNoTrace(err) 451 452 doc := fmt.Sprintf("# Auto-generated with: %v\n#\n", os.Args[:]) 453 if opts.UserOwnership { 454 doc += fmt.Sprint("# NOTE: Env var UID must be exported by the shell\n#\n") 455 } 456 doc += fmt.Sprintf("%s", yml) 457 if opts.OutFile == "-" { 458 _, _ = fmt.Printf("%s", doc) 459 } else { 460 _, _ = fmt.Fprintf(os.Stderr, "Writing file: %s\n", opts.OutFile) 461 err = ioutil.WriteFile(opts.OutFile, []byte(doc), 0644) 462 if err != nil { 463 fatal(errors.Errorf("unable to write file: %v", err)) 464 } 465 } 466 }