1package structs 2 3import ( 4 "encoding/json" 5 "fmt" 6 "net" 7 8 "github.com/hashicorp/consul/api" 9 "github.com/hashicorp/consul/lib" 10) 11 12const ( 13 defaultExposeProtocol = "http" 14) 15 16var allowedExposeProtocols = map[string]bool{"http": true, "http2": true} 17 18type MeshGatewayMode string 19 20const ( 21 // MeshGatewayModeDefault represents no specific mode and should 22 // be used to indicate that a different layer of the configuration 23 // chain should take precedence 24 MeshGatewayModeDefault MeshGatewayMode = "" 25 26 // MeshGatewayModeNone represents that the Upstream Connect connections 27 // should be direct and not flow through a mesh gateway. 28 MeshGatewayModeNone MeshGatewayMode = "none" 29 30 // MeshGatewayModeLocal represents that the Upstream Connect connections 31 // should be made to a mesh gateway in the local datacenter. 32 MeshGatewayModeLocal MeshGatewayMode = "local" 33 34 // MeshGatewayModeRemote represents that the Upstream Connect connections 35 // should be made to a mesh gateway in a remote datacenter. 36 MeshGatewayModeRemote MeshGatewayMode = "remote" 37) 38 39const ( 40 // TODO (freddy) Should we have a TopologySourceMixed when there is a mix of proxy reg and tproxy? 41 // Currently we label as proxy-registration if ANY instance has the explicit upstream definition. 42 // TopologySourceRegistration is used to label upstreams or downstreams from explicit upstream definitions 43 TopologySourceRegistration = "proxy-registration" 44 45 // TopologySourceSpecificIntention is used to label upstreams or downstreams from specific intentions 46 TopologySourceSpecificIntention = "specific-intention" 47 48 // TopologySourceWildcardIntention is used to label upstreams or downstreams from wildcard intentions 49 TopologySourceWildcardIntention = "wildcard-intention" 50 51 // TopologySourceDefaultAllow is used to label upstreams or downstreams from default allow ACL policy 52 TopologySourceDefaultAllow = "default-allow" 53) 54 55// MeshGatewayConfig controls how Mesh Gateways are configured and used 56// This is a struct to allow for future additions without having more free-hanging 57// configuration items all over the place 58type MeshGatewayConfig struct { 59 // The Mesh Gateway routing mode 60 Mode MeshGatewayMode `json:",omitempty"` 61} 62 63func (c *MeshGatewayConfig) IsZero() bool { 64 zeroVal := MeshGatewayConfig{} 65 return *c == zeroVal 66} 67 68func (base *MeshGatewayConfig) OverlayWith(overlay MeshGatewayConfig) MeshGatewayConfig { 69 out := *base 70 if overlay.Mode != MeshGatewayModeDefault { 71 out.Mode = overlay.Mode 72 } 73 return out 74} 75 76func ValidateMeshGatewayMode(mode string) (MeshGatewayMode, error) { 77 switch MeshGatewayMode(mode) { 78 case MeshGatewayModeNone: 79 return MeshGatewayModeNone, nil 80 case MeshGatewayModeDefault: 81 return MeshGatewayModeDefault, nil 82 case MeshGatewayModeLocal: 83 return MeshGatewayModeLocal, nil 84 case MeshGatewayModeRemote: 85 return MeshGatewayModeRemote, nil 86 default: 87 return MeshGatewayModeDefault, fmt.Errorf("Invalid Mesh Gateway Mode: %q", mode) 88 } 89} 90 91func (c *MeshGatewayConfig) ToAPI() api.MeshGatewayConfig { 92 return api.MeshGatewayConfig{Mode: api.MeshGatewayMode(c.Mode)} 93} 94 95type ProxyMode string 96 97const ( 98 // ProxyModeDefault represents no specific mode and should 99 // be used to indicate that a different layer of the configuration 100 // chain should take precedence 101 ProxyModeDefault ProxyMode = "" 102 103 // ProxyModeTransparent represents that inbound and outbound application 104 // traffic is being captured and redirected through the proxy. 105 ProxyModeTransparent ProxyMode = "transparent" 106 107 // ProxyModeDirect represents that the proxy's listeners must be dialed directly 108 // by the local application and other proxies. 109 ProxyModeDirect ProxyMode = "direct" 110) 111 112func ValidateProxyMode(mode string) (ProxyMode, error) { 113 switch ProxyMode(mode) { 114 case ProxyModeDefault: 115 return ProxyModeDefault, nil 116 case ProxyModeDirect: 117 return ProxyModeDirect, nil 118 case ProxyModeTransparent: 119 return ProxyModeTransparent, nil 120 default: 121 return ProxyModeDefault, fmt.Errorf("Invalid Proxy Mode: %q", mode) 122 } 123} 124 125type TransparentProxyConfig struct { 126 // The port of the listener where outbound application traffic is being redirected to. 127 OutboundListenerPort int `json:",omitempty" alias:"outbound_listener_port"` 128 129 // DialedDirectly indicates whether transparent proxies can dial this proxy instance directly. 130 // The discovery chain is not considered when dialing a service instance directly. 131 // This setting is useful when addressing stateful services, such as a database cluster with a leader node. 132 DialedDirectly bool `json:",omitempty" alias:"dialed_directly"` 133} 134 135func (c TransparentProxyConfig) ToAPI() *api.TransparentProxyConfig { 136 if c.IsZero() { 137 return nil 138 } 139 return &api.TransparentProxyConfig{ 140 OutboundListenerPort: c.OutboundListenerPort, 141 DialedDirectly: c.DialedDirectly, 142 } 143} 144 145func (c *TransparentProxyConfig) IsZero() bool { 146 zeroVal := TransparentProxyConfig{} 147 return *c == zeroVal 148} 149 150// ConnectProxyConfig describes the configuration needed for any proxy managed 151// or unmanaged. It describes a single logical service's listener and optionally 152// upstreams and sidecar-related config for a single instance. To describe a 153// centralized proxy that routed traffic for multiple services, a different one 154// of these would be needed for each, sharing the same LogicalProxyID. 155type ConnectProxyConfig struct { 156 // DestinationServiceName is required and is the name of the service to accept 157 // traffic for. 158 DestinationServiceName string `json:",omitempty" alias:"destination_service_name"` 159 160 // DestinationServiceID is optional and should only be specified for 161 // "side-car" style proxies where the proxy is in front of just a single 162 // instance of the service. It should be set to the service ID of the instance 163 // being represented which must be registered to the same agent. It's valid to 164 // provide a service ID that does not yet exist to avoid timing issues when 165 // bootstrapping a service with a proxy. 166 DestinationServiceID string `json:",omitempty" alias:"destination_service_id"` 167 168 // LocalServiceAddress is the address of the local service instance. It is 169 // optional and should only be specified for "side-car" style proxies. It will 170 // default to 127.0.0.1 if the proxy is a "side-car" (DestinationServiceID is 171 // set) but otherwise will be ignored. 172 LocalServiceAddress string `json:",omitempty" alias:"local_service_address"` 173 174 // LocalServicePort is the port of the local service instance. It is optional 175 // and should only be specified for "side-car" style proxies. It will default 176 // to the registered port for the instance if the proxy is a "side-car" 177 // (DestinationServiceID is set) but otherwise will be ignored. 178 LocalServicePort int `json:",omitempty" alias:"local_service_port"` 179 180 // LocalServiceSocketPath is the socket of the local service instance. It is optional 181 // and should only be specified for "side-car" style proxies. 182 LocalServiceSocketPath string `json:",omitempty" alias:"local_service_socket_path"` 183 184 // Mode represents how the proxy's inbound and upstream listeners are dialed. 185 Mode ProxyMode 186 187 // Config is the arbitrary configuration data provided with the proxy 188 // registration. 189 Config map[string]interface{} `json:",omitempty" bexpr:"-"` 190 191 // Upstreams describes any upstream dependencies the proxy instance should 192 // setup. 193 Upstreams Upstreams `json:",omitempty"` 194 195 // MeshGateway defines the mesh gateway configuration for this upstream 196 MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"` 197 198 // Expose defines whether checks or paths are exposed through the proxy 199 Expose ExposeConfig `json:",omitempty"` 200 201 // TransparentProxy defines configuration for when the proxy is in 202 // transparent mode. 203 TransparentProxy TransparentProxyConfig `json:",omitempty" alias:"transparent_proxy"` 204} 205 206func (t *ConnectProxyConfig) UnmarshalJSON(data []byte) (err error) { 207 type Alias ConnectProxyConfig 208 aux := &struct { 209 DestinationServiceNameSnake string `json:"destination_service_name"` 210 DestinationServiceIDSnake string `json:"destination_service_id"` 211 LocalServiceAddressSnake string `json:"local_service_address"` 212 LocalServicePortSnake int `json:"local_service_port"` 213 LocalServiceSocketPathSnake string `json:"local_service_socket_path"` 214 MeshGatewaySnake MeshGatewayConfig `json:"mesh_gateway"` 215 TransparentProxySnake TransparentProxyConfig `json:"transparent_proxy"` 216 *Alias 217 }{ 218 Alias: (*Alias)(t), 219 } 220 if err = lib.UnmarshalJSON(data, &aux); err != nil { 221 return err 222 } 223 if t.DestinationServiceName == "" { 224 t.DestinationServiceName = aux.DestinationServiceNameSnake 225 } 226 if t.DestinationServiceID == "" { 227 t.DestinationServiceID = aux.DestinationServiceIDSnake 228 } 229 if t.LocalServiceAddress == "" { 230 t.LocalServiceAddress = aux.LocalServiceAddressSnake 231 } 232 if t.LocalServicePort == 0 { 233 t.LocalServicePort = aux.LocalServicePortSnake 234 } 235 if t.LocalServiceSocketPath == "" { 236 t.LocalServiceSocketPath = aux.LocalServiceSocketPathSnake 237 } 238 if t.MeshGateway.Mode == "" { 239 t.MeshGateway.Mode = aux.MeshGatewaySnake.Mode 240 } 241 if t.TransparentProxy.OutboundListenerPort == 0 { 242 t.TransparentProxy.OutboundListenerPort = aux.TransparentProxySnake.OutboundListenerPort 243 } 244 if !t.TransparentProxy.DialedDirectly { 245 t.TransparentProxy.DialedDirectly = aux.TransparentProxySnake.DialedDirectly 246 } 247 248 return nil 249 250} 251 252func (c *ConnectProxyConfig) MarshalJSON() ([]byte, error) { 253 type Alias ConnectProxyConfig 254 out := struct { 255 TransparentProxy *TransparentProxyConfig `json:",omitempty"` 256 Alias 257 }{ 258 Alias: (Alias)(*c), 259 } 260 261 proxyConfig, err := lib.MapWalk(c.Config) 262 if err != nil { 263 return nil, err 264 } 265 out.Alias.Config = proxyConfig 266 267 if !c.TransparentProxy.IsZero() { 268 out.TransparentProxy = &out.Alias.TransparentProxy 269 } 270 271 return json.Marshal(&out) 272} 273 274// ToAPI returns the api struct with the same fields. We have duplicates to 275// avoid the api package depending on this one which imports a ton of Consul's 276// core which you don't want if you are just trying to use our client in your 277// app. 278func (c *ConnectProxyConfig) ToAPI() *api.AgentServiceConnectProxyConfig { 279 return &api.AgentServiceConnectProxyConfig{ 280 DestinationServiceName: c.DestinationServiceName, 281 DestinationServiceID: c.DestinationServiceID, 282 LocalServiceAddress: c.LocalServiceAddress, 283 LocalServicePort: c.LocalServicePort, 284 LocalServiceSocketPath: c.LocalServiceSocketPath, 285 Mode: api.ProxyMode(c.Mode), 286 TransparentProxy: c.TransparentProxy.ToAPI(), 287 Config: c.Config, 288 Upstreams: c.Upstreams.ToAPI(), 289 MeshGateway: c.MeshGateway.ToAPI(), 290 Expose: c.Expose.ToAPI(), 291 } 292} 293 294const ( 295 UpstreamDestTypeService = "service" 296 UpstreamDestTypePreparedQuery = "prepared_query" 297) 298 299// Upstreams is a list of upstreams. Aliased to allow ToAPI method. 300type Upstreams []Upstream 301 302// ToAPI returns the api structs with the same fields. We have duplicates to 303// avoid the api package depending on this one which imports a ton of Consul's 304// core which you don't want if you are just trying to use our client in your 305// app. 306func (us Upstreams) ToAPI() []api.Upstream { 307 a := make([]api.Upstream, len(us)) 308 for i, u := range us { 309 a[i] = u.ToAPI() 310 } 311 return a 312} 313 314func (us Upstreams) ToMap() map[string]*Upstream { 315 upstreamMap := make(map[string]*Upstream) 316 317 for i := range us { 318 upstreamMap[us[i].Identifier()] = &us[i] 319 } 320 return upstreamMap 321} 322 323// UpstreamsFromAPI is a helper for converting api.Upstream to Upstream. 324func UpstreamsFromAPI(us []api.Upstream) Upstreams { 325 a := make([]Upstream, len(us)) 326 for i, u := range us { 327 a[i] = UpstreamFromAPI(u) 328 } 329 return a 330} 331 332// Upstream represents a single upstream dependency for a service or proxy. It 333// describes the mechanism used to discover instances to communicate with (the 334// Target) as well as any potential client configuration that may be useful such 335// as load balancer options, timeouts etc. 336type Upstream struct { 337 // Destination fields are the required ones for determining what this upstream 338 // points to. Depending on DestinationType some other fields below might 339 // further restrict the set of instances allowable. 340 // 341 // DestinationType would be better as an int constant but even with custom 342 // JSON marshallers it causes havoc with all the mapstructure mangling we do 343 // on service definitions in various places. 344 DestinationType string `alias:"destination_type"` 345 DestinationNamespace string `json:",omitempty" alias:"destination_namespace"` 346 DestinationName string `alias:"destination_name"` 347 348 // Datacenter that the service discovery request should be run against. Note 349 // for prepared queries, the actual results might be from a different 350 // datacenter. 351 Datacenter string 352 353 // LocalBindAddress is the ip address a side-car proxy should listen on for 354 // traffic destined for this upstream service. Default if empty is 127.0.0.1. 355 LocalBindAddress string `json:",omitempty" alias:"local_bind_address"` 356 357 // LocalBindPort is the ip address a side-car proxy should listen on for traffic 358 // destined for this upstream service. Required. 359 LocalBindPort int `json:",omitempty" alias:"local_bind_port"` 360 361 // These are exclusive with LocalBindAddress/LocalBindPort 362 LocalBindSocketPath string `json:",omitempty" alias:"local_bind_socket_path"` 363 // This might be represented as an int, but because it's octal outputs can be a bit strange. 364 LocalBindSocketMode string `json:",omitempty" alias:"local_bind_socket_mode"` 365 366 // Config is an opaque config that is specific to the proxy process being run. 367 // It can be used to pass arbitrary configuration for this specific upstream 368 // to the proxy. 369 Config map[string]interface{} `json:",omitempty" bexpr:"-"` 370 371 // MeshGateway is the configuration for mesh gateway usage of this upstream 372 MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"` 373 374 // IngressHosts are a list of hosts that should route to this upstream from 375 // an ingress gateway. This cannot and should not be set by a user, it is 376 // used internally to store the association of hosts to an upstream service. 377 IngressHosts []string `json:"-" bexpr:"-"` 378 379 // CentrallyConfigured indicates whether the upstream was defined in a proxy 380 // instance registration or whether it was generated from a config entry. 381 CentrallyConfigured bool `json:",omitempty" bexpr:"-"` 382} 383 384func (t *Upstream) UnmarshalJSON(data []byte) (err error) { 385 type Alias Upstream 386 aux := &struct { 387 DestinationTypeSnake string `json:"destination_type"` 388 DestinationNamespaceSnake string `json:"destination_namespace"` 389 DestinationNameSnake string `json:"destination_name"` 390 391 LocalBindAddressSnake string `json:"local_bind_address"` 392 LocalBindPortSnake int `json:"local_bind_port"` 393 394 LocalBindSocketPathSnake string `json:"local_bind_socket_path"` 395 LocalBindSocketModeSnake string `json:"local_bind_socket_mode"` 396 397 MeshGatewaySnake MeshGatewayConfig `json:"mesh_gateway"` 398 399 *Alias 400 }{ 401 Alias: (*Alias)(t), 402 } 403 if err = lib.UnmarshalJSON(data, &aux); err != nil { 404 return err 405 } 406 if t.DestinationType == "" { 407 t.DestinationType = aux.DestinationTypeSnake 408 } 409 if t.DestinationNamespace == "" { 410 t.DestinationNamespace = aux.DestinationNamespaceSnake 411 } 412 if t.DestinationName == "" { 413 t.DestinationName = aux.DestinationNameSnake 414 } 415 if t.LocalBindAddress == "" { 416 t.LocalBindAddress = aux.LocalBindAddressSnake 417 } 418 if t.LocalBindPort == 0 { 419 t.LocalBindPort = aux.LocalBindPortSnake 420 } 421 if t.LocalBindSocketPath == "" { 422 t.LocalBindSocketPath = aux.LocalBindSocketPathSnake 423 } 424 if t.LocalBindSocketMode == "" { 425 t.LocalBindSocketMode = aux.LocalBindSocketModeSnake 426 } 427 if t.MeshGateway.Mode == "" { 428 t.MeshGateway.Mode = aux.MeshGatewaySnake.Mode 429 } 430 431 return nil 432} 433 434// Validate sanity checks the struct is valid 435func (u *Upstream) Validate() error { 436 switch u.DestinationType { 437 case UpstreamDestTypePreparedQuery: 438 case UpstreamDestTypeService, "": 439 default: 440 return fmt.Errorf("unknown upstream destination type: %q", u.DestinationType) 441 } 442 443 if u.DestinationName == "" { 444 return fmt.Errorf("upstream destination name cannot be empty") 445 } 446 if u.DestinationName == WildcardSpecifier && !u.CentrallyConfigured { 447 return fmt.Errorf("upstream destination name cannot be a wildcard") 448 } 449 450 if u.LocalBindPort == 0 && u.LocalBindSocketPath == "" && !u.CentrallyConfigured { 451 return fmt.Errorf("upstream local bind port or local socket path must be defined and nonzero") 452 } 453 if u.LocalBindPort != 0 && u.LocalBindSocketPath != "" && !u.CentrallyConfigured { 454 return fmt.Errorf("only one of upstream local bind port or local socket path can be defined and nonzero") 455 } 456 457 return nil 458} 459 460// ToAPI returns the api structs with the same fields. We have duplicates to 461// avoid the api package depending on this one which imports a ton of Consul's 462// core which you don't want if you are just trying to use our client in your 463// app. 464func (u *Upstream) ToAPI() api.Upstream { 465 return api.Upstream{ 466 DestinationType: api.UpstreamDestType(u.DestinationType), 467 DestinationNamespace: u.DestinationNamespace, 468 DestinationName: u.DestinationName, 469 Datacenter: u.Datacenter, 470 LocalBindAddress: u.LocalBindAddress, 471 LocalBindPort: u.LocalBindPort, 472 LocalBindSocketPath: u.LocalBindSocketPath, 473 LocalBindSocketMode: u.LocalBindSocketMode, 474 Config: u.Config, 475 MeshGateway: u.MeshGateway.ToAPI(), 476 } 477} 478 479// ToKey returns a value-type representation that uniquely identifies the 480// upstream in a canonical way. Set and unset values are deliberately handled 481// differently. 482// 483// These fields should be user-specificed explicit values and not inferred 484// values. 485func (u *Upstream) ToKey() UpstreamKey { 486 return UpstreamKey{ 487 DestinationType: u.DestinationType, 488 DestinationNamespace: u.DestinationNamespace, 489 DestinationName: u.DestinationName, 490 Datacenter: u.Datacenter, 491 } 492} 493 494func (u Upstream) HasLocalPortOrSocket() bool { 495 return (u.LocalBindPort != 0 || u.LocalBindSocketPath != "") 496} 497 498func (u Upstream) UpstreamIsUnixSocket() bool { 499 return (u.LocalBindPort == 0 && u.LocalBindAddress == "" && u.LocalBindSocketPath != "") 500} 501 502func (u Upstream) UpstreamAddressToString() string { 503 if u.UpstreamIsUnixSocket() { 504 return u.LocalBindSocketPath 505 } 506 507 addr := u.LocalBindAddress 508 if addr == "" { 509 addr = "127.0.0.1" 510 } 511 return net.JoinHostPort(addr, fmt.Sprintf("%d", u.LocalBindPort)) 512} 513 514type UpstreamKey struct { 515 DestinationType string 516 DestinationName string 517 DestinationNamespace string 518 Datacenter string 519} 520 521func (k UpstreamKey) String() string { 522 return fmt.Sprintf( 523 "[type=%q, name=%q, namespace=%q, datacenter=%q]", 524 k.DestinationType, 525 k.DestinationName, 526 k.DestinationNamespace, 527 k.Datacenter, 528 ) 529} 530 531// String implements Stringer by returning the Identifier. 532func (u *Upstream) String() string { 533 return u.Identifier() 534} 535 536// UpstreamFromAPI is a helper for converting api.Upstream to Upstream. 537func UpstreamFromAPI(u api.Upstream) Upstream { 538 return Upstream{ 539 DestinationType: string(u.DestinationType), 540 DestinationNamespace: u.DestinationNamespace, 541 DestinationName: u.DestinationName, 542 Datacenter: u.Datacenter, 543 LocalBindAddress: u.LocalBindAddress, 544 LocalBindPort: u.LocalBindPort, 545 LocalBindSocketPath: u.LocalBindSocketPath, 546 LocalBindSocketMode: u.LocalBindSocketMode, 547 Config: u.Config, 548 } 549} 550 551// ExposeConfig describes HTTP paths to expose through Envoy outside of Connect. 552// Users can expose individual paths and/or all HTTP/GRPC paths for checks. 553type ExposeConfig struct { 554 // Checks defines whether paths associated with Consul checks will be exposed. 555 // This flag triggers exposing all HTTP and GRPC check paths registered for the service. 556 Checks bool `json:",omitempty"` 557 558 // Paths is the list of paths exposed through the proxy. 559 Paths []ExposePath `json:",omitempty"` 560} 561 562func (e ExposeConfig) Clone() ExposeConfig { 563 e2 := e 564 if len(e.Paths) > 0 { 565 e2.Paths = make([]ExposePath, 0, len(e.Paths)) 566 for _, p := range e.Paths { 567 e2.Paths = append(e2.Paths, p) 568 } 569 } 570 return e2 571} 572 573type ExposePath struct { 574 // ListenerPort defines the port of the proxy's listener for exposed paths. 575 ListenerPort int `json:",omitempty" alias:"listener_port"` 576 577 // Path is the path to expose through the proxy, ie. "/metrics." 578 Path string `json:",omitempty"` 579 580 // LocalPathPort is the port that the service is listening on for the given path. 581 LocalPathPort int `json:",omitempty" alias:"local_path_port"` 582 583 // Protocol describes the upstream's service protocol. 584 // Valid values are "http" and "http2", defaults to "http" 585 Protocol string `json:",omitempty"` 586 587 // ParsedFromCheck is set if this path was parsed from a registered check 588 ParsedFromCheck bool `json:",omitempty" alias:"parsed_from_check"` 589} 590 591func (t *ExposePath) UnmarshalJSON(data []byte) (err error) { 592 type Alias ExposePath 593 aux := &struct { 594 ListenerPortSnake int `json:"listener_port"` 595 LocalPathPortSnake int `json:"local_path_port"` 596 ParsedFromCheckSnake bool `json:"parsed_from_check"` 597 598 *Alias 599 }{ 600 Alias: (*Alias)(t), 601 } 602 if err = lib.UnmarshalJSON(data, &aux); err != nil { 603 return err 604 } 605 if t.LocalPathPort == 0 { 606 t.LocalPathPort = aux.LocalPathPortSnake 607 } 608 if t.ListenerPort == 0 { 609 t.ListenerPort = aux.ListenerPortSnake 610 } 611 if aux.ParsedFromCheckSnake { 612 t.ParsedFromCheck = true 613 } 614 615 return nil 616} 617 618func (e *ExposeConfig) ToAPI() api.ExposeConfig { 619 paths := make([]api.ExposePath, 0) 620 for _, p := range e.Paths { 621 paths = append(paths, p.ToAPI()) 622 } 623 if e.Paths == nil { 624 paths = nil 625 } 626 627 return api.ExposeConfig{ 628 Checks: e.Checks, 629 Paths: paths, 630 } 631} 632 633func (p *ExposePath) ToAPI() api.ExposePath { 634 return api.ExposePath{ 635 ListenerPort: p.ListenerPort, 636 Path: p.Path, 637 LocalPathPort: p.LocalPathPort, 638 Protocol: p.Protocol, 639 ParsedFromCheck: p.ParsedFromCheck, 640 } 641} 642 643// Finalize validates ExposeConfig and sets default values 644func (e *ExposeConfig) Finalize() { 645 for i := 0; i < len(e.Paths); i++ { 646 path := &e.Paths[i] 647 648 if path.Protocol == "" { 649 path.Protocol = defaultExposeProtocol 650 } 651 } 652} 653