github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/cli/haproxy.go (about)

     1  // Copyright 2017 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package cli
    12  
    13  import (
    14  	"context"
    15  	"fmt"
    16  	"html/template"
    17  	"io"
    18  	"io/ioutil"
    19  	"os"
    20  	"regexp"
    21  	"sort"
    22  	"strings"
    23  
    24  	"github.com/cockroachdb/cockroach/pkg/base"
    25  	"github.com/cockroachdb/cockroach/pkg/cli/cliflags"
    26  	"github.com/cockroachdb/cockroach/pkg/kv/kvserver/kvserverpb"
    27  	"github.com/cockroachdb/cockroach/pkg/roachpb"
    28  	"github.com/cockroachdb/cockroach/pkg/server/serverpb"
    29  	"github.com/cockroachdb/cockroach/pkg/server/status/statuspb"
    30  	"github.com/cockroachdb/errors"
    31  	"github.com/spf13/cobra"
    32  	"github.com/spf13/pflag"
    33  )
    34  
    35  var haProxyPath string
    36  var haProxyLocality roachpb.Locality
    37  
    38  var genHAProxyCmd = &cobra.Command{
    39  	Use:   "haproxy",
    40  	Short: "generate haproxy.cfg for the connected cluster",
    41  	Long: `This command generates a minimal haproxy configuration file for the cluster
    42  reached through the client flags.
    43  The file is written to --out. Use "--out -" for stdout.
    44  
    45  The addresses used are those advertised by the nodes themselves. Make sure haproxy
    46  can resolve the hostnames in the configuration file, either by using full-qualified names, or
    47  running haproxy in the same network.
    48  
    49  Notes that have been decommissioned are excluded from the generated configuration.
    50  
    51  Nodes to include can be filtered by localities matching the '--locality' regular expression. eg:
    52    --locality=region=us-east                  # Nodes in region "us-east"
    53    --locality=region=us.*                     # Nodes in the US
    54    --locality=region=us.*,deployment=testing  # Nodes in the US AND in deployment tier "testing"
    55  
    56  A regular expression can be specified per locality tier and all specified tiers must match.
    57  The key (eg: 'region') must be fully specified, only values (eg: 'us-east1') can be regular expressions.
    58  An error is returned if no nodes match the locality filter.
    59  `,
    60  	Args: cobra.NoArgs,
    61  	RunE: MaybeDecorateGRPCError(runGenHAProxyCmd),
    62  }
    63  
    64  type haProxyNodeInfo struct {
    65  	NodeID   roachpb.NodeID
    66  	NodeAddr string
    67  	// The port on which health checks are performed.
    68  	CheckPort string
    69  	Locality  roachpb.Locality
    70  }
    71  
    72  func nodeStatusesToNodeInfos(nodes *serverpb.NodesResponse) []haProxyNodeInfo {
    73  	fs := pflag.NewFlagSet("haproxy", pflag.ContinueOnError)
    74  
    75  	httpAddr := ""
    76  	httpPort := base.DefaultHTTPPort
    77  	fs.Var(addrSetter{&httpAddr, &httpPort}, cliflags.ListenHTTPAddr.Name, "" /* usage */)
    78  	fs.Var(aliasStrVar{&httpPort}, cliflags.ListenHTTPPort.Name, "" /* usage */)
    79  
    80  	// Discard parsing output.
    81  	fs.SetOutput(ioutil.Discard)
    82  
    83  	nodeInfos := make([]haProxyNodeInfo, 0, len(nodes.Nodes))
    84  
    85  	// The response can present nodes in arbitrary order. We want them sorted.
    86  	nodeIDs := make([]int, 0, len(nodes.Nodes))
    87  	statusByID := make(map[roachpb.NodeID]statuspb.NodeStatus)
    88  	for _, status := range nodes.Nodes {
    89  		statusByID[status.Desc.NodeID] = status
    90  		nodeIDs = append(nodeIDs, int(status.Desc.NodeID))
    91  	}
    92  	sort.Ints(nodeIDs)
    93  
    94  	for _, inodeID := range nodeIDs {
    95  		nodeID := roachpb.NodeID(inodeID)
    96  		status := statusByID[nodeID]
    97  		liveness := nodes.LivenessByNodeID[nodeID]
    98  		switch liveness {
    99  		case kvserverpb.NodeLivenessStatus_DECOMMISSIONING:
   100  			fmt.Fprintf(stderr, "warning: node %d status is %s, excluding from haproxy configuration\n",
   101  				nodeID, liveness)
   102  			fallthrough
   103  		case kvserverpb.NodeLivenessStatus_DECOMMISSIONED:
   104  			continue
   105  		}
   106  
   107  		info := haProxyNodeInfo{
   108  			NodeID:   nodeID,
   109  			NodeAddr: status.Desc.Address.AddressField,
   110  			Locality: status.Desc.Locality,
   111  		}
   112  
   113  		httpPort = base.DefaultHTTPPort
   114  		// Iterate over the arguments until the ServerHTTPPort flag is found and
   115  		// parse the remainder of the arguments. This is done because Parse returns
   116  		// when it encounters an undefined flag and we do not want to define all
   117  		// possible flags.
   118  		//
   119  		// TODO(knz): this logic is horrendously broken and
   120  		// incorrect. Replace it.
   121  		for j, arg := range status.Args {
   122  			if strings.Contains(arg, cliflags.ListenHTTPPort.Name) ||
   123  				strings.Contains(arg, cliflags.ListenHTTPAddr.Name) {
   124  				_ = fs.Parse(status.Args[j:])
   125  				break
   126  			}
   127  		}
   128  
   129  		info.CheckPort = httpPort
   130  		nodeInfos = append(nodeInfos, info)
   131  	}
   132  
   133  	return nodeInfos
   134  }
   135  
   136  func localityMatches(locality roachpb.Locality, desired roachpb.Locality) (bool, error) {
   137  	for _, filterTier := range desired.Tiers {
   138  		// It's a little silly to recompile the regexp for each node, but not a big deal.
   139  		var b strings.Builder
   140  		b.WriteString("^")
   141  		b.WriteString(filterTier.Value)
   142  		b.WriteString("$")
   143  		re, err := regexp.Compile(b.String())
   144  		if err != nil {
   145  			return false, errors.Wrapf(err, "could not compile regular expression for %q", filterTier)
   146  		}
   147  
   148  		keyFound := false
   149  		for _, nodeTier := range locality.Tiers {
   150  			if filterTier.Key != nodeTier.Key {
   151  				continue
   152  			}
   153  
   154  			keyFound = true
   155  			if !re.MatchString(nodeTier.Value) {
   156  				// Mismatched tier value.
   157  				return false, nil
   158  			}
   159  
   160  			break
   161  		}
   162  
   163  		if !keyFound {
   164  			// Tier not found.
   165  			return false, nil
   166  		}
   167  	}
   168  
   169  	return true, nil
   170  }
   171  
   172  func filterByLocality(nodeInfos []haProxyNodeInfo) ([]haProxyNodeInfo, error) {
   173  	if len(haProxyLocality.Tiers) == 0 {
   174  		// No filter.
   175  		return nodeInfos, nil
   176  	}
   177  
   178  	result := make([]haProxyNodeInfo, 0)
   179  	availableLocalities := make(map[string]struct{})
   180  
   181  	for _, info := range nodeInfos {
   182  		l := info.Locality
   183  		if len(l.Tiers) == 0 {
   184  			continue
   185  		}
   186  
   187  		// Save seen locality.
   188  		availableLocalities[l.String()] = struct{}{}
   189  
   190  		matches, err := localityMatches(l, haProxyLocality)
   191  		if err != nil {
   192  			return nil, err
   193  		}
   194  
   195  		if matches {
   196  			result = append(result, info)
   197  		}
   198  	}
   199  
   200  	if len(result) == 0 {
   201  		seenLocalities := make([]string, len(availableLocalities))
   202  		i := 0
   203  		for l := range availableLocalities {
   204  			seenLocalities[i] = l
   205  			i++
   206  		}
   207  		sort.Strings(seenLocalities)
   208  		return nil, fmt.Errorf("no nodes match locality filter %s. Found localities: %v", haProxyLocality.String(), seenLocalities)
   209  	}
   210  
   211  	return result, nil
   212  }
   213  
   214  func runGenHAProxyCmd(cmd *cobra.Command, args []string) error {
   215  	ctx, cancel := context.WithCancel(context.Background())
   216  	defer cancel()
   217  
   218  	configTemplate, err := template.New("haproxy template").Parse(haProxyTemplate)
   219  	if err != nil {
   220  		return err
   221  	}
   222  
   223  	conn, _, finish, err := getClientGRPCConn(ctx, serverCfg)
   224  	if err != nil {
   225  		return err
   226  	}
   227  	defer finish()
   228  	c := serverpb.NewStatusClient(conn)
   229  
   230  	nodeStatuses, err := c.Nodes(ctx, &serverpb.NodesRequest{})
   231  	if err != nil {
   232  		return err
   233  	}
   234  
   235  	var w io.Writer
   236  	var f *os.File
   237  	if haProxyPath == "-" {
   238  		w = os.Stdout
   239  	} else if f, err = os.OpenFile(haProxyPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
   240  		return err
   241  	} else {
   242  		w = f
   243  	}
   244  
   245  	nodeInfos := nodeStatusesToNodeInfos(nodeStatuses)
   246  	filteredNodeInfos, err := filterByLocality(nodeInfos)
   247  	if err != nil {
   248  		return err
   249  	}
   250  
   251  	err = configTemplate.Execute(w, filteredNodeInfos)
   252  	if err != nil {
   253  		// Return earliest error, but still close the file.
   254  		_ = f.Close()
   255  		return err
   256  	}
   257  
   258  	if f != nil {
   259  		return f.Close()
   260  	}
   261  
   262  	return nil
   263  }
   264  
   265  const haProxyTemplate = `
   266  global
   267    maxconn 4096
   268  
   269  defaults
   270      mode                tcp
   271      # Timeout values should be configured for your specific use.
   272      # See: https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#4-timeout%20connect
   273      timeout connect     10s
   274      timeout client      1m
   275      timeout server      1m
   276      # TCP keep-alive on client side. Server already enables them.
   277      option              clitcpka
   278  
   279  listen psql
   280      bind :26257
   281      mode tcp
   282      balance roundrobin
   283      option httpchk GET /health?ready=1
   284  {{range .}}    server cockroach{{.NodeID}} {{.NodeAddr}} check port {{.CheckPort}}
   285  {{end}}
   286  `