internal/pkg/deploy/deploy.go (264 lines of code) (raw):
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package deploy holds the structures to deploy infrastructure resources.
package deploy
import (
"fmt"
"slices"
"sort"
"github.com/aws/aws-sdk-go/aws/arn"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/copilot-cli/internal/pkg/manifest/manifestinfo"
rg "github.com/aws/copilot-cli/internal/pkg/aws/resourcegroups"
"github.com/aws/copilot-cli/internal/pkg/config"
)
const (
// AppTagKey is tag key for Copilot app.
AppTagKey = "copilot-application"
// EnvTagKey is tag key for Copilot env.
EnvTagKey = "copilot-environment"
// ServiceTagKey is tag key for Copilot service.
ServiceTagKey = "copilot-service"
// PipelineTagKey is tag key for Copilot pipeline.
PipelineTagKey = "copilot-pipeline"
// TaskTagKey is tag key for Copilot task.
TaskTagKey = "copilot-task"
)
const (
stackResourceType = "cloudformation:stack"
pipelineResourceType = "codepipeline:pipeline"
snsResourceType = "sns"
// fmtSNSTopicNamePrefix holds the App-Env-Workload- components of a topic name
fmtSNSTopicNamePrefix = "%s-%s-%s-"
snsServiceName = "sns"
)
// ResourceGetter retrieves a group of resources that satisfy certain conditions, such as tags.
type ResourceGetter interface {
GetResourcesByTags(resourceType string, tags map[string]string) ([]*rg.Resource, error)
}
// ConfigStoreClient wraps config store methods utilized by deploy store.
type ConfigStoreClient interface {
GetEnvironment(appName string, environmentName string) (*config.Environment, error)
ListEnvironments(appName string) ([]*config.Environment, error)
ListWorkloads(appName string) ([]*config.Workload, error)
GetService(appName, svcName string) (*config.Workload, error)
GetJob(appName, jobname string) (*config.Workload, error)
}
// SessionProvider is the interface to provide configuration for the AWS SDK's service clients.
type SessionProvider interface {
FromRole(roleARN, region string) (*session.Session, error)
}
// Store fetches information on deployed services.
type Store struct {
configStore ConfigStoreClient
newRgClientFromIDs func(string, string) (ResourceGetter, error)
newRgClientFromRole func(string, string) (ResourceGetter, error)
}
// NewStore returns a new store.
func NewStore(sessProvider SessionProvider, store ConfigStoreClient) (*Store, error) {
s := &Store{
configStore: store,
}
s.newRgClientFromIDs = func(appName, envName string) (ResourceGetter, error) {
env, err := s.configStore.GetEnvironment(appName, envName)
if err != nil {
return nil, fmt.Errorf("get environment config %s: %w", envName, err)
}
sess, err := sessProvider.FromRole(env.ManagerRoleARN, env.Region)
if err != nil {
return nil, fmt.Errorf("create new session from env role: %w", err)
}
return rg.New(sess), nil
}
s.newRgClientFromRole = func(roleARN, region string) (ResourceGetter, error) {
sess, err := sessProvider.FromRole(roleARN, region)
if err != nil {
return nil, fmt.Errorf("create new session from env role: %w", err)
}
return rg.New(sess), nil
}
return s, nil
}
// Pipeline is a deployed pipeline.
type Pipeline struct {
// The name of the application that the pipeline is associated with.
AppName string
// The pipeline resource name (physical resource ID) generated by CloudFormation.
ResourceName string
// The name given by user in the pipeline manifest.
Name string
// Whether the pipeline follows legacy-naming, i.e. not namespaced with "pipeline-app-".
IsLegacy bool
}
// PipelineStore fetches information on deployed pipelines.
type PipelineStore struct {
getter ResourceGetter
}
// NewPipelineStore returns a new PipelineStore.
func NewPipelineStore(getter ResourceGetter) *PipelineStore {
return &PipelineStore{
getter: getter,
}
}
// ListDeployedPipelines returns a list of names of deployed pipelines by looking up
// pipeline resources with tags.
func (p *PipelineStore) ListDeployedPipelines(appName string) ([]Pipeline, error) {
var pipelines []Pipeline
pipelineResources, err := p.getter.GetResourcesByTags(pipelineResourceType, map[string]string{
AppTagKey: appName,
})
if err != nil {
return nil, fmt.Errorf("get pipeline resources by tags for app %s: %w", appName, err)
}
for _, pipelineRes := range pipelineResources {
resourceName, err := getPipelineResourceName(pipelineRes.ARN)
if err != nil {
return nil, err
}
pipeline := Pipeline{
ResourceName: resourceName,
AppName: appName,
}
if name, ok := pipelineRes.Tags[PipelineTagKey]; !ok {
pipeline.IsLegacy = true
pipeline.Name = resourceName // A legacy-named pipeline's resource name is the same as pipeline name.
} else {
pipeline.Name = name
}
pipelines = append(pipelines, pipeline)
}
return pipelines, nil
}
// ListDeployedServices returns the names of deployed services in an environment.
func (s *Store) ListDeployedServices(appName string, envName string) ([]string, error) {
return s.listDeployedWorkloads(appName, envName, manifestinfo.ServiceTypes())
}
// ListDeployedJobs returns the names of deployed jobs in an environment.
func (s *Store) ListDeployedJobs(appName string, envName string) ([]string, error) {
return s.listDeployedWorkloads(appName, envName, manifestinfo.JobTypes())
}
// ListDeployedWorkloads returns the names of deployed workloads in an environment.
func (s *Store) ListDeployedWorkloads(appName string, envName string) ([]string, error) {
return s.listDeployedWorkloads(appName, envName, manifestinfo.WorkloadTypes())
}
func (s *Store) listDeployedWorkloads(appName string, envName string, workloadType []string) ([]string, error) {
allWorkloads, err := s.configStore.ListWorkloads(appName)
if err != nil {
return nil, fmt.Errorf("list all workloads in application %s: %w", appName, err)
}
filteredWorkloadNames := make(map[string]bool)
for _, wkld := range allWorkloads {
for _, t := range workloadType {
if wkld.Type != t {
continue
}
filteredWorkloadNames[wkld.Name] = true
}
}
rgClient, err := s.newRgClientFromIDs(appName, envName)
if err != nil {
return nil, err
}
resources, err := rgClient.GetResourcesByTags(stackResourceType, map[string]string{
AppTagKey: appName,
EnvTagKey: envName,
})
if err != nil {
return nil, fmt.Errorf("get resources by Copilot tags: %w", err)
}
var wklds []string
for _, resource := range resources {
name := resource.Tags[ServiceTagKey]
if name == "" || slices.Contains(wklds, name) {
// To avoid listing duplicate service entry in a case when service has addons stack.
continue
}
if _, ok := filteredWorkloadNames[name]; ok {
wklds = append(wklds, name)
}
}
sort.Strings(wklds)
return wklds, nil
}
// ListSNSTopics returns a list of SNS topics deployed to the current environment and tagged with
// Copilot identifiers.
func (s *Store) ListSNSTopics(appName string, envName string) ([]Topic, error) {
rgClient, err := s.newRgClientFromIDs(appName, envName)
if err != nil {
return nil, err
}
topics, err := rgClient.GetResourcesByTags(snsResourceType, map[string]string{
AppTagKey: appName,
EnvTagKey: envName,
})
if err != nil {
return nil, err
}
var out []Topic
for _, r := range topics {
// If the topic doesn't have a specific workload tag, don't return it.
if _, ok := r.Tags[ServiceTagKey]; !ok {
continue
}
t, err := NewTopic(r.ARN, appName, envName, r.Tags[ServiceTagKey])
if err != nil {
// If there's an error parsing the topic ARN, don't include it in the list of topics.
// This includes times where the topic name does not match its tags, or the name
// is invalid.
switch err {
case errInvalidARN:
// This error indicates that the returned ARN is not parseable.
return nil, err
default:
continue
}
}
out = append(out, *t)
}
return out, nil
}
type result struct {
name string
err error
}
func (s *Store) deployedServices(rgClient ResourceGetter, app, env, svc string) result {
resources, err := rgClient.GetResourcesByTags(stackResourceType, map[string]string{
AppTagKey: app,
EnvTagKey: env,
ServiceTagKey: svc,
})
if err != nil {
return result{err: fmt.Errorf("get resources by Copilot tags: %w", err)}
}
// If no resources found, the resp length is 0.
var res result
if len(resources) != 0 {
res.name = env
}
return res
}
// ListEnvironmentsDeployedTo returns all the environment that a service is deployed in.
func (s *Store) ListEnvironmentsDeployedTo(appName string, svcName string) ([]string, error) {
envs, err := s.configStore.ListEnvironments(appName)
if err != nil {
return nil, fmt.Errorf("list environment for app %s: %w", appName, err)
}
deployedEnv := make(chan result, len(envs))
defer close(deployedEnv)
for _, env := range envs {
go func(env *config.Environment) {
rgClient, err := s.newRgClientFromRole(env.ManagerRoleARN, env.Region)
if err != nil {
deployedEnv <- result{err: err}
return
}
deployedEnv <- s.deployedServices(rgClient, appName, env.Name, svcName)
}(env)
}
var envsWithDeployment []string
for i := 0; i < len(envs); i++ {
env := <-deployedEnv
if env.err != nil {
return nil, env.err
}
if env.name != "" {
envsWithDeployment = append(envsWithDeployment, env.name)
}
}
return envsWithDeployment, nil
}
// IsServiceDeployed returns whether a service is deployed in an environment or not.
func (s *Store) IsServiceDeployed(appName string, envName string, svcName string) (bool, error) {
return s.IsWorkloadDeployed(appName, envName, svcName)
}
// IsJobDeployed returns whether a job is deployed in an environment or not by checking for a state machine.
func (s *Store) IsJobDeployed(appName, envName, jobName string) (bool, error) {
return s.IsWorkloadDeployed(appName, envName, jobName)
}
// IsWorkloadDeployed returns whether a workload is deployed in an environment or not.
func (s *Store) IsWorkloadDeployed(appName, envName, name string) (bool, error) {
rgClient, err := s.newRgClientFromIDs(appName, envName)
if err != nil {
return false, err
}
stacks, err := rgClient.GetResourcesByTags(stackResourceType, map[string]string{
AppTagKey: appName,
EnvTagKey: envName,
ServiceTagKey: name,
})
if err != nil {
return false, fmt.Errorf("get resources by Copilot tags: %w", err)
}
if len(stacks) != 0 {
return true, nil
}
return false, nil
}
func getPipelineResourceName(resourceArn string) (string, error) {
parsedArn, err := arn.Parse(resourceArn)
if err != nil {
return "", fmt.Errorf("parse pipeline ARN: %s", resourceArn)
}
return parsedArn.Resource, nil
}