internal/pkg/describe/uri.go (375 lines of code) (raw):
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package describe
import (
"fmt"
"regexp"
"strings"
"github.com/aws/copilot-cli/internal/pkg/term/color"
"github.com/dustin/go-humanize/english"
"github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation/stack"
"github.com/aws/copilot-cli/internal/pkg/manifest/manifestinfo"
)
// URIAccessType represents how a URI can be accessed.
type URIAccessType int
// Supported URI Access Types.
const (
URIAccessTypeNone URIAccessType = iota
URIAccessTypeInternet
URIAccessTypeInternal
URIAccessTypeServiceDiscovery
URIAccessTypeServiceConnect
LBDNS = "COPILOT_LB_DNS"
)
var (
fmtSvcDiscoveryEndpointWithPort = "%s.%s:%s" // Format string of the form {svc}.{endpoint}:{port}
)
// URI represents a uri and how it can be accessed.
type URI struct {
URI string
AccessType URIAccessType
}
// ReachableService represents a service describer that has an endpoint.
type ReachableService interface {
URI(env string) (URI, error)
}
// NewReachableService returns a ReachableService based on the type of the service.
func NewReachableService(app, svc string, store ConfigStoreSvc) (ReachableService, error) {
cfg, err := store.GetWorkload(app, svc)
if err != nil {
return nil, err
}
in := NewServiceConfig{
App: app,
Svc: svc,
ConfigStore: store,
}
switch cfg.Type {
case manifestinfo.LoadBalancedWebServiceType:
return NewLBWebServiceDescriber(in)
case manifestinfo.RequestDrivenWebServiceType:
return NewRDWebServiceDescriber(in)
case manifestinfo.BackendServiceType:
return NewBackendServiceDescriber(in)
case manifestinfo.StaticSiteType:
return NewStaticSiteDescriber(in)
default:
return nil, &ErrNonAccessibleServiceType{
name: svc,
svcType: cfg.Type,
}
}
}
// URI returns the LBWebServiceURI to identify this service uniquely given an environment name.
func (d *LBWebServiceDescriber) URI(envName string) (URI, error) {
svcDescr, err := d.initECSServiceDescribers(envName)
if err != nil {
return URI{}, err
}
envDescr, err := d.initEnvDescribers(envName)
if err != nil {
return URI{}, err
}
var albEnabled, importedALB, nlbEnabled bool
resources, err := svcDescr.StackResources()
if err != nil {
return URI{}, fmt.Errorf("get stack resources for service %s: %w", d.svc, err)
}
for _, resource := range resources {
if strings.HasPrefix(resource.LogicalID, svcStackResourceALBTargetGroupLogicalID) {
albEnabled = true
}
if strings.HasPrefix(resource.LogicalID, svcStackResourceListenerRuleForImportedALBLogicalID) {
importedALB = true
}
if strings.HasPrefix(resource.LogicalID, svcStackResourceNLBTargetGroupLogicalID) {
nlbEnabled = true
}
if strings.HasPrefix(resource.LogicalID, svcStackResourceNLBTargetGroupV2LogicalID) {
nlbEnabled = true
}
}
var uri LBWebServiceURI
if albEnabled {
uriDescr := &uriDescriber{
svc: d.svc,
env: envName,
svcDescriber: svcDescr,
envDescriber: envDescr,
initLBDescriber: d.initLBDescriber,
albCFNOutputName: envOutputPublicLoadBalancerDNSName,
}
if importedALB {
uriDescr.albCFNOutputName = ""
}
publicURI, err := uriDescr.uri()
if err != nil {
return URI{}, err
}
uri.access = publicURI
}
if nlbEnabled {
nlbURI, err := d.nlbURI(envName, svcDescr, envDescr)
if err != nil {
return URI{}, err
}
uri.nlbURI = nlbURI
}
return URI{
URI: uri.String(),
AccessType: URIAccessTypeInternet,
}, nil
}
func (d *LBWebServiceDescriber) nlbURI(envName string, svcDescr ecsDescriber, envDescr envDescriber) (nlbURI, error) {
svcParams, err := svcDescr.Params()
if err != nil {
return nlbURI{}, fmt.Errorf("get stack parameters for service %s: %w", d.svc, err)
}
port, ok := svcParams[stack.LBWebServiceNLBPortParamKey]
if !ok {
return nlbURI{}, nil
}
uri := nlbURI{
Port: port,
}
dnsDelegated, ok := svcParams[stack.LBWebServiceDNSDelegatedParamKey]
if !ok || dnsDelegated != "true" {
svcOutputs, err := svcDescr.Outputs()
if err != nil {
return nlbURI{}, fmt.Errorf("get stack outputs for service %s: %w", d.svc, err)
}
uri.DNSNames = []string{svcOutputs[svcOutputPublicNLBDNSName]}
return uri, nil
}
aliases, ok := svcParams[stack.LBWebServiceNLBAliasesParamKey]
if ok && aliases != "" {
uri.DNSNames = strings.Split(aliases, ",")
return uri, nil
}
envOutputs, err := envDescr.Outputs()
if err != nil {
return nlbURI{}, fmt.Errorf("get stack outputs for environment %s: %w", envName, err)
}
uri.DNSNames = []string{fmt.Sprintf("%s-nlb.%s", d.svc, envOutputs[envOutputSubdomain])}
return uri, nil
}
// URI returns the service discovery namespace and is used to make
// BackendServiceDescriber have the same signature as WebServiceDescriber.
func (d *BackendServiceDescriber) URI(envName string) (URI, error) {
svcDescr, err := d.initECSServiceDescribers(envName)
if err != nil {
return URI{}, err
}
envDescr, err := d.initEnvDescribers(envName)
if err != nil {
return URI{}, err
}
resources, err := svcDescr.StackResources()
if err != nil {
return URI{}, fmt.Errorf("get stack resources for service %s: %w", d.svc, err)
}
for _, res := range resources {
if res.LogicalID == svcStackResourceALBTargetGroupLogicalID {
uriDescr := &uriDescriber{
svc: d.svc,
env: envName,
svcDescriber: svcDescr,
envDescriber: envDescr,
initLBDescriber: d.initLBDescriber,
albCFNOutputName: envOutputInternalLoadBalancerDNSName,
}
privateURI, err := uriDescr.uri()
if err != nil {
return URI{}, err
}
if !privateURI.HTTPS && len(privateURI.DNSNames) > 1 {
privateURI = uriDescr.bestEffortRemoveALBDNSName(privateURI)
}
return URI{
URI: english.OxfordWordSeries(privateURI.strings(), "or"),
AccessType: URIAccessTypeInternal,
}, nil
}
}
svcStackParams, err := svcDescr.Params()
if err != nil {
return URI{}, fmt.Errorf("get stack parameters for environment %s: %w", envName, err)
}
if !isReachableWithinVPC(svcStackParams) {
return URI{
URI: BlankServiceDiscoveryURI,
AccessType: URIAccessTypeNone,
}, nil
}
scDNSNames, err := svcDescr.ServiceConnectDNSNames()
if err != nil {
return URI{}, fmt.Errorf("retrieve service connect DNS names: %w", err)
}
if len(scDNSNames) > 0 {
return URI{
URI: english.OxfordWordSeries(scDNSNames, "or"),
AccessType: URIAccessTypeServiceConnect,
}, nil
}
endpoint, err := envDescr.ServiceDiscoveryEndpoint()
if err != nil {
return URI{}, fmt.Errorf("retrieve service discovery endpoint for environment %s: %w", envName, err)
}
s := serviceDiscovery{
Service: d.svc,
Port: svcStackParams[stack.WorkloadTargetPortParamKey],
Endpoint: endpoint,
}
return URI{
URI: s.String(),
AccessType: URIAccessTypeServiceDiscovery,
}, nil
}
type uriDescriber struct {
svc string
env string
svcDescriber ecsDescriber
envDescriber envDescriber
initLBDescriber func(string) (lbDescriber, error)
albCFNOutputName string // The DNS name for the public or private ALB.
}
func (d *uriDescriber) envDNSName(path string) (accessURI, error) {
var dnsNames []string
envOutputs, err := d.envDescriber.Outputs()
if err != nil {
return accessURI{}, fmt.Errorf("get stack outputs for environment %s: %w", d.env, err)
}
// Backward compatibility concern since envOutputPublicALBAccessible didn't exist before,
// and public ALB DNS name previously was always accessible until the introduction of envOutputPublicALBAccessible.
if accessible, ok := envOutputs[envOutputPublicALBAccessible]; !ok || accessible == "true" {
dnsNames = append(dnsNames, envOutputs[d.albCFNOutputName])
}
if cfDNS, ok := envOutputs[envOutputCloudFrontDomainName]; ok {
dnsNames = append(dnsNames, cfDNS)
}
return accessURI{
DNSNames: dnsNames,
Path: path,
}, nil
}
func (d *uriDescriber) uri() (accessURI, error) {
svcParams, err := d.svcDescriber.Params()
if err != nil {
return accessURI{}, fmt.Errorf("get stack parameters for service %s: %w", d.svc, err)
}
path := svcParams[stack.WorkloadRulePathParamKey]
httpsEnabled := svcParams[stack.WorkloadHTTPSParamKey] == "true"
// public load balancers use the env DNS name if https is not enabled
if d.albCFNOutputName == envOutputPublicLoadBalancerDNSName && !httpsEnabled {
return d.envDNSName(path)
}
svcResources, err := d.svcDescriber.StackResources()
if err != nil {
return accessURI{}, fmt.Errorf("get stack resources for service %s: %w", d.svc, err)
}
var ruleARNs []string
for _, resource := range svcResources {
if resource.Type == svcStackResourceListenerRuleResourceType &&
((httpsEnabled && strings.HasPrefix(resource.LogicalID, svcStackResourceHTTPSListenerRuleLogicalID)) ||
(!httpsEnabled && strings.HasPrefix(resource.LogicalID, svcStackResourceHTTPListenerRuleLogicalID))) {
ruleARNs = append(ruleARNs, resource.PhysicalID)
}
}
lbDescr, err := d.initLBDescriber(d.env)
if err != nil {
return accessURI{}, nil
}
dnsNames, err := lbDescr.ListenerRulesHostHeaders(ruleARNs)
if err != nil {
return accessURI{}, fmt.Errorf("get host headers for listener rules %s: %w", strings.Join(ruleARNs, ","), err)
}
if len(dnsNames) == 0 {
envVars, _ := d.svcDescriber.EnvVars()
var lbDNS string
for _, envVar := range envVars {
if envVar.Name == LBDNS {
lbDNS = envVar.Value
return accessURI{
HTTPS: httpsEnabled,
DNSNames: []string{lbDNS},
Path: path,
}, nil
}
}
return d.envDNSName(path)
}
return accessURI{
HTTPS: httpsEnabled,
DNSNames: dnsNames,
Path: path,
}, nil
}
func (d *uriDescriber) bestEffortRemoveALBDNSName(accessURI accessURI) accessURI {
envOutputs, err := d.envDescriber.Outputs()
if err != nil {
return accessURI
}
lbDNSName := envOutputs[d.albCFNOutputName]
for i := range accessURI.DNSNames {
if accessURI.DNSNames[i] == lbDNSName {
accessURI.DNSNames = append(accessURI.DNSNames[:i], accessURI.DNSNames[i+1:]...)
break
}
}
return accessURI
}
// URI returns the WebServiceURI to identify this service uniquely given an environment name.
func (d *RDWebServiceDescriber) URI(envName string) (URI, error) {
describer, err := d.initAppRunnerDescriber(envName)
if err != nil {
return URI{}, err
}
serviceURL, err := describer.ServiceURL()
if err != nil {
return URI{}, fmt.Errorf("get outputs for service %q: %w", d.svc, err)
}
isPrivate, err := describer.IsPrivate()
if err != nil {
return URI{}, fmt.Errorf("check if service %q is private: %w", d.svc, err)
}
accessType := URIAccessTypeInternet
if isPrivate {
accessType = URIAccessTypeInternal
}
return URI{
URI: serviceURL,
AccessType: accessType,
}, nil
}
// LBWebServiceURI represents the unique identifier to access a load balanced web service.
type LBWebServiceURI struct {
access accessURI
nlbURI nlbURI
}
type accessURI struct {
HTTPS bool
DNSNames []string // The environment's subdomain if the service is served on HTTPS. Otherwise, the public application load balancer's access point.
Path string // Empty if the service is served on HTTPS. Otherwise, the pattern used to match the service.
}
type nlbURI struct {
DNSNames []string
Port string
}
func (u *LBWebServiceURI) String() string {
uris := u.access.strings()
for _, dnsName := range u.nlbURI.DNSNames {
uris = append(uris, color.HighlightResource(fmt.Sprintf("%s:%s", dnsName, u.nlbURI.Port)))
}
return english.OxfordWordSeries(uris, "or")
}
func (u *accessURI) strings() []string {
var uris []string
re := regexp.MustCompile("/+")
for _, dnsName := range u.DNSNames {
protocol := "http://"
if u.HTTPS {
protocol = "https://"
}
path := ""
if !strings.HasPrefix(u.Path, "/") {
path = fmt.Sprintf("/%s", u.Path)
} else if u.Path != "/" {
path = re.ReplaceAllString(u.Path, "/")
}
uris = append(uris, color.HighlightResource(protocol+dnsName+path))
}
return uris
}
type serviceDiscovery struct {
Service string
Endpoint string
Port string
}
func (s *serviceDiscovery) String() string {
return fmt.Sprintf(fmtSvcDiscoveryEndpointWithPort, s.Service, s.Endpoint, s.Port)
}