package model import ( "encoding/json" "fmt" "reflect" "strconv" "strings" discovery "k8s.io/api/discovery/v1" "github.com/aws/aws-sdk-go-v2/service/servicediscovery/types" ) const ( HttpNamespaceType NamespaceType = "HTTP" DnsPrivateNamespaceType NamespaceType = "DNS_PRIVATE" // UnsupportedNamespaceType Placeholder NamespaceType to denote not supported values UnsupportedNamespaceType NamespaceType = "" ) type NamespaceType string // Namespace hold namespace attributes type Namespace struct { Id string Name string Type NamespaceType } // Service holds namespace and endpoint state for a named service. type Service struct { Namespace string Name string Endpoints []*Endpoint } const ( HeadlessType ServiceType = "Headless" ClusterSetIPType ServiceType = "ClusterSetIP" ) type ServiceType string // Endpoint holds basic values and attributes for an endpoint. type Endpoint struct { Id string IP string AddressType discovery.AddressType EndpointPort Port ServicePort Port ClusterId string ClusterSetId string ServiceType ServiceType ServiceExportCreationTimestamp int64 Ready bool Hostname string Nodename string Attributes map[string]string } type Port struct { Name string Port int32 TargetPort string Protocol string // TCP, UDP, SCTP } // Cloudmap Instances IP and Port is supposed to be AWS_INSTANCE_IPV4 and AWS_INSTANCE_PORT // Rest are custom attributes const ( EndpointIpv4Attr = "AWS_INSTANCE_IPV4" EndpointIpv6Attr = "AWS_INSTANCE_IPV6" EndpointPortAttr = "AWS_INSTANCE_PORT" EndpointPortNameAttr = "ENDPOINT_PORT_NAME" EndpointProtocolAttr = "ENDPOINT_PROTOCOL" EndpointReadyAttr = "READY" EndpointHostnameAttr = "HOSTNAME" EndpointNodeNameAttr = "NODENAME" ClusterIdAttr = "CLUSTER_ID" ClusterSetIdAttr = "CLUSTERSET_ID" ServicePortNameAttr = "SERVICE_PORT_NAME" ServicePortAttr = "SERVICE_PORT" ServiceTargetPortAttr = "SERVICE_TARGET_PORT" ServiceProtocolAttr = "SERVICE_PROTOCOL" ServiceTypeAttr = "SERVICE_TYPE" ServiceExportCreationAttr = "SERVICE_EXPORT_CREATION_TIMESTAMP" K8sVersionAttr = "K8S_CONTROLLER" ) // NewEndpointFromInstance converts a Cloud Map HttpInstanceSummary to an endpoint. func NewEndpointFromInstance(inst *types.HttpInstanceSummary) (*Endpoint, error) { endpoint := Endpoint{ Id: *inst.InstanceId, Attributes: make(map[string]string), } attributes := make(map[string]string) for key, value := range inst.Attributes { attributes[key] = value } // Remove and set the IP, Port, Service Port, ServiceType, ClusterId, ClusterSetId // ASSUMPTION: Endpoints have either IPV4 OR IPV6, not both. Defaults to IPV4 if both are present. ipv4, ipv4Exists := attributes[EndpointIpv4Attr] ipv6, ipv6Exists := attributes[EndpointIpv6Attr] if ipv6Exists { ip, err := removeStringAttr(attributes, EndpointIpv6Attr) if err != nil { return nil, err } endpoint.IP = ip endpoint.AddressType = discovery.AddressTypeIPv6 } if ipv4Exists { ip, err := removeStringAttr(attributes, EndpointIpv4Attr) if err != nil { return nil, err } endpoint.IP = ip endpoint.AddressType = discovery.AddressTypeIPv4 } if ipv4Exists && ipv6Exists { fmt.Printf("WARNING: Found both address types in one Endpoint... IPv4: %s IPv6: %s\n", ipv4, ipv6) } endpointPort, err := endpointPortFromAttr(attributes) if err != nil { return nil, err } endpoint.EndpointPort = endpointPort servicePort, err := servicePortFromAttr(attributes) if err != nil { return nil, err } endpoint.ServicePort = servicePort serviceTypeStr, err := removeStringAttr(attributes, ServiceTypeAttr) if err != nil { return nil, err } endpoint.ServiceType = ServiceType(serviceTypeStr) if endpoint.ClusterId, err = removeStringAttr(attributes, ClusterIdAttr); err != nil { return nil, err } if endpoint.ClusterSetId, err = removeStringAttr(attributes, ClusterSetIdAttr); err != nil { return nil, err } if endpoint.Ready, err = removeBoolAttr(attributes, EndpointReadyAttr); err != nil { return nil, err } if endpoint.ServiceExportCreationTimestamp, err = removeTimestampAttr(attributes, ServiceExportCreationAttr); err != nil { return nil, err } // Hostname and Nodename are Optional attributes endpoint.Hostname, _ = removeStringAttr(attributes, EndpointHostnameAttr) endpoint.Nodename, _ = removeStringAttr(attributes, EndpointNodeNameAttr) // Add the remaining attributes endpoint.Attributes = attributes return &endpoint, err } func endpointPortFromAttr(attributes map[string]string) (port Port, err error) { port = Port{} if port.Name, err = removeStringAttr(attributes, EndpointPortNameAttr); err != nil { return port, err } if port.Port, err = removeIntAttr(attributes, EndpointPortAttr); err != nil { return port, err } if port.Protocol, err = removeStringAttr(attributes, EndpointProtocolAttr); err != nil { return port, err } return port, err } func servicePortFromAttr(attributes map[string]string) (port Port, err error) { port = Port{} if port.TargetPort, err = removeStringAttr(attributes, ServiceTargetPortAttr); err != nil { return port, err } if port.Name, err = removeStringAttr(attributes, ServicePortNameAttr); err != nil { return port, err } if port.Port, err = removeIntAttr(attributes, ServicePortAttr); err != nil { return port, err } if port.Protocol, err = removeStringAttr(attributes, ServiceProtocolAttr); err != nil { return port, err } return port, err } func removeStringAttr(attributes map[string]string, attr string) (string, error) { if value, hasValue := attributes[attr]; hasValue { delete(attributes, attr) return value, nil } return "", fmt.Errorf("cannot find the attribute %s", attr) } func removeIntAttr(attributes map[string]string, attr string) (int32, error) { if value, hasValue := attributes[attr]; hasValue { parsedValue, parseError := strconv.ParseUint(value, 10, 16) if parseError != nil { return 0, fmt.Errorf("failed to parse the %s as int with error %s", attr, parseError.Error()) } delete(attributes, attr) return int32(parsedValue), nil } return 0, fmt.Errorf("cannot find the attribute %s", attr) } func removeBoolAttr(attributes map[string]string, attr string) (bool, error) { if value, hasValue := attributes[attr]; hasValue { parsedValue, parseError := strconv.ParseBool(value) if parseError != nil { return false, fmt.Errorf("failed to parse the %s as bool with error %s", attr, parseError.Error()) } delete(attributes, attr) return parsedValue, nil } return false, fmt.Errorf("cannot find the attribute %s", attr) } func removeTimestampAttr(attributes map[string]string, attr string) (int64, error) { if value, hasValue := attributes[attr]; hasValue { parsedValue, parseError := strconv.ParseInt(value, 10, 64) if parseError != nil { return 0, fmt.Errorf("failed to parse the %s as int with error %s", attr, parseError.Error()) } delete(attributes, attr) return parsedValue, nil } return 0, fmt.Errorf("cannot find the attribute %s", attr) } // GetCloudMapAttributes extracts endpoint attributes for Cloud Map service instance registration. func (e *Endpoint) GetCloudMapAttributes() map[string]string { attrs := make(map[string]string) if e.AddressType == discovery.AddressTypeIPv4 { attrs[EndpointIpv4Attr] = e.IP } else if e.AddressType == discovery.AddressTypeIPv6 { attrs[EndpointIpv6Attr] = e.IP } attrs[ClusterIdAttr] = e.ClusterId attrs[ClusterSetIdAttr] = e.ClusterSetId attrs[EndpointPortAttr] = strconv.Itoa(int(e.EndpointPort.Port)) attrs[EndpointProtocolAttr] = e.EndpointPort.Protocol attrs[EndpointPortNameAttr] = e.EndpointPort.Name attrs[ServicePortNameAttr] = e.ServicePort.Name attrs[ServicePortAttr] = strconv.Itoa(int(e.ServicePort.Port)) attrs[ServiceTargetPortAttr] = e.ServicePort.TargetPort attrs[ServiceProtocolAttr] = e.ServicePort.Protocol attrs[ServiceTypeAttr] = e.ServiceType.String() attrs[ServiceExportCreationAttr] = strconv.FormatInt(e.ServiceExportCreationTimestamp, 10) attrs[EndpointReadyAttr] = strconv.FormatBool(e.Ready) attrs[EndpointHostnameAttr] = e.Hostname attrs[EndpointNodeNameAttr] = e.Nodename for key, val := range e.Attributes { attrs[key] = val } return attrs } // Equals evaluates if two Endpoints are "deeply equal" (including all fields). func (e *Endpoint) Equals(other *Endpoint) bool { return reflect.DeepEqual(e, other) } // String gives a string representation for an endpoint. func (e *Endpoint) String() string { bytes, err := json.Marshal(e) if err != nil { return "" } return string(bytes) } // EndpointIdFromIPAddressAndPort converts an IP address to human-readable identifier. func EndpointIdFromIPAddressAndPort(address string, port Port) string { address = strings.ReplaceAll(address, ".", "_") address = strings.ReplaceAll(address, ":", "_") return fmt.Sprintf("%s-%s-%d", strings.ToLower(port.Protocol), address, port.Port) } // Gives string representation for ServiceType func (serviceType ServiceType) String() string { return string(serviceType) } func ConvertNamespaceType(nsType types.NamespaceType) (namespaceType NamespaceType) { switch nsType { case types.NamespaceTypeDnsPrivate: return DnsPrivateNamespaceType case types.NamespaceTypeHttp: return HttpNamespaceType default: return UnsupportedNamespaceType } } func (svc *Service) GetEndpoints(clusterId string) (endpts []*Endpoint) { for _, endpt := range svc.Endpoints { if endpt.ClusterId == clusterId { endpts = append(endpts, endpt) } } return endpts } func (namespaceType *NamespaceType) IsUnsupported() bool { return *namespaceType == UnsupportedNamespaceType } func (p *Port) GetID() string { return fmt.Sprintf("%s:%d", p.Protocol, p.Port) } // Equals evaluates if two Ports are "deeply equal" (including all fields). func (p *Port) Equals(other *Port) bool { return reflect.DeepEqual(p, other) } func GetAddressTypeFromString(addressTypeStr string) (discovery.AddressType, error) { switch addressTypeStr { case string(discovery.AddressTypeIPv4): return discovery.AddressTypeIPv4, nil case string(discovery.AddressTypeIPv6): return discovery.AddressTypeIPv6, nil default: return "", fmt.Errorf("Invalid AddressType, could not parse from string: %s", addressTypeStr) } }