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  }