Skip to content

Commit

Permalink
chore: setup proxy connections for run local --proxy (aws#5439)
Browse files Browse the repository at this point in the history
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the Apache 2.0 License.
  • Loading branch information
dannyrandall authored Nov 7, 2023
1 parent ebe48f9 commit 8491d15
Show file tree
Hide file tree
Showing 11 changed files with 803 additions and 136 deletions.
4 changes: 3 additions & 1 deletion internal/pkg/cli/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const (
portOverrideFlag = "port-override"
envVarOverrideFlag = "env-var-override"
proxyFlag = "proxy"
proxyNetworkFlag = "proxy-network"

// Flags for CI/CD.
githubURLFlag = "github-url"
Expand Down Expand Up @@ -321,7 +322,8 @@ Defaults to all logs. Only one of end-time / follow may be used.`
Format: [container]:KEY=VALUE. Omit container name to apply to all containers.`
portOverridesFlagDescription = `Optional. Override ports exposed by service. Format: <host port>:<service port>.
Example: --port-override 5000:80 binds localhost:5000 to the service's port 80.`
proxyFlagDescription = `Optional. Proxy outbound requests to your environment's VPC.`
proxyFlagDescription = `Optional. Proxy outbound requests to your environment's VPC.`
proxyNetworkFlagDescription = `Optional. Set the IP Network used by --proxy.`

svcManifestFlagDescription = `Optional. Name of the environment in which the service was deployed;
output the manifest file used for that deployment.`
Expand Down
2 changes: 2 additions & 0 deletions internal/pkg/cli/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ type repositoryService interface {
type ecsClient interface {
TaskDefinition(app, env, svc string) (*awsecs.TaskDefinition, error)
ServiceConnectServices(app, env, svc string) ([]*awsecs.Service, error)
DescribeService(app, env, svc string) (*ecs.ServiceDesc, error)
}

type logEventsWriter interface {
Expand Down Expand Up @@ -710,6 +711,7 @@ type dockerEngineRunner interface {
Stop(context.Context, string) error
Rm(string) error
Build(context.Context, *dockerengine.BuildArguments, io.Writer) error
Exec(ctx context.Context, container string, out io.Writer, cmd string, args ...string) error
}

type workloadStackGenerator interface {
Expand Down
34 changes: 34 additions & 0 deletions internal/pkg/cli/mocks/mock_interfaces.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

80 changes: 64 additions & 16 deletions internal/pkg/cli/run_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ package cli

import (
"context"
"errors"
"fmt"
"net"
"os"
"os/signal"
"slices"
Expand Down Expand Up @@ -58,12 +60,12 @@ const (

type containerOrchestrator interface {
Start() <-chan error
RunTask(orchestrator.Task)
RunTask(orchestrator.Task, ...orchestrator.RunTaskOption)
Stop()
}

type hostFinder interface {
Hosts(context.Context) ([]host, error)
Hosts(context.Context) ([]orchestrator.Host, error)
}

type runLocalVars struct {
Expand All @@ -74,6 +76,7 @@ type runLocalVars struct {
envOverrides map[string]string
portOverrides portOverrides
proxy bool
proxyNetwork net.IPNet
}

type runLocalOpts struct {
Expand Down Expand Up @@ -287,18 +290,22 @@ func (o *runLocalOpts) Execute() error {
return fmt.Errorf("get task: %w", err)
}

var hosts []orchestrator.Host
var ssmTarget string
if o.proxy {
if err := validateMinEnvVersion(o.ws, o.envChecker, o.appName, o.envName, template.RunLocalProxyMinEnvVersion, "run local --proxy"); err != nil {
return err
}

hosts, err := o.hostFinder.Hosts(ctx)
hosts, err = o.hostFinder.Hosts(ctx)
if err != nil {
return fmt.Errorf("find hosts to connect to: %w", err)
}

// TODO(dannyrandall): inject into orchestrator and use in pause container
fmt.Printf("hosts: %+v\n", hosts)
ssmTarget, err = o.getSSMTarget(ctx)
if err != nil {
return fmt.Errorf("get proxy target container: %w", err)
}
}

mft, _, err := workloadManifest(&workloadManifestInput{
Expand Down Expand Up @@ -334,7 +341,11 @@ func (o *runLocalOpts) Execute() error {
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)

errCh := o.orchestrator.Start()
o.orchestrator.RunTask(task)
var runTaskOpts []orchestrator.RunTaskOption
if o.proxy {
runTaskOpts = append(runTaskOpts, orchestrator.RunTaskWithProxy(ssmTarget, o.proxyNetwork, hosts...))
}
o.orchestrator.RunTask(task, runTaskOpts...)

for {
select {
Expand All @@ -354,6 +365,41 @@ func (o *runLocalOpts) Execute() error {
}
}

// getSSMTarget returns a AWS SSM target for a running container
// that supports ECS Service Exec.
func (o *runLocalOpts) getSSMTarget(ctx context.Context) (string, error) {
svc, err := o.ecsClient.DescribeService(o.appName, o.envName, o.wkldName)
if err != nil {
return "", fmt.Errorf("describe service: %w", err)
}

for _, task := range svc.Tasks {
// TaskArn should have the format: arn:aws:ecs:us-west-2:123456789:task/clusterName/taskName
taskARN, err := arn.Parse(aws.StringValue(task.TaskArn))
if err != nil {
return "", fmt.Errorf("parse task arn: %w", err)
}

split := strings.Split(taskARN.Resource, "/")
if len(split) != 3 {
return "", fmt.Errorf("task ARN in unexpected format: %q", taskARN)
}
taskName := split[2]

for _, ctr := range task.Containers {
id := aws.StringValue(ctr.RuntimeId)
hasECSExec := slices.ContainsFunc(ctr.ManagedAgents, func(a *sdkecs.ManagedAgent) bool {
return aws.StringValue(a.Name) == "ExecuteCommandAgent" && aws.StringValue(a.LastStatus) == "RUNNING"
})
if id != "" && hasECSExec && aws.StringValue(ctr.LastStatus) == "RUNNING" {
return fmt.Sprintf("ecs:%s_%s_%s", svc.ClusterName, taskName, aws.StringValue(ctr.RuntimeId)), nil
}
}
}

return "", errors.New("no running tasks have running containers with ecs exec enabled")
}

func (o *runLocalOpts) getTask(ctx context.Context) (orchestrator.Task, error) {
td, err := o.ecsClient.TaskDefinition(o.appName, o.envName, o.wkldName)
if err != nil {
Expand Down Expand Up @@ -617,25 +663,20 @@ func (o *runLocalOpts) getSecret(ctx context.Context, valueFrom string) (string,
return getter.GetSecretValue(ctx, valueFrom)
}

type host struct {
host string
port string
}

type hostDiscoverer struct {
ecs ecsClient
app string
env string
wkld string
}

func (h *hostDiscoverer) Hosts(ctx context.Context) ([]host, error) {
func (h *hostDiscoverer) Hosts(ctx context.Context) ([]orchestrator.Host, error) {
svcs, err := h.ecs.ServiceConnectServices(h.app, h.env, h.wkld)
if err != nil {
return nil, fmt.Errorf("get service connect services: %w", err)
}

var hosts []host
var hosts []orchestrator.Host
for _, svc := range svcs {
// find the primary deployment with service connect enabled
idx := slices.IndexFunc(svc.Deployments, func(dep *sdkecs.Deployment) bool {
Expand All @@ -647,9 +688,9 @@ func (h *hostDiscoverer) Hosts(ctx context.Context) ([]host, error) {

for _, sc := range svc.Deployments[idx].ServiceConnectConfiguration.Services {
for _, alias := range sc.ClientAliases {
hosts = append(hosts, host{
host: aws.StringValue(alias.DnsName),
port: strconv.Itoa(int(aws.Int64Value(alias.Port))),
hosts = append(hosts, orchestrator.Host{
Name: aws.StringValue(alias.DnsName),
Port: strconv.Itoa(int(aws.Int64Value(alias.Port))),
})
}
}
Expand Down Expand Up @@ -684,5 +725,12 @@ func BuildRunLocalCmd() *cobra.Command {
cmd.Flags().Var(&vars.portOverrides, portOverrideFlag, portOverridesFlagDescription)
cmd.Flags().StringToStringVar(&vars.envOverrides, envVarOverrideFlag, nil, envVarOverrideFlagDescription)
cmd.Flags().BoolVar(&vars.proxy, proxyFlag, false, proxyFlagDescription)
cmd.Flags().IPNetVar(&vars.proxyNetwork, proxyNetworkFlag, net.IPNet{
// docker uses 172.17.0.0/16 for networking by default
// so we'll default to different /16 from the 172.16.0.0/12
// private network defined by RFC 1918.
IP: net.IPv4(172, 20, 0, 0),
Mask: net.CIDRMask(16, 32),
}, proxyNetworkFlag)
return cmd
}
Loading

0 comments on commit 8491d15

Please sign in to comment.