internal/pkg/describe/static_site.go (209 lines of code) (raw):
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package describe
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"text/tabwriter"
awsS3 "github.com/aws/copilot-cli/internal/pkg/aws/s3"
"github.com/aws/copilot-cli/internal/pkg/aws/sessions"
s3 "github.com/aws/copilot-cli/internal/pkg/s3"
"github.com/aws/copilot-cli/internal/pkg/describe/stack"
"github.com/aws/copilot-cli/internal/pkg/manifest/manifestinfo"
"github.com/aws/copilot-cli/internal/pkg/term/color"
"github.com/dustin/go-humanize/english"
)
const (
staticSiteOutputCFDomainName = "CloudFrontDistributionDomainName"
staticSiteOutputCFAltDomainName = "CloudFrontDistributionAlternativeDomainName"
)
// StaticSiteDescriber retrieves information about a static site service.
type StaticSiteDescriber struct {
app string
svc string
enableResources bool
store DeployedEnvServicesLister
initWkldStackDescriber func(string) (workloadDescriber, error)
wkldDescribers map[string]workloadDescriber
initS3Client func(string) (bucketDescriber, bucketNameGetter, error)
}
// NewStaticSiteDescriber instantiates a static site service describer.
func NewStaticSiteDescriber(opt NewServiceConfig) (*StaticSiteDescriber, error) {
describer := &StaticSiteDescriber{
app: opt.App,
svc: opt.Svc,
enableResources: opt.EnableResources,
store: opt.DeployStore,
wkldDescribers: make(map[string]workloadDescriber),
}
describer.initWkldStackDescriber = func(env string) (workloadDescriber, error) {
if describer, ok := describer.wkldDescribers[env]; ok {
return describer, nil
}
svcDescr, err := NewWorkloadStackDescriber(NewWorkloadConfig{
App: opt.App,
Env: env,
Name: opt.Svc,
ConfigStore: opt.ConfigStore,
})
if err != nil {
return nil, err
}
describer.wkldDescribers[env] = svcDescr
return svcDescr, nil
}
describer.initS3Client = func(env string) (bucketDescriber, bucketNameGetter, error) {
environment, err := opt.ConfigStore.GetEnvironment(opt.App, env)
if err != nil {
return nil, nil, fmt.Errorf("get environment %s: %w", env, err)
}
sess, err := sessions.ImmutableProvider().FromRole(environment.ManagerRoleARN, environment.Region)
if err != nil {
return nil, nil, err
}
return awsS3.New(sess), s3.New(sess), nil
}
return describer, nil
}
// URI returns the public accessible URI of a static site service.
func (d *StaticSiteDescriber) URI(envName string) (URI, error) {
wkldDescr, err := d.initWkldStackDescriber(envName)
if err != nil {
return URI{}, err
}
outputs, err := wkldDescr.Outputs()
if err != nil {
return URI{}, fmt.Errorf("get stack output for service %q: %w", d.svc, err)
}
uri := accessURI{
HTTPS: true,
DNSNames: []string{outputs[staticSiteOutputCFDomainName]},
}
if outputs[staticSiteOutputCFAltDomainName] != "" {
uri.DNSNames = append(uri.DNSNames, outputs[staticSiteOutputCFAltDomainName])
}
return URI{
URI: english.OxfordWordSeries(uri.strings(), "or"),
AccessType: URIAccessTypeInternet,
}, nil
}
// Describe returns info of a static site.
func (d *StaticSiteDescriber) Describe() (HumanJSONStringer, error) {
environments, err := d.store.ListEnvironmentsDeployedTo(d.app, d.svc)
if err != nil {
return nil, fmt.Errorf("list deployed environments for service %q: %w", d.svc, err)
}
var routes []*WebServiceRoute
var objects []*S3ObjectTree
for _, env := range environments {
bucketDescriber, bucketNameDescriber, err := d.initS3Client(env)
if err != nil {
return nil, err
}
uri, err := d.URI(env)
if err != nil {
return nil, fmt.Errorf("retrieve service URI: %w", err)
}
if uri.AccessType == URIAccessTypeInternet {
routes = append(routes, &WebServiceRoute{
Environment: env,
URL: uri.URI,
})
}
bucketName, err := bucketNameDescriber.BucketName(d.app, env, d.svc)
if err != nil {
return nil, fmt.Errorf("get bucket name for %q env: %w", env, err)
}
tree, err := bucketDescriber.BucketTree(bucketName)
if err != nil {
return nil, fmt.Errorf("get tree representation of bucket contents: %w", err)
}
if tree != "" {
objects = append(objects, &S3ObjectTree{
Environment: env,
Tree: tree,
})
}
}
resources := make(map[string][]*stack.Resource)
if d.enableResources {
for _, env := range environments {
svcDescr, err := d.initWkldStackDescriber(env)
if err != nil {
return nil, err
}
stackResources, err := svcDescr.StackResources()
if err != nil {
return nil, fmt.Errorf("retrieve service resources: %w", err)
}
resources[env] = stackResources
}
}
return &staticSiteDesc{
Service: d.svc,
Type: manifestinfo.StaticSiteType,
App: d.app,
Routes: routes,
Resources: resources,
Objects: objects,
environments: environments,
}, nil
}
// Manifest returns the contents of the manifest used to deploy a static site stack.
// If the Manifest metadata doesn't exist in the stack template, then returns ErrManifestNotFoundInTemplate.
func (d *StaticSiteDescriber) Manifest(env string) ([]byte, error) {
cfn, err := d.initWkldStackDescriber(env)
if err != nil {
return nil, err
}
return cfn.Manifest()
}
// S3ObjectTree contains serialized parameters for an S3 object tree.
type S3ObjectTree struct {
Environment string
Tree string
}
// staticSiteDesc contains serialized parameters for a static site.
type staticSiteDesc struct {
Service string `json:"service"`
Type string `json:"type"`
App string `json:"application"`
Routes []*WebServiceRoute `json:"routes"`
Objects []*S3ObjectTree `json:"objects,omitempty"`
Resources deployedSvcResources `json:"resources,omitempty"`
environments []string `json:"-"`
}
// JSONString returns the stringified backendService struct with json format.
func (w *staticSiteDesc) JSONString() (string, error) {
b, err := json.Marshal(w)
if err != nil {
return "", fmt.Errorf("marshal static site description: %w", err)
}
return fmt.Sprintf("%s\n", b), nil
}
// HumanString returns the stringified backendService struct with human readable format.
func (w *staticSiteDesc) HumanString() string {
var b bytes.Buffer
writer := tabwriter.NewWriter(&b, minCellWidth, tabWidth, cellPaddingWidth, paddingChar, noAdditionalFormatting)
fmt.Fprint(writer, color.Bold.Sprint("About\n\n"))
writer.Flush()
fmt.Fprintf(writer, " %s\t%s\n", "Application", w.App)
fmt.Fprintf(writer, " %s\t%s\n", "Name", w.Service)
fmt.Fprintf(writer, " %s\t%s\n", "Type", w.Type)
if len(w.Routes) > 0 {
fmt.Fprint(writer, color.Bold.Sprint("\nRoutes\n\n"))
writer.Flush()
headers := []string{"Environment", "URL"}
fmt.Fprintf(writer, " %s\n", strings.Join(headers, "\t"))
fmt.Fprintf(writer, " %s\n", strings.Join(underline(headers), "\t"))
for _, route := range w.Routes {
fmt.Fprintf(writer, " %s\t%s\n", route.Environment, route.URL)
}
}
if len(w.Objects) != 0 {
fmt.Fprint(writer, color.Bold.Sprint("\nS3 Bucket Objects\n"))
writer.Flush()
for _, object := range w.Objects {
fmt.Fprintf(writer, "\n %s\t%s\n", "Environment", object.Environment)
fmt.Fprintf(writer, object.Tree)
}
writer.Flush()
}
if len(w.Resources) != 0 {
fmt.Fprint(writer, color.Bold.Sprint("\nResources\n"))
writer.Flush()
w.Resources.humanStringByEnv(writer, w.environments)
}
writer.Flush()
return b.String()
}