Skip to content

Commit

Permalink
w.i.p.
Browse files Browse the repository at this point in the history
  • Loading branch information
thallgren committed Jan 15, 2025
1 parent de7f1b1 commit 33c0163
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 26 deletions.
3 changes: 1 addition & 2 deletions cmd/traffic/cmd/agent/containerstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,8 @@ func (c *containerState) HandleIntercepts(ctx context.Context, iis []*manager.In
for _, ii := range iis {
if ii.Disposition == manager.InterceptDispositionType_WAITING {
spec := ii.Spec
dlog.Debugf(ctx, "container %s checking replace intercept %q(%d) %s", c.Name(), spec.ContainerName, spec.ContainerPort, spec.Name)
if c.Replace() && c.Name() == spec.ContainerName && spec.ContainerPort == 0 {
dlog.Debugf(ctx, "container %s handling replace intercept %s", c.Name(), spec.Name)
dlog.Debugf(ctx, "container %s handling replace %s", c.Name(), spec.Name)
rs = append(rs, &manager.ReviewInterceptRequest{
Id: ii.Id,
Disposition: manager.InterceptDispositionType_ACTIVE,
Expand Down
5 changes: 4 additions & 1 deletion cmd/traffic/cmd/manager/state/intercept.go
Original file line number Diff line number Diff line change
Expand Up @@ -888,13 +888,16 @@ func findContainer(ac *agentconfig.Sidecar, spec *rpc.InterceptSpec) (foundCN *a
if len(ac.Containers) == 1 {
return ac.Containers[0], nil
}
return nil, errcat.User.Newf("%s %s.%s has more than one container",
ac.WorkloadKind, ac.WorkloadName, ac.Namespace)
}
for _, cn := range ac.Containers {
if spec.ContainerName == cn.Name {
return cn, nil
}
}
return nil, errcat.User.Newf("intercept %s does not uniquely identify a container in %s %s.%s", spec.Name, ac.WorkloadKind, ac.WorkloadName, ac.Namespace)
return nil, errcat.User.Newf("%s %s.%s has no container named %s",
ac.WorkloadKind, ac.WorkloadName, ac.Namespace, spec.ContainerName)
}

// findIntercept finds the intercept configuration that matches the given InterceptSpec's service/service port or container port.
Expand Down
110 changes: 108 additions & 2 deletions docs/howtos/intercepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Telepresence offers three powerful ways to develop your services locally:
- The replaced container is restored when the replace operation ends.
* **Use-cases:**
- You're working with message queue consumers and must stop the remote container.
- You're working with remote containers that have no incoming traffic.
- You're working with remote containers configured without incoming traffic.

### Intercept
* **How it Works:**
Expand All @@ -47,7 +47,7 @@ Telepresence offers three powerful ways to develop your services locally:
- No traffic is rerouted and all containers keep on running.
* **Use-cases:**
- You want to keep the impact of your local development to a minimum.
- You don't need any traffic from the cluster, and you don't need write access to the container's volumes.
- You have don't need traffic being routed from the cluster, and read-only access to the container's volumes is ok.

## Prerequisites

Expand All @@ -57,6 +57,112 @@ in several examples. OpenShift users can substitute oc [commands instead](https:
This guide assumes you have an application represented by a Kubernetes deployment and service accessible publicly by an ingress controller,
and that you can run a copy of that application on your laptop.

## Replace the cluster container

This approach offers the benefit of direct cluster connectivity from your workstation, simplifying debugging and
modification of your application within its familiar environment. However, it requires root access to configure
network telepresence, and remote mounts must be made relative to a specific mount point, which can add complexity.

1. Connect to your cluster with `telepresence connect` and try to curl to the Kubernetes API server. A 401 or 403 response code is expected and indicates that the service could be reached:

```console
$ curl -ik https://kubernetes.default
HTTP/1.1 401 Unauthorized
Cache-Control: no-cache, private
Content-Type: application/json
...
```

You now have access to your remote Kubernetes API server as if you were on the same network. You can now use any local tools to connect to any service in the cluster.

2. Enter `telepresence list` and make sure the workload (deployment in this case) you want to intercept is listed. For example:

```console
$ telepresence list
...
deolpoyment example-app: ready to engage (traffic-agent not yet installed)
...
```

3. Get the name of the container you want to replace (output truncated for brewity)
```console
$ kubectl describe deploy example-app
Name: example-app
Namespace: default
CreationTimestamp: Tue, 14 Jan 2025 03:49:29 +0100
Labels: app=example-app
Annotations: deployment.kubernetes.io/revision: 1
Selector: app=example-app
Replicas: 1 desired | 1 updated | 1 total | 0 available | 1 unavailable
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Pod Template:
Labels: app=example-app
Containers:
echo-server:
Image: ghcr.io/telepresencio/echo-server
Port: 8080/TCP
```

4. Replace the container. Please note that the `--container echo-server` flag here is optional. It's only needed when the workload has more than one container:
```console
$ telepresence replace example-app --container echo-server --env-file /tmp/example-app.env --mount /tmp/example-app-mounts
Using Deployment example-app
Container name : echo-server
State : ACTIVE
Workload kind : Deployment
Port forwards : 10.1.4.106 -> 127.0.0.1
8080 -> 8080 TCP
Volume Mount Point: /tmp/example-app-mounts
```
Your workstation is now ready. You can run the application using the environment in the `/tmp/example-app.env` file and the
mounts under `/tmp/example-app-mounts`. The application can listen to `localhost:8080` to receive traffic intended for the
replaced container. On the cluster side of things, a Traffic Agent container has replaced the `echo-server`.

Telepresence assumes that you want all declared container ports to be mapped to their corresponding port on `localhost`. You
can change this with the `--port` flag. For example, `--port 1080:8080` will map the replaced containers port number `8080`
to `localhost:1080`. The `--port` can also be used when the container is known to listen to ports that are not declared in
the manifest.

5. Query the cluster in which you replaced your application and verify your local instance being invoked. All the traffic previously routed to your Kubernetes Service is now routed to your local environment

You can now:
- Make changes on the fly and see them reflected when interacting with your Kubernetes environment.
- Query services only exposed in your cluster's network.
- Set breakpoints in your IDE to investigate bugs.

6. You end the replace operation with the command `telepresence leave example-app --container echo-server`

## Ingest the cluster container

In some situations, you want to work and debug the code locally, and you want it to be able to access other services in the cluster,
but you don't wish to interfere with the targeted workload. This is where the `telepresence ingest` command comes into play. Just
like `replace` command, it will make the environment and mounted containers of the targeted container available locally, but it will
not replace the container nor will it intercept any of its traffic.

This example assumes that you have the `example-app` deployment.

1. Connect and run and start an ingest from `example-app`:
```console
$ telepresence connect
Launching Telepresence User Daemon
Launching Telepresence Root Daemon
Connected to context xxx, namespace default (https://<some url>)
$ telepresence ingest example-app --container echo-server --env-file /tmp/example-app.env --mount /tmp/example-app-mounts
Using Deployment example-app
Container name : echo-server
Workload kind : Deployment
Volume Mount Point: /tmp/example-app-mounts
```

2. Start your local application using the environment variables retrieved and the volumes that were mounted in the previous step.

You can now:
- Code and debug your local app while it interacts with other services in your cluster.
- Query services only exposed in your cluster's network.
- Set breakpoints in your IDE to investigate bugs.

## Intercept your application

### Running everything directly on the workstation
Expand Down
48 changes: 39 additions & 9 deletions pkg/client/cli/cmd/leave.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ func leave() *cobra.Command {
}
ctx := cmd.Context()
userD := daemon.GetUserClient(ctx)
resp, err := userD.List(ctx, &connector.ListRequest{Filter: connector.ListRequest_INTERCEPTS | connector.ListRequest_INGESTS})
resp, err := userD.List(ctx, &connector.ListRequest{
Filter: connector.ListRequest_INTERCEPTS | connector.ListRequest_REPLACEMENTS | connector.ListRequest_INGESTS})

Check failure on line 50 in pkg/client/cli/cmd/leave.go

View workflow job for this annotation

GitHub Actions / unit (ubuntu-latest)

File is not properly formatted (gofumpt)

Check failure on line 50 in pkg/client/cli/cmd/leave.go

View workflow job for this annotation

GitHub Actions / unit (macos-latest)

File is not properly formatted (gofumpt)

Check failure on line 50 in pkg/client/cli/cmd/leave.go

View workflow job for this annotation

GitHub Actions / unit (windows-latest)

File is not properly formatted (gofumpt)
if err != nil {
return nil, shellCompDir | cobra.ShellCompDirectiveError
}
Expand All @@ -72,22 +73,42 @@ func leave() *cobra.Command {
return completions, shellCompDir
},
}
cmd.Flags().StringVarP(&containerName, "container", "c", "", "Container name (only relevant for ingest)")
cmd.Flags().StringVarP(&containerName, "container", "c", "", "Container name (only relevant for ingest and replace)")
return cmd
}

func findReplacementByWorkload(ctx context.Context, name string) (ic *manager.InterceptInfo, err error) {
userD := daemon.GetUserClient(ctx)
ls, err := userD.List(ctx, &connector.ListRequest{Filter: connector.ListRequest_REPLACEMENTS})
if err != nil {
return nil, err
}
desiredPfx := name + "/"
for _, l := range ls.Workloads {
if strings.HasPrefix(l.Name, desiredPfx) {
ic, err = userD.GetIntercept(ctx, &manager.GetInterceptRequest{Name: l.Name})
if err != nil && status.Code(err) != codes.NotFound {
return nil, err
}
}
}
return ic, nil
}

func removeIngestOrIntercept(ctx context.Context, name, container string) error {
userD := daemon.GetUserClient(ctx)

var ic *manager.InterceptInfo
var ig *connector.IngestInfo
var env map[string]string
var err error
if container == "" {
ic, err = userD.GetIntercept(ctx, &manager.GetInterceptRequest{Name: name})
if err != nil && status.Code(err) != codes.NotFound {
return err
}
icName := name
if container != "" {
icName += "/" + container
}
ic, err = userD.GetIntercept(ctx, &manager.GetInterceptRequest{Name: icName})
if err != nil && status.Code(err) != codes.NotFound {
return err
}

if ic == nil {
Expand All @@ -99,8 +120,17 @@ func removeIngestOrIntercept(ctx context.Context, name, container string) error
if status.Code(err) != codes.NotFound {
return err
}
// User probably misspelled the name of the intercept/ingest
return errcat.User.Newf("Intercept or ingest named %q not found", name)
if container == "" {
// Also check for replacements using name that isn't qualified with container.
ic, err = findReplacementByWorkload(ctx, name)
if err != nil {
return err
}
}
if ic == nil {
// User probably misspelled the name of the replace/intercept/ingest
return errcat.User.Newf("Replace, intercept, or ingest named %q not found", icName)
}
}
env = ig.Environment
} else {
Expand Down
2 changes: 1 addition & 1 deletion pkg/client/cli/ingest/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func NewInfo(ctx context.Context, ii *rpc.IngestInfo, mountError error) *Info {
func (ii *Info) WriteTo(w io.Writer) (int64, error) {
kvf := ioutil.DefaultKeyValueFormatter()
kvf.Prefix = " "
kvf.Add("Container", ii.Container)
kvf.Add("Container name", ii.Container)
kvf.Add("Workload kind", ii.WorkloadKind)
if m := ii.Mount; m != nil {
if m.LocalDir != "" {
Expand Down
8 changes: 4 additions & 4 deletions pkg/client/cli/intercept/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ func (c *Command) AddInterceptFlags(cmd *cobra.Command) {

func (c *Command) AddReplaceFlags(cmd *cobra.Command) {
flagSet := cmd.Flags()
flagSet.StringVarP(&c.AgentName, "workload", "w", "", "Name of workload (Deployment, ReplicaSet, StatefulSet, Rollout) where container is replaced, if different from <name>")
flagSet.StringSliceVarP(&c.Ports, "port", "p", []string{"all"}, ``+
`Local ports to forward to. Use <local port>:<identifier> to uniquely identify container ports, where the <identifier> is the port name or number. `+
`Use "all" (the default) to forward all ports declared in the replaced container to their corresponding local port. `,
Expand Down Expand Up @@ -161,16 +160,17 @@ func (c *Command) ValidateReplace(cmd *cobra.Command, positional []string) error
return errcat.User.New("commands to be run with replace must come after options")
}
c.Name = positional[0]
c.AgentName = c.Name
c.Cmdline = positional[1:]
c.FormattedOutput = output.WantsFormatted(cmd)
c.Mechanism = "tcp"
c.Replace = true
c.NoDefaultPort = true

// Actually intercepting something
if c.AgentName == "" {
c.AgentName = c.Name
if c.ContainerName != "" {
c.Name += "/" + c.ContainerName
}

if err := c.MountFlags.Validate(cmd); err != nil {
return err
}
Expand Down
14 changes: 7 additions & 7 deletions pkg/client/cli/intercept/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,11 @@ func NewInfo(ctx context.Context, ii *manager.InterceptInfo, ro bool, mountError
func (ii *Info) WriteTo(w io.Writer) (int64, error) {
kvf := ioutil.DefaultKeyValueFormatter()
kvf.Prefix = " "
nameKey := "Intercept name"
if ii.Replace {
nameKey = "Replace name"
kvf.Add("Container name", ii.ContainerName)
} else {
kvf.Add("Intercept name", ii.Name)
}
kvf.Add(nameKey, ii.Name)
kvf.Add("State", func() string {
msg := ""
if manager.InterceptDispositionType_value[ii.Disposition] > int32(manager.InterceptDispositionType_WAITING) {
Expand All @@ -140,7 +140,7 @@ func (ii *Info) WriteTo(w io.Writer) (int64, error) {
pkv := ioutil.DefaultKeyValueFormatter()
pkv.Indent = ""
pkv.Separator = " -> "
if ii.PortID != "" {
if ii.ContainerPort != 0 {
pm, _ := agentconfig.NewPortIdentifier(ii.Protocol, strconv.Itoa(int(ii.ContainerPort)))
pkv.Add(pm.String(), fmt.Sprintf("%d %s", ii.TargetPort, ii.Protocol))
}
Expand All @@ -149,11 +149,11 @@ func (ii *Info) WriteTo(w io.Writer) (int64, error) {
to := pm.To()
pkv.Add(pm.From().String(), fmt.Sprintf("%d %s", to.Port, to.Proto))
}
key := "Intercepting"
if ii.Replace {
kvf.Add("Port forwards", fmt.Sprintf("%s/%s -> %s\n%s", ii.PodIP, ii.ContainerName, ii.TargetHost, pkv))
} else {
kvf.Add("Intercepting", fmt.Sprintf("%s -> %s\n%s", ii.PodIP, ii.TargetHost, pkv))
key = "Port forwards"
}
kvf.Add(key, fmt.Sprintf("%s -> %s\n%s", ii.PodIP, ii.TargetHost, pkv))

if !ii.Global {
kvf.Add("Intercepting", func() string {
Expand Down
1 change: 1 addition & 0 deletions pkg/client/userd/trafficmgr/intercept.go
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@ func (s *session) AddIntercept(c context.Context, ir *rpc.CreateInterceptRequest
spec.Protocol = pi.Protocol
spec.ContainerPort = pi.ContainerPort
spec.ContainerName = pi.ContainerName
spec.Name = spec.Agent + "/" + pi.ContainerName
spec.PodPorts = pi.PodPorts
result = iInfo.InterceptResult()

Expand Down

0 comments on commit 33c0163

Please sign in to comment.