1package api 2 3import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "strconv" 9 "strings" 10 11 "github.com/mitchellh/mapstructure" 12) 13 14const ( 15 ServiceDefaults string = "service-defaults" 16 ProxyDefaults string = "proxy-defaults" 17 ServiceRouter string = "service-router" 18 ServiceSplitter string = "service-splitter" 19 ServiceResolver string = "service-resolver" 20 IngressGateway string = "ingress-gateway" 21 TerminatingGateway string = "terminating-gateway" 22 23 ProxyConfigGlobal string = "global" 24) 25 26type ConfigEntry interface { 27 GetKind() string 28 GetName() string 29 GetCreateIndex() uint64 30 GetModifyIndex() uint64 31} 32 33type MeshGatewayMode string 34 35const ( 36 // MeshGatewayModeDefault represents no specific mode and should 37 // be used to indicate that a different layer of the configuration 38 // chain should take precedence 39 MeshGatewayModeDefault MeshGatewayMode = "" 40 41 // MeshGatewayModeNone represents that the Upstream Connect connections 42 // should be direct and not flow through a mesh gateway. 43 MeshGatewayModeNone MeshGatewayMode = "none" 44 45 // MeshGatewayModeLocal represents that the Upstrea Connect connections 46 // should be made to a mesh gateway in the local datacenter. This is 47 MeshGatewayModeLocal MeshGatewayMode = "local" 48 49 // MeshGatewayModeRemote represents that the Upstream Connect connections 50 // should be made to a mesh gateway in a remote datacenter. 51 MeshGatewayModeRemote MeshGatewayMode = "remote" 52) 53 54// MeshGatewayConfig controls how Mesh Gateways are used for upstream Connect 55// services 56type MeshGatewayConfig struct { 57 // Mode is the mode that should be used for the upstream connection. 58 Mode MeshGatewayMode `json:",omitempty"` 59} 60 61// ExposeConfig describes HTTP paths to expose through Envoy outside of Connect. 62// Users can expose individual paths and/or all HTTP/GRPC paths for checks. 63type ExposeConfig struct { 64 // Checks defines whether paths associated with Consul checks will be exposed. 65 // This flag triggers exposing all HTTP and GRPC check paths registered for the service. 66 Checks bool `json:",omitempty"` 67 68 // Paths is the list of paths exposed through the proxy. 69 Paths []ExposePath `json:",omitempty"` 70} 71 72type ExposePath struct { 73 // ListenerPort defines the port of the proxy's listener for exposed paths. 74 ListenerPort int `json:",omitempty" alias:"listener_port"` 75 76 // Path is the path to expose through the proxy, ie. "/metrics." 77 Path string `json:",omitempty"` 78 79 // LocalPathPort is the port that the service is listening on for the given path. 80 LocalPathPort int `json:",omitempty" alias:"local_path_port"` 81 82 // Protocol describes the upstream's service protocol. 83 // Valid values are "http" and "http2", defaults to "http" 84 Protocol string `json:",omitempty"` 85 86 // ParsedFromCheck is set if this path was parsed from a registered check 87 ParsedFromCheck bool 88} 89 90type ServiceConfigEntry struct { 91 Kind string 92 Name string 93 Namespace string `json:",omitempty"` 94 Protocol string `json:",omitempty"` 95 MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"` 96 Expose ExposeConfig `json:",omitempty"` 97 ExternalSNI string `json:",omitempty" alias:"external_sni"` 98 CreateIndex uint64 99 ModifyIndex uint64 100} 101 102func (s *ServiceConfigEntry) GetKind() string { 103 return s.Kind 104} 105 106func (s *ServiceConfigEntry) GetName() string { 107 return s.Name 108} 109 110func (s *ServiceConfigEntry) GetCreateIndex() uint64 { 111 return s.CreateIndex 112} 113 114func (s *ServiceConfigEntry) GetModifyIndex() uint64 { 115 return s.ModifyIndex 116} 117 118type ProxyConfigEntry struct { 119 Kind string 120 Name string 121 Namespace string `json:",omitempty"` 122 Config map[string]interface{} `json:",omitempty"` 123 MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"` 124 Expose ExposeConfig `json:",omitempty"` 125 CreateIndex uint64 126 ModifyIndex uint64 127} 128 129func (p *ProxyConfigEntry) GetKind() string { 130 return p.Kind 131} 132 133func (p *ProxyConfigEntry) GetName() string { 134 return p.Name 135} 136 137func (p *ProxyConfigEntry) GetCreateIndex() uint64 { 138 return p.CreateIndex 139} 140 141func (p *ProxyConfigEntry) GetModifyIndex() uint64 { 142 return p.ModifyIndex 143} 144 145func makeConfigEntry(kind, name string) (ConfigEntry, error) { 146 switch kind { 147 case ServiceDefaults: 148 return &ServiceConfigEntry{Kind: kind, Name: name}, nil 149 case ProxyDefaults: 150 return &ProxyConfigEntry{Kind: kind, Name: name}, nil 151 case ServiceRouter: 152 return &ServiceRouterConfigEntry{Kind: kind, Name: name}, nil 153 case ServiceSplitter: 154 return &ServiceSplitterConfigEntry{Kind: kind, Name: name}, nil 155 case ServiceResolver: 156 return &ServiceResolverConfigEntry{Kind: kind, Name: name}, nil 157 case IngressGateway: 158 return &IngressGatewayConfigEntry{Kind: kind, Name: name}, nil 159 case TerminatingGateway: 160 return &TerminatingGatewayConfigEntry{Kind: kind, Name: name}, nil 161 default: 162 return nil, fmt.Errorf("invalid config entry kind: %s", kind) 163 } 164} 165 166func MakeConfigEntry(kind, name string) (ConfigEntry, error) { 167 return makeConfigEntry(kind, name) 168} 169 170// DecodeConfigEntry will decode the result of using json.Unmarshal of a config 171// entry into a map[string]interface{}. 172// 173// Important caveats: 174// 175// - This will NOT work if the map[string]interface{} was produced using HCL 176// decoding as that requires more extensive parsing to work around the issues 177// with map[string][]interface{} that arise. 178// 179// - This will only decode fields using their camel case json field 180// representations. 181func DecodeConfigEntry(raw map[string]interface{}) (ConfigEntry, error) { 182 var entry ConfigEntry 183 184 kindVal, ok := raw["Kind"] 185 if !ok { 186 kindVal, ok = raw["kind"] 187 } 188 if !ok { 189 return nil, fmt.Errorf("Payload does not contain a kind/Kind key at the top level") 190 } 191 192 if kindStr, ok := kindVal.(string); ok { 193 newEntry, err := makeConfigEntry(kindStr, "") 194 if err != nil { 195 return nil, err 196 } 197 entry = newEntry 198 } else { 199 return nil, fmt.Errorf("Kind value in payload is not a string") 200 } 201 202 decodeConf := &mapstructure.DecoderConfig{ 203 DecodeHook: mapstructure.StringToTimeDurationHookFunc(), 204 Result: &entry, 205 WeaklyTypedInput: true, 206 } 207 208 decoder, err := mapstructure.NewDecoder(decodeConf) 209 if err != nil { 210 return nil, err 211 } 212 213 return entry, decoder.Decode(raw) 214} 215 216func DecodeConfigEntryFromJSON(data []byte) (ConfigEntry, error) { 217 var raw map[string]interface{} 218 if err := json.Unmarshal(data, &raw); err != nil { 219 return nil, err 220 } 221 222 return DecodeConfigEntry(raw) 223} 224 225func decodeConfigEntrySlice(raw []map[string]interface{}) ([]ConfigEntry, error) { 226 var entries []ConfigEntry 227 for _, rawEntry := range raw { 228 entry, err := DecodeConfigEntry(rawEntry) 229 if err != nil { 230 return nil, err 231 } 232 entries = append(entries, entry) 233 } 234 return entries, nil 235} 236 237// ConfigEntries can be used to query the Config endpoints 238type ConfigEntries struct { 239 c *Client 240} 241 242// Config returns a handle to the Config endpoints 243func (c *Client) ConfigEntries() *ConfigEntries { 244 return &ConfigEntries{c} 245} 246 247func (conf *ConfigEntries) Get(kind string, name string, q *QueryOptions) (ConfigEntry, *QueryMeta, error) { 248 if kind == "" || name == "" { 249 return nil, nil, fmt.Errorf("Both kind and name parameters must not be empty") 250 } 251 252 entry, err := makeConfigEntry(kind, name) 253 if err != nil { 254 return nil, nil, err 255 } 256 257 r := conf.c.newRequest("GET", fmt.Sprintf("/v1/config/%s/%s", kind, name)) 258 r.setQueryOptions(q) 259 rtt, resp, err := requireOK(conf.c.doRequest(r)) 260 if err != nil { 261 return nil, nil, err 262 } 263 264 defer resp.Body.Close() 265 266 qm := &QueryMeta{} 267 parseQueryMeta(resp, qm) 268 qm.RequestTime = rtt 269 270 if err := decodeBody(resp, entry); err != nil { 271 return nil, nil, err 272 } 273 274 return entry, qm, nil 275} 276 277func (conf *ConfigEntries) List(kind string, q *QueryOptions) ([]ConfigEntry, *QueryMeta, error) { 278 if kind == "" { 279 return nil, nil, fmt.Errorf("The kind parameter must not be empty") 280 } 281 282 r := conf.c.newRequest("GET", fmt.Sprintf("/v1/config/%s", kind)) 283 r.setQueryOptions(q) 284 rtt, resp, err := requireOK(conf.c.doRequest(r)) 285 if err != nil { 286 return nil, nil, err 287 } 288 289 defer resp.Body.Close() 290 291 qm := &QueryMeta{} 292 parseQueryMeta(resp, qm) 293 qm.RequestTime = rtt 294 295 var raw []map[string]interface{} 296 if err := decodeBody(resp, &raw); err != nil { 297 return nil, nil, err 298 } 299 300 entries, err := decodeConfigEntrySlice(raw) 301 if err != nil { 302 return nil, nil, err 303 } 304 305 return entries, qm, nil 306} 307 308func (conf *ConfigEntries) Set(entry ConfigEntry, w *WriteOptions) (bool, *WriteMeta, error) { 309 return conf.set(entry, nil, w) 310} 311 312func (conf *ConfigEntries) CAS(entry ConfigEntry, index uint64, w *WriteOptions) (bool, *WriteMeta, error) { 313 return conf.set(entry, map[string]string{"cas": strconv.FormatUint(index, 10)}, w) 314} 315 316func (conf *ConfigEntries) set(entry ConfigEntry, params map[string]string, w *WriteOptions) (bool, *WriteMeta, error) { 317 r := conf.c.newRequest("PUT", "/v1/config") 318 r.setWriteOptions(w) 319 for param, value := range params { 320 r.params.Set(param, value) 321 } 322 r.obj = entry 323 rtt, resp, err := requireOK(conf.c.doRequest(r)) 324 if err != nil { 325 return false, nil, err 326 } 327 defer resp.Body.Close() 328 329 var buf bytes.Buffer 330 if _, err := io.Copy(&buf, resp.Body); err != nil { 331 return false, nil, fmt.Errorf("Failed to read response: %v", err) 332 } 333 res := strings.Contains(buf.String(), "true") 334 335 wm := &WriteMeta{RequestTime: rtt} 336 return res, wm, nil 337} 338 339func (conf *ConfigEntries) Delete(kind string, name string, w *WriteOptions) (*WriteMeta, error) { 340 if kind == "" || name == "" { 341 return nil, fmt.Errorf("Both kind and name parameters must not be empty") 342 } 343 344 r := conf.c.newRequest("DELETE", fmt.Sprintf("/v1/config/%s/%s", kind, name)) 345 r.setWriteOptions(w) 346 rtt, resp, err := requireOK(conf.c.doRequest(r)) 347 if err != nil { 348 return nil, err 349 } 350 resp.Body.Close() 351 wm := &WriteMeta{RequestTime: rtt} 352 return wm, nil 353} 354