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 `