Repository: telepresenceio/telepresence Branch: release/v2 Commit: afb4f787ef2a Files: 862 Total size: 4.1 MB Directory structure: gitextract_r2gzeu42/ ├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── Bug_report.md │ │ └── Feature_request.md │ ├── actions/ │ │ ├── install-dependencies/ │ │ │ └── action.yaml │ │ └── upload-logs/ │ │ └── action.yaml │ ├── pull_request_template.md │ └── workflows/ │ ├── dev.yaml │ ├── go-dependency-submission.yaml │ ├── image-scan.yaml │ ├── licenses.yaml │ ├── release-teleroute.yaml │ ├── release.yaml │ ├── stale.yaml │ └── unit_tests.yaml ├── .gitignore ├── .golangci.yml ├── .mailmap ├── .protolint.yaml ├── CHANGELOG.OLD.md ├── CHANGELOG.yml ├── CLAUDE.md ├── CODE-OF-CONDUCT.md ├── CODEOWNERS ├── DEPENDENCIES.md ├── DEPENDENCY_LICENSES.md ├── GOVERNANCE-maintainer.md ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── README.md ├── SECURITY.md ├── build-aux/ │ ├── INSTALLERS.md │ ├── admission_controller_tls/ │ │ └── main.go │ ├── docker/ │ │ └── images/ │ │ ├── Dockerfile.client │ │ ├── Dockerfile.routecontroller │ │ ├── Dockerfile.traffic │ │ └── Dockerfile.winbuild │ ├── genversion/ │ │ └── main.go │ ├── image-importer.yaml │ ├── main.mk │ ├── maintenance.mk │ ├── pkg-installer/ │ │ ├── Distribution.xml │ │ ├── build-pkg.sh │ │ ├── io.telepresence.rootd.plist │ │ ├── postinstall │ │ ├── syslog.conf │ │ ├── telepresence-rootd │ │ ├── uninstall │ │ └── welcome.rtf │ ├── prelude.mk │ ├── systemd-installer/ │ │ ├── .gitignore │ │ ├── build-packages.sh │ │ ├── config.yml │ │ ├── nfpm.yaml.in │ │ ├── postinstall.sh │ │ ├── postremove.sh │ │ ├── preremove.sh │ │ ├── telepresence-rootd.service │ │ └── uninstall.sh │ ├── tools.mk │ ├── unparsable-packages.yaml │ ├── winmake.bat │ └── wix-installer/ │ ├── Dialogs_en-us.wxl │ ├── MainPackage.wxs │ ├── Makefile │ ├── Telepresence.wxs │ ├── config.yml │ ├── tpwrapper.go │ └── txt2rtf.ps1 ├── charts/ │ ├── chart.go │ └── telepresence-oss/ │ ├── .helmignore │ ├── Chart.yaml │ ├── README.md │ ├── templates/ │ │ ├── NOTES.txt │ │ ├── _helpers.tpl │ │ ├── agentInjectorWebhook.yaml │ │ ├── certificate.yaml │ │ ├── clientRbac/ │ │ │ ├── cluster-scope.yaml │ │ │ ├── connect.yaml │ │ │ └── namespace-scope.yaml │ │ ├── deployment.yaml │ │ ├── issuer.yaml │ │ ├── pre-delete-hook.yaml │ │ ├── routecontroller-daemonset.yaml │ │ ├── routecontroller-rbac.yaml │ │ ├── service.yaml │ │ ├── tests/ │ │ │ └── test-connection.yaml │ │ ├── trafficManager-configmap.yaml │ │ └── trafficManagerRbac/ │ │ ├── cluster-scope.yaml │ │ ├── namespace-scope.yaml │ │ ├── service-account.yaml │ │ └── webhook-secret.yaml │ ├── values.schema.yaml │ └── values.yaml ├── cmd/ │ ├── cobraparser/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── generate/ │ │ │ └── generate.go │ │ ├── go.mod │ │ ├── go.sum │ │ ├── main.go │ │ └── types/ │ │ └── commandinfo.go │ ├── routecontroller/ │ │ └── main.go │ └── teleroute/ │ ├── .gitignore │ ├── DEVELOPING.md │ ├── Dockerfile │ ├── Makefile │ ├── config.json │ ├── driver/ │ │ ├── driver.go │ │ ├── network.go │ │ └── options.go │ ├── go.mod │ ├── go.sum │ └── main.go ├── docs/ │ ├── CONTRIBUTING.md │ ├── README.md │ ├── common/ │ │ └── quantity.md │ ├── community.md │ ├── compare/ │ │ └── mirrord.md │ ├── concepts/ │ │ ├── devloop.md │ │ ├── faster.md │ │ └── intercepts.md │ ├── doc-links.yml │ ├── faqs.md │ ├── helm/ │ │ └── values.schema.json │ ├── howtos/ │ │ ├── cluster-in-vm.md │ │ ├── docker-compose.md │ │ ├── docker.md │ │ ├── engage.md │ │ ├── large-clusters.md │ │ └── mtls.md │ ├── install/ │ │ ├── client.md │ │ ├── cloud.md │ │ ├── manager.md │ │ └── upgrade.md │ ├── licenses.md │ ├── quick-start.md │ ├── redirects.yml │ ├── reference/ │ │ ├── architecture.md │ │ ├── cli/ │ │ │ ├── telepresence.md │ │ │ ├── telepresence_completion.md │ │ │ ├── telepresence_compose.md │ │ │ ├── telepresence_compose_attach.md │ │ │ ├── telepresence_compose_bridge.md │ │ │ ├── telepresence_compose_build.md │ │ │ ├── telepresence_compose_commit.md │ │ │ ├── telepresence_compose_config.md │ │ │ ├── telepresence_compose_cp.md │ │ │ ├── telepresence_compose_create.md │ │ │ ├── telepresence_compose_down.md │ │ │ ├── telepresence_compose_events.md │ │ │ ├── telepresence_compose_exec.md │ │ │ ├── telepresence_compose_export.md │ │ │ ├── telepresence_compose_images.md │ │ │ ├── telepresence_compose_kill.md │ │ │ ├── telepresence_compose_logs.md │ │ │ ├── telepresence_compose_ls.md │ │ │ ├── telepresence_compose_pause.md │ │ │ ├── telepresence_compose_port.md │ │ │ ├── telepresence_compose_ps.md │ │ │ ├── telepresence_compose_publish.md │ │ │ ├── telepresence_compose_pull.md │ │ │ ├── telepresence_compose_push.md │ │ │ ├── telepresence_compose_restart.md │ │ │ ├── telepresence_compose_rm.md │ │ │ ├── telepresence_compose_run.md │ │ │ ├── telepresence_compose_scale.md │ │ │ ├── telepresence_compose_start.md │ │ │ ├── telepresence_compose_stats.md │ │ │ ├── telepresence_compose_stop.md │ │ │ ├── telepresence_compose_top.md │ │ │ ├── telepresence_compose_unpause.md │ │ │ ├── telepresence_compose_up.md │ │ │ ├── telepresence_compose_version.md │ │ │ ├── telepresence_compose_volumes.md │ │ │ ├── telepresence_compose_wait.md │ │ │ ├── telepresence_compose_watch.md │ │ │ ├── telepresence_config.md │ │ │ ├── telepresence_config_view.md │ │ │ ├── telepresence_connect.md │ │ │ ├── telepresence_curl.md │ │ │ ├── telepresence_docker-run.md │ │ │ ├── telepresence_gather-logs.md │ │ │ ├── telepresence_genyaml.md │ │ │ ├── telepresence_genyaml_annotations.md │ │ │ ├── telepresence_genyaml_config.md │ │ │ ├── telepresence_genyaml_container.md │ │ │ ├── telepresence_genyaml_initcontainer.md │ │ │ ├── telepresence_genyaml_volume.md │ │ │ ├── telepresence_helm.md │ │ │ ├── telepresence_helm_install.md │ │ │ ├── telepresence_helm_lint.md │ │ │ ├── telepresence_helm_uninstall.md │ │ │ ├── telepresence_helm_upgrade.md │ │ │ ├── telepresence_helm_version.md │ │ │ ├── telepresence_ingest.md │ │ │ ├── telepresence_intercept.md │ │ │ ├── telepresence_leave.md │ │ │ ├── telepresence_list-contexts.md │ │ │ ├── telepresence_list-namespaces.md │ │ │ ├── telepresence_list.md │ │ │ ├── telepresence_loglevel.md │ │ │ ├── telepresence_mcp.md │ │ │ ├── telepresence_mcp_claude.md │ │ │ ├── telepresence_mcp_claude_disable.md │ │ │ ├── telepresence_mcp_claude_enable.md │ │ │ ├── telepresence_mcp_claude_list.md │ │ │ ├── telepresence_mcp_cursor.md │ │ │ ├── telepresence_mcp_cursor_disable.md │ │ │ ├── telepresence_mcp_cursor_enable.md │ │ │ ├── telepresence_mcp_cursor_list.md │ │ │ ├── telepresence_mcp_start.md │ │ │ ├── telepresence_mcp_stream.md │ │ │ ├── telepresence_mcp_tools.md │ │ │ ├── telepresence_mcp_vscode.md │ │ │ ├── telepresence_mcp_vscode_disable.md │ │ │ ├── telepresence_mcp_vscode_enable.md │ │ │ ├── telepresence_mcp_vscode_list.md │ │ │ ├── telepresence_quit.md │ │ │ ├── telepresence_replace.md │ │ │ ├── telepresence_revoke.md │ │ │ ├── telepresence_serve.md │ │ │ ├── telepresence_status.md │ │ │ ├── telepresence_uninstall.md │ │ │ ├── telepresence_version.md │ │ │ └── telepresence_wiretap.md │ │ ├── cluster-config.md │ │ ├── compose.md │ │ ├── config.md │ │ ├── dns.md │ │ ├── docker-run.md │ │ ├── engagements/ │ │ │ ├── cli.md │ │ │ ├── conflicts.md │ │ │ ├── container.md │ │ │ └── sidecar.md │ │ ├── environment.md │ │ ├── inside-container.md │ │ ├── monitoring.md │ │ ├── plugins.md │ │ ├── rbac.md │ │ ├── restapi.md │ │ ├── route-controller.md │ │ ├── routing.md │ │ ├── tun-device.md │ │ ├── volume.md │ │ └── vpn.md │ ├── release-notes.md │ ├── release-notes.mdx │ ├── troubleshooting.md │ └── variables.yml ├── examples/ │ ├── compose/ │ │ ├── proxy-voting.override.yaml │ │ ├── proxy-web.override.yaml │ │ └── replace.override.yaml │ └── docker/ │ └── iam-authenticator/ │ └── Dockerfile ├── go.mod ├── go.sum ├── integration_test/ │ ├── agent_injector_disabled_test.go │ ├── also_proxy_test.go │ ├── argo_rollouts_test.go │ ├── bind_to_podip_test.go │ ├── cidr_conflict_test.go │ ├── cli_test.go │ ├── cloud_config_test.go │ ├── compose_test.go │ ├── config_test.go │ ├── connected_test.go │ ├── container_test.go │ ├── docker_daemon_test.go │ ├── docker_run_test.go │ ├── env_interpolate_test.go │ ├── gather_logs_test.go │ ├── h2c_intercept_test.go │ ├── headless_test.go │ ├── helm_test.go │ ├── http_intercepts_test.go │ ├── ignored_mounts_test.go │ ├── inactive_client_test.go │ ├── ingest_test.go │ ├── inject_policy_test.go │ ├── injector_test.go │ ├── install_test.go │ ├── integration_test.go │ ├── intercept_env_test.go │ ├── intercept_flags_test.go │ ├── intercept_localhost_test.go │ ├── intercept_mount_test.go │ ├── itest/ │ │ ├── apply_app.go │ │ ├── assertions.go │ │ ├── cluster.go │ │ ├── config.go │ │ ├── connected.go │ │ ├── context.go │ │ ├── env.go │ │ ├── harness.go │ │ ├── helm.go │ │ ├── image.go │ │ ├── logdir.go │ │ ├── multiple_services.go │ │ ├── namespace.go │ │ ├── runner.go │ │ ├── single_service.go │ │ ├── status.go │ │ ├── suite.go │ │ ├── tempdir.go │ │ ├── template.go │ │ ├── timed.go │ │ └── traffic_manager.go │ ├── kubeauth_test.go │ ├── kubeconfig_extension_test.go │ ├── large_files_test.go │ ├── limitrange_test.go │ ├── list_watch_test.go │ ├── loglevel_test.go │ ├── manager_grpc_test.go │ ├── manual_agent_test.go │ ├── mounts_test.go │ ├── multi_connect_test.go │ ├── multiple_intercepts_test.go │ ├── multiple_port_intercept_test.go │ ├── multiple_services_test.go │ ├── multiport_test.go │ ├── namespaces_test.go │ ├── not_connected_test.go │ ├── otel_test.go │ ├── pod_cidr_test.go │ ├── podscaling_test.go │ ├── proxy_via_test.go │ ├── reconnect_session_test.go │ ├── replace_test.go │ ├── restapi_test.go │ ├── single_service_test.go │ ├── subdomain_test.go │ ├── svcdomain_test.go │ ├── testdata/ │ │ ├── apiserveraccess/ │ │ │ └── main.go │ │ ├── cli-container/ │ │ │ └── Dockerfile │ │ ├── count-server/ │ │ │ └── main.go │ │ ├── echo-server/ │ │ │ ├── Dockerfile │ │ │ ├── certs/ │ │ │ │ ├── tls.crt │ │ │ │ └── tls.key │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ └── main.go │ │ ├── k8s/ │ │ │ ├── client_experiment.yaml │ │ │ ├── client_rancher.goyaml │ │ │ ├── client_sa.yaml │ │ │ ├── disruption-budget.goyaml │ │ │ ├── echo-argo-rollout.yaml │ │ │ ├── echo-auto-inject.yaml │ │ │ ├── echo-both.yaml │ │ │ ├── echo-double-one-unnamed.yaml │ │ │ ├── echo-easy.yaml │ │ │ ├── echo-extra-udp.yaml │ │ │ ├── echo-headless.yaml │ │ │ ├── echo-interpolate.yaml │ │ │ ├── echo-manual-inject-deploy.yaml │ │ │ ├── echo-manual-inject-svc.yaml │ │ │ ├── echo-min.yaml │ │ │ ├── echo-no-containerport.yaml │ │ │ ├── echo-no-svc-ann.yaml │ │ │ ├── echo-no-svc.yaml │ │ │ ├── echo-no-vols.yaml │ │ │ ├── echo-one.yaml │ │ │ ├── echo-same-target-port.yaml │ │ │ ├── echo-secondary.yaml │ │ │ ├── echo-spring.yaml │ │ │ ├── echo-tls.goyaml │ │ │ ├── echo-udp-tcp-unnamed.yaml │ │ │ ├── echo-w-hostalias.goyaml │ │ │ ├── echo-w-sidecars.yaml │ │ │ ├── echo-w-subdomain.yaml │ │ │ ├── echo_with_env.yaml │ │ │ ├── generic.goyaml │ │ │ ├── hello-w-volumes.goyaml │ │ │ ├── many-volumes.yaml │ │ │ ├── memory-constraints.yaml │ │ │ ├── namespaces.yaml │ │ │ ├── pol-disabled.yaml │ │ │ ├── pol-enabled.yaml │ │ │ ├── pol-none.yaml │ │ │ ├── pv.goyaml │ │ │ ├── pvc.goyaml │ │ │ ├── rs-echo.yaml │ │ │ ├── secret-reader-rbac.goyaml │ │ │ ├── ss-echo.yaml │ │ │ ├── svc-deploy.goyaml │ │ │ ├── tel-cert.yaml │ │ │ └── with-probes.yaml │ │ ├── k8screds/ │ │ │ └── main.go │ │ ├── otel/ │ │ │ ├── helm-yamls/ │ │ │ │ └── otel-operator.yml │ │ │ └── instrumentation.yml │ │ ├── routing-values.yaml │ │ ├── scripts/ │ │ │ ├── veth-down.sh │ │ │ └── veth-up.sh │ │ ├── stdiotest/ │ │ │ └── main.go │ │ ├── telepresence-1.9.9.tgz │ │ └── udp-echo/ │ │ ├── Dockerfile │ │ ├── go.mod │ │ └── main.go │ ├── tls_test.go │ ├── to_pod_test.go │ ├── udp_test.go │ ├── uhn_dns_test.go │ ├── uninstall_test.go │ ├── webhook_test.go │ ├── wiretap_test.go │ ├── workload_configuration_test.go │ ├── workload_watch_test.go │ ├── workloads_test.go │ └── wpad_test.go ├── k8s/ │ ├── agent-injector-rbac.yaml │ ├── agent-injector-secret.yaml │ ├── agent-injector.yaml │ ├── apitest.yaml │ ├── dnsutils-headless.yaml │ ├── echo-auto-headless.yaml │ ├── echo-double-one.yaml │ ├── echo-double.yaml │ ├── echo-sc.yaml │ ├── ext-example.yaml │ ├── hello-w-volume.yaml │ ├── local-echo-easy.yaml │ ├── local-echo-next.yaml │ ├── manager.yaml │ ├── minikube-registry.yaml │ ├── private-reg-proxy.yaml │ └── rs-echo-svc2.yaml ├── packaging/ │ ├── artifacthub-repo.yml │ ├── bundle.wxs.in │ ├── helmpackage.go │ ├── homebrew-oss-formula.rb │ ├── homebrew-package.sh │ ├── install-telepresence.ps1 │ ├── telepresence.wxs.in │ └── windows-package.sh ├── pkg/ │ ├── agentconfig/ │ │ ├── container.go │ │ ├── container_test.go │ │ ├── initcontainer.go │ │ ├── injectpolicy.go │ │ ├── intercepttarget.go │ │ ├── sidecar.go │ │ ├── util.go │ │ └── volumes.go │ ├── agentmap/ │ │ ├── capsbase26.go │ │ ├── capsbase26_test.go │ │ ├── discorvery.go │ │ └── generator.go │ ├── annotation/ │ │ └── annotation.go │ ├── authenticator/ │ │ ├── authenticator.go │ │ ├── config.go │ │ ├── exec.go │ │ ├── exec_test.go │ │ ├── grpc/ │ │ │ └── authenticator.go │ │ └── patcher/ │ │ └── patcher.go │ ├── cache/ │ │ ├── client.go │ │ └── map.go │ ├── client/ │ │ ├── agentpf/ │ │ │ └── clients.go │ │ ├── bwcompat/ │ │ │ └── clusterinfo.go │ │ ├── cache/ │ │ │ ├── cache.go │ │ │ └── watcher.go │ │ ├── cli/ │ │ │ ├── ann/ │ │ │ │ └── annotations.go │ │ │ ├── cmd/ │ │ │ │ ├── completion.go │ │ │ │ ├── compose.go │ │ │ │ ├── config.go │ │ │ │ ├── connect.go │ │ │ │ ├── curl.go │ │ │ │ ├── docker_run.go │ │ │ │ ├── gather_logs.go │ │ │ │ ├── gather_logs_test.go │ │ │ │ ├── genyaml.go │ │ │ │ ├── helm.go │ │ │ │ ├── ingest.go │ │ │ │ ├── intercept.go │ │ │ │ ├── kubeauth.go │ │ │ │ ├── leave.go │ │ │ │ ├── list.go │ │ │ │ ├── list_contexts.go │ │ │ │ ├── list_namespaces.go │ │ │ │ ├── loglevel.go │ │ │ │ ├── man-pages.go │ │ │ │ ├── mcp.go │ │ │ │ ├── mcp_test.go │ │ │ │ ├── quit.go │ │ │ │ ├── replace.go │ │ │ │ ├── revoke.go │ │ │ │ ├── serve.go │ │ │ │ ├── status.go │ │ │ │ ├── telepresence.go │ │ │ │ ├── testdata/ │ │ │ │ │ ├── license │ │ │ │ │ ├── testLogDir/ │ │ │ │ │ │ ├── cli.log │ │ │ │ │ │ ├── connector-20210916T130347.log │ │ │ │ │ │ ├── connector-20210916T130356.log │ │ │ │ │ │ ├── connector-20210916T130624.log │ │ │ │ │ │ ├── connector-20210916T130643.log │ │ │ │ │ │ ├── connector.log │ │ │ │ │ │ ├── daemon-20210916T130318.log │ │ │ │ │ │ ├── daemon-20210916T130402.log │ │ │ │ │ │ ├── daemon-20210916T130624.log │ │ │ │ │ │ ├── daemon-20210916T130643.log │ │ │ │ │ │ ├── daemon.log │ │ │ │ │ │ ├── echo-auto-inject-6496f77cbd-n86nc │ │ │ │ │ │ └── traffic-manager-5c69859f94-g4ntj │ │ │ │ │ └── zipDir/ │ │ │ │ │ ├── diff_name.log │ │ │ │ │ ├── file1.log │ │ │ │ │ └── file2.log │ │ │ │ ├── uninstall.go │ │ │ │ ├── usage.go │ │ │ │ ├── version.go │ │ │ │ └── wiretap.go │ │ │ ├── connect/ │ │ │ │ ├── connector.go │ │ │ │ ├── daemon.go │ │ │ │ ├── init_command.go │ │ │ │ └── version_check.go │ │ │ ├── daemon/ │ │ │ │ ├── dial.go │ │ │ │ ├── identifier.go │ │ │ │ ├── identifier_test.go │ │ │ │ ├── info.go │ │ │ │ ├── request.go │ │ │ │ ├── request_test.go │ │ │ │ └── userd.go │ │ │ ├── docker/ │ │ │ │ ├── auto_complete.go │ │ │ │ ├── compose/ │ │ │ │ │ ├── config.go │ │ │ │ │ ├── connection.go │ │ │ │ │ ├── dc-cli.json │ │ │ │ │ ├── engagement.go │ │ │ │ │ ├── extension.go │ │ │ │ │ ├── extension_test.go │ │ │ │ │ ├── toplevelextension.go │ │ │ │ │ └── transform.go │ │ │ │ ├── flags.go │ │ │ │ ├── published_ports.go │ │ │ │ ├── run_flags.go │ │ │ │ └── runner.go │ │ │ ├── env/ │ │ │ │ ├── flags.go │ │ │ │ ├── syntax.go │ │ │ │ └── syntax_test.go │ │ │ ├── flags/ │ │ │ │ ├── addunique.go │ │ │ │ ├── context.go │ │ │ │ ├── deprecation.go │ │ │ │ ├── map.go │ │ │ │ ├── unparsed.go │ │ │ │ ├── unparsed_test.go │ │ │ │ └── zerovalue.go │ │ │ ├── global/ │ │ │ │ └── flags.go │ │ │ ├── helm/ │ │ │ │ ├── chart.go │ │ │ │ ├── install.go │ │ │ │ ├── lint.go │ │ │ │ └── release.go │ │ │ ├── ingest/ │ │ │ │ ├── command.go │ │ │ │ ├── info.go │ │ │ │ └── state.go │ │ │ ├── intercept/ │ │ │ │ ├── command.go │ │ │ │ ├── command_test.go │ │ │ │ ├── describe_intercepts.go │ │ │ │ ├── info.go │ │ │ │ ├── info_test.go │ │ │ │ └── state.go │ │ │ ├── main.go │ │ │ ├── mount/ │ │ │ │ ├── flags.go │ │ │ │ ├── info.go │ │ │ │ ├── prepare_unix.go │ │ │ │ └── prepare_windows.go │ │ │ ├── output/ │ │ │ │ ├── output.go │ │ │ │ └── output_test.go │ │ │ └── progress/ │ │ │ ├── colors.go │ │ │ ├── event.go │ │ │ ├── json.go │ │ │ ├── noop.go │ │ │ ├── plain.go │ │ │ ├── quiet.go │ │ │ ├── spinner.go │ │ │ ├── tty.go │ │ │ ├── tty_test.go │ │ │ └── writer.go │ │ ├── cmd_error.go │ │ ├── config.go │ │ ├── config_darwin.go │ │ ├── config_linux.go │ │ ├── config_test.go │ │ ├── config_unix.go │ │ ├── config_util.go │ │ ├── config_windows.go │ │ ├── const.go │ │ ├── docker/ │ │ │ ├── container.go │ │ │ ├── context.go │ │ │ ├── daemon.go │ │ │ ├── daemon_test.go │ │ │ ├── image.go │ │ │ ├── kubeauth/ │ │ │ │ └── cmd.go │ │ │ ├── plugin.go │ │ │ ├── teleroute/ │ │ │ │ ├── network.go │ │ │ │ ├── server.go │ │ │ │ ├── server_linux.go │ │ │ │ └── server_other.go │ │ │ ├── volume.go │ │ │ └── volume_test.go │ │ ├── ensured_state.go │ │ ├── envconfig.go │ │ ├── envconfig_unix.go │ │ ├── envconfig_windows.go │ │ ├── install_id.go │ │ ├── k8s/ │ │ │ ├── cani.go │ │ │ ├── cluster.go │ │ │ ├── config.go │ │ │ └── connect.go │ │ ├── logging/ │ │ │ ├── cached_timed_level.go │ │ │ ├── dup_test.go │ │ │ ├── dup_unix.go │ │ │ ├── dup_windows.go │ │ │ ├── initcontext.go │ │ │ ├── initcontext_test.go │ │ │ ├── initcontext_unix_test.go │ │ │ ├── initcontext_windows_test.go │ │ │ ├── rotatingfile.go │ │ │ ├── rotatingfile_unix.go │ │ │ ├── rotatingfile_windows.go │ │ │ ├── stat.go │ │ │ ├── stat_darwin.go │ │ │ ├── stat_linux.go │ │ │ ├── stat_linux_test.go │ │ │ ├── stat_test.go │ │ │ └── stat_windows.go │ │ ├── portforward/ │ │ │ ├── borrowed_kubectl_cmdutil.go │ │ │ ├── grpcresolver.go │ │ │ ├── podaddr.go │ │ │ ├── resolve.go │ │ │ └── streamconn.go │ │ ├── remotefs/ │ │ │ ├── bridge.go │ │ │ ├── fuseftp.go │ │ │ ├── fuseftp_docker.go │ │ │ ├── fuseftp_embedded.go │ │ │ ├── fuseftp_external.go │ │ │ ├── fuseftp_grpc.go │ │ │ ├── fuseftp_linked.go │ │ │ ├── fuseftp_other.go │ │ │ ├── mounter.go │ │ │ └── sftp.go │ │ ├── rootd/ │ │ │ ├── dbus/ │ │ │ │ └── resolved.go │ │ │ ├── dns/ │ │ │ │ ├── client.go │ │ │ │ ├── client_queue.go │ │ │ │ ├── connpool.go │ │ │ │ ├── connpool_test.go │ │ │ │ ├── resolved_linux.go │ │ │ │ ├── server.go │ │ │ │ ├── server_darwin.go │ │ │ │ ├── server_linux.go │ │ │ │ ├── server_test.go │ │ │ │ └── server_windows.go │ │ │ ├── grpc.go │ │ │ ├── in_process.go │ │ │ ├── service.go │ │ │ ├── session.go │ │ │ ├── stream_creator.go │ │ │ └── vip/ │ │ │ ├── env_nat.go │ │ │ ├── env_nat_test.go │ │ │ └── vip.go │ │ ├── stream_error.go │ │ ├── userd/ │ │ │ ├── daemon/ │ │ │ │ ├── grpc.go │ │ │ │ └── service.go │ │ │ ├── service.go │ │ │ ├── session.go │ │ │ └── trafficmgr/ │ │ │ ├── agents.go │ │ │ ├── config.go │ │ │ ├── context.go │ │ │ ├── gather_logs.go │ │ │ ├── ingest.go │ │ │ ├── ingest_test.go │ │ │ ├── intercept.go │ │ │ ├── mount.go │ │ │ ├── podaccess.go │ │ │ ├── session.go │ │ │ └── session_info_cache.go │ │ ├── version.go │ │ └── version_test.go │ ├── dnsproxy/ │ │ ├── lookup.go │ │ ├── lookup_test.go │ │ ├── resolvefile_unix.go │ │ ├── resolvefile_unix_test.go │ │ └── rpc.go │ ├── dos/ │ │ ├── aferofs/ │ │ │ └── wrapper.go │ │ ├── env.go │ │ ├── exe.go │ │ ├── filesystem.go │ │ ├── filesystem_test.go │ │ ├── lockedfile.go │ │ ├── package.go │ │ ├── stdio.go │ │ └── wdwrapper.go │ ├── dpipe/ │ │ ├── dpipe.go │ │ ├── dpipe_test.go │ │ ├── dpipe_unix.go │ │ ├── dpipe_windows.go │ │ └── testdata/ │ │ └── echo/ │ │ └── echo.go │ ├── errcat/ │ │ └── errors.go │ ├── filelocation/ │ │ ├── app.go │ │ ├── ctx.go │ │ ├── osfile.go │ │ ├── osfile_darwin.go │ │ ├── osfile_linux.go │ │ └── osfile_windows.go │ ├── forwarder/ │ │ ├── basic.go │ │ ├── tcp.go │ │ └── udp.go │ ├── grpc/ │ │ ├── client/ │ │ │ └── connect.go │ │ ├── error.go │ │ ├── errors/ │ │ │ └── errors.go │ │ ├── server/ │ │ │ ├── context.go │ │ │ ├── logging.go │ │ │ └── server.go │ │ └── watcher/ │ │ └── watcher.go │ ├── icept/ │ │ ├── conflicts.go │ │ ├── conflicts_test.go │ │ └── find.go │ ├── informer/ │ │ ├── context.go │ │ └── factory.go │ ├── ioutil/ │ │ ├── free_ports.go │ │ ├── keyvalueformatter.go │ │ ├── print.go │ │ ├── safename.go │ │ ├── tempname.go │ │ └── writeallto.go │ ├── iputil/ │ │ ├── ipnet.go │ │ ├── ips.go │ │ ├── join.go │ │ ├── normalize.go │ │ └── parse.go │ ├── json/ │ │ └── marshal.go │ ├── k8sapi/ │ │ ├── cani.go │ │ ├── clientconfigprovider.go │ │ ├── interface.go │ │ ├── kind.go │ │ ├── namespaceid.go │ │ ├── object.go │ │ ├── util.go │ │ └── workload.go │ ├── labels/ │ │ └── selector.go │ ├── log/ │ │ ├── base_logger.go │ │ ├── group.go │ │ └── timed_level.go │ ├── maps/ │ │ ├── utils.go │ │ └── xmap.go │ ├── matcher/ │ │ ├── header_stringer.go │ │ ├── header_stringer_test.go │ │ ├── headers.go │ │ ├── headers_test.go │ │ ├── paths.go │ │ ├── request.go │ │ ├── request_test.go │ │ └── value.go │ ├── pprof/ │ │ └── pprof.go │ ├── proc/ │ │ ├── cmd.go │ │ ├── docker_linux.go │ │ ├── docker_other.go │ │ ├── exec.go │ │ ├── exec_unix.go │ │ ├── exec_windows.go │ │ ├── wsl_linux.go │ │ └── wsl_other.go │ ├── restapi/ │ │ ├── api.go │ │ └── api_test.go │ ├── routing/ │ │ ├── routing.go │ │ ├── routing_darwin.go │ │ ├── routing_linux.go │ │ ├── routing_test.go │ │ ├── routing_unix.go │ │ ├── routing_unix_test.go │ │ └── routing_windows.go │ ├── shellquote/ │ │ ├── shellstring.go │ │ ├── shellstring_unix.go │ │ ├── shellstring_unix_test.go │ │ ├── shellstring_windows.go │ │ └── shellstring_windows_test.go │ ├── sigctx/ │ │ └── context.go │ ├── slice/ │ │ ├── contains.go │ │ ├── csv.go │ │ └── strings.go │ ├── subnet/ │ │ ├── bitfield256.go │ │ ├── bitfield256_test.go │ │ ├── set.go │ │ ├── set_test.go │ │ ├── subnet.go │ │ ├── subnet_test.go │ │ └── testdata/ │ │ └── ips.txt │ ├── tmconfig/ │ │ ├── admin_command.go │ │ └── configmap.go │ ├── tunnel/ │ │ ├── client_stream.go │ │ ├── connid.go │ │ ├── connid_test.go │ │ ├── context.go │ │ ├── dialer.go │ │ ├── message.go │ │ ├── metrics.go │ │ ├── pipe.go │ │ ├── pool.go │ │ ├── probe.go │ │ ├── provider.go │ │ ├── server_stream.go │ │ ├── stream.go │ │ ├── stream_conn.go │ │ ├── stream_conn_test.go │ │ ├── stream_test.go │ │ ├── synthetic.go │ │ ├── timed_handler.go │ │ └── udplistener.go │ ├── types/ │ │ ├── addrportandproto.go │ │ ├── engagement.go │ │ ├── mountpolicy.go │ │ ├── portandproto.go │ │ ├── portidentifier.go │ │ ├── portmapping.go │ │ └── proto.go │ ├── version/ │ │ └── version.go │ ├── vif/ │ │ ├── device.go │ │ ├── device_darwin.go │ │ ├── device_linux.go │ │ ├── device_notlinux.go │ │ ├── device_unix.go │ │ ├── device_windows.go │ │ ├── logging.go │ │ ├── router.go │ │ ├── router_test.go │ │ ├── stack.go │ │ └── tunneling_device.go │ └── workload/ │ ├── informers.go │ ├── state.go │ └── util.go ├── rpc/ │ ├── LICENSE │ ├── agent/ │ │ ├── agent.pb.go │ │ ├── agent.proto │ │ └── agent_grpc.pb.go │ ├── authenticator/ │ │ ├── authenticator.pb.go │ │ ├── authenticator.proto │ │ └── authenticator_grpc.pb.go │ ├── common/ │ │ ├── version.pb.go │ │ └── version.proto │ ├── connector/ │ │ ├── connector.pb.go │ │ ├── connector.proto │ │ └── connector_grpc.pb.go │ ├── daemon/ │ │ ├── daemon.pb.go │ │ ├── daemon.proto │ │ └── daemon_grpc.pb.go │ ├── go.mod │ ├── go.sum │ ├── manager/ │ │ ├── manager.pb.go │ │ ├── manager.proto │ │ └── manager_grpc.pb.go │ └── teleroute/ │ ├── service.pb.go │ ├── service.proto │ └── service_grpc.pb.go └── test-infra/ ├── aws-okd/ │ ├── README.md │ └── dns/ │ ├── .gitignore │ └── dns.tf └── aws-vpn/ ├── .gitignore ├── README.md ├── dns.tf ├── eks.tf ├── locals.tf ├── network.tf ├── pki.sh ├── provider.tf └── vpn.tf ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ .circleci/ .idea/ .git/ build-aux/ build-output/ !build-output/version.txt !build-output/helm-version.txt integration_test/ k8s/ packaging/ test-infra/ tools/ ================================================ FILE: .editorconfig ================================================ root = true [*] end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.go] indent_style = tab ; mimic gofmt's go/printer.printer.funcBody().maxSize max_line_length = 100 ; mimic gofmt's cmd/gofmt.tabWidth tab_width = 8 [{go.mod,go.sum}] indent_style = tab [*.proto] indent_style = space indent_size = 2 ================================================ FILE: .gitattributes ================================================ # Always check out with LF so that golangci-lint doesn't break *.go text eol=lf ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [thallgren] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry polar: # Replace with a single Polar username buy_me_a_coffee: # Replace with a single Buy Me a Coffee username thanks_dev: # Replace with a single thanks.dev username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/ISSUE_TEMPLATE/Bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve Telepresence --- **Describe the bug** A clear and concise description of what the bug is. Please use `telepresence loglevel debug` to ensure we have the most helpful logs, reproduce the error, and then run `telepresence gather-logs` to create a zip file of all logs for Telepresence's components (root and user daemons, traffic-manager, and traffic-agents) and attach it to this issue. See an example command below: ``` telepresence loglevel debug * reproduce the error * telepresence gather-logs --output-file /tmp/telepresence_logs.zip # To see all options, run the following command telepresence gather-logs --help ``` **To Reproduce** Steps to reproduce the behavior: 1. When I run '...' 2. I see '....' 3. So I look at '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Versions (please complete the following information):** - Output of `telepresence version` (preferably while telepresence is connected) - Operating system of workstation running `telepresence` commands - Kubernetes environment and Version [e.g. Minikube, bare metal, Google Kubernetes Engine] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/Feature_request.md ================================================ --- name: Feature request about: Suggest an idea for Telepresence --- **Please describe your use case / problem.** A clear and concise description of your use case / problem. The "why" is really valuable to us. For example, "I have a set of services whose clients have long-lived connections." **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Versions (please complete the following information)** - Output of `telepresence version` (just in case the feature exists in future versions) - Kubernetes Environment and Version **Additional context** Add any other context about the feature request here. ================================================ FILE: .github/actions/install-dependencies/action.yaml ================================================ name: "Install dependencies" description: "Install dependencies required by the runner" runs: using: composite steps: - uses: actions/setup-go@v5 with: go-version: stable - if: runner.os == 'Linux' name: install Linux dependencies shell: bash run: | sudo rm -f /etc/apt/sources.list.d/google-chrome.list sudo apt-get update sudo apt-get install -y gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu sshfs socat libfuse-dev make jq sudo sh -c 'echo user_allow_other >> /etc/fuse.conf' - if: runner.os == 'macOS' name: install macOS dependencies shell: bash env: HOMEBREW_NO_INSTALL_FROM_API: "" HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: "1" run: | brew untap homebrew/core || : brew untap homebrew/cask || : # Remove Python3 symlinks in /usr/local/bin as workaround to brew update issues # https://github.com/actions/setup-python/issues/577 rm /usr/local/bin/2to3* || : rm /usr/local/bin/idle3* || : rm /usr/local/bin/pydoc* || : rm /usr/local/bin/python3* || : brew update brew install --cask macfuse brew install gromgit/fuse/sshfs-mac brew link --overwrite sshfs-mac if [[ ${RUNNER_ARCH} == "ARM64" ]]; then brew install jq fi - if: runner.os == 'Windows' name: setup make shell: pwsh run: | # Use make from Git for Windows (pre-installed on runners) echo "C:\Program Files\Git\usr\bin" | Out-File -FilePath $env:GITHUB_PATH -Append - if: runner.os == 'Windows' name: download winfsp uses: nick-fields/retry/@v3 with: max_attempts: 3 timeout_minutes: 1 shell: bash command: make winfsp.msi - if: runner.os == 'Windows' name: download sshfs uses: nick-fields/retry/@v3 with: max_attempts: 3 timeout_minutes: 1 shell: bash command: make sshfs-win.msi - if: runner.os == 'Windows' name: download sshfs uses: nick-fields/retry/@v3 with: max_attempts: 3 timeout_minutes: 1 shell: bash command: make wintun.dll - if: runner.os == 'Windows' name: install winfsp and sshfs shell: powershell run: | Start-Process msiexec -Wait -verb runAs -Args "/i build-output\\winfsp.msi /passive /qn /L*V winfsp-install.log" Start-Process msiexec -Wait -verb runAs -Args "/i build-output\\sshfs-win.msi /passive /qn /L*V sshfs-win-install.log" [Environment]::SetEnvironmentVariable("Path", "C:\\;C:\\Program Files\\SSHFS-Win\\bin;$ENV:Path", "Machine") ================================================ FILE: .github/actions/upload-logs/action.yaml ================================================ name: "Upload logs" description: "Upload logs from the test run" runs: using: composite steps: - run: | LOGS="test-logs/${{ runner.os }}/${{ runner.arch }}" mkdir -p "$LOGS" if [[ $RUNNER_OS != Windows ]]; then rsync -ma --include='*/' --include='*.tap' --include='*.log' --include='Test*.webm' --exclude='*' . "$LOGS" fi for file in \ {"${XDG_CACHE_HOME:-$HOME/.cache}/telepresence/logs","$HOME/Library/Logs/telepresence","$LOCALAPPDATA/telepresence/logs","."}/*.log do if [ -s "$file" ]; then cp -v "$file" "$LOGS" || true fi done shell: bash name: Gather logs - name: Upload logs uses: actions/upload-artifact@v4 with: # If an environment variable LOG_SUFFIX is set, it will be appended to the log filename. name: ${{github.job}}-logs-${{ env.LOG_SUFFIX }} path: | test-logs/${{ runner.os }}/${{ runner.arch }}/* ================================================ FILE: .github/pull_request_template.md ================================================ ## Description A few sentences describing the overall goals of the pull request's commits. ## Checklist - [ ] I made sure to update `./CHANGELOG.yml` (our release notes are generated from this file). - [ ] I made sure to add any documentation changes required for my change. - [ ] My change is adequately tested. - [ ] I updated `CONTRIBUTING.md` with any special dev tricks I had to use to work on this code efficiently. - [ ] Once my PR is ready to have integration tests ran, I posted the PR in #telepresence-oss channel on the [CNCF Slack](https://slack.cncf.io/) so that the "ok to test" label can be applied. ================================================ FILE: .github/workflows/dev.yaml ================================================ name: "Integration Tests" on: pull_request: types: - labeled permissions: contents: read pull-requests: write env: TELEPRESENCE_REGISTRY: local jobs: build_and_test: if: ${{ github.event.label.name == 'ok to test' || github.event.label.name == 'compatibility test' }} strategy: fail-fast: false matrix: runners: - ubuntu-latest # Re-enable when we can run a proper cluster. Colima almost works on macOS but the very limited # resources available make the test fail very often. On windows, we'll need WSL2 # - macos-latest # - windows-latest runs-on: ${{ matrix.runners }} steps: - name: Remove ok to test label if: github.event.label.name == 'ok to test' uses: buildsville/add-remove-label@v2.0.1 with: token: ${{ secrets.GITHUB_TOKEN }} labels: ok to test type: remove - name: Remove compatibility test label if: github.event.label.name == 'compatibility test' uses: buildsville/add-remove-label@v2.0.1 with: token: ${{ secrets.GITHUB_TOKEN }} labels: compatibility test type: remove - uses: actions/checkout@v4 with: fetch-depth: 0 ref: "${{ github.event.pull_request.head.sha }}" - name: install dependencies uses: ./.github/actions/install-dependencies - name: Get Telepresence Version id: version run: | v=$(go run build-aux/genversion/main.go ${{github.run_id}}) echo "TELEPRESENCE_VERSION=$v" >> "$GITHUB_ENV" echo "TELEPRESENCE_SEMVER=${v#v}" >> "$GITHUB_ENV" - name: Start minikube if: runner.os == 'Linux' uses: medyagh/setup-minikube@latest with: kubernetes-version: v1.33.5 - name: Build client run: make build - name: Build images run: make client-image tel2-image routecontroller-image - name: Load images into minikube run: | minikube image load ${{env.TELEPRESENCE_REGISTRY}}/telepresence:${{env.TELEPRESENCE_SEMVER}} minikube image load ${{env.TELEPRESENCE_REGISTRY}}/tel2:${{env.TELEPRESENCE_SEMVER}} minikube image load ${{env.TELEPRESENCE_REGISTRY}}/route-controller:${{env.TELEPRESENCE_SEMVER}} - name: Run integration tests if: github.event.label.name == 'ok to test' uses: nick-fields/retry/@v3 with: max_attempts: 3 shell: bash timeout_minutes: 90 command: | set -ex if [[ ${RUNNER_OS} == "Windows" ]]; then export PATH="$PATH:/C/Program Files/SSHFS-Win/bin:$HOME/kubectl-plugins" fi make check-integration - name: Compatibility with older manager and agent if: ${{ github.event.label.name == 'compatibility test' }} env: DEV_MANAGER_VERSION: "2.24.1" DEV_MANAGER_REGISTRY: ghcr.io/telepresenceio uses: nick-fields/retry/@v3 with: max_attempts: 3 shell: bash timeout_minutes: 90 command: | set -ex if [[ ${RUNNER_OS} == "Windows" ]]; then export PATH="$PATH:/C/Program Files/SSHFS-Win/bin:$HOME/kubectl-plugins" fi make check-integration - name: Compatibility with older client if: ${{ github.event.label.name == 'compatibility test' }} env: DEV_CLIENT_VERSION: "2.24.1" DEV_CLIENT_REGISTRY: ghcr.io/telepresenceio uses: nick-fields/retry/@v3 with: max_attempts: 3 shell: bash timeout_minutes: 90 command: | set -ex if [[ ${RUNNER_OS} == "Windows" ]]; then export PATH="$PATH:/C/Program Files/SSHFS-Win/bin:$HOME/kubectl-plugins" fi make check-integration - uses: ./.github/actions/upload-logs env: LOG_SUFFIX: "${{ runner.os }}-${{ runner.arch }}-${{ matrix.clusters.distribution }}-${{ matrix.clusters.version }}" if: ${{ always() }} ================================================ FILE: .github/workflows/go-dependency-submission.yaml ================================================ name: Go Dependency Submission on: push: branches: - release/v2 # The API requires write permission on the repository to submit dependencies permissions: contents: write jobs: go-action-detection: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-go@v5 with: go-version: stable # we need fuseftp.bits - name: "Build dependencies" run: make build-deps - name: Run snapshot action uses: actions/go-dependency-submission@v1 with: go-mod-path: go.mod go-build-target: cmd/telepresence/main.go ================================================ FILE: .github/workflows/image-scan.yaml ================================================ name: image-scan on: push: branches: - release/v2 pull_request: jobs: trivy-container-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} - uses: actions/setup-go@v5 with: go-version: stable - name: Build dev image run: | make save-tel2-image - name: Scan uses: aquasecurity/trivy-action@master with: input: build-output/tel2-image.tar format: sarif exit-code: 0 # only warn for now until we have backed it into our processes output: trivy-results.sarif ignore-unfixed: true vuln-type: "os,library" severity: "CRITICAL,HIGH" hide-progress: false - name: Upload Scan to GitHub Security Tab uses: github/codeql-action/upload-sarif@v3 with: sarif_file: "trivy-results.sarif" pass: name: image-scan needs: - trivy-container-scan runs-on: ubuntu-latest steps: - name: No-Op if: ${{ false }} run: "echo Pass" ================================================ FILE: .github/workflows/licenses.yaml ================================================ name: "License verification" on: pull_request: workflow_dispatch: jobs: dependency_info_linux: name: "Linux: Verify use of forbidden licenses" runs-on: ubuntu-latest outputs: is-up-to-date: ${{ steps.check.outputs.up-to-date }} diff: ${{ steps.check.outputs.diff }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 ref: "${{ github.event.pull_request.head.sha }}" - name: install dependencies uses: ./.github/actions/install-dependencies - name: "Generate dependency information" shell: bash run: make generate - name: "Update dependency information after dependabot change" uses: datawire/go-mkopensource/actions/save-dependabot-changes@v0.0.7 id: changed-by-dependabot with: branches_to_skip: 'release/v2' - name: "Abort if dependencies changed" if: steps.changed-by-dependabot.outputs.is_dirty == 'true' run: | echo "Dependabot triggered a dependency update. Aborting workflow." exit 1 - name: "Check dependency files are up-to-date" id: check shell: bash run: | set -e git add . if [[ -n "$(git status --porcelain)" ]]; then echo "diff=$(PAGER= git diff --cached 2>&1 | base64 -w0)" >> $GITHUB_OUTPUT echo "up-to-date=no" >> $GITHUB_OUTPUT else echo "up-to-date=yes" >> $GITHUB_OUTPUT fi up_to_date_check_linux: name: "Linux: Check out-of-date licenses" needs: dependency_info_linux runs-on: ubuntu-latest steps: - name: "Check dependency files are up-to-date" shell: bash run: | set -e if [[ "${{needs.dependency_info_linux.outputs.is-up-to-date}}" == "no" ]]; then echo '::error:: Changes detected after dependency generation. Run ' \ '`make generate` and commit the latest version of the ' \ 'dependency information files' echo ${{needs.dependency_info_linux.outputs.diff}} | base64 -d exit 1 fi echo '::info:: Files are up-to-date' ================================================ FILE: .github/workflows/release-teleroute.yaml ================================================ on: push: tags: # These aren't regexps. They are "Workflow Filter patterns" - teleroute-[0-9]+.[0-9]+.[0-9] jobs: build-release: strategy: fail-fast: false matrix: architecture: - amd64 - arm64 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: stable - name: Push image working-directory: ./cmd/teleroute env: PLUGIN_ARCH: ${{ matrix.architecture }} VER: ${{ github.ref }} run: | echo '${{ secrets.GITHUB_TOKEN }}' | docker login ghcr.io -u='${{ github.actor }}' --password-stdin PLUGIN_VERSION=${VER#refs/tags/teleroute-} make push ================================================ FILE: .github/workflows/release.yaml ================================================ name: Releases on: push: tags: # These aren't regexps. They are "Workflow Filter patterns" - v[0-9]+.[0-9]+.[0-9] - v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+ - v[0-9]+.[0-9]+.[0-9]+-test.[0-9]+ env: TELEPRESENCE_REGISTRY: ghcr.io/telepresenceio jobs: build-release: strategy: fail-fast: false matrix: runner: - ubuntu-latest - macos-latest - windows-latest arch: - amd64 - arm64 runs-on: ${{ matrix.runner }} env: GOARCH: ${{ matrix.arch }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: ./.github/actions/install-dependencies name: install dependencies - name: set version shell: bash run: echo "TELEPRESENCE_VERSION=${{ github.ref_name }}" >> $GITHUB_ENV - name: generate binaries run: make release-binary - name: Install WiX Toolset if: runner.os == 'Windows' && matrix.arch == 'amd64' shell: pwsh run: | dotnet tool install --global wix --version 6.0.2 wix extension add -g WixToolset.BootstrapperApplications.wixext/6.0.2 wix extension add -g WixToolset.UI.wixext/6.0.2 wix extension add -g WixToolset.Util.wixext/6.0.2 - name: Build WiX Installer if: runner.os == 'Windows' && matrix.arch == 'amd64' shell: bash run: | export PATH="$USERPROFILE/.dotnet/tools:$PATH" cd build-aux/wix-installer make bundle ARCH=${{ matrix.arch }} cp ../../build-output/bin/TelepresenceInstall.exe ../../build-output/release/telepresence-windows-${{ matrix.arch }}-setup.exe - name: Install nfpm if: runner.os == 'Linux' shell: bash run: | # Always download x86_64 nfpm since ubuntu-latest runners are x86_64 # nfpm cross-compiles packages for the target arch specified in config curl -sfL "https://github.com/goreleaser/nfpm/releases/download/v2.44.1/nfpm_2.44.1_Linux_x86_64.tar.gz" | tar xz -C /tmp sudo mv /tmp/nfpm /usr/local/bin/nfpm - name: Build Linux Packages if: runner.os == 'Linux' shell: bash run: | cd build-aux/systemd-installer VERSION="${TELEPRESENCE_VERSION#v}" ARCH=${{ matrix.arch }} ./build-packages.sh - name: Upload binaries uses: actions/upload-artifact@v4 with: name: binaries-${{ matrix.runner }}-${{ matrix.arch }} path: build-output/release retention-days: 1 push-images: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: ./.github/actions/install-dependencies name: install dependencies - name: set version shell: bash run: | v=${{ github.ref_name }} echo "TELEPRESENCE_VERSION=$v" >> "$GITHUB_ENV" echo "TELEPRESENCE_SEMVER=${v#v}" >> "$GITHUB_ENV" - name: Setup docker buildx uses: docker/setup-buildx-action@v3 with: platforms: linux/amd64,linux/arm64 - name: Build image dependencies run: make images-deps - name: Make helm chart run: make helm-chart - name: Log in to registry run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - name: Push client image run: | docker buildx build --platform=linux/amd64,linux/arm64 --build-arg TELEPRESENCE_VERSION=${{env.TELEPRESENCE_SEMVER}} \ --push --tag ${{env.TELEPRESENCE_REGISTRY}}/telepresence:${{env.TELEPRESENCE_SEMVER}} -f build-aux/docker/images/Dockerfile.client . - name: Push tel2 image run: | docker buildx build --platform=linux/amd64,linux/arm64 --build-arg TELEPRESENCE_VERSION=${{env.TELEPRESENCE_SEMVER}} \ --push --tag ${{env.TELEPRESENCE_REGISTRY}}/tel2:${{env.TELEPRESENCE_SEMVER}} -f build-aux/docker/images/Dockerfile.traffic . - name: Push routeController image run: | docker buildx build --platform=linux/amd64,linux/arm64 \ --push --tag ${{env.TELEPRESENCE_REGISTRY}}/route-controller:${{env.TELEPRESENCE_SEMVER}} -f build-aux/docker/images/Dockerfile.routecontroller . - name: Push Helm Chart run: helm push build-output/telepresence-oss-chart.tgz oci://${{env.TELEPRESENCE_REGISTRY}} - name: Log out from registry if: always() run: docker logout publish-release: runs-on: ubuntu-latest needs: - build-release outputs: draft: ${{ steps.semver_check.outputs.draft }} prerelease: ${{ steps.semver_check.outputs.prerelease }} semver: ${{ steps.semver_check.outputs.semver }} steps: - name: Download artifacts uses: actions/download-artifact@v4 - name: Display structure of downloaded files run: ls -R - name: Determine if version is RC, TEST, or GA id: semver_check run: | v=${{ github.ref_name }} echo "semver=${v#v}" >> "$GITHUB_OUTPUT" if [[ "${v}" =~ ^v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+$ ]]; then echo "make_latest=false" >> $GITHUB_OUTPUT echo "draft=false" >> $GITHUB_OUTPUT echo "prerelease=true" >> $GITHUB_OUTPUT elif [[ "${v}" =~ ^v[0-9]+.[0-9]+.[0-9]+-test.[0-9]+$ ]]; then echo "make_latest=false" >> $GITHUB_OUTPUT echo "draft=false" >> $GITHUB_OUTPUT echo "prerelease=true" >> $GITHUB_OUTPUT elif [[ "${v}" =~ ^v[0-9]+.[0-9]+.[0-9]+-draft.[0-9]+$ ]]; then echo "make_latest=false" >> $GITHUB_OUTPUT echo "draft=true" >> $GITHUB_OUTPUT echo "prerelease=false" >> $GITHUB_OUTPUT else echo "make_latest=true" >> $GITHUB_OUTPUT echo "draft=false" >> $GITHUB_OUTPUT echo "prerelease=false" >> $GITHUB_OUTPUT fi - name: Create draft release if: ${{ steps.semver_check.outputs.draft == 'true' }} uses: ncipollo/release-action@v1 with: artifacts: "binaries-*/*" token: ${{ secrets.GITHUB_TOKEN }} draft: tag: ${{ github.ref_name }} body: | ## Draft Release For more information, visit our [installation docs](https://www.telepresence.io/docs/latest/quick-start). - name: Create release if: ${{ steps.semver_check.outputs.draft == 'false' }} uses: ncipollo/release-action@v1 with: artifacts: "binaries-*/*" token: ${{ secrets.GITHUB_TOKEN }} prerelease: ${{ steps.semver_check.outputs.prerelease }} makeLatest: ${{ steps.semver_check.outputs.make_latest }} tag: ${{ github.ref_name }} body: | ## Official Release Artifacts ### Installers (with root daemon as a system service) These installers include the option to run the root daemon as a system service, eliminating the need for elevated privileges when using Telepresence. | Platform | Architecture | Package | |----------|--------------|---------| | Linux (Debian/Ubuntu) | amd64 | [telepresence-linux-amd64.deb](https://github.com/telepresenceio/telepresence/releases/download/${{ github.ref_name }}/telepresence-${{ steps.semver_check.outputs.semver }}-linux-amd64.deb) | | Linux (Debian/Ubuntu) | arm64 | [telepresence-linux-arm64.deb](https://github.com/telepresenceio/telepresence/releases/download/${{ github.ref_name }}/telepresence-${{ steps.semver_check.outputs.semver }}-linux-arm64.deb) | | Linux (Fedora/RHEL) | amd64 | [telepresence-linux-amd64.rpm](https://github.com/telepresenceio/telepresence/releases/download/${{ github.ref_name }}/telepresence-${{ steps.semver_check.outputs.semver }}-linux-amd64.rpm) | | Linux (Fedora/RHEL) | arm64 | [telepresence-linux-arm64.rpm](https://github.com/telepresenceio/telepresence/releases/download/${{ github.ref_name }}/telepresence-${{ steps.semver_check.outputs.semver }}-linux-arm64.rpm) | | macOS | amd64 | [telepresence-darwin-amd64.pkg](https://github.com/telepresenceio/telepresence/releases/download/${{ github.ref_name }}/telepresence-darwin-amd64.pkg) | | macOS | arm64 | [telepresence-darwin-arm64.pkg](https://github.com/telepresenceio/telepresence/releases/download/${{ github.ref_name }}/telepresence-darwin-arm64.pkg) | | Windows | amd64 | [telepresence-windows-amd64-setup.exe](https://github.com/telepresenceio/telepresence/releases/download/${{ github.ref_name }}/telepresence-windows-amd64-setup.exe) | | Windows | arm64 | *Not available — WinFSP and SSHFS-Win installers lack arm64 versions. Use the zip file below.* | ### Standalone Binaries Standalone binaries for manual installation. The root daemon runs on-demand with elevated privileges. | Platform | Architecture | Binary | |----------|--------------|--------| | Linux | amd64 | [telepresence-linux-amd64](https://github.com/telepresenceio/telepresence/releases/download/${{ github.ref_name }}/telepresence-linux-amd64) | | Linux | arm64 | [telepresence-linux-arm64](https://github.com/telepresenceio/telepresence/releases/download/${{ github.ref_name }}/telepresence-linux-arm64) | | macOS | amd64 | [telepresence-darwin-amd64](https://github.com/telepresenceio/telepresence/releases/download/${{ github.ref_name }}/telepresence-darwin-amd64) | | macOS | arm64 | [telepresence-darwin-arm64](https://github.com/telepresenceio/telepresence/releases/download/${{ github.ref_name }}/telepresence-darwin-arm64) | | Windows | amd64 | [telepresence-windows-amd64.zip](https://github.com/telepresenceio/telepresence/releases/download/${{ github.ref_name }}/telepresence-windows-amd64.zip) | | Windows | arm64 | [telepresence-windows-arm64.zip](https://github.com/telepresenceio/telepresence/releases/download/${{ github.ref_name }}/telepresence-windows-arm64.zip) | ### Helm Chart - 📦 [Telepresence Helm Chart](https://artifacthub.io/packages/helm/telepresence-oss/telepresence-oss/${{ steps.semver_check.outputs.semver }}) For more information, visit our [installation docs](https://www.telepresence.io/docs/quick-start). - uses: actions/checkout@v4 if: ${{ steps.semver_check.outputs.make_latest == 'true' }} - name: Update Homebrew if: ${{ steps.semver_check.outputs.make_latest == 'true' }} run: | v=${{ github.ref_name }} packaging/homebrew-package.sh "${v#v}" "${{ vars.GH_BOT_USER }}" "${{ vars.GH_BOT_EMAIL }}" "${{ secrets.HOMEBREW_TAP_TOKEN }}" prune-images: if: ${{ always() }} runs-on: ubuntu-latest permissions: packages: write needs: - push-images steps: - name: Prune tel2 and telepresence uses: dataaxiom/ghcr-cleanup-action@v1 with: owner: telepresenceio packages: tel2,telepresence token: ${{ secrets.GITHUB_TOKEN }} delete-untagged: true delete-ghost-images: true delete-partial-images: true delete-orphaned-images: true build-macos-pkg: # This job builds signed and notarized macOS .pkg installers. # It uses a protected environment requiring approval from authorized reviewers. # If not approved, the release proceeds without .pkg installers (standalone binaries are still available). environment: macos-signing needs: - publish-release strategy: fail-fast: false matrix: arch: - amd64 - arm64 runs-on: macos-latest env: GOARCH: ${{ matrix.arch }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: ./.github/actions/install-dependencies name: install dependencies - name: set version shell: bash run: echo "TELEPRESENCE_VERSION=${{ github.ref_name }}" >> $GITHUB_ENV - name: generate binaries run: make release-binary - name: Import Apple certificates env: MACOS_CERTIFICATE_P12: ${{ secrets.MACOS_CERTIFICATE_P12 }} MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} run: | # Create a temporary keychain KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db KEYCHAIN_PASSWORD=$(openssl rand -base64 32) security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" # Import certificates from base64-encoded P12 echo "$MACOS_CERTIFICATE_P12" | base64 --decode > $RUNNER_TEMP/certificate.p12 security import $RUNNER_TEMP/certificate.p12 -P "$MACOS_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" rm $RUNNER_TEMP/certificate.p12 # Add keychain to search list and set as default security list-keychain -d user -s "$KEYCHAIN_PATH" login.keychain security default-keychain -s "$KEYCHAIN_PATH" # Allow codesign to access the keychain without prompting security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" # Store keychain path for cleanup echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> $GITHUB_ENV - name: Build signed macOS Installer env: MACOS_SIGN_APPLICATION: ${{ secrets.MACOS_SIGN_APPLICATION }} MACOS_SIGN_INSTALLER: ${{ secrets.MACOS_SIGN_INSTALLER }} MACOS_NOTARIZE_APPLE_ID: ${{ secrets.MACOS_NOTARIZE_APPLE_ID }} MACOS_NOTARIZE_TEAM_ID: ${{ secrets.MACOS_NOTARIZE_TEAM_ID }} MACOS_NOTARIZE_PASSWORD: ${{ secrets.MACOS_NOTARIZE_PASSWORD }} run: | cd build-aux/pkg-installer VERSION="${TELEPRESENCE_VERSION#v}" ARCH=${{ matrix.arch }} ./build-pkg.sh - name: Cleanup macOS keychain if: always() && env.KEYCHAIN_PATH != '' run: | security delete-keychain "$KEYCHAIN_PATH" || true - name: Add signed package to release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | cp build-output/Telepresence.pkg telepresence-darwin-${{ matrix.arch }}.pkg gh release upload ${{ github.ref_name }} telepresence-darwin-${{ matrix.arch }}.pkg --clobber test-release: needs: - push-images - publish-release if: ${{ needs.publish-release.outputs.draft == 'false' }} strategy: fail-fast: false matrix: runner: - ubuntu-latest - macos-latest - windows-latest arch: - amd64 - arm64 runs-on: ${{ matrix.runner }} steps: - name: download binary env: DOWNLOAD_URL: "https://github.com/telepresenceio/telepresence/releases/download/${{ github.ref_name }}" shell: bash run: | if [ "${{ runner.os }}" = "macOS" ]; then curl -fL ${{ env.DOWNLOAD_URL }}/telepresence-darwin-${{ matrix.arch }} -o ./telepresence || { echo "Curl command failed" ; exit 1; } elif [ "${{ runner.os }}" = "Windows" ]; then curl -fL ${{ env.DOWNLOAD_URL }}/telepresence-windows-${{ matrix.arch }}.zip -o ./telepresence.zip || { echo "Curl command failed" ; exit 1; } unzip ./telepresence.zip || { echo "Unzip command failed" ; exit 1; } else curl -fL ${{ env.DOWNLOAD_URL }}/telepresence-linux-${{ matrix.arch }} -o ./telepresence || { echo "Curl command failed" ; exit 1; } fi - name: test binary shell: bash if: ${{ !((runner.os == 'Linux' || runner.os == 'Windows') && runner.arch == 'X64' && matrix.arch == 'arm64') }} run: | chmod +x ./telepresence output=$(./telepresence version) if [ $? -eq 0 ]; then echo "Telepresence command executed successfully" else echo "Telepresence command failed" exit 1 fi echo "$output" | grep -q "Client\s*:\s*${{ github.ref_name }}" if [ $? -eq 0 ]; then echo "Version match!" else echo "Version does not match!" exit 1 fi ================================================ FILE: .github/workflows/stale.yaml ================================================ name: 'Close stale issues and PR' on: schedule: - cron: '12 10 * * *' jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v9 with: stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment, or this will be closed in 7 days.' stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment, or this will be closed in 14 days.' close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.' close-pr-message: 'This PR was closed because it has been stalled for 14 days with no activity.' days-before-issue-stale: 60 days-before-pr-stale: 30 days-before-issue-close: 7 days-before-pr-close: 14 operations-per-run: 80 exempt-issue-labels: feature,friction ================================================ FILE: .github/workflows/unit_tests.yaml ================================================ name: "Build and Unit test" on: pull_request: env: HOMEBREW_NO_INSTALL_FROM_API: jobs: unit: strategy: fail-fast: false matrix: runners: - ubuntu-latest - macos-latest - windows-latest runs-on: ${{ matrix.runners }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 ref: "${{ github.event.pull_request.head.sha }}" - name: install dependencies uses: ./.github/actions/install-dependencies - name: install build dependencies run: make build-deps - name: Lint if: ${{ runner.os != 'Windows' }} uses: golangci/golangci-lint-action@v7 with: args: --timeout 8m ./... - name: Lint (limited on windows) if: ${{ runner.os == 'Windows' }} uses: golangci/golangci-lint-action@v7 with: args: --timeout 8m ./cmd/telepresence/... ./integration_test/... ./pkg/... - name: Build run: make build - name: Run tests uses: nick-fields/retry/@v3 with: max_attempts: 3 timeout_minutes: 12 command: make check-unit ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories vendor/ # Editor nonsense .vscode .idea *.iml *.swp .DS_Store build-output /tools/* !/tools/src/ # Don't accidentally add files from `go build` telepresence traffic tst-manager !/cmd/*/ mcp-tools.json # Include Chart directory !/charts/telepresence-oss # But don't include this one /charts/telepresence-oss/k8s-defs.json # Downloaded fuseftp bits fuseftp.bits # Needed to build on windows .wintools .gocache # Don't accidentally add binaries built byintegration tests /integration_test/testdata/echo-server/echo-server /build-aux/genversion/genversion # Vagrant .vagrant/ tests.log /.claude/settings.local.json /bugs/ ================================================ FILE: .golangci.yml ================================================ version: "2" run: build-tags: - citest modules-download-mode: readonly linters: enable: - asasalint - asciicheck - bidichk - bodyclose - copyloopvar - cyclop - decorder - depguard - dogsled - durationcheck - errname - forbidigo - gochecknoglobals - gocognit - gocritic - gocyclo - godot - goheader - gomodguard - goprintffuncname - grouper - importas - lll - loggercheck - makezero - misspell - nakedret - nolintlint - nosprintfhostport - prealloc - predeclared - reassign - unconvert - unparam - usestdlibvars - whitespace disable: - containedctx - contextcheck - dupl - err113 - errchkjson - errorlint - exhaustive - exhaustruct - forcetypeassert - funlen - gochecknoinits - goconst - godox - gomoddirectives - gosec - interfacebloat - ireturn - maintidx - nestif - nilerr - nilnil - nlreturn - noctx - nonamedreturns - paralleltest - promlinter - revive - rowserrcheck - sqlclosecheck - tagliatelle - testpackage - thelper - tparallel - varnamelen - wastedassign - wrapcheck - wsl settings: cyclop: max-complexity: 32 depguard: rules: main: files: - $all deny: - pkg: io/ioutil desc: '`io/ioutil` is deprecated in Go 1.16, use `io` or `os` instead' - pkg: syscall desc: Use `golang.org/x/sys/...` instead of `syscall` - pkg: github.com/golang/protobuf desc: Use `google.golang.org/protobuf` instead of `github.com/golang/protobuf` - pkg: github.com/kballard/go-shellquote desc: Use `github.com/telepresenceio/telepresence/pkg/shellquote.ShellString` instead of `github.com/kballard/go-shellquote.Join` forbidigo: forbid: - pattern: '^os\.IsNotExist$' msg: Use errors.Is(err, fs.ErrNotExist) gocognit: min-complexity: 60 gocyclo: min-complexity: 35 gomodguard: blocked: modules: - gotest.tools: recommendations: - github.com/stretchr/testify - github.com/google/go-cmp/cmp - gotest.tools/v2: recommendations: - github.com/stretchr/testify - github.com/google/go-cmp/cmp - gotest.tools/v3: recommendations: - github.com/stretchr/testify - github.com/google/go-cmp/cmp lll: line-length: 180 tab-width: 2 nolintlint: require-explanation: true require-specific: true allow-no-explanation: - gocognit allow-unused: true prealloc: range-loops: false staticcheck: checks: - all # Don't check "Apply De Morgan's law". # https://staticcheck.dev/docs/checks/#QF1001 - -QF1001 # Don't check "Incorrect or missing package comment". # https://staticcheck.dev/docs/checks/#ST1000 - -ST1000 # Don't check "Poorly chosen identifier". # https://staticcheck.dev/docs/checks/#ST1003 - -ST1003 exclusions: generated: lax rules: - linters: - const - dupl - gochecknoglobals - goconst - golint - lll - unparam path: _test\.go - path: (.+)\.go$ text: Error return value of `(\w+\.)+(Close|CloseSend|Flush|Remove|(Un)?Setenv|(Fp|P)rint(f|ln))\` is not checked - path: (.+)\.go$ text: 'structtag: struct field \w+ repeats json tag' - path: (.+)\.go$ text: Subprocess launched with function call as argument or cmd arguments - linters: - cyclop path: (.+)_test\.go paths: - third_party$ - builtin$ - examples$ issues: max-same-issues: 0 formatters: enable: - gofmt - gofumpt exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ ================================================ FILE: .mailmap ================================================ Alex Gervais alex Alex Gervais Donny Yung Flynn John Esmet Luke Shumaker Maxime Legault-Venne Rafael Schloming Thomas Hallgren Abhay Saxena Alvaro Saurin <1841612+inercia@users.noreply.github.com> Philip Lombardi plombardi Philip Lombardi <893096+plombardi89@users.noreply.github.com> Philip Lombardi ================================================ FILE: .protolint.yaml ================================================ lint: # Which rules to enable/disable. rules: remove: - ENUM_FIELD_NAMES_PREFIX # Settings for those rules. rules_option: max_line_length: max_chars: 120 ================================================ FILE: CHANGELOG.OLD.md ================================================ # Changelog ### 2.13.3 (May 25, 2023) - Feature: Add `.Values.hooks.curl.imagePullSecrets` and `.Values.hooks.curl.imagePullSecrets` to Helm values. PR [3079](https://github.com/telepresenceio/telepresence/pull/3079). - Change: The default setting of the reinvocationPolicy for the mutating webhook dealing with agent injections changed from `Never` to `IfNeeded`. - Bugfix: The `eks.amazonaws.com/serviceaccount` volume injected by EKS is now exported and remotely mounted during an intercept. Ticket [3166](https://github.com/telepresenceio/telepresence/issues/3166). - Bugfix: The mutating webhook now correctly applies the namespace selector even if the cluster version contains non-numeric characters. For example, it can now handle versions such as Major:"1", Minor:"22+". PR [3184](https://github.com/telepresenceio/telepresence/pull/3184). - Bugfix: The "telepresence" Docker network will now propagate DNS AAAA queries to the Telepresence DNS resolver when it runs in a Docker container. Ticket [3179](https://github.com/telepresenceio/telepresence/issues/3179). - Bugfix: Running `telepresence intercept --local-only --docker-run` no longer results in a panic. Ticket [3171](https://github.com/telepresenceio/telepresence/issues/3171). - Bugfix: Running `telepresence intercept --local-only --mount false` no longer results in an incorrect error message saying "a local-only intercept cannot have mounts". Ticket [3171](https://github.com/telepresenceio/telepresence/issues/3171). - Bugfix: The helm chart now correctly handles custom `agentInjector.webhook.port` that was not being set in hook URLs. PR [3161](https://github.com/telepresenceio/telepresence/pull/3161). - Bugfix: `.intercept.disableGlobal` and `.timeouts.agentArrival` are now correctly honored. ### 2.13.2 (May 12, 2023) - Bugfix: Replaced `/` characters with a `-` when the authenticator service creates the kubeconfig in the Telepresence cache. PR [3167](https://github.com/telepresenceio/telepresence/pull/3167). - Feature: Configurable strategy (`auto`, `powershell`. or `registry`) to set the global DNS search path on Windows. Default is `auto` which means try `powershell` first, and if it fails, fall back to `registry`. Ticket [3152](https://github.com/telepresenceio/telepresence/issues/3152). - Feature: The timeout for the traffic manager to wait for traffic agent to arrive can now be configured in the `values.yaml` file using `timeouts.agentArrival`. The default timeout is still 30 seconds. PR [3148](https://github.com/telepresenceio/telepresence/pull/3148). - Bugfix: The automatic discovery of a local container based cluster (minikube or kind) used when the Telepresence daemon runs in a container, now works on macOS and Windows, and with different profiles, ports, and cluster names PR [3165](https://github.com/telepresenceio/telepresence/pull/3165). - Bugfix: FTP Stability improvements. Multiple simultaneous intercepts can transfer large files in bidirectionally and in parallel. PR [3157](https://github.com/telepresenceio/telepresence/pull/3157). - Bugfix: Pods using persistent volumes no longer causes timeouts when intercepted. - Bugfix: Ensure that `telepresence connect` succeeds even though DNS isn't configured correctly. Ticket [3143](https://github.com/telepresenceio/telepresence/issues/3143). PR [3154](https://github.com/telepresenceio/telepresence/pull/3154). - Bugfix: The traffic-manager would sometimes panic with a "close of closed channel" message and exit. PR [3160](https://github.com/telepresenceio/telepresence/pull/3160). - Bugfix: The traffic-manager would sometimes panic and exit after some time due to a type cast panic. Ticket [3149](https://github.com/telepresenceio/telepresence/issues/3149). ### 2.13.1 (April 20, 2023) - Change: Update ambassador-agent to version 1.13.13 ### 2.13.0 (April 18, 2023) - Feature: The Docker network used by a Kind or Minikube (using the "docker" driver) installation, is automatically detected and connected to a Docker container running the Telepresence daemon. - Feature: Mapped namespaces are included in the output of the `telepresence status` command. - Feature: There's a new --address flag to the intercept command allowing users to set the target IP of the intercept. - Feature: The new flags `--docker-build`, and `--docker-build-opt` was added to `telepresence intercept` to facilitate a docker run directly from a docker context. - Bugfix: Using `telepresence intercept --docker-run` now works with a container based daemon started with `telepresence connect --docker` - Bugfix: DNS works properly even when no cluster subnet is routed by the Telepresence VIF. - Bugfix: The Traffic Manager uses a fail-proof way to determine the cluster domain. - Bugfix: DNS on windows is more reliable and performant. - Bugfix: The agent is now correctly injected even with a high number of deployment starting at the same time. - Bugfix: The kubeconfig is made self-contained before running Telepresence daemon in a Docker container. - Bugfix: The client will no longer need cluster wide permissions when connected to a namespace scoped Traffic Manager. - BugFix: The version command won't throw an error anymore if there is no kubeconfig file defined. ### 2.12.2 (April 4, 2023) - Security: Update golang to 1.20.3 to address CVE-2023-24534, CVE-2023-24536, CVE-2023-24537, CVE-2023-24538 ### 2.12.1 (March 22, 2023) - Bugfix: Illegal characters are now replaced when a docker container name is generated from a kubernetes context name. ### 2.12.0 (March 20, 2023) - Feature: Telepresence can now start or connect to a daemon in a docker container by use of the global `--docker` flag. - Feature: Adds an authenticator package to support integration with the [client-go credential](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins) plugins when the daemon runs in a docker container. - Feature: The `telepresence helm` command now accepts a `--namespace` flag. - Change: Telepresence will now detect if services and pods are routable, independently of one another, before adding their routes. This is a change from before, when being already able to connect to pods prevented the addition of routes for services too. - Bugfix: The traffic-manager will no longer panic when the CNAME of kubernetes.default doesn't contain .svc. - Bugfix: The `telepresence helm install/upgrade --set` family of flags now work correctly with comma separated values. ### 2.11.1 (February 27, 2023) - Bugfix: The multi-arch build now for the proprietary traffic-manager and traffic-agent now works for both amd64 and arm64. ### 2.11.0 (February 22, 2023) - Feature: When Telepresence detects that it runs in a docker container, it will now expose its DNS on `localhost:53`. This makes the container itself a DNS server. Very handy when other containers use `--network container:[tp-container]`. - Feature: A new flag, `--local-mount-port ` will make `telepresence intercept --detailed-output --output=[yaml|json]` create a bridge to the remote SFTP service instead of starting an sshfs client. This enables the sshfs client to be started outside of the container, and thus, mount filesystems that then can be used as source for volumes that other containers will use. - Feature: The Telepresence daemon can now run as a long-lived process in a docker container so that CLI commands that run in other containers can use a common daemon for network access and intercepts. - Feature: A new boolean flag `--detailed-output` was added to the `telepresence intercept` command. It will output very detailed information about an intercept when used together with `--output=[json|yaml]`. Pull Request [3013](https://github.com/telepresenceio/telepresence/pull/3013). - Feature: IPv6 support. Thanks to [@0x6a77](https://www.github.com/0x6a77). Ticket [2978](https://github.com/telepresenceio/telepresence/issues/2978). - Feature: Adds two parameters `--also-proxy` and `--never-proxy` to the `telepresence connect` command. Ticket [2950](https://github.com/telepresenceio/telepresence/issues/2950). - Feature: Add a parameter `--manager-namespace` to the `telepresence connect` command. Ticket [2968](https://github.com/telepresenceio/telepresence/issues/2968) - Feature: Add a configuration `cluster.defaultManagerNamespace` for setting the default manager namespace. Ticket [2968](https://github.com/telepresenceio/telepresence/issues/2968) - Change: The namespace of the connected manager is now displayed in the `telepresence status` output. Ticket [2968](https://github.com/telepresenceio/telepresence/issues/2968) - Change: Depreciate `--watch` flag in `telepresence list` command. This is now covered by `--output json-stream` - Change: Add `--output` option `json-stream` - Bugfix: Fixed a bug when detecting VPN conflicts on macOS that removed conflicting gateway links. - Bugfix: Fixed a bug where connecting to certain VPNs that map the CIDR range of the cluster would result in no routes getting added. Ticket [3006](https://github.com/telepresenceio/telepresence/issues/3006) - Bugfix: Support ARM64 architecture Ticket [2786](https://github.com/telepresenceio/telepresence/issues/2786) ### 2.10.6 (February 14, 2023) Security release to rebuild with go 1.19.6 ### 2.10.5 (February 6, 2023) - Change: mTLS Secrets will now be mounted into the traffic agent, instead of expected to be read by it from the API. - Bugfix: Fixed a bug that prevented the local daemons from automatically reconnecting to the traffic manager when the network connection was lost. ### 2.10.4 (January 20, 2023) - Bugfix: Fix backward compatibility issue when using traffic-managers of version 2.9.5 or older. ### 2.10.2 (January 16, 2023) - Bugfix: Ensure that CLI and user-daemon binaries are the same version when running `telepresence helm install` or `telepresence helm upgrade`. ### 2.10.1 (January 11, 2023) - Bugfix: Fixed a regex in our release process. ### 2.10.0 (January 11, 2023) - Feature: The Traffic Manager can now be set to either "team" mode or "single user" mode. When in team mode, intercepts will default to http intercepts. - Feature: The `telepresence helm` sub-commands `insert` and `upgrade` now accepts all types of helm `--set-XXX` flags. - Feature: A new `telepresence helm upgrade` command was added with the additional flags `--reuse-values` and `--reset-values`. This means that the `telpresence helm install --upgrade` flag has been deprecated. - Feature: Image pull secrets for the traffic-agent can now be added using the Helm chart setting `agent.image.pullSecrets`. - Change: The configmap `traffic-manager-clients` has been renamed to `traffic-manager`. - Change: The Helm installation will now fail if `intercept.disableGlobal=true` and `traffiManager.mode` is not set to `team`. - Change: If the cluster is Kubernetes 1.21 or later, the mutating webhook will find the correct namespace using the label `kubernetes.io/metadata.name` rather than `app.kuberenetes.io/name`. Ticket [2913](https://github.com/telepresenceio/telepresence/issues/2913). - Change: The name of the mutating webhook now contains the namespace of the traffic-manager so that the webhook is easier to identify when there are multiple namespace scoped telepresence installations in the cluster. - Change: The OSS Helm chart is no longer pushed to the datawire Helm repository. It will instead be pushed from the telepresence proprietary repository. The OSS Helm chart is still what's embedded in the OSS telepresence client. PR [2943](https://github.com/telepresenceio/telepresence/pull/2943). - Bugfix: Telepresence no longer panics when `--docker-run` is combined with `--name ` instead of `--name=`. Ticket [2953](https://github.com/telepresenceio/telepresence/issues/2953). - Bugfix: Telepresence traffic-manager extracts the cluster domain (e.g. "cluster.local") using a CNAME lookup for "kubernetes.default" instead of "kubernetes.default.svc". - Bugfix: A timeout was added to the pre-delete hook `uninstall-agents`, so that a helm uninstall doesn't hang when there is no running traffic-manager. PR [2937](https://github.com/telepresenceio/telepresence/pull/2937). ### 2.9.5 (December 8, 2022) - Security: Update golang to 1.19.4 to address [CVE-2022-41720 and CVE-2022-41717](https://groups.google.com/g/golang-announce/c/L_3rmdT0BMU). - Bugfix: A regression that was introduced in 2.9.3, preventing use of gce authentication without also having a config element present in the gce configuration in the kubeconfig, has been fixed. ### 2.9.4 (December 5, 2022) - Feature: The traffic-manager can automatically detect that the node subnets are different from the pod subnets, and switch detection strategy to instead use subnets that cover the pod IPs. - Bugfix: The `telepresence helm` command `--set x=y` flag didn't correctly set values of other types than `string`. The code now uses standard Helm semantics for this flag. - Bugfix: Telepresence now uses the correct `agent.image` properties in the Helm chart when copying agent image settings from the `config.yml` file. - Bugfix: Initialization of FTP type file sharing is delayed, so that setting it using the Helm chart value `intercept.useFtp=true` works as expected. - Bugfix: The port-forward that is created when Telepresence connects to a cluster is now properly closed when `telepresence quit` is called. - Bugfix: The user daemon no longer panics when the `config.yml` is modified at a time when the user daemon is running but no session is active. - Bugfix: Fix race condition that would occur when `telepresence connect` `telepresence leave` was called several times in rapid succession. ### 2.9.3 (November 23, 2022) - Feature: The helm chart now supports `livenessProbe` and `readinessProbe` for the traffic-manager deployment, so that the pod automatically restarts if it doesn't respond. - Change: The root daemon now communicates directly with the traffic-manager instead of routing all outbound traffic through the user daemon. - Change: The output of `telepresence version` is now aligned and no longer contains "(api v3)" - Bugfix: Using `telepresence loglevel LEVEL` now also sets the log level in the root daemon. - Bugfix: Multi valued kubernetes flags such as `--as-group` are now propagated correctly. - Bugfix: The root daemon would sometimes hang indefinitely when quit and connect were called in rapid succession. - Bugfix: Don't use `systemd.resolved` base DNS resolver unless cluster is proxied. ### 2.9.2 (November 16, 2022) - Bugfix: Fix panic when connecting to an older traffic-manager. - Bugfix: Fix `http-header` flag sometimes wouldn't propagate correctly. ### 2.9.1 (November 15, 2022) - Bugfix: Fix regression in 2.9.0 causing `no Auth Provider found for name “gcp”` when connecting. ### 2.9.0 (November 15, 2022) - Feature: A new `telepresence config view` command was added that shows how the client is currently configured. - Feature: The traffic-manager can now configure all clients that connect through the `client:` map in the `values.yaml` file. - Feature: The traffic-manager version is now included in the output from the `telepresence version` command. - Feature: add `podLabels` values to Helm Chart to add extra labels to deployment. - Feature: The telepresence flag `--output` now accepts `yaml` as a valid format. - Change: The `telepresence status --json` flag is deprecated. Use `telepresence status --output=json` instead. - Bugfix: Informational messages that don't really originate from the command, such as "Launching Telepresence Root Daemon", or "An update of telepresence ...", are discarded instead of being printed as plain text before the actual formatted output when using the `--output=json`. - Bugfix: An attempt to use an invalid value for the global `--output` flag now renders a proper error message. - Bugfix: Unqualified service names now resolves OK when using `telepresence intercept --docker-run`. - Bugfix: Files lingering under /etc/resolver on macOS are now removed when a new root daemon starts. ### 2.8.5 (November 2, 2022) - Change: This is a security release. It's identical with 2.8.3 but built using Go 1.19.3 to address [CVE-2022-41716 and Go issue https://go.dev/issue/56284](https://github.com/golang/go/issues/56284). ### 2.8.4 (November 2, 2022) - Change: Failed security release. Use 2.8.5. ### 2.8.3 (October 27, 2022) - Feature: The traffic-manager can be configured to disable global (non-http) intercepts using the Helm chart setting `intercept.disableGlobal`. - Feature: The port used for the mutating webhook can be configured using the Helm chart setting `agentInjector.webhook.port`. - Feature: A new repeated `--set a.b.c=v` flag was added to the `telepresence helm install` command so that values can be passed directly from the command line, without first storing them in a file. - Change: The default port for the mutating webhook is now `443`. It used to be `8443`. - Change: The traffic-manager will no longer default to use the `tel2` image for the traffic-agent when it is unable to connect to Ambassador Cloud. Air-gapped environments must declare what image to use in the Helm chart. - Bugfix: `telepresence connect` now works as long as the traffic manager is installed, even if it wasn't installed via `helm install` - Bugfix: Telepresence check-vpn no longer crashes when the daemons don't start properly. - Bugfix: The root daemon no longer crashes when the session boot times out before the cluster connection succeeds. ### 2.8.2 (October 15, 2022) - Feature: The Telepresence DNS resolver is now capable of resolving queries of type `A`, `AAAA`, `CNAME`, `MX`, `NS`, `PTR`, `SRV`, and `TXT`. - Feature: A new `client` struct was added to the Helm chart. It contains a `connectionTTL` that controls how long the traffic manager will retain a client connection without seeing any sign of life from the client. - Feature: A `dns` struct container the fields `includeSuffixes` and `excludeSuffixes` was added to the Helm chart `client` struct, making those values configurable per cluster. - Feature: The API port used by the traffic-manager is now configurable using the Helm chart value `apiPort`. The default port is 8081. - Change: The Helm chart `dnsConfig` was deprecated but retained for backward compatibility. The fields `alsoProxySubnets` and `neverProxySubnets` can now be found under `routing` in the `client` struct. - Change: The Helm chart `agentInjector.agentImage` was moved to `agent.image`. The old value is deprecated but retained for backward compatibility. - Change: The Helm chart `agentInjector.appProtocolStrategy` was moved to `agent.appProtocolStrategy`. The old value is deprecated but retained for backward compatibility. - Change: The Helm chart `dnsServiceName`, `dnsServiceNamespace`, and `dnsServiceIP` has been removed, because they are no longer needed. The TUN-device will use the traffic-manager pod-IP on platforms where it needs to dedicate an IP for its local resolver. - Bugfix: Environment variable interpolation now works for all definitions that are copied from pod containers into the injected traffic-agent container. - Bugfix: An attempt to create simultaneous intercepts that span multiple namespace on the same workstation is detected early and prohibited instead of resulting in failing DNS lookups later on. - Bugfix: Spurious and incorrect ""!! SRV xxx"" messages will no longer appear in the logs when the reason is normal context cancellation. - Bugfix: Single label names now resolves correctly when using Telepresence in Docker on a Linux host - Bugfix: The Helm chart value `appProtocolStrategy` is now correctly named (used to be `appPortStategy`) - Bugfix: Include file name in error message when failing to parse JSON file. ### 2.7.6 (September 16, 2022) - Reintroduce everything from 2.7.4 with fix for issue preventing the CLI from launching on arm64 builds ### 2.7.5 (September 14, 2022) - Revert of release 2.7.5 (so essentially the same as 2.7.3) ### 2.7.4 (September 14, 2022) - Feature: The `resources` for the traffic-agent container and the optional init container can be specified in the Helm chart using the `resource` and `initResource` fields of the `agentInjector.agentImage`. - Feature: When the traffic-manager fails to inject a traffic-agent, the cause for the failure is detected by reading the cluster events, and propagated to the user. - Feature: Telepresence can now use an embedded FTP client and load an existing FUSE library instead of running an external `sshfs` or `sshfs-win` binary. This feature is experimental in 2.7.x and enabled by setting `intercept.useFtp` to `true` in the `config.yml`. - Change: Telepresence on Windows upgraded winfsp from version 1.10 to 1.11 - Bugfix: Running CLI commands on Apple M1 machines will no longer throw warnings about `/proc/cpuinfo` and `/proc/self/auxv`. ### 2.7.3 (September 7, 2022) - Bugfix: CLI commands that are executed by the user daemon now use a pseudo TTY. This enables `docker run -it` to allocate a TTY and will also give other commands like `bash read` the same behavior as when executed directly in a terminal. - Bugfix: The traffic-manager will no longer log numerous warnings saying: "Issuing a systema request without ApiKey or InstallID may result in an error". - Bugfix: The traffic-manager will no longer log an error saying: "Unable to derive subnets from nodes" when the `podCIDRStrategy` is `auto` and it chooses to instead derive the subnets from the pod IPs. ### 2.7.2 (August 25, 2022) - Bugfix: Standard I/O is restored when using `telepresence intercept -- `. - Bugfix: Graciously handle nil intercept environment from the traffic-manager. - Feature: The timeout for the initial connectivity check that Telepresence performs in order to determine if the cluster's subnets are proxied or not can now be configured in the `config.yml` file using `timeouts.connectivityCheck`. The default timeout was changed from 5 seconds to 500 milliseconds to speed up the actual connect. - Feature: Adds cli autocompletion for the `--namespace` flag on the `list` and `intercept` commands, autocompletion for interceptable workloads on the `intercept` command, and autocompletion for active intercepts on the `leave` command. - Change: The command `telepresence gather-traces` now prints out a message on success. - Change: The command `telepresence upload-traces` now prints out a message on success. - Change: The command `telepresence gather-traces` now traces itself and reports errors with trace gathering - Change: The `cli.log` log is now logged at the same level as the `connector.log` - Bugfix: Streams created between the traffic-agent and the workstation are now properly closed when no interceptor process has been started on the workstation. This fixes a potential problem where a large number of attempts to connect to a non-existing interceptor would cause stream congestion and an unresponsive intercept. - Bugfix: Telepresence help message functionality without a running user daemon has been restored. - Bugfix: The `telepresence list` command no longer includes the `traffic-manager` deployment. ### 2.7.1 (August 10, 2022) - Change: The command `telepresence uninstall` has been restored, but the `--everything` flag is now deprecated. - Change: `telepresence helm uninstall` will only uninstall the traffic-manager and no longer accepts the `--everything`, `--agent`, or `--all-agents` flags. - Bugfix: `telepresence intercept` will attempt to connect to the traffic manager before creating an intercept. ### 2.7.0 (August 8, 2022) - Feature: `telepresence intercept` has gained a `--preview-url-add-request-headers` flag (and `telepresence preview create` a `--add-request-headers` flag) that can be used to inject request headers in to every request made through the preview URL. - Feature: The Docker image now contains a new program in addition to the existing traffic-manager and traffic-agent: the pod-daemon. The pod-daemon is a trimmed-down version of the user-daemon that is designed to run as a sidecar in a Pod, enabling CI systems to create preview deploys. - Feature: The Telepresence components now collect OpenTelemetry traces. Up to 10MB of trace data are available at any given time for collection from components. `telepresence gather-traces` is a new command that will collect all that data and place it into a gzip file, and `telepresence upload-traces` is a new command that will push the gzipped data into an OTLP collector. - Feature: The agent injector now supports a new annotation, `telepresence.getambassador.io/inject-ignore-volume-mounts`, that can be used to make the injector ignore specified volume mounts denoted by a comma-separated string. - Change: The traffic manager is no longer automatically installed into the cluster. Connecting or creating an intercept in a cluster without a traffic manager will return an error. - Feature: A new telepresence helm command was added to provide an easy way to install, upgrade, or uninstall the telepresence traffic-manager. - Change: The command `telepresence uninstall` has been moved to `telepresence helm uninstall`. - Change: Add an emptyDir volume and volume mount under `/tmp` on the agent sidecar so it works with `readOnlyRootFileSystem: true` - Feature: Added prometheus support to the traffic manager. ### 2.6.8 (June 23, 2022) - Feature: The name and namespace for the DNS Service that the traffic-manager uses in DNS auto-detection can now be specified. - Feature: Should the DNS auto-detection logic in the traffic-manager fail, users can now specify a fallback IP to use. - Feature: It is now possible to intercept UDP ports with Telepresence and also use `--to-pod` to forward UDP traffic from ports on localhost. - Change: The Helm chart will now add the `nodeSelector`, `affinity` and `tolerations` values to the traffic-manager's post-upgrade-hook and pre-delete-hook jobs. - Bugfix: Telepresence no longer fails to inject the traffic agent into the pod generated for workloads that have no volumes and `automountServiceAccountToken: false`. - Feature: The helm-chart now supports settings resources, securityContext and podSecurityContext for use with chart hooks. ### 2.6.7 (June 22, 2022) - Bugfix: The Telepresence client will remember and reuse the traffic-manager session after a network failure or other reason that caused an unclean disconnect. - Bugfix: Telepresence will no longer forward DNS requests for "wpad" to the cluster. - Bugfix: The traffic-agent will properly shut down if one of its goroutines errors. ### 2.6.6 (June 9, 2022) - Bugfix: The propagation of the `TELEPRESENCE_API_PORT` environment variable now works correctly. - Bugfix: The `--output json` global flag no longer outputs multiple objects. ### 2.6.5 (June 3, 2022) - Feature: The `reinvocationPolicy` or the traffic-agent injector webhook can now be configured using the Helm chart. - Feature: The traffic manager now accepts a root CA for a proxy, allowing it to connect to ambassador cloud from behind an HTTPS proxy. This can be configured through the helm chart. - Feature: A policy that controls when the mutating webhook injects the traffic-agent was added, and can be configured in the Helm chart. - Change: Telepresence on Windows upgraded wintun.dll from version 0.12 to version 0.14.1 - Change: Telepresence on Windows upgraded winfsp from version 1.9 to 1.10 - Change: Telepresence upgraded its embedded Helm from version 3.8.1 to 3.9 - Change: Telepresence upgraded its embedded Kubernetes API from version 0.23.4 to 0.24.1 - Feature: Added a `--watch` flag to `telepresence list` that can be used to watch interceptable workloads. - Change: The configuration setting for `images.webhookAgentImage` is now deprecated. Use `images.agentImage` instead. - Bugfix: The `reinvocationPolicy` or the traffic-agent injector webhook now defaults to `Never` insteadof `IfNeeded` so that `LimitRange`s on namespaces can inject a missing `resources` element into the injected traffic-agent container. - Bugfix: UDP based communication with services in the cluster now works as expected. - Bugfix: The command help will only show Kubernetes flags on the commands that supports them - Bugfix: Only the errors from the last session will be considered when counting the number of errors in the log after a command failure. ### 2.6.4 (May 23, 2022) - Bugfix: The traffic-manager RBAC grants permissions to update services, deployments, replicatsets, and statefulsets. Those permissions are needed when the traffic-manager upgrades from versions < 2.6.0 and can be revoked after the upgrade. ### 2.6.3 (May 20, 2022) - Bugfix: The `--mount` intercept flag now handles relative mount points correctly on non-windows platforms. Windows still require the argument to be a drive letter followed by a colon. - Bugfix: The traffic-agent's configuration update automatically when services are added, updated or deleted. - Bugfix: The `--mount` intercept flag now handles relative mount points correctly on non-windows platforms. Windows still require the argument to be a drive letter followed by a colon. - Bugfix: The traffic-agent's configuration update automatically when services are added, updated or deleted. - Bugfix: Telepresence will now always inject an initContainer when the service's targetPort is numeric - Bugfix: Workloads that have several matching services pointing to the same target port are now handled correctly. - Bugfix: A potential race condition causing a panic when closing a DNS connection is now handled correctly. - Bugfix: A container start would sometimes fail because and old directory remained in a mounted temp volume. ### 2.6.2 (May 17, 2022) - Bugfix: Workloads controlled by workloads like Argo `Rollout` are injected correctly. - Bugfix: Multiple services appointing the same container port no longer result in duplicated ports in an injected pod. - Bugfix: The `telepresence list` command no longer errors out with "grpc: received message larger than max" when listing namespaces with a large number of workloads. ### 2.6.1 (May 16, 2022) - Bugfix: Telepresence will now handle multiple path entries in the KUBECONFIG environment correctly. - Bugfix: Telepresence will no longer panic when using preview URLs with traffic-managers < 2.6.0 - Change: Traffic-manager now attempts to obtain a cluster id from the license if it could not obtain it from the Kubernetes API. ### 2.6.0 (May 13, 2022) - Feature: Traffic-agent is now capable of intercepting multiple containers and multiple ports per container. - Feature: Telepresence client now require less RBAC permissions in order to intercept. - Change: All pod-injection is performed by the mutating webhook. Client will no longer modify workloads. - Change: Traffic-agent is configured using a ConfigMap entry. In prior versions, the configuration was passed in the container environment. - Change: The helm-chart no longer has a default set for the agentInjector.image.name, and unless its set, the traffic-manager will ask SystemA for the preferred image. - Change: Client no longer needs RBAC permissions to update deployments, replicasets, and statefulsets. - Change: Telepresence now uses Helm version 3.8.1 when installing the traffic-manager - Change: The traffic-manager will not accept connections from clients older than 2.6.0. It can't, because they still use the old way of injecting the agent by modifying the workload. - Change: When upgrading, all workloads with injected agents will have their agent "uninstalled" automatically. The mutating webhook will then ensure that their pods will receive an updated traffic-agent. - Bugfix: Remote mounts will now function correctly with custom `securityContext`. - Bugfix: The help for commands that accept kubernetes flags will now display those flags in a separate group. - Bugfix: Using `telepresence leave` or `telepresence quit` on an intercept that spawned a command using `--` on the command line will now terminate that command since it's considered parented by the intercept that is removed. - Change: Add support for structured output as JSON by setting the global --output=json flag. ### 2.5.8 (April 27, 2022) - Bugfix: Telepresence now ensures that the download folder for the enhanced free client is created prior to downloading it. ### 2.5.7 (April 25, 2022) - Change: A namespaced traffic-manager will no longer require cluster wide RBAC. Only Roles and RoleBindings are now used. - Bugfix: The DNS recursion detector didn't work correctly on Windows, resulting in sporadic failures to resolve names that were resolved correctly at other times. - Bugfix: A telepresence session will now last for 24 hours after the user's last connectivity. If a session expires, the connector will automatically try to reconnect. ### 2.5.6 (April 15, 2022) - Bugfix: The `gather-logs` command will no longer send any logs through `gRPC`. - Change: Telepresence agents watcher will now only watch namespaces that the user has accessed since the last `connect`. ### 2.5.5 (April 8, 2022) - Change: The traffic-manager now requires permissions to read pods across namespaces even if installed with limited permissions - Bugfix: The DNS resolver used on Linux with systemd-resolved now flushes the cache when the search path changes. - Bugfix: The `telepresence list` command will produce a correct listing even when not preceded by a `telepresence connect`. - Bugfix: The root daemon will no longer get into a bad state when a disconnect is rapidly followed by a new connect. - Bugfix: The client will now only watch agents from accessible namespaces, and is also constrained to namespaces explicitly mapped using the `connect` command's `--mapped-namespaces` flag. - Bugfix: The `gather-logs` command will only gather traffic-agent logs from accessible namespaces, and is also constrained to namespaces explicitly mapped using the `connect` command's `--mapped-namespaces` flag. ### 2.5.4 (March 29, 2022) - Change: The list command, when used with the `--intercepts` flag, will list the users intercepts from all namespaces - Change: The status command includes the install id, user id, account id, and user email in its result, and can print output as JSON - Change: The lookup-timeout config flag used to set timeouts for DNS queries resolved by a cluster now also configures the timeout for fallback queries (i.e. queries not resolved by the cluster) when connected to the cluster. - Change: The TUN device will no longer route pod or service subnets if it is running in a machine that's already connected to the cluster - Bugfix: The client's gather logs command and agent watcher will now respect the configured grpc.maxReceiveSize - Bugfix: Client and agent sessions no longer leaves dangling waiters in the traffic-manager when they depart. - Bugfix: An advice to "see logs for details" is no longer printed when the argument count is incorrect in a CLI command. - Bugfix: Removed a bad concatenation that corrupted the output path of `telepresence gather-logs`. - Bugfix: Agent container is no longer sensitive to a random UID or an UID imposed by a SecurityContext. - Bugfix: Intercepts that fail to create are now consistently removed to prevent non-working dangling intercepts from sticking around. - Bugfix: The ingress-l5 flag will no longer be forcefully set to equal the --ingress-host flag - Bugfix: The DNS fallback resolver on Linux now correctly handles concurrent requests without timing them out ### 2.5.3 (February 25, 2022) - Feature: Client-side binaries for the arm64 architecture are now available for linux - Bugfix: Fixed bug in the TCP stack causing timeouts after repeated connects to the same address ### 2.5.2 (February 23, 2022) - Bugfix: Fixed a bug where Telepresence would use the last server in resolv.conf ### 2.5.1 (February 19, 2022) - Bugfix: Fixed a bug where using a GKE cluster would error with: No Auth Provider found for name "gcp" ### 2.5.0 (February 18, 2022) - Feature: The flags `--http-path-equal`, `--http-path-prefix`, and `--http-path-regex` can can be used in addition to the `--http-match` flag to filter personal intercepts by the request URL path - Feature: The flag `--http-meta` can be used to declare metadata key value pairs that will be returned by the Telepresence rest API endpoint /intercept-info - Feature: Telepresence Login now prompts you to optionally install an enhanced free client, which has some additional features when used with Ambassador Cloud. - Change: Logs generated by the CLI are no longer discarded. Instead, they will end up in `cli.log`. - Change: Both daemon logfiles now rotate daily instead of once for each new connect - Change: The flag `--http-match` was renamed to `--http-header`. Old flag still works, but is deprecated and doesn't show up in the help. - Change: The verb "watch" was added to the set of required verbs when accessing services and workloads for the client RBAC ClusterRole - Change: Telepresence is no longer backward compatible with versions 2.4.4 or older because the deprecated multiplexing tunnel functionality was removed. - Change: The global networking flags are no longer global. Using them will render a deprecation warning unless they are supported by the command. The subcommands that support networking flags are `connect`, `current-cluster-id`, and `genyaml`. - Change: Telepresence now includes GOARCH of the binary in the metadata reported. - Bugfix: The also-proxy and never-proxy subnets are now displayed correctly when using the `telepresence status` command - Bugfix: Telepresence will no longer require `SETENV` privileges when starting the root daemon. - Bugfix: Telepresence will now parse device names containing dashes correctly when determining routes that it should never block. - Bugfix: The cluster domain (typically "cluster.local") is no longer added to the DNS `search` on Linux using `systemd-resolved`. Instead, it is added as a `domain` so that names ending with it are routed to the DNS server. - Bugfix: Fixed a bug where the `--json` flag did not output json for `telepresence list` when there were no workloads. - Change: Updated README file with more details about the project. - Bugfix: Fixed a bug where the overriding DNS resolver would break down in Linux if /etc/resolv.conf listed an ipv6 resolver ### 2.4.11 (February 10, 2022) - Change: Include goarch metadata for reporting to distinguish between Intel and Apple Silicon Macs ### 2.4.10 (January 13, 2022) - Feature: The flag `--http-plaintext` can be used to ensure that an intercept uses plaintext http or grpc when communicating with the workstation process. - Feature: The port used by default in the `telepresence intercept` command (8080), can now be changed by setting the `intercept.defaultPort` in the `config.yml` file. - Feature: The strategy when selecting the application protocol for personal intercepts in agents injected by the mutating webhook can now be configured using the `agentInjector.appProtocolStrategy` in the Helm chart. - Feature: The strategy when selecting the application protocol for personal intercepts can now be configured using the `intercept.appProtocolStrategy` in the `config.yml` file. - Change: Telepresence CI now runs in GitHub Actions instead of Circle CI. - Bugfix: Telepresence will no longer log invalid: "unhandled connection control message: code DIAL_OK" errors. - Bugfix: User will not be asked to log in or add ingress information when creating an intercept until a check has been made that the intercept is possible. - Bugfix: Output to `stderr` from the traffic-agent's `sftp` and the client's `sshfs` processes are properly logged as errors. - Bugfix: Auto installer will no longer not emit backslash separators for the `/tel-app-mounts` paths in the traffic-agent container spec when running on Windows ### 2.4.9 (December 9, 2021) - Bugfix: Fixed an error where access tokens were not refreshed if you log in while the daemons are already running. - Bugfix: A helm upgrade using the --reuse-values flag no longer fails on a "nil pointer" error caused by a nil `telpresenceAPI` value. ### 2.4.8 (December 3, 2021) - Feature: A RESTful service was added to Telepresence, both locally to the client and to the `traffic-agent` to help determine if messages with a set of headers should be consumed or not from a message queue where the intercept headers are added to the messages. - Change: The environment variable TELEPRESENCE_LOGIN_CLIENT_ID is no longer used. - Feature: There is a new subcommand, `test-vpn`, that can be used to diagnose connectivity issues with a VPN. - Bugfix: The tunneled network connections between Telepresence and Ambassador Cloud now behave more like ordinary TCP connections, especially around timeouts. ### 2.4.7 (November 24, 2021) - Feature: The agent injector now supports a new annotation, `telepresence.getambassador.io/inject-service-name`, that can be used to set the name of the service to be intercepted. This will help disambiguate which service to intercept for when a workload is exposed by multiple services, such as can happen with Argo Rollouts - Feature: The kubeconfig extensions now support a `never-proxy` argument, analogous to `also-proxy`, that defines a set of subnets that will never be proxied via telepresence. - Feature: Added flags to "telepresence intercept" that set the ingress fields as an alternative to using the dialogue. - Change: Telepresence check the versions of the client and the daemons and ask the user to quit and restart if they don't match. - Change: Telepresence DNS now uses a very short TTL instead of explicitly flushing DNS by killing the `mDNSResponder` or doing `resolvectl flush-caches` - Bugfix: Legacy flags such as `--swap-deployment` can now be used together with global flags. - Bugfix: Outbound connections are now properly closed when the peer closes. - Bugfix: The DNS-resolver will trap recursive resolution attempts (may happen when the cluster runs in a docker-container on the client). - Bugfix: The TUN-device will trap failed connection attempts that results in recursive calls back into the TUN-device (may happen when the cluster runs in a docker-container on the client). - Bugfix: Fixed a potential deadlock when a new agent joined the traffic manager. - Bugfix: The app-version value of the Helm chart embedded in the telepresence binary is now automatically updated at build time. The value is hardcoded in the original Helm chart when we release so this fix will only affect our nightly builds. - Bugfix: The configured webhookRegistry is now propagated to the webhook installer even if no webhookAgentImage has been set. - Bugfix: Login logs the user in when their access token has expired, instead of having no effect. ### 2.4.6 (November 2, 2021) - Feature: Telepresence CLI is now built and published for Apple Silicon Macs. - Feature: Telepresence now supports manually injecting the traffic-agent YAML into workload manifests. Use the `genyaml` command to create the sidecar YAML, then add the `telepresence.getambassador.io/manually-injected: "true"` annotation to your pods to allow Telepresence to intercept them. - Feature: Added a json flag for the "telepresence list" command. This will aid automation. - Change: `--help` text now includes a link to https://www.telepresence.io/ so users who download Telepresence via Brew or some other mechanism are able to find the documentation easily. - Bugfix: Telepresence will no longer attempt to proxy requests to the API server when it happens to have an IP address within the CIDR range of pods/services. ### 2.4.5 (October 15, 2021) - Feature: Intercepting headless services is now supported. It's now possible to request a headless service on whatever port it exposes and get a response from the intercept. - Feature: Preview url questions have more context and provide "best guess" defaults. - Feature: The `gather-logs` command added two new flags. One to anonymize pod names + namespaces and the other for getting the pod yaml of the `traffic-manager` and any pod that contains a `traffic-agent`. - Change: Use one tunnel per connection instead of multiplexing into one tunnel. This client will still be backwards compatible with older `traffic-manager`s that only support multiplexing. - Bugfix: Telepresence will now log that the kubernetes server version is unsupported when using a version older than 1.17. - Bugfix: Telepresence only adds the security context when necessary: intercepting a headless service or using a numeric port with the webhook agent injector. ### 2.4.4 (September 27, 2021) - Feature: The strategy used by traffic-manager's discovery of pod CIDRs can now be configured using the Helm chart. - Feature: Add the command `telepresence gather-logs`, which bundles the logs for all components into one zip file that can then be shared in a GitHub issue, in slack, etc. Use `telepresence gather-logs --help` to see additional options for running the command. - Feature: The agent injector now supports injecting Traffic Agents into pods that have unnamed ports. - Bugfix: The traffic-manager now uses less CPU-cycles when computing the pod CIDRs. - Bugfix: If a deployment annotated with webhook annotations is deployed before telepresence is installed, telepresence will now install an agent in that deployment before intercept - Bugfix: Fix an issue where the traffic-manager would sometimes go into a CPU loop. - Bugfix: The TUN-device no longer builds an unlimited internal buffer before sending it when receiving lots of TCP-packets without PSH. Instead, the buffer is flushed when it reaches a size of 64K. - Bugfix: The user daemon would sometimes hang when it encountered a problem connecting to the cluster or the root daemon. - Bugfix: Telepresence correctly reports an intercept port conflict instead of panicking with segfault. ### 2.4.3 (September 15, 2021) - Feature: The environment variable `TELEPRESENCE_INTERCEPT_ID` is now available in the interceptor's environment. - Bugfix: A timing related bug was fixed that sometimes caused a "daemon did not start" failure. - Bugfix: On Windows, crash stack traces and other errors were not written to the log files, now they are. - Bugfix: On Linux kernel 4.11 and above, the log file rotation now properly reads the birth-time of the log file. On older kernels, it continues to use the old behavior of using the change-time in place of the birth-time. - Bugfix: Telepresence will no longer refer the user to the daemon logs for errors that aren't related to problems that are logged there. - Bugfix: The overriding DNS resolver will no longer apply search paths when resolving "localhost". - Bugfix: The cluster domain used by the DNS resolver is retrieved from the traffic-manager instead of being hard-coded to "cluster.local". - Bugfix: "Telepresence uninstall --everything" now also uninstalls agents installed via mutating webhook - Bugfix: Downloading large files during an intercept will no longer cause timeouts and hanging traffic-agent. - Bugfix: Passing false to the intercept command's --mount flag will no longer result in a filesystem being mounted. - Bugfix: The traffic manager will establish outbound connections in parallel instead of sequentially. - Bugfix: The `telepresence status` command reports correct DNS settings instead of "Local IP: nil, Remote IP: nil" ### 2.4.2 (September 1, 2021) - Feature: A new `telepresence loglevel ` subcommand was added that enables changing the loglevel temporarily for the local daemons, the `traffic-manager` and the `traffic-agents`. - Change: The default log-level is now `info` for all components of Telepresence. - Bugfix: The overriding DNS resolver will no longer apply search paths when resolving "localhost". - Bugfix: The RBAC was not updated in the helm chart to enable the traffic-manager to `get` and `list` namespaces, which would impact users who use licensed features of the Telepresence extensions in an air-gapped environment. - Bugfix: The timeout for Helm actions wasn't always respected which could cause a failing install of the `traffic-manager` to make the user daemon to hang indefinitely. ### 2.4.1 (August 30, 2021) - Bugfix: Telepresence will now mount all directories from `/var/run/secrets`, not just the kubernetes.io ones. This allows the mounting of secrets directories such as eks.amazonaws.com (for IRSA tokens) - Bugfix: The grpc.maxReceiveSize setting is now correctly propagated to all grpc servers. This allows users to mitigate a root daemon crash when sending a message over the default maximum size. - Bugfix: Some slight fixes to the `homebrew-package.sh` script which will enable us to run it manually if we ever need to make homebrew point at an older version. - Feature: Helm chart has now a feature to on demand regenerate certificate used for mutating webhook by setting value. `agentInjector.certificate.regenerate` - Change: The traffic-manager now requires `get` namespace permissions to get the cluster ID instead of that value being passed in as an environment variable to the traffic-manager's deployment. - Change: The traffic-manager is now installed via an embedded version of the Helm chart when `telepresence connect` is first performed on a cluster. This change is transparent to the user. A new configuration flag, `timeouts.helm` sets the timeouts for all helm operations performed by the Telepresence binary. - Bugfix: Telepresence will initialize the default namespace from the kubeconfig on each call instead of just doing it when connecting. - Bugfix: The timeout to keep idle outbound TCP connections alive was increased from 60 to 7200 seconds which is the same as the Linux `tcp_keepalive_time` default. - Bugfix: Telepresence will now remove a socket that is the result of an ungraceful termination and retry instead of printing an error saying "this usually means that the process has terminated ungracefully" - Change: Failure to report metrics is logged using loglevel info rather than error. - Bugfix: A potential deadlock situation is fixed that sometimes caused the user daemon to hang when the user was logged in. - Feature: The scout reports will now include additional metadata coming from environment variables starting with `TELEPRESENCE_REPORT_`. - Bugfix: The config setting `images.agentImage` is no longer required to contain the repository. The repository is instead picked from `images.repository`. - Change: The `registry`, `webhookRegistry`, `agentImage` and `webhookAgentImage` settings in the `images` group of the `config.yml` now get their defaults from `TELEPRESENCE_AGENT_IMAGE` and `TELEPRESENCE_REGISTRY`. ### 2.4.0 (August 4, 2021) - Feature: There is now a native Windows client for Telepresence. All the same features supported by the macOS and Linux client are available on Windows. - Feature: Telepresence can now receive messages from the cloud and raise them to the user when they perform certain commands. - Bugfix: Initialization of `systemd-resolved` based DNS sets routing domain to improve stability in non-standard configurations. - Bugfix: Edge case error when targeting a container by port number. Before if your matching/target container was at containers list index 0, but if there was a container at index 1 with no ports, then the "no ports" container would end up the selected one - Bugfix: A `$(NAME)` reference in the agent's environment will now be interpolated correctly. - Bugfix: Telepresence will no longer print an INFO level log message when no config.yml file is found. - Bugfix: A panic is no longer raised when passing an argument to the `telepresence intercept` option `--http-match` that doesn't contain an equal sign. - Bugfix: The `traffic-manager` will only send subnet updates to a client root daemon when the subnets actually change. - Bugfix: The agent uninstaller now distinguishes between recoverable and unrecoverable failures, allowing uninstallation from manually changed resources ### 2.3.7 (July 23, 2021) - Feature: An `also-proxy` entry in the Kubernetes cluster config will show up in the output of the `telepresence status` command. - Feature: `telepresence login` now has an `--apikey=KEY` flag that allows for non-interactive logins. This is useful for headless environments where launching a web-browser is impossible, such as cloud shells, Docker containers, or CI. - Bugfix: Dialer will now close if it gets a ConnectReject. This was encountered when doing an intercept without a local process running and would result in requests hanging indefinitely. - Bugfix: Made `telepresence list` command faster. - Bugfix: Mutating webhook injector correctly hides named ports for probes. - Bugfix: Initialization of `systemd-resolved` based DNS is more stable and failures causing telepresence to default to the overriding resolver will no longer cause general DNS lookup failures. - Bugfix: Fixed a regression introduced in 2.3.5 that caused `telepresence current-cluster-id` to crash. - Bugfix: New API keys generated internally for communication with Ambassador Cloud no longer show up as "no description" in the Ambassador Cloud web UI. Existing API keys generated by older versions of Telepresence will still show up this way. - Bugfix: Fixed a race condition that logging in and logging out rapidly could cause memory corruption or corruption of the `user-info.json` cache file used when authenticating with Ambassador Cloud. ### 2.3.6 (July 20, 2021) - Bugfix: Fixed a regression introduced in 2.3.5 that caused preview URLs to not work. - Bugfix: Fixed a regression introduced in 2.3.5 where the Traffic Manager's `RoleBinding` did not correctly appoint the `traffic-manager` `Role`, causing subnet discovery to not be able to work correctly. - Bugfix: Fixed a regression introduced in 2.3.5 where the root daemon did not correctly read the configuration file; ignoring the user's configured log levels and timeouts. - Bugfix: Fixed an issue that could cause the user daemon to crash during shutdown, as during shutdown it unconditionally attempted to close a channel even though the channel might already be closed. ### 2.3.5 (July 15, 2021) - Feature: Telepresence no longer depends on having an external `kubectl` binary, which might not be present for OpenShift users (who have `oc` instead of `kubectl`). - Feature: `skipLogin` can be used in the config.yml to tell the cli not to connect to cloud when using an air-gapped environment. - Feature: The Telepresence Helm chart now supports installing multiple Traffic Managers in multiple namespaces. This will allow operators to install Traffic Managers with limited permissions that match the permissions restrictions that Telepresence users are subject to. - Feature: The maximum size of messages that the client can receive over gRPC can now be configured. The gRPC default of 4MB isn't enough under some circumstances. - Change: `TELEPRESENCE_AGENT_IMAGE` and `TELEPRESENCE_REGISTRY` are now only configurable via config.yml. - Bugfix: Fixed and improved several error messages, to hopefully be more helpful. - Bugfix: Fixed a DNS problem on macOS causing slow DNS lookups when connecting to a local cluster. ### 2.3.4 (July 9, 2021) - Bugfix: Some log statements that contained garbage instead of a proper IP address now produce the correct address. - Bugfix: Telepresence will no longer panic when multiple services match a workload. - Bugfix: The traffic-manager will now accurately determine the service subnet by creating a dummy-service in its own namespace. - Bugfix: Telepresence connect will no longer try to update the traffic-manager's clusterrole if the live one is identical to the desired one. - Bugfix: The Telepresence helm chart no longer fails when installing with `--set clientRbac.namespaced=true` ### 2.3.3 (July 7, 2021) - Feature: Telepresence now supports installing the Traffic Manager via Helm. This will make it easy for operators to install and configure the server-side components of Telepresence separately from the CLI (which in turn allows for better separation of permissions). - Feature: As the `traffic-manager` can now be installed in any namespace via Helm, Telepresence can now be configured to look for the traffic manager in a namespace other than `ambassador`. This can be configured on a per-cluster basis. - Feature: `telepresence intercept` now supports a `--to-pod` flag that can be used to port-forward sidecars' ports from an intercepted pod - Feature: `telepresence status` now includes more information about the root daemon. - Feature: We now do nightly builds of Telepresence for commits on release/v2 that haven't been tagged and published yet. - Change: Telepresence no longer automatically shuts down the old `api_version=1` `edgectl` daemon. If migrating from such an old version of `edgectl` you must now manually shut down the `edgectl` daemon before running Telepresence. This was already the case when migrating from the newer `api_version=2` `edgectl`. - Bugfix: The root daemon no longer terminates when the user daemon disconnects from its gRPC streams, and instead waits to be terminated by the CLI. This could cause problems with things not being cleaned up correctly. - Bugfix: An intercept will survive deletion of the intercepted pod provided that another pod is created (or already exists) that can take over. ### 2.3.2 (June 18, 2021) - Feature: The mutator webhook for injecting traffic-agents now recognizes a `telepresence.getambassador.io/inject-service-port` annotation to specify which port to intercept; bringing the functionality of the `--port` flag to users who use the mutator webook in order to control Telepresence via GitOps. - Feature: Outbound connections are now routed through the intercepted Pods which means that the connections originate from that Pod from the cluster's perspective. This allows service meshes to correctly identify the traffic. - Change: Inbound connections from an intercepted agent are now tunneled to the manager over the existing gRPC connection, instead of establishing a new connection to the manager for each inbound connection. This avoids interference from certain service mesh configurations. - Change: The traffic-manager requires RBAC permissions to list Nodes, Pods, and to create a dummy Service in the manager's namespace. - Change: The on-laptop client no longer requires RBAC permissions to list Nodes in the cluster or to create Services, as that functionality has been moved to the traffic-manager. - Bugfix: Telepresence will now detect the pod CIDR ranges even if they are not listed in the Nodes. - Bugfix: The list of cluster subnets that the virtual network interface will route is now configured dynamically and will follow changes in the cluster. - Bugfix: Subnets fully covered by other subnets are now pruned internally and thus never superfluously added to the laptop's routing table. - Change: The `trafficManagerAPI` timout default has changed from 5 seconds to 15 seconds, in order to facilitate the extended time it takes for the traffic-manager to do its initial discovery of cluster info as a result of the above bugfixes. - Bugfix: On macOS, files generated under `/etc/resolver/` as the result of using `include-suffixes` in the cluster config are now properly removed on quit. - Bugfix: Telepresence no longer erroneously terminates connections early when sending a large HTTP response from an intercepted service. - Bugfix: When shutting down the user-daemon or root-daemon on the laptop, `telepresence quit` and related commands no longer return early before everything is fully shut down. Now it can be counted on that by the time the command has returned that all the side-effects on the laptop have been cleaned up. ### 2.3.1 (June 14, 2021) - Feature: Agents can now be installed using a mutator webhook - Feature: DNS resolver can now be configured with respect to what IP addresses that are used, and what lookups that gets sent to the cluster. - Feature: Telepresence can now be configured to proxy subnets that aren't part of the cluster but only accesible from the cluster. - Change: The `trafficManagerConnect` timout default has changed from 20 seconds to 60 seconds, in order to facilitate the extended time it takes to apply everything needed for the mutator webhook. - Change: Telepresence is now installable via `brew install datawire/blackbird/telepresence` - Bugfix: Fix a bug where sometimes large transfers from services on the cluster would hang indefinitely ### 2.3.0 (June 1, 2021) - Feature: Telepresence is now installable via brew - Feature: `telepresence version` now also includes the version of the currently running user daemon. - Change: A TUN-device is used instead of firewall rules for routing outbound connections. - Change: Outbound connections now use gRPC instead of ssh, and the traffic-manager no longer has a sshd running. - Change: The traffic-agent no longer has a sshd running. Remote volume mounts use sshfs in slave mode, talking directly to sftp. - Change: The local DNS now routes the name lookups to intercepted agents or traffic-manager. - Change: The default log-level for the traffic-manager and the root-daemon was changed from "debug" to "info". - Change: The command line is now statically-linked, so it is usable on systems with different libc's. - Bugfix: Using --docker-run no longer fail to mount remote volumes when docker runs as root. - Bugfix: Fixed a number of race conditions. - Bugfix: Fix a crash when there is an error communicating with the traffic-manager about Ambassador Cloud. - Bugfix: Fix a bug where sometimes when displaying or logging a timeout error it fails to determine which configurable timeout is responsible. - Bugfix: The root-user daemon now respects the timeouts in the normal user's configuration file. ### 2.2.2 (May 17, 2021) - Feature: Telepresence translates legacy Telepresence commands into viable Telepresence commands. - Bugfix: Intercepts will only look for agents that are in the same namespace as the intercept. ### 2.2.1 (April 29, 2021) - Bugfix: Improve `ambassador` namespace detection that was trying to create the namespace even when the namespace existed, which was an undesired RBAC escalation for operators. - Bugfix: Telepresence will now no longer generate excessive traffic trying repeatedly to exchange auth tokens with Ambassador Cloud. This could happen when upgrading from <2.1.4 if you had an expired `telepresence login` from before upgrading. - Bugfix: `telepresence login` now correctly handles expired logins, just like all of the other subcommands. ### 2.2.0 (April 19, 2021) - Feature: `telepresence intercept` now has the option `--docker-run` which will start a docker container with intercepted environment and volume mounts. - Bugfix: `telepresence uninstall` can once again uninstall agents installed by older versions of Telepresence. - Feature: Addition of `telepresence current-cluster-id` and `telepresence license` commands for using licenses with the Ambassador extension, primarily in air-gapped environments. ### 2.1.5 (April 12, 2021) - Feature: When intercepting `--port` now supports specifying a service port or a service name. Previously, only service name was supported. - Feature: Intercepts using `--mechanism=http` now support mTLS. - Bugfix: One of the log messages was using the incorrect variable, which led to misleading error messages on `telepresence uninstall`. - Bugfix: Telepresence no longer generates port names longer than 15 characters. ### 2.1.4 (April 5, 2021) - Feature: `telepresence status` has been enhanced to provide more information. In particular, it now provides separate information on the daemon and connector processes, as well as showing login status. - Feature: Telepresence now supports intercepting StatefulSets - Change: Telepresence necessary RBAC has been refined to support StatefulSets and now requires "get,list,update" for StatefulSets - Change: Telepresence no longer requires that port 1080 must be available. - Change: Telepresence now makes use of refresh tokens to avoid requiring the user to manually log in so often. - Bugfix: Fix race condition that occurred when intercepting a ReplicaSet while another pod was terminating in the same namespace (this fixes a transient test failure) - Bugfix: Fix error when intercepting a ReplicaSet requires the containerPort to be hidden. - Bugfix: `telepresence quit` no longer starts the daemon process just to shut it down. - Bugfix: Telepresence no longer hangs the next time it's run after getting killed. - Bugfix: Telepresence now does a better job of automatically logging in as necessary, especially with regard to expired logins. - Bugfix: Telepresence was incorrectly looking across all namespaces for services when intercepting, but now it only looks in the given namespace. This should prevent people from running into "Found multiple services" errors when services with the same selectors existed in other namespaces. ### 2.1.3 (March 29, 2021) - Feature: Telepresence now supports intercepting ReplicaSets (that aren't owned by a Deployment) - Change: The --deployment (-d) flag is now --workload (-w), as we start supporting more workloads than just Deployments - Change: Telepresence necessary RBAC has changed and now requires "delete" for Pods and "get,list,update" for ReplicaSets - Security: Upgrade to a newer OpenSSL, to address OpenSSL CVE-2021-23840. - Bugfix: Connecting to Minikube/Hyperkit no longer fails intermittently. - Bugfix: Telepresence will now make /var/run/secrets/kubernetes.io available when mounting remote volumes. - Bugfix: Hiccups in the connection to the cluster will no longer cause the connector to shut down; it now retries properly. - Bugfix: Fix a crash when binary dependencies are missing. - Bugfix: You can now specify a service when doing an intercept (--service), this is useful if you have two services that select on the same labels (e.g. If using Argo Rollouts do deployments) ### 2.1.2 (March 19, 2021) - Bugfix: Uninstalling agents now only happens once per deployment instead of once per agent. - Bugfix: The list command no longer shows agents from namespaces that aren't mapped. - Bugfix: IPv6 routes now work and don't prevent other pfctl rules being written in macOS - Bugfix: Pods with `hostname` and/or `subdomain` now get correct DNS-names and routes. - Change: Service UID was added to InterceptSpec to better link intercepts and services. - Feature: All timeouts can now be configured in a /telepresence/config.yml file ### 2.1.1 (March 12, 2021) - Bugfix: When looking at the container to intercept, it will check if there's a better match before using a container without containerPorts. - Bugfix: Telepresence will now map `kube-*` and `ambassador` namespaces by default. - Bugfix: Service port declarations that lack a TargetPort field will now correctly default to using the Port field instead. - Bugfix: Several DNS fixes. Notably, introduce a fake "tel2-search" domain that gets replaced with a dynamic DNS search when queried, which fixes DNS for Docker with no `-net host`. - Change: Improvements to how we report the requirements for volume mounts; notably, if the requirements are not met then it defaults to `--mount=false`. - Change: There has been substantial code cleanup in the "connector" process. ### 2.1.0 (March 8, 2021) - Feature: Support headless services (including ExternalName), which you can use if you used "Also Proxy" in telepresence 1. - Feature: Preview URLs can now set a layer-5 hostname (TLS-SNI and HTTP "Host" header) that is different than the layer-3 hostname (IP-address/DNS-name) that is used to dial to the ingress. - Feature: The Ingress info will now contain a layer-5 hostname that can be used for TLS-SLI and HTTP "Host" header when accessing a service. - Feature: Users can choose which port to intercept when intercepting a service with multiple ports. - Bugfix: Environment variables declared with `envFrom` in the app-container are now propagated correctly to the client during intercept. - Bugfix: The description of the `--everything` flag for the `uninstall` command was corrected. - Bugfix: Connecting to a large cluster could take a very long time and even make the process hang. This is no longer the case. - Bugfix: Telepresence now explicitly requires macFUSE version 4.0.5 or higher for macOS. - Bugfix: A `tail -F ` no longer results in a "Permission denied" when reconnecting to the cluster. - Change: The telepresence daemon will no longer use port 1234 for the firewall-to-SOCKS server, but will instead choose an available port dynamically. - Change: On connect, telepresence will no longer suggest the `--mapped-namespaces` flag when the user connects to a large cluster. ### 2.0.3 (February 24, 2021) - Feature: There is now an extension mechanism where you can tell Telepresence about different agents and what arguments they support. The new `--mechanism` flag can explicitly identify which extension to use. - Feature: An intercept of `NAME` that is made using `--namespace=NAMESPACE` but not using `--deployment` will use `NAME` as the name of the deployment and `NAME-NAMESPACE` as the name of the intercept. - Feature: Declare a local-only intercept for the purpose of getting direct outbound access to the intercept's namespace using boolean flag `--local-only`. - Bugfix: Fix a regression in the DNS resolver that prevented name resolution using NAME.NAMESPACE. Instead, NAME.NAMESPACE.svc.cluster.local was required. - Bugfix: Fixed race-condition in the agent causing attempts to dial to `:0`. - Bugfix: It is now more strict about which agent versions are acceptable and will be more eager to apply upgrades. - Change: Related to things now being in extensions, the `--match` flag has been renamed to `--http-match`. - Change: Cluster connection timeout has been increased from 10s to 20s. - Change: On connect, if telepresence detects a large cluster, it will suggest the `--mapped-namespaces` flag to the user as a way to speed it up. - Change: The traffic-agent now has a readiness probe associated with its container. ### 2.0.2 (February 18, 2021) - Feature: Telepresence is now capable of forwarding the intercepted Pod's volume mounts (as Telepresence 0.x did) via the `--mount` flag to `telepresence intercept`. - Feature: Telepresence will now allow simultaneous intercepts in different namespaces. - Feature: It is now possible for a user to limit what namespaces that will be used by the DNS-resolver and the NAT. - Bugfix: Fix the kubectl version number check to handle version numbers with a "+" in them. - Bugfix: Fix a bug with some configurations on macOS where we clash with mDNSResponder's use of port 53. ### 2.0.1 (February 9, 2021) - Feature: Telepresence is now capable of forwarding the environment variables of an intercepted service (as Telepresence 0.x did) and emit them to a file as text or JSON. The environment variables will also be propagated to any command started by doing a `telepresence intercept nnn -- `. - Bugfix: A bug causing a failure in the Telepresence DNS resolver when attempting to listen to the Docker gateway IP was fixed. The fix affects Windows using a combination of Docker and WSL2 only. - Bugfix: Telepresence now works correctly while OpenVPN is running on macOS. - Change: The background processes `connector` and `daemon` will now use rotating logs and a common directory. - macOS: `~/Library/Logs/telepresence/` - Linux: `$XDG_CACHE_HOME/telepresence/logs/` or `$HOME/.cache/telepresence/logs/` ================================================ FILE: CHANGELOG.yml ================================================ # The YAML in this file should contain: # # items: An array of releases with the following attributes: # - version: The (optional) version number of the release, if applicable. # - date: >- # The date of the release in the format YYYY-MM-DD. # If the date is (SUPERSEDED), then the release didn't happen, which means # that its notes belong to the next release. # - notes: An array of noteworthy changes included in the release, each having the following attributes: # - type: The type of change, one of `bugfix`, `feature`, `security` or `change`. # - title: A short title of the noteworthy change. # - body: >- # Two or three sentences describing the change and why it # is noteworthy. This is HTML, not plain text or # markdown. It is handy to use YAML's ">-" feature to # allow line-wrapping. # - image: >- # The URL of an image that visually represents the # noteworthy change. This path is relative to the # `release-notes` directory; if this file is # `FOO/releaseNotes.yml`, then the image paths are # relative to `FOO/release-notes/`. # - docs: The path to the documentation page where additional information can be found. # # For older changes, see CHANGELOG.OLD.md items: - version: 2.27.2 date: 2026-03-09 notes: - type: bugfix title: Fix duplicate Section field in .deb package causing dpkg install failure body: >- The .deb package control file contained a duplicate Section field, which caused dpkg -i to fail with a parsing error. The Section field was specified both as a top-level nfpm default and explicitly under deb.fields. Moved to the top-level section key and removed the deb.fields block entirely. docs: https://github.com/telepresenceio/telepresence/issues/4073 - version: 2.27.1 date: 2026-03-08 notes: - type: bugfix title: Fix duplicate Priority field in .deb package causing dpkg install failure body: >- The .deb package control file contained a duplicate Priority field, which caused dpkg -i to fail with a parsing error. This was caused by nfpm adding Priority: optional by default while the build configuration also specified it explicitly under deb.fields. docs: https://github.com/telepresenceio/telepresence/issues/4070 - type: bugfix title: Fix ingest command lookup when container name is not specified body: >- The traffic manager now properly handles ingest lookups when using telepresence ingest <workload> -- command without specifying a container name. Previously, this would fail for workloads because the lookup couldn't find ingests with empty container names. The traffic manager now provides clearer error messages when a container name is required but not specified, and correctly resolves ingests for single-container workloads automatically. docs: https://github.com/telepresenceio/telepresence/issues/4067 - version: 2.27.0 date: 2026-02-28 notes: - type: feature title: Add macOS package installer with root daemon as a system service body: >- A new macOS package installer (.pkg) is now available that installs Telepresence with the root daemon configured as a launchd service. This eliminates the need for elevated privileges when using Telepresence, as the daemon starts automatically at boot and runs in the background. Available for both Intel (amd64) and Apple Silicon (arm64) Macs. docs: install/client - type: feature title: Add Linux package installers with root daemon as a system service body: >- New Linux package installers (.deb for Debian/Ubuntu and .rpm for Fedora/RHEL) are now available that install Telepresence with the root daemon configured as a systemd service. This eliminates the need for elevated privileges when using Telepresence, as the service is enabled and started automatically during installation. Available for both amd64 and arm64 architectures. docs: install/client - type: feature title: Add Windows installer with root daemon as a system service body: >- A new Windows installer (.exe) is now available that installs Telepresence with the root daemon configured as a Windows service. The installer bundles WinFSP and SSHFS-Win dependencies for volume mount support, adds Telepresence to the system PATH, and optionally installs the TelepresenceDaemon service. This eliminates the need for elevated privileges when using Telepresence. Currently available for amd64 architecture only due to dependency constraints. docs: install/client - type: feature title: Add route-controller DaemonSet to prevent routing loops on local clusters body: >- A new optional route-controller DaemonSet can be deployed alongside the traffic-manager on local Kubernetes clusters (Kind, minikube, k3d, Docker Desktop) to prevent routing loops caused by deleted or non-existent service ClusterIPs. It installs an iptables FORWARD chain DROP rule for the service CIDR on every node, and adds per-IP kernel blackhole routes when a Service is deleted. Enable it with routeController.enabled=true in the Helm chart. docs: reference/route-controller - type: feature title: Automatic cache cleanup on version change body: >- Telepresence now tracks its version in a version.json file in the cache directory. When the CLI detects that the major.minor version differs from the running binary, it automatically quits running daemons and clears stale cache entries (preserving logs). This prevents issues caused by leftover cache files from a previous version. Patch and pre-release version changes do not trigger a cache cleanup. - type: bugfix title: Cluster DNS not injected into containers started by telepresence compose body: >- When using telepresence compose up, cluster hostnames did not resolve inside the compose container because the daemon DNS IP and the tel2-search DNS search domain were not being added to the generated compose spec. DNS and dns_search are now correctly set for all engaged compose services. docs: https://github.com/telepresenceio/telepresence/issues/4053 - version: 2.26.2 date: 2026-02-14 notes: - type: bugfix title: Dial 127.0.0.1 instead of 0.0.0.0 when connecting to local daemons body: >- When a VPN routes all private network addresses, dialing 0.0.0.0 gets routed through the VPN instead of reaching the locally running daemon. This causes Telepresence to report that no daemon is running even though the processes are active and listening. The client now dials 127.0.0.1 explicitly, and the Docker-published daemon port is bound to 127.0.0.1 to match. docs: https://github.com/telepresenceio/telepresence/issues/4048 - type: bugfix title: Fix HTTP intercepts via telepresence compose failing when httpFilters or httpPaths are set body: >- The compose extension set HeaderFilters and PathFilters on the intercept spec but left the mechanism as "tcp". This caused the traffic manager to reject the intercept with "global TCP/UDP intercepts are disabled". The mechanism is now correctly switched to "http" when any HTTP filters are specified, matching the behavior of the CLI intercept command. - type: bugfix title: Fix "root daemon is embedded" error on Windows elevated terminals body: >- When running Telepresence in an elevated (administrator) terminal on Windows, commands like connect and loglevel failed with "root daemon is embedded". The user daemon now correctly delegates to the in-process root daemon session instead of returning an error. docs: https://github.com/telepresenceio/telepresence/issues/4049 - type: bugfix title: Fix h2c (HTTP/2 cleartext) prior knowledge not working on transport body: >- Since Go 1.24, having both HTTP1 and UnencryptedHTTP2 enabled on an HTTP transport causes Go to default to HTTP/1.1 instead of using h2c prior knowledge. The transport now explicitly disables HTTP1 when UnencryptedHTTP2 is enabled, fixing h2c communication for both the default forwarding handler and intercepted traffic. docs: https://github.com/telepresenceio/telepresence/issues/4056 - version: 2.26.1 date: 2026-01-26 notes: - type: bugfix title: Add support for "warning" as an alias for "warn" in log levels body: >- The "warning" alias for "warn" was the only acceptable value in the Helm chart, yet the traffic manager didn't accept it. This is now fixed so that both names are accepted by the Helm chart and by the traffic manager. docs: https://github.com/telepresenceio/telepresence/issues/4043 - version: 2.26.0 date: 2026-01-23 notes: - type: feature title: Add ability for cluster admins to revoke other users' intercepts. body: |- The traffic-manager can now execute commands defined in the traffic-manager ConfigMap. As a result, clients that are authorized to update this ConfigMap can issue those commands. This mechanism lays the groundwork for distinguishing administrators from regular users in Telepresence. Administrators can be granted RBAC permissions that allow them to update the traffic-manager ConfigMap, while regular users cannot. The new command, `telepresence revoke `, uses this mechanism to revoke the intercept associated with the specified ID. docs: reference/engagements/conflicts - type: feature title: Add support for overriding intercepts owned by inactive clients body: >- Introduce the Helm chart setting `intercept.inactiveBlockTimeout` that controls the maximum amount of time an intercept may be held by a client that is unreachable or inactive. Once this timeout is exceeded, the intercept no longer blocks conflicting intercepts and may be automatically removed when another client attempts to create a conflicting intercept. docs: reference/engagements/conflicts - type: feature title: Add support for sudo-rs body: >- Telepresence now supports running the root daemon using `sudo-rs`. This is necessary on Ubuntu where `sudo-rs` has become the default for `sudo`. - type: feature title: Add configuration to disable global TCP/UDP intercepts body: >- A new Helm chart configuration option `intercept.allowGlobalIntercepts` has been added to control whether global TCP/UDP intercepts are permitted. When set to `false`, only HTTP intercepts with header or path filters are allowed, preventing users from creating global intercepts that block other developers from intercepting the same port. This is particularly useful in shared development environments where multiple developers need to work on the same service simultaneously. The setting defaults to `true` to maintain full backward compatibility with existing deployments. When a user attempts to create a global intercept while the setting is disabled, they receive a helpful error message suggesting the use of `--http-header` or `--http-path-*` flags for HTTP-filtered intercepts. docs: reference/cluster_config#restricting_global_intercepts - type: feature title: Enhanced Traffic Manager Startup Reliability body: |- The Traffic Manager deployment now includes a `startupProbe` that accurately detects when the service is fully initialized and ready to handle traffic. This enhancement brings the following benefits: - **Prevents Premature Traffic Routing**: The manager only reports itself as ready after all configurations are loaded, eliminating potential race conditions - **Smoother Upgrades and Rollouts**: Deployment orchestration tools can reliably determine when the Traffic Manager is operational, improving overall installation stability This change is particularly beneficial in large clusters or complex networking environments where initialization may take longer than expected. - type: feature title: Support customizable daemon config file body: >- The config file for Telepresence is now configurable through the command-line flag `--config`. The `--config ` flag of the `telepresence genyaml` command was renamed to `--agent` to avoid confusion. - type: feature title: Support customizable daemon log file paths body: >- Log file paths for the Telepresence daemons are now configurable through the command-line flag `--logfile` that denotes a custom log file location or redirect of the log output to stdout/stderr. Two new log-level configuration entries for `cli` and `kubeAuthDaemon` are also introduced, expanding the existing log-level controls beyond just `userDaemon` and `rootDaemon`. - type: feature title: Add ability to exclude or include modifications made by other injectors when injecting the traffic agent. body: >- The Traffic Agent now has a new configuration option `agentInjector.mutationAware` that can be set to `false` to exclude modifications made by other injectors when injecting the traffic agent. Setting `agentInjector.mutationAware=true` requires `agentInjector.webhook.reinvocationPolicy=IfNeeded`. The default setting is `true`. - type: feature title: Add ability to disable the Traffic Agent's HTTP2/Clear-Text probing. body: >- The Traffic Agent now has a new configuration option `agent.enableH2cProbing` that can be set to `false` to disable the HTTP2/Clear-Text probing. The default setting is `true` to preserve backwards compatibility, but this will be changed to `false` in a future release. - type: feature title: Add ability to configure the Traffic Agent's retry interval for watching intercepts. body: >- The Traffic Agent's retry interval when it establishes its watcher for intercepts is now configurable using the Helm chart value `agent.watchRetryInterval`. The default retry interval was also increased from 2 seconds to 10 seconds to improve resilience when connections to the traffic manager are lost. - type: feature title: Make traffic-agent consumption metrics reporting optional. body: >- Metrics reporting for the traffic-agent can now be optionally enabled or disabled via the `agent.enableConsumptionMetrics` Helm chart value. The traffic-agent metrics will also be completely disabled when the Helm chart value `prometheus.port` is unset or zero. This change is intended to reduce the amount of unnecessary traffic generated by the traffic-agent when consumption metrics reporting is of no interest to the user. - type: feature title: Improved efficiency of traffic manager map updates. body: >- The watchable map has been refactored into a client/server model that supports delta-based updates. Where supported, gRPC now transmits only incremental changes instead of full snapshots, significantly reducing payload sizes. This improvement is especially important for large clusters, where full snapshots can be sizable and costly to transmit. Full snapshot streaming remains available as a backward-compatible fallback for clients that do not support delta methods. - type: change title: Use TCP/IP instead of Unix sockets for all communication between local processes. body: >- Telepresence now uses TCP/IP sockets for all communication between local processes. This change reduces the risk of socket-related errors caused by lingering sockets from previous Telepresence sessions. It also eliminates the difficulties of using Unix sockets for communication between a system service and user processes on Windows. - type: change title: Don't allow connect with --docker when client is configured with intercept.useFtp=true body: |- The `--docker` flag is not allowed when the client is configured with `intercept.useFtp=true` and an error is now generated instantly by the `telepresence connect --docker` command. The docker volume plugin cannot use FTP because it requires two ports: a fixed control port that Telepresence can proxy, and a dynamic data port (randomly chosen during connection) that Telepresence cannot proxy on-demand. The port-forwarder only forwards pre-configured ports and doesn't understand FTP's protocol. Consequently, FTP isn't allowed in this scenario. If it was, then when an FTP server tells the client to use an unpredictable second port for file transfers, Telepresence would block it—causing the connection to fail every time. - type: change title: Better names for the Telepresence Daemons body: |- Using the name xxx-foreground isn't very intuitive when talking about daemon processes. Yes, the command, when issued in a terminal, will start the daemon in foreground so the names of the commands does have some logic to them, but then again, starting in the foreground is the default behavior of any command. And when the same command is started from the CLI, it will be started in the background, despite its name. The daemons are therefore now renamed: - connector-foreground => userd - daemon-foreground => rootd - kubeauth-foreground => kubeauthd This also affects the Telepresence hidden CLI commands `telepresence daemon-foreground` and `telepresence connector-foreground`. - type: bugfix title: Add retry logic for tunnel connection attempts body: >- The tunnel dialer now uses a backoff-based retry mechanism when establishing connections. This ensures that dialing attempts for both TCP and UDP protocols persist if the target IP is not immediately ready to receive requests. - type: bugfix title: Retry mechanism for client tunnel creation body: >- The traffic agent now includes a backoff-based retry mechanism when establishing tunnel streams to a client. This prevents "no dial watcher" connection failures caused by a race condition where the tunnel request arrives before the client has fully initialized its communication channel. - type: bugfix title: Fix "close of closed channel" panic in the root daemon process. body: >- The root daemon process would sometimes panic with "close of closed channel" due to a race condition in the DNS cache logic. This issue has been fixed. - version: 2.25.2 date: 2025-12-26 notes: - type: bugfix title: Ensure that the exit code from a docker command becomes the exit code of the Telepresence command. body: >- When running a Docker command using `telepresence docker-run` or `telepresence curl`, the exit code would be 1 for all non-zero exit codes from the Docker command. This has been fixed so that the exit code from the Docker command becomes the exit code of the Telepresence command. - type: bugfix title: Fix a bug causing truncation of command text when generating external command help. body: >- The Telepresence CLI would truncate the command text when generating help for external commands such as `docker compose` that had text spanning more than one line. This has been fixed so that the full command text is displayed. - type: bugfix title: Fix schema for agent.image.pullSecrets body: >- The `agent.image.pullSecrets` is referenced by the helm chart's deployment.yaml but was previously disallowed by the schema file. - version: 2.25.1 date: 2025-11-10 notes: - type: bugfix title: Volumes did not mount correctly when using `telepresence connect --docker` when Docker had IPv6 enabled. body: >- Telepresence failed to mount volumes after connecting with `telepresence connect --docker` when Docker Engine had IPv6 enabled in its default bridge network. Disabling IPv6 in the Telepresence client configuration did not resolve the issue. This was fixed in Telepresence Volume Plugin "telemount" version 0.3.2, which circumvented a [bug in sshfs](https://github.com/libfuse/sshfs/issues/335). Additionally, the volume plugin will no longer use IPv6 when the client configuration `docker.enableIPv6` is set to `false`. - type: bugfix title: Remove unnecessary setcap from traffic binary body: >- The setcap capability (cap_net_bind_service) was removed from the traffic binary build process. This capability was originally added to allow the binary to bind to privileged ports, specifically port 443 for the mutating webhook. Since version 2.24.0, the default mutating webhook port was changed to 8443 (a non-privileged port), making this capability unnecessary. Removing it simplifies the build process and reduces the security surface area. - version: 2.25.0 date: 2025-10-16 notes: - type: feature title: HTTP Intercepts with HTTP header and path filtering body: |- Telepresence now supports HTTP Intercepts, enabling fine-grained HTTP traffic filtering for intercepts. Users can intercept only specific HTTP requests based on headers and URL paths using the new `--http-header`, `--http-path-prefix`, `--http-path-equal`, and `--http-path-regex` flags. This allows multiple developers to work on the same service simultaneously by intercepting only their specific traffic patterns, rather than intercepting all traffic to a service. **Routing Precedence Model**: Header-based intercepts take priority over path-only intercepts. When multiple intercepts are active on the same workload, requests are evaluated against header-based filters first, then path-only filters. This enables different developers to use header-based personal intercepts (e.g., `x-user=alice`) while others use path-based intercepts (e.g., `/admin/*`) without conflicts. **Conflict Detection**: Intercepts conflict only when their filters would route the same traffic to different destinations. Key rules: - Different header values (e.g., `X-User=adam` vs `X-User=bertil`) do NOT conflict - Header filters use subset logic: `X-User=adam` conflicts with `X-User=adam, X-Session=123` (first is subset) - Same headers with different paths do NOT conflict: `X-User=adam + /api/*` vs `X-User=adam + /admin/*` - Path-only intercepts operate at a lower priority tier than header-based intercepts The feature maintains full backward compatibility with existing TCP intercepts. docs: howtos/engage#intercept-your-application - type: feature title: TLS/mTLS Intercept Support body: |- Support was added for HTTP-filtered intercepts on applications using TLS/mTLS encryption. The new functionality enables Telepresence to decrypt and inspect encrypted traffic by accessing TLS certificates, facilitating debugging and testing of secure applications. Certificates can be accessed via mounted volumes or Kubernetes secrets in the same namespace. New annotations (`telepresence.io/downstream-cert-path` and `telepresence.io/downstream-cert-secret`) allow configuration of certificate paths or secrets for decrypting traffic on specified ports. For mTLS, the `telepresence.io/upstream-cert-` annotation prefix supports re-encryption of upstream traffic using client-side certificates. For self-signed certificates, the `telepresence.io/upstream-insecure-skip-verify` annotation bypasses verification, enabling HTTP-filtered intercepts in development environments. The `--plaintext` option allows unencrypted traffic during intercepts or wiretaps. docs: howtos/mtls - type: feature title: Add MCP server to Telepresence CLI body: >- The Telepresence CLI now includes a lightweight MCP server that can be used to allow local AI agents to execute some CLI commands, such as connecting to a traffic manager, listing interceptable apps, and creating an intercept. The server can be enabled using the new `telepresence mcp claude enable` or `telepresence mcp vscode enable` commands. - type: feature title: Enhance Resilience of Engagements During Traffic-Manager Redeploys body: >- The telepresence client and traffic-agent now automatically reconnect to the traffic-manager after a restart. Upon reconnection, they share their current state, ensuring ongoing engagements remain uninterrupted. This improvement minimizes user impact during traffic-manager upgrades. - type: feature title: Add support for IPv6 and dual-stack when using `telepresence connect --docker` body: |- Telepresence now supports using `telepresence connect --docker` together with Kubernetes single-stack IPv4 networking, single-stack IPv6 networking, or dual-stack networking with both network families. Both are enabled by default, but can be disabled by setting the `client.docker.enableIPv4` or `client.docker.enableIPv6` to false in the Helm chart, or by using the corresponding settings `docker.enableIPv4` or `docker.enableIPv6` the client configuration file. The new dual-stack support requires the teleroute network plugin 0.4.0 or later. The client will install this version automatically unless you work in an air-gapped environment. - type: feature title: RESTful API Service Reintroduced with HTTP Filtering Support body: >- The Telepresence RESTful API service has been restored with enhanced support for HTTP header and path filtering. This service enables workloads to programmatically query whether they should handle requests based on active intercepts. Added `--metadata` flag allows attaching custom metadata to intercepts that can be retrieved through the API endpoints. The API server is now accessible via `TELEPRESENCE_API_HOST` and `TELEPRESENCE_API_PORT` environment variables in both cluster pods and local intercept handlers. docs: reference/restapi - type: feature title: More efficient DNS handling in the traffic-manager body: |- Telepresence will no longer send DNS queries for A and AAAA records to the traffic-manager. Instead, it will send a single query for the name and then derive the record type from the type of IP address (IPv4 or IPv6) in the response. This reduces the number of DNS queries sent to the cluster's DNS server and makes the behavior more consistent with the `net.LookupNetIP` function in the Go standard library, which the traffic-manager ultimately uses. The lookups will now be performed exclusively by the traffic-manager, never by the traffic-agent. This means that traffic-agents with special DNS configurations might stop working. If this is a problem, the old behavior can be restored by setting the `client.dns.useComplexLookup` parameter in the Helm chart or the `dns.useComplexLookup` parameter in the client configuration file. - type: feature title: Updated Helm chart to include keywords and the source repository URL. body: >- Improves the Helm chart's discoverability on platforms like Artifact Hub and automatically adds a direct link to the source code for users, providing better context. - type: change title: Telepresence client now requires a traffic manager version of at least 2.21.0. body: >- The traffic manager is now required to be at least version 2.21.0. Versions earlier than 2.21.0 will no longer work. The reason for this is that implementing the new reconnect behavior would require too much conditional code with older traffic-managers, and a lot of functionality wouldn't work anyway. - type: change title: Build binaries and docker images that are stripped from dwarf and debug info. body: >- The Telepresence binaries and docker images are now built with the `-w -s` flags, which strip the debug symbols and the DWARF information. This reduces the size of the binaries and docker images by about 50MB. Debug binaries can be built using `DEBUG=1 make build`. - version: 2.24.1 date: 2025-09-05 notes: - type: bugfix title: Fix invalid filename generated by telepresence gather-logs command body: >- The `telepresence gather-logs` command would generate a filename that was invalid on Windows unless the user specified the filename explicitly using the `--output-file` flag. This was fixed using a more condensed format for the timestamp in the filename. docs: https://github.com/telepresenceio/telepresence/issues/3956 - type: bugfix title: A `telepresence connect --docker` fails kubeconfig points to a port-forwarded localhost body: >- Telepresence would fail to connect to a cluster from within a container if the kubeconfig pointed to a port-forwarded localhost because that localhost is not reachable from within the container. This situation is now detected so that the address used from within the container has "localhost" replaced with "host.docker.internal", or an alternative alias configured by the user using the new `docker.hostGateway` parameter in the client configuration. - type: bugfix title: A `telepresence connect --docker` would fail with some k3s configurations body: >- Telepresence would fail to connect to a k3s cluster unless the cluster's IP was a loopback address. This is now changed so that any IP:port combination is accepted as long as a container can be found that defines a mapping for it. - type: bugfix title: Restore default value for agent-state.yaml in the traffic-manager configmap body: >- The value was previously an empty string which caused problems when when Argo CD tried to synchronize it. docs: https://github.com/telepresenceio/telepresence/pull/3953 - version: 2.24.0 date: 2025-08-25 notes: - type: feature title: Support for Docker Compose body: >- Telepresence now supports integration with Docker Compose. It connects to and interacts with cluster resources by utilizing `x-tele` extensions within a Docker Compose specification. These extensions configure your local services to effectively act as handlers for Telepresence connections, providing them with the necessary access to the traffic, volumes, and environment of the engaged container. docs: howtos/docker-compose - type: feature title: Serve up a web-page with telepresence serve. body: >- A new `telepresence serve ` command was added that starts a web browser on the specified service. The command is especially useful when used in combination with `telepresence connect --docker` because it will then expose the given service on a random port on localhost. docs: reference/cli/telepresence_serve - type: feature title: Add ability to optionally clean up sidecars that have been idle above a specified duration body: >- Added configuration parameter `agent.maxIdleTime` to the Helm Chart, to control how long a sidecar can be idle before it is cleaned up. The updating of latestEngagementTime is done every 1m in the Remain Call, which is now called every 1min, compared to every 5s in the past. The agent state (containing latestEngagementTime) is updated in memory, and lazily persisted to the traffic-manager configmap every 2min. Removal of the sidecar is also done in the Remain Call, if the sidecar has been idle for longer than the configured `agent.maxIdleTime`. - type: feature title: Add option to drop client label in prometheus metrics for GDPR compliance docs: https://github.com/telepresenceio/telepresence/issues/3491 body: >- The Helm Chart now has a `prometheus.dropClientLabel` option that can be set to true to drop the client label from the prometheus metrics. This is useful for GDPR compliance, as the client label contains personal data, which can be potentially problematic, i.e allowing the ability to track the working times of an individual. - type: feature title: Prefix metrics with "telepresence_" docs: https://github.com/telepresenceio/telepresence/issues/3920 body: Avoids metric conflicts and makes these more explicit to improve search in observability stacks. - type: feature title: CLI documentation in markdown format body: >- The Telepresence CLI is now capable of generating its own documentation in markdown format using the new `telepresence man-pages` command. The generated documentation is included under the heading "Telepresence CLI" in the the Telepresence reference documentation. docs: reference/cli/telepresence - type: feature title: Service Port Rerouting body: >- The telepresence connect command introduces a new `--reroute-remote ::[/{tcp|udp}]` flag, allowing users to remap service ports. This feature redirects requests sent to `:` to `:` within the Telepresence VIF. The flag can be repeated. - type: feature title: Local Port Rerouting body: >- The telepresence connect command introduces a new `--reroute-local ::[/{tcp|udp}]` flag, allowing users to redirect requests sent to ports on localhost to arbitrary service ports. This feature enables requests sent to `localhost:` to be redirected to ``. The flag can be repeated. - type: feature title: Add information about using Kubernetes auth plugins when using Telepresence CLI in a container body: >- Kubernetes, and hence the Telepresence CLI, must have access to auth plugins declared in the kubeconfig. A section was added to the documentation explaining how to achieve this when using the Telepresence CLI in a container. docs: reference/inside-container#kubernetes-auth-plugins - type: feature title: Add log directory to the output of `telepresence config view` body: >- The `telepresence config view` command now includes the path to the directory where the Telepresence logs are stored. - type: change title: The default port for the mutating webhook is now 8443. It used to be 443 body: >- Port numbers below 1000 are reserved for privileged processes and are often restricted by firewalls. Consequently, the default port for the mutating webhook was changed from 443 to 8443. You can override this default port using the agentInjector.webhook.port value in the Helm Chart. This change is particularly significant for clusters using Telepresence, where firewall rules limit the admission webhook's access to worker nodes, such as in an Amazon EKS cluster. - version: 2.23.6 date: 2025-07-23 notes: - type: bugfix title: Public DNS names aren't resolved from local docker application started by Telepresence body: >- A container running using `telepresence docker-run` or `telepresence --docker-run` was not able to resolve public DNS names such as "google.com". The problem is caused by an undocumented behavior in Docker's internal DNS-resolver when the `--dns` flag is used in conjunction with the teleroute network, where the DNS-server no longer finds public names even thought another bridge network is connected. The problem was solved by using a fallback resolver in the Telepresence DNS resolver. - version: 2.23.5 date: 2025-07-20 notes: - type: bugfix title: Let docker.Start pass on --interactive to docker start. body: >- An `-i` or `--interactive` flag given when the user runs a container with telepresence must be propagated to `docker start` to attach `stdin`. - version: 2.23.4 date: 2025-07-18 notes: - type: bugfix title: Never truncate meaningful output from a command body: >- The new human-friendly output using a progress reporter would sometimes truncate error output. This is no longer the case. Instead, all output will be wrapped. - type: bugfix title: Typo in client mount-policy "RemoteReadonly" should be "RemoteReadOnly" body: >- The Helm Chart correctly expects the remote read-only mount policy to be `RemoteReadOnly`, but the client expected it to be `RemoteReadonly` (without a leading capital letter in the word "only"). - type: bugfix title: DNS server does not respect semicolons as comments in resolv.conf files body: >- Telepresence does not work correctly if `/etc/resolv.conf` contains semicolons, which are valid comments as of [linux manpage](https://man7.org/linux/man-pages/man5/resolv.conf.5.html). docs: https://github.com/telepresenceio/telepresence/issues/3908 - type: bugfix title: Use NXDOMAIN instead SERVFAIL for DNS recursion errors (timeouts) body: >- A DNS for a single label name that fails in a minikube - or another type of local cluster - will sometimes result in a recursive lookup on the host. Without any type of recursion detection, this lookup will timeout waiting for itself. Previously, this resulted in a `SERVFAIL` from the cluster DNS, which triggered renewed lookup attempts that never stopped. This is now changed so that the same type of timeouts instead results in an `NXDOMAIN` error that doesn't trigger renewed attempts. Also, the recursion check now handles that the cluster's DNS adds suffixes from its search-path. docs: reference/config#recursioncheck - version: 2.23.3 date: 2025-07-07 notes: - type: bugfix title: Fix tunnel channel reuse in traffic-agent to prevent connection failures body: >- Previously, when a tunnel became active, the map maintained by the traffic-agent containing channel values for agent-to-client tunnels wasn't cleared. This resulted in closed channels remaining in the map. When port numbers were eventually reused, these stale entries would be discovered and their closed channels would cause immediate stream termination, leading to data loss. - type: bugfix title: The -p flags would have no effect in combination with --docker-run body: |- When using `telepresence --docker-run` with a `-p ` flag, the Docker driver silently ignored the port specification. This occurred because the `--network=` flag disabled both additional network directives and the default bridge network (which is normally used when no network is specified). This has been resolved by: 1. Adding the teleroute network after container creation instead of using a flag 2. Replacing the single `docker run` command with a sequence of: - `docker create` - Network addition - `docker start` - type: bugfix title: Requests lost when using wiretap body: >- Wiretap connection `Close` and `Write` could sometimes be out of sync, so that the `Close` would be executed before the `Write`, causing a "read/write on closed pipe" error and loss of data. - version: 2.23.2 date: 2025-06-27 notes: - type: bugfix title: Adding an alsoProxy subnet with 32-bit mask no longer works on macOS body: >- Routing improvements introduced in 2.23.0 surfaced a problem when using special handling of submets with 32-bit masks. The special handling now causes problems on macOS. The logic is now conditioned to only run under linux since it's no longer needed on other operating systems. - type: bugfix title: The gather-logs command produces no cluster-side logs when connected with --docker body: >- The `telepresence gather-logs` command did not include cluster-side logs when connected using `--docker`, because it tried to store such logs in a temporary directory created by the CLI. The directory was not mounted in the daemon container. This is now changed so that the temporary directory is created under the users cache directory, which is guaranteed to be mounted on the container. - type: bugfix title: Docker volume mounts failing when connected using both --docker --proxy-via flags body: >- The volume mounter would fail when doing a `telepresence connect --docker --proxy-via all=` followed by an intercept using `--docker-run`, because the bridge that the daemon created would try to access the intercepted pod using its proxied IP. Now, the bridge will instead use the pod's real IP. - version: 2.23.1 date: 2025-06-24 notes: - type: feature title: New telepresence helm version command. body: >- The new `telepresence helm version` command prints the version of the helm client that is embedded in the telepresence binary. - type: bugfix title: Engagement disconnects after certain amount of time body: |- The configuration parameter `connectionTTL`, controlling how long a client could be completely idle before the traffic-manager or traffic-agent would consider it dead and disconnect (default 24 hours), had no effect. Instead, an engagement would disconnect after 2 hours (the default gRPC `keepAlive.Time` duration). The default of 24 hours is now reinstated. The Helm value `client.connectionTTL` was moved to `grpc.connectionTTL` because it is a server configuration. The old value will still work, but it is deprecated and will be removed eventually. docs: https://github.com/telepresenceio/telepresence/issues/3861 - type: bugfix title: Telepresence breaks if config.yml exists but is empty body: >- Telepresence would refuse to connect with a misleading error if the `config.yml` file containing the client's configuration parameters existed but was empty. docs: https://github.com/telepresenceio/telepresence/issues/3887 - version: 2.23.0 date: 2025-06-17 notes: - type: feature title: New telepresence wiretap command body: >- The new `telepresence wiretap` command introduces a read-only form of an `intercept` where the original container will run unaffected while a copy of the wiretapped traffic is sent to the client. Similar to an `ingest`, a `wiretap` will always enforce read-only status on all volume mounts, and since that makes the `wiretap` completely read-only, there's no limit to how many simultaneous wiretaps that can be served. In fact, a `wiretap` and an `intercept` on the same port can run simultaneously. - type: feature title: Add Telepresence Docker Network Plugin "Teleroute" body: |- The new Teleroute plugin makes it possible for containers to use the Telepresence daemon's VIF without having to change their network mode, i.e. a `--network container:` is no longer needed. Instead, a container can use a custom network created when the Telepresence daemon connects to the cluster. This network uses the new driver "teleroute" which is provided by Telepresence. With the Teleroute Docker network plugin in place, there's no longer a need for special handling of network related docker flags, and the following changes have been made: 1. The Teleroute Docker network driver will be installed unless it is already present. 2. A Teleroute network will be created when starting the Telepresence daemon as a container. This network will then communicate with that container and expose the same CIDRs as the daemon's VIF. 3. A container started with `telepresence curl`, or `telepresence {ingest|intercept|replace|wiretap} --docker-{run|build|debug}` will no longer change its network mode using `--network container:`, instead it will use `--network `. 4. As a consequence of #3, published ports and other networks that are added no longer need special handling using socat containers, so all of that has been removed. docs: reference/teleroute - type: feature title: Control whether the initContainer injection is enabled/disabled body: >- The initContainer injection can be optionally disabled by setting the `agent.initContainer.enabled` parameter to false in the `values.yaml` file of the Helm chart. This feature was added to improve compatibility with systems like OpenShift where the initContainer injection cannot be used due to inability to give initContainer NET_ADMIN permissions. - type: feature title: Human friendly progress reporting body: >- Telepresence now uses a progress reporter that is very similar to the one used by Docker compose. The implementation is a variation of that reporter's source code, so big thanks to the Docker compose CLI authors for making it available as OSS. A new global `--progress ` flag was added. It defaults to "auto" which means that the style is chosen depending on whether the command runs from a tty type terminal. Other possible values are "plain", "quiet", and "json". `--progress quiet` is implied when formatted output is chosen using `--output json|yaml`. - type: feature title: Add the ability to use a name for the target host, and defer its resolution body: >- Knowing the IP of the local service that acts as the handler service for an intercept, replace, or wiretap is not possible until that service has been started, and telepresence will therefore now accept a name for the `--address` flag. The name is not resolved by the daemon until a request is made to the engaged container on a port that is routed to the local service. - type: feature title: Add intercept.mountsRoot to the client configuration body: >- The new `intercept.mountsRoot` can be set to a directory that will be used as the root for all automatically generated mount directories. The default is to use the platforms temp directory. The setting is not used on windows, where the mounts use drive letters. - type: feature title: Add docker.addHostGateway to the client configuration. body: >- When `docker.addHostGateway` is set to `true`, the `docker run` that starts the containerized Telepresence daemon will include the flag `--add-host host.docker.internal:host-gateway`. The flag is set to `true` by default on linux platforms and `false` on other platforms. - type: feature title: Client configuration to override the Helm download URL body: >- The default download URL `oci://ghcr.io/telepresenceio/telepresence-oss` used when installing Helm charts with versions that differ from the version of the embedded Helm chart can now be overridden using the client config value `helm.chartURL`. - type: change title: Dropped support for Telepresence legacy flags body: |- The `telepresence` CLI command will no longer support legacy flags such as: - `--swap-deployment` - `--new-deployment` - `--docker-mount` - `--method` A "Legacy Telepresence command used" warning has been printed for several years now, and the mapping for the `--swap-deployment` was the `intercept` command, which is very confusing today since we now have the `replace` command. - type: bugfix title: Let containerized daemon consistently use the same port for gRPC body: >- The port used for the containerized gRPC was randomly selected using the hosts network namespace. This is now changed so that the port used by the container is preset and configurable and then mapped to a random port on the host. The port number can be configured using `grpc.daemonPort` and defaults to `4038`. - type: bugfix title: Telepresence fails to start the root daemon on Windows unless current user is the administrator body: The telepresence CLI starts a user daemon and a root daemon. The latter is started using administrator privileges. On a Windows box, this means that the root daemon runs using a different user account (typically "Administrator") unless the current user can run processes with elevated privileges. The socket used for communication with the root daemon was assumed to reside in `%USERPROFILE%\AppData\Local\telepresence` and was therefore not found by the CLI and the user daemon. The location will henceforth always be based on the `%USERDATA` of the CLI user. docs: https://github.com/telepresenceio/telepresence/issues/3875 - type: bugfix title: Telepresence DNS Fallback stripping CNAME information from DNS Records. body: >- The fallback DNS server used on Linux systems without a systemd.resolved configuration, would assume that suffixes belonging to the `search` defined in the `/etc/resolved` had been added by the caller. Since this search path was assumed to be intended for the local machine only, the suffix was stripped off prior to sending the name to the cluster for resolution. This made queries fail that relied on the qualified name to resolve CNAME records. The logic stripping the suffix was therefore removed. docs: https://github.com/telepresenceio/telepresence/issues/3873 - version: 2.22.6 date: 2025-06-03 notes: - type: bugfix title: Regression causing "unexpected slice size" with older traffic-managers. body: >- Older traffic-managers have a different way of reporting the service-subnet. The new way, using a list of subnets reused a proto slice in the GRPC message that was expected to be empty, but older traffic-managers will pass the IP of the kube-dns here. It cannot be parsed as a list of subnets. A check that remedies this mismatch was inserted. - version: 2.22.5 date: 2025-05-29 notes: - type: bugfix title: Unable to correctly determine service CIDR with Kubernetes >= 1.33 body: >- Starting with Kubernetes 1.33, the strategy of extracting the cluster's service CIDR from an error message no longer works because the error message has changed. The root cause for this is that Kubernetes introduced the ability to use [Multiple Service CIDRs](https://gist.github.com/aojea/c20eb117bf1c1214f8bba26c495be9c7). Since `ServiceCIDR` is now a resource, it can be easily retrieved (and modified) using standard Kubernetes client API calls, and this is what the traffic manager will use going forward. The fix required an addition to the traffic-manager's RBAC, granting it sufficient permissions to list `networking.k8s.io/servicecidrs`. A future enhancement will allow the traffic manager to watch for service CIDR changes. - type: bugfix title: Helm chart schema type for nodeSelector was incorrect body: >- The Helm chart schema for the `nodeSelector` value was incorrect. Kubernetes defines different types for nodeSelector (inside PodSpec objects) and NodeSelector (inside NodeAffinity, VolumeNodeAffinity and a bunch of other places). The schema was changed to use the correct type. The name `nodeSelector` is still used in the Helm chart so this change is backwards compatible. - type: bugfix title: Pods with container ports named the same caused intercept to fail body: >- Intercept container ports now have numbers appended to them if there are multiple ports from multiple containers with the same name. This bugfix works around an issue where Kubernetes allows multiple port definitions in a pod spec to have the same name. - type: bugfix title: Don't include k8s-defs.json to chart package body: >- The k8s-defs.json was unnecessarily included to the Helm chart package and this increased the Helm release secret size so much that it could prevent installation of the Helm chart depending on k8s settings. To fix this k8s-defs.json is not included to the Helm chart anymore. - version: 2.22.4 date: 2025-04-26 notes: - type: bugfix title: Don't require internet access when installing the traffic-manager using Helm body: >- A regression occurred with the introduction of Helm validation in version 2.22.0. The schema relied on external HTTPS links, which inadvertently created a requirement for internet accessibility. To resolve this, we have embedded these resources within the schema, thus removing the need for an internet connection. - type: bugfix title: Client failed connect with "failed to exit idle mode" in the connector.log after being idle body: >- The port-forward connections used for connecting the daemon to the traffic-agents were using an incorrect context, causing them to fail after being idle for some time. - type: bugfix title: Fix deadlock in Telepresence daemon body: >- A deadlock would sometimes occur in the Telepresence daemon that prevented it from doing a clean exit during `telepresence quit`. - type: bugfix title: Don't log error message when a pod watcher ends due to cancellation body: >- Errors printed in the daemon log during normal cancellation of the WatchAgentPods goroutine are now removed. - version: 2.22.3 date: 2025-04-08 notes: - type: change title: The Windows install script will now install Telepresence to "%ProgramFiles%\telepresence" body: |- Telepresence is now installed into "%ProgramFiles%\telepresence" instead of "C:\telepresence". The directory and the Path entry for `C:\telepresence` are not longer used and should be removed. - type: bugfix title: The Windows install script didn't handle upgrades properly body: |- The following changes were made: - The script now requires administrator privileges - The Path environment is only updated when there's a need for it docs: https://github.com/telepresenceio/telepresence/issues/3827 - type: bugfix title: The Telepresence Helm chart could not be used as a dependency in another chart. body: >- The JSON schema validation implemented in Telepresence 2.22.0 had a defect: it rejected the `global` object. This object, a Helm-managed construct, facilitates the propagation of arbitrary configurations from a parent chart to its dependencies. Consequently, charts intended for dependency use must permit the presence of the `global` object. docs: https://github.com/telepresenceio/telepresence/issues/3833 - type: bugfix title: Recreating namespaces was not possible when using a dynamically namespaced Traffic Manager body: >- A shared informer was sometimes reused when namespaces were removed and then later added again, leading to errors like "handler ... was not added to shared informer because it has stopped already". docs: https://github.com/telepresenceio/telepresence/issues/3831 - type: bugfix title: Single label name DNS lookups didn't work unless at least one traffic-agent was installed body: >- A problem with incorrect handling of single label names in the traffic-manager's DNS resolver was fixed. The problem would cause lookups like `curl echo` to fail, even though telepresence was connected to a namespace containing an "echo" service, unless at least one of the workloads in the connected namespace had a traffic-agent. - version: 2.22.2 date: 2025-03-28 notes: - type: bugfix title: Panic when using telepresence replace in a IPv6-only cluster body: |- A "slice bounds out of range" would occur when the targeted Pod's Traffic Agent requested a local dialer to be created on the client. This was due to a glitch in the VPN-tunnel implementation that got triggered when a remote IPv6-address was combined with a local IPv4-address. docs: https://github.com/telepresenceio/telepresence/issues/3828 - version: 2.22.1 date: 2025-03-27 notes: - type: bugfix title: Only restore inactive traffic-agent after a replace. body: |- A regression in the 2.20.0 release would cause the traffic-agent to be replaced with a dormant version that didn't touch any ports when an intercept ended. This terminated other ongoing intercepts on the same pod. This is now changed so that the traffic-agent remains unaffected for this use-case. - version: 2.22.0 date: 2025-03-14 notes: - type: feature title: New telepresence replace command. body: |- The new `telepresence replace` command simplifies and clarifies container replacement. Previously, the `--replace` flag within the `telepresence intercept` command was used to replace containers. However, this approach introduced inconsistencies and limitations: * **Confusion:** Using a flag to modify the core function of a command designed for traffic interception led to ambiguity. * **Inaccurate Behavior:** Replacement was not possible when no incoming traffic was intercepted, as the command's design focused on traffic routing. To address these issues, the `--replace` flag within `telepresence intercept` has been deprecated. The new `telepresence replace` command provides a dedicated and consistent method for replacing containers, enhancing clarity and reliability. Key differences between `replace` and `intercept`: 1. **Scope:** The `replace` command targets and affects an entire container, impacting all its traffic, while an `intercept` targets specific services and/or service/container ports. 2. **Port Declarations:** Remote ports specified using the `--port` flag are container ports. 3. **No Default Port:** A `replace` can occur without intercepting any ports. 4. **Container State:** During a `replace`, the original container is no longer active within the cluster. The deprecated `--replace` flag still works, but is hidden from the `telepresence intercept` command help, and will print a deprecation warning when used. - type: feature title: Add json-schema for the Telepresence Helm Chart body: >- Helm can validate a chart using a json-schema using the command `helm lint`, and this schema can be part of the actual Helm chart. The telepresence-oss Helm chart now includes such a schema, and a new `telepresence helm lint` command was added so that linting can be performed using the embedded chart. - type: feature title: No dormant container present during replace. body: |- Telepresence will no longer inject a dormant container during a `telepresence replace` operation. Instead, the Traffic Agent now directly serves as the replacement container, eliminating the need to forward traffic to the original application container. This simplification offers several advantages when using the `--replace` flag: - **Removal of the init-container:** The need for a separate init-container is no longer necessary. - **Elimination of port renames:** Port renames within the intercepted pod are no longer required. - type: feature title: One single invocation of the Telepresence intercept command can now intercept multiple ports. body: >- It is now possible to intercept multiple ports with one single invocation of `telepresence intercept` by just repeating the `--port` flag. - type: feature title: Unify how Traffic Manager selects namespaces body: |- The definition of what namespaces that a Traffic Manager would manage use was scattered into several Helm chart values, such as `manager.Rbac.namespaces`, `client.Rbac.namespaces`, and `agentInjector.webhook.namespaceSelector`. The definition is now unified to the mutual exclusive top-level Helm chart values `namespaces` and `namespaceSelector`. The `namespaces` value is just for convenience and a short form of expressing: ```yaml namespaceSelector: matchExpressions: - key: kubernetes.io/metadata.name operator: in values: . ``` docs: install/manager#static-versus-dynamic-namespace-selection - type: feature title: Improved control over how remote volumes are mounted using mount policies body: >- Mount policies, that affects how the telepresence traffic-agent shares the pod's volumes, and also how the client will mount them, can now be provided using the Helm chart value `agent.mountPolicies` or as JSON object in the workload annotation `telepresence.io/mount-policies`. A mount policy is applied to a volume or to all paths matching a path-prefix (distinguished by checking if first character is a '/'), and can be one of `Ignore`, `Local`, `Remote`, or `RemoteReadOnly`. - type: feature title: List output includes workload kind. body: >- The output of the `telepresence list` command will now include the workload kind (deployment, replicaset, statefulset, or rollout) in all entries. - type: feature title: Add ability to override the default securityContext for the Telepresence init-container body: >- Users can now use the Helm value `agent.initSecurityContext` to override the default securityContext for the Telepresence init-container. - type: change title: Let download page use direct links to GitHub body: >- The download links on the release page now points directly to the assets on the download page, instead of using being routed from getambassador.io/download/tel2oss/releases. - type: change title: Use telepresence.io as annotation prefix instead of telepresence.getambassador.io body: >- The workload and pod annotations used by Telepresence will now use the prefix `telepresence.io` instead of `telepresence.getambassador.io`. The new prefix is consistent with the prefix used by labels, and it also matches the host name of the documentation site. Annotations using the old name will still work, but warnings will be logged when they are encountered. - type: change title: Make the DNS recursion check configurable and turn it off by default. body: >- Very few systems experience a DNS recursion lookup problem. It can only occur when the cluster runs locally and the cluster's DNS is configured to somehow use DNS server that is started by Telepresence. The check is therefore now configurable through the client setting `dns.recursionCheck`, and it is `false` by default. - type: change title: Trigger the mutating webhook with Kubernetes eviction objects instead of patching workloads. body: >- Telepresence will now attempt to evict pods in order to trigger the traffic-agent's injection or removal, and revert to patching workloads if evictions are prevented by the pod's disruption budget. This causes a slight change in the traffic-manager RBAC, as the traffic-manager must be able to create "pod/eviction" objects. - type: change title: The telepresence-agents configmap is no longer used. body: >- The traffic-agent configuration was moved into a pod-annotation. This avoids sync problems between the telepresence-agents (which is no no longer present) and the pods. - type: change title: Drop deprecated current-cluster-id command. body: >- The clusterID was deprecated some time ago, and replaced by the ID of the namespace where the traffic-manager is installed. - type: bugfix title: Make telepresence connect --docker work with Rancher Desktop body: >- Rancher Desktop will start a K3s control-plane and typically expose the Kubernetes API server at `127.0.0.1:6443`. Telepresence can connect to this cluster when running on the host, but the address is not available when connecting in docker mode. The problem is solved by ensuring that the Kubernetes API server address used when doing a `telepresence connect --docker` is swapped from 127.0.0.1 to the internal address of the control-plane node. This works because that address is available to other docker containers, and the Kubernetes API server is configured with a certificate that accepts it. - type: bugfix title: Rename charts/telepresence to charts/telepresence-oss. body: >- The Helm chart name "telepresence-oss" was inconsistent with its contained folder "telepresence". As a result, attempts to install the chart using an argo ApplicationSet failed. The contained folder was renamed to match the chart name. - type: bugfix title: Conflict detection between namespaced and cluster-wide install. body: >- The namespace conflict detection mechanism would only discover conflicts between two _namespaced_ Traffic Managers trying to manage the same namespace. This is now fixed so that all types conflicts are discovered. docs: install/manager#namespace-collision-detection - type: bugfix title: Don't dispatch DNS discovery queries to the cluster. body: >- macOS based systems will often PTR queries using nameslike `b._dns-sd._udp`, lb._dns-sd._udp`, or `db-dns-sd._udp`. Those queries are no longer dispatched to the cluster. - type: bugfix title: Using the --namespace option with telepresence causes a deadlock. body: >- Using `telepresence list --namespace ` with a namespace different from the one that telepresence was connected to, would cause a deadlock, and then produce an empty list. - type: bugfix title: Fix problem with exclude-suffix being hidden by DNS search path. body: >- In some situations, a name ending with an exclude-suffix like "xyz.com" would be expanded by a search path into "xyz.com.<connected namespace>" and therefore not be excluded. Instead, the name was sent to the cluster to be resolved, causing an unnecessary load on its DNS server. - version: 2.21.3 date: 2025-02-06 notes: - type: bugfix title: Using the --proxy-via flag would sometimes cause connection timeouts. body: >- Typically, a `telepresence connect --proxy-via =` would fail with a "deadline exceeded" message when several workloads were present in the namespace, the one targeted by the proxy-via didn't yet have an agent installed, and other workloads had an agent. This was due to a race condition in the logic for the agent-based port-forwards in the root daemon. The conditions causing this race are now eliminated. - type: bugfix title: Fix panic in root daemon when using the "allow conflicting subnets" feature on macOS. body: >- A regression was introduced in version 2.21.0, causing a panic due to an unimplemented method in the TUN-device on macOS based clients. - type: bugfix title: Ensure that annotation enabled traffic-agents are uninstall when uninstalling the traffic-manager. body: >- A traffic-agent injected because the workload had the inject annotation enabled would sometimes not get uninstalled when the traffic-manager was uninstalled. - version: 2.21.2 date: 2025-01-26 notes: - type: bugfix title: Fix panic when agentpf.client creates a Tunnel body: >- A race could occur where several requests where made to `agentpf.client.Tunnel` on a client that had errored when creating its port-forward to the agent. The implementation could handle one such requests but not several, resulting in a panic in situations where multiple simultaneous requests were made to the same client during a very short time period, - type: bugfix title: Fix goroutine leak in dialer. body: >- The context passed to the `Tunnel` call that creates a stream for a dialer, was not cancelled when the dialer was finished, so the stream was never properly closed, leading to one dormant goroutine for each stream. - version: 2.21.1 date: 2024-12-17 notes: - type: bugfix title: Allow ingest of serverless deployments without specifying an inject-container-ports annotation body: >- The ability to intercept a workload without a service is built around the `telepresence.getambassador.io/inject-container-ports` annotation, and it was also required in order to ingest such a workload. This was counterintuitive and the requirement was removed. An ingest doesn't use a port. docs: https://github.com/telepresenceio/telepresence/issues/3741 - type: bugfix title: Upgrade module dependencies to get rid of critical vulnerability. body: >- Upgrade module dependencies to latest available stable. This includes upgrading golang.org/x/crypto, which had critical issues, from 0.30.0 to 0.31.0 where those issues are resolved. - version: 2.21.0 date: 2024-12-13 notes: - type: feature title: Automatic VPN conflict avoidance body: >- Telepresence not only detects subnet conflicts between the cluster and workstation VPNs but also resolves them by performing network address translation to move conflicting subnets out of the way. docs: reference/vpn - type: feature title: Virtual Address Translation (VNAT). body: >- It is now possible to use a virtual subnet without routing the affected IPs to a specific workload. A new `telepresence connect --vnat CIDR` flag was added that will perform virtual network address translation of cluster IPs. This flag is very similar to the `--proxy-via CIDR=WORKLOAD` introduced in 2.19, but without the need to specify a workload. docs: reference/vpn - type: feature title: Intercepts targeting a specific container body: >- In certain scenarios, the container owning the intercepted port differs from the container the intercept targets. This port owner's sole purpose is to route traffic from the service to the intended container, often using a direct localhost connection. This update introduces a `--container ` option to the intercept command. While this option doesn't influence the port selection, it guarantees that the environment variables and mounts propagated to the client originate from the specified container. Additionally, if the `--replace` option is used, it ensures that this container is replaced. docs: reference/engagements/container - type: feature title: New telepresence ingest command body: >- The new `telepresence ingest` command, similar to `telepresence intercept`, provides local access to the volume mounts and environment variables of a targeted container. However, unlike `telepresence intercept`, `telepresence ingest` does not redirect traffic to the container and ensures that the mounted volumes are read-only. An ingest requires a traffic-agent to be installed in the pods of the targeted workload. Beyond that, it's a client-side operation. This allows developers to have multiple simultaneous ingests on the same container. docs: howtos/intercepts#ingest-your-service - type: feature title: New telepresence curl command body: >- The new `telepresence curl` command runs curl from within a container. The command requires that a connection has been established using `telepresence connect --docker`, and the container that runs `curl` will share the same network as the containerized telepresence daemon. docs: reference/docker-run#the-telepresence-curl-command - type: feature title: New telepresence docker-run command body: >- The new `telepresence docker-run ` requires that a connection has been established using `telepresence connect --docker` It will perform a `docker run ` and add the flag necessary to ensure that started container shares the same network as the containerized telepresence daemon. docs: reference/docker-run#the-telepresence-docker-run-command - type: feature title: Mount everything read-only during intercept body: >- It is now possible to append ":ro" to the intercept `--mount` flag value. This ensures that all remote volumes that the intercept mounts are read-only. - type: feature title: Unify client configuration body: >- Previously, client configuration was divided between the config.yml file and a Kubernetes extension. DNS and routing settings were initially found only in the extension. However, the Helm client structure allowed entries from both. To simplify this, we've now aligned the config.yml and Kubernetes extension with the Helm client structure. This means DNS and routing settings are now included in both. The Kubernetes extension takes precedence over the config.yml and Helm client object. While the old-style Kubernetes extension is still supported for compatibility, it cannot be used with the new style. docs: reference/config - type: feature title: Use WebSockets for port-forward instead of the now deprecated SPDY. body: >- Telepresence will now use WebSockets instead of SPDY when creating port-forwards to the Kubernetes Cluster, and will fall back to SPDY when connecting to clusters that don't support SPDY. Use of the deprecated SPDY can be forced by setting `cluster.forceSPDY=true` in the `config.yml`. See [Streaming Transitions from SPDY to WebSockets](https://kubernetes.io/blog/2024/08/20/websockets-transition/) for more information about this transition. - type: feature title: Make usage data collection configurable using an extension point, and default to no-ops body: >- The OSS code-base will no longer report usage data to the proprietary collector at Ambassador Labs. The actual calls to the collector remain, but will be no-ops unless a proper collector client is installed using an extension point. - type: feature title: Add deployments, statefulSets, replicaSets to workloads Helm chart value body: >- The Helm chart value `workloads` now supports the kinds `deployments.enabled`, `statefulSets.enabled`, `replicaSets.enabled`. and `rollouts.enabled`. All except `rollouts` are enabled by default. The traffic-manager will ignore workloads, and Telepresence will not be able to intercept them, if the `enabled` of the corresponding kind is set to `false`. docs: reference/engagements/sidecar#disable-workloads - type: feature title: Improved command auto-completion body: >- The auto-completion of namespaces, services, and containers have been added where appropriate, and the default file auto completion has been removed from most commands. - type: feature title: Docker run flags --publish, --expose, and --network now work with docker mode connections body: >- After establishing a connection to a cluster using `telepresence connect --docker`, you can run new containers that share the same network as the containerized daemon that maintains the connection. This enables seamless communication between your local development environment and the remote services. Normally, Docker has a limitation that prevents combining a shared network configuration with custom networks and exposing ports. However, Telepresence now elegantly circumvents this limitation so that a container started with `telepresence docker-run`, `telepresence intercept --docker-run`, or `telepresence ingest --docker-run` can use flags like `--network`, `--publish`, or `--expose`. To achieve this, Telepresence temporarily adds the necessary network to the containerized daemon. This allows the new container to join the same network. Additionally, Telepresence starts extra socat containers to handle port mapping, ensuring that the desired ports are exposed to the local environment. docs: reference/docker-run#the-telepresence-docker-run-command - type: feature title: Prevent recursion in the Telepresence Virtual Network Interface (VIF) body: >- Network problems may arise when running Kubernetes locally (e.g., Docker Desktop, Kind, Minikube, k3s), because the VIF on the host is also accessible from the cluster's nodes. A request that isn't handled by a cluster resource might be routed back into the VIF and cause a recursion. These recursions can now be prevented by setting the client configuration property `routing.recursionBlockDuration` so that new connection attempts are temporarily blocked for a specific IP:PORT pair immediately after an initial attempt, thereby effectively ending the recursion. docs: howtos/cluster-in-vm - type: feature title: Allow Helm chart to be included as a sub-chart body: >- The Helm chart previously had the unnecessary restriction that the .Release.Name under which telepresence is installed is literally called "traffic-manager". This restriction was preventing telepresence from being included as a sub-chart in a parent chart called anything but "traffic-manager". This restriction has been lifted. - type: feature title: Add Windows arm64 client build body: >- Telepresence client is now available for Windows ARM64. Updated the release workflow files in github actions to build and publish the Windows ARM64 client. - type: change title: The --agents flag to telepresence uninstall is now the default. body: >- The `telepresence uninstall` was once capable of uninstalling the traffic-manager as well as traffic-agents. This behavior has been deprecated for some time now and in this release, the command is all about uninstalling the agents. Therefore the `--agents` flag was made redundant and whatever arguments that are given to the command must be name of workloads that have an agent installed unless the `--all-agents` is used, in which case no arguments are allowed. - type: change title: Performance improvement for the telepresence list command body: >- The `telepresence list` command will now retrieve its data from the traffic-manager, which significantly improves its performance when used on namespaces that have a lot of workloads. - type: change title: During an intercept, the local port defaults to the targeted port of the intercepted container instead of 8080. body: >- Telepresence mimics the environment of a target container during an intercept, so it's only natural that the default for the local port is determined by the targeted container port rather than just defaulting to 8080. A default can still be explicitly defined using the `config.intercept.defaultPort` setting. - type: change title: Move the telepresence-intercept-env configmap data into traffic-manager configmap. body: >- There's no need for two configmaps that store configuration data for the traffic manager. The traffic-manager configmap is also watched, so consolidating the configuration there saves some k8s API calls. - type: change title: Tracing was removed. body: >- The ability to collect trace has been removed along with the `telepresence gather-traces` and `telepresence upload-traces` commands. The underlying code was complex and has not been well maintained since its inception in 2022. We have received no feedback on it and seen no indication that it has ever been used. - type: bugfix title: Remove obsolete code checking the Docker Bridge for DNS body: >- The DNS resolver checked the Docker bridge for messages on Linux. This code was obsolete and caused problems when running in Codespaces. - type: bugfix title: Fix telepresence connect confusion caused by /.dockerenv file body: >- A `/.dockerenv` will be present when running in a GitHub Codespaces environment. That doesn't mean that telepresence cannot use docker, or that the root daemon shouldn't start. - type: bugfix title: Cap timeouts.connectivityCheck at 5 seconds. body: >- The timeout value of `timeouts.connectivityCheck` is used when checking if a cluster is already reachable without Telepresence setting up an additional network route. If it is, this timeout should be high enough to cover the delay when establishing a connection. If this delay is higher than a second, then chances are very low that the cluster already is reachable, and if it can, that all accesses to it will be very slow. In such cases, Telepresence will create its own network interface and do perform its own tunneling. The default timeout for the check remains at 500 millisecond, which is more than sufficient for the majority of cases. - type: bugfix title: Prevent that traffic-manager injects a traffic-agent into itself. body: >- The traffic-manager can never be a subject for an intercept, ingest, or proxy-via, because that means that it injects the traffic-agent into itself, and it is not designed to do that. A user attempting this will now see a meaningful error message. - type: bugfix title: Don't include pods in the kube-system namespace when computing pod-subnets from pod IPs body: >- A user would normally never access pods in the `kube-system` namespace directly, and automatically including pods included there when computing the subnets will often lead to problems when running the cluster locally. This namespace is therefore now excluded in situations when the pod subnets are computed from the IPs of pods. Services in this namespace will still be available through the service subnet. If a user should require the pod-subnet to be mapped, it can be added to the `client.routing.alsoProxy` list in the helm chart. - type: bugfix title: Let routes belonging to an allowed conflict be added as a static route on Linux. body: >- The `allowConflicting` setting didn't always work on Linux because the conflicting subnet was just added as a link to the TUN device, and therefore didn't get subjected to routing rule used to assign priority to the given subnet. - version: 2.20.3 date: 2024-11-18 notes: - type: bugfix title: Ensure that Telepresence works with GitHub Codespaces body: >- GitHub Codespaces runs in a container, but not as root. Telepresence didn't handle this situation correctly and only started the user daemon. The root daemon was never started. docs: https://github.com/telepresenceio/telepresence/issues/3722 - type: bugfix title: Mounts not working correctly when connected with --proxy-via body: >- A mount would try to connect to the sftp/ftp server using the original (cluster side) IP although that IP was translated into a virtual IP when using `--proxy-via`. docs: https://github.com/telepresenceio/telepresence/issues/3715 - version: 2.20.2 date: 2024-10-21 notes: - type: bugfix title: Crash in traffic-manager configured with agentInjector.enabled=false body: >- A traffic-manager that was installed with the Helm value `agentInjector.enabled=false` crashed when a client used the commands `telepresence version` or `telepresence status`. Those commands would call a method on the traffic-manager that panicked if no traffic-agent was present. This method will now instead return the standard `Unavailable` error code, which is expected by the caller. - version: 2.20.1 date: 2024-10-10 notes: - type: bugfix title: Some workloads missing in the telepresence list output (typically replicasets owned by rollouts). body: >- Version 2.20.0 introduced a regression in the `telepresence list` command, resulting in the omission of all workloads that were owned by another workload. The correct behavior is to just omit those workloads that are owned by the supported workload kinds `Deployment`, `ReplicaSet`, `StatefulSet`, and `Rollout`. Furthermore, the `Rollout` kind must only be considered supported when the Argo Rollouts feature is enabled in the traffic-manager. - type: bugfix title: Allow comma separated list of daemons for the gather-logs command. body: >- The name of the `telepresence gather-logs` flag `--daemons` suggests that the argument can contain more than one daemon, but prior to this fix, it couldn't. It is now possible to use a comma separated list, e.g. `telepresence gather-logs --daemons root,user`. - version: 2.20.0 date: 2024-10-03 notes: - type: feature title: Add timestamp to telepresence_logs.zip filename. body: >- Telepresence is now capable of easily find telepresence gather-logs by certain timestamp. - type: feature title: Enable intercepts of workloads that have no service. body: >- Telepresence is now capable of intercepting workloads that have no associated service. The intercept will then target container port instead of a service port. The new behavior is enabled by adding a telepresence.getambassador.io/inject-container-ports annotation where the value is a comma separated list of port identifiers consisting of either the name or the port number of a container port, optionally suffixed with `/TCP` or `/UDP`. docs: https://telepresence.io/docs/reference/engagements/cli#intercepting-without-a-service - type: feature title: Publish the OSS version of the telepresence Helm chart body: >- The OSS version of the telepresence helm chart is now available at ghcr.io/telepresenceio/telepresence-oss, and can be installed using the command:
helm install traffic-manager oci://ghcr.io/telepresenceio/telepresence-oss --namespace ambassador --version 2.20.0 The chart documentation is published at ArtifactHUB. docs: https://artifacthub.io/packages/helm/telepresence-oss/telepresence-oss - type: feature title: Control the syntax of the environment file created with the intercept flag --env-file body: >- A new --env-syntax <syntax> was introduced to allow control over the syntax of the file created when using the intercept flag --env-file <file>. Valid syntaxes are "docker", "compose", "sh", "csh", "cmd", and "ps"; where "sh", "csh", and "ps" can be suffixed with ":export". docs: https://telepresence.io/docs/reference/environment - type: feature title: Add support for Argo Rollout workloads. body: >- Telepresence now has an opt-in support for Argo Rollout workloads. The behavior is controlled by `workloads.argoRollouts.enabled` Helm chart value. It is recommended to set the following annotation telepresence.getambassador.io/inject-traffic-agent: enabled to avoid creation of unwanted revisions. - type: bugfix title: Enable intercepts of containers that bind to podIP body: >- In previous versions, the traffic-agent would route traffic to localhost during periods when an intercept wasn't active. This made it impossible for an application to bind to the pod's IP, and it also meant that service meshes binding to the podIP would get bypassed, both during and after an intercept had been made. This is now changed, so that the traffic-agent instead forwards non intercepted requests to the pod's IP, thereby enabling the application to either bind to localhost or to that IP. - type: change title: Use ghcr.io/telepresenceio instead of docker.io/datawire for OSS images and the telemount Docker volume plugin. body: >- All OSS telepresence images and the telemount Docker plugin are now published at the public registry ghcr.io/telepresenceio and all references from the client and traffic-manager has been updated to use this registry instead of the one at docker.io/datawire. - title: Use nftables instead of iptables-legacy type: change body: >- Some time ago, we introduced iptables-legacy because users had problems using Telepresence with Fly.io where nftables wasn't supported by the kernel. Fly.io has since fixed this, so Telepresence will now use nftables again. This in turn, ensures that modern systems that lack support iptables-legacy will work. - type: bugfix title: Root daemon wouldn't start when sudo timeout was zero. body: >- The root daemon refused to start when sudo was configured with a timestamp_timeout=0. This was due to logic that first requested root privileges using a sudo call, and then relied on that these privileges were cached, so that a subsequent call using --non-interactive was guaranteed to succeed. This logic will now instead do one single sudo call, and rely solely on sudo to print an informative prompt and start the daemon in the background. - type: bugfix title: Detect minikube network when connecting with --docker body: >- A telepresence connect --docker failed when attempting to connect to a minikube that uses a docker driver because the containerized daemon did not have access to the minikube docker network. Telepresence will now detect an attempt to connect to that network and attach it to the daemon container as needed. - version: 2.19.1 date: "2024-07-12" notes: - type: feature title: Add brew support for the OSS version of Telepresence. body: >- The Open-Source Software version of Telepresence can now be installed using the brew formula via brew install telepresenceio/telepresence/telepresence-oss. docs: https://github.com/telepresenceio/telepresence/issues/3609 - type: feature title: Add --create-namespace flag to the telepresence helm install command. body: >- A --create-namespace (default true) flag was added to the telepresence helm install command. No attempt will be made to create a namespace for the traffic-manager if it is explicitly set to false. The command will then fail if the namespace is missing. - type: feature title: Introduce DNS fallback on Windows. body: >- A network.defaultDNSWithFallback config option has been introduced on Windows. It will cause the DNS-resolver to fall back to the resolver that was first in the list prior to when Telepresence establishes a connection. The option is default true since it is believed to give the best experience but can be set to false to restore the old behavior. - type: feature title: Brew now supports MacOS (amd64/arm64) / Linux (amd64) body: >- The brew formula can now dynamically support MacOS (amd64/arm64) / Linux (amd64) in a single formula docs: https://github.com/datawire/homebrew-blackbird/issues/19 - type: feature title: Add ability to provide an externally-provisioned webhook secret body: >- Added supplied as a new option for agentInjector.certificate.method. This fully disables the generation of the Mutating Webhook's secret, allowing the chart to use the values of a pre-existing secret named agentInjector.secret.name. Previously, the install would fail when it attempted to create or update the externally-managed secret. - type: feature title: Let PTR query for DNS server return the cluster domain. body: >- The nslookup program on Windows uses a PTR query to retrieve its displayed "Server" property. This Telepresence DNS resolver will now return the cluster domain on such a query. - type: feature title: Add scheduler name to PODs templates. body: >- A new Helm chart value schedulerName has been added. With this feature, we are able to define some particular schedulers from Kubernetes to apply some different strategies to allocate telepresence resources, including the Traffic Manager and hooks pods. - type: bugfix title: Race in traffic-agent injector when using inject annotation body: >- Applying multiple deployments that used the telepresence.getambassador.io/inject-traffic-agent: enabled would cause a race condition, resulting in a large number of created pods that eventually had to be deleted, or sometimes in pods that didn't contain a traffic agent. - type: bugfix title: Fix configuring custom agent security context body: -> The traffic-manager helm chart will now correctly use a custom agent security context if one is provided. - version: 2.19.0 date: "2024-06-15" notes: - type: feature title: Warn when an Open Source Client connects to an Enterprise Traffic Manager. body: >- The difference between the OSS and the Enterprise offering is not well understood, and OSS users often install a traffic-manager using the Helm chart published at getambassador.io. This Helm chart installs an enterprise traffic-manager, which is probably not what the user would expect. Telepresence will now warn when an OSS client connects to an enterprise traffic-manager and suggest switching to an enterprise client, or use telepresence helm install to install an OSS traffic-manager. - type: feature title: Add scheduler name to PODs templates. body: >- A new Helm chart value schedulerName has been added. With this feature, we are able to define some particular schedulers from Kubernetes to apply some different strategies to allocate telepresence resources, including the Traffic Manager and hooks pods. - type: bugfix title: Improve traffic-manager performance in very large clusters. body: -> The traffic-manager will now use a shared-informer when keeping track of deployments. This will significantly reduce the load on the Kublet in large clusters and therefore lessen the risk for the traffic-manager being throttled, which can lead to other problems. - type: bugfix title: Kubeconfig exec authentication failure when connecting with --docker from a WSL linux host body: >- Clusters like Amazon EKS often use a special authentication binary that is declared in the kubeconfig using an exec authentication strategy. This binary is normally not available inside a container. Consequently, a modified kubeconfig is used when telepresence connect --docker executes, appointing a kubeauth binary which instead retrieves the authentication from a port on the Docker host that communicates with another process outside of Docker. This process then executes the original exec command to retrieve the necessary credentials. This setup was problematic when using WSL, because even though telepresence connect --docker was executed on a Linux host, the Docker host available from host.docker.internal that the kubeauth connected to was the Windows host running Docker Desktop. The fix for this was to use the local IP of the default route instead of host.docker.internal when running under WSL.. - version: 2.18.6 date: (SUPERSEDED) notes: - type: bugfix title: Fix bug in workload cache, causing endless recursion when a workload uses the same name as its owner. body: >- The workload cache was keyed by name and namespace, but not by kind, so a workload named the same as its owner workload would be found using the same key. This led to the workload finding itself when looking up its owner, which in turn resulted in an endless recursion when searching for the topmost owner. - type: bugfix title: FailedScheduling events mentioning node availability considered fatal when waiting for agent to arrive. body: >- The traffic-manager considers some events as fatal when waiting for a traffic-agent to arrive after an injection has been initiated. This logic would trigger on events like "Warning FailedScheduling 0/63 nodes are available" although those events indicate a recoverable condition and kill the wait. This is now fixed so that the events are logged but the wait continues. - version: 2.18.5 date: (SUPERSEDED) notes: - type: bugfix title: Improve how the traffic-manager resolves DNS when no agent is installed. body: >- The traffic-manager is typically installed into a namespace different from the one that clients are connected to. It's therefore important that the traffic-manager adds the client's namespace when resolving single label names in situations where there are any agents to dispatch the DNS query to. - type: change title: Removal of ability import legacy artifact into Helm. body: >- A helm install would make attempts to find manually installed artifacts and make them managed by Helm by adding the necessary labels and annotations. This was important when the Helm chart was first introduced but is far less so today, and this legacy import was therefore removed. - type: bugfix title: Docker aliases deprecation caused failure to detect Kind cluster. body: >- The logic for detecting if a cluster is a local Kind cluster, and therefore needs some special attention when using telepresence connect --docker, relied on the presence of Aliases in the Docker network that a Kind cluster sets up. In Docker versions from 26 and up, this value is no longer used, but the corresponding info can instead be found in the new DNSNames field. docs: https://docs.docker.com/engine/deprecated/#container-short-id-in-network-aliases-field - type: bugfix title: Include svc as a top-level domain in the DNS resolver. body: >- It's not uncommon that use-cases involving Kafka or other middleware use FQNs that end with "svc". The core-DNS resolver in Kubernetes can resolve such names. With this bugfix, the Telepresence DNS resolver will also be able to resolve them, and thereby remove the need to add ".svc" to the include-suffix list. docs: https://github.com/telepresenceio/telepresence/issues/2814 - type: feature title: Add ability to enable/disable the mutating webhook. body: >- A new Helm chart boolean value agentInjector.enable has been added that controls the agent-injector service and its associated mutating webhook. If set to false, the service, the webhook, and the secrets and certificates associated with it, will no longer be installed. - type: feature title: Add ability to mount a webhook secret. body: >- A new Helm chart value agentInjector.certificate.accessMethod which can be set to watch (the default) or mount has been added. The mount setting is intended for clusters with policies that prevent containers from doing a get, list or watch of a Secret, but where a latency of up to 90 seconds is acceptable between the time the secret is regenerated and the agent-injector picks it up. - type: feature title: Make it possible to specify ignored volume mounts using path prefix. body: >- Volume mounts like /var/run/secrets/kubernetes.io are not declared in the workload. Instead, they are injected during pod-creation and their names are generated. It is now possible to ignore such mounts using a matching path prefix. - type: feature title: Make the telemount Docker Volume plugin configurable body: >- A telemount object was added to the intercept object in config.yml (or Helm value client.intercept), so that the automatic download and installation of this plugin can be fully customised. - type: feature title: Add option to load the kubeconfig yaml from stdin during connect. body: >- This allows another process with a kubeconfig already loaded in memory to directly pass it to telepresence connect without needing a separate file. Simply use a dash "-" as the filename for the --kubeconfig flag. - type: feature title: Add ability to specify agent security context. body: >- A new Helm chart value agent.securityContext that will allow configuring the security context of the injected traffic agent. The value can be set to a valid Kubernetes securityContext object, or can be set to an empty value ({}) to ensure the agent has no defined security context. If no value is specified, the traffic manager will set the agent's security context to the same as the first container's of the workload being injected into. - type: change title: Tracing is no longer enabled by default. body: >- Tracing must now be enabled explicitly in order to use the telepresence gather-traces command. - type: change title: Removal of timeouts that are no longer in use body: >- The config.yml values timeouts.agentInstall and timeouts.apply haven't been in use since versions prior to 2.6.0, when the client was responsible for installing the traffic-agent. These timeouts are now removed from the code-base, and a warning will be printed when attempts are made to use them. - type: bugfix title: Search all private subnets to find one open for dnsServerSubnet body: >- This resolves a bug that did not test all subnets in a private range, sometimes resulting in the warning, "DNS doesn't seem to work properly." - version: 2.18.4 date: (SUPERSEDED) notes: - type: bugfix title: Docker aliases deprecation caused failure to detect Kind cluster. body: >- The logic for detecting if a cluster is a local Kind cluster, and therefore needs some special attention when using telepresence connect --docker, relied on the presence of Aliases in the Docker network that a Kind cluster sets up. In Docker versions from 26 and up, this value is no longer used, but the corresponding info can instead be found in the new DNSNames field. - version: 2.18.3 date: (SUPERSEDED) notes: - type: bugfix title: Creation of individual pods was blocked by the agent-injector webhook. body: >- An attempt to create a pod was blocked unless it was provided by a workload. Hence, commands like kubectl run -i busybox --rm --image=curlimages/curl --restart=Never -- curl echo-easy.default would be blocked from executing. - version: 2.18.2 date: (SUPERSEDED) notes: - type: bugfix title: Fix panic due to root daemon not running. body: >- If a telepresence connect was made at a time when the root daemon was not running (an abnormal condition) and a subsequent intercept was then made, a panic would occur when the port-forward to the agent was set up. This is now fixed so that the initial telepresence connect is refused unless the root daemon is running. - version: 2.18.1 date: (SUPERSEDED) notes: - type: bugfix title: Get rid of telemount plugin stickiness body: >- The datawire/telemount that is automatically downloaded and installed, would never be updated once the installation was made. Telepresence will now check for the latest release of the plugin and cache the result of that check for 24 hours. If a new version arrives, it will be installed and used. - type: bugfix title: Use route instead of address for CIDRs with masks that don't allow "via" body: >- A CIDR with a mask that leaves less than two bits (/31 or /32 for IPv4) cannot be added as an address to the VIF, because such addresses must have bits allowing a "via" IP. The logic was modified to allow such CIDRs to become static routes, using the VIF base address as their "via", rather than being VIF addresses in their own right. - type: bugfix title: Containerized daemon created cache files owned by root body: >- When using telepresence connect --docker to create a containerized daemon, that daemon would sometimes create files in the cache that were owned by root, which then caused problems when connecting without the --docker flag. - type: bugfix title: Remove large number of requests when traffic-manager is used in large clusters. body: >- The traffic-manager would make a very large number of API requests during cluster start-up or when many services were changed for other reasons. The logic that did this was refactored and the number of queries were significantly reduced. - type: bugfix title: Don't patch probes on replaced containers. body: >- A container that is being replaced by a telepresence intercept --replace invocation will have no liveness-, readiness, nor startup-probes. Telepresence didn't take this into consideration when injecting the traffic-agent, but now it will refrain from patching symbolic port names of those probes. - type: bugfix title: Don't rely on context name when deciding if a kind cluster is used. body: >- The code that auto-patches the kubeconfig when connecting to a kind cluster from within a docker container, relied on the context name starting with "kind-", but although all contexts created by kind have that name, the user is still free to rename it or to create other contexts using the same connection properties. The logic was therefore changed to instead look for a loopback service address. - version: 2.18.0 date: "2024-02-09" notes: - type: feature title: Include the image for the traffic-agent in the output of the version and status commands. body: >- The version and status commands will now output the image that the traffic-agent will be using when injected by the agent-injector. - type: feature title: Custom DNS using the client DNS resolver. body: >-

A new telepresence connect --proxy-via CIDR=WORKLOAD flag was introduced, allowing Telepresence to translate DNS responses matching specific subnets into virtual IPs that are used locally. Those virtual IPs are then routed (with reverse translation) via the pod's of a given workload. This makes it possible to handle custom DNS servers that resolve domains into loopback IPs. The flag may also be used in cases where the cluster's subnets are in conflict with the workstation's VPN.

The CIDR can also be a symbolic name that identifies a subnet or list of subnets:

alsoAll subnets added with --also-proxy
serviceThe cluster's service subnet
podsThe cluster's pod subnets.
allAll of the above.
- type: bugfix title: Ensure that agent.appProtocolStrategy is propagated correctly. body: >- The agent.appProtocolStrategy was inadvertently dropped when moving license related code fromm the OSS repository the repository for the Enterprise version of Telepresence. It has now been restored. - type: bugfix title: Include non-default zero values in output of telepresence config view. body: >- The telepresence config view command will now print zero values in the output when the default for the value is non-zero. - type: bugfix title: Restore ability to run the telepresence CLI in a docker container. body: >- The improvements made to be able to run the telepresence daemon in docker using telepresence connect --docker made it impossible to run both the CLI and the daemon in docker. This commit fixes that and also ensures that the user- and root-daemons are merged in this scenario when the container runs as root. - type: bugfix title: Remote mounts when intercepting with the --replace flag. body: >- A telepresence intercept --replace did not correctly mount all volumes, because when the intercepted container was removed, its mounts were no longer visible to the agent-injector when it was subjected to a second invocation. The container is now kept in place, but with an image that just sleeps infinitely. - type: bugfix title: Intercepting with the --replace flag will no longer require all subsequent intercepts to use --replace. body: >- A telepresence intercept --replace will no longer switch the mode of the intercepted workload, forcing all subsequent intercepts on that workload to use --replace until the agent is uninstalled. Instead, --replace can be used interchangeably just like any other intercept flag. - type: bugfix title: Kubeconfig exec authentication with context names containing colon didn't work on Windows body: >- The logic added to allow the root daemon to connect directly to the cluster using the user daemon as a proxy for exec type authentication in the kube-config, didn't take into account that a context name sometimes contains the colon ":" character. That character cannot be used in filenames on windows because it is the drive letter separator. - type: bugfix title: Provide agent name and tag as separate values in Helm chart body: >- The AGENT_IMAGE was a concatenation of the agent's name and tag. This is now changed so that the env instead contains an AGENT_IMAGE_NAME and AGENT_INAGE_TAG. The AGENT_IMAGE is removed. Also, a new env REGISTRY is added, where the registry of the traffic- manager image is provided. The AGENT_REGISTRY is no longer required and will default to REGISTRY if not set. - type: bugfix title: Environment interpolation expressions were prefixed twice. body: >- Telepresence would sometimes prefix environment interpolation expressions in the traffic-agent twice so that an expression that looked like $(SOME_NAME) in the app-container, ended up as $(_TEL_APP_A__TEL_APP_A_SOME_NAME) in the corresponding expression in the traffic-agent. - type: bugfix title: Panic in root-daemon on darwin workstations with full access to cluster network. body: >- A darwin machine with full access to the cluster's subnets will never create a TUN-device, and a check was missing if the device actually existed, which caused a panic in the root daemon. - type: bugfix title: Show allow-conflicting-subnets in telepresence status and telepresence config view. body: >- The telepresence status and telepresence config view commands didn't show the allowConflictingSubnets CIDRs because the value wasn't propagated correctly to the CLI. - type: feature title: It is now possible use a host-based connection and containerized connections simultaneously. body: >- Only one host-based connection can exist because that connection will alter the DNS to reflect the namespace of the connection. but it's now possible to create additional connections using --docker while retaining the host-based connection. - type: feature title: Ability to set the hostname of a containerized daemon. body: >- The hostname of a containerized daemon defaults to be the container's ID in Docker. You now can override the hostname using telepresence connect --docker --hostname <a name>. - type: feature title: New --multi-daemonflag to enforce a consistent structure for the status command output. body: >- The output of the telepresence status when using --output json or --output yaml will either show an object where the user_daemon and root_daemon are top level elements, or when multiple connections are used, an object where a connections list contains objects with those daemons. The flag --multi-daemon will enforce the latter structure even when only one daemon is connected so that the output can be parsed consistently. The reason for keeping the former structure is to retain backward compatibility with existing parsers. - type: bugfix title: Make output from telepresence quit more consistent. body: >- A quit (without -s) just disconnects the host user and root daemons but will quit a container based daemon. The message printed was simplified to remove some have/has is/are errors caused by the difference. - type: bugfix title: "Fix "tls: bad certificate" errors when refreshing the mutator-webhook secret" body: >- The agent-injector service will now refresh the secret used by the mutator-webhook each time a new connection is established, thus preventing the certificates to go out-of-sync when the secret is regenerated. - type: bugfix title: Keep telepresence-agents configmap in sync with pod states. body: >- An intercept attempt that resulted in a timeout due to failure of injecting the traffic-agent left the telepresence-agents configmap in a state that indicated that an agent had been added, which caused problems for subsequent intercepts after the problem causing the first failure had been fixed. - type: bugfix title: The telepresence status command will now report the status of all running daemons. body: >- A telepresence status, issued when multiple containerized daemons were active, would error with "multiple daemons are running, please select one using the --use <match> flag". This is now fixed so that the command instead reports the status of all running daemons. - type: bugfix title: The telepresence version command will now report the version of all running daemons. body: >- A telepresence version, issued when multiple containerized daemons were active, would error with "multiple daemons are running, please select one using the --use <match> flag". This is now fixed so that the command instead reports the version of all running daemons. - type: bugfix title: Multiple containerized daemons can now be disconnected using telepresence quit -s body: >- A telepresence quit -s, issued when multiple containerized daemons were active, would error with "multiple daemons are running, please select one using the --use <match> flag". This is now fixed so that the command instead quits all daemons. - type: bugfix title: The DNS search path on Windows is now restored when Telepresence quits body: >- The DNS search path that Telepresence uses to simulate the DNS lookup functionality in the connected cluster namespace was not removed by a telepresence quit, resulting in connectivity problems from the workstation. Telepresence will now remove the entries that it has added to the search list when it quits. - type: bugfix title: The user-daemon would sometimes get killed when used by multiple simultaneous CLI clients. body: >- The user-daemon would die with a fatal "fatal error: concurrent map writes" error in the connector.log, effectively killing the ongoing connection. - type: bugfix title: Multiple services ports using the same target port would not get intercepted correctly. body: >- Intercepts didn't work when multiple service ports were using the same container port. Telepresence would think that one of the ports wasn't intercepted and therefore disable the intercept of the container port. - type: bugfix title: Root daemon refuses to disconnect. body: >- The root daemon would sometimes hang forever when attempting to disconnect due to a deadlock in the VIF-device. - type: bugfix title: Fix panic in user daemon when traffic-manager was unreachable body: >- The user daemon would panic if the traffic-manager was unreachable. It will now instead report a proper error to the client. - type: change title: Removal of backward support for versions predating 2.6.0 body: >- The telepresence helm installer will no longer discover and convert workloads that were modified by versions prior to 2.6.0. The traffic manager will and no longer support the muxed tunnels used in versions prior to 2.5.0. - version: 2.17.0 date: "2023-11-14" notes: - type: feature title: Additional Prometheus metrics to track intercept/connect activity body: >- This feature adds the following metrics to the Prometheus endpoint: connect_count, connect_active_status, intercept_count, and intercept_active_status. These are labeled by client/install_id. Additionally, the intercept_count metric has been renamed to active_intercept_count for clarity. - type: feature title: Make the Telepresence client docker image configurable. body: >- The docker image used when running a Telepresence intercept in docker mode can now be configured using the setting images.clientImage and will default first to the value of the environment TELEPRESENCE_CLIENT_IMAGE, and then to the value preset by the telepresence binary. This configuration setting is primarily intended for testing purposes. - type: feature title: Use traffic-agent port-forwards for outbound and intercepted traffic. body: >- The telepresence TUN-device is now capable of establishing direct port-forwards to a traffic-agent in the connected namespace. That port-forward is then used for all outbound traffic to the device, and also for all traffic that arrives from intercepted workloads. Getting rid of the extra hop via the traffic-manager improves performance and reduces the load on the traffic-manager. The feature can only be used if the client has Kubernetes port-forward permissions to the connected namespace. It can be disabled by setting cluster.agentPortForward to false in config.yml. - type: feature title: Improve outbound traffic performance. body: >- The root-daemon now communicates directly with the traffic-manager instead of routing all outbound traffic through the user-daemon. The root-daemon uses a patched kubeconfig where exec configurations to obtain credentials are dispatched to the user-daemon. This to ensure that all authentication plugins will execute in user-space. The old behavior of routing everything through the user-daemon can be restored by setting cluster.connectFromRootDaemon to false in config.yml. - type: feature title: New networking CLI flag --allow-conflicting-subnets body: >- telepresence connect (and other commands that kick off a connect) now accepts an --allow-conflicting-subnets CLI flag. This is equivalent to client.routing.allowConflictingSubnets in the helm chart, but can be specified at connect time. It will be appended to any configuration pushed from the traffic manager. - type: change title: Warn if large version mismatch between traffic manager and client. body: >- Print a warning if the minor version diff between the client and the traffic manager is greater than three. - type: change title: The authenticator binary was removed from the docker image. body: >- The authenticator binary, used when serving proxied exec kubeconfig credential retrieval, has been removed. The functionality was instead added as a subcommand to the telepresence binary. - version: 2.16.1 date: "2023-10-12" notes: - type: feature title: Add --docker-debug flag to the telepresence intercept command. body: >- This flag is similar to --docker-build but will start the container with more relaxed security using the docker run flags --security-opt apparmor=unconfined --cap-add SYS_PTRACE. - type: feature title: Add a --export option to the telepresence connect command. body: >- In some situations it is necessary to make some ports available to the host from a containerized telepresence daemon. This commit adds a repeatable --expose <docker port exposure> flag to the connect command. - type: feature title: Prevent agent-injector webhook from selecting from kube-xxx namespaces. body: >- The kube-system and kube-node-lease namespaces should not be affected by a global agent-injector webhook by default. A default namespaceSelector was therefore added to the Helm Chart agentInjector.webhook that contains a NotIn preventing those namespaces from being selected. - type: bugfix title: Backward compatibility for pod template TLS annotations. body: >- Users of Telepresence < 2.9.0 that make use of the pod template TLS annotations were unable to upgrade because the annotation names have changed (now prefixed by "telepresence."), and the environment expansion of the annotation values was dropped. This fix restores support for the old names (while retaining the new ones) and the environment expansion. - type: security title: Built with go 1.21.3 body: >- Built Telepresence with go 1.21.3 to address CVEs. - type: bugfix title: Match service selector against pod template labels body: >- When listing intercepts (typically by calling telepresence list) selectors of services are matched against workloads. Previously the match was made against the labels of the workload, but now they are matched against the labels pod template of the workload. Since the service would actually be matched against pods this is more correct. The most common case when this makes a difference is that statefulsets now are listed when they should. - version: 2.16.0 date: "2023-10-02" notes: - type: bugfix title: The helm sub-commands will no longer start the user daemon. body: >- The telepresence helm install/upgrade/uninstall commands will no longer start the telepresence user daemon because there's no need to connect to the traffic-manager in order for them to execute. - type: bugfix title: Routing table race condition body: >- A race condition would sometimes occur when a Telepresence TUN device was deleted and another created in rapid succession that caused the routing table to reference interfaces that no longer existed. - type: bugfix title: Stop lingering daemon container body: >- When using telepresence connect --docker, a lingering container could be present, causing errors like "The container name NN is already in use by container XX ...". When this happens, the connect logic will now give the container some time to stop and then call docker stop NN to stop it before retrying to start it. - type: bugfix title: Add file locking to the Telepresence cache body: >- Files in the Telepresence cache are accesses by multiple processes. The processes will now use advisory locks on the files to guarantee consistency. - type: change title: Lock connection to namespace body: >- The behavior changed so that a connected Telepresence client is bound to a namespace. The namespace can then not be changed unless the client disconnects and reconnects. A connection is also given a name. The default name is composed from <kube context name>-<namespace> but can be given explicitly when connecting using --name. The connection can optionally be identified using the option --use <name match> (only needed when docker is used and more than one connection is active). - type: change title: Deprecation of global --context and --docker flags. body: >- The global flags --context and --docker will now be considered deprecated unless used with commands that accept the full set of Kubernetes flags (e.g. telepresence connect). - type: change title: Deprecation of the --namespace flag for the intercept command. body: >- The --namespace flag is now deprecated for telepresence intercept command. The flag can instead be used with all commands that accept the full set of Kubernetes flags (e.g. telepresence connect). - type: change title: Legacy code predating version 2.6.0 was removed. body: >- The telepresence code-base still contained a lot of code that would modify workloads instead of relying on the mutating webhook installer when a traffic-manager version predating version 2.6.0 was discovered. This code has now been removed. - type: feature title: Add `telepresence list-namespaces` and `telepresence list-contexts` commands body: >- These commands can be used to check accessible namespaces and for automation. - type: change title: Implicit connect warning body: >- A deprecation warning will be printed if a command other than telepresence connect causes an implicit connect to happen. Implicit connects will be removed in a future release. - version: 2.15.1 date: "2023-09-06" notes: - type: security title: Rebuild with go 1.21.1 body: >- Rebuild Telepresence with go 1.21.1 to address CVEs. - type: security title: Set security context for traffic agent body: >- Openshift users reported that the traffic agent injection was failing due to a missing security context. - version: 2.15.0 date: "2023-08-29" notes: - type: security title: Add ASLR to telepresence binaries body: >- ASLR hardens binary sercurity against fixed memory attacks. - type: feature title: Added client builds for arm64 architecture. body: >- Updated the release workflow files in github actions to including building and publishing the client binaries for arm64 architecture. docs: https://github.com/telepresenceio/telepresence/issues/3259 - type: bugfix title: KUBECONFIG env var can now be used with the docker mode. body: >- If provided, the KUBECONFIG environment variable was passed to the kubeauth-foreground service as a parameter. However, since it didn't exist, the CLI was throwing an error when using telepresence connect --docker. docs: https://github.com/telepresenceio/telepresence/pull/3300 - type: bugfix title: Fix deadlock while watching workloads body: >- The telepresence list --output json-stream wasn't releasing the session's lock after being stopped, including with a telepresence quit. The user could be blocked as a result. docs: https://github.com/telepresenceio/telepresence/pull/3298 - type: bugfix title: Change json output of telepresence list command body: >- Replace deprecated info in the JSON output of the telepresence list command. - version: 2.14.4 date: "2023-08-21" notes: - type: bugfix title: Nil pointer exception when upgrading the traffic-manager. body: >- Upgrading the traffic-manager using telepresence helm upgrade would sometimes result in a helm error message executing "telepresence/templates/intercept-env-configmap.yaml" at <.Values.intercept.environment.excluded>: nil pointer evaluating interface {}.excluded" docs: https://github.com/telepresenceio/telepresence/issues/3313 - version: 2.14.2 date: "2023-07-26" notes: - type: bugfix title: Telepresence now use the OSS agent in its latest version by default. body: >- The traffic manager admin was forced to set it manually during the chart installation. docs: https://github.com/telepresenceio/telepresence/issues/3271 - version: 2.14.1 date: "2023-07-07" notes: - type: feature title: Envoy's http idle timout is now configurable. body: >- A new agent.helm.httpIdleTimeout setting was added to the Helm chart that controls the proprietary Traffic agent's http idle timeout. The default of one hour, which in some situations would cause a lot of resource consuming and lingering connections, was changed to 70 seconds. - type: feature title: Add more gauges to the Traffic manager's Prometheus client. body: >- Several gauges were added to the Prometheus client to make it easier to monitor what the Traffic manager spends resources on. - type: feature title: Agent Pull Policy body: >- Add option to set traffic agent pull policy in helm chart. - type: bugfix title: Resource leak in the Traffic manager. body: >- Fixes a resource leak in the Traffic manager caused by lingering tunnels between the clients and Traffic agents. The tunnels are now closed correctly when terminated from the side that created them. - type: bugfix title: Fixed problem setting traffic manager namespace using the kubeconfig extension. body: >- Fixes a regression introduced in version 2.10.5, making it impossible to set the traffic-manager namespace using the telepresence.io kubeconfig extension. docs: https://www.telepresence.io/docs/reference/config#manager - version: 2.14.0 date: "2023-06-12" notes: - type: feature title: DNS configuration now supports excludes and mappings. body: >- The DNS configuration now supports two new fields, excludes and mappings. The excludes field allows you to exclude a given list of hostnames from resolution, while the mappings field can be used to resolve a hostname with another. docs: https://github.com/telepresenceio/telepresence/pull/3172 - type: feature title: Added the ability to exclude environment variables body: >- Added a new config map that can take an array of environment variables that will then be excluded from an intercept that retrieves the environment of a pod. - type: bugfix title: Fixed traffic-agent backward incompatibility issue causing lack of remote mounts body: >- A traffic-agent of version 2.13.3 (or 1.13.15) would not propagate the directories under /var/run/secrets when used with a traffic manager older than 2.13.3. - type: bugfix title: Fixed race condition causing segfaults on rare occasions when a tunnel stream timed out. body: >- A context cancellation could sometimes be trapped in a stream reader, causing it to incorrectly return an undefined message which in turn caused the parent reader to panic on a nil pointer reference. docs: https://github.com/telepresenceio/telepresence/pull/2963 - type: change title: Routing conflict reporting. body: >- Telepresence will now attempt to detect and report routing conflicts with other running VPN software on client machines. There is a new configuration flag that can be tweaked to allow certain CIDRs to be overridden by Telepresence. - type: change title: test-vpn command deprecated body: >- Running telepresence test-vpn will now print a deprecation warning and exit. The command will be removed in a future release. Instead, please configure telepresence for your VPN's routes. - version: 2.13.3 date: "2023-05-25" notes: - type: feature title: Add imagePullSecrets to hooks body: >- Add .Values.hooks.curl.imagePullSecrets and .Values.hooks curl.imagePullSecrets to Helm values. docs: https://github.com/telepresenceio/telepresence/pull/3079 - type: change title: Change reinvocation policy to Never for the mutating webhook body: >- The default setting of the reinvocationPolicy for the mutating webhook dealing with agent injections changed from Never to IfNeeded. - type: bugfix title: Fix mounting fail of IAM roles for service accounts web identity token body: >- The eks.amazonaws.com/serviceaccount volume injected by EKS is now exported and remotely mounted during an intercept. docs: https://github.com/telepresenceio/telepresence/issues/3166 - type: bugfix title: Correct namespace selector for cluster versions with non-numeric characters body: >- The mutating webhook now correctly applies the namespace selector even if the cluster version contains non-numeric characters. For example, it can now handle versions such as Major:"1", Minor:"22+". docs: https://github.com/telepresenceio/telepresence/pull/3184 - type: bugfix title: Enable IPv6 on the telepresence docker network body: >- The "telepresence" Docker network will now propagate DNS AAAA queries to the Telepresence DNS resolver when it runs in a Docker container. docs: https://github.com/telepresenceio/telepresence/issues/3179 - type: bugfix title: Fix the crash when intercepting with --local-only and --docker-run body: >- Running telepresence intercept --local-only --docker-run no longer results in a panic. docs: https://github.com/telepresenceio/telepresence/issues/3171 - type: bugfix title: Fix incorrect error message with local-only mounts body: >- Running telepresence intercept --local-only --mount false no longer results in an incorrect error message saying "a local-only intercept cannot have mounts". docs: https://github.com/telepresenceio/telepresence/issues/3171 - type: bugfix title: specify port in hook urls body: >- The helm chart now correctly handles custom agentInjector.webhook.port that was not being set in hook URLs. docs: https://github.com/telepresenceio/telepresence/pull/3161 - type: bugfix title: Fix wrong default value for disableGlobal and agentArrival body: >- Params .intercept.disableGlobal and .timeouts.agentArrival are now correctly honored. ================================================ FILE: CLAUDE.md ================================================ # CLAUDE.md This file provides guidance for contributors and AI assistants working with this repository. ## Project Overview Telepresence is a Kubernetes development tool that enables fast local development by connecting your local workstation to a Kubernetes cluster. It allows developers to run services locally while accessing cluster resources and intercepting traffic from the cluster to their local machine. ## Git Workflow - Never commit directly to the `release/v2` branch. Always create a feature branch with a name following the pattern `username/topic` (e.g., `thallgren/fix-dns-resolution`). - All commits must be signed and signed-off (`git commit -s -S`). - Push the branch and create a pull request for review. - Always merge PRs with a merge commit (never squash or rebase). ## Build Artifacts The Open Source version of Telepresence consists of three artifacts: **Client-side (runs on developer workstation):** - **`telepresence` binary** - The same binary serves as CLI, user daemon, and root daemon. - **`telepresence` Docker image** - Used as both user and root daemon when running `telepresence connect --docker`. **Cluster-side (runs in Kubernetes):** - **`tel2` Docker image** - Used by the traffic-manager deployment and injected as traffic-agent sidecars. ## Build Commands ```bash # Set required environment variables export TELEPRESENCE_VERSION=v2.x.x-alpha.0 # or use auto-generated version export TELEPRESENCE_REGISTRY=local # 'local' for Docker Desktop, or 'ghcr.io/telepresenceio' # Build the telepresence binary make build # Build Docker images (for local Kubernetes development) make client-image # Client container image make tel2-image # Traffic-manager/traffic-agent image # Build everything for local development make build client-image tel2-image # Install to system make install # Clean build artifacts make clean make clobber # Also removes tools ``` Environment variables: - `TELEPRESENCE_REGISTRY` (required) - Docker registry for images. Use `local` for docker-based Kubernetes, or `ghcr.io/telepresenceio` for the release registry. - `TELEPRESENCE_VERSION` (optional) - Version string to compile into binaries and images. If not set, auto-generated from CHANGELOG.yml and source hash. Run `make help` for more information. ### Building on Windows Windows builds use `build-aux\winmake.bat` instead of `make` directly. Pass the same parameters as you would to make. The script runs make inside a Docker container with appropriate parameters for Windows binaries. ## Testing ```bash # Run unit tests make check-unit # Run all integration tests (requires Kubernetes cluster) make check-integration # Run a single integration test go test ./integration_test/... -v -testify.m=Test_InterceptDetailedOutput # Run an integration test suite TEST_SUITE='^WorkloadConfiguration$' go test ./integration_test/... -v # Build tests without running (useful for caching) make build-tests ``` Integration tests use testify suites. The test harness is in `integration_test/itest/`. Use `-testify.m=` to filter tests by name. Verbose output (`-v`) is recommended as tests produce human-readable output with timestamps that correlate with log files. ### Integration Test Environment Variables | Environment Name | Description | Default | |----------------------------|-----------------------------------------------|---------------------------| | `DEV_KUBECONFIG` | Cluster configuration used by the tests | Kubernetes default | | `DEV_CLIENT_REGISTRY` | Docker registry for the client image | ${TELEPRESENCE_REGISTRY} | | `DEV_MANAGER_REGISTRY` | Docker registry for the traffic-manager image | ${TELEPRESENCE_REGISTRY} | | `DEV_AGENT_REGISTRY` | Docker registry for the traffic-agent image | Traffic-manager registry | | `DEV_CLIENT_IMAGE` | Name of the client image | "telepresence" | | `DEV_MANAGER_IMAGE` | Name of the traffic-manager image | "tel2" | | `DEV_AGENT_IMAGE` | Name of the traffic-agent image | Traffic-manager image | | `DEV_CLIENT_VERSION` | Client version | ${TELEPRESENCE_VERSION#v} | | `DEV_MANAGER_VERSION` | Traffic-manager version | ${TELEPRESENCE_VERSION#v} | | `DEV_AGENT_VERSION` | Traffic-agent image version | Traffic-manager version | | `DEV_USERD_PROFILING_PORT` | Start user daemon with pprof enabled | | | `DEV_ROOTD_PROFILING_PORT` | Start root daemon with pprof enabled | | | `TEST_SUITE` | Regexp matching test suite name(s) | | These can also be provided in an `itest.yml` file placed next to `config.yml`: ```yaml Env: DEV_CLIENT_VERSION: v2.x.x-alpha.0 DEV_KUBECONFIG: /path/to/kubeconfig Config: docker: addHostGateway: false ``` ### Using Docker Desktop with Kubernetes Using Kubernetes bundled with Docker Desktop is the quickest way to run tests. No need to push images to a registry - Kubernetes finds them in Docker's local cache. Integration tests automatically use `pullPolicy=Never` when `DEV_CLIENT_REGISTRY` is set to "local". ```bash export TELEPRESENCE_VERSION=v2.x.x-alpha.0 export TELEPRESENCE_REGISTRY=local make build client-image tel2-image go test ./integration_test/... -v -testify.m=Test_InterceptDetailedOutput ``` ## Linting ```bash # Run all linters make lint # Run Go linter only make lint-go # Run protobuf linter only make lint-rpc # Auto-fix lint issues make format ``` Linting uses golangci-lint v2 running in Docker. Configuration is in `.golangci.yml`. ## Code Generation ```bash # Regenerate protobuf and license files make generate # Regenerate protobuf files only make protoc # Regenerate documentation files (after changing CHANGELOG.yml) make docs-files ``` **Important:** After modifying `CHANGELOG.yml`, always run `make docs-files` to regenerate documentation files (`docs/release-notes.md`, `docs/release-notes.mdx`, `docs/variables.yml`). **Important:** All files under `docs/reference/cli/` are generated from Go source code. Do not edit them directly; instead, modify the corresponding Go source and regenerate. ### Updating License Documentation Run `make generate` and commit changes to `DEPENDENCY_LICENSES.md` and `DEPENDENCIES.md`. ## Architecture ### Main Components 1. **CLI/Client** (`cmd/telepresence/`, `pkg/client/cli/`) - Single binary serving as CLI, user daemon, and root daemon - Commands are in `pkg/client/cli/cmd/` 2. **User Daemon (userd)** (`pkg/client/userd/`) - Runs as the user, manages connection to traffic-manager - Handles intercepts, port forwards, cluster communication 3. **Root Daemon (rootd)** (`pkg/client/rootd/`) - Runs with elevated privileges - Manages virtual network interface (VIF) and DNS 4. **Traffic Manager** (`cmd/traffic/cmd/manager/`) - Runs in the Kubernetes cluster (ambassador namespace by default) - Coordinates intercepts between clients and traffic-agents 5. **Traffic Agent** (`cmd/traffic/cmd/agent/`) - Injected as sidecar into intercepted pods - Routes traffic between the pod and the local machine 6. **Agent Init** (`cmd/traffic/cmd/agentinit/`) - Init container for setting up iptables rules in pods 7. **Docker Network Driver** (`cmd/teleroute/`) - Only used when connecting with `--docker` flag - Provides the Docker network that enables communication between the Telepresence daemon container and other containers ### Key Packages - `pkg/vif/` - Virtual network interface implementation - `pkg/tunnel/` - gRPC-based tunneling for network traffic - `pkg/dnsproxy/` - DNS resolution and proxying - `pkg/agentconfig/` - Traffic-agent configuration - `pkg/client/k8s/` - Kubernetes client interactions - `pkg/routing/` - Network routing logic - `pkg/client/cli/cmd/` - CLI commands. One per file. ### RPC Definitions Protocol buffers are in `rpc/` with separate packages: - `rpc/connector/` - Client-to-userd communication - `rpc/daemon/` - Client-to-rootd communication - `rpc/manager/` - Client/userd-to-traffic-manager communication - `rpc/agent/` - Traffic-manager-to-traffic-agent communication ### Helm Chart The traffic-manager Helm chart is in `charts/telepresence-oss/`. ## Debugging and Troubleshooting ### Log Files There are three log files: - `connector.log` - Output from user daemon: traffic-manager interaction, intercepts, port forwards - `daemon.log` - Output from root daemon: networking changes on your workstation - `cli.log` - Output from the command line interface Locations: - macOS: `~/Library/Logs/telepresence/` - Linux: `~/.cache/telepresence/logs/` - Windows: `%USERPROFILE%\AppData\Local\logs` Logs rotate daily. Use `tail -F ` to watch rotating logs seamlessly. ### Debugging Early-Initialization Errors If daemons fail during early initialization before logfiles are set up, run them directly to see stderr output. The `--address` flag is mandatory: ```bash # Run user daemon directly telepresence userd --logfile - --address :8083 # Run root daemon directly (requires sudo) sudo telepresence rootd --logfile - --address :8084 ``` ### Profiling the Daemons Enable [pprof](https://pkg.go.dev/net/http/pprof) profiling: ```bash telepresence quit -s telepresence connect --userd-profiling-port 6060 --rootd-profiling-port 6061 # Then browse http://localhost:6060/debug/pprof/ ``` ### Dumping Goroutine Stacks Send SIGQUIT to a daemon to dump goroutine stacks to its log file. On Windows, use profiling instead. ### RBAC Testing To test with limited RBAC privileges: ```bash kubectl apply -f k8s/client_rbac.yaml kubectl get sa telepresence-test-developer -o "jsonpath={.secrets[0].name}" # Get the token from the secret and configure kubectl kubectl get secret -o "jsonpath={.data.token}" | base64 --decode kubectl config set-credentials telepresence-test-developer --token kubectl config use-context telepresence-test-developer ``` ## Releases To create a release, set `TELEPRESENCE_VERSION` and run `make prepare-release`. This creates two annotated tags (`vX.Y.Z` and `rpc/vX.Y.Z`) and a commit updating go.mod references. Pushing the tags and branch triggers the release workflow. ```bash # Test release (marked as pre-release, not promoted to latest) export TELEPRESENCE_VERSION=v2.27.0-test.0 make prepare-release git push origin HEAD $TELEPRESENCE_VERSION rpc/$TELEPRESENCE_VERSION # Release candidate export TELEPRESENCE_VERSION=v2.27.0-rc.0 make prepare-release git push origin HEAD $TELEPRESENCE_VERSION rpc/$TELEPRESENCE_VERSION # GA release (becomes "latest", updates Homebrew) export TELEPRESENCE_VERSION=v2.27.0 make prepare-release git push origin HEAD $TELEPRESENCE_VERSION rpc/$TELEPRESENCE_VERSION ``` Version formats: - `vX.Y.Z-test.N` - Test release (pre-release) - `vX.Y.Z-rc.N` - Release candidate (pre-release) - `vX.Y.Z` - GA release (marked as latest, triggers Homebrew update) ### Changelog When adding entries to `CHANGELOG.yml` for an upcoming release: - Use `date: (TBD)` for unreleased versions - The `make prepare-release` command will set the actual date when `TELEPRESENCE_VERSION` is a GA version (e.g., `v2.27.0`) - After modifying `CHANGELOG.yml`, run `make docs-files` to regenerate documentation ### Documentation Website The documentation website at [telepresence.io](https://telepresence.io) is managed in the [telepresenceio/telepresence.io](https://github.com/telepresenceio/telepresence.io) repository. When creating a GA release, update the website by running `make generate-version` in that repository with: - `DOCS_BRANCH` - Branch in this repository containing the docs (e.g., `release/v2`) - `DOCS_VERSION` - Major.minor version to generate or update (e.g., `2.27`) See the telepresence.io repository for full instructions. ### macOS Installer Signing and Notarization The macOS `.pkg` installers are signed and notarized to pass Gatekeeper verification. The signing process uses a protected GitHub Environment to secure the signing credentials. #### Environment Setup The `build-macos-pkg` job uses the `macos-signing` environment, which must be configured in the repository settings: 1. Go to https://github.com/telepresenceio/telepresence/settings/environments 2. Create an environment named `macos-signing` 3. Enable "Required reviewers" and add authorized personnel 4. Optionally restrict deployment branches to `release/*` 5. Add the following secrets to the environment (not repository-level): | Secret Name | Description | |-------------|-------------| | `MACOS_CERTIFICATE_P12` | Base64-encoded P12 file containing both Application and Developer ID Installer certificates | | `MACOS_CERTIFICATE_PASSWORD` | Password for the P12 file | | `MACOS_SIGN_APPLICATION` | Developer ID Application certificate name (e.g., `Developer ID Application: Your Name (TEAMID)`) | | `MACOS_SIGN_INSTALLER` | Developer ID Installer certificate name (e.g., `Developer ID Installer: Your Name (TEAMID)`) | | `MACOS_NOTARIZE_APPLE_ID` | Apple ID email for notarization | | `MACOS_NOTARIZE_TEAM_ID` | Apple Developer Team ID | | `MACOS_NOTARIZE_PASSWORD` | App-specific password for notarization | #### Release Workflow When a release tag is pushed: 1. All platform binaries (Linux, Windows, macOS) are built immediately 2. Linux `.deb`/`.rpm` and Windows `.exe` installers are built 3. The release is published with all binaries and Linux/Windows installers 4. The `build-macos-pkg` job waits for approval from a required reviewer 5. Once approved, signed `.pkg` installers are built and added to the release This design ensures: - **Emergency releases can proceed** without the signing approver being available (all binaries and Linux/Windows installers are released) - **Signing credentials are protected** by requiring explicit approval before they are exposed - **Signed packages are added later** when the approver reviews and approves the job If the environment is not configured or never approved, the release will contain macOS standalone binaries but not `.pkg` installers. #### Obtaining the Certificates You need an [Apple Developer Program](https://developer.apple.com/programs/) membership ($99/year) to obtain signing certificates. 1. **Create certificates in Apple Developer Portal:** - Go to [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/certificates/list) - Click the + button to create a new certificate - Create **Developer ID Application** certificate (for signing binaries) - Create **Developer ID Installer** certificate (for signing .pkg files) - Download both certificates and double-click to install in Keychain Access 2. **Find your Team ID:** - Go to [Membership Details](https://developer.apple.com/account#MembershipDetailsCard) - Copy the Team ID (10-character alphanumeric string) - Set as `MACOS_NOTARIZE_TEAM_ID` 3. **Find the certificate names:** - Open Keychain Access and look under "My Certificates" - The names will be like: - `Developer ID Application: Your Name (TEAMID)` → `MACOS_SIGN_APPLICATION` - `Developer ID Installer: Your Name (TEAMID)` → `MACOS_SIGN_INSTALLER` - You can also list them with: `security find-identity -v -p codesigning` 4. **Export certificates to P12:** ```bash # Export each certificate from Keychain Access: # - Right-click certificate → Export # - Choose .p12 format # - Set a strong password (will be MACOS_CERTIFICATE_PASSWORD) # If you have both in separate .p12 files, you can import them together # or export them together from Keychain Access by selecting both # Base64-encode for GitHub secrets: base64 -i certificates.p12 | pbcopy # Paste as MACOS_CERTIFICATE_P12 ``` 5. **Create app-specific password for notarization:** - Go to [appleid.apple.com](https://appleid.apple.com/) → Sign-In and Security → App-Specific Passwords - Generate a new password with a descriptive name (e.g., "GitHub Actions Notarization") - Copy the generated password → `MACOS_NOTARIZE_PASSWORD` - Use your Apple ID email → `MACOS_NOTARIZE_APPLE_ID` #### Testing Locally To test signing locally before configuring GitHub secrets: ```bash # Set environment variables export MACOS_SIGN_APPLICATION="Developer ID Application: Your Name (TEAMID)" export MACOS_SIGN_INSTALLER="Developer ID Installer: Your Name (TEAMID)" export MACOS_NOTARIZE_APPLE_ID="your@email.com" export MACOS_NOTARIZE_TEAM_ID="ABCD123456" export MACOS_NOTARIZE_PASSWORD="xxxx-xxxx-xxxx-xxxx" # Build the signed and notarized package cd build-aux/pkg-installer VERSION=2.26.0 ./build-pkg.sh # Verify the signature pkgutil --check-signature ../../build-output/Telepresence.pkg spctl --assess --type install ../../build-output/Telepresence.pkg ``` ================================================ FILE: CODE-OF-CONDUCT.md ================================================ # Code of Conduct We follow the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). Please contact the [CNCF Code of Conduct Committee](mailto:conduct@cncf.io) in order to report violations of the Code of Conduct. ================================================ FILE: CODEOWNERS ================================================ # Default codeowners for the telepresenceio/telepresence repository * @telepresenceio/telepresence ================================================ FILE: DEPENDENCIES.md ================================================ The Go module "github.com/telepresenceio/telepresence/v2" incorporates the following Free and Open Source software: Name Version License(s) ---- ------- ---------- the Go language standard library ("std") v1.26 3-clause BSD license dario.cat/mergo v1.0.2 3-clause BSD license github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c MIT license github.com/BurntSushi/toml v1.6.0 MIT license github.com/MakeNowJust/heredoc v1.0.0 MIT license github.com/Masterminds/goutils v1.1.1 Apache License 2.0 github.com/Masterminds/semver/v3 v3.4.0 MIT license github.com/Masterminds/sprig/v3 v3.3.0 MIT license github.com/Masterminds/squirrel v1.5.4 MIT license github.com/Microsoft/go-winio v0.6.2 MIT license github.com/alexflint/go-filemutex v1.3.0 MIT license github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 MIT license github.com/beorn7/perks v1.0.1 MIT license github.com/blang/semver/v4 v4.0.0 MIT license github.com/thallgren/env/v11 (modified from github.com/caarlos0/env/v11) v11.0.0-20260107112108-5d5593a09332 MIT license github.com/cenkalti/backoff/v4 v4.3.0 MIT license github.com/cespare/xxhash/v2 v2.3.0 MIT license github.com/chai2010/gettext-go v1.0.3 3-clause BSD license github.com/clipperhouse/uax29/v2 v2.7.0 MIT license github.com/compose-spec/compose-go/v2 v2.10.1 Apache License 2.0, MIT license github.com/containerd/containerd v1.7.30 Apache License 2.0 github.com/containerd/errdefs v1.0.0 Apache License 2.0 github.com/containerd/errdefs/pkg v0.3.0 Apache License 2.0 github.com/containerd/log v0.1.0 Apache License 2.0 github.com/containerd/platforms v0.2.1 Apache License 2.0 github.com/coreos/go-iptables v0.8.0 Apache License 2.0 github.com/cyphar/filepath-securejoin v0.6.1 3-clause BSD license, Mozilla Public License 2.0 github.com/datawire/argo-rollouts-go-client v0.0.0-20241216133646-cb1073556c99 Apache License 2.0 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc ISC license github.com/distribution/reference v0.6.0 Apache License 2.0 github.com/docker/docker v28.5.2+incompatible Apache License 2.0 github.com/docker/go-connections v0.6.0 Apache License 2.0 github.com/docker/go-units v0.5.0 Apache License 2.0 github.com/emicklei/go-restful/v3 v3.13.0 MIT license github.com/evanphx/json-patch v5.9.11+incompatible 3-clause BSD license github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f MIT license github.com/fatih/camelcase v1.0.0 MIT license github.com/fatih/color v1.18.0 MIT license github.com/fclairamb/ftpserverlib v0.30.0 MIT license github.com/felixge/httpsnoop v1.0.4 MIT license github.com/fsnotify/fsnotify v1.9.0 3-clause BSD license github.com/fxamacker/cbor/v2 v2.9.0 MIT license github.com/go-errors/errors v1.5.1 MIT license github.com/go-gorp/gorp/v3 v3.1.0 MIT license github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 3-clause BSD license github.com/go-logr/logr v1.4.3 Apache License 2.0 github.com/go-logr/stdr v1.2.2 Apache License 2.0 github.com/go-openapi/jsonpointer v0.22.4 Apache License 2.0 github.com/go-openapi/jsonreference v0.21.4 Apache License 2.0 github.com/go-openapi/swag v0.25.4 Apache License 2.0 github.com/go-openapi/swag/cmdutils v0.25.4 Apache License 2.0 github.com/go-openapi/swag/conv v0.25.4 Apache License 2.0 github.com/go-openapi/swag/fileutils v0.25.4 Apache License 2.0 github.com/go-openapi/swag/jsonname v0.25.4 Apache License 2.0 github.com/go-openapi/swag/jsonutils v0.25.4 Apache License 2.0 github.com/go-openapi/swag/loading v0.25.4 Apache License 2.0 github.com/go-openapi/swag/mangling v0.25.4 Apache License 2.0 github.com/go-openapi/swag/netutils v0.25.4 Apache License 2.0 github.com/go-openapi/swag/stringutils v0.25.4 Apache License 2.0 github.com/go-openapi/swag/typeutils v0.25.4 Apache License 2.0 github.com/go-openapi/swag/yamlutils v0.25.4 Apache License 2.0 github.com/go-viper/mapstructure/v2 v2.5.0 MIT license github.com/gobwas/glob v0.2.3 MIT license github.com/godbus/dbus/v5 v5.2.2 2-clause BSD license github.com/gogo/protobuf v1.3.2 3-clause BSD license github.com/google/btree v1.1.3 Apache License 2.0 github.com/google/gnostic-models v0.7.1 Apache License 2.0 github.com/google/go-cmp v0.7.0 3-clause BSD license github.com/google/jsonschema-go v0.4.2 MIT license github.com/google/uuid v1.6.0 3-clause BSD license github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 2-clause BSD license github.com/gosuri/uitable v0.0.4 MIT license github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 MIT license github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 Apache License 2.0 github.com/hashicorp/errwrap v1.1.0 Mozilla Public License 2.0 github.com/hashicorp/go-multierror v1.1.1 Mozilla Public License 2.0 github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb MIT license github.com/huandu/xstrings v1.5.0 MIT license github.com/inconshreveable/mousetrap v1.1.0 Apache License 2.0 github.com/jlaffaye/ftp v0.2.0 ISC license github.com/jmoiron/sqlx v1.4.0 MIT license github.com/json-iterator/go v1.1.12 MIT license github.com/klauspost/compress v1.18.4 3-clause BSD license, Apache License 2.0, MIT license github.com/kr/fs v0.1.0 3-clause BSD license github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 MIT license github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 MIT license github.com/lib/pq v1.11.2 MIT license github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de 3-clause BSD license github.com/mattn/go-colorable v0.1.14 MIT license github.com/mattn/go-isatty v0.0.20 MIT license github.com/mattn/go-runewidth v0.0.20 MIT license github.com/mattn/go-shellwords v1.0.12 MIT license github.com/miekg/dns v1.1.72 3-clause BSD license github.com/mitchellh/copystructure v1.2.0 MIT license github.com/mitchellh/go-wordwrap v1.0.1 MIT license github.com/mitchellh/reflectwalk v1.0.2 MIT license github.com/moby/docker-image-spec v1.3.1 Apache License 2.0 github.com/moby/spdystream v0.5.0 Apache License 2.0 github.com/moby/term v0.5.2 Apache License 2.0 github.com/modelcontextprotocol/go-sdk v1.4.0 Apache License 2.0, MIT license github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd Apache License 2.0 github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee Apache License 2.0 github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 MIT license github.com/morikuni/aec v1.1.0 MIT license github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 3-clause BSD license github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f 3-clause BSD license github.com/njayp/ophis v1.1.4 Apache License 2.0 github.com/opencontainers/go-digest v1.0.0 Apache License 2.0 github.com/opencontainers/image-spec v1.1.1 Apache License 2.0 github.com/peterbourgon/diskv v2.0.1+incompatible MIT license github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c 2-clause BSD license github.com/pkg/errors v0.9.1 2-clause BSD license github.com/pkg/sftp v1.13.10 2-clause BSD license github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 3-clause BSD license github.com/prometheus/client_golang v1.23.2 3-clause BSD license, Apache License 2.0 github.com/prometheus/client_model v0.6.2 Apache License 2.0 github.com/prometheus/common v0.67.5 Apache License 2.0 github.com/prometheus/procfs v0.20.0 Apache License 2.0 github.com/puzpuzpuz/xsync/v4 v4.4.0 Apache License 2.0 github.com/rogpeppe/go-internal v1.14.1 3-clause BSD license github.com/rubenv/sql-migrate v1.8.1 MIT license github.com/russross/blackfriday/v2 v2.1.0 2-clause BSD license github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 MIT license github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 Apache License 2.0 github.com/segmentio/asm v1.2.1 MIT No Attribution license github.com/segmentio/encoding v0.5.3 MIT license github.com/shopspring/decimal v1.4.0 MIT license github.com/sirupsen/logrus v1.9.4 MIT license github.com/spf13/afero v1.15.0 Apache License 2.0 github.com/spf13/cast v1.10.0 MIT license github.com/spf13/cobra v1.10.2 Apache License 2.0 github.com/spf13/pflag v1.0.10 3-clause BSD license github.com/stretchr/testify v1.11.1 MIT license github.com/telepresenceio/clog v0.0.0-20260114221933-287514cf9831 Apache License 2.0 github.com/telepresenceio/go-ftpserver v1.2.1 Apache License 2.0 github.com/telepresenceio/go-fuseftp v1.0.1 Apache License 2.0 github.com/telepresenceio/go-fuseftp/rpc v1.0.1 Apache License 2.0 github.com/telepresenceio/telepresence/cmd/cobraparser/v2 (modified) Apache License 2.0 github.com/telepresenceio/telepresence/rpc/v2 (modified) Apache License 2.0 github.com/vishvananda/netlink v1.3.1 Apache License 2.0 github.com/vishvananda/netns v0.0.5 Apache License 2.0 github.com/winfsp/cgofuse v1.6.0 MIT license github.com/x448/float16 v0.8.4 MIT license github.com/xhit/go-str2duration/v2 v2.1.0 3-clause BSD license github.com/xlab/treeprint v1.2.0 MIT license github.com/yosida95/uritemplate/v3 v3.0.2 3-clause BSD license go.opentelemetry.io/auto/sdk v1.2.1 Apache License 2.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 3-clause BSD license, Apache License 2.0 go.opentelemetry.io/otel v1.40.0 3-clause BSD license, Apache License 2.0 go.opentelemetry.io/otel/metric v1.40.0 3-clause BSD license, Apache License 2.0 go.opentelemetry.io/otel/trace v1.40.0 3-clause BSD license, Apache License 2.0 go.yaml.in/yaml/v2 v2.4.3 Apache License 2.0, MIT license go.yaml.in/yaml/v3 v3.0.4 Apache License 2.0, MIT license go.yaml.in/yaml/v4 v4.0.0-rc.4 Apache License 2.0, MIT license golang.org/x/crypto v0.48.0 3-clause BSD license golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa 3-clause BSD license golang.org/x/mod v0.33.0 3-clause BSD license golang.org/x/net v0.51.0 3-clause BSD license golang.org/x/oauth2 v0.35.0 3-clause BSD license golang.org/x/sync v0.19.0 3-clause BSD license golang.org/x/sys v0.41.0 3-clause BSD license golang.org/x/term v0.40.0 3-clause BSD license golang.org/x/text v0.34.0 3-clause BSD license golang.org/x/time v0.14.0 3-clause BSD license golang.org/x/tools v0.42.0 3-clause BSD license golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 MIT license golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb MIT license golang.zx2c4.com/wireguard/windows v0.5.3 MIT license google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 Apache License 2.0 google.golang.org/grpc v1.79.1 Apache License 2.0 google.golang.org/protobuf v1.36.11 3-clause BSD license gopkg.in/evanphx/json-patch.v4 v4.13.0 3-clause BSD license gopkg.in/inf.v0 v0.9.1 3-clause BSD license gopkg.in/yaml.v3 v3.0.1 Apache License 2.0, MIT license gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 3-clause BSD license, Apache License 2.0, MIT license helm.sh/helm/v3 v3.20.0 Apache License 2.0 k8s.io/api v0.35.2 Apache License 2.0 k8s.io/apiextensions-apiserver v0.35.2 Apache License 2.0 k8s.io/apimachinery v0.35.2 3-clause BSD license, Apache License 2.0 k8s.io/apiserver v0.35.2 Apache License 2.0 k8s.io/cli-runtime v0.35.2 Apache License 2.0 k8s.io/client-go v0.35.2 3-clause BSD license, Apache License 2.0 k8s.io/component-base v0.35.2 Apache License 2.0 k8s.io/component-helpers v0.35.2 Apache License 2.0 k8s.io/klog/v2 v2.130.1 Apache License 2.0 k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 3-clause BSD license, Apache License 2.0 k8s.io/kubectl v0.35.2 Apache License 2.0 k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 3-clause BSD license, Apache License 2.0 oras.land/oras-go/v2 v2.6.0 Apache License 2.0 sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 3-clause BSD license, Apache License 2.0 sigs.k8s.io/kustomize/api v0.21.1 Apache License 2.0 sigs.k8s.io/kustomize/kyaml v0.21.1 Apache License 2.0 sigs.k8s.io/randfill v1.0.0 Apache License 2.0 sigs.k8s.io/structured-merge-diff/v6 v6.3.2 Apache License 2.0 sigs.k8s.io/yaml v1.6.0 3-clause BSD license, Apache License 2.0, MIT license ================================================ FILE: DEPENDENCY_LICENSES.md ================================================ Telepresence CLI incorporates Free and Open Source software under the following licenses: * [2-clause BSD license](https://opensource.org/licenses/BSD-2-Clause) * [3-clause BSD license](https://opensource.org/licenses/BSD-3-Clause) * [Apache License 2.0](https://opensource.org/licenses/Apache-2.0) * [ISC license](https://opensource.org/licenses/ISC) * [MIT No Attribution license](https://spdx.org/licenses/MIT-0.html) * [MIT license](https://opensource.org/licenses/MIT) * [Mozilla Public License 2.0](https://opensource.org/licenses/MPL-2.0) ================================================ FILE: GOVERNANCE-maintainer.md ================================================ # Telepresence Project Governance The goal of the Telepresence project is to accelerate the "developer inner loop" for cloud-native application development on Kubernetes. Telepresence achieves this by creating a dynamic network bridge between a local development environment (e.g., a laptop) and a remote Kubernetes cluster. This governance explains how the project is run. - [Values](#values) - [Maintainers](#maintainers) - [Becoming a Maintainer](#becoming-a-maintainer) - [Meetings](#meetings) - [CNCF Resources](#cncf-resources) - [Code of Conduct Enforcement](#code-of-conduct) - [Security Response Team](#security-response-team) - [Voting](#voting) - [Modifications](#modifying-this-charter) ## Values The Telepresence project and its leadership embrace the following values: * Openness: Communication and decision-making happens in the open and is discoverable for future reference. As much as possible, all discussions and work take place in public forums and open repositories. * Fairness: All stakeholders have the opportunity to provide feedback and submit contributions, which will be considered on their merits. * Community over Product or Company: Sustaining and growing our community takes priority over shipping code or sponsors' organizational goals. Each contributor participates in the project as an individual. * Inclusivity: We innovate through different perspectives and skill sets, which can only be accomplished in a welcoming and respectful environment. * Participation: Responsibilities within the project are earned through participation, and there is a clear path up the contributor ladder into leadership positions. ## Maintainers Telepresence Maintainers have write access to the [project GitHub repository](https://github.com/telepresenceio/telepresence). They can merge their own patches or patches from others. The current maintainers can be found in [MAINTAINERS.md](./MAINTAINERS.md). Maintainers collectively manage the project's resources and contributors. This privilege is granted with some expectation of responsibility: maintainers are people who care about the Telepresence project and want to help it grow and improve. A maintainer is not just someone who can make changes, but someone who has demonstrated their ability to collaborate with the team, get the most knowledgeable people to review code and docs, contribute high-quality code, and follow through to fix issues (in code or tests). A maintainer is a contributor to the project's success and a citizen helping the project succeed. The collective team of all Maintainers is known as the Maintainer Council, which is the governing body for the project. ### Maintainer responsibilities * Monitor email aliases. * Monitor Slack (delayed response is perfectly acceptable). * Triage GitHub issues and perform pull request reviews for other maintainers and the community. * Make sure that ongoing PRs are moving forward at the right pace or closing them. * In general continue to be willing to spend at least 20% of one's time working on Telepresence (~1 business day/week). ### Becoming a Maintainer * Express interest to the current maintainers (see [MAINTAINERS.md](MAINTAINERS.md)) that your organization is interested in becoming a maintainer. Becoming a maintainer generally means that you are going to be spending substantial time on Telepresence for the foreseeable future. * We will expect you to start contributing increasingly complicated PRs, under the guidance of the existing maintainers. * We may ask you to do some PRs from our backlog. * As you gain experience with the code base and our standards, we will ask you to do code reviews for incoming PRs. All maintainers are expected to shoulder a proportional share of community reviews. * After a period of approximately 2-3 months of working together and making sure we see eye to eye, the existing maintainers will confer and decide whether to grant maintainer status or not. We make no guarantees on the length of time this will take, but 2-3 months is the goal. A new Maintainer can apply by sending a message in our [OSS Slack workspace](https://communityinviter.com/apps/cloud-native/cncf), in the [#telepresence-oss](https://cloud-native.slack.com/archives/C06B36KJ85P) channel. A simple majority vote of existing Maintainers approves the application. Maintainers nominations will be evaluated without prejudice to employer or demographics. Maintainers who are selected will be granted the necessary GitHub rights. ### Removing a Maintainer Maintainers may resign at any time if they feel that they will not be able to continue fulfilling their project duties. Maintainers may also be removed after being inactive, failure to fulfill their Maintainer responsibilities, violating the Code of Conduct, or other reasons. Inactivity is defined as a period of very low or no activity in the project for a year or more, with no definite schedule to return to full Maintainer activity. A Maintainer may be removed at any time by a 2/3 vote of the remaining maintainers. Depending on the reason for removal, a Maintainer may be converted to Emeritus status. Emeritus Maintainers will still be consulted on some project matters, and can be rapidly returned to Maintainer status if their availability changes. ## Meetings Maintainers meet on demand to discuss project matters, security reports, or Code of Conduct violations. Any Maintainer may schedule a meeting as needed. All current Maintainers must be invited, except for any Maintainer who is accused of a CoC violation. ## CNCF Resources Any Maintainer may suggest a request for CNCF resources, either in the [#telepresence-dev](https://datawire-oss.slack.com/archives/CC5D1UTTN) in slack, or during a meeting. A simple majority of Maintainers approves the request. The Maintainers may also choose to delegate working with the CNCF to non-Maintainer community members, who will then be added to the [CNCF's Maintainer List](https://github.com/cncf/foundation/blob/main/project-maintainers.csv) for that purpose. ## Code of Conduct [Code of Conduct](./code-of-conduct.md) violations by community members will be discussed and resolved on the [private slack channel](https://datawire-oss.slack.com/archives/C061Q45SU4F). If a Maintainer is directly involved in the report, the Maintainers will instead designate two Maintainers to work with the CNCF Code of Conduct Committee in resolving it. ## Security Response Team The Maintainers will appoint a Security Response Team to handle security reports. This committee may simply consist of the Maintainer Council themselves. If this responsibility is delegated, the Maintainers will appoint a team of at least two contributors to handle it. The Maintainers will review who is assigned to this at least once a year. The Security Response Team is responsible for handling all reports of security holes and breaches according to the [security policy](./SECURITY.md). ## Voting In general, we prefer that technical issues and maintainer membership are amicably worked out between the persons involved. If a dispute cannot be decided independently, the maintainers can be called in to decide an issue. If the maintainers themselves cannot decide an issue, the issue will be resolved by voting. The voting process is a simple majority in which each maintainer receives one vote. A vote can be taken on [#telepresence-dev](https://datawire-oss.slack.com/archives/CC5D1UTTN) or [#telepresence-dev-private](https://datawire-oss.slack.com/archives/C061Q45SU4F) for security or conduct matters. Any Maintainer may demand a vote be taken. Most votes require a simple majority of all Maintainers to succeed, except where otherwise noted. Two-thirds majority votes mean at least two-thirds of all existing maintainers. ## Modifying this Charter Changes to this Governance and its supporting documents may be approved by a 2/3 vote of the Maintainers. ## Adding new projects to the Telepresence GitHub organization New projects will be added to the Telepresence organization via GitHub issue discussion in one of the existing projects in the organization. Once sufficient discussion has taken place (~3-5 business days but depending on the volume of conversation), the maintainers of *the project where the issue was opened* (since different projects in the organization may have different maintainers) will decide whether the new project should be added. See the section above on voting if the maintainers cannot easily decide. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: MAINTAINERS.md ================================================ # Telepresence Maintainers [GOVERNANCE.md](GOVERNANCE.md) describes governance guidelines and maintainer responsibilities. ## Maintainers Maintainers are listed in alphabetical order. | Maintainer | GitHub ID | Affiliation | |-----------------|-------------------------------------------|-------------| | Blazej Gruszka | [bgruszka](https://github.com/bgruszka) | Displate | | Nick Powell | [njayp](https://github.com/njayp) | | | Thomas Hallgren | [thallgren](https://github.com/thallgren) | | ## Maintainers Emeriti * Abhay Saxena ([ark3](https://github.com/ark3)) * Donny Yung ([donnyyung](https://github.com/donnyyung)) * Guillaume Veschambre ([shepz](https://github.com/shepz)) * Jakub Rożek [P0lip](https://github.com/P0lip) * Jose Cortes ([josecv](https://github.com/josecv)) * Kévin Lambert ([knlambert](https://github.com/knlambert)) * Luke Shumaker ([LukeShu](https://github.com/LukeShu)) * Rafael Schloming ([rhs](https://github.com/rhs)) * Raphael Reyna ([raphaelreyna](https://github.com/raphaelreyna)) * Richard Li ([richarddli](https://github.com/richarddli)) * Sarabraj Singh ([sarabrajsingh](https://github.com/sarabrajsingh)) ================================================ FILE: Makefile ================================================ # Copyright 2020-2021 Datawire. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. TELEPRESENCE_REGISTRY ?= ghcr.io/telepresenceio TELEPRESENCE_VERSION ?= $(shell unset GOOS GOARCH; go run ./build-aux/genversion) # Ensure that the variable is fully expanded. We don't want to call genversion repeatedly # as it may produce different results every time. TELEPRESENCE_VERSION := ${TELEPRESENCE_VERSION} SHELL:=$(shell which bash) $(if $(filter v2.%,$(TELEPRESENCE_VERSION)),\ $(info [make] TELEPRESENCE_VERSION=$(TELEPRESENCE_VERSION)),\ $(error TELEPRESENCE_VERSION variable is invalid: It must be a v2.* string, but is '$(TELEPRESENCE_VERSION)')) export TELEPRESENCE_VERSION .DEFAULT_GOAL = help include build-aux/prelude.mk include build-aux/tools.mk include build-aux/main.mk include build-aux/maintenance.mk ================================================ FILE: README.md ================================================ # Telepresence: Fast, Local Development for Kubernetes [](https://telepresence.io) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/telepresence-oss)](https://artifacthub.io/packages/search?repo=telepresence-oss) [![Gurubase](https://img.shields.io/badge/Gurubase-Ask%20Telepresence%20Guru-006BFF)](https://gurubase.io/g/telepresence) Telepresence is a [CNCF](https://www.cncf.io/) project that connects your local workstation to a remote Kubernetes cluster, letting you run services locally while accessing cluster resources. It enables fast local development without the container build/push/deploy cycle. ## Key Features - **Local Development** - Run services on your workstation using your favorite IDE, debugger, and tools - **Cluster Access** - Your local machine can reach cluster services as if it were inside the cluster - **Traffic Interception** - Redirect traffic from cluster services to your local machine for debugging - **Fast Iteration** - No waiting for container builds or deployments ## Getting Started - [Quick Start Guide](https://telepresence.io/docs/quick-start) - Get up and running in minutes - [Installation](https://telepresence.io/docs/install/client) - Install the Telepresence client - [Documentation](https://telepresence.io/docs/) - Full documentation ## How It Works When Telepresence connects to a cluster, it creates a virtual network interface on your workstation and routes traffic through a Traffic Manager deployed in the cluster. This allows your local services to communicate with cluster resources and optionally intercept traffic destined for cluster workloads. ## Community - [CNCF Slack](https://communityinviter.com/apps/cloud-native/cncf) - Join [#telepresence-oss](https://cloud-native.slack.com/archives/C06B36KJ85P) - [Troubleshooting](https://telepresence.io/docs/troubleshooting/) - Common issues and solutions ## Contributing See [CLAUDE.md](CLAUDE.md) for build instructions, architecture overview, and development guidelines. ## License Telepresence is licensed under the [Apache License 2.0](LICENSE). ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions Security updates will be provided for the latest 2.x release. ### How do we handle vulnerabilities #### User reports If you discover any security vulnerabilities, please follow these guidelines: - Email your findings to [secalert@tada.se](secalert@tada.se). - Provide sufficient details, including steps to reproduce the vulnerability. - Do not publicly disclose the issue until we have had a chance to address it. #### Dependabot We run dependabot against our repo. We also have it create PRs with the updates. One of the maintainers responsibilities is to review these PRs, make any necessary updates, and merge them in so that they go out in our next set of releases. #### Keeping Go updated We're set up to receive embargoed security announcements for Golang. When it happens, we create a new security incident, evaluate if we're impacted, and release a hotfix as soon as possible. ================================================ FILE: build-aux/INSTALLERS.md ================================================ # Telepresence Installers Specification This document describes the platform-specific installers that bundle Telepresence with a system service for the root daemon. ## Overview Telepresence provides two installation methods: 1. **Standalone binaries** - Manual installation, root daemon runs on-demand with elevated privileges 2. **Platform installers** - Include root daemon as a system service, eliminating repeated privilege elevation All installers are built and published via `.github/workflows/release.yaml`. ## Windows Installer (WiX) **Location:** `build-aux/wix-installer/` **Output:** `telepresence-windows-amd64-setup.exe` (WiX bundle/bootstrapper) **Architecture:** amd64 only (WinFSP and SSHFS-Win installers lack arm64 versions) ### Key Files - `Makefile` - Build orchestration, downloads binary if not present locally - `TeleProduct.wxs` - MSI product definition - `TeleBundle.wxs` - Bundle/bootstrapper definition - `variables.wxi` - Version and path variables ### CI Build Steps ```yaml - name: Install WiX Toolset run: | dotnet tool install --global wix wix extension add -g WixToolset.BootstrapperApplications.wixext wix extension add -g WixToolset.UI.wixext wix extension add -g WixToolset.Util.wixext - name: Build WiX Installer run: make bundle ARCH=amd64 ``` ### Service Details The Windows installer registers Telepresence and configures PATH. The root daemon service management is handled differently on Windows (not a persistent service like Unix platforms). --- ## macOS Installer (pkg) **Location:** `build-aux/pkg-installer/` **Output:** `telepresence-darwin-{amd64,arm64}.pkg` **Architecture:** amd64 and arm64 ### Key Files - `build-pkg.sh` - Build script using `pkgbuild` and `productbuild` - `io.telepresence.rootd.plist` - launchd daemon configuration - `scripts/postinstall` - Post-installation script - `scripts/preinstall` - Pre-installation script - `distribution.xml` - Installer UI/flow definition ### CI Build Steps ```yaml - name: Build macOS Installer run: | cd build-aux/pkg-installer VERSION="${TELEPRESENCE_VERSION#v}" ARCH=${{ matrix.arch }} ./build-pkg.sh ``` ### launchd Service (`io.telepresence.rootd.plist`) ```xml ProgramArguments /usr/local/bin/telepresence rootd --logfile std --config /Library/Application Support/telepresence/config.yml --address :4037 --managed RunAtLoad KeepAlive ``` **Log location:** `/var/log/telepresence-rootd.log` **Working directory:** `/Library/Caches/telepresence` --- ## Linux Installer (nfpm) **Location:** `build-aux/systemd-installer/` **Output:** - `telepresence-{version}-linux-{amd64,arm64}.deb` (Debian/Ubuntu) - `telepresence-{version}-linux-{amd64,arm64}.rpm` (Fedora/RHEL) **Architecture:** amd64 and arm64 ### Key Files - `build-packages.sh` - Build script, handles version parsing and nfpm invocation - `nfpm.yaml.in` - nfpm configuration template (uses envsubst for variable expansion) - `telepresence-rootd.service` - systemd unit file - `postinstall.sh` - Post-installation script (creates directories, enables service) - `preremove.sh` - Pre-removal script (stops and disables service) - `postremove.sh` - Post-removal script (cleans up directories) ### CI Build Steps ```yaml - name: Install nfpm run: | curl -sfL "https://github.com/goreleaser/nfpm/releases/download/v2.44.1/nfpm_2.44.1_Linux_x86_64.tar.gz" | tar xz -C /tmp sudo mv /tmp/nfpm /usr/local/bin/nfpm - name: Build Linux Packages run: | cd build-aux/systemd-installer VERSION="${TELEPRESENCE_VERSION#v}" ARCH=${{ matrix.arch }} ./build-packages.sh ``` ### Version Handling RPM doesn't allow hyphens in version numbers. The build script splits versions: - `2.27.0` → version=`2.27.0`, release=`1` - `2.27.0-rc.1` → version=`2.27.0`, release=`rc.1` - `2.27.0-test.5` → version=`2.27.0`, release=`test.5` ### nfpm.yaml.in Template ```yaml name: telepresence arch: ${ARCH} platform: linux version: ${PKG_VERSION} release: ${PKG_RELEASE} maintainer: Telepresence Maintainers description: Fast local Kubernetes development vendor: Ambassador Labs homepage: https://www.telepresence.io/ license: Apache-2.0 contents: - src: ${BINDIR}/telepresence dst: /usr/local/bin/telepresence - src: telepresence-rootd.service dst: /usr/lib/systemd/system/telepresence-rootd.service scripts: postinstall: postinstall.sh preremove: preremove.sh postremove: postremove.sh ``` ### systemd Service (`telepresence-rootd.service`) ```ini [Service] Type=simple ExecStart=/usr/local/bin/telepresence rootd \ --logfile managed \ --config /etc/telepresence/config.yml \ --address :4037 \ --managed User=root Group=root WorkingDirectory=/var/cache/telepresence RuntimeDirectory=telepresence Restart=always RestartSec=3 # Logging StandardOutput=append:/var/log/telepresence/rootd.log StandardError=append:/var/log/telepresence/rootd.log # Security hardening NoNewPrivileges=yes ProtectSystem=strict ReadWritePaths=/var/cache/telepresence /var/log/telepresence ProtectHome=read-only PrivateTmp=yes ``` ### Directory Structure | Path | Permissions | Purpose | |------|-------------|---------| | `/var/cache/telepresence` | 755 | Working directory | | `/var/cache/telepresence/rootd` | 755 | Root daemon cache | | `/var/log/telepresence` | 755 | Log directory | | `/etc/telepresence` | 755 | Configuration | ### Post-install Behavior - Creates required directories with 755 permissions - Runs `systemctl daemon-reload` - Enables service (`systemctl enable telepresence-rootd`) - Does NOT start service (user decides when to start) --- ## Root Daemon Communication All platforms use TCP for client-daemon communication: - **Address:** `:4037` (localhost port 4037) - **Flag:** `--address :4037` The `--managed` flag indicates the daemon is running as a system service. --- ## Release Notes Structure The GitHub release body separates installer types: **Installers (with root daemon as a system service):** - Linux .deb/.rpm - macOS .pkg - Windows setup.exe **Standalone Binaries:** - Linux/macOS executables - Windows .zip ================================================ FILE: build-aux/admission_controller_tls/main.go ================================================ package main import ( "bytes" cryptorand "crypto/rand" "crypto/rsa" "crypto/sha1" "crypto/x509" "crypto/x509/pkix" "encoding/base64" "encoding/pem" "fmt" "math/big" "os" "path/filepath" "time" ) // The program creates the crt.pem, key.pem, and ca.pem needed when // setting up the mutator webhook for agent auto-injection. func main() { if len(os.Args) != 3 { fmt.Fprintf(os.Stderr, "usage: %s ", os.Args[0]) os.Exit(1) } if err := generateKeys(os.Args[1], os.Args[2]); err != nil { fmt.Fprintln(os.Stderr, err.Error()) os.Exit(1) } } func generateKeys(mgrNamespace, dir string) error { err := os.MkdirAll(dir, 0o777) if err != nil { return fmt.Errorf("failed to create directory %q: %w", dir, err) } crtPem, keyPem, caPem, err := generateKeyTriplet(mgrNamespace) if err != nil { return err } if err = writeFile(dir, "ca.pem", caPem); err != nil { return err } if err = writeFile(dir, "crt.pem", crtPem); err != nil { return err } return writeFile(dir, "key.pem", keyPem) } // writeFile writes the file verbatim and as base64 encoded in the given directory. func writeFile(dir, file string, data []byte) error { filePath := filepath.Join(dir, file) f, err := os.Create(filePath) if err != nil { return fmt.Errorf("failed to create file %q, %w", filePath, err) } defer f.Close() if _, err = f.Write(data); err != nil { return fmt.Errorf("failed to write to file %q, %w", filePath, err) } filePath64 := filePath + ".base64" f64, err := os.Create(filePath64) if err != nil { return fmt.Errorf("failed to create file %q, %w", filePath64, err) } defer f64.Close() enc := base64.NewEncoder(base64.StdEncoding, f64) defer enc.Close() if _, err = enc.Write(data); err != nil { return fmt.Errorf("failed to write to file %q, %w", filePath64, err) } return nil } // generateKeyTriplet creates the crt.pem, key.pem, and ca.pem needed when // setting up the mutator webhook for agent auto-injection. func generateKeyTriplet(mgrNamespace string) (crtPem, keyPem, caPem []byte, err error) { caCert := &x509.Certificate{ SerialNumber: big.NewInt(0xefecab0), Subject: pkix.Name{ Organization: []string{"telepresence.io"}, }, NotBefore: time.Now(), NotAfter: time.Now().AddDate(1, 0, 0), IsCA: true, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, BasicConstraintsValid: true, } caPrivKey, err := rsa.GenerateKey(cryptorand.Reader, 4096) if err != nil { return nil, nil, nil, fmt.Errorf("failed to generate CA private key: %w", err) } caBytes, err := x509.CreateCertificate(cryptorand.Reader, caCert, caCert, &caPrivKey.PublicKey, caPrivKey) if err != nil { return nil, nil, nil, fmt.Errorf("failed to generate CA certificate: %w", err) } if caPem, err = ToPEM("ca.pem", "CERTIFICATE", caBytes); err != nil { return nil, nil, nil, err } commonName := fmt.Sprintf("agent-injector.%s.svc", mgrNamespace) dnsNames := []string{"agent-injector", "agent-injector." + mgrNamespace, commonName} // server cert config cert := &x509.Certificate{ DNSNames: dnsNames, SerialNumber: big.NewInt(0xefecab1), Subject: pkix.Name{ CommonName: commonName, Organization: []string{"telepresence.io"}, }, NotBefore: time.Now(), NotAfter: time.Now().AddDate(10, 0, 0), // Valid 10 years SubjectKeyId: bigIntHash(caPrivKey.N), ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, KeyUsage: x509.KeyUsageDigitalSignature, } serverPrivateKey, err := rsa.GenerateKey(cryptorand.Reader, 4096) if err != nil { return nil, nil, nil, fmt.Errorf("failed to server private key: %w", err) } serverCert, err := x509.CreateCertificate(cryptorand.Reader, cert, caCert, &serverPrivateKey.PublicKey, caPrivKey) if err != nil { return nil, nil, nil, fmt.Errorf("failed to sign the server certificate: %w", err) } if keyPem, err = ToPEM("key.pem", "RSA PRIVATE KEY", x509.MarshalPKCS1PrivateKey(serverPrivateKey)); err != nil { return nil, nil, nil, err } if crtPem, err = ToPEM("crt.pem", "CERTIFICATE", serverCert); err != nil { return nil, nil, nil, err } return crtPem, keyPem, caPem, nil } func bigIntHash(n *big.Int) []byte { h := sha1.New() _, _ = h.Write(n.Bytes()) return h.Sum(nil) } // ToPEM returns the PEM encoding of data. func ToPEM(file, keyType string, data []byte) ([]byte, error) { wrt := bytes.Buffer{} if err := pem.Encode(&wrt, &pem.Block{Type: keyType, Bytes: data}); err != nil { return nil, fmt.Errorf("failed to PEM encode %s %s: %w", keyType, file, err) } return wrt.Bytes(), nil } ================================================ FILE: build-aux/docker/images/Dockerfile.client ================================================ # syntax = docker/dockerfile:1.3 # Copyright 2020-2022 Datawire. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. FROM --platform=$BUILDPLATFORM golang:alpine as telepresence-build RUN apk add --no-cache gcc musl-dev fuse-dev libcap binutils-gold WORKDIR telepresence COPY go.mod . COPY go.sum . COPY cmd/cobraparser/ cmd/cobraparser/ COPY cmd/telepresence/ cmd/telepresence/ COPY pkg/ pkg/ COPY rpc/ rpc/ COPY charts/ charts/ COPY build-output/version.txt . COPY build-output/helm-version.txt . ARG TARGETOS ARG TARGETARCH RUN \ --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg/mod \ GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /usr/local/bin/ -trimpath -tags docker -ldflags="-s -w -X=$(go list ./pkg/version).Version=$(cat version.txt) -X=$(go list ./pkg/version).HelmVersion=$(cat helm-version.txt)" ./cmd/telepresence/... # setcap is necessary because the process will listen to privileged ports RUN setcap 'cap_net_bind_service+ep' /usr/local/bin/telepresence # The telepresence target is the one that gets published. It aims to be a small as possible. FROM alpine as telepresence RUN apk add --no-cache ca-certificates iptables # the telepresence binary COPY --from=telepresence-build /usr/local/bin/telepresence /usr/local/bin ENTRYPOINT ["telepresence"] CMD [] ================================================ FILE: build-aux/docker/images/Dockerfile.routecontroller ================================================ # syntax = docker/dockerfile:1.3 # Copyright 2024 Datawire. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. FROM --platform=$BUILDPLATFORM golang:alpine AS routecontroller-build WORKDIR telepresence COPY go.mod go.sum . COPY cmd/routecontroller/ cmd/routecontroller/ COPY pkg/ pkg/ COPY build-output/version.txt . ARG TARGETOS ARG TARGETARCH RUN \ --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg/mod \ GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /usr/local/bin/route-controller -trimpath -ldflags="-s -w" ./cmd/routecontroller/ FROM alpine AS routecontroller RUN apk add --no-cache ca-certificates iptables ip6tables COPY --from=routecontroller-build /usr/local/bin/route-controller /usr/local/bin/route-controller ENTRYPOINT ["/usr/local/bin/route-controller"] CMD [] ================================================ FILE: build-aux/docker/images/Dockerfile.traffic ================================================ # syntax = docker/dockerfile:1.3 # Copyright 2020-2022 Datawire. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. FROM --platform=$BUILDPLATFORM golang:alpine as tel2-build RUN apk add --no-cache gcc musl-dev fuse-dev libcap binutils-gold WORKDIR telepresence COPY go.mod go.sum . COPY cmd/ cmd/ COPY pkg/ pkg/ COPY rpc/ rpc/ COPY charts/ charts/ COPY build-output/version.txt . ARG TARGETOS ARG TARGETARCH RUN \ --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg/mod \ GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /usr/local/bin/ -trimpath -ldflags="-s -w -X=$(go list ./pkg/version).Version=$(cat version.txt)" ./cmd/traffic/... # The tel2 target is the one that gets published. It aims to be a small as possible. FROM alpine as tel2 RUN apk add --no-cache ca-certificates iptables # the traffic binary COPY --from=tel2-build /usr/local/bin/traffic /usr/local/bin RUN \ mkdir /tel_app_mounts && \ chgrp -R 0 /tel_app_mounts && \ chmod -R g=u /tel_app_mounts && \ mkdir -p /home/telepresence && \ chgrp -R 0 /home/telepresence && \ chmod -R g=u /home/telepresence && \ chmod 0777 /home/telepresence ENTRYPOINT ["traffic"] CMD [] ================================================ FILE: build-aux/docker/images/Dockerfile.winbuild ================================================ FROM golang:1.19.4 RUN apt-get update && \ apt-get install -y \ apt-transport-https \ ca-certificates \ curl \ gnupg \ lsb-release && \ curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && \ echo \ "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian \ $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null RUN apt-get update && \ apt-get install -y docker-ce docker-ce-cli containerd.io ================================================ FILE: build-aux/genversion/main.go ================================================ // Copyright 2021 Datawire. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "crypto/md5" "encoding/base64" "fmt" "io/fs" "os" //nolint:depguard // This short script has no logging and no Contexts. "os/exec" "path/filepath" "strings" "time" "github.com/blang/semver/v4" ignore "github.com/sabhiram/go-gitignore" "sigs.k8s.io/yaml" ) // isReleased returns true if a release tag exist for the given version // A release tag is a tag that represents a semver version, without pre-version // or build suffixes, that is prefixed with "v", e.g. "v1.2.3" is considered // a release tag whereas "v1.2.3-rc.3" isn't. func isReleased(v semver.Version) bool { v.Build = nil v.Pre = nil return exec.Command("git", "describe", "v"+v.String()).Run() == nil } // dirMD5 computes the MD5 checksum of all files found when recursively // traversing a directory, skipping .gitignore's, _test/, and _test.go. The // general idea is to avoid rebuilds and pushes when repeatedly running tests, // even if the tests themselves actually change. func dirMD5(root string) ([]byte, error) { ign, err := ignore.CompileIgnoreFile(filepath.Join(root, ".gitignore")) if err != nil { return nil, err } d := md5.New() testMach := fmt.Sprintf("_test%c", filepath.Separator) isTest := func(path string) bool { return strings.Contains(path, testMach) || strings.HasSuffix(path, "_test.go") } err = filepath.Walk(root, func(path string, info fs.FileInfo, err error) error { if err == nil && info.Mode().IsRegular() && !(ign.MatchesPath(path) || isTest(path)) { var data []byte if data, err = os.ReadFile(path); err == nil { d.Write(data) } } return err }) if err != nil { return nil, err } return d.Sum(make([]byte, 0, md5.Size)), nil } func getLatestChangelogVersion() (v semver.Version, released bool, err error) { data, err := os.ReadFile("CHANGELOG.yml") if err != nil { return v, false, err } notes := struct { Items []map[string]any `json:"items"` }{} if err = yaml.Unmarshal(data, ¬es); err != nil { return v, false, err } if len(notes.Items) == 0 { return v, false, fmt.Errorf("no versions in CHANGELOG.yml") } top := notes.Items[0] vs, ok := top["version"].(string) if !ok { return v, false, fmt.Errorf("no version in top entry ofCHANGELOG.yml") } v, err = semver.Parse(vs) if err != nil { return v, false, err } if date, ok := top["date"].(string); ok { if _, dateErr := time.Parse("2006-01-02", date); dateErr == nil { released = true } } return v, released, nil } func Main() error { cmd := exec.Command("git", "describe", "--tags", "--match=v*") cmd.Stderr = os.Stderr gitDescBytes, err := cmd.Output() if err != nil { return fmt.Errorf("unable to git describe: %w", err) } gitDescStr := strings.TrimSuffix(strings.TrimPrefix(string(gitDescBytes), "v"), "\n") gitDescVer, err := semver.Parse(gitDescStr) if err != nil { return fmt.Errorf("unable to parse semver %s: %w", gitDescStr, err) } clv, released, err := getLatestChangelogVersion() if err == nil { gitDescVer.Major = clv.Major gitDescVer.Minor = clv.Minor gitDescVer.Patch = clv.Patch } else { released = isReleased(gitDescVer) } // Bump to next patch version only if the version has been released. if released { gitDescVer.Patch++ } out := os.Stdout // If an additional arg has been used, we include it in the tag if len(os.Args) >= 2 { // gitDescVer.Pre[0] contains the number of commits since the last tag and the // shortHash with a 'g' appended. Since the first section isn't relevant, // we get the shortHash this way since we don't need that extra information. cmd = exec.Command("git", "rev-parse", "--short", "HEAD") cmd.Stderr = os.Stderr shortHash, err := cmd.Output() if err != nil { return fmt.Errorf("unable to git rev-parse: %w", err) } if _, err = fmt.Fprintf(out, "v%d.%d.%d-%s-%s\n", gitDescVer.Major, gitDescVer.Minor, gitDescVer.Patch, os.Args[1], shortHash); err != nil { return fmt.Errorf("unable to printf: %w", err) } return nil } // Append a mangled md5 if the directory is dirty. cmd = exec.Command("git", "status", "--short") cmd.Stderr = os.Stderr statusBytes, err := cmd.Output() if err != nil { return fmt.Errorf("unable to git rev-parse: %w", err) } if len(statusBytes) > 0 { var md5Out []byte md5Out, err = dirMD5(".") if err != nil { return fmt.Errorf("unable to compute MD5: %w", err) } b64 := base64.RawURLEncoding.EncodeToString(md5Out) b64 = strings.ReplaceAll(b64, "_", "Z") b64 = strings.ReplaceAll(b64, "-", "z") _, err = fmt.Fprintf(out, "v%s-%s\n", gitDescVer, b64) } else { _, err = fmt.Fprintf(out, "v%s\n", gitDescVer) } return err } func main() { if err := Main(); err != nil { fmt.Fprintf(os.Stderr, "%s: error: %v\n", filepath.Base(os.Args[0]), err) os.Exit(1) } } ================================================ FILE: build-aux/image-importer.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: creationTimestamp: null labels: app: image-importer name: image-importer spec: replicas: 1 selector: matchLabels: app: image-importer strategy: {} template: metadata: creationTimestamp: null labels: app: image-importer spec: containers: - image: ubuntu name: ubuntu resources: {} command: - sleep - "10000000" volumeMounts: - mountPath: /run/k3s name: run - mountPath: /var/lib/rancher name: rancher - mountPath: /hostbin name: bin volumes: - name: run hostPath: path: /run/k3s - name: rancher hostPath: path: /var/lib/rancher - name: bin hostPath: path: /bin status: {} ================================================ FILE: build-aux/main.mk ================================================ # Copyright 2020-2021 Datawire. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # This file deals with the "main" flow of Make. The user-facing # targets, the generate/build/release cycle. Try to keep boilerplate # out of this file. Try to keep this file simple; anything complex or # clever should probably be factored into a separate file. # All build artifacts that are files end up in $(BUILDDIR). BUILDDIR=build-output BINDIR=$(BUILDDIR)/bin RELEASEDIR=$(BUILDDIR)/release bindir ?= $(or $(shell go env GOBIN),$(shell go env GOPATH|cut -d: -f1)/bin) # DOCKER_BUILDKIT is _required_ by our Dockerfile, since we use # Dockerfile extensions for the Go build cache. See # https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/syntax.md. export DOCKER_BUILDKIT := 1 .PHONY: FORCE FORCE: EXTERNAL_FUSEFTP ?= 0 LINKED_FUSEFTP ?= 1 # Build with CGO_ENABLED=0 on all platforms to ensure that the binary is as # portable as possible, but we must make an exception for darwin, because # the Go implementation of the DNS resolver doesn't work properly there unless # it's using clib ifeq ($(GOOS),darwin) CGO_ENABLED ?= 1 else ifeq ($(GOOS),linux) # The winfsp module requires CGO on Linux. CGO_ENABLED ?= $(LINKED_FUSEFTP) else CGO_ENABLED ?= 0 endif endif ifeq ($(GOOS),windows) BEXE=.exe BZIP=.zip else BEXE= BZIP= endif # Generate: artifacts that get checked in to Git # ============================================== $(BUILDDIR)/go1%.src.tar.gz: mkdir -p $(BUILDDIR) curl -o $@ --fail -L https://dl.google.com/go/$(@F) .PHONY: clean clean: rm -rf $(BUILDDIR) .PHONY: protoc-clean protoc-clean: find ./rpc -name '*.go' -delete .PHONY: protoc protoc: protoc-clean $(tools/protoc) $(tools/protoc-gen-go) $(tools/protoc-gen-go-grpc) $(tools/protoc) \ -I rpc \ \ --go_out=./rpc \ --go_opt=module=github.com/telepresenceio/telepresence/rpc/v2 \ \ --go-grpc_out=./rpc \ --go-grpc_opt=module=github.com/telepresenceio/telepresence/rpc/v2 \ \ --proto_path=. \ $$(find ./rpc/ -name '*.proto') .PHONY: generate generate: ## (Generate) Update generated files that get checked in to Git generate: generate-clean generate: protoc $(tools/go-mkopensource) $(BUILDDIR)/$(shell go env GOVERSION | grep -oE '^go[0-9]+\.[0-9]+\.[0-9]+').src.tar.gz cd ./rpc && export GOFLAGS=-mod=mod && go mod tidy && go mod vendor && rm -rf vendor cd ./pkg/vif/testdata/router && export GOFLAGS=-mod=mod && go mod tidy && go mod vendor && rm -rf vendor cd ./tools/src/test-report && export GOFLAGS=-mod=mod && go mod tidy && go mod vendor && rm -rf vendor cd ./cmd/teleroute && $(MAKE) rpc/teleroute/.rsync-stamp cd ./cmd/teleroute && go mod tidy cd ./cmd/teleroute/rpc && go mod tidy cd ./integration_test/testdata/echo-server && export GOFLAGS=-mod=mod && go mod tidy && go mod vendor && rm -rf vendor export GOFLAGS=-mod=mod && go mod tidy && go mod vendor mkdir -p $(BUILDDIR) $(tools/go-mkopensource) --gotar=$(filter %.src.tar.gz,$^) --ignore-dirty --output-format=txt --package=mod --application-type=external \ --unparsable-packages build-aux/unparsable-packages.yaml >$(BUILDDIR)/DEPENDENCIES.txt sed 's/\(^.*the Go language standard library ."std".[ ]*v[1-9]\.[0-9]*\)\..../\1 /' $(BUILDDIR)/DEPENDENCIES.txt >DEPENDENCIES.md printf "Telepresence CLI incorporates Free and Open Source software under the following licenses:\n\n" > DEPENDENCY_LICENSES.md $(tools/go-mkopensource) --gotar=$(filter %.src.tar.gz,$^) --ignore-dirty --output-format=txt --package=mod \ --output-type=json --application-type=external --unparsable-packages build-aux/unparsable-packages.yaml > $(BUILDDIR)/DEPENDENCIES.json jq -r '.licenseInfo | to_entries | .[] | "* [" + .key + "](" + .value + ")"' $(BUILDDIR)/DEPENDENCIES.json > $(BUILDDIR)/LICENSES.txt sed -e 's/\[\([^]]*\)]()/\1/' $(BUILDDIR)/LICENSES.txt >> DEPENDENCY_LICENSES.md rsync -vc DEPENDENCY_LICENSES.md docs/licenses.md rm -rf vendor # Build: artifacts that don't get checked in to Git # ================================================= TELEPRESENCE=$(BINDIR)/telepresence$(BEXE) generate: docs-files .PHONY: generate-clean generate-clean: ## (Generate) Delete generated files rm -rf ./rpc/vendor rm -rf ./vendor rm -f DEPENDENCIES.md rm -f DEPENDENCY_LICENSES.md rm -f docs/release-notes.md* rm -f docs/README.md rm -f docs/helm/values.schema.json CHANGELOG.yml: FORCE @# Check if the version is in the x.x.x format (GA release) if echo "$(TELEPRESENCE_VERSION)" | grep -qE 'v[0-9]+\.[0-9]+\.[0-9]+$$'; then \ echo $$file; \ sed -i.bak -r "s/date: (TBD|\(TBD\)|\"TBD\"|\"\(TBD\)\")$$/date: $$(date +'%Y-%m-%d')/" CHANGELOG.yml; \ rm -f CHANGELOG.yml.bak; \ git add CHANGELOG.yml; \ fi docs-files: docs/README.md docs/release-notes.md docs/release-notes.mdx docs/variables.yml docs/helm/values.schema.json docs/reference/cli/telepresence.md docs/reference/cli/telepresence.md: $(TELEPRESENCE) $(TELEPRESENCE) man-pages --dir $(@D) git add $(@D) docs/README.md: docs/doc-links.yml $(tools/tocgen) $(tools/tocgen) --input $< > $@ git add $@ docs/release-notes.md: CHANGELOG.yml $(tools/relnotesgen) $(tools/relnotesgen) --input $< > $@ git add $@ docs/release-notes.mdx: CHANGELOG.yml $(tools/relnotesgen) $(tools/relnotesgen) --mdx --input $< > $@ git add $@ docs/variables.yml: CHANGELOG.yml $(tools/relnotesgen) $(tools/relnotesgen) --variables --input $< > $@ git add $@ docs/helm/values.schema.json: charts/telepresence-oss/values.schema.yaml $(tools/y2j) mkdir -p $(@D) $(tools/y2j) < $< > $@ git add $@ PKG_VERSION = $(shell go list ./pkg/version) ifeq ($(GOOS),windows) TELEPRESENCE_INSTALLER=$(BINDIR)/telepresence$(BZIP) endif .PHONY: build build: $(TELEPRESENCE) ## (Build) Produce a `telepresence` binary for GOOS/GOARCH # We might be building for arm64 on a mac that doesn't have an M1 chip # (which is definitely the case with circle), so GOARCH may be set for that, # but we need to ensure it's using the host's architecture so the go command runs successfully. ifeq ($(GOHOSTOS),darwin) sdkroot=SDKROOT=$(shell xcrun --sdk macosx --show-sdk-path) else sdkroot= endif BUILD_TAGS= build-deps: ifeq ($(DOCKER_BUILD),1) BUILD_TAGS=-tags docker else ifeq ($(EXTERNAL_FUSEFTP),1) BUILD_TAGS=-tags external_fuseftp pkg/client/remotefs/fuseftp.bits: touch $@ else ifeq ($(LINKED_FUSEFTP),1) BUILD_TAGS=-tags linked_fuseftp pkg/client/remotefs/fuseftp.bits: touch $@ else FUSEFTP_VERSION=$(shell go list -m -f {{.Version}} github.com/telepresenceio/go-fuseftp/rpc) $(BUILDDIR)/fuseftp-$(GOOS)-$(GOARCH)$(BEXE): go.mod mkdir -p $(BUILDDIR) curl --fail -L https://github.com/telepresenceio/go-fuseftp/releases/download/$(FUSEFTP_VERSION)/fuseftp-$(GOOS)-$(GOARCH)$(BEXE) -o $@ pkg/client/remotefs/fuseftp.bits: $(BUILDDIR)/fuseftp-$(GOOS)-$(GOARCH)$(BEXE) FORCE cp $< $@ endif endif endif build-deps: pkg/client/remotefs/fuseftp.bits pkg/client/cli/docker/compose/dc-cli.json: go.mod go.mod cmd/cobraparser/main.go go mod tidy (cd cmd/cobraparser && go mod tidy) && GOOS= GOARCH= go run cmd/cobraparser/main.go docker compose > $@ build-deps: pkg/client/cli/docker/compose/dc-cli.json ifeq ($(GOHOSTOS),windows) WINTUN_VERSION=0.14.1 $(BUILDDIR)/wintun-$(WINTUN_VERSION)/wintun/bin/$(GOARCH)/wintun.dll: mkdir -p $(BUILDDIR) curl --fail -L https://www.wintun.net/builds/wintun-$(WINTUN_VERSION).zip -o $(BUILDDIR)/wintun-$(WINTUN_VERSION).zip rm -rf $(BUILDDIR)/wintun-$(WINTUN_VERSION) unzip $(BUILDDIR)/wintun-$(WINTUN_VERSION).zip -d $(BUILDDIR)/wintun-$(WINTUN_VERSION) $(BINDIR)/wintun.dll: $(BUILDDIR)/wintun-$(WINTUN_VERSION)/wintun/bin/$(GOARCH)/wintun.dll mkdir -p $(@D) cp $< $@ wintun.dll: $(BINDIR)/wintun.dll winfsp.msi: mkdir -p $(BUILDDIR) curl --fail -L https://github.com/winfsp/winfsp/releases/download/v1.11/winfsp-1.11.22176.msi -o $(BUILDDIR)/winfsp.msi sshfs-win.msi: mkdir -p $(BUILDDIR) curl --fail -L https://github.com/billziss-gh/sshfs-win/releases/download/v3.7.21011/sshfs-win-3.7.21011-x64.msi -o $(BUILDDIR)/sshfs-win.msi endif HELM_VERSION = $(shell go mod edit -json | jq -r '.Require[] | select(.Path == "helm.sh/helm/v3") | .Version') LDFLAGS := -X=$(PKG_VERSION).Version=$(TELEPRESENCE_VERSION) -X=$(PKG_VERSION).HelmVersion=$(HELM_VERSION) ifneq ($(DEBUG),1) # strip debug information and dwarf LDFLAGS := -s -w $(LDFLAGS) endif $(info LDFLAGS=$(LDFLAGS)) ifeq ($(GOOS),linux) ifeq ($(GOARCH),arm64) ifeq ($(CGO_ENABLED),1) BUILD_ENV := CC=aarch64-linux-gnu-gcc endif endif endif $(info BUILD_ENV=$(BUILD_ENV)) $(TELEPRESENCE): build-deps FORCE ifeq ($(GOHOSTOS),windows) $(TELEPRESENCE): build-deps $(BINDIR)/wintun.dll FORCE endif mkdir -p $(@D) ifeq ($(DOCKER_BUILD),1) CGO_ENABLED=$(CGO_ENABLED) $(sdkroot) go build $(BUILD_TAGS) -trimpath -ldflags="$(LDFLAGS)" -o $@ ./cmd/telepresence else # -buildmode=pie enables PIE compilation for binary harderning. Default on darwin and windows (since 1.23) but not in linux. $(BUILD_ENV) CGO_ENABLED=$(CGO_ENABLED) $(sdkroot) go build $(BUILD_TAGS) -buildmode=pie -trimpath -ldflags="$(LDFLAGS)" -o $@ ./cmd/telepresence endif ifeq ($(GOOS),windows) $(TELEPRESENCE_INSTALLER): $(TELEPRESENCE) bash ./packaging/windows-package.sh endif .PHONY: release-binary ifeq ($(GOOS),windows) release-binary: $(TELEPRESENCE_INSTALLER) mkdir -p $(RELEASEDIR) cp $(TELEPRESENCE_INSTALLER) $(RELEASEDIR)/telepresence-windows-$(GOARCH)$(BZIP) else release-binary: $(TELEPRESENCE) mkdir -p $(RELEASEDIR) cp $(TELEPRESENCE) $(RELEASEDIR)/telepresence-$(GOOS)-$(GOARCH)$(BEXE) endif .PHONY: setup-build-dir setup-build-dir: mkdir -p $(BUILDDIR) printf $(TELEPRESENCE_VERSION) > $(BUILDDIR)/version.txt ## Pass version in a file instead of a --build-arg to maximize cache usage printf $(HELM_VERSION) > $(BUILDDIR)/helm-version.txt TELEPRESENCE_SEMVER=$(patsubst v%,%,$(TELEPRESENCE_VERSION)) CLIENT_IMAGE_FQN=$(TELEPRESENCE_REGISTRY)/telepresence:$(TELEPRESENCE_SEMVER) TEL2_IMAGE_FQN=$(TELEPRESENCE_REGISTRY)/tel2:$(TELEPRESENCE_SEMVER) .PHONY: images-deps images-deps: build-deps setup-build-dir .PHONY: tel2-image tel2-image: images-deps $(eval PLATFORM_ARG := $(if $(TELEPRESENCE_TEL2_IMAGE_PLATFORM), --platform=$(TELEPRESENCE_TEL2_IMAGE_PLATFORM),)) docker build $(PLATFORM_ARG) --target tel2 --tag tel2 --tag $(TEL2_IMAGE_FQN) -f build-aux/docker/images/Dockerfile.traffic . .PHONY: client-image client-image: images-deps docker build --target telepresence --tag telepresence --tag $(CLIENT_IMAGE_FQN) -f build-aux/docker/images/Dockerfile.client . .PHONY: push-tel2-image push-tel2-image: tel2-image ## (Build) Push the manager/agent container image to $(TELEPRESENCE_REGISTRY) docker push $(TEL2_IMAGE_FQN) .PHONY: save-tel2-image save-tel2-image: tel2-image docker save $(TEL2_IMAGE_FQN) > $(BUILDDIR)/tel2-image.tar .PHONY: push-client-image push-client-image: client-image ## (Build) Push the client container image to $(TELEPRESENCE_REGISTRY) docker push $(CLIENT_IMAGE_FQN) ROUTECONTROLLER_IMAGE_FQN=$(TELEPRESENCE_REGISTRY)/route-controller:$(TELEPRESENCE_SEMVER) .PHONY: routecontroller-image routecontroller-image: images-deps ## (Build) Build the route-controller DaemonSet image $(eval PLATFORM_ARG := $(if $(TELEPRESENCE_ROUTECONTROLLER_IMAGE_PLATFORM), --platform=$(TELEPRESENCE_ROUTECONTROLLER_IMAGE_PLATFORM),)) docker build $(PLATFORM_ARG) --target routecontroller --tag route-controller --tag $(ROUTECONTROLLER_IMAGE_FQN) \ -f build-aux/docker/images/Dockerfile.routecontroller . .PHONY: push-routecontroller-image push-routecontroller-image: routecontroller-image ## (Build) Push the route-controller DaemonSet image to $(TELEPRESENCE_REGISTRY) docker push $(ROUTECONTROLLER_IMAGE_FQN) .PHONY: push-images push-images: push-tel2-image push-client-image push-routecontroller-image .PHONY: helm-chart helm-chart: $(BUILDDIR)/telepresence-oss-chart.tgz $(BUILDDIR)/telepresence-oss-chart.tgz: $(wildcard charts/**/*) mkdir -p $(BUILDDIR) go run packaging/helmpackage.go -o $@ -v $(TELEPRESENCE_SEMVER) .PHONY: clobber clobber: clobber-tools generate-clean ## (Build) Remove all build artifacts and tools rm -rf $(BUILDDIR) rm -rf cmd/teleroute/rpc rm -rf cmd/teleroute/build-output rm -f pkg/client/cli/docker/compose/dc-cli.json rm -f docs/helm/values.schema.json # Release: Push the artifacts places, update pointers ot them # =========================================================== .PHONY: prepare-release prepare-release: generate go mod edit -require=github.com/telepresenceio/telepresence/rpc/v2@$(TELEPRESENCE_VERSION) git add go.mod (cd pkg/vif/testdata/router && \ go mod edit -require=github.com/telepresenceio/telepresence/rpc/v2@$(TELEPRESENCE_VERSION) && \ git add go.mod) git commit --signoff --message='Prepare $(TELEPRESENCE_VERSION)' || true git tag --annotate --message='$(TELEPRESENCE_VERSION)' $(TELEPRESENCE_VERSION) git tag --annotate --message='$(TELEPRESENCE_VERSION)' rpc/$(TELEPRESENCE_VERSION) .PHONY: push-tags push-tags: git push origin $(TELEPRESENCE_VERSION) git push origin rpc/$(TELEPRESENCE_VERSION) # Prerequisites: # The awscli command must be installed and configured with credentials to upload # to the datawire-static-files bucket. .PHONY: push-executable push-executable: build ## (Release) Upload the executable to S3 ifeq ($(GOHOSTOS), windows) packaging/windows-package.sh AWS_PAGER="" aws s3api put-object \ --bucket datawire-static-files \ --key tel2-oss/$(GOHOSTOS)/$(GOARCH)/$(TELEPRESENCE_SEMVER)/telepresence.zip \ --body $(BINDIR)/telepresence.zip AWS_PAGER="" aws s3api put-object \ --region us-east-1 \ --bucket datawire-static-files \ --key tel2-oss/$(GOHOSTOS)/$(GOARCH)/$(TELEPRESENCE_SEMVER)/telepresence-setup.exe \ --body $(BINDIR)/telepresence-setup.exe else AWS_PAGER="" aws s3api put-object \ --bucket datawire-static-files \ --key tel2-oss/$(GOHOSTOS)/$(GOARCH)/$(TELEPRESENCE_SEMVER)/telepresence \ --body $(BINDIR)/telepresence endif # Prerequisites: # The awscli command must be installed and configured with credentials to upload # to the datawire-static-files bucket. .PHONY: promote-to-stable promote-to-stable: ## (Release) Update stable.txt in S3 mkdir -p $(BUILDDIR) echo $(TELEPRESENCE_SEMVER) > $(BUILDDIR)/stable.txt AWS_PAGER="" aws s3api put-object \ --bucket datawire-static-files \ --key tel2-oss/$(GOHOSTOS)/$(GOARCH)/stable.txt \ --body $(BUILDDIR)/stable.txt ifeq ($(GOHOSTOS), darwin) # Since the enterprise version is built from a different makefile, we only use the oss target here. Ref: https://github.com/telepresenceio/telepresence/pull/3626#issuecomment-2200150895 packaging/homebrew-package.sh $(TELEPRESENCE_SEMVER) endif # Prerequisites: # The awscli command must be installed and configured with credentials to upload # to the datawire-static-files bucket. .PHONY: promote-nightly promote-nightly: ## (Release) Update nightly.txt in S3 mkdir -p $(BUILDDIR) echo $(TELEPRESENCE_SEMVER) > $(BUILDDIR)/nightly.txt AWS_PAGER="" aws s3api put-object \ --bucket datawire-static-files \ --key tel2-oss/$(GOHOSTOS)/$(GOARCH)/nightly.txt \ --body $(BUILDDIR)/nightly.txt # Quality Assurance: Make sure things are good # ============================================ .PHONY: lint-deps lint-deps: build-deps ## (QA) Everything necessary to lint lint-deps: $(tools/protolint) ifneq ($(GOHOSTOS), windows) lint-deps: $(tools/shellcheck) endif .PHONY: build-tests build-tests: build-deps ## (Test) Build (but don't run) the test suite. Useful for pre-loading the Go build cache. go list ./... | xargs -n1 go test -c -o /dev/null shellscripts += ./packaging/homebrew-package.sh shellscripts += ./packaging/windows-package.sh .PHONY: lint lint-rpc lint-go lint: lint-rpc lint-go lint-go: lint-deps ## (QA) Run the golangci-lint ifeq ($(GOOS),windows) @ver=$$(curl -fsSL 'https://api.github.com/repos/golangci/golangci-lint/releases/latest' | grep -o '"tag_name": *"[^"]*"' | head -1 | cut -d'"' -f4) && \ docker run -e GOOS=$(GOOS) --rm -v $$(pwd):/app -v ~/.cache/golangci-lint/$$ver:/root/.cache -w /app golangci/golangci-lint:$$ver golangci-lint \ run --timeout 8m ./cmd/cobraparser/... ./cmd/telepresence/... ./integration_test/... ./pkg/... else @ver=$$(curl -fsSL 'https://api.github.com/repos/golangci/golangci-lint/releases/latest' | grep -o '"tag_name": *"[^"]*"' | head -1 | cut -d'"' -f4) && \ docker run -e GOOS=$(GOOS) --rm -v $$(pwd):/app -v ~/.cache/golangci-lint/$$ver:/root/.cache -w /app golangci/golangci-lint:$$ver golangci-lint \ run --timeout 8m ./... endif lint-rpc: lint-deps ## (QA) Run rpc linter $(tools/protolint) lint rpc ifneq ($(GOHOSTOS), windows) $(tools/shellcheck) $(shellscripts) endif .PHONY: format format: lint-deps ## (QA) Automatically fix linter complaints ifeq ($(GOHOSTOS),windows) @ver=$$(curl -fsSL 'https://api.github.com/repos/golangci/golangci-lint/releases/latest' | grep -o '"tag_name": *"[^"]*"' | head -1 | cut -d'"' -f4) && \ docker run -e GOOS=$(GOOS) --rm -v $$(pwd):/app -v ~/.cache/golangci-lint/$$ver:/root/.cache -w /app golangci/golangci-lint:$$ver golangci-lint \ run --timeout 8m --fix ./cmd/telepresence/... ./integration_test/... ./pkg/... else @ver=$$(curl -fsSL 'https://api.github.com/repos/golangci/golangci-lint/releases/latest' | grep -o '"tag_name": *"[^"]*"' | head -1 | cut -d'"' -f4) && \ docker run -e GOOS=$(GOOS) --rm -v $$(pwd):/app -v ~/.cache/golangci-lint/$$ver:/root/.cache -w /app golangci/golangci-lint:$$ver golangci-lint \ run --timeout 8m --fix ./... endif $(tools/protolint) lint --fix rpc || true .PHONY: check-all check-all: check-integration check-unit ## (QA) Run the test suite .PHONY: check-unit check-unit: build-deps $(tools/test-report) ## (QA) Run the test suite set -o pipefail ifeq ($(GOOS),linux) CGO_ENABLED=$(CGO_ENABLED) go test -json -failfast -timeout=20m ./cmd/... ./pkg/... | $(tools/test-report) else CGO_ENABLED=$(CGO_ENABLED) go test -json -failfast -timeout=20m ./pkg/... | $(tools/test-report) endif .PHONY: check-integration ifeq ($(GOHOSTOS), linux) check-integration: client-image $(tools/test-report) $(tools/helm) ## (QA) Run the test suite else check-integration: build-deps $(tools/test-report) $(tools/helm) ## (QA) Run the test suite endif # We run the test suite with TELEPRESENCE_LOGIN_DOMAIN set to localhost since that value # is only used for extensions. Therefore, we want to validate that our tests, and # telepresence, run without requiring any outside dependencies. set -o pipefail TELEPRESENCE_MAX_LOGFILES=300 TELEPRESENCE_LOGIN_DOMAIN=127.0.0.1 CGO_ENABLED=$(CGO_ENABLED) go test $(BUILD_TAGS) \ -count=1 -failfast -json -timeout=80m ./integration_test/... | $(tools/test-report) .PHONY: _login _login: docker login --username "$$TELEPRESENCE_REGISTRY_USERNAME" --password "$$TELEPRESENCE_REGISTRY_PASSWORD" # Install # ======= .PHONY: install install: build ## (Install) Installs the telepresence binary to $(bindir) install -Dm755 $(BINDIR)/telepresence $(bindir)/telepresence .PHONY: private-registry private-registry: $(tools/helm) ## (Test) Add a private docker registry to the current k8s cluster and make it available on localhost:5000. mkdir -p $(BUILDDIR) $(tools/helm) repo add twuni https://helm.twun.io $(tools/helm) repo update $(tools/helm) install docker-registry twuni/docker-registry kubectl apply -f k8s/private-reg-proxy.yaml kubectl rollout status -w daemonset/private-registry-proxy sleep 5 kubectl wait --for=condition=ready pod --all kubectl port-forward daemonset/private-registry-proxy 5000:5000 > /dev/null & # Aliases # ======= .PHONY: test save-image push-image test: check-all ## (ZAlias) Alias for 'check-all' save-image: save-tel2-image push-image: push-tel2-image .PHONY: push-test-images push-test-images: push-echo-server push-udp-echo .PHONY: push-echo-server push-echo-server: (cd integration_test/testdata/echo-server && \ docker buildx build --platform=linux/amd64,linux/arm64 --push \ --tag ghcr.io/telepresenceio/echo-server:latest \ --tag ghcr.io/telepresenceio/echo-server:0.3.1 .) .PHONY: push-udp-echo push-udp-echo: (cd integration_test/testdata/udp-echo && \ docker buildx build --platform=linux/amd64,linux/arm64 --push \ --tag ghcr.io/telepresenceio/udp-echo:latest \ --tag ghcr.io/telepresenceio/udp-echo:0.1.0 .) K8S_VERSION ?= $(shell go list -m k8s.io/client-go | awk '{print $$2}' | sed -e 's/v0./v1./') charts/telepresence-oss/k8s-defs.json: go.mod curl -o $@ --fail -L https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/$(K8S_VERSION)-standalone/_definitions.json build-deps: charts/telepresence-oss/k8s-defs.json ================================================ FILE: build-aux/maintenance.mk ================================================ update-dependencies: $(dir $(shell find . -name go.mod)) for dir in $?; do\ (cd $$dir && \ (go get -u ./... && \ (grep -q gvisor.dev go.mod && go get -u gvisor.dev/gvisor@go) \ ) || \ go get -u .);\ done $(MAKE) clobber generate check-unit ================================================ FILE: build-aux/pkg-installer/Distribution.xml ================================================ Telepresence __VERSION__ cli.pkg rootd.pkg ================================================ FILE: build-aux/pkg-installer/build-pkg.sh ================================================ #!/usr/bin/env bash set -e # Validate that VERSION is set and a valid SemVer [[ -z "${VERSION}" ]] && { echo "Error: VERSION required" >&2; exit 1; } if ! [[ "$VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-([0-9A-Za-z\.-]+))?(\+([0-9A-Za-z\.-]+))?$ ]]; then echo "Error: VERSION='$VERSION' is not a valid SemVer string." >&2 exit 1 fi # Only run on Darwin. Other OSes are unlikely to have the pkgbuild and productbuild commands if [[ "$(uname -s)" != "Darwin" ]]; then echo "Error: This script only supports macOS (Darwin)." >&2 exit 1 fi # === Signing and Notarization Configuration === # These environment variables enable code signing and notarization: # MACOS_SIGN_APPLICATION - Developer ID Application certificate name (for codesign) # MACOS_SIGN_INSTALLER - Developer ID Installer certificate name (for productsign) # MACOS_NOTARIZE_APPLE_ID - Apple ID for notarization # MACOS_NOTARIZE_TEAM_ID - Apple Developer Team ID # MACOS_NOTARIZE_PASSWORD - App-specific password for notarization # # If signing variables are not set, packages will be built unsigned (suitable for local development). sign_binary() { local binary="$1" if [[ -n "${MACOS_SIGN_APPLICATION}" ]]; then echo "Signing binary: $binary" codesign --force --options runtime --timestamp --sign "${MACOS_SIGN_APPLICATION}" "$binary" codesign --verify --verbose "$binary" fi } sign_package() { local unsigned_pkg="$1" local signed_pkg="$2" if [[ -n "${MACOS_SIGN_INSTALLER}" ]]; then echo "Signing package: $unsigned_pkg -> $signed_pkg" productsign --sign "${MACOS_SIGN_INSTALLER}" "$unsigned_pkg" "$signed_pkg" pkgutil --check-signature "$signed_pkg" else # No signing, just rename mv "$unsigned_pkg" "$signed_pkg" fi } notarize_package() { local pkg="$1" if [[ -n "${MACOS_NOTARIZE_APPLE_ID}" && -n "${MACOS_NOTARIZE_TEAM_ID}" && -n "${MACOS_NOTARIZE_PASSWORD}" ]]; then echo "Submitting package for notarization: $pkg" # Use --timeout to prevent indefinite waiting (15 minutes should be plenty) xcrun notarytool submit "$pkg" \ --apple-id "${MACOS_NOTARIZE_APPLE_ID}" \ --team-id "${MACOS_NOTARIZE_TEAM_ID}" \ --password "${MACOS_NOTARIZE_PASSWORD}" \ --wait \ --timeout 15m echo "Stapling notarization ticket to: $pkg" xcrun stapler staple "$pkg" xcrun stapler validate "$pkg" else echo "Skipping notarization (credentials not provided)" fi } # Allow ARCH override from environment (for CI cross-builds), otherwise detect if [[ -n "${ARCH}" ]]; then arch="${ARCH}" else raw_arch=$(uname -m) case "$raw_arch" in arm64) arch="arm64" ;; x86_64) arch="amd64" ;; *) echo "Error: Unsupported architecture: $raw_arch" >&2 echo "This script only supports arm64 (Apple Silicon) and x86_64 (Intel)." >&2 exit 1 ;; esac fi build_output=../../build-output mkdir -p "$build_output" telepresence_binary="$build_output/telepresence-$VERSION" telepresence_url="https://github.com/telepresenceio/telepresence/releases/download/v${VERSION}/telepresence-darwin-${arch}" # For CI builds, check if a local binary exists from the main build process local_binary="$build_output/bin/telepresence" if [[ -f "$local_binary" ]] && file "$local_binary" | grep -q "Mach-O"; then echo "Using local build: $local_binary" cp "$local_binary" "$telepresence_binary" elif [[ ! -f "$telepresence_binary" ]]; then # Download only if missing OR outdated curl -L --fail --remote-time --output "$telepresence_binary" "$telepresence_url" || exit 1 else curl -L --fail --remote-time --output "$telepresence_binary" --time-cond "$telepresence_binary" "$telepresence_url" || exit 1 fi # === Build CLI-only package === cli_payload="$build_output/cli/Payload" mkdir -p "$cli_payload/usr/local/bin" cp "$telepresence_binary" "$cli_payload/usr/local/bin/telepresence" chmod +x "$cli_payload/usr/local/bin/telepresence" sign_binary "$cli_payload/usr/local/bin/telepresence" cp uninstall "$cli_payload/usr/local/bin/telepresence-uninstall" chmod +x "$cli_payload/usr/local/bin/telepresence-uninstall" product="$build_output/product" mkdir -p "$product" sed "s|__VERSION__|$VERSION|g" Distribution.xml > "$product/Distribution.xml" pkgbuild --identifier io.telepresence.cli \ --version "$VERSION" \ --root "$cli_payload" \ --install-location / \ "$product/cli.pkg" # === Build Rootd package === rootd_payload="$build_output/rootd/Payload" mkdir -p "$rootd_payload/usr/local/bin" cp telepresence-rootd "$rootd_payload/usr/local/bin" chmod +x "$rootd_payload/usr/local/bin/telepresence-rootd" mkdir -p "$rootd_payload/Library/LaunchDaemons" cp io.telepresence.rootd.plist "$rootd_payload/Library/LaunchDaemons/" mkdir -p "$rootd_payload/etc/newsyslog.d" cp syslog.conf "$rootd_payload/etc/newsyslog.d/telepresence-rootd.conf" rootd_scripts="$build_output/rootd/Scripts" mkdir -p "$rootd_scripts" cp postinstall "$rootd_scripts/postinstall" chmod +x "$rootd_scripts/postinstall" pkgbuild --identifier io.telepresence.rootd \ --version "$VERSION" \ --root "$rootd_payload" \ --scripts "$rootd_scripts" \ --install-location / \ "$product/rootd.pkg" resources="$build_output/resources" mkdir -p "$resources" cp welcome.rtf "$resources/welcome.rtf" textutil -convert rtf -stdin -stdout < ../../LICENSE > "$resources/license.rtf" productbuild --distribution "$product/Distribution.xml" \ --package-path "$product" \ --resources "$resources" \ --version "$VERSION" \ "$build_output/Telepresence-unsigned.pkg" # Sign the package (or rename if no signing certificate) sign_package "$build_output/Telepresence-unsigned.pkg" "$build_output/Telepresence.pkg" # Notarize the signed package (if credentials are provided) notarize_package "$build_output/Telepresence.pkg" rm -rf cli rootd resources product ================================================ FILE: build-aux/pkg-installer/io.telepresence.rootd.plist ================================================ Label io.telepresence.rootd ProgramArguments /usr/local/bin/telepresence rootd --logfile std --config /Library/Application Support/telepresence/config.yml --address :4037 --managed RunAtLoad KeepAlive StandardOutPath /var/log/telepresence-rootd.log StandardErrorPath /var/log/telepresence-rootd.log ProcessType Interactive WorkingDirectory /Library/Caches/telepresence EnvironmentVariables PATH /usr/local/bin:/usr/bin:/bin HOME /var/root ================================================ FILE: build-aux/pkg-installer/postinstall ================================================ #!/bin/bash set -e mkdir -p "/Library/Application Support/telepresence" mkdir -p "/Library/Caches/telepresence" touch "/Library/Application Support/telepresence/config.yml" chown -R root:wheel \ "/Library/Application Support/telepresence" \ "/Library/Caches/telepresence" chmod 755 "/Library/Application Support/telepresence" chmod 755 "/Library/Caches/telepresence" chmod 644 "/Library/Application Support/telepresence/config.yml" echo "Starting Telepresence Root Daemon..." /bin/launchctl load -w /Library/LaunchDaemons/io.telepresence.rootd.plist echo "Telepresence Root Daemon is running." echo "Control with: telepresence-rootd start|stop|restart" ================================================ FILE: build-aux/pkg-installer/syslog.conf ================================================ # /etc/newsyslog.d/telepresence-rootd.conf # Rotate telepresence-rootd.log daily, keep 7 days, compress, restart daemon /var/log/telepresence-rootd.log { daily rotate 7 compress missingok } ================================================ FILE: build-aux/pkg-installer/telepresence-rootd ================================================ #!/bin/bash ACTION="$1" if [[ "$ACTION" != "start" && "$ACTION" != "stop" && "$ACTION" != "restart" ]]; then echo "Usage: $0 start|stop|restart" exit 1 fi case "$ACTION" in start) sudo /bin/launchctl load -w /Library/LaunchDaemons/io.telepresence.rootd.plist ;; stop) sudo /bin/launchctl unload -w /Library/LaunchDaemons/io.telepresence.rootd.plist ;; restart) sudo /bin/launchctl unload -w /Library/LaunchDaemons/io.telepresence.rootd.plist 2>/dev/null || true; sudo /bin/launchctl load -w /Library/LaunchDaemons/io.telepresence.rootd.plist ;; esac echo "Telepresence Daemon $ACTION requested." ================================================ FILE: build-aux/pkg-installer/uninstall ================================================ #!/usr/bin/env bash echo "Uninstalling Telepresence..." sudo /bin/launchctl unload -w /Library/LaunchDaemons/io.telepresence.rootd.plist 2>/dev/null || true sudo rm -f /usr/local/bin/telepresence sudo rm -f /usr/local/bin/telepresence-rootd sudo rm -f /Library/LaunchDaemons/io.telepresence.rootd.plist sudo rm -rf "/Library/Application Support/telepresence" sudo rm -rf "/Library/Caches/telepresence" echo "Uninstalled." ================================================ FILE: build-aux/pkg-installer/welcome.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf2822 \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica-Bold;\f1\fswiss\fcharset0 Helvetica;\f2\froman\fcharset0 Times-Roman; \f3\fmodern\fcharset0 CourierNewPSMT;} {\colortbl;\red255\green255\blue255;\red0\green0\blue0;\red255\green255\blue255;\red255\green255\blue255; } {\*\expandedcolortbl;;\cssrgb\c0\c0\c0;\cssrgb\c100000\c100000\c100000\c0;\cssrgb\c100000\c100000\c99985\c0; } \paperw12240\paperh15840\vieww36540\viewh8400\viewkind0 \pard\sa240\pardirnatural\partightenfactor0 \f0\b\fs28 \cf2 \cb3 Install CLI only or CLI + system daemon \f1\b0 \ \pard\sa240\pardirnatural\partightenfactor0 \fs24 \cf2 \cb4 Click the " \f2 \cb4 Customise" \f1 \cb4 button in the "Installation Type" step to configure the telepresence root daemon as a system daemon.\cb3 \ \pard\sa240\pardirnatural\partightenfactor0 \fs22 \cf2 When the system daemon is running, a \f3 telepresence connect \f1 will no longer need to use \f3 sudo \f1 .} ================================================ FILE: build-aux/prelude.mk ================================================ # Copyright 2020-2021 Datawire. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # This file deals with baseline 'Makefile' utilities, without doing # anything specific to Telepresence. # Delete implicit rules not used here (clutters debug output). MAKEFLAGS += --no-builtin-rules # Turn off .INTERMEDIATE file removal by marking all files as # .SECONDARY. .INTERMEDIATE file removal is a space-saving hack from # a time when drives were small; on modern computers with plenty of # storage, it causes nothing but headaches. # # https://news.ycombinator.com/item?id=16486331 .SECONDARY: # If a recipe errors, remove the target it was building. This # prevents outdated/incomplete results of failed runs from tainting # future runs. The only reason .DELETE_ON_ERROR is off by default is # for historical compatibility. # # If for some reason this behavior is not desired for a specific # target, mark that target as .PRECIOUS. .DELETE_ON_ERROR: # Be silent $(VERBOSE).SILENT: # Add a rule to generate `make help` output from comments in the # Makefiles. .PHONY: help help: ## (ZSupport) Show this message @echo 'Usage: [VARIABLE=VALUE...] $(MAKE) [TARGETS...]' @echo @echo VARIABLES: @{ $(foreach varname,$(shell sed -n '/[?]=/{ s/[ ?].*//; s/^/ /; p; }' $(sort $(abspath $(MAKEFILE_LIST)))),printf '%s = %s\n' '$(varname)' '$($(varname))';) } | column -t | sed 's/^/ /' @echo @echo TARGETS: @sed -En 's/^([^:]*):[^#]*## *(\([^)]*\))? */\2 \1 /p' $(sort $(abspath $(MAKEFILE_LIST))) | sed 's/^ /($(or $(NAME),this project))&/' | column -t -s ' ' | sed 's/^/ /' | sort @echo @echo "See DEVELOPING.md for more information" ================================================ FILE: build-aux/systemd-installer/.gitignore ================================================ nfpm.yaml ================================================ FILE: build-aux/systemd-installer/build-packages.sh ================================================ #!/bin/bash set -e # Build .deb and .rpm packages using nfpm # Validate required environment variables [[ -z "${VERSION}" ]] && { echo "Error: VERSION required" >&2; exit 1; } [[ -z "${ARCH}" ]] && { echo "Error: ARCH required (amd64 or arm64)" >&2; exit 1; } # Only run on Linux if [[ "$(uname -s)" != "Linux" ]]; then echo "Error: This script only supports Linux." >&2 exit 1 fi # Parse version for package managers (RPM doesn't allow hyphens in version) # Convert 2.27.0-test.4 to version=2.27.0 release=test.4 if [[ "$VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-(.+)$ ]]; then PKG_VERSION="${BASH_REMATCH[1]}" PKG_RELEASE="${BASH_REMATCH[2]}" else PKG_VERSION="$VERSION" PKG_RELEASE="1" fi export PKG_VERSION PKG_RELEASE echo "Building packages: version=${PKG_VERSION}, release=${PKG_RELEASE}, arch=${ARCH}" script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" build_output="$(cd "${script_dir}/../.." && pwd)/build-output" mkdir -p "$build_output/release" echo "script_dir=$script_dir" echo "build_output=$build_output" # For CI builds, check if a local binary exists from the main build process local_binary="${build_output}/bin/telepresence" echo "Checking for local binary at: $local_binary" if [[ -f "$local_binary" ]] && file "$local_binary" | grep -q "ELF"; then echo "Using local build: $local_binary" BINDIR="${build_output}/bin" else # Download from GitHub releases echo "Downloading telepresence v${VERSION} for linux-${ARCH}..." mkdir -p "${build_output}/bin" curl -L --fail --remote-time \ -o "${build_output}/bin/telepresence" \ "https://github.com/telepresenceio/telepresence/releases/download/v${VERSION}/telepresence-linux-${ARCH}" chmod +x "${build_output}/bin/telepresence" BINDIR="${build_output}/bin" fi export BINDIR echo "BINDIR=$BINDIR" echo "Binary exists: $(ls -la "$BINDIR/telepresence" 2>&1)" cd "$script_dir" # Map arch to nfpm format case "$ARCH" in amd64) nfpm_arch="amd64" ;; arm64) nfpm_arch="arm64" ;; *) echo "Error: Unsupported architecture: $ARCH" >&2 exit 1 ;; esac echo "Building packages for linux-${ARCH}, version ${VERSION}..." # Export all variables needed by nfpm.yaml.in and preprocess with envsubst export ARCH="$nfpm_arch" # PKG_VERSION, PKG_RELEASE, and BINDIR are already exported above echo "Environment for nfpm: ARCH=$ARCH PKG_VERSION=$PKG_VERSION PKG_RELEASE=$PKG_RELEASE BINDIR=$BINDIR" envsubst < nfpm.yaml.in > nfpm.yaml # Build .deb package echo "Building .deb package..." nfpm package \ --config nfpm.yaml \ --packager deb \ --target "${build_output}/release/telepresence-${VERSION}-linux-${ARCH}.deb" # Build .rpm package echo "Building .rpm package..." nfpm package \ --config nfpm.yaml \ --packager rpm \ --target "${build_output}/release/telepresence-${VERSION}-linux-${ARCH}.rpm" echo "" echo "Packages built successfully:" ls -la "${build_output}/release/"*.deb "${build_output}/release/"*.rpm 2>/dev/null || true ================================================ FILE: build-aux/systemd-installer/config.yml ================================================ # Telepresence root daemon configuration # See https://www.telepresence.io/docs/reference/config for all options logLevels: rootDaemon: info ================================================ FILE: build-aux/systemd-installer/nfpm.yaml.in ================================================ # nfpm configuration for building .deb and .rpm packages # Documentation: https://nfpm.goreleaser.com/configuration/ name: telepresence arch: ${ARCH} platform: linux version: ${PKG_VERSION} release: ${PKG_RELEASE} maintainer: https://github.com/telepresenceio/telepresence description: | Telepresence: fast, local development for Kubernetes. Telepresence connects your local workstation to a remote Kubernetes cluster, allowing you to run services locally while accessing cluster resources. vendor: Telepresence.io homepage: https://telepresence.io/ license: Apache-2.0 section: net # Package dependencies depends: - fuse3 recommends: - sshfs contents: # Main binary - src: ${BINDIR}/telepresence dst: /usr/local/bin/telepresence file_info: mode: 0755 # Systemd service file - src: telepresence-rootd.service dst: /usr/lib/systemd/system/telepresence-rootd.service file_info: mode: 0644 # Default configuration - src: config.yml dst: /etc/telepresence/config.yml type: config|noreplace file_info: mode: 0644 # Uninstall script - src: uninstall.sh dst: /usr/local/bin/telepresence-uninstall file_info: mode: 0755 # Create required directories - dst: /var/cache/telepresence type: dir file_info: mode: 0700 owner: root group: root scripts: postinstall: ./postinstall.sh preremove: ./preremove.sh postremove: ./postremove.sh # RPM-specific settings rpm: group: Applications/Internet summary: Fast, local development for Kubernetes ================================================ FILE: build-aux/systemd-installer/postinstall.sh ================================================ #!/bin/bash set -e # Create required directories mkdir -p /var/cache/telepresence/rootd mkdir -p /etc/telepresence chmod 755 /var/cache/telepresence chmod 755 /var/cache/telepresence/rootd # Reload systemd to pick up the new service file (may fail in containers) systemctl daemon-reload 2>/dev/null || true # Enable and start the service systemctl enable telepresence-rootd.service 2>/dev/null || true systemctl start telepresence-rootd.service 2>/dev/null || true echo "" echo "Telepresence has been installed successfully!" echo "" echo "The root daemon service has been enabled and started." echo "" echo "To check service status:" echo " sudo systemctl status telepresence-rootd" echo "" echo "To view service logs:" echo " sudo journalctl -u telepresence-rootd" echo "" ================================================ FILE: build-aux/systemd-installer/postremove.sh ================================================ #!/bin/bash set -e # Reload systemd to forget about the removed service systemctl daemon-reload || true echo "" echo "Telepresence has been removed." echo "" echo "Note: Configuration in /etc/telepresence and logs in /var/log/telepresence" echo "have been preserved. Remove them manually if no longer needed." echo "" ================================================ FILE: build-aux/systemd-installer/preremove.sh ================================================ #!/bin/bash set -e # Stop the service if running if systemctl is-active --quiet telepresence-rootd.service 2>/dev/null; then echo "Stopping telepresence-rootd service..." systemctl stop telepresence-rootd.service || true fi # Disable the service if systemctl is-enabled --quiet telepresence-rootd.service 2>/dev/null; then echo "Disabling telepresence-rootd service..." systemctl disable telepresence-rootd.service || true fi ================================================ FILE: build-aux/systemd-installer/telepresence-rootd.service ================================================ [Unit] Description=Telepresence root daemon (rootd) Documentation=https://telepresence.io/ After=network.target [Service] Type=simple ExecStart=/usr/local/bin/telepresence rootd \ --logfile managed \ --config /etc/telepresence/config.yml \ --address :4037 \ --managed # Runs as root (LaunchDaemon equivalent) User=root Group=root # Linux-standard directories WorkingDirectory=/var/cache/telepresence # Creates /run/telepresence for runtime socket RuntimeDirectory=telepresence RuntimeDirectoryMode=0755 # Restart behaviour = KeepAlive + RunAtLoad Restart=always RestartSec=3 # Logging (use journalctl -u telepresence-rootd to view logs) StandardOutput=journal StandardError=journal # Environment Environment=PATH=/usr/local/bin:/usr/bin:/bin # Telepresence sometimes expects a writable HOME Environment=HOME=/var/root # Security hardening (recommended) NoNewPrivileges=yes ProtectSystem=strict ReadWritePaths=/var/cache/telepresence ProtectHome=read-only PrivateTmp=yes RestrictSUIDSGID=yes RemoveIPC=yes RestrictNamespaces=yes ProtectHostname=yes ProtectKernelTunables=yes ProtectKernelModules=yes ProtectControlGroups=yes [Install] WantedBy=multi-user.target ================================================ FILE: build-aux/systemd-installer/uninstall.sh ================================================ #!/bin/bash set -e echo "Uninstalling Telepresence..." # Stop and disable the service if systemctl is-active --quiet telepresence-rootd.service 2>/dev/null; then echo "Stopping telepresence-rootd service..." sudo systemctl stop telepresence-rootd.service fi if systemctl is-enabled --quiet telepresence-rootd.service 2>/dev/null; then echo "Disabling telepresence-rootd service..." sudo systemctl disable telepresence-rootd.service fi # Remove files echo "Removing files..." sudo rm -f /usr/local/bin/telepresence sudo rm -f /usr/local/bin/telepresence-uninstall sudo rm -f /usr/lib/systemd/system/telepresence-rootd.service # Reload systemd sudo systemctl daemon-reload echo "" echo "Telepresence has been uninstalled." echo "" echo "The following directories have been preserved:" echo " /etc/telepresence - configuration" echo " /var/cache/telepresence - cache data" echo " /var/log/telepresence - logs" echo "" echo "Remove them manually if no longer needed:" echo " sudo rm -rf /etc/telepresence /var/cache/telepresence /var/log/telepresence" echo "" ================================================ FILE: build-aux/tools.mk ================================================ # Copyright 2020-2021 Datawire. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # This file deals with installing programs used by the build. TOOLSDIR=tools TOOLSBINDIR=$(TOOLSDIR)/bin TOOLSSRCDIR=$(TOOLSDIR)/src GOHOSTOS ?= $(shell go env GOHOSTOS) GOHOSTARCH ?= $(shell go env GOHOSTARCH) GOOS?=$(shell go env GOOS) GOARCH?=$(shell go env GOARCH) export PATH := $(abspath $(TOOLSBINDIR)):$(PATH) clobber: clobber-tools .PHONY: clobber-tools clobber-tools: rm -rf $(TOOLSBINDIR) $(TOOLSDIR)/include $(TOOLSDIR)/*.* # Protobuf compiler # ================= # # Install protoc under $TOOLSDIR. A protoc that is already installed locally # cannot be trusted since this must be the exact same version as used when # running CI. If it isn't, the generate-check will fail. PROTOC_VERSION=30.2 ifeq ($(GOHOSTARCH),arm64) PROTOC_ARCH=aarch_64 else ifeq ($(GOHOSTARCH),amd64) PROTOC_ARCH=x86_64 else PROTOC_ARCH=$(GOHOSTARCH) endif ifeq ($(GOHOSTOS),windows) PROTOC_OS_ARCH=win64 EXE=.exe else PROTOC_OS_ARCH=$(patsubst darwin,osx,$(GOHOSTOS))-$(PROTOC_ARCH) EXE= endif tools/protoc = $(TOOLSBINDIR)/protoc$(EXE) PROTOC_ZIP=protoc-$(PROTOC_VERSION)-$(PROTOC_OS_ARCH).zip $(TOOLSDIR)/$(PROTOC_ZIP): mkdir -p $(@D) curl -sfL https://github.com/protocolbuffers/protobuf/releases/download/v$(PROTOC_VERSION)/$(PROTOC_ZIP) -o $@ %/bin/protoc$(EXE) %/include %/readme.txt: %/$(PROTOC_ZIP) cd $* && unzip -q -o -DD $([^^/]*)/.*\"; \"\(.h)\")" > .wintools\docker-host for /f "delims=" %%i in (.wintools\docker-host) do set TELEPRESENCE_REGISTRY_HOST=%%i del .wintools\docker-host for /f "delims=" %%i in ('echo %TELEPRESENCE_REGISTRY_HOST% ^| docker-credential-desktop.exe get ^| .wintools\jq.exe -r .Username') do set "TELEPRESENCE_REGISTRY_USERNAME=%%i" for /f "delims=" %%i in ('echo %TELEPRESENCE_REGISTRY_HOST% ^| docker-credential-desktop.exe get ^| .wintools\jq.exe -r .Secret') do set "TELEPRESENCE_REGISTRY_PASSWORD=%%i" docker run --rm ^ -v /host_mnt/%drive%/%pwd%:/source ^ -v //var/run/docker.sock:/var/run/docker.sock ^ -w /source ^ -e GOOS=windows ^ -e GOCACHE=/source/.gocache ^ -e GOARCH ^ -e TELEPRESENCE_REGISTRY ^ -e TELEPRESENCE_REGISTRY_USERNAME ^ -e TELEPRESENCE_REGISTRY_PASSWORD ^ -e TELEPRESENCE_VERSION ^ -e KO_DOCKER_REPO ^ tel2-winbuild ^ make _login %* ================================================ FILE: build-aux/wix-installer/Dialogs_en-us.wxl ================================================ ================================================ FILE: build-aux/wix-installer/MainPackage.wxs ================================================ ================================================ FILE: build-aux/wix-installer/Makefile ================================================ # Makefile — complete Telepresence Windows installer # Run: make bundle → produces TelepresenceInstall.exe # make clean → remove everything SHELL:=$(shell which bash) .PHONY: FORCE # VERSION is the telepresence version to bundle. Use TELEPRESENCE_VERSION if set (for CI). VERSION ?= $(if $(TELEPRESENCE_VERSION),$(patsubst v%,%,$(TELEPRESENCE_VERSION)),2.26.0) ARCH ?= amd64 WIX := wix GO := go CURL := curl UNZIP := unzip SEVENZ := 7z # install: sudo apt install p7zip-full WINFSP_VERSION := 2.1.25156 SSHFS_VERSION := 3.7.21011 WINTUN_VERSION := 0.14.1 WINTUN_SUM := 07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51 # update if version changes BUILD := ../../build-output BINDIR := $(BUILD)/bin MSIDIR := $(BUILD)/msi ZIPDIR := $(BUILD)/zip EXE := telepresence.exe BUNDLE := TelepresenceInstall.exe MAIN_MSI := MainPackage.msi LICENSE := License.rtf DAEMON_EXE := TelepresenceDaemon.exe BUNDLE_VERSION := $(word 1, $(subst -, ,$(TELEPRESENCE_VERSION:v%=%))).0 WIXDEFS := -d BundleVersion=$(BUNDLE_VERSION) .PHONY: all bundle msi deps clean all: bundle # Final single-click installer bundle: $(BINDIR)/$(BUNDLE) @echo "SUCCESS! Installer ready: $<" @du -h $< $(BINDIR)/$(BUNDLE): Telepresence.wxs $(MSIDIR)/$(MAIN_MSI) $(MSIDIR)/winfsp.msi $(MSIDIR)/sshfs-win.msi | $(BINDIR) $(WIX) build $(WIXDEFS) \ -ext WixToolset.BootstrapperApplications.wixext \ -ext WixToolset.UI.wixext \ -arch x64 \ -src Telepresence.wxs \ -bindpath . \ -bindpath "$(MSIDIR)" \ -o $@ # Main MSI (binary, service daemon, and wintun.dll) $(MSIDIR)/$(MAIN_MSI): MainPackage.wxs Dialogs_en-us.wxl $(BINDIR)/$(EXE) $(BINDIR)/$(DAEMON_EXE) $(BINDIR)/wintun.dll $(BUILD)/$(LICENSE) | $(MSIDIR) $(WIX) build $(WIXDEFS) \ -ext WixToolset.UI.wixext \ -ext WixToolset.Util.wixext \ -arch x64 \ -loc Dialogs_en-us.wxl \ -bindpath . \ -bindpath "$(BUILD)" \ -bindpath "$(BINDIR)" \ -o $@ $< $(ZIPDIR): mkdir -p $@ $(BINDIR): mkdir -p $@ $(MSIDIR): mkdir -p $@ $(BUILD): mkdir -p $@ # Dependencies deps: $(MSIDIR)/winfsp.msi $(MSIDIR)/sshfs-win.msi $(BINDIR)/wintun.dll $(BINDIR)/$(EXE) $(BINDIR)/$(DAEMON_EXE) $(BUILD)/$(LICENSE) WF_URL := https://github.com/winfsp/winfsp/releases/download/v2.1/winfsp-$(WINFSP_VERSION).msi $(MSIDIR)/winfsp.msi: | $(MSIDIR) @if [ -f "$@" ]; then \ $(CURL) -L --fail -z "$@" -R -o "$@" "$(WF_URL)"; \ else \ $(CURL) -L --fail -R -o "$@" "$(WF_URL)"; \ fi SSHFS_URL := https://github.com/winfsp/sshfs-win/releases/download/v$(SSHFS_VERSION)/sshfs-win-$(SSHFS_VERSION)-x64.msi $(MSIDIR)/sshfs-win.msi: | $(MSIDIR) @if [ -f "$@" ]; then \ $(CURL) -L --fail -z "$@" -R -o "$@" "$(SSHFS_URL)"; \ else \ $(CURL) -L --fail -R -o "$@" "$(SSHFS_URL)"; \ fi # wintun.dll # Paths WT_ZIP := $(ZIPDIR)/wintun-$(WINTUN_VERSION).zip WT_URL := https://www.wintun.net/builds/wintun-$(WINTUN_VERSION).zip WT_FILE := wintun/bin/$(ARCH)/wintun.dll $(BINDIR)/wintun.dll: FORCE | $(BINDIR) $(ZIPDIR) @echo "Checking for newer $@..." @if [ -f "$@" -a -f "$(WT_ZIP)" ]; then \ # Try conditional request first \ http_code=$$($(CURL) -L --fail -z "$@" -R -o "$(WT_ZIP)" --write-out '%{http_code}' "$(WT_URL)"); \ if [ "$$http_code" = "200" ]; then \ echo "Downloaded updated wintun $(WINTUN_VERSION). Extracting $@..."; \ $(UNZIP) -op "$(WT_ZIP)" "$(WT_FILE)" > "$@" && touch -r "$(WT_ZIP)" "$@"; \ elif [ "$$http_code" = "304" ]; then \ echo "$@ already up to date (not modified)"; \ else \ echo "Download failed (status $$?)"; \ exit 1; \ fi \ else \ # File doesn't exist -> full download \ echo "Downloading wintun $(WINTUN_VERSION) (first time)..."; \ $(CURL) -L --fail -R -o "$(WT_ZIP)" "$(WT_URL)" || { \ rm -f "$@"; exit 1; \ }; \ echo "Extracting $@..."; \ $(UNZIP) -op "$(WT_ZIP)" "$(WT_FILE)" > "$@" && touch -r "$(WT_ZIP)" "$@"; \ fi # Telepresence binary # Paths TP_ZIP := $(ZIPDIR)/telepresence-windows-$(ARCH)-v$(VERSION).zip TP_URL := https://github.com/telepresenceio/telepresence/releases/download/v$(VERSION)/telepresence-windows-$(ARCH).zip # For CI builds, the binary may already exist from the main build process. # Check if it exists and is a valid Windows executable (has MZ header). $(BINDIR)/$(EXE): FORCE | $(BINDIR) $(ZIPDIR) @if [ -f "$@" ] && head -c2 "$@" 2>/dev/null | grep -q "MZ"; then \ echo "$@ already exists and is valid (using local build)"; \ elif [ -f "$@" -a -f "$(TP_ZIP)" ]; then \ echo "Checking for newer Telepresence v$(VERSION)..."; \ http_code=$$($(CURL) -L --fail -z "$@" -R -o "$(TP_ZIP)" --write-out '%{http_code}' "$(TP_URL)"); \ if [ "$$http_code" = "200" ]; then \ echo "Downloaded updated telepresence v$(VERSION). Extracting $@..."; \ $(UNZIP) -o "$(TP_ZIP)" $(EXE) -d $(dir $@); \ touch -r "$(TP_ZIP)" "$@"; \ elif [ "$$http_code" = "304" ]; then \ echo "$@ already up to date (not modified)"; \ else \ echo "Download failed (status $$http_code)"; \ exit 1; \ fi; \ else \ echo "Downloading telepresence v$(VERSION) (first time)..."; \ $(CURL) -L --fail -R -o "$(TP_ZIP)" "$(TP_URL)" || { \ rm -f "$@"; exit 1; \ }; \ echo "Extracting $@..."; \ $(UNZIP) -o "$(TP_ZIP)" $(EXE) -d $(dir $@); \ touch -r "$(TP_ZIP)" "$@"; \ fi # Build the TelepresenceDaemon service wrapper $(BINDIR)/$(DAEMON_EXE): tpwrapper.go | $(BINDIR) GOOS=windows GOARCH=$(ARCH) $(GO) build --trimpath -ldflags="-s -w" -o $@ $< $(BUILD)/$(LICENSE): ../../LICENSE | $(BUILD) pwsh -File txt2rtf.ps1 "$<" "$@" # First-time setup init: $(GO) mod init telepresence-wix-customaction $(GO) get golang.org/x/sys@v0.25.0 $(GO) mod tidy clean: rm -rf $(BUILD) $(BINDIR) $(MSI) $(BUNDLE) *.wixobj *.wixpdb ================================================ FILE: build-aux/wix-installer/Telepresence.wxs ================================================ ================================================ FILE: build-aux/wix-installer/config.yml ================================================ ================================================ FILE: build-aux/wix-installer/tpwrapper.go ================================================ //go:build windows package main import ( "flag" "fmt" "log" "os" "os/exec" "strconv" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/debug" ) const svcName = "TelepresenceDaemon" type wrapper struct { executable string cacheDir string configPath string logPath string logLevel string address string pprofPort uint } func (w *wrapper) Execute(_ []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) { const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown changes <- svc.Status{State: svc.StartPending} args := make([]string, 0, 5) args = append(args, "rootd", "--config", w.configPath) if w.cacheDir != "" { args = append(args, "--cache", w.cacheDir) } if w.logPath != "" { args = append(args, "--logfile", w.logPath) } if w.address != "" { args = append(args, "--address", w.address) } if w.pprofPort > 0 { args = append(args, "--pprof", strconv.Itoa(int(w.pprofPort))) } args = append(args, "--managed") cmd := exec.Command(w.executable, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { log.Printf("Failed to start %s: %v", w.executable, err) changes <- svc.Status{State: svc.Stopped} return false, 1 } // Success — we are running log.Println("telepresence.exe started (PID", cmd.Process.Pid, ")") changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} // Wait for stop request or child exit go func() { _ = cmd.Wait() // ignore error, we just want to know when it dies changes <- svc.Status{State: svc.Stopped} }() for { select { case c := <-r: switch c.Cmd { case svc.Interrogate: changes <- c.CurrentStatus case svc.Stop, svc.Shutdown: log.Println("Stopping telepresence.exe...") changes <- svc.Status{State: svc.StopPending} // Graceful SIGTERM first if err := cmd.Process.Kill(); err != nil { log.Printf("Kill failed: %v", err) } return false, 0 } } } } func main() { var logLevel string isDebug := false w := &wrapper{} flag.BoolVar(&isDebug, "debug", false, "run in console, not as a real service") flag.StringVar(&w.executable, "executable", "", `path to the executable (required)`) flag.StringVar(&w.configPath, "config", "", `Path to the Telepresence configuration file (required)`) flag.StringVar(&logLevel, "loglevel", "info", `one of error, warning, info, debug, or trace`) flag.StringVar(&w.cacheDir, "cache", "", `Path to the Telepresence cache directory`) flag.StringVar(&w.logPath, "logfile", "", "path to the log file") flag.StringVar(&w.address, "address", ":4037", "TCP address to listen to") flag.UintVar(&w.pprofPort, "pprof", uint(0), "start pprof server on the given port") flag.Parse() if w.configPath == "" { flag.Usage() os.Exit(1) } var err error switch logLevel { case "error", "warning", "info", "debug", "trace": err = os.WriteFile(w.configPath, []byte(fmt.Sprintf("logLevels:\n rootDaemon: %s\n", logLevel)), 0o644) default: err = fmt.Errorf("invalid loglevel: %q", logLevel) } if err != nil { log.Fatal(err) } if isDebug { err = debug.Run(svcName, w) } else { err = svc.Run(svcName, w) } if err != nil { log.Fatal(err) } } ================================================ FILE: build-aux/wix-installer/txt2rtf.ps1 ================================================ param( [Parameter(Mandatory)][string]$InputFile, [Parameter(Mandatory)][string]$OutputFile ) Add-Type -AssemblyName System.Windows.Forms $rtb = New-Object System.Windows.Forms.RichTextBox $rtb.LoadFile($InputFile, [System.Windows.Forms.RichTextBoxStreamType]::PlainText) $rtb.SaveFile($OutputFile, [System.Windows.Forms.RichTextBoxStreamType]::RichText) Write-Host "Converted: $($InputFile) -> $OutputFile" ================================================ FILE: charts/chart.go ================================================ package charts import ( "archive/tar" "compress/gzip" "embed" "errors" "fmt" "io" "io/fs" "sort" "strings" "github.com/blang/semver/v4" "github.com/go-json-experiment/json" "github.com/spf13/afero" "helm.sh/helm/v3/pkg/chart" "sigs.k8s.io/yaml" ) type DirType int8 const ( DirTypeTelepresence DirType = iota TelepresenceChartName = "telepresence-oss" ) //go:embed all:telepresence-oss var TelepresenceFS embed.FS // filePriority returns the sort-priority of a filename; higher priority files sorts earlier. func filePriority(chartName, filename string) int { prio := map[string]int{ fmt.Sprintf("%s/Chart.yaml)", chartName): 4, fmt.Sprintf("%s/values.yaml)", chartName): 3, fmt.Sprintf("%s/values.schema.json", chartName): 2, // "telepresence/templates-oss/**": 1, // "otherwise": 0, }[filename] if prio == 0 && strings.HasPrefix(filename, fmt.Sprintf("%s/templates/", chartName)) { prio = 1 } return prio } func addFile(tarWriter *tar.Writer, vfs fs.FS, filename string, content []byte) error { var header *tar.Header // Build the tar.Header. fi, err := fs.Stat(vfs, filename) if err == nil { header, err = tar.FileInfoHeader(fi, "") if err != nil { return err } } else { if !errors.Is(err, fs.ErrNotExist) { return err } header = &tar.Header{} } header.Name = filename header.Mode = 0o644 header.Size = int64(len(content)) // Write the tar.Header. if err := tarWriter.WriteHeader(header); err != nil { return err } // Write the content. if _, err := tarWriter.Write(content); err != nil { return err } return nil } type ChartOverlayFuncDef func(base afero.Fs) (afero.Fs, error) // ChartOverlayFunc can be used by module extensions to add or overwrite the charts directory. // type ChartOverlayFunc func(base afero.Fs) (afero.Fs, error). var ChartOverlayFunc map[DirType]ChartOverlayFuncDef //nolint:gochecknoglobals // extension point // WriteChart is a minimal `helm package`. func WriteChart(helmChartDir DirType, out io.Writer, chartName string, version semver.Version, overlays ...fs.FS) error { embedChart := map[DirType]embed.FS{ DirTypeTelepresence: TelepresenceFS, }[helmChartDir] var baseDir fs.FS = embedChart if chartOverlayFunc, ok := ChartOverlayFunc[helmChartDir]; ok { base := afero.FromIOFS{FS: embedChart} ovl, err := chartOverlayFunc(base) if err != nil { return err } baseDir = afero.NewIOFS(afero.NewCopyOnWriteFs(base, ovl)) } var filenames []string if err := fs.WalkDir(baseDir, ".", func(filename string, dirent fs.DirEntry, err error) error { if err != nil { return err } if dirent.Type().IsRegular() { filenames = append(filenames, filename) } return nil }); err != nil { return err } sort.Slice(filenames, func(i, j int) bool { iName := filenames[i] jName := filenames[j] // higher priority files sorts earlier. iPrio := filePriority(chartName, iName) jPrio := filePriority(chartName, jName) if d := iPrio - jPrio; d != 0 { return d > 0 } // priority is the same return iName < jName }) zipper := gzip.NewWriter(out) zipper.Extra = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=") // magic number for Helm zipper.Comment = "Helm" tarWriter := tar.NewWriter(zipper) for _, filename := range filenames { switch filename { case fmt.Sprintf("%s/values.schema.yaml", chartName): content, err := fs.ReadFile(baseDir, filename) if err != nil { return err } defs, err := fs.ReadFile(baseDir, fmt.Sprintf("%s/k8s-defs.json", chartName)) if err != nil { return err } contentMap := make(map[string]any) err = yaml.Unmarshal(content, &contentMap) if err != nil { return err } defsMap := make(map[string]any) err = json.Unmarshal(defs, &defsMap) if err != nil { return err } contentMap["definitions"] = defsMap["definitions"] content, err = json.Marshal(contentMap) if err != nil { return err } if err = addFile(tarWriter, baseDir, fmt.Sprintf("%s/values.schema.json", chartName), content); err != nil { return err } case fmt.Sprintf("%s/Chart.yaml", chartName): content, err := fs.ReadFile(baseDir, filename) if err != nil { return err } var dat chart.Metadata if err := yaml.Unmarshal(content, &dat); err != nil { return err } vs := version.String() dat.Version = vs dat.AppVersion = vs content, err = yaml.Marshal(dat) if err != nil { return err } if err = addFile(tarWriter, baseDir, filename, content); err != nil { return err } case fmt.Sprintf("%s/k8s-defs.json", chartName): // Don't include k8s-defs.json to the chart package default: content, err := fs.ReadFile(baseDir, filename) if err != nil { return err } if err = addFile(tarWriter, baseDir, filename, content); err != nil { return err } } } if err := tarWriter.Close(); err != nil { return err } if err := zipper.Close(); err != nil { return err } return nil } ================================================ FILE: charts/telepresence-oss/.helmignore ================================================ # Patterns to ignore when building packages. # This supports shell glob matching, relative path matching, and # negation (prefixed with !). Only one pattern per line. .DS_Store # Common VCS dirs .git/ .gitignore .bzr/ .bzrignore .hg/ .hgignore .svn/ # Common backup files *.swp *.bak *.tmp *.orig *~ # Various IDEs .project .idea/ *.tmproj .vscode/ ================================================ FILE: charts/telepresence-oss/Chart.yaml ================================================ apiVersion: v2 name: telepresence-oss description: A chart for deploying the server-side components of Telepresence type: application version: "1.1.1-bogus.overwritten.by.chart.go" keywords: - ambassador - telepresence - traffic-manager sources: - https://github.com/telepresenceio/telepresence icon: https://raw.githubusercontent.com/telepresenceio/telepresence.io/master/src/assets/images/telepresence-edgy.svg # Note: This is the version of the Traffic Manager that will be installed by # this chart. The telepresence CLI will always attempt to update the Traffic # Manager if it is not the same version as the CLI so ensure you are keeping # these in sync. appVersion: "1.1.1-bogus.overwritten.by.chart.go" annotations: artifacthub.io/license: Apache-2.0 ================================================ FILE: charts/telepresence-oss/README.md ================================================ # Telepresence [Telepresence](https://telepresence.io/) is a tool that allows for local development of microservices running in a remote Kubernetes cluster. This chart manages the server-side components of Telepresence so that an operations team can give limited access to the cluster for developers to work on their services. ## Install The telepresence binary embeds the helm chart, so the easiest way to install is: ```sh $ telepresence helm install [--set x=y | --values ] ``` ## Configuration The following tables lists the configurable parameters of the Telepresence chart and their default values. | Parameter | Description | Default | |------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------| | affinity | Define the `Node` Affinity and Anti-Affinity for the Traffic Manager. | `{}` | | agent.image.name | The name of the injected agent image | `""` | | agent.image.pullPolicy | Pull policy in the webhook for the traffic agent image | `IfNotPresent` | | agent.image.pullSecrets | The `Secret` storing any credentials needed to access the image in a private registry. | | | agent.image.tag | The tag for the injected agent image | `""` (Defined in `appVersion` Chart.yaml) | | agent.image.registry | The registry for the injected agent image | `ghcr.io/telepresenceio` | | agent.initResources | The resources for the injected init container | | | agent.logLevel | The logging level for the traffic-agent | defaults to logLevel | | agent.maxIdleTime | Maximum time the agent is idle (no engagements involving the workload) before it is cleaned up by the traffic manager | `0h` (infinite, never cleaned up) | | agent.mountPolicies | The policies for the agents. Key is either volume name or path prefix starting with '/' | `/tmp`: Local | | agent.resources | The resources for the injected agent container | | | agent.securityContext | The security context to use for the injected agent container | defaults to the securityContext of the first container of the app | | agent.initSecurityContext | The security context to use for the injected init container | `{}` | | agent.initContainer.enabled | Whether to enable/disable injection of the initContainer | true | | agentInjector.certificate.accessMethod | Method used by the agent injector to access the certificate (watch or mount). | `watch` | | agentInjector.certificate.certmanager.commonName | The common name of the generated Certmanager certificate. | `agent-injector` | | agentInjector.certificate.certmanager.duration | The certificate validity duration. (optional value) | `2160h0m0s` | | agentInjector.certificate.certmanager.issuerRef.kind | The Issuer kind to use to generate the self signed certificate. (Issuer of ClusterIssuer) | `Issuer` | | agentInjector.certificate.certmanager.issuerRef.name | The Issuer name to use to generate the self signed certificate. | `telepresence` | | agentInjector.certificate.method | Method used when generating the certificate used for mutating webhook (helm, supplied, or certmanager). | `helm` | | agentInjector.certificate.regenerate | Whether the certificate used for the mutating webhook should be regenerated. | `false` | | agentInjector.enabled | Enable/Disable the agent-injector and its webhook. | `true` | | agentInjector.name | Name to use with objects associated with the agent-injector. | `agent-injector` | | agentInjector.injectPolicy | Determines when an agent is injected, possible values are `OnDemand` and `WhenEnabled` | `OnDemand` | | agentInjector.secret.name | The name of the secret the agent-injector webhook uses for authorization with the kubernetes api will expose. | `mutator-webhook-tls` | | agentInjector.service.type | Type of service for the agent-injector. | `ClusterIP` | | agentInjector.webhook.admissionReviewVersions: | List of supported admissionReviewVersions. | `["v1"]` | | agentInjector.webhook.failurePolicy: | Action to take on unexpected failure or timeout of webhook. | `Ignore` | | agentInjector.webhook.name | The name of the agent-injector webhook | `agent-injector-webhook` | | ~~agentInjector.webhook.namespaceSelector~~: | The namespaceSelector used by the agent-injector webhook when the traffic-manager is not namespaced. Deprecated, use top level `namespaces` or `namespaceSelector` | {} | | agentInjector.webhook.port: | Port for the service that provides the admission webhook | `8443` | | agentInjector.webhook.reinvocationPolicy: | Specify if the webhook may be called again after the initial webhook call. Possible values are `Never` and `IfNeeded`. | `IfNeeded` | | agentInjector.webhook.servicePath: | Path to the service that provides the admission webhook | `/traffic-agent` | | agentInjector.webhook.sideEffects: | Any side effects the admission webhook makes outside of AdmissionReview. | `None` | | agentInjector.webhook.timeoutSeconds: | Timeout of the admission webhook | `5` | | apiPort | The port used by the Traffic Manager gRPC API | 8081 | | client.connectionTTL | Deprecated: using grpc.connectionTTL | `24h` | | client.dns.excludeSuffixes | Suffixes for which the client DNS resolver will always fail (or fallback in case of the overriding resolver) | `[".com", ".io", ".net", ".org", ".ru"]` | | client.dns.includeSuffixes | Suffixes for which the client DNS resolver will always attempt to do a lookup. Includes have higher priority than excludes. | `[]` | | client.routing.allowConflictingSubnets | Allow the specified subnets to be routed even if they conflict with other routes on the local machine. | `[]` | | client.routing.alsoProxySubnets | The virtual network interface of connected clients will also proxy these subnets | `[]` | | client.routing.neverProxySubnets | The virtual network interface of connected clients never proxy these subnets | `[]` | | clientRbac.create | Create RBAC resources for non-admin users with this release. | `false` | | ~~clientRbac.namespaced~~ | Restrict the users to specific namespaces. Deprecated and no longer used. | `false` | | clientRbac.namespaces | The namespaces to give users access to. | Traffic Manager's namespaces (unless dynamic) | | clientRbac.subjects | The user accounts to tie the created roles to. | `{}` | | grpc.connectionTTL | The time that the traffic-manager will retain a client connection without any sign of life from the workstation | `24h` | | grpc.maxReceiveSize | Max size of a gRCP message | `4Mi` | | hooks.busybox.image | The name of the image to use for busybox. | `busybox` | | hooks.busybox.imagePullSecrets | The `Secret` storing any credentials needed to access the image in a private registry. | `[]` | | hooks.busybox.registry | The registry to download the image from. | `docker.io` | | hooks.busybox.tag | Override the version of busybox to be installed. | `latest` | | hooks.curl.registry | The repository to download the image from. | `docker.io` | | hooks.curl.image | The name of the image to use for curl. | `curlimages/curl` | | hooks.curl.imagePullSecrets | The `Secret` storing any credentials needed to access the image in a private registry. | `[]` | | hooks.curl.pullPolicy | Pull policy used when pulling the curl image. | `IfNotPresent` | | hooks.curl.tag | Override the version of busybox to be installed. | `latest` | | hooks.podSecurityContext | The Kubernetes SecurityContext for the chart hooks `Pod` | `{}` | | image.registry | The repository to download the image from. Set `TELEPRESENCE_REGISTRY=image.registry` locally if changing this value. | `ghcr.io/telepresenceio` | | hooks.resources | Define resource requests and limits for the chart hooks | `{}` | | hooks.securityContext | The Kubernetes SecurityContext for the chart hooks `Container` | securityContext | | image.imagePullSecrets | The `Secret` storing any credentials needed to access the image in a private registry. | `[]` | | image.name | The name of the image to use for the traffic-manager | `tel2` | | image.pullPolicy | How the `Pod` will attempt to pull the image. | `IfNotPresent` | | image.tag | Override the version of the Traffic Manager to be installed. | `""` (Defined in `appVersion` Chart.yaml) | | livenessProbe | Define livenessProbe for the Traffic Manger. | `{}` | | logLevel | Define the logging level of the Traffic Manager | `debug` | | managerRbac.create | Create RBAC resources for traffic-manager with this release. | `true` | | ~~managerRbac.namespaced~~ | Whether the traffic manager should be restricted to specific namespaces. Deprecated and no longer used. | `false` | | ~~managerRbac.namespaces~~ | Which namespaces the traffic manager should be restricted to. Deprecated, use top level `namespaces` or `namespaceSelector` | `[]` | | maxNamespaceSpecificWatchers | Threshold controlling when the traffic-manager switches from using watchers for each managed namespace to using cluster-wide watchers. | `10` | | namespaces | Declares a fixed set of managed namespaces. Mutually exclusive to `namespaceSelector` | `[]` | | namespaceSelector | Declares the managed namespace using `matchLabels` and `matchExpressions`. Mutually exclusive to `namespaces` | `{}` | | nodeSelector | Define which `Node`s you want to the Traffic Manager to be deployed to. | `{}` | | podAnnotations | Annotations for the Traffic Manager `Pod` | `{}` | | podLabels | Labels for the Traffic Manager `Pod` | `{}` | | podCIDRs | Verbatim list of CIDRs that the cluster uses for pods. Only valid together with `podCIDRStrategy: environment` | `[]` | | podCIDRStrategy | Define the strategy that the traffic-manager uses to discover what CIDRs the cluster uses for pods | `auto` | | podSecurityContext | The Kubernetes SecurityContext for the `Pod` | `{}` | | priorityClassName | Name of the existing priority class to be used | `""` | | rbac.only | Only create the RBAC resources and omit the traffic-manger. | `false` | | readinessProbe | Define readinessProbe for the Traffic Manger. | `{}` | | resources | Define resource requests and limits for the Traffic Manger. | `{}` | | schedulerName | Specify a scheduler for Traffic Manager `Pod` and hooks `Pod`. | | | securityContext | The Kubernetes SecurityContext for the `Deployment` | `{"readOnlyRootFilesystem": true, "runAsNonRoot": true, "runAsUser": 1000}` | | service.type | The type of `Service` for the Traffic Manager. | `ClusterIP` | | telepresenceAPI.port | The port on agent's localhost where the Telepresence API server can be found | | | timeouts.agentArrival | The time that the traffic-manager will wait for the traffic-agent to arrive | `30s` | | tolerations | Define tolerations for the Traffic Manager to ignore `Node` taints. | `[]` | | workloads.argoRollouts.enabled | Enable/Disable the argo-rollouts integration. | `false` | | workloads.deployments.enabled | Enable/Disable the support for Deployments. | `true` | | workloads.replicaSets.enabled | Enable/Disable the support for ReplicaSets. | `true` | | workloads.statefulSets.enabled | Enable/Disable the support for StatefulSets. | `true` | ### RBAC Telepresence requires a cluster for installation but restricted RBAC roles can be used to give users access to create intercepts if they are not cluster admins. The chart gives you the ability to create these RBAC roles for your users and give access to the entire cluster or restrict to certain namespaces. You can also create a separate release for managing RBAC by setting `Values.rbac.only: true`. ### Namespace-scoped traffic manager Telepresence's Helm chart supports installing a Traffic Manager at the namespace scope. You might want to do this if you have multiple namespaces, say representing multiple different environments, and would like their Traffic Managers to be isolated from one another. To do this, set `managerRbac.namespaced=true` and `managerRbac.namespaces={a,b,c}` to manage namespaces `a`, `b` and `c`. **NOTE** Do not install namespace-scoped traffic managers and a cluster-scoped traffic manager in the same cluster! #### Namespace collision detection The Telepresence Helm chart will try to prevent namespace-scoped Traffic Managers from managing the same namespaces. It will do this by creating a ConfigMap, called `traffic-manager-claim`, in each namespace that a given install manages. So, for example, suppose you install one Traffic Manager to manage namespaces `a` and `b`, as: ```bash $ telepresence helm install --namespace a --set 'managerRbac.namespaced=true' --set 'managerRbac.namespaces={a,b}' ``` You might then attempt to install another Traffic Manager to manage namespaces `b` and `c`: ```bash $ telepresence helm install --namespace c --set 'managerRbac.namespaced=true' --set 'managerRbac.namespaces={b,c}' ``` This would fail with an error: ``` Error: rendered manifests contain a resource that already exists. Unable to continue with install: ConfigMap "traffic-manager-claim" in namespace "b" exists and cannot be imported into the current release: invalid ownership metadata; annotation validation error: key "meta.helm.sh/release-namespace" must equal "c": current value is "a" ``` To fix this error, fix the overlap either by removing `b` from the first install, or from the second. #### Pod CIDRs The traffic manager is responsible for keeping track of what CIDRs the cluster uses for the pods. The Telepresence client uses this information to configure the network so that it provides access to the pods. In some cases, the traffic-manager will not be able to retrieve this information, or will do it in a way that is inefficient. To remedy this, the strategy that the traffic manager uses can be configured using the `podCIDRStrategy`. | Value | Meaning | | -------------- | ------------------------------------------------------------------------------------------------------------------------- | | `auto` | First try `nodePodCIDRs` and if that fails, try `coverPodIPs` | | `coverPodIPs` | Obtain all IPs from the `podIP` and `podIPs` of all `Pod` resource statuses and calculate the CIDRs needed to cover them. | | `environment` | Pick the CIDRs from the traffic manager's `POD_CIDRS` environment variable. Use `podCIDRs` to set that variable. | | `nodePodCIDRs` | Obtain the CIDRs from the`podCIDR` and `podCIDRs` of all `Node` resource specifications. | ================================================ FILE: charts/telepresence-oss/templates/NOTES.txt ================================================ -------------------------------------------------------------------------------- Congratulations! You have successfully installed the Traffic Manager component of Telepresence! Now your users will be able to `telepresence connect` to this Cluster and create intercepts for their services! -------------------------------------------------------------------------------- Next Steps -------------------------------------------------------------------------------- - Take a look at our RBAC documentation for setting up the minimal required RBAC roles for your users at https://www.telepresence.io/docs/reference/rbac - Ensure that you are keeping up to date with Telepresence releases https://github.com/telepresenceio/telepresence/releases so that your Traffic Manager is the same version as the telepresence client your users are running! ================================================ FILE: charts/telepresence-oss/templates/_helpers.tpl ================================================ {{/* Expand the name of the chart. */}} {{- define "telepresence.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Traffic Manager deployment/service name - as of v2.20.3, must be "traffic-manager" to align with code base. */}} {{- define "traffic-manager.name" -}} {{- $name := default "traffic-manager" }} {{- print $name }} {{- end -}} {{- /* Traffic Manager Namespace */}} {{- define "traffic-manager.namespace" -}} {{- if .Values.isCI }} {{- print "ambassador" }} {{- else }} {{- printf "%s" .Release.Namespace }} {{- end }} {{- end -}} {{- /* traffic-manager.namespace-list extracts the list of namespace names from the namespaces variable. For backward compatibility, it will also consider names from the deprecated managerRbac.namespaces. It's an error if namespaces and managerRbac.namespaces both have values. */}} {{- define "private.namespace-list" }} {{- $names := .Values.namespaces }} {{- if .Values.managerRbac.namespaces }} {{- if $names }} {{- fail "namespaces and managerRbac.namespaces are mutually exclusive" }} {{- end }} {{- $names = .Values.managerRbac.namespaces }} {{- end }} {{- range $names }} {{- if not (regexMatch `^[a-z0-9]([a-z0-9-]*[a-z0-9])?$` .) }} {{- fail (printf "namespace %q is not a valid RFC 1123 namespace name" .) }} {{- end }} {{- else }} {{ $names = list }} {{- end }} {{- toJson (uniq ($names)) }} {{- end }} {{- define "private.namespaceSelector" }} {{- $labels := list }} {{- $matches := list }} {{- with .Values.namespaceSelector }} {{- with .matchLabels }} {{- $labels = . }} {{- end }} {{- with .matchExpressions }} {{- $matches = . }} {{- end }} {{- end }} {{- with fromJsonArray (include "private.namespace-list" $) }} {{- if (or $labels $matches) }}{{ fail "namespaces and namespaceSelector are mutually exclusive" }}{{ end }} {{- $matches = append $matches (dict "key" "kubernetes.io/metadata.name" "operator" "In" "values" .) }} {{- end }} {{- $selector := dict }} {{- with $labels }} {{- $selector = set $selector "matchLabels" . }} {{- end }} {{- with $matches }} {{- $selector = set $selector "matchExpressions" . }} {{- end }} {{- toJson $selector }} {{- end }} {{- /* traffic-manager.namespaceSelector extracts the selector to use when selecting namespaces. This selector will either include the namespaceSelector variable or include namespaces returned by the private.namespace-list definition. It will fail if both of them have values. The selector will default to the deprecated agentInjector.webhook.namespaceSelector when neither the namespaceSelector nor the private.namespace-list definition has any value. A selector can be dynamic or static. This in turn controls if telepresence is "cluster-wide" or "namespaced". A dynamic selector requires cluster-wide access for the traffic-manager, and only a static selector can serve as base when installing Role/RoleBinding pairs. A selector is considered static if it meets the following conditions: - The selector must have exactly one element in the `matchLabels` or the `matchExpression` list (if the element is in the `matchLabels` list, it is normalized into "key in [value]"). - The element must meet the following criteria: The `key` of the match expression must be "kubernetes.io/metadata.name". The `operator` of the match expression must be "In" (case sensitive). The `values` list of the match expression must contain at least one value. */}} {{- define "traffic-manager.namespaceSelector" }} {{- $selector := mustFromJson (include "private.namespaceSelector" $) }} {{- $legacy := false }} {{- if not $selector }} {{- with .Values.agentInjector.webhook.namespaceSelector }} {{- $legacy = true }} {{- $selector = . }} {{- end }} {{- end }} {{- if not (or $legacy (fromJsonArray (include "traffic-manager.namespaces" $))) }} {{- /*Ensure that his dynamic selector rejects "kube-system" and "kube-node-lease" */}} {{- $mes := $selector.matchExpressions }} {{- if not $mes }} {{- $mes = list }} {{- end }} {{- $selector = set $selector "matchExpressions" (append $mes (dict "key" "kubernetes.io/metadata.name" "operator" "NotIn" "values" (list "kube-system" "kube-node-lease"))) }} {{- end }} {{- toJson $selector }} {{- end }} {{- /* traffic-manager.namespaced will yield the string "true" if the traffic-manager.namespaceSelector that is static. */}} {{- define "traffic-manager.namespaced" }} {{- if fromJsonArray (include "traffic-manager.namespaces" $) }} {{- true }} {{- end }} {{- end }} {{- /* traffic-manager.namespaces will return a list of namespaces, provided that the traffic-manager.namespaceSelector is static. */}} {{- define "traffic-manager.namespaces" }} {{- $namespaces := list }} {{- with mustFromJson (include "private.namespaceSelector" $) }} {{- if and .matchExpressions (eq (len .matchExpressions) 1) (not .matchLabels) }} {{- with index .matchExpressions 0}} {{- if (and (eq .operator "In") (eq .key "kubernetes.io/metadata.name")) }} {{- $namespaces = .values }} {{- end }} {{- end }} {{- end }} {{- if and .matchLabels (eq (len .matchLabels) 1) (not .matchExpressions) }} {{- with get .matchLabels "kubernetes.io/metadata.name" }} {{- $namespaces = list . }} {{- end }} {{- end }} {{- end }} {{- toJson $namespaces }} {{- end }} {{- /* Create chart name and version as used by the chart label. */}} {{- define "telepresence.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{- /* Common labels */}} {{- define "telepresence.labels" -}} {{ include "telepresence.selectorLabels" $ }} helm.sh/chart: {{ include "telepresence.chart" $ }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- /* This value is intentionally undocumented -- it's used by the telepresence binary to determine ownership of the release */}} {{- if .Values.createdBy }} app.kubernetes.io/created-by: {{ .Values.createdBy }} {{- else }} app.kubernetes.io/created-by: {{ .Release.Service }} {{- end }} {{- end }} {{- /* Selector labels */}} {{- define "telepresence.selectorLabels" -}} app: traffic-manager telepresence: manager {{- end }} {{- /* Client RBAC name suffix */}} {{- define "telepresence.clientRbacName" -}} {{ printf "%s-%s" (include "telepresence.name" $) (include "traffic-manager.namespace" $) }} {{- end -}} {{- /* RBAC rules required to create an intercept in a namespace; excludes any rules that are always cluster wide. */}} {{- define "telepresence.clientRbacInterceptRules" -}} {{- /* Mandatory. Controls namespace access command completion experience */}} - apiGroups: [""] resources: ["pods"] verbs: ["get","list"] {{- /* "list" is only necessary if the client should be able to gather the pod logs */}} - apiGroups: [""] resources: ["pods/log"] verbs: ["get"] {{- /* All traffic will be routed via the traffic-manager unless a portforward can be created directly to a pod */}} - apiGroups: [""] resources: ["pods/portforward"] verbs: ["create"] {{- if and .Values.clientRbac .Values.clientRbac.ruleExtras }} {{ template "clientRbac-ruleExtras" . }} {{- end }} {{- end }} {{/* Kubernetes version */}} {{- define "kube.version.major" }} {{- $version := regexFind "^[0-9]+" .Capabilities.KubeVersion.Major -}} {{- printf "%s" $version -}} {{- end -}} {{- define "kube.version.minor" }} {{- $version := regexFind "^[0-9]+" .Capabilities.KubeVersion.Minor -}} {{- printf "%s" $version -}} {{- end -}} ================================================ FILE: charts/telepresence-oss/templates/agentInjectorWebhook.yaml ================================================ {{- if and (not (and .Values.rbac .Values.rbac.only)) .Values.agentInjector.enabled }} {{- $namespaceSelector := mustFromJson (include "traffic-manager.namespaceSelector" $) }} {{- /* Perform a check that the new namespaceSelector doesn't select namespaces that are already managed by some other traffic-manager. */}} {{- $namespaces := (lookup "v1" "Namespace" "" "").items }} {{- $configs := dict }} {{- $cmName := include "traffic-manager.name" $ }} {{- $cmNs := include "traffic-manager.namespace" $}} {{- /* Find all existing traffic-manager configmaps and their namespaceSelectors */}} {{- range $namespaces }} {{- $ns := .metadata.name }} {{- $cm := lookup "v1" "ConfigMap" $ns $cmName }} {{- with $cm }} {{- with fromYaml (get .data "namespace-selector.yaml" ) }} {{- $configs = set $configs $ns . }} {{- end }} {{- end }} {{- end }} {{- /* No use testing if the added selector is the only one */}} {{- if $configs }} {{- $configs = set $configs $cmNs $namespaceSelector }} {{- /* Validate that no selector overlaps with another */}} {{- $allManagedNamespaces := dict }} {{- range $configNs, $config := $configs }} {{- $rqs := $config.matchExpressions }} {{- /* Normalise the selector, i.e. turn each matchLabel into a machRequirement */}} {{- range $key, $value := $config.matchLabels }} {{- $rqs = append $rqs (dict "key" $key "operator" "In" "values" (list $value))}} {{- end }} {{- /* Figure out what namespaces this selector selects, and for each one, assert that it's not selected already */}} {{- range $namespaces }} {{- $ns := .metadata.name }} {{- $labels := .metadata.labels }} {{- $isMatch := true }} {{- range $rqs }} {{- $rqMatch := false }} {{- $val := get $labels .key }} {{- if eq .operator "In" }} {{- $rqMatch = has $val .values }} {{- else if eq .operator "NotIn" }} {{- $rqMatch = not (has $val .values) }} {{- else if eq .operator "Exists" }} {{- $rqMatch = not (eq $val "") }} {{- else if eq .operator "DoesNotExist" }} {{- $rqMatch = eq $val "" }} {{- else }} {{- fail printf "unsupported labelSelectorOperator %s" .operator}} {{- end }} {{- if not $rqMatch }} {{- $isMatch = false }} {{- break }} {{- end }} {{- end }} {{- if $isMatch }} {{- $conflictingConfig := get $allManagedNamespaces $ns }} {{- if $conflictingConfig }} {{- if eq $conflictingConfig $cmNs }} {{- $conflictingConfig = $configNs }} {{- end }} {{- fail (printf "traffic-manager in namespace %s already manages namespace %s" $conflictingConfig $ns) }} {{- end }} {{- $allManagedNamespaces = set $allManagedNamespaces $ns $configNs }} {{- end }} {{- end }} {{- end }} {{- end }} {{- $altNames := list ( printf "agent-injector.%s" (include "traffic-manager.namespace" $)) ( printf "agent-injector.%s.svc" (include "traffic-manager.namespace" $)) -}} {{- $genCA := genCA "agent-injector-ca" 365 -}} {{- $genCert := genSignedCert "agent-injector" nil $altNames 365 $genCA -}} {{- $secretData := (lookup "v1" "Secret" (include "traffic-manager.namespace" $) .Values.agentInjector.secret.name).data -}} {{- $reinvocationPolicy := .Values.agentInjector.webhook.reinvocationPolicy }} {{- if (and .Values.agentInjector.mutationAware (not (eq $reinvocationPolicy "IfNeeded"))) }} {{- fail (printf "agentInjector.mutationAware=true cannot be combined with reinvocationPolicy=%s" $reinvocationPolicy) }} {{- end }} --- apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: {{- if eq .Values.agentInjector.certificate.method "certmanager" }} annotations: cert-manager.io/inject-ca-from: {{ include "traffic-manager.namespace" $}}/{{ .Values.agentInjector.secret.name }} {{- end }} name: {{ .Values.agentInjector.webhook.name }}-{{ include "traffic-manager.namespace" $ }} labels: {{- include "telepresence.labels" $ | nindent 4 }} webhooks: {{- with .Values.agentInjector.webhook.admissionReviewVersions }} - admissionReviewVersions: {{- toYaml . | nindent 2 }} {{- end }} clientConfig: {{- if not (eq .Values.agentInjector.certificate.method "certmanager") }} {{- if and ($secretData) (or (not .Values.agentInjector.certificate.regenerate) (eq .Values.agentInjector.certificate.method "supplied") )}} caBundle: {{ or (get $secretData "ca.crt") (get $secretData "ca.pem") }} {{- else }} caBundle: {{ $genCA.Cert | b64enc }} {{- end }} {{- end }} service: name: {{ .Values.agentInjector.name }} namespace: {{ include "traffic-manager.namespace" $ }} path: {{ .Values.agentInjector.webhook.servicePath }} port: {{ .Values.agentInjector.webhook.port }} rules: - apiGroups: - "" apiVersions: - v1 operations: - CREATE - DELETE resources: - pods scope: '*' failurePolicy: {{ .Values.agentInjector.webhook.failurePolicy }} reinvocationPolicy: {{ $reinvocationPolicy }} name: agent-injector-{{ include "traffic-manager.namespace" $ }}.telepresence.io sideEffects: {{ .Values.agentInjector.webhook.sideEffects }} timeoutSeconds: {{ .Values.agentInjector.webhook.timeoutSeconds }} namespaceSelector: {{- toYaml $namespaceSelector | nindent 4 }} {{- if not (or (eq .Values.agentInjector.certificate.method "certmanager") (eq .Values.agentInjector.certificate.method "supplied")) }} --- apiVersion: v1 kind: Secret metadata: name: {{ .Values.agentInjector.secret.name }} namespace: {{ include "traffic-manager.namespace" $ }} labels: {{- include "telepresence.labels" $ | nindent 4 }} data: {{- if and ($secretData) (not .Values.agentInjector.certificate.regenerate) }} ca.crt: {{ or (get $secretData "ca.crt") (get $secretData "ca.pem") }} tls.crt: {{ or (get $secretData "tls.crt") (get $secretData "crt.pem") }} tls.key: {{ or (get $secretData "tls.key") (get $secretData "key.pem") }} {{- else }} ca.crt: {{ $genCA.Cert | b64enc }} tls.crt: {{ $genCert.Cert | b64enc }} tls.key: {{ $genCert.Key | b64enc }} {{- end }} {{- end }} {{- end }} ================================================ FILE: charts/telepresence-oss/templates/certificate.yaml ================================================ {{- if and (eq .Values.agentInjector.certificate.method "certmanager") .Values.agentInjector.enabled }} apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: {{ .Values.agentInjector.secret.name }} spec: secretName: {{ .Values.agentInjector.secret.name }} dnsNames: - {{ (printf "%s.%s" .Values.agentInjector.name .Release.Namespace ) }} - {{ (printf "%s.%s.svc" .Values.agentInjector.name .Release.Namespace ) }} {{- with .Values.agentInjector.certificate.certmanager }} {{- toYaml . | nindent 2 }} {{- end }} {{- end }} ================================================ FILE: charts/telepresence-oss/templates/clientRbac/cluster-scope.yaml ================================================ {{- /* These are the cluster-wide rbac roles + bindings that will be used by users who want to use telepresence once its components have been set up in the cluster. */}} {{- with .Values.clientRbac }} {{- if (and .create (not (or .namespaces (include "traffic-manager.namespaced" $)))) }} {{- $roleName := include "telepresence.clientRbacName" $ }} --- kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: {{ $roleName }} labels: {{- include "telepresence.labels" $ | nindent 4 }} rules: - apiGroups: [""] resources: ["namespaces"] verbs: ["get", "list", "watch"] {{- include "telepresence.clientRbacInterceptRules" $ }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: {{ $roleName }} labels: {{- include "telepresence.labels" $ | nindent 4 }} subjects: {{ toYaml .subjects }} roleRef: kind: ClusterRole name: {{ $roleName }} apiGroup: rbac.authorization.k8s.io {{- end }} {{- end }} ================================================ FILE: charts/telepresence-oss/templates/clientRbac/connect.yaml ================================================ {{- with .Values.clientRbac }} {{- if .create }} {{- /* Client must have the following RBAC in the traffic-manager.namespace to establish a port-forward to the traffic-manager pod. */}} kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: name: traffic-manager-connect namespace: {{ include "traffic-manager.namespace" $ }} labels: {{- include "telepresence.labels" $ | nindent 4 }} rules: - apiGroups: [""] resources: ["pods"] verbs: ["get", "list"] - apiGroups: [""] resources: ["services"] resourceNames: - {{ include "traffic-manager.name" $ }} verbs: ["get"] - apiGroups: [""] resources: ["pods/portforward"] verbs: ["create"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: traffic-manager-connect namespace: {{ include "traffic-manager.namespace" $ }} labels: {{- include "telepresence.labels" $ | nindent 4 }} subjects: {{ toYaml .subjects }} roleRef: apiGroup: rbac.authorization.k8s.io name: traffic-manager-connect kind: Role {{- end }} {{- end }} ================================================ FILE: charts/telepresence-oss/templates/clientRbac/namespace-scope.yaml ================================================ {{- /* These are the namespace-scoped rbac roles + bindings that will be used by users who want to use telepresence once its components have been set up in the cluster. */}} {{- with .Values.clientRbac }} {{- if .create }} {{- $subjects := .subjects }} {{- if (not $subjects) }} {{- /* fail comes out really ugly if we just do fail "the message here" */}} {{- $msg := "You must set clientRbac.subjects to a list of valid rbac subjects. See the kubernetes docs for more: https://kubernetes.io/docs/reference/access-authn-authz/rbac/#referring-to-subjects" }} {{- fail $msg }} {{- end }} {{- $namespaces := .namespaces }} {{- if not $namespaces }} {{ $namespaces = fromJsonArray (include "traffic-manager.namespaces" $) }} {{- end }} {{- $name := include "telepresence.clientRbacName" $ }} {{- $labels := include "telepresence.labels" $ | nindent 4 }} {{- range $namespaces }} --- kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: name: {{ $name }} namespace: {{ . }} labels: {{- $labels }} rules: {{ include "telepresence.clientRbacInterceptRules" $ }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: {{ $name }} namespace: {{ . }} labels: {{- $labels }} subjects: {{- toYaml $subjects | nindent 0}} roleRef: kind: Role name: {{ $name }} apiGroup: rbac.authorization.k8s.io {{- end }} {{- $managerNamespace := include "traffic-manager.namespace" $ }} {{- if and $namespaces (not (has $managerNamespace $namespaces)) }} {{- /* This is required only if the client should be permitted to gather the traffic-manager logs, and it is only required when the traffic-manager isn't managing its own namespace. */}} --- kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: name: traffic-manager-logs namespace: {{ $managerNamespace }} labels: {{- $labels }} rules: - apiGroups: [""] resources: ["pods/log"] verbs: ["get"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: traffic-manager-logs namespace: {{ $managerNamespace }} labels: {{- $labels }} subjects: {{ toYaml $subjects }} roleRef: kind: Role name: traffic-manager-logs apiGroup: rbac.authorization.k8s.io {{- end }} {{- end }} {{- end }} ================================================ FILE: charts/telepresence-oss/templates/deployment.yaml ================================================ {{- with .Values }} {{- if not (and .rbac .rbac.only) }} apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "traffic-manager.name" $ }} namespace: {{ include "traffic-manager.namespace" $ }} labels: {{ include "telepresence.labels" $ | nindent 4 }} spec: replicas: {{ .replicaCount }} selector: matchLabels: {{ include "telepresence.selectorLabels" $ | nindent 6 }} strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 # Allows creating one extra pod during the update maxUnavailable: 0 # Ensures no pods are terminated until new ones are ready template: metadata: {{- with .podAnnotations }} annotations: {{ toYaml . | nindent 8 }} {{- end }} labels: {{ include "telepresence.selectorLabels" $ | nindent 8 }} {{- with .podLabels }} {{ toYaml . | nindent 8 }} {{- end }} spec: {{- with .image.imagePullSecrets }} imagePullSecrets: {{ toYaml . | nindent 8 }} {{- end }} securityContext: {{ toYaml .podSecurityContext | nindent 8 }} {{- if .hostNetwork }} hostNetwork: true {{- end }} containers: - name: {{ include "traffic-manager.name" $ }} securityContext: {{ toYaml .securityContext | nindent 12 }} {{- with .image }} image: "{{ .registry }}/{{ .name }}:{{ .tag | default $.Chart.AppVersion }}" imagePullPolicy: {{ .pullPolicy }} {{- end }} env: - name: LOG_LEVEL value: {{ .logLevel }} {{- with .image }} - name: REGISTRY value: "{{ .registry }}" {{- end }} - name: SERVER_PORT value: {{ .apiPort | quote }} - name: POD_CIDR_STRATEGY value: {{ .podCIDRStrategy }} {{- with .podCIDRs }} - name: POD_CIDRS value: "{{ join " " . }}" {{- end }} {{- if .agentInjector.enabled }} - name: MUTATOR_WEBHOOK_PORT value: {{ .agentInjector.webhook.port | quote }} - name: AGENT_INJECTOR_SECRET {{- if eq .agentInjector.certificate.accessMethod "mount" }} value: /var/run/secrets/tls {{- else }} value: {{ .agentInjector.secret.name }} {{- end }} {{- end }} {{- with .telepresenceAPI }} {{- if .port }} - name: AGENT_REST_API_PORT value: {{ .port | quote }} {{- end }} {{- end }} {{- with .grpc }} {{- if .maxReceiveSize }} - name: GRPC_MAX_RECEIVE_SIZE value: {{ .maxReceiveSize }} {{- if and .connectionTTL (not $.Values.client.connectionTTL) }} - name: CLIENT_CONNECTION_TTL value: {{ .connectionTTL }} {{- end }} {{- end }} {{- end }} {{- if .workloads }} {{- with .workloads }} - name: ENABLED_WORKLOAD_KINDS value: >- {{- if or (not .deployments) .deployments.enabled }} Deployment {{- end }} {{- if or (not .statefulSets) .statefulSets.enabled }} StatefulSet {{- end }} {{- if or (not .replicaSets) .replicaSets.enabled }} ReplicaSet {{- end }} {{- if and .argoRollouts .argoRollouts.enabled }} Rollout {{- end }} {{- end }} {{- else }} - name: ENABLED_WORKLOAD_KINDS value: Deployment StatefulSet ReplicaSet {{- end }} {{- with .intercept }} {{- if not (eq .allowGlobalIntercepts nil) }} - name: INTERCEPT_ALLOW_GLOBAL value: {{ .allowGlobalIntercepts | quote }} {{- end }} - name: INTERCEPT_INACTIVE_BLOCK_TIMEOUT value: {{ .inactiveBlockTimeout }} {{- end }} {{- if .agentInjector.enabled }} {{- /* Traffic agent injector configuration */}} - name: AGENT_ARRIVAL_TIMEOUT value: {{ quote (default "30s" .timeouts.agentArrival) }} {{- with .agentInjector }} - name: AGENT_INJECT_POLICY value: {{ .injectPolicy }} - name: AGENT_INJECTOR_NAME value: {{ .name | quote }} - name: AGENT_INJECTOR_MUTATION_AWARE value: {{ (default false .mutationAware) | quote }} {{- end }} {{- /* Traffic agent configuration */}} {{- with .agent }} {{- if .logLevel }} - name: AGENT_LOG_LEVEL value: {{ .logLevel }} {{- end }} {{- if .port }} - name: AGENT_PORT value: {{ .port | quote }} {{- end }} - name: AGENT_ENABLE_H2C_PROBING value: {{ (default false .enableH2cProbing) | quote }} - name: AGENT_CONSUMPTION_METRICS value: {{ (default true .enableConsumptionMetrics) | quote }} {{- if .resources }} - name: AGENT_RESOURCES value: '{{ toJson .resources }}' {{- end }} {{- if .initResources }} - name: AGENT_INIT_RESOURCES value: '{{ toJson .initResources }}' {{- end }} {{- if .mountPolicies }} - name: AGENT_MOUNT_POLICIES value: '{{ toJson .mountPolicies }}' {{- end }} {{- if .maxIdleTime }} - name: AGENT_MAX_IDLE_TIME value: {{ .maxIdleTime }} {{- end }} {{- with .initContainer }} - name: AGENT_INIT_CONTAINER_ENABLED value: {{ .enabled | quote }} {{- end }} {{- with .image }} {{- if .name }} - name: AGENT_IMAGE_NAME value: {{ .name }} {{- end }} {{- if .tag }} - name: AGENT_IMAGE_TAG value: {{ .tag }} {{- end }} {{- if .registry }} - name: AGENT_REGISTRY value: {{ .registry }} {{- end }} {{- with .pullSecrets }} - name: AGENT_IMAGE_PULL_SECRETS value: '{{ toJson . }}' {{- end }} - name: AGENT_IMAGE_PULL_POLICY value: {{ .pullPolicy }} {{- end }} {{- /* must check against nil. An empty security context is a valid override */}} {{- if not (eq .securityContext nil) }} - name: AGENT_SECURITY_CONTEXT value: '{{ toJson .securityContext }}' {{- end }} {{- /* must check against nil. An empty security context is a valid override */}} {{- if not (eq .initSecurityContext nil) }} - name: AGENT_INIT_SECURITY_CONTEXT value: '{{ toJson .initSecurityContext }}' {{- end }} {{- if .watchRetryInterval }} - name: AGENT_WATCH_RETRY_INTERVAL value: .watchRetryInterval {{- end }} {{- end }} {{- with fromJsonArray (include "traffic-manager.namespaces" $) }} {{- /* This environment variable is not used, it is here to force a redeploy of the traffic manager when the list changes, because it updates roles and rolebindings and potentially also changes from roles to clusterroles or vice versa. */}} - name: NOT_USED_NSS value: {{ toJson . | quote }} {{- end }} {{- end }} {{- with .prometheus }} {{- if .port }} # 0 is false - name: PROMETHEUS_PORT value: "{{ .port }}" {{- end }} {{- if .dropClientLabel }} - name: PROMETHEUS_DROP_CLIENT_LABEL value: "{{ .dropClientLabel }}" {{- end }} {{- end }} - name: MAX_NAMESPACE_SPECIFIC_WATCHERS value: {{.maxNamespaceSpecificWatchers | quote }} - name: MANAGER_NAMESPACE valueFrom: fieldRef: apiVersion: v1 fieldPath: metadata.namespace - name: POD_IP valueFrom: fieldRef: apiVersion: v1 fieldPath: status.podIP {{- /* Client configuration */}} {{- with .client }} {{- if .connectionTTL }} - name: CLIENT_CONNECTION_TTL value: {{ .connectionTTL }} {{- end }} {{- with .routing }} {{- if .alsoProxySubnets }} - name: CLIENT_ROUTING_ALSO_PROXY_SUBNETS value: "{{ join " " .alsoProxySubnets }}" {{- end }} {{- if .neverProxySubnets }} - name: CLIENT_ROUTING_NEVER_PROXY_SUBNETS value: "{{ join " " .neverProxySubnets }}" {{- end }} {{- if .allowConflictingSubnets }} - name: CLIENT_ROUTING_ALLOW_CONFLICTING_SUBNETS value: "{{ join " " .allowConflictingSubnets }}" {{- end }} {{- end }} {{- with .dns }} {{- with .excludeSuffixes }} - name: CLIENT_DNS_EXCLUDE_SUFFIXES value: "{{ join " " . }}" {{- end }} {{- with .includeSuffixes }} - name: CLIENT_DNS_INCLUDE_SUFFIXES value: "{{ join " " . }}" {{- end }} {{- end }} {{- end }} {{- with .compatibility }} {{- if .version }} - name: COMPATIBILITY_VERSION value: {{ .version }} {{- end }} {{- end }} ports: - name: api containerPort: {{ .apiPort }} - name: https containerPort: {{ .agentInjector.webhook.port }} {{- if .prometheus.port }} # 0 is false - name: prometheus containerPort: {{ .prometheus.port }} {{- end }} {{- with .livenessProbe }} livenessProbe: {{ toYaml . | nindent 12 }} {{- end }} startupProbe: {{- with .startupProbe }} {{ toYaml . | nindent 12 }} {{- else }} grpc: port: {{ .apiPort }} periodSeconds: 2 failureThreshold: 5 {{- end }} {{- with .readinessProbe }} readinessProbe: {{ toYaml . | nindent 12 }} {{- end }} {{- with .resources }} resources: {{ toYaml . | nindent 12 }} {{- end }} {{- if eq .agentInjector.certificate.accessMethod "mount" }} volumeMounts: {{- if .agentInjector.enabled }} - name: tls mountPath: /var/run/secrets/tls readOnly: true {{- end }} {{- end }} {{- with .schedulerName }} schedulerName: {{ . }} {{- end }} {{- with .nodeSelector }} nodeSelector: {{ toYaml . | nindent 8 }} {{- end }} {{- with .affinity }} affinity: {{ toYaml . | nindent 8 }} {{- end }} {{- with .tolerations }} tolerations: {{ toYaml . | nindent 8 }} {{- end }} {{- with .priorityClassName }} priorityClassName: {{ . | quote }} {{- end }} {{- if eq .agentInjector.certificate.accessMethod "mount" }} volumes: {{- if .agentInjector.enabled }} - name: tls secret: defaultMode: 420 secretName: {{ .agentInjector.secret.name }} {{- end }} {{- end }} serviceAccountName: traffic-manager {{- end }} {{- end }} ================================================ FILE: charts/telepresence-oss/templates/issuer.yaml ================================================ {{- if and (eq .Values.agentInjector.certificate.method "certmanager") .Values.agentInjector.enabled }} apiVersion: cert-manager.io/v1 kind: {{ .Values.agentInjector.certificate.certmanager.issuerRef.kind }} metadata: name: {{ .Values.agentInjector.certificate.certmanager.issuerRef.name }} spec: selfSigned: {} {{- end }} ================================================ FILE: charts/telepresence-oss/templates/pre-delete-hook.yaml ================================================ {{- if and (not (and .Values.rbac .Values.rbac.only)) .Values.agentInjector.enabled }} apiVersion: batch/v1 kind: Job metadata: name: uninstall-agents namespace: {{ include "traffic-manager.namespace" $ }} labels: app.kubernetes.io/managed-by: {{ .Release.Service | quote }} app.kubernetes.io/instance: {{ .Release.Name | quote }} app.kubernetes.io/version: {{ .Chart.AppVersion }} helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" annotations: {{- /* This is what defines this resource as a hook. Without this line, the job is considered part of the release. */}} "helm.sh/hook": pre-delete "helm.sh/hook-weight": "-5" "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded spec: backoffLimit: 1 template: metadata: name: uninstall-agents labels: app.kubernetes.io/managed-by: {{ .Release.Service | quote }} app.kubernetes.io/instance: {{ .Release.Name | quote }} helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" spec: securityContext: {{- toYaml .Values.hooks.podSecurityContext | nindent 8 }} restartPolicy: Never {{- with .Values.hooks.curl.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} containers: - name: uninstall-agents securityContext: {{- if .Values.hooks.securityContext }} {{- toYaml .Values.hooks.securityContext | nindent 12 }} {{- else }} {{- toYaml .Values.securityContext | nindent 12 }} {{- end }} image: "{{ .Values.hooks.curl.registry }}/{{ .Values.hooks.curl.image }}:{{ .Values.hooks.curl.tag }}" imagePullPolicy: {{ .Values.hooks.curl.pullPolicy }} volumeMounts: - name: secret-volume mountPath: /secret env: - name: CURL_CA_BUNDLE value: /secret/ca.crt resources: {{- toYaml .Values.hooks.resources | nindent 12 }} command: - sh - -c args: - 'curl --fail --connect-timeout 5 --max-time 60 --request DELETE https://{{ .Values.agentInjector.name }}.{{ include "traffic-manager.namespace" $ }}:{{ .Values.agentInjector.webhook.port }}/uninstall || exit 0' volumes: - name: secret-volume secret: secretName: {{ .Values.agentInjector.secret.name }} {{- with .Values.schedulerName }} schedulerName: {{ . }} {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} {{- end }} ================================================ FILE: charts/telepresence-oss/templates/routecontroller-daemonset.yaml ================================================ {{- $localCluster := or (eq .Values.image.registry "local") (hasPrefix "localhost:" .Values.image.registry) }} {{- $rcEnabled := ternary $localCluster .Values.routeController.enabled (kindIs "invalid" .Values.routeController.enabled) }} {{- if and .Values.routeController $rcEnabled }} apiVersion: apps/v1 kind: DaemonSet metadata: name: route-controller namespace: {{ include "traffic-manager.namespace" $ }} labels: {{ include "telepresence.labels" $ | nindent 4 }} spec: selector: matchLabels: app: route-controller template: metadata: labels: app: route-controller spec: hostNetwork: true dnsPolicy: ClusterFirstWithHostNet serviceAccountName: route-controller containers: - name: route-controller image: "{{ coalesce .Values.routeController.image.registry .Values.image.registry }}/{{ .Values.routeController.image.name }}:{{ .Chart.AppVersion }}" imagePullPolicy: {{ coalesce .Values.routeController.image.pullPolicy .Values.image.pullPolicy }} env: - name: LOG_LEVEL value: {{ coalesce .Values.routeController.logLevel .Values.logLevel }} {{- with .Values.routeController.serviceCIDRs }} - name: SERVICE_CIDRS value: {{ join "," . }} {{- end }} securityContext: capabilities: add: ["NET_ADMIN"] resources: requests: cpu: 10m memory: 32Mi limits: memory: 64Mi {{- end }} ================================================ FILE: charts/telepresence-oss/templates/routecontroller-rbac.yaml ================================================ {{- $localCluster := or (eq .Values.image.registry "local") (hasPrefix "localhost:" .Values.image.registry) }} {{- $rcEnabled := ternary $localCluster .Values.routeController.enabled (kindIs "invalid" .Values.routeController.enabled) }} {{- if and .Values.routeController $rcEnabled }} {{- $roleName := printf "route-controller-%s" (include "traffic-manager.namespace" $) }} --- apiVersion: v1 kind: ServiceAccount metadata: name: route-controller namespace: {{ include "traffic-manager.namespace" $ }} labels: {{- include "telepresence.labels" $ | nindent 4 }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: {{ $roleName }} labels: {{- include "telepresence.labels" $ | nindent 4 }} rules: - apiGroups: ["networking.k8s.io"] resources: ["servicecidrs"] verbs: ["get", "list"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: {{ $roleName }} labels: {{- include "telepresence.labels" $ | nindent 4 }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: {{ $roleName }} subjects: - kind: ServiceAccount name: route-controller namespace: {{ include "traffic-manager.namespace" $ }} {{- end }} ================================================ FILE: charts/telepresence-oss/templates/service.yaml ================================================ {{- with .Values }} {{- if not (and .rbac .rbac.only) }} apiVersion: v1 kind: Service metadata: name: {{ include "traffic-manager.name" $ }} namespace: {{ include "traffic-manager.namespace" $ }} labels: {{- include "telepresence.labels" $ | nindent 4 }} spec: type: {{ .service.type }} clusterIP: None ports: - name: api port: {{ .apiPort }} targetPort: api selector: {{- include "telepresence.selectorLabels" $ | nindent 4 }} {{- if .agentInjector.enabled }} --- apiVersion: v1 kind: Service metadata: name: {{ .agentInjector.name }} namespace: {{ include "traffic-manager.namespace" $ }} labels: {{- include "telepresence.labels" $ | nindent 4 }} spec: type: {{ .service.type }} ports: - name: https port: {{ .agentInjector.webhook.port }} targetPort: https selector: {{- include "telepresence.selectorLabels" $ | nindent 4 }} {{- end }} {{- if .prometheus.port }} # 0 is false --- apiVersion: v1 kind: Service metadata: name: telepresence-prometheus namespace: {{ include "traffic-manager.namespace" $ }} labels: name: telepresence-prometheus spec: type: {{ .service.type }} ports: - name: telepresence-prometheus port: 80 targetPort: prometheus selector: {{- include "telepresence.selectorLabels" $ | nindent 4 }} {{- end }} {{- end }} {{- end }} ================================================ FILE: charts/telepresence-oss/templates/tests/test-connection.yaml ================================================ {{- if not (and .Values.rbac .Values.rbac.only) }} apiVersion: v1 kind: Pod metadata: name: "{{ include "traffic-manager.name" $ }}-test-connection" namespace: {{ include "traffic-manager.namespace" $ }} labels: {{- include "telepresence.labels" $ | nindent 4 }} annotations: "helm.sh/hook": test-success spec: {{- with .Values.hooks.busybox.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 2 }} {{- end }} containers: - name: wget image: "{{ .Values.hooks.busybox.registry }}/{{ .Values.hooks.busybox.image }}:{{ .Values.hooks.busybox.tag }}" command: ['wget'] args: ['{{ include "traffic-manager.name" $ }}:8081'] restartPolicy: Never {{- end }} ================================================ FILE: charts/telepresence-oss/templates/trafficManager-configmap.yaml ================================================ apiVersion: v1 kind: ConfigMap metadata: name: {{ include "traffic-manager.name" $ }} namespace: {{ include "traffic-manager.namespace" $ }} labels: {{- include "telepresence.labels" $ | nindent 4 }} data: {{- if .Values.client }} client.yaml: | {{- toYaml .Values.client | nindent 4 }} {{- end }} {{- with .Values.intercept }} {{- if .environment }} agent-env.yaml: | {{- toYaml .environment | nindent 4 }} {{- end }} {{- end }} namespace-selector.yaml: | {{- toYaml (mustFromJson (include "traffic-manager.namespaceSelector" $)) | nindent 4 }} agent-state.yaml: | AgentStates: {} ================================================ FILE: charts/telepresence-oss/templates/trafficManagerRbac/cluster-scope.yaml ================================================ {{- with .Values }} {{- if and .managerRbac.create (not (include "traffic-manager.namespaced" $)) }} {{- /* This file contains all cluster-scoped permissions that the traffic manager needs. This will be larger if namespaced: false, or smaller if it is true This will also likely expand over time as we move more things from the clients domain into the traffic-manager. But the good news there is that it will require less permissions in clientRbac.yaml */}} {{- $roleName := (printf "traffic-manager-%s" (include "traffic-manager.namespace" $)) }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: {{ $roleName }} labels: {{- include "telepresence.labels" $ | nindent 4 }} rules: - apiGroups: - "" resources: - services verbs: - update {{/* Only needed for upgrade of older versions */}} - apiGroups: - "" resources: - nodes - services - namespaces - pods verbs: - list - get - watch {{- if .agentInjector.enabled }} - apiGroups: - "" resources: - pods/eviction verbs: - create {{- end }} - apiGroups: - "" resources: - pods/log verbs: - get - apiGroups: - "apps" resources: - deployments - replicasets - statefulsets verbs: - get - list - watch {{- if .agentInjector.enabled }} - patch {{- end }} {{- if .workloads.argoRollouts.enabled }} - apiGroups: - "argoproj.io" resources: - rollouts verbs: - get - list - watch {{- if .agentInjector.enabled }} - patch {{- end }} {{- end }} - apiGroups: - "events.k8s.io" resources: - events verbs: - get - watch - apiGroups: - "networking.k8s.io" resources: - servicecidrs verbs: - list --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: {{ $roleName }} labels: {{- include "telepresence.labels" $ | nindent 4 }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: {{ $roleName }} subjects: - kind: ServiceAccount name: traffic-manager namespace: {{ include "traffic-manager.namespace" $ }} {{- end }} {{- end }} ================================================ FILE: charts/telepresence-oss/templates/trafficManagerRbac/namespace-scope.yaml ================================================ {{- if .Values.managerRbac.create }} {{- /* This file contains the various namespace-scoped roles + bindings that the traffic-manager needs. This will likely expand over time as we move more things from the clients domain into the traffic-manager. But the good news there is that it will require less permissions in clientRbac.yaml */}} {{- $managerNamespace := include "traffic-manager.namespace" $}} {{- $namespaces := fromJsonArray (include "traffic-manager.namespaces" $)}} {{- if $namespaces }} {{- $interceptEnabled := .Values.agentInjector.enabled}} {{- $argoRolloutsEnabled := .Values.workloads.argoRollouts.enabled}} {{- $allNamespaces := uniq (append $namespaces $managerNamespace)}} {{- range $allNamespaces }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: traffic-manager namespace: {{ . }} labels: {{- include "telepresence.labels" $ | nindent 4 }} rules: - apiGroups: - "" resources: - services verbs: - update {{/* Only needed for upgrade of older versions */}} - apiGroups: - "" resources: - services - pods verbs: - list - get - watch {{- if $interceptEnabled }} - apiGroups: - "" resources: - pods/eviction verbs: - create {{- end }} - apiGroups: - "" resources: - pods/log verbs: - get - apiGroups: - "" resources: - configmaps verbs: - list - get - watch resourceNames: {{- if eq . $managerNamespace }} - {{ include "traffic-manager.name" $ }} {{- end }} - apiGroups: - "apps" resources: - deployments - replicasets - statefulsets verbs: - get - list - watch {{- if $interceptEnabled }} - patch {{- end }} {{- if $argoRolloutsEnabled }} - apiGroups: - "argoproj.io" resources: - rollouts verbs: - get - list - watch {{- if $interceptEnabled }} - patch {{- end }} {{- end }} - apiGroups: - "events.k8s.io" resources: - events verbs: - get - watch {{- if eq . $managerNamespace }} {{- /* Must be able to get the manager namespace in order to get the install-id */}} - apiGroups: - "" resources: - namespaces resourceNames: - {{ . }} verbs: - get {{- if and (eq (int $.Capabilities.KubeVersion.Major) 1) (lt (int $.Capabilities.KubeVersion.Minor) 33) }} {{- /* Must be able to make an unsuccessful attempt to create a dummy service in order to receive the error message containing correct service CIDR */}} - apiGroups: - "" resources: - services verbs: - create {{- end }} {{- end }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: traffic-manager namespace: {{ . }} labels: {{- include "telepresence.labels" $ | nindent 4 }} roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: traffic-manager subjects: - kind: ServiceAccount name: traffic-manager namespace: {{ $managerNamespace }} {{- end }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: traffic-manager-cluster-wide-{{ $managerNamespace }} labels: {{- include "telepresence.labels" $ | nindent 4 }} rules: - apiGroups: - "networking.k8s.io" resources: - servicecidrs verbs: - list --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: traffic-manager-cluster-wide-{{ $managerNamespace }} labels: {{- include "telepresence.labels" $ | nindent 4 }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: traffic-manager-cluster-wide-{{ $managerNamespace }} subjects: - kind: ServiceAccount name: traffic-manager namespace: {{ $managerNamespace }} {{- else }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: namespace: {{ $managerNamespace }} name: traffic-manager labels: {{- include "telepresence.labels" $ | nindent 4 }} rules: - apiGroups: - "" resources: - services verbs: - create - apiGroups: - "" resources: - configmaps verbs: - get - list - watch - patch - update resourceNames: - {{ include "traffic-manager.name" $ }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: traffic-manager namespace: {{ $managerNamespace }} labels: {{- include "telepresence.labels" $ | nindent 4 }} roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: traffic-manager subjects: - kind: ServiceAccount name: traffic-manager namespace: {{ $managerNamespace }} {{- end }} {{- end }} ================================================ FILE: charts/telepresence-oss/templates/trafficManagerRbac/service-account.yaml ================================================ {{- if .Values.managerRbac.create }} {{- /* This file contains the serviceAccount used for the traffic-manager deployment. */}} apiVersion: v1 kind: ServiceAccount metadata: name: traffic-manager namespace: {{ include "traffic-manager.namespace" $ }} labels: {{- include "telepresence.labels" $ | nindent 4 }} {{- end }} ================================================ FILE: charts/telepresence-oss/templates/trafficManagerRbac/webhook-secret.yaml ================================================ {{- if and (not (eq .Values.agentInjector.certificate.accessMethod "mount")) .Values.agentInjector.enabled }} apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: namespace: {{ include "traffic-manager.namespace" $ }} name: agent-injector-webhook-secret labels: {{- include "telepresence.labels" $ | nindent 4 }} rules: - apiGroups: - "" resources: - secrets resourceNames: [ {{ .Values.agentInjector.secret.name }} ] verbs: - get - list - watch --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: agent-injector-webhook-secret namespace: {{ include "traffic-manager.namespace" $ }} labels: {{- include "telepresence.labels" $ | nindent 4 }} roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: agent-injector-webhook-secret subjects: - kind: ServiceAccount name: traffic-manager namespace: {{ include "traffic-manager.namespace" $ }} {{- end }} ================================================ FILE: charts/telepresence-oss/values.schema.yaml ================================================ $schema: https://json-schema.org/draft/2020-12/schema# $id: https://github.com/telepresenceio/telepresence-oss.schema.json title: Telepresence Values description: Values Schema for the Telepresence OSS Helm Chart type: object additionalProperties: false properties: affinity: description: If specified, the pod's scheduling constraints $ref: "#/$defs/affinity" agent: description: Telepresence traffic-agent configuration type: object additionalProperties: false properties: enableConsumptionMetrics: description: Enable (the default) or disable the consumption metrics of the traffic-agent. Only valid when the prometheus.port is set. type: boolean enableH2cProbing: description: If enabled, the agent will probe for HTTP2 using clear-text (h2c) support non-TLS ports unless they are configured with appProtocol. type: boolean initContainer: description: Configuration for the init-container that runs before the traffic-agent type: object additionalProperties: false properties: enabled: description: Enable/Disable the init-container type: boolean image: type: object additionalProperties: false properties: name: description: The name of the injected agent image type: string pullPolicy: description: Pull policy in the webhook for the traffic agent image $ref: "#/$defs/pullPolicy" pullSecrets: description: The Secret storing any credentials needed to access the image in a private registry type: array items: $ref: "#/$defs/localObjectReference" registry: description: The registry for the injected agent image type: string tag: description: Overrides the image tag whose default is the chart appVersion type: string initResources: description: Resource requirements for the init-container $ref: "#/$defs/resourceRequirements" initSecurityContext: description: Security context for the init-container $ref: "#/$defs/securityContext" logLevel: description: Loglevel for the agent, if different from the traffic-manager $ref: "#/$defs/logLevel" maxIdleTime: description: maximum time the agent is idle (no engagements to the workload) before it is cleaned up by the traffic manager $ref: "#/$defs/duration" mountPolicies: description: 'List of volume mount policies. Defaults to {"/tmp" : "Ignore"}' type: object additionalProperties: description: The name or path prefix match of the volume that the policy applies to type: string enum: - Remote - RemoteReadOnly - Local - Ignore port: description: Port number of the first port that the agent will use type: integer resources: description: Resource requirements for the agent $ref: "#/$defs/resourceRequirements" securityContext: description: Security context for the agent $ref: "#/$defs/securityContext" watchRetryInterval: description: The interval between retries that a watcher uses when the gRPC connection to the traffic manager is lost $ref: "#/$defs/duration" agentInjector: description: Properties for the agent injector service type: object additionalProperties: false properties: certificate: type: object additionalProperties: false properties: accessMethod: description: Method used by the agent injector to access the certificate (watch or mount) type: string enum: - watch - mount certmanager: type: object additionalProperties: false properties: commonName: description: The common name of the generated Certmanager certificate type: string duration: description: The certificate validity duration $ref: "#/$defs/duration" issuerRef: type: object additionalProperties: false properties: kind: description: The Issuer kind to use to generate the self signed certificate (Issuer of ClusterIssuer) type: string enum: - Issuer - ClusterIssuer name: description: The Issuer name to use to generate the self signed certificate type: string method: description: Method used when generating the certificate used for mutating webhook (helm, supplied, or certmanager) type: string enum: - helm - supplied - certmanager regenerate: description: Whether the certificate used for the mutating webhook should be regenerated type: boolean enabled: description: Enable/Disable the agent-injector and its webhook type: boolean injectPolicy: description: Determines when an agent is injected, possible values are OnDemand and WhenEnabled type: string enum: - OnDemand - WhenEnabled mutationAware: description: Include changes to the pod template that are contributed by other injectors. Implies reinvocationPolicy=IfNeeded type: boolean name: description: Name to use with objects associated with the agent-injector type: string secret: type: object properties: name: description: The name of the secret the agent-injector webhook uses for authorization with the kubernetes api will expose $ref: "#/$defs/rfc1123Label" service: type: object additionalProperties: false properties: type: description: Type of service for the agent-injector. type: string webhook: type: object additionalProperties: false properties: admissionReviewVersions: description: List of supported admissionReviewVersions type: array items: type: string failurePolicy: description: Action to take on unexpected failure or timeout of webhook. type: string enum: - Fail - Ignore name: description: The name of the agent-injector webhook type: string port: description: Port for the service that provides the admission webhook type: integer reinvocationPolicy: description: Specify if the webhook may be called again after the initial webhook call. Possible values are Never and IfNeeded type: string enum: - IfNeeded - Never servicePath: description: Path to the service that provides the admission webhook type: string sideEffects: description: Any side effects the admission webhook makes outside of AdmissionReview type: string enum: - None - NoneOnDryRun - Some - Unknown timeoutSeconds: description: Timeout of the admission webhook type: integer apiPort: description: The port used by the Traffic Manager gRPC API type: integer client: type: object properties: connectionTTL: description: Deprecated use grpc.connectionTTL type: string $ref: "#/$defs/duration" dns: type: object properties: excludeSuffixes: description: >- Suffixes for which the client DNS resolver will always fail (or fallback in case of the overriding resolver) Defaults to [".com", ".io", ".net", ".org", ".ru"] type: array items: type: string includeSuffixes: description: >- Suffixes for which the client DNS resolver will always attempt to do a lookup. Includes have higher priority than excludes type: array items: type: string routing: type: object properties: allowConflictingSubnets: description: Allow the specified subnets to be routed even if they conflict with other routes on the local machine type: array items: type: string alsoProxySubnets: description: The virtual network interface of connected clients will also proxy these subnets type: array items: type: string neverProxySubnets: description: The virtual network interface of connected clients never proxy these subnets type: array items: type: string clientRbac: type: object additionalProperties: false properties: create: description: Create RBAC resources for non-admin users with this release type: boolean namespaces: description: The namespaces to give users access to. Defaults to Traffic Manager's namespaces (unless dynamic) type: array items: $ref: "#/$defs/rfc1123Label" ruleExtras: description: If set, run the clientRbac-ruleExtras template to add additional RBAC rules. type: boolean subjects: description: The user accounts to tie the created roles to type: array items: $ref: "#/$defs/subject" global: type: object additionalProperties: true grpc: type: object additionalProperties: false properties: connectionTTL: description: The time that the traffic-manager or traffic-agent will retain a client connection without any sign of life from the workstation type: string $ref: "#/$defs/duration" maxReceiveSize: description: maxReceiveSize is a quantity that configures the maximum message size that the traffic manager will service. $ref: "#/$defs/quantity" hooks: type: object additionalProperties: false properties: busybox: type: object additionalProperties: false properties: image: description: The name of the image to use for busybox type: string imagePullSecrets: description: The Secret storing any credentials needed to access the image in a private registry type: array items: $ref: "#/$defs/localObjectReference" pullPolicy: description: Pull policy in the webhook for the image $ref: "#/$defs/pullPolicy" registry: description: The registry to download the image from type: string tag: description: Override the version of busybox to be installed type: string curl: type: object additionalProperties: false properties: image: description: The name of the image to use for curl type: string imagePullSecrets: description: The Secret storing any credentials needed to access the image in a private registry type: array items: $ref: "#/$defs/localObjectReference" pullPolicy: description: Pull policy in the webhook for the image $ref: "#/$defs/pullPolicy" registry: description: The registry to download the image from type: string tag: description: Override the version of curl to be installed type: string podSecurityContext: description: The Kubernetes SecurityContext for the chart hooks Pod $ref: "#/$defs/podSecurityContext" resources: description: Define resource requests and limits for the chart hooks $ref: "#/$defs/resourceRequirements" securityContext: description: The Kubernetes SecurityContext for the chart hooks Container $ref: "#/$defs/securityContext" hostNetwork: description: >- Sets the spec.template.spec.hostNetwork for the Traffic Manager. Set this to true when using Calico on AWS EKS to ensure that the mutating webhook can communicate with the traffic manager type: boolean image: type: object additionalProperties: false properties: imagePullSecrets: description: The Secret storing any credentials needed to access the image in a private registry type: array items: $ref: "#/$defs/localObjectReference" name: description: The name of the image to use for the traffic-manager type: string pullPolicy: description: Pull policy in the webhook for the traffic-manager image $ref: "#/$defs/pullPolicy" registry: description: The registry to download the image from type: string tag: description: Overrides the image tag whose default is the chart appVersion type: string intercept: type: object additionalProperties: false properties: allowGlobalIntercepts: description: Allow global TCP/UDP intercepts. When set to false, only HTTP intercepts with header or path filters are allowed. This prevents users from creating global intercepts that block other users from intercepting the same port. type: boolean environment: type: object properties: excluded: description: Environment variables to exclude from the list sent to the client during engagement type: array items: type: string inactiveBlockTimeout: description: >- The maximum amount of time an intercept may be held by a client that is unreachable or inactive. Once this timeout is exceeded, the intercept no longer blocks conflicting intercepts and may be automatically removed when another client attempts to create a conflicting intercept. $ref: "#/$defs/duration" isCI: description: isCI can be set to force the traffic-manager namespace to be "ambassador" type: boolean livenessProbe: description: Define livenessProbe for the traffic-manager $ref: "#/$defs/probe" logLevel: description: Define the logging level of the Traffic Manager $ref: "#/$defs/logLevel" managerRbac: type: object additionalProperties: false properties: create: description: Create RBAC resources for traffic-manager with this release type: boolean namespaces: description: Declares a fixed set of managed namespaces deprecated: true type: array items: $ref: "#/$defs/rfc1123Label" maxNamespaceSpecificWatchers: description: >- Threshold controlling when the traffic-manager switches from using watchers for each managed namespace to using cluster-wide watchers type: integer minimum: 0 maximum: 50 nameOverride: description: Override the default name prefix used when creating RBAC resources type: string namespaces: description: Declares a fixed set of managed namespaces. Mutually exclusive to namespaceSelector type: array items: $ref: "#/$defs/rfc1123Label" namespaceSelector: description: Declares the managed namespace using matchLabels and matchExpressions. Mutually exclusive to namespaces $ref: "#/$defs/labelSelector" nodeSelector: description: Define which Nodes you want to the Traffic Manager to be deployed to. $ref: "#/$defs/nodeSelector" podAnnotations: description: Annotations for the Traffic Manager Pod type: object additionalProperties: type: string podCIDRs: description: podCIDRs is the verbatim list of CIDRs used when the podCIDRStrategy is set to environment type: array items: type: string podCIDRStrategy: description: Define the strategy that the traffic-manager uses to discover what CIDRs the cluster uses for pods type: string enum: - auto - coverPodIPs - environment - nodePodCIDRs podLabels: description: Labels for the Traffic Manager Pod type: object additionalProperties: type: string podSecurityContext: description: The Kubernetes SecurityContext for the traffic-manager Pod $ref: "#/$defs/podSecurityContext" priorityClassName: description: Name of the existing pod priority class to be used type: string prometheus: type: object additionalProperties: false properties: port: description: Set this port number to enable a prometheus metrics http server for the traffic manager type: integer dropClientLabel: description: optionally drop the client label from prometheus metrics for GDPR personal data compliance type: boolean rbac: type: object additionalProperties: false properties: only: description: Only create the RBAC resources and omit the traffic-manger type: boolean readinessProbe: description: Define readinessProbe for the Traffic Manger. $ref: "#/$defs/probe" replicaCount: description: Number or replicas for the traffic-manager. The Traffic Manager only support running with one replica at the moment. const: 1 routeController: description: >- Configuration for the route-controller DaemonSet that installs blackhole routes for deleted service ClusterIPs to prevent routing loops on local clusters. type: object additionalProperties: false properties: enabled: description: >- Enable or disable the route-controller DaemonSet. When null (the default), the route-controller is automatically enabled for local clusters, detected by image.registry being "local" or starting with "localhost:". Set to true to force-enable or false to force-disable regardless of registry. anyOf: - type: "null" - type: boolean image: type: object additionalProperties: false properties: name: description: The name of the route-controller image type: string pullPolicy: description: Pull policy for the route-controller image. Empty string inherits from image.pullPolicy. anyOf: - type: string const: "" - $ref: "#/$defs/pullPolicy" registry: description: The registry for the route-controller image type: string logLevel: description: Log level for the route-controller. Empty string inherits from logLevel. anyOf: - type: string const: "" - $ref: "#/$defs/logLevel" serviceCIDRs: description: >- Service CIDRs for which subnet-level blackhole routes are installed at startup. If empty, the controller queries the ServiceCIDR API (k8s >= 1.33) automatically. For older clusters, set this explicitly (e.g. ["10.96.0.0/12"]). type: array items: type: string resources: description: Define resource requests and limits for the Traffic Manger $ref: "#/$defs/resourceRequirements" schedulerName: description: Specify a scheduler for Traffic Manager Pod and hooks Pod type: string securityContext: description: The Kubernetes SecurityContext for the traffic-manager container $ref: "#/$defs/securityContext" service: type: object additionalProperties: false properties: type: description: The type of service for the traffic-manager type: string startupProbe: description: Define startupProbe for the Traffic Manger. $ref: "#/$defs/probe" telepresenceAPI: type: object additionalProperties: false properties: port: description: The port on agent's localhost where the Telepresence API server can be found type: integer timeouts: type: object additionalProperties: false properties: agentArrival: description: The time that the traffic-manager will wait for the traffic-agent to arrive $ref: "#/$defs/duration" tolerations: description: Define tolerations for the Traffic Manager to ignore Node taints type: array items: $ref: "#/$defs/toleration" workloads: type: object additionalProperties: false properties: argoRollouts: type: object additionalProperties: false properties: enabled: description: Enable/Disable the argo-rollouts integration type: boolean deployments: type: object additionalProperties: false properties: enabled: description: Enable/Disable the support for Deployments type: boolean replicaSets: type: object additionalProperties: false properties: enabled: description: Enable/Disable the support for ReplicaSets type: boolean statefulSets: type: object additionalProperties: false properties: enabled: description: Enable/Disable the support for StatefulSets type: boolean $defs: duration: type: string pattern: "^[+-]?(\\d+(\\.\\d+)?(h|m|s|ms|us|µs))+" rfc1123Label: description: RFC 1123 label name type: string pattern: "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$" logLevel: type: string enum: - error - warning - warn - info - debug - trace pullPolicy: type: string enum: - Always - Never - IfNotPresent affinity: $ref: "#/definitions/io.k8s.api.core.v1.Affinity" labelSelector: $ref: "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector" localObjectReference: $ref: "#/definitions/io.k8s.api.core.v1.LocalObjectReference" nodeSelector: type: object additionalProperties: type: string resourceRequirements: $ref: "#/definitions/io.k8s.api.core.v1.ResourceRequirements" podSecurityContext: $ref: "#/definitions/io.k8s.api.core.v1.PodSecurityContext" probe: $ref: "#/definitions/io.k8s.api.core.v1.Probe" quantity: $ref: "#/definitions/io.k8s.apimachinery.pkg.api.resource.Quantity" securityContext: $ref: "#/definitions/io.k8s.api.core.v1.SecurityContext" subject: $ref: "#/definitions/io.k8s.api.rbac.v1.Subject" toleration: $ref: "#/definitions/io.k8s.api.core.v1.Toleration" ================================================ FILE: charts/telepresence-oss/values.yaml ================================================ ################################################################################ ## Deployment Configuration ################################################################################ # The Traffic Manager only support running with one replica at the moment. # Configuring the replicaCount will be added in future versions of Telepresence replicaCount: 1 # The Telepresence client will try to ensure that the Traffic Manager image is # up to date and from the right registry. If you are changing the value below, # ensure that the tag is the same as the client version and that the # TELEPRESENCE_REGISTRY environment variable is equal to image.repository. # # The client will default to ghcr.io/telepresenceio/tel2:{{CLIENT_VERSION}} image: registry: ghcr.io/telepresenceio name: tel2 pullPolicy: IfNotPresent apiPort: 8081 securityContext: readOnlyRootFilesystem: true runAsNonRoot: true runAsUser: 1000 ################################################################################ ## Traffic Manager Service Configuration ################################################################################ service: type: ClusterIP ################################################################################ ## Traffic Manager Configuration ################################################################################ # The log level of the Traffic Manager. logLevel: info # GRPC configuration for the Traffic Manager. # This is identical to the grpc configuration for local clients. # See https://www.telepresence.io/docs/latest/reference/config/#grpc for more info grpc: # Max time that the traffic-manager or traffic-agent will keep an idle client connection alive connectionTTL: 24h # maxReceiveSize is a quantity that configures the maximum message size that the traffic # manager will service. maxReceiveSize: 4Mi # podCIDRStrategy controls what strategy the traffic-manager will use for finding out what # CIDRs the cluster is using for its pods. Valid values are: # # nodePodCIDRs extract CIDRs from the podCIDR and podCIDRs field of the Node Spec. # coverPodIPs extract IPs from the podIP and podIPs field of the Pod Status and compute the CIDRs needed to cover those IPs. # environment use CIDRs listed in the space separated POD_CIDRS environment variable verbatim. # auto first try nodePodCIDRs and if that fails, tru coverPodIPs # # Default: auto podCIDRStrategy: auto # maxNamespaceSpecificWatchers configures the threshold for when the traffic-manager switches from using one set of # watchers for each managed namespace to using cluster-wide watchers. This threshold only applies when using a # namespaceSelector, and only when the traffic-manager is permitted to list the cluster's namespaces. maxNamespaceSpecificWatchers: 10 managerRbac: # Default: true create: true timeouts: # The duration the traffic manager should wait for an agent to arrive (i.e., to be registered in the traffic manager's state) # Default: 30s agentArrival: 30s ################################################################################ ## Agent Injector Configuration ################################################################################ agentInjector: enabled: true name: agent-injector secret: name: mutator-webhook-tls certificate: # The method used by the agent-injector to access the generated secret. # Possible options: watch or mount # # Default watch accessMethod: watch # The method used to generate the TLS certificate for the agent-injector. # # Possible options: helm, supplied, or certmanager. # # If set to `supplied`, ensure your Secret is in the same namespace as the traffic-manager, # and that `.agentInjector.secret.name` is set to its name. # See the Secret in `agentInjectorWebhook.yaml` for the expected structure of the data. # NOTE: If the Secret values update, the helm chart MUST be re-applied to ensure the # MutatingWebhookConfiguration uses the new values. # # Default: helm method: helm # The certmanager configuration block # certmanager: commonName: agent-injector duration: 2160h0m0s issuerRef: name: telepresence kind: Issuer injectPolicy: OnDemand mutationAware: true webhook: name: agent-injector-webhook admissionReviewVersions: ["v1"] servicePath: /traffic-agent port: 8443 failurePolicy: Ignore reinvocationPolicy: IfNeeded sideEffects: None timeoutSeconds: 5 ################################################################################ ## Telepresence traffic-agent configuration ################################################################################ agent: port: 9900 enableH2cProbing: true mountPolicies: "/tmp": Local image: pullPolicy: IfNotPresent initContainer: enabled: true ################################################################################ ## Telepresence API Server Configuration ################################################################################ telepresenceAPI: {} # The port on agent's localhost where the API service can be found # Default: 0 # port: 0 ################################################################################ ## Prometheus Server Configuration ################################################################################ prometheus: {} # Set this port number to enable a prometheus metrics http server for the # traffic manager # Default: 0 # port: 0 # dropClientLabel: false # Values specific to the helm chart hooks for managing upgrade/deleting hooks: busybox: registry: docker.io image: busybox tag: latest imagePullSecrets: [] curl: registry: docker.io image: "curlimages/curl" tag: 8.1.1 imagePullSecrets: [] pullPolicy: IfNotPresent client: dns: # Tell client's DNS resolver to never send names with these suffixes to the cluster side resolver excludeSuffixes: [".com", ".io", ".net", ".org", ".ru"] # Controls which workload kinds are recognized by Telepresence workloads: deployments: enabled: true replicaSets: enabled: true statefulSets: enabled: true argoRollouts: enabled: false # Intercept configuration intercept: # Allow global TCP/UDP intercepts. When set to false, only HTTP intercepts # with header or path filters are allowed. This prevents users from creating # global intercepts that block other users from intercepting the same port. allowGlobalIntercepts: true # The maximum amount of time an intercept may be held by a client that is unreachable or inactive. # Once this timeout is exceeded, the intercept no longer blocks conflicting intercepts and may be # automatically removed when another client attempts to create a conflicting intercept. inactiveBlockTimeout: 10m ################################################################################ ## Route Controller Configuration ################################################################################ # routeController installs a DaemonSet that watches for Service deletions and # adds temporary blackhole routes on each node for deleted ClusterIPs, preventing # routing loops on local clusters (Kind, minikube, k3d, Docker Desktop) where # deleted service IPs fall through to the node's default route. routeController: # enabled controls the route-controller DaemonSet: # null (default) - auto-enable when image.registry is "local" or starts with "localhost:" # true - always enable # false - always disable, even on local clusters enabled: ~ image: # registry and pullPolicy default to the traffic-manager image settings when not set registry: "" name: route-controller pullPolicy: "" # logLevel defaults to the traffic-manager logLevel when not set logLevel: "" # serviceCIDRs is a list of service CIDRs (e.g. ["10.96.0.0/12"]) for which the # route-controller installs subnet-level blackhole routes at startup. These are # overridden by kube-proxy for active services and catch any non-existent IP in # the range without waiting for a deletion event. # If empty, the controller queries the ServiceCIDR API (k8s >= 1.33) and falls # back to per-IP blackhole routes on deletion for older clusters. serviceCIDRs: [] ================================================ FILE: cmd/cobraparser/LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: cmd/cobraparser/README.md ================================================ # Cobra Help Output Parser ## Parser The `cobraparser` is a tool to parse the output produced by the `spf13.CobraCommand` help function. The parser invokes an executable with `--help` and creates JSON structured data from the resulting output. Subcommands are traversed recursively. ## Generator Package The `generator` package contains the code to dynamically configure a `spf13.CobraCommand` using the JSON data produced by the parser. The command will be populated with the subcommands and flags defined in the JSON data. ## Sample usage: See `build-aux/main.mk` (the `pkg/client/cli/docker/compose/dc-cli.json` target) and the corresponding use of the generator package in `pkg/client/cli/docker/compose/config.go`. ================================================ FILE: cmd/cobraparser/generate/generate.go ================================================ package generate import ( "fmt" "strconv" "strings" "time" "github.com/docker/docker/opts" "github.com/spf13/pflag" "github.com/telepresenceio/telepresence/cmd/cobraparser/v2/types" ) // FlagSet creates a FlagSet based on the given CommandInfo. // //nolint:gocyclo // there are a lot of cases here func FlagSet(setName string, ci *types.CommandInfo) *pflag.FlagSet { flags := pflag.NewFlagSet(setName, pflag.ContinueOnError) for _, flag := range ci.Flags { switch { case flag.Type == "bool": val := false if flag.Default != "" { val, _ = strconv.ParseBool(flag.Default) } flags.Bool(flag.Name, val, flag.Description) case flag.Type == "boolSlice": flags.BoolSlice(flag.Name, nil, flag.Description) case flag.Type == "duration": val := time.Duration(0) if flag.Default != "" { val, _ = time.ParseDuration(flag.Default) } flags.Duration(flag.Name, val, flag.Description) case flag.Type == "durationSlice": flags.DurationSlice(flag.Name, nil, flag.Description) case flag.Type == "string": flags.String(flag.Name, flag.Default, flag.Description) case flag.Type == "stringArray": var val []string if flag.Default != "" { val = strings.Split(flag.Default, ",") } flags.StringArray(flag.Name, val, flag.Description) case flag.Type == "stringSlice": flags.StringSlice(flag.Name, nil, flag.Description) case strings.HasPrefix(flag.Type, "int"): val := int64(0) if flag.Default != "" { val, _ = strconv.ParseInt(flag.Default, 10, 64) } switch flag.Type { case "int": flags.Int(flag.Name, int(val), flag.Description) case "int8": flags.Int8(flag.Name, int8(val), flag.Description) case "int16": flags.Int16(flag.Name, int16(val), flag.Description) case "int32": flags.Int32(flag.Name, int32(val), flag.Description) case "int64": flags.Int64(flag.Name, val, flag.Description) case "intSlice": flags.IntSlice(flag.Name, nil, flag.Description) case "int8Slice": flags.IntSlice(flag.Name, nil, flag.Description) case "int16Slice": flags.IntSlice(flag.Name, nil, flag.Description) case "int32Slice": flags.Int32Slice(flag.Name, nil, flag.Description) case "int64Slice": flags.Int64Slice(flag.Name, nil, flag.Description) } case strings.HasPrefix(flag.Type, "uint"): val := uint64(0) if flag.Default != "" { val, _ = strconv.ParseUint(flag.Default, 10, 64) } switch flag.Type { case "uint": flags.Uint(flag.Name, uint(val), flag.Description) case "uint8": flags.Uint8(flag.Name, uint8(val), flag.Description) case "uint16": flags.Uint16(flag.Name, uint16(val), flag.Description) case "uint32": flags.Uint32(flag.Name, uint32(val), flag.Description) case "uint64": flags.Uint64(flag.Name, val, flag.Description) case "uintSlice": flags.UintSlice(flag.Name, nil, flag.Description) } case strings.HasPrefix(flag.Type, "float"): val := float64(0) if flag.Default != "" { val, _ = strconv.ParseFloat(flag.Default, 64) } switch flag.Type { case "float32": flags.Float32(flag.Name, float32(val), flag.Description) case "float64": flags.Float64(flag.Name, val, flag.Description) case "float32Slice": flags.Float32Slice(flag.Name, nil, flag.Description) case "float564Slice": flags.Float64Slice(flag.Name, nil, flag.Description) } case flag.Type == "bytes": flags.Var(new(opts.MemBytes), flag.Name, flag.Description) case flag.Type == "filter": flags.String(flag.Name, flag.Default, flag.Description) case flag.Type == "list": flags.Var(opts.NewListOptsRef(new([]string), nil), flag.Name, flag.Description) case flag.Type == "scale": flags.Uint32(flag.Name, 0, flag.Description) default: panic(fmt.Sprintf("unknown flag type %s in flagset %s", flag.Type, setName)) } if flag.Shorthand != "" { flags.Lookup(flag.Name).Shorthand = flag.Shorthand } } return flags } ================================================ FILE: cmd/cobraparser/go.mod ================================================ module github.com/telepresenceio/telepresence/cmd/cobraparser/v2 go 1.24 require ( github.com/docker/docker v28.5.2+incompatible github.com/spf13/pflag v1.0.10 ) require ( github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect gotest.tools/v3 v3.5.2 // indirect ) ================================================ FILE: cmd/cobraparser/go.sum ================================================ github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= ================================================ FILE: cmd/cobraparser/main.go ================================================ //go:build go1.24 package main import ( "encoding/json" "fmt" "log" "os" "github.com/spf13/pflag" "github.com/telepresenceio/telepresence/cmd/cobraparser/v2/types" ) func main() { fs := pflag.NewFlagSet("cobraparse", pflag.ExitOnError) var helpArg string fs.BoolP("help", "h", false, "Help for this command") fs.StringVar(&helpArg, "help-arg", "--help", "Help for this argument") fs.Usage = func() { fmt.Fprint(fs.Output(), "Usage:\n cobraparse [...args]") fs.PrintDefaults() } _ = fs.Parse(os.Args[1:]) args := fs.Args() if len(args) == 0 { fs.Usage() os.Exit(2) } f := types.HelpFetcher{ Executable: args[0], HelpArg: helpArg, } root, err := f.BuildCommandTree(args[1:], 8) if err != nil { log.Fatalf("error: %v\n", err) } enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") if err := enc.Encode(root); err != nil { log.Fatalf("error writing JSON: %v\n", err) } } ================================================ FILE: cmd/cobraparser/types/commandinfo.go ================================================ package types import ( "bytes" "fmt" "os" "os/exec" "regexp" "strconv" "strings" "time" ) // CommandInfo describes a command and its hierarchy. type CommandInfo struct { Name string `json:"name"` Usage string `json:"usage,omitempty"` Description string `json:"description,omitempty"` Flags []FlagInfo `json:"flags,omitempty"` Subcommands []CommandInfo `json:"subcommands,omitempty"` } // FlagInfo describes a flag parsed from help text. type FlagInfo struct { Name string `json:"name,omitempty"` Shorthand string `json:"shorthand,omitempty"` Type string `json:"type,omitempty"` Description string `json:"description,omitempty"` Default string `json:"default,omitempty"` // parsed from "(default ...)" Global bool `json:"global,omitempty"` Inherited bool `json:"inherited,omitempty"` } // HelpFetcher fetches help text for a command path (root and subcommands). type HelpFetcher struct { Executable string HelpArg string } func (f *HelpFetcher) fetch(path []string) (string, error) { args := make([]string, 0, len(path)+1) args = append(args, path...) args = append(args, f.HelpArg) cmd := exec.Command(f.Executable, args...) cmd.Env = os.Environ() var buf bytes.Buffer cmd.Stdout = &buf cmd.Stderr = &buf err := cmd.Run() out := buf.String() // Some binaries return non-zero for --help; accept if output looks like help. if err != nil { if looksLikeHelp(out) { return out, nil } return "", fmt.Errorf("error running %q %v: %w\nOutput:\n%s", f, args, err, out) } return out, nil } func looksLikeHelp(s string) bool { return strings.Contains(s, "Usage:") || strings.Contains(s, "Commands:") } // Parse and traverse // BuildCommandTree recursively builds the command tree starting at path. func (f *HelpFetcher) BuildCommandTree(path []string, maxDepth int) (*CommandInfo, error) { text, err := f.fetch(path) if err != nil { return nil, err } info := &CommandInfo{} if len(path) > 0 { info.Name = path[len(path)-1] } else { info.Name = f.Executable } info.Usage = parseUsage(text) info.Description = parseShortDescription(text) flags := make([]FlagInfo, 0, 16) flags = append(flags, parseFlagsFromSection(text, false, false, `(?m:^(?:Flags|Options):)`)...) flags = append(flags, parseFlagsFromSection(text, true, false, `(?m:^Global (?:Flags|Options):)`)...) // Cobra shows inherited flags on subcommands using this heading: flags = append(flags, parseFlagsFromSection(text, false, true, `(?m:^(?:Flags|Options) inherited from parent commands:)`)...) // Some Cobra setups may use alternate headings; add heuristics if needed. info.Flags = flags // Discover subcommands subCommands := parseAvailableCommands(text) if len(subCommands) == 0 || maxDepth < 1 { return info, nil } children := make([]CommandInfo, 0, len(subCommands)) for _, name := range subCommands { if name == info.Name { // We consider it highly unlikely that a command will be named the same as its parent. This assumption // prevents us from recursing down in docker swarm init [init...] continue } subPath := append(path, name) child, err := f.BuildCommandTree(subPath, maxDepth-1) if err != nil { return nil, err } children = append(children, *child) } info.Subcommands = children return info, nil } // Helpers to parse sections func parseUsage(text string) string { for _, line := range strings.Split(text, "\n") { t := strings.TrimSpace(line) if strings.HasPrefix(t, "Usage:") || strings.HasPrefix(t, "usage:") { return strings.TrimSpace(t[len("Usage:"):]) } } return "" } var afterUsagePrefixes = []string{ "Available Commands:", "Flags:", "Global Flags:", "Flags inherited from parent commands:", "Commands:", "Options:", "Global Options:", "Options inherited from parent commands:", } func parseShortDescription(text string) string { for _, t := range strings.Split(text, "\n") { t = strings.TrimSpace(t) if t == "" || strings.HasPrefix(t, "Usage:") { continue } for _, prefix := range afterUsagePrefixes { if strings.HasPrefix(t, prefix) { // We've gone too far. return "" } } // Take this as a description and stop. return t } return "" } func parseAvailableCommands(text string) []string { var res []string for { text = sliceAfter(text, `(?m:^(?:[A-Z][0-9A-Za-z-]*\s+)?Commands:)`) if text == "" { break } for _, line := range strings.Split(text, "\n") { trim := strings.TrimSpace(line) if trim == "" { continue } if !startsWithIndent(line) { break } name := trim spaceIdx := strings.IndexAny(trim, " \t\n") if spaceIdx > 0 { name = trim[:spaceIdx] } name = strings.TrimSuffix(name, "*") res = append(res, name) } } return res } func startsWithIndent(s string) bool { return strings.HasPrefix(s, " ") || strings.HasPrefix(s, "\t") } var ( // Matches lines like: // -n, --name string description (default "foo") // --verbose description // --count int description (default 3) // -q description flagLineRe = regexp.MustCompile(`^(?:-([A-Za-z0-9]),\s*)?(?:--([A-Za-z0-9][A-Za-z0-9-]*))?(?:\s+(\S+))?\s{2,}(.+)$`) defaultRe = regexp.MustCompile(`^(.*)\s*\((?i:default)[:\s]*([^)]+)\)`) ) func parseFlagsFromSection(text string, global, inherited bool, headerPattern string) []FlagInfo { section := sliceAfter(text, headerPattern) if section == "" { return nil } lines := strings.Split(section, "\n") var flags []FlagInfo for _, line := range lines { // Expect flag lines to be indented. trim := strings.TrimSpace(line) if trim == "" { continue } if !startsWithIndent(line) { break } line = trim m := flagLineRe.FindStringSubmatch(line) if len(m) == 0 { // Indented line that doesn't match the flag regexp belongs to the previous message if len(flags) > 0 { flags[len(flags)-1].Description += " " + line } continue } short := m[1] long := m[2] typ := m[3] if typ == "" { typ = "bool" } desc := strings.TrimSpace(m[4]) if long == "" && short == "" { continue } dflt := "" if mm := defaultRe.FindStringSubmatch(desc); len(mm) == 3 { // This might be a "fake" default, i.e. a default that is embedded in the description rather // than a true default for the flag. We don't want defaults that are unparseable. dflt = strings.TrimSpace(mm[2]) switch typ { case "int", "int16", "int32", "int64", "uint16", "uint32", "uint64": if _, err := strconv.ParseInt(dflt, 10, 64); err != nil { dflt = "" } case "bool": if _, err := strconv.ParseBool(dflt); err != nil { dflt = "" } case "duration": if _, err := time.ParseDuration(dflt); err != nil { dflt = "" } case "float32", "float64": if _, err := strconv.ParseFloat(dflt, 64); err != nil { dflt = "" } case "string": if len(dflt) < 3 || dflt[0] != '"' || dflt[len(dflt)-1] != '"' { dflt = "" } } if dflt != "" { desc = strings.TrimSpace(mm[1]) } } flags = append(flags, FlagInfo{ Name: long, Shorthand: short, Type: typ, Description: desc, Default: dflt, Global: global, Inherited: inherited, }) } return flags } func sliceAfter(text, headerPattern string) string { if ixs := regexp.MustCompile(headerPattern).FindStringIndex(text); ixs != nil { return text[ixs[1]:] } return "" } ================================================ FILE: cmd/routecontroller/main.go ================================================ package main import ( "context" "fmt" "net" "os" "strings" "github.com/coreos/go-iptables/iptables" meta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "github.com/telepresenceio/clog" tplog "github.com/telepresenceio/telepresence/v2/pkg/log" "github.com/telepresenceio/telepresence/v2/pkg/sigctx" ) // subnetRule tracks an iptables FORWARD DROP rule installed for a service CIDR. type subnetRule struct { ipt *iptables.IPTables cidr string } func main() { ctx := context.Background() logLevel := os.Getenv("LOG_LEVEL") ctx = tplog.MakeBaseLogger(ctx, os.Stdout, logLevel) if err := sigctx.DoWithSignalHandler(ctx, run); err != nil { clog.Error(ctx, err) os.Exit(1) } } func run(ctx context.Context) error { cfg, err := rest.InClusterConfig() if err != nil { return fmt.Errorf("failed to get in-cluster config: %w", err) } cs, err := kubernetes.NewForConfig(cfg) if err != nil { return fmt.Errorf("failed to create clientset: %w", err) } rules := installSubnetBlackholes(ctx, cs) defer removeSubnetBlackholes(ctx, rules) clog.Info(ctx, "Route controller started") <-ctx.Done() return nil } // discoverServiceCIDRs returns the service CIDRs for which iptables FORWARD DROP rules // will be installed. It first checks the SERVICE_CIDRS environment variable // (comma-separated list of CIDRs). If that is not set, it queries the Kubernetes // ServiceCIDR API (available in k8s 1.33+). If neither is available it returns nil and // no subnet-level protection is installed; set SERVICE_CIDRS explicitly in that case. func discoverServiceCIDRs(ctx context.Context, cs *kubernetes.Clientset) []string { if envCIDRs := os.Getenv("SERVICE_CIDRS"); envCIDRs != "" { cidrs := strings.Split(envCIDRs, ",") clog.Infof(ctx, "Using service CIDRs from SERVICE_CIDRS env: %v", cidrs) return cidrs } list, err := cs.NetworkingV1().ServiceCIDRs().List(ctx, meta.ListOptions{}) if err != nil { clog.Warnf(ctx, "ServiceCIDR API unavailable: %v; set SERVICE_CIDRS to enable subnet blackholing", err) return nil } var cidrs []string for _, item := range list.Items { cidrs = append(cidrs, item.Spec.CIDRs...) } clog.Infof(ctx, "Discovered service CIDRs from cluster API: %v", cidrs) return cidrs } // installSubnetBlackholes installs iptables FORWARD chain DROP rules for each service CIDR. // // An iptables rule in the FORWARD chain (rather than a kernel blackhole route) is used // because RTN_BLACKHOLE routes fail connect()/sendmsg() at the socket level before // any iptables hook can fire, which breaks locally-generated traffic such as // kube-apiserver → mutating-webhook calls. // // The FORWARD chain only affects traffic forwarded through the host (i.e. pod traffic via // veth pairs). Locally-generated host traffic is never subject to the FORWARD chain. // // For active services, kube-proxy's PREROUTING DNAT fires before the FORWARD chain and // rewrites the destination from ClusterIP to a pod IP, so the DROP rule (which matches // on the original service CIDR) does not apply. For deleted or never-assigned ClusterIPs // no DNAT rule exists, the FORWARD chain sees the original ClusterIP, and the DROP fires. func installSubnetBlackholes(ctx context.Context, cs *kubernetes.Clientset) []subnetRule { var rules []subnetRule for _, cidr := range discoverServiceCIDRs(ctx, cs) { cidr = strings.TrimSpace(cidr) _, network, err := net.ParseCIDR(cidr) if err != nil { clog.Errorf(ctx, "Failed to parse service CIDR %q: %v", cidr, err) continue } proto := iptables.ProtocolIPv4 if network.IP.To4() == nil { proto = iptables.ProtocolIPv6 } ipt, err := iptables.NewWithProtocol(proto) if err != nil { clog.Errorf(ctx, "Failed to initialise iptables for %s: %v", network, err) continue } exists, err := ipt.Exists("filter", "FORWARD", "-d", network.String(), "-j", "DROP") if err != nil { clog.Errorf(ctx, "Failed to check iptables FORWARD rule for %s: %v", network, err) continue } if !exists { if err := ipt.Insert("filter", "FORWARD", 1, "-d", network.String(), "-j", "DROP"); err != nil { clog.Errorf(ctx, "Failed to add iptables FORWARD DROP for service CIDR %s: %v", network, err) continue } clog.Infof(ctx, "Added iptables FORWARD DROP for service CIDR %s", network) } else { clog.Debugf(ctx, "iptables FORWARD DROP for %s already present", network) } rules = append(rules, subnetRule{ipt: ipt, cidr: network.String()}) } return rules } func removeSubnetBlackholes(ctx context.Context, rules []subnetRule) { for _, rule := range rules { if err := rule.ipt.Delete("filter", "FORWARD", "-d", rule.cidr, "-j", "DROP"); err != nil { clog.Debugf(ctx, "Failed to remove iptables FORWARD DROP for %s: %v", rule.cidr, err) } else { clog.Infof(ctx, "Removed iptables FORWARD DROP for service CIDR %s", rule.cidr) } } } ================================================ FILE: cmd/teleroute/.gitignore ================================================ rpc/ ================================================ FILE: cmd/teleroute/DEVELOPING.md ================================================ # Docker network plugin for Telepresence ## Debugging Start by configuring telepresence to not check for the latest version of the plugin, but instead use our debug version by adding the following yaml to the `config.yml` (on Linux, this will be in `~/.config/telepresence/config.yml`, and on mac you'll find it in `"$HOME/Library/Application Support/telepresence/config.yml"`: ```yaml intercept: teleroute: tag: debug ``` Build the plugin for debugging. The command both builds and enables the plugin: ```console $ make debug ``` Use runc to tail the plugin's log output ```console sudo runc --root /run/docker/runtime-runc/plugins.moby exec $(docker plugin list --no-trunc -f capability=networkdriver -f enabled=true -q) tail -n 400 -f /var/log/teleroute.log ``` ================================================ FILE: cmd/teleroute/Dockerfile ================================================ FROM --platform=$BUILDPLATFORM golang:alpine AS builder RUN apk add --no-cache --virtual .build-deps gcc libc-dev WORKDIR /docker-network-teleroute COPY . . ARG TARGETOS ARG TARGETARCH RUN \ --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg/mod \ GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /go/bin/docker-network-teleroute FROM alpine RUN apk add --no-cache bash COPY --from=builder /go/bin/docker-network-teleroute /bin/ ================================================ FILE: cmd/teleroute/Makefile ================================================ # Avoid potential confusion about what shell to use SHELL := /bin/bash # Delete implicit rules not used here (clutters debug output). MAKEFLAGS += --no-builtin-rules # Turn off .INTERMEDIATE file removal by marking all files as # .SECONDARY. .INTERMEDIATE file removal is a space-saving hack from # a time when drives were small; on modern computers with plenty of # storage, it causes nothing but headaches. # # https://news.ycombinator.com/item?id=16486331 .SECONDARY: # If a recipe errors, remove the target it was building. This # prevents outdated/incomplete results of failed runs from tainting # future runs. The only reason .DELETE_ON_ERROR is off by default is # for historical compatibility. # # If for some reason this behavior is not desired for a specific # target, mark that target as .PRECIOUS. .DELETE_ON_ERROR: PLUGIN_ARCH ?= $(shell go env GOARCH) PLUGIN_ARCH := $(PLUGIN_ARCH) PLUGIN_REGISTRY ?= ghcr.io/telepresenceio PLUGIN_NAME = teleroute PLUGIN_FQN = $(PLUGIN_REGISTRY)/$(PLUGIN_NAME) PLUGIN_DEV_IMAGE = $(PLUGIN_FQN):$(PLUGIN_ARCH) BUILD_DIR=build-output export DOCKER_BUILDKIT := 1 .PHONY: clean clean: rm -rf $(BUILD_DIR) rpc rpc/teleroute/.rsync-stamp: ../../rpc/go.mod ../../rpc/go.sum $(wildcard ../../rpc/teleroute/*.go) mkdir -p rpc/teleroute rsync -au --delete ../../rpc/go.mod rpc/ rsync -au --delete ../../rpc/go.sum rpc/ rsync -au --delete ../../rpc/teleroute rpc/ touch rpc/teleroute/.rsync-stamp .PHONY: rootfs rootfs: rpc/teleroute/.rsync-stamp docker buildx inspect |grep -q /$(PLUGIN_ARCH) || \ docker run --rm --privileged tonistiigi/binfmt --install all rm -rf $(BUILD_DIR) docker buildx build --platform linux/$(PLUGIN_ARCH) --output $(BUILD_DIR)/rootfs . cp config.json $(BUILD_DIR) # Enable is to support faster dev-loop (retries without pushing) .PHONY: enable enable: rootfs docker plugin rm --force $(PLUGIN_DEV_IMAGE) 2>/dev/null || true docker plugin create $(PLUGIN_DEV_IMAGE) $(BUILD_DIR) docker plugin enable $(PLUGIN_DEV_IMAGE) .PHONY: debug debug: rootfs docker plugin rm --force $(PLUGIN_DEV_IMAGE)-debug 2>/dev/null || true docker plugin create $(PLUGIN_DEV_IMAGE)-debug $(BUILD_DIR) docker plugin set $(PLUGIN_DEV_IMAGE)-debug DEBUG=true docker plugin enable $(PLUGIN_DEV_IMAGE)-debug # Recreate the plugin from the rootfs with some tag. This target is called # repeatedly in order to give the plugin different tags (because plugins cannot # be tagged like images). $(BUILD_DIR)/tag-%.ts: docker plugin rm --force $(PLUGIN_FQN):$* 2>/dev/null || true docker plugin create $(PLUGIN_FQN):$* $(BUILD_DIR) docker plugin push $(PLUGIN_FQN):$* docker plugin rm --force $(PLUGIN_FQN):$* 2>/dev/null || true touch $(BUILD_DIR)/tag-$*.ts gaPattern = ^[0-9]+.[0-9]+.[0-9]+$$ # For amd64 we push the tags "amd64-SEMVER" and "SEMVER", and if it is a release, also "amd64" and "latest". # For arm64 we push the tag "arm64-SEMVER", and if it is a release also "arm64". .PHONY: push ifeq ($(PLUGIN_ARCH), amd64) push: clean rootfs $(BUILD_DIR)/tag-$(PLUGIN_ARCH)-$(PLUGIN_VERSION).ts $(BUILD_DIR)/tag-$(PLUGIN_VERSION).ts else push: clean rootfs $(BUILD_DIR)/tag-$(PLUGIN_ARCH)-$(PLUGIN_VERSION).ts endif if [[ $(PLUGIN_VERSION) =~ $(gaPattern) ]]; \ then \ make push-latest; \ fi .PHONY: push-latest ifeq ($(PLUGIN_ARCH), amd64) push-latest: $(BUILD_DIR)/tag-$(PLUGIN_ARCH).ts $(BUILD_DIR)/tag-latest.ts else push-latest: $(BUILD_DIR)/tag-$(PLUGIN_ARCH).ts endif ================================================ FILE: cmd/teleroute/config.json ================================================ { "description": "Docker network plugin for Telepresence", "documentation": "https://docs.docker.com/engine/extend/plugins/", "entrypoint": [ "/bin/docker-network-teleroute" ], "env": [ { "name": "DEBUG", "settable": [ "value" ], "value": "false" } ], "interface": { "socket": "teleroute.sock", "types": [ "docker.networkdriver/1.0" ] }, "pidhost": true, "linux": { "capabilities": [ "CAP_NET_ADMIN" ] }, "network": { "type": "host" } } ================================================ FILE: cmd/teleroute/driver/driver.go ================================================ package driver import ( "context" "fmt" "github.com/docker/go-plugins-helpers/network" "github.com/puzpuzpuz/xsync/v4" "github.com/telepresenceio/clog" ) type driver struct { ctx context.Context pid int networks *xsync.Map[string, *networkState] } func New(ctx context.Context, pid int) network.Driver { return &driver{ ctx: ctx, pid: pid, networks: xsync.NewMap[string, *networkState](), } } type errNetworkNotFound string func (e errNetworkNotFound) Error() string { return fmt.Sprintf("network %q not found", string(e)) } func (d *driver) GetCapabilities() (*network.CapabilitiesResponse, error) { clog.Debug(d.ctx, "GetCapabilities") return &network.CapabilitiesResponse{ Scope: network.LocalScope, }, nil } func (d *driver) CreateNetwork(r *network.CreateNetworkRequest) error { n, err := newNetwork(d.ctx, d.pid, r) if err != nil { return err } d.networks.Store(r.NetworkID, n) return nil } func (d *driver) DeleteNetwork(r *network.DeleteNetworkRequest) error { if n, ok := d.networks.LoadAndDelete(r.NetworkID); ok { clog.Debug(n.ctx, "Deleting network") n.cancel() } return nil } func (d *driver) CreateEndpoint(r *network.CreateEndpointRequest) (*network.CreateEndpointResponse, error) { n, ok := d.networks.Load(r.NetworkID) if !ok { return nil, errNetworkNotFound(r.NetworkID) } return n.createEndpoint(r) } func (d *driver) DeleteEndpoint(r *network.DeleteEndpointRequest) error { return d.withNetwork(r.NetworkID, func(ns *networkState) error { return ns.deleteEndpoint(r.EndpointID) }) } func (d *driver) Join(r *network.JoinRequest) (*network.JoinResponse, error) { n, ok := d.networks.Load(r.NetworkID) if !ok { return nil, errNetworkNotFound(r.NetworkID) } return n.join(r) } func (d *driver) Leave(r *network.LeaveRequest) (err error) { return d.withNetwork(r.NetworkID, func(ns *networkState) error { return ns.leaveEndpoint(r.EndpointID) }) } func (d *driver) EndpointInfo(r *network.InfoRequest) (*network.InfoResponse, error) { return nil, nil } func (d *driver) DiscoverNew(*network.DiscoveryNotification) error { return nil } func (d *driver) DiscoverDelete(*network.DiscoveryNotification) error { return nil } func (d *driver) ProgramExternalConnectivity(*network.ProgramExternalConnectivityRequest) error { return nil } func (d *driver) RevokeExternalConnectivity(*network.RevokeExternalConnectivityRequest) error { return nil } func (d *driver) AllocateNetwork(*network.AllocateNetworkRequest) (*network.AllocateNetworkResponse, error) { return nil, nil } func (d *driver) FreeNetwork(*network.FreeNetworkRequest) error { return nil } func (d *driver) withNetwork(id string, f func(*networkState) error) (err error) { n, ok := d.networks.Load(id) if !ok { return errNetworkNotFound(id) } return f(n) } ================================================ FILE: cmd/teleroute/driver/network.go ================================================ package driver import ( "context" "errors" "fmt" "io" "net/netip" "strconv" "time" "github.com/cenkalti/backoff/v4" "github.com/docker/go-plugins-helpers/network" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/emptypb" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/rpc/v2/teleroute" ) type networkState struct { options ctx context.Context cancel context.CancelFunc pid int // clientConn is connected to the Telepresence daemon telemount gRPC. clientConn *grpc.ClientConn } func newNetwork(ctx context.Context, pid int, r *network.CreateNetworkRequest) (n *networkState, err error) { ctx, cancel := context.WithCancel(ctx) ctx = clog.WithGroup(ctx, r.NetworkID[:12]) clog.Debugf(ctx, "CreateNetwork, %v", r.Options) n = &networkState{ ctx: ctx, cancel: cancel, pid: pid, } err = n.initialize(r) if err != nil { clog.Error(ctx, err) return nil, err } return n, nil } func (n *networkState) initialize(r *network.CreateNetworkRequest) (err error) { var gws []netip.Prefix for _, ia := range r.IPv4Data { if ia.Gateway != "" { gw, err := netip.ParsePrefix(ia.Gateway) if err != nil { return fmt.Errorf("invalid IPv4 gateway address: %s", ia.Gateway) } gws = append(gws, gw) } } for _, ia := range r.IPv6Data { if ia.Gateway != "" { gw, err := netip.ParsePrefix(ia.Gateway) if err != nil { return fmt.Errorf("invalid IPv4 gateway address: %s", ia.Gateway) } gws = append(gws, gw) } } driverOpts, ok := r.Options["com.docker.network.generic"].(map[string]any) if !ok { return fmt.Errorf("network options are missing com.docker.network.generic") } err = n.options.parse(driverOpts) if err != nil { return err } time.Sleep(time.Second) return n.connectToDaemon(gws) } // callDaemon adds a short timeout when calling the daemon's gRPC so that we don't run into long waits in case something goes wrong. func callDaemon[R proto.Message](ns *networkState, f func(ctx context.Context, client teleroute.TelerouteClient) (R, error)) (rsp R, err error) { ctx, cancel := context.WithTimeout(ns.ctx, 3*time.Second) rsp, err = f(ctx, teleroute.NewTelerouteClient(ns.clientConn)) cancel() return rsp, err } func (n *networkState) connectToDaemon(gateways []netip.Prefix) (err error) { ap := netip.AddrPortFrom(n.host, n.port) if n.clientConn != nil { n.clientConn.Close() } n.clientConn, err = grpc.NewClient(ap.String(), grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { err = fmt.Errorf("unable to create gRPC connection to daemon: %w", err) clog.Error(n.ctx, err) return err } defer func() { if err != nil { n.clientConn.Close() } }() gws := make([][]byte, len(gateways)) for i, gw := range gateways { gws[i], err = gw.MarshalBinary() if err != nil { return err } } client := teleroute.NewTelerouteClient(n.clientConn) rq := &teleroute.ConnectRequest{Pid: int64(n.pid), Gateways: gws} var infoStream grpc.ServerStreamingClient[teleroute.Info] err = backoff.Retry(func() error { infoStream, err = client.Connect(n.ctx, rq) if status.Code(err) != codes.Unavailable { err = backoff.Permanent(err) } return err }, backoff.WithMaxRetries(backoff.NewConstantBackOff(50*time.Millisecond), 20)) if err != nil { return err } go func() { defer n.cancel() for { info, err := infoStream.Recv() if err != nil { if !errors.Is(err, io.EOF) { clog.Errorf(n.ctx, "error receiving info: %v", err) } break } im := info.GetInfo() clog.Infof(n.ctx, "Connected to %s version %s", im["name"], im["version"]) } }() return nil } func rawAddrFromPrefixString(s string) (rawAddr []byte, err error) { if len(s) == 0 { return nil, nil } var pfx netip.Prefix pfx, err = netip.ParsePrefix(s) if err == nil { rawAddr, err = pfx.Addr().MarshalBinary() } if err != nil { err = fmt.Errorf("invalid CIDR %q: %w", s, err) } return rawAddr, err } func (n *networkState) createEndpoint(r *network.CreateEndpointRequest) (_ *network.CreateEndpointResponse, err error) { clog.Debugf(n.ctx, "Create endpoint %.8s %v %v", r.EndpointID, r.Interface, r.Options) defer func() { if err != nil { clog.Error(n.ctx, err) } }() request := &teleroute.CreateEndpointRequest{ Id: r.EndpointID, } request.AddrIpv4, err = rawAddrFromPrefixString(r.Interface.Address) if err != nil { return nil, err } request.AddrIpv6, err = rawAddrFromPrefixString(r.Interface.AddressIPv6) if err != nil { return nil, err } if daemonOpt, ok := r.Options["daemon"].(string); ok { request.Daemon, _ = strconv.ParseBool(daemonOpt) } _, err = callDaemon(n, func(ctx context.Context, client teleroute.TelerouteClient) (*emptypb.Empty, error) { return client.CreateEndpoint(ctx, request) }) return &network.CreateEndpointResponse{}, err } func (n *networkState) join(r *network.JoinRequest) (response *network.JoinResponse, err error) { endpointID := r.EndpointID clog.Debugf(n.ctx, "Join endpoint %.8s, sandbox %.8s %v", endpointID, r.SandboxKey, r.Options) rsp, err := callDaemon(n, func(ctx context.Context, client teleroute.TelerouteClient) (*teleroute.JoinResponse, error) { return client.Join(ctx, &teleroute.EndpointIdentifier{ Id: endpointID, }) }) if err != nil { return nil, err } routeType := 1 var viaStr string if len(rsp.Via) > 0 { var nxt netip.Addr err = nxt.UnmarshalBinary(rsp.Via) if err != nil { return nil, fmt.Errorf("unable to unmarshal via with length %d: %w", len(rsp.Via), err) } viaStr = nxt.String() routeType = 0 } srs := make([]*network.StaticRoute, len(rsp.Routes)) for i, r := range rsp.Routes { var pfx netip.Prefix err = pfx.UnmarshalBinary(r) if err != nil { return nil, fmt.Errorf("unable to unmarshal route with length %d: %w", len(r), err) } srs[i] = &network.StaticRoute{ Destination: pfx.String(), RouteType: routeType, NextHop: viaStr, } } response = &network.JoinResponse{ InterfaceName: network.InterfaceName{ SrcName: rsp.InterfaceSrcName, DstPrefix: rsp.InterfaceDstPrefix, }, StaticRoutes: srs, DisableGatewayService: rsp.DisableGw, } if len(rsp.GwIpV4) > 0 { var gwIPv4 netip.Prefix err = gwIPv4.UnmarshalBinary(rsp.GwIpV4) if err != nil { return nil, fmt.Errorf("unable to unmarshal GwIpV4 with length %d: %w", len(rsp.GwIpV4), err) } response.Gateway = gwIPv4.Addr().String() } if len(rsp.GwIpV6) > 0 { var gwIPv6 netip.Prefix err = gwIPv6.UnmarshalBinary(rsp.GwIpV6) if err != nil { return nil, fmt.Errorf("unable to unmarshal GwIpV6 with length %d: %w", len(rsp.GwIpV6), err) } response.GatewayIPv6 = gwIPv6.Addr().String() } clog.Debugf(n.ctx, "Join response %v", response) return response, nil } func (n *networkState) leaveEndpoint(endpointID string) error { clog.Debugf(n.ctx, "Leave endpoint %.8s", endpointID) _, err := callDaemon(n, func(ctx context.Context, client teleroute.TelerouteClient) (*emptypb.Empty, error) { return client.Leave(ctx, &teleroute.EndpointIdentifier{ Id: endpointID, }) }) if err != nil { code := status.Code(err) if code == codes.Canceled || code == codes.Unavailable { err = nil } else { clog.Errorf(n.ctx, "failed to leave endpoint %s: %v", endpointID, err) } } return err } func (n *networkState) deleteEndpoint(endpointID string) error { clog.Debugf(n.ctx, "Delete endpoint %.8s", endpointID) _, err := callDaemon(n, func(ctx context.Context, client teleroute.TelerouteClient) (*emptypb.Empty, error) { return client.RemoveEndpoint(ctx, &teleroute.EndpointIdentifier{Id: endpointID}) }) if err != nil { code := status.Code(err) if code == codes.Canceled || code == codes.Unavailable { err = nil } else { clog.Errorf(n.ctx, "failed to leave endpoint %s: %v", endpointID, err) } } return err } ================================================ FILE: cmd/teleroute/driver/options.go ================================================ package driver import ( "errors" "fmt" "net/netip" "strconv" ) type options struct { // host IP on the default bridge network for the Telepresence daemon host netip.Addr // port where the Telepresence daemon starts its teleroute server port uint16 } type errRequiredOption string func (e errRequiredOption) Error() string { return fmt.Sprintf("option %q is required", e) } func (o *options) parse(gos map[string]any) error { unableToParse := func(opt string, err error) error { return fmt.Errorf("unable to parse %q: %w", opt, err) } for key, anyVal := range gos { val, ok := anyVal.(string) if !ok { return unableToParse(key, fmt.Errorf("invalid value type %T", anyVal)) } var err error switch key { case "host": o.host, err = netip.ParseAddr(val) if err != nil { return unableToParse(key, err) } case "port": var port uint64 if port, err = strconv.ParseUint(val, 10, 16); err == nil { if port == 0 { return unableToParse(key, errors.New("cannot be 0")) } o.port = uint16(port) } default: return fmt.Errorf("illegal option %q", key) } } if !o.host.IsValid() { return errRequiredOption("host") } if o.port == 0 { return errRequiredOption("port") } return nil } ================================================ FILE: cmd/teleroute/go.mod ================================================ module github.com/telepresenceio/telepresence/cmd/teleroute go 1.25.0 require ( github.com/cenkalti/backoff/v4 v4.3.0 github.com/docker/go-plugins-helpers v0.0.0-20240701071450-45e2431495c8 github.com/puzpuzpuz/xsync/v4 v4.4.0 github.com/telepresenceio/clog v0.0.0-20260114221933-287514cf9831 github.com/telepresenceio/telepresence/rpc/v2 v2.26.2 google.golang.org/grpc v1.79.1 google.golang.org/protobuf v1.36.11 ) require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect github.com/docker/go-connections v0.6.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect ) replace github.com/telepresenceio/telepresence/rpc/v2 => ./rpc ================================================ FILE: cmd/teleroute/go.sum ================================================ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-plugins-helpers v0.0.0-20240701071450-45e2431495c8 h1:IMfrF5LCzP2Vhw7j4IIH3HxPsCLuZYjDqFAM/C88ulg= github.com/docker/go-plugins-helpers v0.0.0-20240701071450-45e2431495c8/go.mod h1:LFyLie6XcDbyKGeVK6bHe+9aJTYCxWLBg5IrJZOaXKA= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo= github.com/puzpuzpuz/xsync/v4 v4.4.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo= github.com/telepresenceio/clog v0.0.0-20260114221933-287514cf9831 h1:STun1hGf7oDZqRChINrQekn2EfypqhB8z67WlCZGHoU= github.com/telepresenceio/clog v0.0.0-20260114221933-287514cf9831/go.mod h1:UNeuJUkHiI0y2x59WmgSNMTTCYLMU12U5AUHhFi3BOA= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= ================================================ FILE: cmd/teleroute/main.go ================================================ package main import ( "context" "fmt" "log/slog" "os" "os/signal" "path/filepath" "strconv" "syscall" "github.com/docker/go-plugins-helpers/network" "github.com/telepresenceio/clog" "github.com/telepresenceio/clog/handler" "github.com/telepresenceio/telepresence/cmd/teleroute/driver" ) const ( pluginSocket = "/run/docker/plugins/teleroute.sock" pluginLog = "/var/log/teleroute.log" ) func main() { lf, err := os.Create(pluginLog) if err != nil { _, _ = fmt.Fprintf(os.Stderr, "Failed to create plugin log file %s: %s\n", pluginLog, err) os.Exit(1) } defer lf.Close() level := clog.LevelInfo if debug, ok := os.LookupEnv("DEBUG"); ok { if ok, _ = strconv.ParseBool(debug); ok { level = clog.LevelDebug } } ctx := clog.WithLogger(context.Background(), slog.New(handler.NewText(handler.Output(lf), handler.TimeFormat("15:04:05.0000"), handler.EnabledLevel(level)))) pid, err := getPluginHostPID(ctx) if err != nil { clog.Error(ctx, err) os.Exit(1) } sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) ctx, cancel := context.WithCancel(ctx) go func() { <-sigs clog.Info(ctx, "Received shutdown signal") cancel() }() if err := network.NewHandler(driver.New(ctx, pid)).ServeUnix(pluginSocket, 0); err != nil { clog.Error(ctx, err) os.Exit(1) } } const pluginEntryPoint = "/bin/docker-network-teleroute" func getPluginHostPID(ctx context.Context) (int, error) { pls, err := filepath.Glob("/proc/*/exe") if err != nil { return 0, fmt.Errorf("error listing entries matching glob /proc/*/exe: %v", err) } for _, pl := range pls { lt, _ := os.Readlink(pl) if lt == pluginEntryPoint { clog.Debugf(ctx, "%s links to %s", pl, pluginEntryPoint) if pid, err := strconv.Atoi(pl[6 : len(pl)-4]); err == nil { clog.Debugf(ctx, "pid of %s is %d", pluginEntryPoint, pid) return pid, nil } } } return 0, fmt.Errorf("unable to find PID for %s", pluginEntryPoint) } ================================================ FILE: docs/CONTRIBUTING.md ================================================ # Telepresence Documentation This folder contains the Telepresence documentation in a format suitable for a versioned folder in the telepresenceio/telepresence.io repository. The folder will show up in that repository when a new minor revision tag is created here. Assuming that a 2.20.0 release is pending, and that a release/v2.20.0 branch has been created, then: ```console $ export TELEPRESENCE_VERSION=v2.20.0 $ make prepare-release $ git push origin {,rpc/}v2.20.0 release/v2.20.0 ``` will result in a `docs/v2.20` folder with this folder's contents in the telepresenceio/telepresence.io repository. Subsequent bugfix tags for the same minor tag, i.e.: ```console $ export TELEPRESENCE_VERSION=v2.20.1 $ make prepare-release $ git push origin {,rpc/}v2.20.1 release/v2.20.1 ``` will not result in a new folder when it is pushed, but it will update the content of the `docs/v2.20` folder to reflect this folder's content for that tag. ================================================ FILE: docs/README.md ================================================ --- description: Main menu when using plain markdown. Excluded when generating the website --- # Telepresence Documentation raw markdown version, more bells and whistles at [telepresence.io](https://telepresence.io) - [Quick start](quick-start.md) - Install Telepresence - [Install Client](install/client.md) - [Upgrade Client](install/upgrade.md) - [Install Traffic Manager](install/manager.md) - [Cloud Provider Prerequisites](install/cloud.md) - Core concepts - [The developer experience and the inner dev loop](concepts/devloop.md) - [Making the remote local: Faster feedback, collaboration and debugging](concepts/faster.md) - [Intercepts](concepts/intercepts.md) - How do I... - [Code and debug an application locally](howtos/engage.md) - [Use Telepresence with Docker](howtos/docker.md) - [Extend Docker Compose with Telepresence](howtos/docker-compose.md) - [Work with large clusters](howtos/large-clusters.md) - [Host a cluster in Docker or a VM](howtos/cluster-in-vm.md) - [Intercept TLS/mTLS Applications](howtos/mtls.md) - [Use Telepresence with Azure (Microsoft Learn)](https://learn.microsoft.com/en-us/azure/aks/use-telepresence-aks.md) - Technical reference - [Architecture](reference/architecture.md) - [Telepresence CLI](reference/cli/telepresence.md) - [telepresence completion](reference/cli/telepresence_completion.md) - [telepresence compose](reference/cli/telepresence_compose.md) - [attach](reference/cli/telepresence_compose_attach.md) - [bridge](reference/cli/telepresence_compose_bridge.md) - [build](reference/cli/telepresence_compose_build.md) - [commit](reference/cli/telepresence_compose_commit.md) - [config](reference/cli/telepresence_compose_config.md) - [cp](reference/cli/telepresence_compose_cp.md) - [create](reference/cli/telepresence_compose_create.md) - [down](reference/cli/telepresence_compose_down.md) - [events](reference/cli/telepresence_compose_events.md) - [exec](reference/cli/telepresence_compose_exec.md) - [export](reference/cli/telepresence_compose_export.md) - [images](reference/cli/telepresence_compose_images.md) - [kill](reference/cli/telepresence_compose_kill.md) - [logs](reference/cli/telepresence_compose_logs.md) - [ls](reference/cli/telepresence_compose_ls.md) - [pause](reference/cli/telepresence_compose_pause.md) - [port](reference/cli/telepresence_compose_port.md) - [ps](reference/cli/telepresence_compose_ps.md) - [publish](reference/cli/telepresence_compose_publish.md) - [pull](reference/cli/telepresence_compose_pull.md) - [push](reference/cli/telepresence_compose_push.md) - [restart](reference/cli/telepresence_compose_restart.md) - [rm](reference/cli/telepresence_compose_rm.md) - [run](reference/cli/telepresence_compose_run.md) - [scale](reference/cli/telepresence_compose_scale.md) - [start](reference/cli/telepresence_compose_start.md) - [stats](reference/cli/telepresence_compose_stats.md) - [stop](reference/cli/telepresence_compose_stop.md) - [top](reference/cli/telepresence_compose_top.md) - [unpause](reference/cli/telepresence_compose_unpause.md) - [up](reference/cli/telepresence_compose_up.md) - [version](reference/cli/telepresence_compose_version.md) - [volumes](reference/cli/telepresence_compose_volumes.md) - [wait](reference/cli/telepresence_compose_wait.md) - [watch](reference/cli/telepresence_compose_watch.md) - [telepresence config](reference/cli/telepresence_config.md) - [view](reference/cli/telepresence_config_view.md) - [telepresence connect](reference/cli/telepresence_connect.md) - [telepresence curl](reference/cli/telepresence_curl.md) - [telepresence docker-run](reference/cli/telepresence_docker-run.md) - [telepresence gather-logs](reference/cli/telepresence_gather-logs.md) - [telepresence genyaml](reference/cli/telepresence_genyaml.md) - [annotations](reference/cli/telepresence_genyaml_annotations.md) - [annotations](reference/cli/telepresence_genyaml_annotations.md) - [config](reference/cli/telepresence_genyaml_config.md) - [container](reference/cli/telepresence_genyaml_container.md) - [initcontainer](reference/cli/telepresence_genyaml_initcontainer.md) - [volume](reference/cli/telepresence_genyaml_volume.md) - [telepresence helm](reference/cli/telepresence_helm.md) - [helm install](reference/cli/telepresence_helm_install.md) - [helm lint](reference/cli/telepresence_helm_lint.md) - [helm uninstall](reference/cli/telepresence_helm_uninstall.md) - [helm upgrade](reference/cli/telepresence_helm_upgrade.md) - [helm version](reference/cli/telepresence_helm_version.md) - [telepresence ingest](reference/cli/telepresence_ingest.md) - [telepresence intercept](reference/cli/telepresence_intercept.md) - [telepresence leave](reference/cli/telepresence_leave.md) - [telepresence list](reference/cli/telepresence_list.md) - [telepresence list-contexts](reference/cli/telepresence_list-contexts.md) - [telepresence list-namespaces](reference/cli/telepresence_list-namespaces.md) - [telepresence loglevel](reference/cli/telepresence_loglevel.md) - [telepresence mcp](reference/cli/telepresence_mcp.md) - [start](reference/cli/telepresence_mcp_start.md) - [tools](reference/cli/telepresence_mcp_tools.md) - [stream](reference/cli/telepresence_mcp_stream.md) - [claude](reference/cli/telepresence_mcp_claude.md) - [enable](reference/cli/telepresence_mcp_claude_enable.md) - [disable](reference/cli/telepresence_mcp_claude_disable.md) - [list](reference/cli/telepresence_mcp_claude_list.md) - [cursor](reference/cli/telepresence_mcp_cursor.md) - [enable](reference/cli/telepresence_mcp_cursor_enable.md) - [disable](reference/cli/telepresence_mcp_cursor_disable.md) - [list](reference/cli/telepresence_mcp_cursor_list.md) - [vscode](reference/cli/telepresence_mcp_vscode.md) - [enable](reference/cli/telepresence_mcp_vscode_enable.md) - [disable](reference/cli/telepresence_mcp_vscode_disable.md) - [list](reference/cli/telepresence_mcp_vscode_list.md) - [telepresence quit](reference/cli/telepresence_quit.md) - [telepresence replace](reference/cli/telepresence_replace.md) - [telepresence revoke](reference/cli/telepresence_revoke.md) - [telepresence serve](reference/cli/telepresence_serve.md) - [telepresence status](reference/cli/telepresence_status.md) - [telepresence uninstall](reference/cli/telepresence_uninstall.md) - [telepresence version](reference/cli/telepresence_version.md) - [telepresence wiretap](reference/cli/telepresence_wiretap.md) - [Laptop-side configuration](reference/config.md) - [Cluster-side configuration](reference/cluster-config.md) - [Using Docker for engagements](reference/docker-run.md) - [Telepresence Compose Extensions](reference/compose.md) - [Running Telepresence in a Docker container](reference/inside-container.md) - [Environment variables](reference/environment.md) - Engagements - [Configure intercept using CLI](reference/engagements/cli.md) - [Traffic Agent Sidecar](reference/engagements/sidecar.md) - [Target a specific container](reference/engagements/container.md) - [Dealing With Conflicting Engagements](reference/engagements/conflicts.md) - [Telepresence Docker Plugins](reference/plugins.md) - [Volume mounts](reference/volume.md) - [RESTful API service](reference/restapi.md) - [DNS resolution](reference/dns.md) - [RBAC](reference/rbac.md) - [Telepresence and VPNs](reference/vpn.md) - [Networking through Virtual Network Interface](reference/tun-device.md) - [Connection Routing](reference/routing.md) - [Route Controller](reference/route-controller.md) - [Monitoring](reference/monitoring.md) - Comparisons - [Telepresence vs mirrord](compare/mirrord.md) - [FAQs](faqs.md) - [Troubleshooting](troubleshooting.md) - [Community](community.md) - [Release Notes](release-notes.md) - [Licenses](licenses.md) ================================================ FILE: docs/common/quantity.md ================================================ --- title: Quantity --- Quantity is measured in bytes. You can express it as a plain integer or as a fixed-point number using E, G, M, or K. You can also use the power-of-two equivalents: Gi, Mi, Ki. For example, the following represent roughly the same value: ``` 128974848, 129e6, 129M, 123Mi ``` ================================================ FILE: docs/community.md ================================================ --- title: Community hide_table_of_contents: true --- # Community ## Contributor's guide Please review our [contributor's guide](https://github.com/telepresenceio/telepresence/blob/release/v2/CONTRIBUTING.md) on GitHub to learn how you can help make Telepresence better. ## Meetings Check out our community [meeting schedule](https://github.com/telepresenceio/telepresence/blob/release/v2/MEETING_SCHEDULE.md) for opportunities to interact with Telepresence developers. ================================================ FILE: docs/compare/mirrord.md ================================================ --- title: "Telepresence vs mirrord" hide_table_of_contents: true --- ## Telepresence Telepresence is a very feature rich tool, designed to handle a large majority of use-cases. You can use it as a cluster VPN only, or use one of its three different ways (replace, intercept, or ingest) to engage with the cluster's resources. Telepresence is intended to be installed in the cluster by an administrator and then let clients connect with a very limited set of permissions. This model is generally required by larger companies. The client can be either completely contained in Docker or run directly on the workstation. The latter requires the creation of a virtual network device, but admin access is not needed when Telepresence is installed using a package installer that configures the root daemon as a system service. ## mirrord Mirrord was designed with simplicity in mind. You install the CLI tool, and that's it. It will do the rest automatically under the hood. Mirrord solves the same problem as Telepresence, but in a different way. Instead of providing a proper network device and remotely mounted filesystems, mirrord will link the client application with a `mirrord-layer` shared library. This library will inject code that intercepts accesses to the network, file system, and environment variables, and reroute them to a corresponding process in the cluster (the `mirrord-agent`) which then interacts with the targeted pod. ### Limitations with Code Injection Telepresence 1.x used the [code injection approach](https://www.getambassador.io/blog/code-injection-on-linux-and-macos), but desided to abandon it due to several limitations: 1. It will only work on Linux and macOS platforms. There's no native support on Windows. 2. It will only work with dynamically linked executables. 3. It cannot be used with docker unless you rebuild the container and inject the `mirrord-layer` into it. 4. `DYLD_INSERT_LIBRARIES` causes various problems on macOS (SIP prevents it from being used), especially on silicon-based machines where mirrord will require Rosetta. 5. Should Apple decide to protect their intel-based platform the same way as the silicon-based one in a future release, then mirrord will likely be problematic to use on that platform. ### Cluster Permissions Mirrord does not require a sidecar. Instead they install a the `mirror-agent` into the namespace of the pod that it impersonates. That agent requires several permissions that a cluster admin might consider a security risk: * `CAP_NET_ADMIN` and `CAP_NET_RAW` - required for modifying routing tables * `CAP_SYS_PTRACE` - required for reading target pod environment * `CAP_SYS_ADMIN` - required for joining target pod network namespace Unless using "mirrord for Teams" (proprietary), all users must have permissions to create the job running the `mirror-agent` in the cluster. ## Comparison Telepresence vs mirrord This comparison chart applies to the Open Source editions of both products. | Feature | Telepresence | mirrord | |------------------------------------------------------------------------------|--------------|---------| | Run or Debug your cluster containers locally | ✅ | ✅ | | Does not need administrative permission on workstation | ✅ [^1] | ✅ | | Can be used with very large clusters | ✅ | ✅ | | Works without interrupting the remote service | ✅ [^2] | ✅ | | Doesn't require injection of a sidecar | ✅ [^3] | ✅ | | Supports connecting to clusters over a corporate VPN | ✅ | ✅ | | Can intercept traffic | ✅ | ✅ | | Can filter traffic based on HTTP headers and paths | ✅ | ✅ | | Can mirror traffic | ✅ | ✅ | | Can ingest a container | ✅ | ❌ | | Can replace a container | ✅ | ❌ | | Can act as a cluster VPN only | ✅ | ❌ | | Will work with statically linked binaries | ✅ | ❌ | | Runs natively on windows | ✅ | ❌ | | Can intercept traffic to and from pod's localhost | ✅ | ❌ | | Remotely mounted file system available from all applications | ✅ | ❌ | | Cluster network available to all applications (including browser) | ✅ | ❌ | | Can run the same docker container locally without rebuilding it | ✅ | ❌ | | Integrates with Docker Compose | ✅ | ❌ | | Provides an API server allowing introspection of replacements and intercepts | ✅ | ❌ | | Provides remote mounts as volumes in docker | ✅ | ❌ | | Does not require special capabilities such as CAP_SYS_ADMIN in the cluster | ✅ | ❌ | | Centralized client configuration using Helm chart | ✅ | ❌ | | Installed using a JSON-schema validated Helm chart | ✅ | ❌ | | Client need no special RBAC permissions | ✅ | ❌ | [^1]: Telepresence does not require root access on the workstation when installed using a package installer (which configures the root daemon as a system service) or when running in docker mode. [^2]: The remote service will only restart when a traffic-agent sidecar is installed. Pod disruption budgets or pre-installed agents can be used to avoid interruptions. [^3]: A traffic-agent is necessary when engaging with a pod. It is unnecessary when using Telepresence as a VPN. ================================================ FILE: docs/concepts/devloop.md ================================================ --- title: The developer experience and the inner dev loop hide_table_of_contents: true --- # The developer experience and the inner dev loop ## How is the developer experience changing? The developer experience is the workflow a developer uses to develop, test, deploy, and release software. Typically this experience has consisted of both an inner dev loop and an outer dev loop. The inner dev loop is where the individual developer codes and tests, and once the developer pushes their code to version control, the outer dev loop is triggered. The outer dev loop is _everything else_ that happens leading up to release. This includes code merge, automated code review, test execution, deployment, controlled (canary) release, and observation of results. The modern outer dev loop might include, for example, an automated CI/CD pipeline as part of a GitOps workflow and a progressive delivery strategy relying on automated canaries, i.e. to make the outer loop as fast, efficient and automated as possible. Cloud-native technologies have fundamentally altered the developer experience in two ways: one, developers now have to take extra steps in the inner dev loop; two, developers need to be concerned with the outer dev loop as part of their workflow, even if most of their time is spent in the inner dev loop. Engineers now must design and build distributed service-based applications _and_ also assume responsibility for the full development life cycle. The new developer experience means that developers can no longer rely on monolithic application developer best practices, such as checking out the entire codebase and coding locally with a rapid “live-reload” inner development loop. Now developers have to manage external dependencies, build containers, and implement orchestration configuration (e.g. Kubernetes YAML). This may appear trivial at first glance, but this adds development time to the equation. ## What is the inner dev loop? The inner dev loop is the single developer workflow. A single developer should be able to set up and use an inner dev loop to code and test changes quickly. Even within the Kubernetes space, developers will find much of the inner dev loop familiar. That is, code can still be written locally at a level that a developer controls and committed to version control. In a traditional inner dev loop, if a typical developer codes for 360 minutes (6 hours) a day, with a traditional local iterative development loop of 5 minutes — 3 coding, 1 building, i.e. compiling/deploying/reloading, 1 testing inspecting, and 10-20 seconds for committing code — they can expect to make ~70 iterations of their code per day. Any one of these iterations could be a release candidate. The only “developer tax” being paid here is for the commit process, which is negligible. ![traditional inner dev loop](../images/trad-inner-dev-loop.png#devloop) ## In search of lost time: How does containerization change the inner dev loop? The inner dev loop is where writing and testing code happens, and time is critical for maximum developer productivity and getting features in front of end users. The faster the feedback loop, the faster developers can refactor and test again. Changes to the inner dev loop process, i.e., containerization, threaten to slow this development workflow down. Coding stays the same in the new inner dev loop, but code has to be containerized. The _containerized_ inner dev loop requires a number of new steps: * packaging code in containers * writing a manifest to specify how Kubernetes should run the application (e.g., YAML-based configuration information, such as how much memory should be given to a container) * pushing the container to the registry * deploying containers in Kubernetes Each new step within the container inner dev loop adds to overall development time, and developers are repeating this process frequently. If the build time is incremented to 5 minutes — not atypical with a standard container build, registry upload, and deploy — then the number of possible development iterations per day drops to ~40. At the extreme that’s a 40% decrease in potential new features being released. This new container build step is a hidden tax, which is quite expensive. ![container inner dev loop](../images/container-inner-dev-loop.png#devloop) ## Tackling the slow inner dev loop A slow inner dev loop can negatively impact frontend and backend teams, delaying work on individual and team levels and slowing releases into production overall. For example: * Frontend developers have to wait for previews of backend changes on a shared dev/staging environment (for example, until CI/CD deploys a new version) and/or rely on mocks/stubs/virtual services when coding their application locally. These changes are only verifiable by going through the CI/CD process to build and deploy within a target environment. * Backend developers have to wait for CI/CD to build and deploy their app to a target environment to verify that their code works correctly with cluster or cloud-based dependencies as well as to share their work to get feedback. New technologies and tools can facilitate cloud-native, containerized development. And in the case of a sluggish inner dev loop, developers can accelerate productivity with tools that help speed the loop up again. ================================================ FILE: docs/concepts/faster.md ================================================ --- title: "Making the remote local: Faster feedback, collaboration and debugging" hide_table_of_contents: true --- --- # Making the remote local: Faster feedback, collaboration and debugging With the goal of achieving fast, efficient development, developers need a set of approaches to bridge the gap between remote Kubernetes clusters and local development, and reduce time to feedback and debugging. ## How should I set up a Kubernetes development environment? Setting up a development environment for Kubernetes can be much more complex than the setup for traditional web applications. Creating and maintaining a Kubernetes development environment relies on a number of external dependencies, such as databases or authentication. While there are several ways to set up a Kubernetes development environment, most introduce complexities and impediments to speed. The dev environment should be set up to easily code and test in conditions where a service can access the resources it depends on. A good way to meet the goals of faster feedback, possibilities for collaboration, and scale in a realistic production environment is the "single service local, all other remote" environment. Developing in a fully remote environment offers some benefits, but for developers, it offers the slowest possible feedback loop. With local development in a remote environment, the developer retains considerable control while using tools like [Telepresence](../quick-start.md) to facilitate fast feedback, debugging and collaboration. ## What is Telepresence? Telepresence is an open source tool that lets developers [code and test microservices locally against a remote Kubernetes cluster](../quick-start.md). Telepresence facilitates more efficient development workflows while relieving the need to worry about other service dependencies. ## How can I get fast, efficient local development? The dev loop can be jump-started with the right development environment and Kubernetes development tools to support speed, efficiency and collaboration. Telepresence is designed to let Kubernetes developers code as though their laptop is in their Kubernetes cluster, enabling the service to run locally and be proxied into the remote cluster. Telepresence runs code locally and forwards requests to and from the remote Kubernetes cluster, bypassing the much slower process of waiting for a container to build, pushing it to registry, and deploying to production. A rapid and continuous feedback loop is essential for productivity and speed; Telepresence enables the fast, efficient feedback loop to ensure that developers can access the rapid local development loop they rely on without disrupting their own or other developers' workflows. Telepresence safely intercepts traffic from the production cluster and enables near-instant testing of code and local debugging in production. Telepresence works by deploying a two-way network proxy in a pod running in a Kubernetes cluster. This pod proxies data from the Kubernetes environment (e.g., TCP/UDP connections, environment variables, volumes) to the local process. This proxy can intercept traffic meant for the service and reroute it to a local copy, which is ready for further (local) development. ================================================ FILE: docs/concepts/intercepts.md ================================================ --- title: "Intercepts" description: "Short demonstration of global intercepts" hide_table_of_contents: true --- import Admonition from '@theme/Admonition'; import Paper from '@mui/material/Paper'; import Tab from '@mui/material/Tab'; import TabContext from '@mui/lab/TabContext'; import TabList from '@mui/lab/TabList'; import TabPanel from '@mui/lab/TabPanel'; import TabsContainer from '@site/src/components/TabsContainer'; import Animation from '@site/src/components/InterceptAnimation'; # No intercept This is the normal operation of your cluster without Telepresence. # Intercept **Intercepts** replace the Kubernetes "Orders" service with the Orders service running on your laptop. The users see no change, but with all the traffic coming to your laptop, you can observe and debug with all your dev tools. ### Creating and using intercepts 1. Creating the intercept: Intercept your service from your CLI: ```shell telepresence intercept SERVICENAME ``` Make sure your current kubectl context points to the target cluster. If your service is running in a different namespace than your current active context, use or change the `--namespace` flag. 2. Using the intercept: Send requests to your service: All requests will be sent to the version of your service that is running in the local development environment. ================================================ FILE: docs/doc-links.yml ================================================ - title: Quick start link: quick-start - title: Install Telepresence items: - title: Install Client link: install/client - title: Upgrade Client link: install/upgrade - title: Install Traffic Manager link: install/manager - title: Cloud Provider Prerequisites link: install/cloud - title: Core concepts items: - title: The developer experience and the inner dev loop link: concepts/devloop - title: "Making the remote local: Faster feedback, collaboration and debugging" link: concepts/faster - title: Intercepts link: concepts/intercepts - title: How do I... items: - title: Code and debug an application locally link: howtos/engage - title: Use Telepresence with Docker link: howtos/docker - title: Extend Docker Compose with Telepresence link: howtos/docker-compose - title: Work with large clusters link: howtos/large-clusters - title: Host a cluster in Docker or a VM link: howtos/cluster-in-vm - title: Intercept TLS/mTLS Applications link: howtos/mtls - title: Use Telepresence with Azure (Microsoft Learn) link: https://learn.microsoft.com/en-us/azure/aks/use-telepresence-aks - title: Technical reference items: - title: Architecture link: reference/architecture - title: Telepresence CLI link: reference/cli/telepresence items: - title: telepresence completion link: reference/cli/telepresence_completion - title: telepresence compose link: reference/cli/telepresence_compose items: - title: attach link: reference/cli/telepresence_compose_attach - title: bridge link: reference/cli/telepresence_compose_bridge - title: build link: reference/cli/telepresence_compose_build - title: commit link: reference/cli/telepresence_compose_commit - title: config link: reference/cli/telepresence_compose_config - title: cp link: reference/cli/telepresence_compose_cp - title: create link: reference/cli/telepresence_compose_create - title: down link: reference/cli/telepresence_compose_down - title: events link: reference/cli/telepresence_compose_events - title: exec link: reference/cli/telepresence_compose_exec - title: export link: reference/cli/telepresence_compose_export - title: images link: reference/cli/telepresence_compose_images - title: kill link: reference/cli/telepresence_compose_kill - title: logs link: reference/cli/telepresence_compose_logs - title: ls link: reference/cli/telepresence_compose_ls - title: pause link: reference/cli/telepresence_compose_pause - title: port link: reference/cli/telepresence_compose_port - title: ps link: reference/cli/telepresence_compose_ps - title: publish link: reference/cli/telepresence_compose_publish - title: pull link: reference/cli/telepresence_compose_pull - title: push link: reference/cli/telepresence_compose_push - title: restart link: reference/cli/telepresence_compose_restart - title: rm link: reference/cli/telepresence_compose_rm - title: run link: reference/cli/telepresence_compose_run - title: scale link: reference/cli/telepresence_compose_scale - title: start link: reference/cli/telepresence_compose_start - title: stats link: reference/cli/telepresence_compose_stats - title: stop link: reference/cli/telepresence_compose_stop - title: top link: reference/cli/telepresence_compose_top - title: unpause link: reference/cli/telepresence_compose_unpause - title: up link: reference/cli/telepresence_compose_up - title: version link: reference/cli/telepresence_compose_version - title: volumes link: reference/cli/telepresence_compose_volumes - title: wait link: reference/cli/telepresence_compose_wait - title: watch link: reference/cli/telepresence_compose_watch - title: telepresence config link: reference/cli/telepresence_config items: - title: view link: reference/cli/telepresence_config_view - title: telepresence connect link: reference/cli/telepresence_connect - title: telepresence curl link: reference/cli/telepresence_curl - title: telepresence docker-run link: reference/cli/telepresence_docker-run - title: telepresence gather-logs link: reference/cli/telepresence_gather-logs - title: telepresence genyaml link: reference/cli/telepresence_genyaml items: - title: annotations link: reference/cli/telepresence_genyaml_annotations - title: annotations link: reference/cli/telepresence_genyaml_annotations - title: config link: reference/cli/telepresence_genyaml_config - title: container link: reference/cli/telepresence_genyaml_container - title: initcontainer link: reference/cli/telepresence_genyaml_initcontainer - title: volume link: reference/cli/telepresence_genyaml_volume - title: telepresence helm link: reference/cli/telepresence_helm items: - title: helm install link: reference/cli/telepresence_helm_install - title: helm lint link: reference/cli/telepresence_helm_lint - title: helm uninstall link: reference/cli/telepresence_helm_uninstall - title: helm upgrade link: reference/cli/telepresence_helm_upgrade - title: helm version link: reference/cli/telepresence_helm_version - title: telepresence ingest link: reference/cli/telepresence_ingest - title: telepresence intercept link: reference/cli/telepresence_intercept - title: telepresence leave link: reference/cli/telepresence_leave - title: telepresence list link: reference/cli/telepresence_list - title: telepresence list-contexts link: reference/cli/telepresence_list-contexts - title: telepresence list-namespaces link: reference/cli/telepresence_list-namespaces - title: telepresence loglevel link: reference/cli/telepresence_loglevel - title: telepresence mcp link: reference/cli/telepresence_mcp items: - title: start link: reference/cli/telepresence_mcp_start - title: tools link: reference/cli/telepresence_mcp_tools - title: stream link: reference/cli/telepresence_mcp_stream - title: claude link: reference/cli/telepresence_mcp_claude items: - title: enable link: reference/cli/telepresence_mcp_claude_enable - title: disable link: reference/cli/telepresence_mcp_claude_disable - title: list link: reference/cli/telepresence_mcp_claude_list - title: cursor link: reference/cli/telepresence_mcp_cursor items: - title: enable link: reference/cli/telepresence_mcp_cursor_enable - title: disable link: reference/cli/telepresence_mcp_cursor_disable - title: list link: reference/cli/telepresence_mcp_cursor_list - title: vscode link: reference/cli/telepresence_mcp_vscode items: - title: enable link: reference/cli/telepresence_mcp_vscode_enable - title: disable link: reference/cli/telepresence_mcp_vscode_disable - title: list link: reference/cli/telepresence_mcp_vscode_list - title: telepresence quit link: reference/cli/telepresence_quit - title: telepresence replace link: reference/cli/telepresence_replace - title: telepresence revoke link: reference/cli/telepresence_revoke - title: telepresence serve link: reference/cli/telepresence_serve - title: telepresence status link: reference/cli/telepresence_status - title: telepresence uninstall link: reference/cli/telepresence_uninstall - title: telepresence version link: reference/cli/telepresence_version - title: telepresence wiretap link: reference/cli/telepresence_wiretap - title: Laptop-side configuration link: reference/config - title: Cluster-side configuration link: reference/cluster-config - title: Using Docker for engagements link: reference/docker-run - title: Telepresence Compose Extensions link: reference/compose - title: Running Telepresence in a Docker container link: reference/inside-container - title: Environment variables link: reference/environment - title: Engagements items: - title: Configure intercept using CLI link: reference/engagements/cli - title: Traffic Agent Sidecar link: reference/engagements/sidecar - title: Target a specific container link: reference/engagements/container - title: Dealing With Conflicting Engagements link: reference/engagements/conflicts - title: Telepresence Docker Plugins link: reference/plugins - title: Volume mounts link: reference/volume - title: RESTful API service link: reference/restapi - title: DNS resolution link: reference/dns - title: RBAC link: reference/rbac - title: Telepresence and VPNs link: reference/vpn - title: Networking through Virtual Network Interface link: reference/tun-device - title: Connection Routing link: reference/routing - title: Route Controller link: reference/route-controller - title: Monitoring link: reference/monitoring - title: Comparisons items: - title: Telepresence vs mirrord link: compare/mirrord - title: FAQs link: faqs - title: Troubleshooting link: troubleshooting - title: Community link: community - title: Release Notes link: release-notes - title: Licenses link: licenses ================================================ FILE: docs/faqs.md ================================================ --- title: FAQs description: "Learn how Telepresence helps with fast development and debugging in your Kubernetes cluster." hide_table_of_contents: true --- # FAQs ### Why Telepresence Modern microservices-based applications that are deployed into Kubernetes often consist of tens or hundreds of services. The resource constraints and number of these services means that it is often difficult to impossible to run all of this on a local development machine, which makes fast development and debugging very challenging. The fast [inner development loop](concepts/devloop.md) from previous software projects is often a distant memory for cloud developers. Telepresence enables you to connect your local development machine seamlessly to the cluster via a two-way proxying mechanism. This enables you to code locally and run the majority of your services within a remote Kubernetes cluster — which in the cloud means you have access to effectively unlimited resources. Ultimately, this empowers you to develop services locally and still test integrations with dependent services or data stores running in the remote cluster. Telepresence provides three different ways for you to code, debug, and test your service locally using your favourite local IDE and in-process debugger. First off, you can "replace" the service with your own local version. This means even though you run your service locally, you can see how it interacts with the rest of the services in the cluster. It's like swapping out a piece of a puzzle and seeing how the whole picture changes. Your local process will have access to the same network, environment, and volumes as the service that it replaces. You can also "intercept" any requests made to a service. This is similar to replacing the service, but the remote service will keep running, perform background tasks, and handle traffic that isn't intercepted. Finally, you can "ingest" a service. Again, similar to a "replace", but nothing changes in the cluster during an "ingest", and no traffic is routed to the workstation. #### What operating systems does Telepresence work on? Telepresence currently works natively on macOS, Linux, and Windows. #### What architecture does Telepresence work on? All Telepresence binaries are released for both AMD (Intel) and ARM (Apple Silicon) chips. #### What protocols can be intercepted by Telepresence? Both TCP and UDP are supported. #### When using Telepresence to run a cluster service locally, are the Kubernetes cluster environment variables proxied on my local machine? Yes, you can either set the container's environment variables on your machine or write the variables to a file to use with Docker or another build process. You can also directly pass the environments to a handler that runs locally. Please see [the environment variable reference doc](reference/environment.md) for more information. #### When using Telepresence to run a cluster service locally, can the associated container volume mounts also be mounted by my local machine? Yes, and when running Docker, they can be used as docker volumes. Please see [the volume mounts reference doc](reference/volume.md) for more information. #### When connected to a Kubernetes cluster via Telepresence, can I access cluster-based services via their DNS name? Yes. After you have successfully connected to your cluster via `telepresence connect -n ` you will be able to access any service in the connected namespace in your cluster via their DNS name. This means you can curl endpoints directly e.g. `curl :8080/mypath`. You can also access services in other namespaces using their namespaced qualified name, e.g.`curl .:8080/mypath`. In essence, Telepresence makes the DNS of the connected namespace available locally. This means that you can connect to all databases or middleware running in the cluster, such as MySQL, PostgreSQL and RabbitMQ, via their service name. #### When connected to a Kubernetes cluster via Telepresence, can I access cloud-based services and data stores via their DNS name? You can connect to cloud-based data stores and services that are directly addressable within the cluster (e.g. when using an [ExternalName](https://kubernetes.io/docs/concepts/services-networking/service/#externalname) Service type), such as AWS RDS, Google pub-sub, or Azure SQL Database. #### Will Telepresence be able to engage with workloads running on a private cluster or cluster running within a virtual private cloud (VPC)? Yes, but it doesn't need to have a publicly accessible IP address. The cluster must also have access to an external registry to be able to download the traffic-manager and traffic-agent images that are deployed when connecting with Telepresence. #### Why does running Telepresence sometimes require sudo access for the local daemon? The local daemon needs to create a VIF (Virtual Network Interface) for outbound routing and DNS, which is a privileged operation. However, sudo is **not** required when: - Telepresence was installed using a [package installer](install/client.md) (`.pkg` on macOS, `.deb`/`.rpm` on Linux, or the Windows setup installer), which configures the root daemon as a system service. - Telepresence runs in [Docker mode](howtos/docker.md) (`telepresence connect --docker`). Sudo is only needed when using a standalone binary installation without a system service. #### What components get installed in the cluster when running Telepresence? A `traffic-manager` service is deployed in a namespace of your choice (default 'ambassador') within your cluster, and this manages resilient intercepts and connections between your local machine and the cluster. A Traffic Agent container is injected per pod that is being engaged. The injection happens the first time a `replace`, an `ingest`, or an `intercept` is made on a workload, unless you choose to control the injection using an annotation, in which case the injection happens when the `traffic-manager` is installed. #### How can I remove all the Telepresence components installed within my cluster? You can run the command `telepresence helm uninstall` to remove everything from the cluster, including the `traffic-manager`, and all the `traffic-agent` containers injected into each pod being engaged. You also can run the command `telepresence uninstall ` to remove the injected `traffic-agent` containers injected into each pod for that workload. Also run `telepresence quit -s` to stop all local daemons running. #### What language is Telepresence written in? All components of the Telepresence application and cluster components are written using Go. #### How does Telepresence connect and tunnel into the Kubernetes cluster? The connection between your laptop and cluster is established by using the `kubectl port-forward` machinery (though without actually spawning a separate program) to establish a TLS encrypted connection to Telepresence Traffic Manager and Traffic Agents in the cluster, and running Telepresence's custom VPN protocol over that connection. #### Is Telepresence OSS open source? Yes, it is! You'll find both source code and documentation in the [Telepresence GitHub repository](https://github.com/telepresenceio/telepresence), licensed using the [apache License Version 2.0](https://github.com/telepresenceio/telepresence?tab=License-1-ov-file#readme). #### How do I share my feedback on Telepresence? Your feedback is always appreciated and helps us build a product that provides as much value as possible for our community. You can chat with us directly on our #telepresence-oss channel at the [CNCF Slack](https://slack.cncf.io), and also report issues or create pull-requests on the GitHub repository. ================================================ FILE: docs/helm/values.schema.json ================================================ {"$defs":{"affinity":{"$ref":"#/definitions/io.k8s.api.core.v1.Affinity"},"duration":{"pattern":"^[+-]?(\\d+(\\.\\d+)?(h|m|s|ms|us|µs))+","type":"string"},"labelSelector":{"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector"},"localObjectReference":{"$ref":"#/definitions/io.k8s.api.core.v1.LocalObjectReference"},"logLevel":{"enum":["error","warning","warn","info","debug","trace"],"type":"string"},"nodeSelector":{"additionalProperties":{"type":"string"},"type":"object"},"podSecurityContext":{"$ref":"#/definitions/io.k8s.api.core.v1.PodSecurityContext"},"probe":{"$ref":"#/definitions/io.k8s.api.core.v1.Probe"},"pullPolicy":{"enum":["Always","Never","IfNotPresent"],"type":"string"},"quantity":{"$ref":"#/definitions/io.k8s.apimachinery.pkg.api.resource.Quantity"},"resourceRequirements":{"$ref":"#/definitions/io.k8s.api.core.v1.ResourceRequirements"},"rfc1123Label":{"description":"RFC 1123 label name","pattern":"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$","type":"string"},"securityContext":{"$ref":"#/definitions/io.k8s.api.core.v1.SecurityContext"},"subject":{"$ref":"#/definitions/io.k8s.api.rbac.v1.Subject"},"toleration":{"$ref":"#/definitions/io.k8s.api.core.v1.Toleration"}},"$id":"https://github.com/telepresenceio/telepresence-oss.schema.json","$schema":"https://json-schema.org/draft/2020-12/schema#","additionalProperties":false,"description":"Values Schema for the Telepresence OSS Helm Chart","properties":{"affinity":{"$ref":"#/$defs/affinity","description":"If specified, the pod's scheduling constraints"},"agent":{"additionalProperties":false,"description":"Telepresence traffic-agent configuration","properties":{"enableConsumptionMetrics":{"description":"Enable (the default) or disable the consumption metrics of the traffic-agent. Only valid when the prometheus.port is set.","type":"boolean"},"enableH2cProbing":{"description":"If enabled, the agent will probe for HTTP2 using clear-text (h2c) support non-TLS ports unless they are configured with appProtocol.","type":"boolean"},"image":{"additionalProperties":false,"properties":{"name":{"description":"The name of the injected agent image","type":"string"},"pullPolicy":{"$ref":"#/$defs/pullPolicy","description":"Pull policy in the webhook for the traffic agent image"},"pullSecrets":{"description":"The Secret storing any credentials needed to access the image in a private registry","items":{"$ref":"#/$defs/localObjectReference"},"type":"array"},"registry":{"description":"The registry for the injected agent image","type":"string"},"tag":{"description":"Overrides the image tag whose default is the chart appVersion","type":"string"}},"type":"object"},"initContainer":{"additionalProperties":false,"description":"Configuration for the init-container that runs before the traffic-agent","properties":{"enabled":{"description":"Enable/Disable the init-container","type":"boolean"}},"type":"object"},"initResources":{"$ref":"#/$defs/resourceRequirements","description":"Resource requirements for the init-container"},"initSecurityContext":{"$ref":"#/$defs/securityContext","description":"Security context for the init-container"},"logLevel":{"$ref":"#/$defs/logLevel","description":"Loglevel for the agent, if different from the traffic-manager"},"maxIdleTime":{"$ref":"#/$defs/duration","description":"maximum time the agent is idle (no engagements to the workload) before it is cleaned up by the traffic manager"},"mountPolicies":{"additionalProperties":{"description":"The name or path prefix match of the volume that the policy applies to","enum":["Remote","RemoteReadOnly","Local","Ignore"],"type":"string"},"description":"List of volume mount policies. Defaults to {\"/tmp\" : \"Ignore\"}","type":"object"},"port":{"description":"Port number of the first port that the agent will use","type":"integer"},"resources":{"$ref":"#/$defs/resourceRequirements","description":"Resource requirements for the agent"},"securityContext":{"$ref":"#/$defs/securityContext","description":"Security context for the agent"},"watchRetryInterval":{"$ref":"#/$defs/duration","description":"The interval between retries that a watcher uses when the gRPC connection to the traffic manager is lost"}},"type":"object"},"agentInjector":{"additionalProperties":false,"description":"Properties for the agent injector service","properties":{"certificate":{"additionalProperties":false,"properties":{"accessMethod":{"description":"Method used by the agent injector to access the certificate (watch or mount)","enum":["watch","mount"],"type":"string"},"certmanager":{"additionalProperties":false,"properties":{"commonName":{"description":"The common name of the generated Certmanager certificate","type":"string"},"duration":{"$ref":"#/$defs/duration","description":"The certificate validity duration"},"issuerRef":{"additionalProperties":false,"properties":{"kind":{"description":"The Issuer kind to use to generate the self signed certificate (Issuer of ClusterIssuer)","enum":["Issuer","ClusterIssuer"],"type":"string"},"name":{"description":"The Issuer name to use to generate the self signed certificate","type":"string"}},"type":"object"}},"type":"object"},"method":{"description":"Method used when generating the certificate used for mutating webhook (helm, supplied, or certmanager)","enum":["helm","supplied","certmanager"],"type":"string"},"regenerate":{"description":"Whether the certificate used for the mutating webhook should be regenerated","type":"boolean"}},"type":"object"},"enabled":{"description":"Enable/Disable the agent-injector and its webhook","type":"boolean"},"injectPolicy":{"description":"Determines when an agent is injected, possible values are OnDemand and WhenEnabled","enum":["OnDemand","WhenEnabled"],"type":"string"},"mutationAware":{"description":"Include changes to the pod template that are contributed by other injectors. Implies reinvocationPolicy=IfNeeded","type":"boolean"},"name":{"description":"Name to use with objects associated with the agent-injector","type":"string"},"secret":{"properties":{"name":{"$ref":"#/$defs/rfc1123Label","description":"The name of the secret the agent-injector webhook uses for authorization with the kubernetes api will expose"}},"type":"object"},"service":{"additionalProperties":false,"properties":{"type":{"description":"Type of service for the agent-injector.","type":"string"}},"type":"object"},"webhook":{"additionalProperties":false,"properties":{"admissionReviewVersions":{"description":"List of supported admissionReviewVersions","items":{"type":"string"},"type":"array"},"failurePolicy":{"description":"Action to take on unexpected failure or timeout of webhook.","enum":["Fail","Ignore"],"type":"string"},"name":{"description":"The name of the agent-injector webhook","type":"string"},"port":{"description":"Port for the service that provides the admission webhook","type":"integer"},"reinvocationPolicy":{"description":"Specify if the webhook may be called again after the initial webhook call. Possible values are Never and IfNeeded","enum":["IfNeeded","Never"],"type":"string"},"servicePath":{"description":"Path to the service that provides the admission webhook","type":"string"},"sideEffects":{"description":"Any side effects the admission webhook makes outside of AdmissionReview","enum":["None","NoneOnDryRun","Some","Unknown"],"type":"string"},"timeoutSeconds":{"description":"Timeout of the admission webhook","type":"integer"}},"type":"object"}},"type":"object"},"apiPort":{"description":"The port used by the Traffic Manager gRPC API","type":"integer"},"client":{"properties":{"connectionTTL":{"$ref":"#/$defs/duration","description":"Deprecated use grpc.connectionTTL","type":"string"},"dns":{"properties":{"excludeSuffixes":{"description":"Suffixes for which the client DNS resolver will always fail (or fallback in case of the overriding resolver) Defaults to [\".com\", \".io\", \".net\", \".org\", \".ru\"]","items":{"type":"string"},"type":"array"},"includeSuffixes":{"description":"Suffixes for which the client DNS resolver will always attempt to do a lookup. Includes have higher priority than excludes","items":{"type":"string"},"type":"array"}},"type":"object"},"routing":{"properties":{"allowConflictingSubnets":{"description":"Allow the specified subnets to be routed even if they conflict with other routes on the local machine","items":{"type":"string"},"type":"array"},"alsoProxySubnets":{"description":"The virtual network interface of connected clients will also proxy these subnets","items":{"type":"string"},"type":"array"},"neverProxySubnets":{"description":"The virtual network interface of connected clients never proxy these subnets","items":{"type":"string"},"type":"array"}},"type":"object"}},"type":"object"},"clientRbac":{"additionalProperties":false,"properties":{"create":{"description":"Create RBAC resources for non-admin users with this release","type":"boolean"},"namespaces":{"description":"The namespaces to give users access to. Defaults to Traffic Manager's namespaces (unless dynamic)","items":{"$ref":"#/$defs/rfc1123Label"},"type":"array"},"ruleExtras":{"description":"If set, run the clientRbac-ruleExtras template to add additional RBAC rules.","type":"boolean"},"subjects":{"description":"The user accounts to tie the created roles to","items":{"$ref":"#/$defs/subject"},"type":"array"}},"type":"object"},"global":{"additionalProperties":true,"type":"object"},"grpc":{"additionalProperties":false,"properties":{"connectionTTL":{"$ref":"#/$defs/duration","description":"The time that the traffic-manager or traffic-agent will retain a client connection without any sign of life from the workstation","type":"string"},"maxReceiveSize":{"$ref":"#/$defs/quantity","description":"maxReceiveSize is a quantity that configures the maximum message size that the traffic manager will service."}},"type":"object"},"hooks":{"additionalProperties":false,"properties":{"busybox":{"additionalProperties":false,"properties":{"image":{"description":"The name of the image to use for busybox","type":"string"},"imagePullSecrets":{"description":"The Secret storing any credentials needed to access the image in a private registry","items":{"$ref":"#/$defs/localObjectReference"},"type":"array"},"pullPolicy":{"$ref":"#/$defs/pullPolicy","description":"Pull policy in the webhook for the image"},"registry":{"description":"The registry to download the image from","type":"string"},"tag":{"description":"Override the version of busybox to be installed","type":"string"}},"type":"object"},"curl":{"additionalProperties":false,"properties":{"image":{"description":"The name of the image to use for curl","type":"string"},"imagePullSecrets":{"description":"The Secret storing any credentials needed to access the image in a private registry","items":{"$ref":"#/$defs/localObjectReference"},"type":"array"},"pullPolicy":{"$ref":"#/$defs/pullPolicy","description":"Pull policy in the webhook for the image"},"registry":{"description":"The registry to download the image from","type":"string"},"tag":{"description":"Override the version of curl to be installed","type":"string"}},"type":"object"},"podSecurityContext":{"$ref":"#/$defs/podSecurityContext","description":"The Kubernetes SecurityContext for the chart hooks Pod"},"resources":{"$ref":"#/$defs/resourceRequirements","description":"Define resource requests and limits for the chart hooks"},"securityContext":{"$ref":"#/$defs/securityContext","description":"The Kubernetes SecurityContext for the chart hooks Container"}},"type":"object"},"hostNetwork":{"description":"Sets the spec.template.spec.hostNetwork for the Traffic Manager. Set this to true when using Calico on AWS EKS to ensure that the mutating webhook can communicate with the traffic manager","type":"boolean"},"image":{"additionalProperties":false,"properties":{"imagePullSecrets":{"description":"The Secret storing any credentials needed to access the image in a private registry","items":{"$ref":"#/$defs/localObjectReference"},"type":"array"},"name":{"description":"The name of the image to use for the traffic-manager","type":"string"},"pullPolicy":{"$ref":"#/$defs/pullPolicy","description":"Pull policy in the webhook for the traffic-manager image"},"registry":{"description":"The registry to download the image from","type":"string"},"tag":{"description":"Overrides the image tag whose default is the chart appVersion","type":"string"}},"type":"object"},"intercept":{"additionalProperties":false,"properties":{"allowGlobalIntercepts":{"description":"Allow global TCP/UDP intercepts. When set to false, only HTTP intercepts with header or path filters are allowed. This prevents users from creating global intercepts that block other users from intercepting the same port.","type":"boolean"},"environment":{"properties":{"excluded":{"description":"Environment variables to exclude from the list sent to the client during engagement","items":{"type":"string"},"type":"array"}},"type":"object"},"inactiveBlockTimeout":{"$ref":"#/$defs/duration","description":"The maximum amount of time an intercept may be held by a client that is unreachable or inactive. Once this timeout is exceeded, the intercept no longer blocks conflicting intercepts and may be automatically removed when another client attempts to create a conflicting intercept."}},"type":"object"},"isCI":{"description":"isCI can be set to force the traffic-manager namespace to be \"ambassador\"","type":"boolean"},"livenessProbe":{"$ref":"#/$defs/probe","description":"Define livenessProbe for the traffic-manager"},"logLevel":{"$ref":"#/$defs/logLevel","description":"Define the logging level of the Traffic Manager"},"managerRbac":{"additionalProperties":false,"properties":{"create":{"description":"Create RBAC resources for traffic-manager with this release","type":"boolean"},"namespaces":{"deprecated":true,"description":"Declares a fixed set of managed namespaces","items":{"$ref":"#/$defs/rfc1123Label"},"type":"array"}},"type":"object"},"maxNamespaceSpecificWatchers":{"description":"Threshold controlling when the traffic-manager switches from using watchers for each managed namespace to using cluster-wide watchers","maximum":50,"minimum":0,"type":"integer"},"nameOverride":{"description":"Override the default name prefix used when creating RBAC resources","type":"string"},"namespaceSelector":{"$ref":"#/$defs/labelSelector","description":"Declares the managed namespace using matchLabels and matchExpressions. Mutually exclusive to namespaces"},"namespaces":{"description":"Declares a fixed set of managed namespaces. Mutually exclusive to namespaceSelector","items":{"$ref":"#/$defs/rfc1123Label"},"type":"array"},"nodeSelector":{"$ref":"#/$defs/nodeSelector","description":"Define which Nodes you want to the Traffic Manager to be deployed to."},"podAnnotations":{"additionalProperties":{"type":"string"},"description":"Annotations for the Traffic Manager Pod","type":"object"},"podCIDRStrategy":{"description":"Define the strategy that the traffic-manager uses to discover what CIDRs the cluster uses for pods","enum":["auto","coverPodIPs","environment","nodePodCIDRs"],"type":"string"},"podCIDRs":{"description":"podCIDRs is the verbatim list of CIDRs used when the podCIDRStrategy is set to environment","items":{"type":"string"},"type":"array"},"podLabels":{"additionalProperties":{"type":"string"},"description":"Labels for the Traffic Manager Pod","type":"object"},"podSecurityContext":{"$ref":"#/$defs/podSecurityContext","description":"The Kubernetes SecurityContext for the traffic-manager Pod"},"priorityClassName":{"description":"Name of the existing pod priority class to be used","type":"string"},"prometheus":{"additionalProperties":false,"properties":{"dropClientLabel":{"description":"optionally drop the client label from prometheus metrics for GDPR personal data compliance","type":"boolean"},"port":{"description":"Set this port number to enable a prometheus metrics http server for the traffic manager","type":"integer"}},"type":"object"},"rbac":{"additionalProperties":false,"properties":{"only":{"description":"Only create the RBAC resources and omit the traffic-manger","type":"boolean"}},"type":"object"},"readinessProbe":{"$ref":"#/$defs/probe","description":"Define readinessProbe for the Traffic Manger."},"replicaCount":{"const":1,"description":"Number or replicas for the traffic-manager. The Traffic Manager only support running with one replica at the moment."},"resources":{"$ref":"#/$defs/resourceRequirements","description":"Define resource requests and limits for the Traffic Manger"},"routeController":{"additionalProperties":false,"description":"Configuration for the route-controller DaemonSet that installs blackhole routes for deleted service ClusterIPs to prevent routing loops on local clusters.","properties":{"enabled":{"anyOf":[{"type":"null"},{"type":"boolean"}],"description":"Enable or disable the route-controller DaemonSet. When null (the default), the route-controller is automatically enabled for local clusters, detected by image.registry being \"local\" or starting with \"localhost:\". Set to true to force-enable or false to force-disable regardless of registry."},"image":{"additionalProperties":false,"properties":{"name":{"description":"The name of the route-controller image","type":"string"},"pullPolicy":{"anyOf":[{"const":"","type":"string"},{"$ref":"#/$defs/pullPolicy"}],"description":"Pull policy for the route-controller image. Empty string inherits from image.pullPolicy."},"registry":{"description":"The registry for the route-controller image","type":"string"}},"type":"object"},"logLevel":{"anyOf":[{"const":"","type":"string"},{"$ref":"#/$defs/logLevel"}],"description":"Log level for the route-controller. Empty string inherits from logLevel."},"serviceCIDRs":{"description":"Service CIDRs for which subnet-level blackhole routes are installed at startup. If empty, the controller queries the ServiceCIDR API (k8s \u003e= 1.33) automatically. For older clusters, set this explicitly (e.g. [\"10.96.0.0/12\"]).","items":{"type":"string"},"type":"array"}},"type":"object"},"schedulerName":{"description":"Specify a scheduler for Traffic Manager Pod and hooks Pod","type":"string"},"securityContext":{"$ref":"#/$defs/securityContext","description":"The Kubernetes SecurityContext for the traffic-manager container"},"service":{"additionalProperties":false,"properties":{"type":{"description":"The type of service for the traffic-manager","type":"string"}},"type":"object"},"startupProbe":{"$ref":"#/$defs/probe","description":"Define startupProbe for the Traffic Manger."},"telepresenceAPI":{"additionalProperties":false,"properties":{"port":{"description":"The port on agent's localhost where the Telepresence API server can be found","type":"integer"}},"type":"object"},"timeouts":{"additionalProperties":false,"properties":{"agentArrival":{"$ref":"#/$defs/duration","description":"The time that the traffic-manager will wait for the traffic-agent to arrive"}},"type":"object"},"tolerations":{"description":"Define tolerations for the Traffic Manager to ignore Node taints","items":{"$ref":"#/$defs/toleration"},"type":"array"},"workloads":{"additionalProperties":false,"properties":{"argoRollouts":{"additionalProperties":false,"properties":{"enabled":{"description":"Enable/Disable the argo-rollouts integration","type":"boolean"}},"type":"object"},"deployments":{"additionalProperties":false,"properties":{"enabled":{"description":"Enable/Disable the support for Deployments","type":"boolean"}},"type":"object"},"replicaSets":{"additionalProperties":false,"properties":{"enabled":{"description":"Enable/Disable the support for ReplicaSets","type":"boolean"}},"type":"object"},"statefulSets":{"additionalProperties":false,"properties":{"enabled":{"description":"Enable/Disable the support for StatefulSets","type":"boolean"}},"type":"object"}},"type":"object"}},"title":"Telepresence Values","type":"object"} ================================================ FILE: docs/howtos/cluster-in-vm.md ================================================ --- title: Host a cluster in Docker or a VM description: Use Telepresence to engage with services in a cluster running in a hosted docker container or virtual machine. hide_table_of_contents: true --- # Network considerations for locally hosted clusters ## The problem Telepresence creates a Virtual Network Interface ([VIF](../reference/tun-device.md)) that maps the cluster subnets to the host machine when it connects. If you're running Kubernetes locally (e.g., Docker Desktop, Kind, Minikube, k3s), you may encounter network problems because the devices in the host are also accessible from the cluster's nodes. ### Example: A k3s cluster runs in a headless VirtualBox machine that uses a "host-only" network. This network will allow both host-to-guest and guest-to-host connections. In other words, the cluster will have access to the host's network and, while Telepresence is connected, also to its VIF. This means that from the cluster's perspective, there will now be more than one interface that maps the cluster's subnets; the ones already present in the cluster's nodes, and then the Telepresence VIF, mapping them again. Now, if a request arrives to Telepresence covered by a subnet mapped by the VIF, the request is routed to the cluster. If the cluster for some reason doesn't find a corresponding listener that can handle the request, it will eventually try the host network, and find the VIF. The VIF routes the request to the cluster and now the recursion is in motion. The final outcome of the request will likely be a timeout but since the recursion is very resource intensive (a large amount of very rapid connection requests), this will likely also affect other connections in a bad way. ## Solution ### Prevent recursion in the VIF To prevent recursive connections within the VIF, set the client configuration property `routing.recursionBlockDuration` to a short timeout value. A value of `1ms` is typically sufficient. This configuration will temporarily block new connections to a specific IP:PORT pair immediately after a connection has been established, thereby preventing looped connections back into the VIF. The block remains in effect for the specified duration. ### Create a bridge network An alternative to using the `routing.recursionBlockDuration` can be to create a bridge network. It acts as a Link Layer (L2) device that forwards traffic between network segments. By creating a bridge network, you can bypass the host's network stack, and instead make the Kubernetes cluster to connect directly to the same router as your host. To create a bridge network, you need to change the network settings of the guest running a cluster's node so that it connects directly to a physical network device on your host. The details on how to configure the bridge depend on what type of virtualization solution you're using. #### Vagrant + Virtualbox + k3s example Here's a sample `Vagrantfile` that will spin up a server node and two agent nodes in three headless instances using a bridged network. It also adds the configuration needed for the cluster to host a docker repository (very handy in case you want to save bandwidth). The Kubernetes registry manifest must be applied using `kubectl -f registry.yaml` once the cluster is up and running. ##### Vagrantfile ```ruby # -*- mode: ruby -*- # vi: set ft=ruby : # bridge is the name of the host's default network device $bridge = 'wlp5s0' # default_route should be the IP of the host's default route. $default_route = '192.168.1.1' # nameserver must be the IP of an external DNS, such as 8.8.8.8 $nameserver = '8.8.8.8' # server_name should also be added to the host's /etc/hosts file and point to the server_ip # for easy access when pushing docker images server_name = 'multi' # static IPs for the server and agents. Those IPs must be on the default router's subnet server_ip = '192.168.1.110' agents = { 'agent1' => '192.168.1.111', 'agent2' => '192.168.1.112', } # Extra parameters in INSTALL_K3S_EXEC variable because of # K3s picking up the wrong interface when starting server and agent # https://github.com/alexellis/k3sup/issues/306 server_script = <<-SHELL sudo -i apk add curl export INSTALL_K3S_EXEC="--bind-address=#{server_ip} --node-external-ip=#{server_ip} --flannel-iface=eth1" mkdir -p /etc/rancher/k3s cat <<-'EOF' > /etc/rancher/k3s/registries.yaml mirrors: "multi:5000": endpoint: - "http://#{server_ip}:5000" EOF curl -sfL https://get.k3s.io | sh - echo "Sleeping for 5 seconds to wait for k3s to start" sleep 5 cp /var/lib/rancher/k3s/server/token /vagrant_shared cp /etc/rancher/k3s/k3s.yaml /vagrant_shared cp /etc/rancher/k3s/registries.yaml /vagrant_shared SHELL agent_script = <<-SHELL sudo -i apk add curl export K3S_TOKEN_FILE=/vagrant_shared/token export K3S_URL=https://#{server_ip}:6443 export INSTALL_K3S_EXEC="--flannel-iface=eth1" mkdir -p /etc/rancher/k3s cat <<-'EOF' > /etc/rancher/k3s/registries.yaml mirrors: "multi:5000": endpoint: - "http://#{server_ip}:5000" EOF curl -sfL https://get.k3s.io | sh - SHELL def config_vm(name, ip, script, vm) # The network_script has two objectives: # 1. Ensure that the guest's default route is the bridged network (bypass the network of the host) # 2. Ensure that the DNS points to an external DNS service, as opposed to the DNS of the host that # the NAT network provides. network_script = <<-SHELL sudo -i ip route delete default 2>&1 >/dev/null || true; ip route add default via #{$default_route} cp /etc/resolv.conf /etc/resolv.conf.orig sed 's/^nameserver.*/nameserver #{$nameserver}/' /etc/resolv.conf.orig > /etc/resolv.conf SHELL vm.hostname = name vm.network 'public_network', bridge: $bridge, ip: ip vm.synced_folder './shared', '/vagrant_shared' vm.provider 'virtualbox' do |vb| vb.memory = '4096' vb.cpus = '2' end vm.provision 'shell', inline: script vm.provision 'shell', inline: network_script, run: 'always' end Vagrant.configure('2') do |config| config.vm.box = 'generic/alpine314' config.vm.define 'server', primary: true do |server| config_vm(server_name, server_ip, server_script, server.vm) end agents.each do |agent_name, agent_ip| config.vm.define agent_name do |agent| config_vm(agent_name, agent_ip, agent_script, agent.vm) end end end ``` The Kubernetes manifest to add the registry: ##### registry.yaml ```yaml apiVersion: v1 kind: ReplicationController metadata: name: kube-registry-v0 namespace: kube-system labels: k8s-app: kube-registry version: v0 spec: replicas: 1 selector: app: kube-registry version: v0 template: metadata: labels: app: kube-registry version: v0 spec: containers: - name: registry image: registry:2 resources: limits: cpu: 100m memory: 200Mi env: - name: REGISTRY_HTTP_ADDR value: :5000 - name: REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY value: /var/lib/registry volumeMounts: - name: image-store mountPath: /var/lib/registry ports: - containerPort: 5000 name: registry protocol: TCP volumes: - name: image-store hostPath: path: /var/lib/registry-storage --- apiVersion: v1 kind: Service metadata: name: kube-registry namespace: kube-system labels: app: kube-registry kubernetes.io/name: "KubeRegistry" spec: selector: app: kube-registry ports: - name: registry port: 5000 targetPort: 5000 protocol: TCP type: LoadBalancer ``` ================================================ FILE: docs/howtos/docker-compose.md ================================================ --- title: "Using Telepresence with Docker Compose" hide_table_of_contents: true --- # Telepresence Docker Compose Extensions A Docker Compose file can contain extensions that Docker Compose ignores. The `telepresence compose` command functions similarly to `docker compose`, but will process any `x-tele` extensions present in the Docker Compose file or its overrides before passing the final Compose specification to Docker Compose. The `x-tele` extensions are particularly useful when you have a set of services defined in a Docker Compose file that mirrors services running in a cluster, and you want your local services to interact with remote services or vice versa. The `x-tele` extensions enable your Compose services to either act as handlers when telepresence engages a remote service, or to temporarily act as proxies for remotely running services. The extensions can be added directly to the `compose.yaml` file, or to a `compose.override.yaml` (merged automatically by Docker Compose). ## Supported `x-tele` Extensions ### Top-level Extension The `x-tele` [top-level extension](../reference/compose#top-level-extension) is used to define a connection to the cluster, and to override the default mount behavior when engaging with remote services. ### Service Extensions The `x-tele` [service extensions](../reference/compose#service-extensions) are used to define the behavior of a service when engaged with a remote service. Telepresence supports the following types: | Type | Local service Behavior | Similar to | |---------------------------------------------|-----------------------------------------------------------------|--------------------------| | [connect](../reference/compose#connect) | Service has access to the cluster's resources (DNS and routing) | `telepresence connect` | | [proxy](../reference/compose#proxy) | Replaced with a proxy for a service in the cluster | N/A | | [wiretap](../reference/compose#wiretap) | Receives wiretapped data from a service in the cluster | `telepresence wiretap` | | [ingest](../reference/compose#ingest) | Acts as the handler for an ingested container the cluster | `telepresence ingest` | | [intercept](../reference/compose#intercept) | Acts as the handler for an intercepted service the cluster | `telepresence intercept` | | [replace](../reference/compose#replace) | Replaces a remote container in the cluster | `telepresence replace` | All types imply a `connect`, and thus rely on the top-level `x-tele` extension that defines the connection to the cluster. ## Walkthrough and Samples This documentation will give some examples on how to use the `x-tele` extension using the sample Emoji application, originally developed by Buoyant.io, from the https://github.com/telepresenceio/emojivoto repository. This app is easy to deploy locally using `docker compose up` or remotely to a cluster using `kubectl apply --kustomize`. ### Initial Steps The examples assume that you have [installed](../install/manager.md) the Telepresence Traffic Manager in your cluster. We start by ensuring that the Emojivoto application can be deployed, both locally using `docker compose up` and remotely in your cluster. #### 1. Download the Emojivoto App Clone the emojivoto git repository with the following command: ```console $ git clone https://github.com/telepresenceio/emojivoto.git ``` #### 2. Use the app locally ```console $ cd emojivoto $ docker compose up ``` Now point your browser to http://localhost:8080/. The "Emoji Vote" page shows up. Try it out. Tear down the local app ```console $ docker compose down ``` #### 3. Use the app remotely We Create the cluster resources by applying the `kustomize/deployment` directory using the following command: ```console $ kubectl apply -k kustomize/deployment namespace/emojivoto created serviceaccount/emoji created serviceaccount/voting created serviceaccount/web created service/emoji created service/voting created service/web created deployment.apps/emoji created deployment.apps/vote-bot created deployment.apps/voting created deployment.apps/web created ``` Check that the pods are up and running with: ```console $ kubectl -n emojivoto get pod NAME READY STATUS RESTARTS AGE emoji-7d8d6fb869-wp5kc 1/1 Running 0 23s vote-bot-766b9f68b-9bsqk 1/1 Running 0 23s voting-7d49b58d7b-n7bc8 1/1 Running 0 23s web-7cc498695b-7dtcl 1/1 Running 0 23s ``` Connect to the cluster and verify that the web service is functional. The `telepresence serve web` will start the browser with a URL that points to the "web" service: ```console $ telepresence connect -n emojivoto --docker ✔ Connected to context minikube, namespace emojivoto (https://192.168.49.2:8443) 0.8s $ telepresence serve web ``` ## Extend With "proxy" Let's assume that we don't want to run the "voting" service locally. Instead, we want to replace it with a corresponding service that runs in the cluster. In other words, we want the local service to act as a _proxy_ for the remote service. The original `compose.yaml` file contains this: ```yaml services: web: image: ghcr.io/telepresenceio/emojivoto-web:0.3.0 environment: - WEB_PORT=8080 - EMOJISVC_HOST=emoji:8080 - VOTINGSVC_HOST=voting:8080 - INDEX_BUNDLE=dist/index_bundle.js ports: - "8080:8080" depends_on: - voting - emoji vote-bot: image: ghcr.io/telepresenceio/emojivoto-web:0.3.0 entrypoint: emojivoto-vote-bot environment: - WEB_HOST=web:8080 depends_on: - web emoji: image: ghcr.io/telepresenceio/emojivoto-emoji:0.3.0 environment: - GRPC_PORT=8080 ports: - "8081:8080" voting: image: ghcr.io/telepresenceio/emojivoto-voting:0.3.0 environment: - GRPC_PORT=8080 - POLL_FILE=/data/polls.json ports: - "8082:8080" volumes: - data:/data volumes: data: ``` The proxy requires a Telepresence connection to the cluster, using the `emojivoto` namespace in this example. Connections are defined in a top-level `x-tele` extension, where each connection is identified by a name and configured with attributes that correspond to the flags used in the `telepresence connect` command. The top-level extension is structured as follows: ```yaml x-tele: connections: - name: emojivoto namespace: emojivoto ``` To enable proxy functionality, an extension of type `proxy` is added to the `voting` service declaration: ```yaml x-tele: type: proxy connection: emojivoto ``` The `connection: emojivoto` field specifies that the proxy uses the `emojivoto` connection defined in the top-level extension, linking it to the `emojivoto` namespace. This field is optional when the top-level extension includes only one connection. Similarly, the `name: emojivoto` in the connection declaration is optional and only required when multiple connections are defined. The resulting file will then look like this: ```yaml x-tele: connections: - name: emojivoto namespace: emojivoto services: web: image: ghcr.io/telepresenceio/emojivoto-web:0.3.0 environment: - WEB_PORT=8080 - EMOJISVC_HOST=emoji:8080 - VOTINGSVC_HOST=voting:8080 - INDEX_BUNDLE=dist/index_bundle.js ports: - "8080:8080" depends_on: - voting - emoji vote-bot: image: ghcr.io/telepresenceio/emojivoto-web:0.3.0 entrypoint: emojivoto-vote-bot environment: - WEB_HOST=web:8080 depends_on: - web emoji: image: ghcr.io/telepresenceio/emojivoto-emoji:0.3.0 environment: - GRPC_PORT=8080 ports: - "8081:8080" voting: x-tele: type: proxy connection: emojivoto image: ghcr.io/telepresenceio/emojivoto-voting:0.3.0 environment: - GRPC_PORT=8080 - POLL_FILE=/data/polls.json ports: - "8082:8080" volumes: - data:/data volumes: data: ``` > [!TIP] > Instead of modifying the original `compose.yaml` file, we can add a new file adjacent to it and call it `compose.override.yaml`. Docker Compose will automatically merge this override with the `compose.yaml`. So, leave original `compose.yaml` intact, and instead add a `compose.override.yaml` file with the following contents (the optional connection name and proxy connection reference are both removed): > > ```yaml > x-tele: > connections: > - namespace: emojivoto > services: > voting: > x-tele: > type: proxy > ``` ### Running the Extended Sample Running with `telepresence compose up` will discover the extension, connect to the cluster, modify an in-memory version of the Compose specification so that it no longer contains the "voting" service, alter the DNS so that lookups for this service instead find the one in the cluster, and configure routing so that the "web" service still finds the "voting" service. We can verify this using: ```console $ telepresence compose ✔ Connected to context minikube, namespace emojivoto (https://192.168.49.2:8443) 2.4s ✔ Proxied service voting 0.0s [+] Running 4/4 ✔ Network emojivoto_default Created 0.1s ✔ Container emojivoto-emoji-1 Created 0.0s ✔ Container emojivoto-web-1 Created 0.0s ✔ Container emojivoto-vote-bot-1 Created 0.0s Attaching to emoji-1, vote-bot-1, web-1 emoji-1 | 2025/07/19 05:42:39 Starting grpc server on GRPC_PORT=[8080] web-1 | 2025/07/19 05:42:39 Connecting to [voting:8080] web-1 | 2025/07/19 05:42:39 Connecting to [emoji:8080] web-1 | 2025/07/19 05:42:39 Starting web server on WEB_PORT=[8080] and MESSAGE_OF_THE_DAY=[] vote-bot-1 | ✔ Voting for :older_man: vote-bot-1 | ✔ Voting for :100: vote-bot-1 | ✔ Voting for :bulb: ... ``` We now see "Proxied service voting" and then, in contrast to the output from a `docker compose up`, no further output from that service. The `vote-bot-1` continues to vote though, to it's obviously still talking to a `voting`. ### Takeaways Using our Telepresence "proxy" extension, we have now successfully modified our setup so that the services in the compose.yaml file interact with a service in the cluster. ### Proxy the web Can we proxy the web service and still reach it using `localhost:8080` in our browser? Let's give it a try using the following `compose.override.yaml` file: ```yaml x-tele: connections: - namespace: emojivoto services: web: x-tele: type: proxy ports: - 8080:80 ``` Worth noting here is that the original docker-compose service will expose port 8080, so that's what our proxy must expose to other containers. In the cluster, however, the web service uses port 80. This is why we need to specify the port mapping in the extension: ```yaml ports: - 8080:80 ``` With this change, we can now run the sample using `telepresence compose up` and connect to the web service using `localhost:8080` in our browser. ## Extend With "replace" Our previous example used a proxy to replace a local service with a remote service. In this sample we will do the opposite. We will make the remote services talk to services in our Docker Compose file. In essence, we will let the remote `web` and `vote-bot` service use the `emoji` and `vote` service that we run locally. Our extensions look like this: ```yaml x-tele: connections: - namespace: emojivoto services: emoji: x-tele: type: replace voting: x-tele: type: replace vote-bot: profiles: - notEnabled ``` > [!NOTE] > The last part: > ```yaml > vote-bot: > profiles: > - notEnabled > ``` > effectively disables the local `vote-bot` service so that only the vote-bot running in the cluster is active. It's optional, but it makes it easier to see what happens when we run the sample: ```console $ telepresence compose up ✔ Connected to context minikube, namespace emojivoto (https://192.168.49.2:8443) 2.8s [+] Engaging 2/2 ✔ emoji Replaced service emoji 2.0s ✔ voting Replaced service voting 1.6s [+] Running 4/4 ✔ Network emojivoto_default Created 0.0s ✔ Container emojivoto-voting1 Created 0.1s ✔ Container emojivoto-emoji-1 Created 0.1s ✔ Container emojivoto-web-1 Created 0.1s Attaching to emoji-1, voting-1, web-1 emoji-1 | 2025/08/05 09:17:39 Starting prom metrics on PROM_PORT=[8801] emoji-1 | 2025/08/05 09:17:39 Starting grpc server on GRPC_PORT=[8080] voting-1 | 2025/08/05 09:17:39 Storing votes in file /data/polls.json voting-1 | 2025/08/05 09:17:39 Starting prom metrics on PROM_PORT=[8801] voting-1 | 2025/08/05 09:17:39 Starting grpc server on GRPC_PORT=[8080] voting-1 | 2025/08/05 09:17:39 Using failureRate [0.000000] and artificialDelayDuration [0s] web-1 | 2025/08/05 09:17:39 Connecting to [voting:8080] web-1 | 2025/08/05 09:17:39 Connecting to [emoji:8080] web-1 | 2025/08/05 09:17:39 Starting web server on WEB_PORT=[8080] and MESSAGE_OF_THE_DAY=[] voting-1 | 2025/07/20 04:39:58 Voted for [:fax:], which now has a total of [12] votes voting-1 | 2025/07/20 04:39:59 Voted for [:doughnut:], which now has a total of [231] votes voting-1 | 2025/07/20 04:40:00 Voted for [:flight_departure:], which now has a total of [8] votes ``` We can observe that after an initial delay - caused by the remote vote bot reconnecting after the replacement of the vote container - the votes arrive, even though no vote-bot is running locally. Furthermore, if we start a browser on http://localhost:8080 now, we see the same leaderboard as a browser started using `telepresence serve web` which serves up the remote service. In the cluster, the pods for the "voting" and "emoji" deployments have been replaced with traffic-agents that redirect all traffic to their corresponding "voting" and "emoji" Docker Compose service. We can easily verify this using: ```console $ kubectl -n emojivoto get pod -l app=voting -o jsonpath='{.items.*.spec.containers.*.name}' traffic-agent $ kubectl -n emojivoto get pod -l app=emoji -o jsonpath='{.items.*.spec.containers.*.name}' traffic-agent ``` ### Remote Mounts One interesting observation is that the vote counts don't start from zero. Instead, they are synced with the vote counts used by the voting service in the cluster. This is because the "replace" extension automatically replaced the mounted "data" volume with a remote mount of the corresponding volume in the replaced container. This default behavior can be controlled using mount policies. #### Preventing Remote Mounts To prevent the remote mounts from happening, and instead keep the volumes created by Docker Compose, we can add a `mounts` object to the top-level `x-tele` extension in the `compose.override.yaml` file: ```yaml x-tele: connections: - namespace: emojivoto mounts: - volume: data policy: local ``` The `mounts` object is a list of objects, each with a `volume` field that corresponds to the name of the volume in the Compose file or a `volumePattern`, a regular expression that matches that name, and a `policy` field that can be set to either `local`, `ignore`, `remote` or `remoteReadOnly`. The default is to use whatever policy that the traffic-agent uses for the volume. ### Takeaways Using our Telepresence "replace" extension, we have successfully modified our setup so that multiple services in the cluster have been replaced by services that run locally as part of our Docker Compose spec. ================================================ FILE: docs/howtos/docker.md ================================================ --- title: "Using Telepresence with Docker" hide_table_of_contents: true --- # Telepresence with Docker ## Why? It can be tedious to adopt Telepresence across your organization, since the [package installer](../install/client.md) requires organizational approval, and Telepresence needs to get along with any exotic networking setup that your company may have. If Docker is already approved in your organization, this approach should be considered. ## How? When using Telepresence in Docker mode, users don't need organizational approval of a package installer, can address several networking challenges, and forego the need for third-party applications to enable volume mounts. You can simply add the docker flag to any Telepresence command, and it will start your daemon in a container, making it easier to adopt as an organization. Let's illustrate with a quick demo, assuming a default Kubernetes context named default, and a simple HTTP service: ```console $ telepresence connect --docker Connected to context default, namespace default (https://kubernetes.docker.internal:6443) ``` This method limits the scope of the potential networking issues since everything stays inside Docker. The Telepresence daemon can be found under the name `tp--cn` when listing your containers. ```console $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 540a3c12f45b ghcr.io/telepresenceio/telepresence:2.22.0 "telepresence connec…" 18 seconds ago Up 16 seconds 127.0.0.1:58802->58802/tcp tp-default-cn ``` Replace a container in the cluster and start a corresponding local container: ```cli $ telepresence replace echo-sc --docker-run -- ghcr.io/telepresenceio/echo-server:latest Using Deployment echo-sc Container name: echo-sc State : ACTIVE Workload kind : Deployment Port forwards : 127.0.0.1 -> 127.0.0.1 8080 -> 8080 TCP Echo server listening on port 8080. ``` Using `--docker-run` starts the local container that acts as the handler, so that it uses the same network as the container that runs the telepresence daemon. It will also receive the same incoming traffic and have the remote volumes mounted in the same way as the remote container that it replaces. If you want to curl your remote service, you'll need to do that from a container that shares the daemon container's network. Telepresence provides a `curl` command that will do just that. ```console $ telepresence curl echo-sc % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 196 100 196 0 0 4232 0 --:--:-- --:--:-- --:--:-- 4260 Request served by 540a3c12f45b Intercept id b0bd5e75-2618-4bef-ac4e-4c08c4b58ec7:echo-sc/echo-sc Intercepted container "echo-sc" HTTP/1.1 GET / Host: echo-sc User-Agent: curl/8.11.1 Accept: */* ``` ### Starting the local container prior to the intercept If you want to start your container manually using `docker run`, you must ensure that it shares the daemon container's network. A convenient way to do that is to use the `--docker-run` flag as explained above, but you can also start a container separately using `telepresence docker-run`. This can be done before or after the intercept and the run will survive cycling the intercept on or off. ```console $ telepresence docker-run ghcr.io/telepresenceio/echo-server:latest Echo server listening on port 8080. ``` Check what name the started container has: ```console $ docker ps --last 1 --format {{.Names}} fervent_goodall ``` You can now redirect intercepted traffic to your "echo" container using the address flag, e.g.: ```console $ telepresence intercept --port 8080:80 --address echo fervent_goodall ``` > [!TIP] > Name your container using the `--name` flag, e.g. `telepresence docker-run --name echo ghcr.io/telepresenceio/echo-server:latest`. > This will make it easier to refer to it later. > [!IMPORTANT] > Never name your container the same as a service in the cluster. If you do, you'll get a warning that the name overrides > the service IP, and the service will not be reachable. ### Use named connections You can use the `--name` flag to name the connection if you want to connect to several namespaces simultaneously, e.g. ```console $ telepresence connect --docker --name alpha --namespace alpha $ telepresence connect --docker --name beta --namespace beta ``` Now, with two connections active, you must pass the flag `--use ` to other commands, e.g. ```console $ telepresence replace echo-easy --use alpha --docker-run -- ghcr.io/telepresenceio/echo-server:latest ``` ## Key learnings * Using the Docker mode of telepresence **does not require organizational approval of a package installer**, and makes it **easier** to adopt across your organization. * It **limits the potential networking issues** you can encounter. * It **limits the potential mount issues** you can encounter. * It **enables simultaneous engagements in multiple namespaces**. ================================================ FILE: docs/howtos/engage.md ================================================ --- title: Code and debug an application locally description: Start using Telepresence in your own environment. Follow these steps to work locally with cluster applications. hide_table_of_contents: true --- # Code and debug an application locally ## Local Development Methods Telepresence offers three powerful ways to develop your services locally: ### Replace * **How it Works:** - Replaces an existing container within your Kubernetes cluster with a Traffic Agent. - Reroutes traffic intended for the replaced container to your local workstation. - Makes the remote environment of the replaced container available to the local workstation. - Provides read-write access to the volumes mounted by replaced container. * **Impact:** - A Traffic Agent is injected into the pods of the targeted workload. - The replaced container is removed from the pods of the targeted workload. - 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 configured without incoming traffic. ### Intercept * **How it Works:** - Intercepts requests destined for a specific service port (or ports) and reroutes them to the local workstation. - Makes the remote environment of the targeted container available to the local workstation. - Provides read-write access to the volumes mounted by the targeted container. - Makes it possible to filter traffic using HTTP headers and paths. * **Impact:** - A Traffic Agent is injected into the pods of the targeted workload. - Intercepted traffic is rerouted to the local workstation and will no longer reach the remote service. - Only traffic that matches the intercept filters will be rerouted. - All containers keep on running. * **Use-cases:** - Your main focus is the service API rather than the cluster's pods and containers. - You want your local service to only receive specific ingress traffic, while other traffic must be untouched. - You want your remote container to continue processing other requests or background tasks. ### Wiretap * **How it Works:** - Adds a wiretap on a specific service port (or ports) and sends the data to the local workstation. - Makes the remote environment of the targeted container available to the local workstation. - Provides read-only access to the volumes mounted by the targeted container. - Makes it possible to filter traffic using HTTP headers and paths. * **Impact:** - A Traffic Agent is injected into the pods of the targeted workload. - All containers keep on running. - All traffic will still reach the remote service. - Wiretapped traffic is rerouted to the local workstation. * **Use-cases:** - You need a solution where several developers can engage with the same service simultaneously. - Your main focus is the service API rather than the cluster's pods and containers. - You want your local service to only receive specific ingress traffic. - You don't care about the responses sent by your local service. - You don't want breakpoints in your local service to affect the remote service. - You want to keep the impact that your local development has on the cluster to a minimum. ### Ingest * **How it Works:** - Makes the remote environment of the ingested container available to the local workstation. - Provides read-only access to the volumes mounted by replaced container. * **Impact:** - A Traffic Agent is injected into the pods of the targeted workload. - No traffic is rerouted and all containers keep on running. * **Use-cases:** - You want to keep the impact that your local development has on the cluster to a minimum. - You have don't need traffic being routed from the cluster, and read-only access to the container's volumes is ok. ## Prerequisites Before you begin, you need to have [Telepresence installed](../install/client.md). This document uses the Kubernetes command-line tool, [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl/) in several examples. OpenShift users can substitute oc [commands instead](https://docs.openshift.com/container-platform/4.1/cli_reference/developer-cli-commands.html). 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 Your Container This approach offers the benefit of direct cluster connectivity from your workstation, simplifying debugging and modification of your application within its familiar environment. Note that if Telepresence was installed using a standalone binary rather than a [package installer](../install/client.md), it will require root access to configure the network interface. 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 Your 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://) $ 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 The `telepresence intercept` command allows you to redirect traffic for a specific service to your local workstation. Compared to the replace command, intercept is less invasive because it: a) enables precise filtering of intercepted traffic using HTTP headers or paths, and b) allows the original service to continue running, handling all other traffic and tasks not directly related to the intercepted traffic. 1. Connect to your cluster with `telepresence connect`. 2. Intercept all traffic going to the application's http port in your cluster and redirect to port 8080 on your workstation. ```console $ telepresence intercept example-app --http-header 'x-user=margret' --http-path-prefix '/api' --port 8080:http --env-file ~/example-app-intercept.env --mount /tmp/example-app-mounts Using Deployment example-app intercepted Intercept name: example-app State : ACTIVE Workload kind : Deployment Destination : 127.0.0.1:8080 Intercepting : HTTP requests with path-prefix /api and header 'X-User: margret' ``` * For `--http-header`: specify the HTTP header you want to filter on. You can specify multiple headers by repeating the flag. Header-based intercepts take priority over path-only intercepts, so that when multiple intercepts are active on the same workload, requests are evaluated against header-based filters first, then path-only filters. This allows different developers to use header-based personal intercepts (e.g., `x-user=alice`) while others use path-based intercepts (e.g., `--http-path-prefix /admin/`) without conflicts. * For '--http-path-prefix': specify the path prefix you want to filter on. You can specify multiple path prefixes by repeating the flag. Path-based intercepts have lower priority than header-based intercepts. * For `--port`: specify the port the local instance of your application is running on, and optionally the remote port that you want to intercept. Telepresence will select the remote port automatically when there's only one service port available to access the workload. You must specify the port to intercept when the workload exposes multiple ports. You can do this by specifying the port you want to intercept after a colon in the `--port` argument (like in the example), and/or by specifying the service you want to intercept using the `--service` flag. * For `--env-file`: specify a file path for Telepresence to write the environment variables that are set for the targeted container. 3. Start your local application using the environment variables retrieved and the volumes that were mounted in the previous step. You can now: - Make changes on the fly and see them reflected when interacting with your Kubernetes environment without affecting other users of the same service. - Query services that are only exposed in your cluster's network. - Set breakpoints in your IDE to investigate bugs. ## Wiretap Your Application You can use the `telepresence wiretap` command when you want to wiretap the traffic for a specific service and send a copy of it to your workstation. The `wiretap` is less intrusive than the `intercept`, because it does not interfere with the traffic at all. 1. Connect to your cluster with `telepresence connect`. 2. Put a wiretap on all traffic going to the application's http port in your cluster and send it to port 8080 on your workstation. ```console $ telepresence wiretap example-app --port 8080:http --env-file ~/example-app-intercept.env --mount /tmp/example-app-mounts Using Deployment example-app wiretapped Wiretap name : example-app State : ACTIVE Workload kind : Deployment Destination : 127.0.0.1:8080 Intercepting : all TCP connections ``` * For `--port`: specify the port the local instance of your application is running on, and optionally the remote port that you want to wiretap. Telepresence will select the remote port automatically when there's only one service port available to access the workload. You must specify the port to wiretap when the workload exposes multiple ports. You can do this by specifying the port you want to wiretap after a colon in the `--port` argument (like in the example), and/or by specifying the service you want to wiretap using the `--service` flag. * For `--env-file`: specify a file path for Telepresence to write the environment variables that are set for the targeted container. 3. Start your local application using the environment variables retrieved and the volumes that were mounted in the previous step. You can now: - Query services only exposed in your cluster's network. - Set breakpoints in your IDE to investigate bugs. ### Running Everything Using Docker This approach confines the Telepresence network interface and remote mounts to a container, and like the [package installer](../install/client.md) approach, eliminates the need for root access. Additionally, it allows for precise replication of the target container's volume mounts, using identical mount points. However, this method will require docker to get cluster connectivity, and the containerized environment can present challenges in terms of toolchain integration, debugging, and the overall development workflow. 1. Connect to your cluster with `telepresence connect --docker`. This starts the Telepresence daemon in a docker container and ensures that this container has access to the cluster network. 2. Use `telepresence curl` to access the Kubernetes API server from a container. A 401 or 403 response code is expected and indicates that the service could be reached. The `telepresence curl` command used will execute a standard `curl` command from a container that shares the network created by the `connect` call: ```console $ telepresence 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. 3. Enter `telepresence list` and make sure the workload you want to engage is listed. For example: ```console $ telepresence list ... deployment example-app: ready to engage (traffic-agent not yet installed) ... ``` 4. Use `replace`, `inject`, or `intercept` to engage the container in combination with the `--docker-run` flag. Example using `telepresence replace` ```console $ telepresence replace example-app --container echo-server --docker-run -- Using Deployment example-app intercepted Intercept name: example-app State : ACTIVE Workload kind : Deployment Destination : 127.0.0.1:8080 Intercepting : all TCP connections ``` You can now: - Make changes on the fly and see them reflected when interacting with your Kubernetes environment; although depending on how your local container is configured, this might require that it is rebuilt. - Query services only exposed in your cluster's network using `telepresence curl`. - Set breakpoints in a _Remote Debug_ configuration in your IDE to investigate bugs. ================================================ FILE: docs/howtos/large-clusters.md ================================================ --- title: Work with large clusters description: Use Telepresence to intercept services in clusters with a large number of namespaces and workloads. hide_table_of_contents: true --- # Working with large clusters ## Large number of namespaces ### The problem When telepresence connects to a cluster, it will configure the local DNS server so that each namespace in the cluster can be used as a top-level domain (TLD). E.g. if the cluster contains the namespace "example", then a curl for the name "my_service.example" will be directed to Telepresence DNS server, because it has announced that it wants to resolve the "example" domain. Telepresence tries to be conservative about what namespaces that it will create TLDs for, and first check if the namespace is accessible by the user. This check can be time-consuming in a cluster with a large number of namespaces, because each check will typically take up to a second to complete, which means that for a cluster with 120 namespaces, this check can take two minutes. That's a long time to wait when doing `telepresence connect`. ### How to solve it #### Limiting at connect The `telepresence connect` command will accept the flag `--mapped-namespaces `, which will limit the names that Telepresence create TLDs for in the DNS resolver. This may drastically decrease the time it takes to connect, and also improve the DNS resolver's performance. #### Limiting the traffic-manager It is possible to limit the namespaces that the traffic-manager will care about when it is installed or upgraded by passing the Helm chart value `namespaces` or `namespaceSelector`. This will tell the manager to only manage those namespaces with respect to connects and engagements. A namespace-limited manager creates an implicit `mapped-namespaces` set for all clients that connect to it. ## Large number of pods ### The problem A cluster with a large number of pods can be problematic in situations where the traffic-manager is unable to use its default behavior of retrieving the pod-subnets from the cluster nodes. The manager will then use a fallback method, which is to retrieve the IP of all pods and then use those IPs to calculate the pod-subnets. This in turn, might cause a very large number of requests to the Kubernetes API server. ### The solution If it is RBAC permission limitations that prevent the traffic-manager from reading the `podCIDR` from the nodes, then adding the necessary permissions might help. But in many cases, the nodes will not have a `podCIDR` defined. The fallback for such cases is to specify the `podCIDRs` manually (and thus prevent the scan + calculation) using the Helm chart values: ```yaml podCIDRStrategy: environment podCIDRs: - ... ``` ## Traffic Manager Namespaces Depending on use-case, it's sometimes beneficial to have several Traffic Managers installed, each being responsible from a limited number of namespaces and prohibited from accessing other namespaces. A cluster can have any number of Traffic Managers, as long as each one manages its own unique set of namespaces. A client that connects to a Traffic Manager will automatically be limited to its managed namespaces. See [Installing a namespaced-scoped traffic-manager](../install/manager.md#limiting-the-namespace-scope) for details. ================================================ FILE: docs/howtos/mtls.md ================================================ --- title: Intercepting Applications Using TLS/mTLS description: How to perform HTTP-filtered intercepts with encrypted data hide_table_of_contents: true --- # Intercepting TLS/mTLS Applications ## Overview Telepresence requires access to HTTP headers and paths to perform HTTP-filtered intercepts. When traffic is encrypted with TLS/mTLS, Telepresence must decrypt the data to inspect these headers. This document explains how to configure Telepresence to handle encrypted traffic. ## Decrypting Traffic To decrypt TLS/mTLS traffic, Telepresence needs access to the TLS certificates used by your application. You can provide this access in two ways: 1. **Mount existing volumes**: Use certificates already mounted in a volume by your application. 2. **Reference a secret**: Mount a Kubernetes secret containing the certificate directly. ### Option 1: Using a Mounted Certificate If your application mounts a volume containing TLS certificates, the Telepresence traffic-agent automatically mounts the same volume. You only need to specify the certificate's path using an annotation. #### Example Suppose your application defines a `tls` volume for the secret `tel-cert`, which contains `tls.crt` and `tls.key`: ```yaml volumes: - name: tls secret: secretName: tel-cert ``` The volume is mounted at `/etc/certs`: ```yaml volumeMounts: - name: tls mountPath: /etc/certs readOnly: true ``` Add the following annotation to your workload to enable Telepresence to use this certificate: ```yaml template: metadata: annotations: telepresence.io/downstream-tls-path.8443: /etc/certs ``` This annotation directs Telepresence to use the certificate at `/etc/certs` for decrypting traffic on port 8443. **Notes:** - For multiple ports, repeat the annotation with different port suffixes. - Ensure the certificate is mounted by all containers whose ports are specified in the annotation. ### Using a Secret If your application containers do not mount the TLS certificate, the traffic-agent can independently mount a Kubernetes secret. The secret must reside in the same namespace as the workload. Add the following annotation to your workload: ```yaml template: metadata: annotations: telepresence.io/downstream-tls-secret.8443: secret-name ``` This annotation prompts the traffic-agent injector to: 1. Add a volume for the specified secret to the pod 2. Mount that volume where the traffic-agent can access the certificate ## Encrypting Upstream Traffic After decrypting traffic and inspecting HTTP filters, Telepresence must re-encrypt the traffic before forwarding it to the application. For applications requiring mutual TLS (mTLS), Telepresence must use the client-side TLS certificate for the upstream connection. Use annotations similar to those for decrypting traffic, but with the prefix `telepresence.io/upstream-tls-` instead of `telepresence.io/downstream-tls-`. ### Self-Signed Certificates Self-signed certificates are common in development environments, and services that use them can be accessed using `curl --insecure` or `curl -k`. Telepresence cannot detect whether this option was used. If downstream traffic is decrypted for HTTP filtering and the application uses a self-signed certificate, Telepresence will fail to establish a secure upstream connection unless verification is skipped. To bypass verification for self-signed certificates, add the following annotation to the workload: ```yaml telepresence.io/upstream-insecure-skip-verify.: enabled ``` ## Using the --plaintext option The `--plaintext` option for intercepts or wiretaps disables encryption of traffic sent to the client during an intercept or wiretap. ## Protocol Selection Telepresence supports HTTP-filtered intercepts on both clear-text and TLS encrypted ports that use either HTTP/1.x and HTTP/2. Telepresence will automatically detect the protocol used by the application and use the appropriate protocol for the intercept. This is done by inspecting the [appProtocol](https://kubernetes.io/docs/concepts/services-networking/service/#application-protocol) of the Kubernetes service for the workload, or if that is not set, by looking at the port name and number or by probing the application. See below for more details. Telepresence will never use TLS or HTTP/2 for services that have a `appProtocol` that is `tcp` or `udp`. Those values imply a "no app-layer sniffing" mode, so when set, they effectively rule out all uses of HTTP-filtered intercepts on encrypted traffic. ### TLS Detection Telepresence will always use TLS for service ports that have: - A TLS certificate configured using the `telepresence.io/downstream-tls-path.` or `telepresence.io/downstream-tls-secret.` annotation.` - An `appProtocol` that is `kubernetes.io/wss`, `wss`, `http2`, `https`, or `grpc`. - The name `https`. - The number 443. Telepresence will never use TLS for service ports that have a `appProtocol` that is `kubernetes.io/ws`, `kubernetes.io/h2c`, or `h2c`. If the selection cannot be made from the above criteria, Telepresence will probe the application to determine if TLS is used. > [!NOTE] > Telepresence will never care about encryption on UDP ports because HTTP filters are only supported on TCP connections. Encrypted UDP traffic can still be intercepted, but the intercept must be global as it happens in the Transport Layer. ### HTTP/2 Detection Telepresence will always use HTTP/2 for services that have `appProtocol` that is "kubernetes.io/h2c", "h2c", "http2", or "grpc". If the selection cannot be made from the above criteria, Telepresence will probe the application to determine if HTTP/2 is used. ### Probing Probing is a fallback mechanism for determining the protocol. It is recommended that it is avoided by setting the appropriate [appProtocol](https://kubernetes.io/docs/concepts/services-networking/service/#application-protocol). The probing takes place when the first connection is made to an intercepted service. If the probe times out because the application is slow to start, the default timeout of 2 seconds can be overridden with the annotation `telepresence.io/upstream-probe-timeout.`. The value must be a number followed by a duration unit, such as `10s` or `500ms`. ================================================ FILE: docs/install/client.md ================================================ --- title: Install client hide_table_of_contents: true --- import Platform from '@site/src/components/Platform'; # Client Installation Install the Telepresence client on your workstation by running the commands below for your OS. ## Install with the package installer (Recommended) The package installer sets up the root daemon as a system service via launchd, eliminating the need for elevated privileges when using Telepresence. Download the appropriate installer for your architecture: - [telepresence-darwin-amd64.pkg](https://github.com/telepresenceio/telepresence/releases/latest/download/telepresence-darwin-amd64.pkg) (Intel Macs) - [telepresence-darwin-arm64.pkg](https://github.com/telepresenceio/telepresence/releases/latest/download/telepresence-darwin-arm64.pkg) (Apple Silicon Macs) Double-click the downloaded `.pkg` file and follow the installation prompts. You can optionally deselect the root daemon component during installation if you prefer to run it manually. ### Allowing the system extension After installation, macOS will block the root daemon from running until you explicitly allow it. You need to: 1. Open **System Settings** 2. Go to **General > Login Items & Extensions** 3. Find **Tada AB** in the list and enable it The macOS installer is signed and distributed by Tada AB, a Swedish company founded by the lead maintainer of the Telepresence open source project. This approval is required because the root daemon manages virtual network interfaces and DNS on your workstation, which macOS treats as a privileged operation. > [!NOTE] > If you skip this step, the root daemon service will not start and Telepresence will fall back to requesting > elevated privileges each time you run `telepresence connect`. ## OR install with Homebrew ```shell brew install telepresenceio/telepresence/telepresence-oss ``` > [!NOTE] > Homebrew installs the standalone binary only. The root daemon is not installed as a system service, so > Telepresence will request elevated privileges when connecting. ## OR download the binary manually ### Intel Macs ```shell # 1. Download the binary. sudo curl -fL https://github.com/telepresenceio/telepresence/releases/latest/download/telepresence-darwin-amd64 -o /usr/local/bin/telepresence # 2. Make the binary executable: sudo chmod a+x /usr/local/bin/telepresence ``` ### Apple Silicon Macs ```shell # 1. Ensure that no old binary exists. This is very important because Apple Silicon macs track the executable's # signature and just updating it in place will not work. sudo rm -f /usr/local/bin/telepresence # 2. Download the binary. sudo curl -fL https://github.com/telepresenceio/telepresence/releases/latest/download/telepresence-darwin-arm64 -o /usr/local/bin/telepresence # 3. Make the binary executable: sudo chmod a+x /usr/local/bin/telepresence ``` ## Install using a package manager (Recommended) The package installers set up the root daemon as a systemd service, eliminating the need for elevated privileges when using Telepresence. ### Debian/Ubuntu (.deb) ```shell # Download the latest .deb package # AMD64 curl -fLO https://github.com/telepresenceio/telepresence/releases/latest/download/telepresence-linux-amd64.deb # ARM64 curl -fLO https://github.com/telepresenceio/telepresence/releases/latest/download/telepresence-linux-arm64.deb # Install the package sudo apt install ./telepresence-linux-*.deb ``` ### Fedora/RHEL (.rpm) ```shell # Download the latest .rpm package # AMD64 curl -fLO https://github.com/telepresenceio/telepresence/releases/latest/download/telepresence-linux-amd64.rpm # ARM64 curl -fLO https://github.com/telepresenceio/telepresence/releases/latest/download/telepresence-linux-arm64.rpm # Install the package sudo dnf install ./telepresence-linux-*.rpm ``` ### Viewing service logs The root daemon service logs to the systemd journal: ```shell journalctl -u telepresence-rootd ``` > [!NOTE] > If you get a permission error, your user needs to be in a group that can read the journal. > On Fedora/RHEL, add yourself to the `wheel` group. On Debian/Ubuntu, use `systemd-journal` or `adm`: > ```shell > sudo usermod -aG systemd-journal $USER > ``` > Log out and back in for the change to take effect. ## OR download the binary manually ```shell # 1. Download the latest binary (~95 MB): # AMD64 sudo curl -fL https://github.com/telepresenceio/telepresence/releases/latest/download/telepresence-linux-amd64 -o /usr/local/bin/telepresence # ARM64 sudo curl -fL https://github.com/telepresenceio/telepresence/releases/latest/download/telepresence-linux-arm64 -o /usr/local/bin/telepresence # 2. Make the binary executable: sudo chmod a+x /usr/local/bin/telepresence ``` > [!NOTE] > Installing the standalone binary does not set up the root daemon as a system service. Telepresence will > request elevated privileges when connecting. ## Install using the setup installer (Recommended) The setup installer sets up the root daemon as a Windows service, eliminating the need for elevated privileges when using Telepresence. It also bundles WinFSP and SSHFS-Win for volume mount support. Download and run the installer: - [telepresence-windows-amd64-setup.exe](https://github.com/telepresenceio/telepresence/releases/latest/download/telepresence-windows-amd64-setup.exe) During installation, you can optionally configure the daemon port and log level. The defaults work for most users. You can also deselect the "Telepresence Network Service" feature if you prefer to run the daemon manually. > [!NOTE] > The Windows installer is currently only available for AMD64. For ARM64, use the manual installation method below. ## OR install manually using PowerShell ### Windows AMD64 ```powershell # To install Telepresence, run the following commands # from PowerShell as Administrator. # 1. Download the latest windows zip containing telepresence.exe and its dependencies (~60 MB): $ProgressPreference = 'SilentlyContinue' Invoke-WebRequest https://github.com/telepresenceio/telepresence/releases/latest/download/telepresence-windows-amd64.zip -OutFile telepresence.zip # 2. Unzip the telepresence.zip file to the desired directory, then remove the zip file: Expand-Archive -Path telepresence.zip -DestinationPath telepresenceInstaller/telepresence Remove-Item 'telepresence.zip' cd telepresenceInstaller/telepresence # 3. Run the install-telepresence.ps1 to install telepresence's dependencies. It will install telepresence to # C:\telepresence by default, but you can specify a custom path by passing in -Path C:\my\custom\path powershell.exe -ExecutionPolicy bypass -c " . '.\install-telepresence.ps1';" # 4. Remove the unzipped directory: cd ../.. Remove-Item telepresenceInstaller -Recurse -Confirm:$false -Force # 5. Telepresence is now installed and you can use telepresence commands in PowerShell. ``` ### Windows ARM64 ```powershell # To install Telepresence, run the following commands # from PowerShell as Administrator. # 1. Download the latest windows zip containing telepresence.exe and its dependencies (~60 MB): $ProgressPreference = 'SilentlyContinue' Invoke-WebRequest https://github.com/telepresenceio/telepresence/releases/latest/download/telepresence-windows-arm64.zip -OutFile telepresence.zip # 2. Unzip the telepresence.zip file to the desired directory, then remove the zip file: Expand-Archive -Path telepresence.zip -DestinationPath telepresenceInstaller/telepresence Remove-Item 'telepresence.zip' cd telepresenceInstaller/telepresence # 3. Run the install-telepresence.ps1 to install telepresence's dependencies. It will install telepresence to # C:\telepresence by default, but you can specify a custom path by passing in -Path C:\my\custom\path powershell.exe -ExecutionPolicy bypass -c " . '.\install-telepresence.ps1';" # 4. Remove the unzipped directory: cd ../.. Remove-Item telepresenceInstaller -Recurse -Confirm:$false -Force # 5. Telepresence is now installed and you can use telepresence commands in PowerShell. ``` > [!NOTE] > Manual installation does not set up the root daemon as a Windows service or install WinFSP/SSHFS-Win. > Telepresence will request elevated privileges when connecting, and volume mounts will not work without > WinFSP and SSHFS-Win installed separately. > [!TIP] > What's Next? > Follow one of our [quick start guides](../quick-start.md) to start using Telepresence, either with our sample app or in your own environment. ## Uninstalling If you installed using the package installer, run the uninstall script: ```shell sudo telepresence-uninstall ``` This removes the Telepresence binaries and the root daemon launchd service. If you installed with Homebrew: ```shell brew uninstall telepresenceio/telepresence/telepresence-oss ``` Use your package manager to remove Telepresence: ```shell # Debian/Ubuntu sudo apt remove telepresence # Fedora/RHEL sudo dnf remove telepresence ``` This stops and removes the root daemon systemd service and the Telepresence binaries. Open **Settings > Apps > Installed apps**, find Telepresence, and select **Uninstall**. This removes the Telepresence binaries, the root daemon Windows service, and the bundled WinFSP and SSHFS-Win components. ## Installing older versions of Telepresence Use these URLs to download an older version for your OS (including older nightly builds), replacing `X.Y.Z` with the version you want. ```shell # Intel Macs https://github.com/telepresenceio/telepresence/releases/download/vX.Y.Z/telepresence-darwin-amd64 # Apple Silicon Macs https://github.com/telepresenceio/telepresence/releases/download/vX.Y.Z/telepresence-darwin-arm64 # Package installers (available from v2.27.0) https://github.com/telepresenceio/telepresence/releases/download/vX.Y.Z/telepresence-darwin-amd64.pkg https://github.com/telepresenceio/telepresence/releases/download/vX.Y.Z/telepresence-darwin-arm64.pkg ``` ``` # AMD64 https://github.com/telepresenceio/telepresence/releases/download/vX.Y.Z/telepresence-linux-amd64 https://github.com/telepresenceio/telepresence/releases/download/vX.Y.Z/telepresence-linux-amd64.deb https://github.com/telepresenceio/telepresence/releases/download/vX.Y.Z/telepresence-linux-amd64.rpm # ARM64 https://github.com/telepresenceio/telepresence/releases/download/vX.Y.Z/telepresence-linux-arm64 https://github.com/telepresenceio/telepresence/releases/download/vX.Y.Z/telepresence-linux-arm64.deb https://github.com/telepresenceio/telepresence/releases/download/vX.Y.Z/telepresence-linux-arm64.rpm ``` ``` # Windows AMD64 https://github.com/telepresenceio/telepresence/releases/download/vX.Y.Z/telepresence-windows-amd64-setup.exe https://github.com/telepresenceio/telepresence/releases/download/vX.Y.Z/telepresence-windows-amd64.zip # Windows ARM64 https://github.com/telepresenceio/telepresence/releases/download/vX.Y.Z/telepresence-windows-arm64.zip ``` ================================================ FILE: docs/install/cloud.md ================================================ --- title: Cloud Provider Prerequisites hide_table_of_contents: true --- # Provider Prerequisites for Traffic Manager ## GKE ### Firewall Rules for private clusters A GKE cluster with private networking will come preconfigured with firewall rules that prevent the Traffic Manager's webhook injector from being invoked by the Kubernetes API server. For Telepresence to work in such a cluster, you'll need to [add a firewall rule](https://cloud.google.com/kubernetes-engine/docs/how-to/private-clusters#add_firewall_rules) allowing the Kubernetes masters to access TCP port `8443` in your pods. For example, for a cluster named `tele-webhook-gke` in region `us-central1-c1`: ```bash $ gcloud container clusters describe tele-webhook-gke --region us-central1-c | grep masterIpv4CidrBlock masterIpv4CidrBlock: 172.16.0.0/28 # Take note of the IP range, 172.16.0.0/28 $ gcloud compute firewall-rules list \ --filter 'name~^gke-tele-webhook-gke' \ --format 'table( name, network, direction, sourceRanges.list():label=SRC_RANGES, allowed[].map().firewall_rule().list():label=ALLOW, targetTags.list():label=TARGET_TAGS )' NAME NETWORK DIRECTION SRC_RANGES ALLOW TARGET_TAGS gke-tele-webhook-gke-33fa1791-all tele-webhook-net INGRESS 10.40.0.0/14 esp,ah,sctp,tcp,udp,icmp gke-tele-webhook-gke-33fa1791-node gke-tele-webhook-gke-33fa1791-master tele-webhook-net INGRESS 172.16.0.0/28 tcp:10250,tcp:8443 gke-tele-webhook-gke-33fa1791-node gke-tele-webhook-gke-33fa1791-vms tele-webhook-net INGRESS 10.128.0.0/9 icmp,tcp:1-65535,udp:1-65535 gke-tele-webhook-gke-33fa1791-node # Take note fo the TARGET_TAGS value, gke-tele-webhook-gke-33fa1791-node $ gcloud compute firewall-rules create gke-tele-webhook-gke-webhook \ --action ALLOW \ --direction INGRESS \ --source-ranges 172.16.0.0/28 \ --rules tcp:8443 \ --target-tags gke-tele-webhook-gke-33fa1791-node --network tele-webhook-net Creating firewall...⠹Created [https://www.googleapis.com/compute/v1/projects/datawire-dev/global/firewalls/gke-tele-webhook-gke-webhook]. Creating firewall...done. NAME NETWORK DIRECTION PRIORITY ALLOW DENY DISABLED gke-tele-webhook-gke-webhook tele-webhook-net INGRESS 1000 tcp:8443 False ``` ### GKE Authentication Plugin Starting with Kubernetes version 1.26 GKE will require the use of the [gke-gcloud-auth-plugin](https://cloud.google.com/blog/products/containers-kubernetes/kubectl-auth-changes-in-gke). You will need to install this plugin to use Telepresence with Docker while using GKE. ## EKS ### EKS Authentication Plugin If you are using AWS CLI version earlier than `1.16.156` you will need to install [aws-iam-authenticator](https://docs.aws.amazon.com/eks/latest/userguide/install-aws-iam-authenticator.html). You will need to install this plugin to use Telepresence with Docker while using EKS. ================================================ FILE: docs/install/manager.md ================================================ --- title: Install Traffic Manager hide_table_of_contents: true --- # Install/Uninstall the Traffic Manager Telepresence uses a traffic manager to send/receive cloud traffic to the user. Telepresence uses [Helm](https://helm.sh) under the hood to install the traffic manager in your cluster. The `telepresence` binary embeds both `helm` and a helm-chart for a traffic-manager that is of the same version as the binary. The Telepresence Helm chart documentation is published at [ArtifactHUB](https://artifacthub.io/packages/helm/telepresence-oss/telepresence-oss). You can also use `helm` command directly, see [Install With Helm](#install-with-helm) for more details. ## Prerequisites Before you begin, you need to have [Telepresence installed](../install/client.md). If you are not the administrator of your cluster, you will need [administrative RBAC permissions](../reference/rbac.md#administrating-telepresence) to install and use Telepresence in your cluster. In addition, you may need certain prerequisites depending on your cloud provider and platform. See the [cloud provider installation notes](../install/cloud.md) for more. ## Install the Traffic Manager The telepresence cli can install the traffic manager for you. The basic install will install the same version as the client used. 1. Install the Telepresence Traffic Manager with the following command: ```shell telepresence helm install ``` ### Customizing the Traffic Manager. For details on what the Helm chart installs and what can be configured, see the Helm chart [configuration on artifacthub](https://artifacthub.io/packages/helm/datawire/telepresence). 1. Create a values.yaml file with your config values. 2. Run the `install` command with the `--values` flag set to the path to your values file: ```shell telepresence helm install --values values.yaml ``` alternatively, provide values using the `--set` flag: ```shell telepresence helm install --set logLevel=debug ``` ### Install into custom namespace The Helm chart supports being installed into any namespace, not necessarily `ambassador`. Simply pass a different `namespace` argument to `telepresence helm install`. For example, if you wanted to deploy the traffic manager to the `staging` namespace: ```shell telepresence helm install traffic-manager --namespace staging datawire/telepresence ``` > [!NOTE] > If you have several traffic-managers installed, or if users don't have permissions to list > namespaces, they will need to either use a `--manager-namespace ` flag when connecting or > configure their config.yml or kubeconfig to find the desired installation of the Traffic Manager As kubeconfig extension: ```yaml apiVersion: v1 clusters: - cluster: server: https://127.0.0.1 extensions: - name: telepresence.io extension: cluster: defaultManagerNamespace: staging name: example-cluster ``` or in the `config.yml`: ```yaml cluster: defaultManagerNamespace: staging ``` See [the kubeconfig documentation](../reference/config.md#manager) for more information. ## Upgrading/Downgrading the Traffic Manager. 1. Download the cli of the version of Telepresence you wish to use. 2. Run the `upgrade` command. Optionally with `--values` and/or `--set` flags ```shell telepresence helm upgrade ``` You can also use the `--reuse-values` or `--reset-values` to specify if previously installed values should be reused or reset. ## Uninstall The telepresence cli can uninstall the traffic manager for you using the `telepresence helm uninstall`. 1. Uninstall the Telepresence Traffic Manager and all the agents installed by it using the following command: ```shell telepresence helm uninstall ``` ## Limiting the Namespace Scope You might not want the Traffic Manager to have permissions across the entire kubernetes cluster, or you might want to be able to install multiple traffic managers per cluster (for example, to separate them by environment). In these cases, the traffic manager supports being installed with a namespace scope, allowing cluster administrators to limit the reach of a traffic manager's permissions. For example, suppose you want a Traffic Manager that only works on namespaces `dev` and `staging`. To do this, create a `values.yaml` like the following: ```yaml namespaces: - dev - staging ``` This can then be installed via: ```shell telepresence helm install --namespace staging -f ./values.yaml ``` ### Namespace collision detection The Telepresence Helm chart incorporates a mechanism to prevent conflicts between Traffic Managers operating within different namespaces. This is achieved by: 1. Determining the Traffic Manager's set of namespaces by applying its namespace selector to all of the cluster's namespaces. 2. Verifying that there is no overlap between the sets of namespaces for any pair of Traffic Managers. So, for example, suppose you install one Traffic Manager to manage namespaces `dev` and `staging`, as: ```bash telepresence helm install --namespace dev --set 'namespaces={dev,staging}' ``` You might then attempt to install another Traffic Manager to manage namespaces `staging` and `prod`: ```bash telepresence helm install --namespace prod --set 'namespaces={staging,prod}' ``` This would fail with an error: ``` telepresence helm install: error: execution error at (telepresence-oss/templates/agentInjectorWebhook.yaml:61:14): traffic-manager in namespace dev already manages namespace staging ``` To fix this error, fix the overlap either by removing `staging` from the first install, or from the second. ### Static versus Dynamic Namespace Selection A namespace selector can be dynamic or static. This in turn controls if telepresence needs "cluster-wide" or "namespaced" role/rolebinding pairs. A Traffic Manager configured with a dynamic selector requires cluster-wide namespace access and `ClusterRole`/`ClusterRoleBinding` pairs. A Traffic Manager configured with a static selector needs a `Role`/`RoleBinding` pair in each of the selected namespaces. A selector is considered _static_ if it meets the following conditions: - The selector must have exactly one element in either the `matchLabels` or the `matchExpression` list (a `key=value` element in the `matchLabels` list, it is normalized into a `key in [value]` expression element). - The element must meet the following criteria: The `key` of the match expression must be "kubernetes.io/metadata.name". The `operator` of the match expression must be "In" (case sensitive). The `values` list of the match expression must contain at least one value. ## Static Namespace Selection RBAC Optionally, you can also configure user rbac to be scoped to the same namespaces as the manager itself. You might want to do this if you don't give your users permissions throughout the cluster, and want to make sure they only have the minimum set required to perform telepresence commands on certain namespaces. Continuing with the `dev` and `staging` example from the previous section, simply add the following to `values.yaml` (make sure you set the `subjects`!): ```yaml clientRbac: create: true # These are the users or groups to which the user rbac will be bound. # This MUST be set. subjects: {} # - kind: User # name: jane # apiGroup: rbac.authorization.k8s.io # The namespaces can be explicitly specified here, but can be omitted unless the # Traffic Manager's namespaceSelector is dynamic. namespaces: - dev - staging ``` ### Installing RBAC only Telepresence Traffic Manager does require some [RBAC](../reference/rbac.md) for the traffic-manager deployment itself, as well as for users. To make it easier for operators to introspect / manage RBAC separately, you can use `rbac.only=true` to only create the rbac-related objects. Additionally, you can use `clientRbac.create=true` and `managerRbac.create=true` to toggle which subset(s) of RBAC objects you wish to create. ## Install with Helm Before you begin, you must ensure that the [helm command](https://helm.sh/docs/intro/install/) is installed. ### Recommended version In general, we recommend that you use a helm binary with a minor version that is less than three numbers below the version embedded in the telepresence binary. E.g., for embedded version 3.18.x, use helm >= 3.16.x. You can check your helm version using: ```bash helm version ``` and the Telepresence embedded helm version using: ```bash telepresence helm version ``` The Telepresence Helm chart is published at GitHub in the ghcr.io repository. ### Installing Install the latest stable version of the traffic-manager into the default "ambassador" namespace with the following command: ```bash helm install --create-namespace --namespace ambassador traffic-manager oci://ghcr.io/telepresenceio/telepresence-oss ``` ### Upgrading/Downgrading Use this command if you installed the Traffic Manager into the "ambassador" namespace, and you just wish to upgrade it to the latest version without changing any configuration values: ```bash helm upgrade --namespace ambassador --reuse-values traffic-manager oci://ghcr.io/telepresenceio/telepresence-oss ``` If you want to upgrade (or downgrade) the Traffic Manager to a specific version, add a `--version` flag with the version number to the upgrade command, e.g.: `--version v2.20.3`. ### Uninstalling Use the following command to uninstall the Traffic Manager: ```bash helm uninstall --namespace ambassador traffic-manager ``` ================================================ FILE: docs/install/upgrade.md ================================================ --- title: Upgrade client description: "How to upgrade your installation of Telepresence and install previous versions." hide_table_of_contents: true --- import Platform from '@site/src/components/Platform'; # Upgrade Process The Telepresence CLI will periodically check for new versions and notify you when an upgrade is available. Running the same commands used for installation will replace your current version with the latest. Before upgrading your CLI, you must stop any live Telepresence processes by issuing `telepresence quit -s` (or `telepresence quit -ur` if your current version is less than 2.8.0). ## Upgrade with the package installer (Recommended) Download and install the latest `.pkg` for your architecture from the [install page](client.md). The installer will replace the previous version and restart the root daemon service. ## OR upgrade with Homebrew ```shell brew upgrade telepresenceio/telepresence/telepresence-oss ``` ## OR upgrade by downloading the binary manually ### Intel Macs ```shell # 1. Download the binary. sudo curl -fL https://github.com/telepresenceio/telepresence/releases/latest/download/telepresence-darwin-amd64 -o /usr/local/bin/telepresence # 2. Make the binary executable: sudo chmod a+x /usr/local/bin/telepresence ``` ### Apple Silicon Macs ```shell # 1. Ensure that no old binary exists. This is very important because Apple Silicon macs track the executable's # signature and just updating it in place will not work. sudo rm -f /usr/local/bin/telepresence # 2. Download the binary. sudo curl -fL https://github.com/telepresenceio/telepresence/releases/latest/download/telepresence-darwin-arm64 -o /usr/local/bin/telepresence # 3. Make the binary executable: sudo chmod a+x /usr/local/bin/telepresence ``` ## Upgrade with a package manager (Recommended) Download and install the latest `.deb` or `.rpm` package using the same commands as the initial [installation](client.md). The package manager will handle replacing the previous version and restarting the root daemon service. ## OR upgrade by downloading the binary manually ```shell # 1. Download the latest binary (~95 MB): ### AMD64 sudo curl -fL https://github.com/telepresenceio/telepresence/releases/latest/download/telepresence-linux-amd64 -o /usr/local/bin/telepresence ### ARM64 sudo curl -fL https://github.com/telepresenceio/telepresence/releases/latest/download/telepresence-linux-arm64 -o /usr/local/bin/telepresence # 2. Make the binary executable: sudo chmod a+x /usr/local/bin/telepresence ``` ## Upgrade with the setup installer (Recommended) Download and run the latest setup installer from the [install page](client.md). The installer will replace the previous version and restart the root daemon service. ## OR upgrade by downloading manually Download the latest zip from the [install page](client.md) and follow the manual installation steps, which will replace the existing installation. The Telepresence CLI contains an embedded Helm chart. See [Install/Uninstall the Traffic Manager](manager.md) if you want to also upgrade the Traffic Manager in your cluster. ================================================ FILE: docs/licenses.md ================================================ Telepresence CLI incorporates Free and Open Source software under the following licenses: * [2-clause BSD license](https://opensource.org/licenses/BSD-2-Clause) * [3-clause BSD license](https://opensource.org/licenses/BSD-3-Clause) * [Apache License 2.0](https://opensource.org/licenses/Apache-2.0) * [ISC license](https://opensource.org/licenses/ISC) * [MIT No Attribution license](https://spdx.org/licenses/MIT-0.html) * [MIT license](https://opensource.org/licenses/MIT) * [Mozilla Public License 2.0](https://opensource.org/licenses/MPL-2.0) ================================================ FILE: docs/quick-start.md ================================================ --- title: Quick start description: "Start using Telepresence in your own environment. Follow these steps to intercept your service in your cluster." hide_table_of_contents: true --- # Telepresence Quickstart Telepresence is an open source tool that enables you to set up remote development environments for Kubernetes where you can still use all of your favorite local tools like IDEs, debuggers, and profilers. ## Prerequisites - [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/), the Kubernetes command-line tool, or the OpenShift Container Platform command-line interface, [oc](https://docs.openshift.com/container-platform/4.2/cli_reference/openshift_cli/getting-started-cli.html#cli-installing-cli_cli-developer-commands). - A Kubernetes Deployment and Service. ## Install Telepresence Follow [Install Client](install/client.md) and [Install Traffic Manager](install/manager.md) instructions to install the telepresence client on your workstation, and the traffic manager in your cluster. Checkout the [Howto](howtos/engage.md) to learn how Telepresence can engage with resources in your remote cluster, enabling you to run the code on your local workstation. ## What’s Next? - [Learn about the Telepresence architecture.](reference/architecture) ================================================ FILE: docs/redirects.yml ================================================ - {from: "", to: "quick-start"} ================================================ FILE: docs/reference/architecture.md ================================================ --- title: Architecture description: How Telepresence works to intercept traffic from your Kubernetes cluster to code running on your laptop. hide_table_of_contents: true --- # Telepresence Architecture ![Architecture](../images/TP_Architecture.svg) ## Telepresence CLI The Telepresence CLI orchestrates the moving parts on the workstation: it starts the Telepresence Daemons and then acts as a user-friendly interface to the Telepresence User Daemon. ## Telepresence Daemons Telepresence has Daemons that run on a developer's workstation and act as the main point of communication to the cluster's network in order to communicate with the cluster and handle intercepted traffic. ### User-Daemon The User-Daemon coordinates the creation and deletion of replacements, ingests and intercepts by communicating with the [Traffic Manager](#traffic-manager). All requests from and to the cluster go through this Daemon. ### Root-Daemon The Root-Daemon manages the networking necessary to handle traffic between the local workstation and the cluster by setting up a [Virtual Network Device](tun-device.md) (VIF). ## Traffic Manager The Traffic Manager is the central point of communication between Traffic Agents in the cluster and Telepresence Daemons on developer workstations. It is responsible for injecting the Traffic Agent sidecar into engaged pods, proxying all relevant inbound and outbound traffic, and tracking active engagements. The Traffic-Manager is installed by a cluster administrator. It can either be installed using the Helm chart embedded in the telepresence client binary (`telepresence helm install`) or by using a Helm Chart directly. ## Traffic Agent The Traffic Agent is a sidecar container that facilitates engagements. When a `replace`, `ingest`, `intercept`, or `wiretap` is first started, the Traffic Agent container is injected into the workload's pod(s). You can see the Traffic Agent's status by running `telepresence list` or `kubectl describe pod `. Depending on if an `replace` or `intercept` is active or not, the Traffic Agent will either route the incoming request to your workstation, or it will pass it along to the container in the pod usually handling requests. When a `wiretap` is active, the Traffic Agent will send a copy of the incoming requests to your workstation. Please see [Traffic Agent Sidecar](engagements/sidecar.md) for details. ================================================ FILE: docs/reference/cli/telepresence.md ================================================ --- title: telepresence description: Connect your workstation to a Kubernetes cluster hide_table_of_contents: true --- Connect your workstation to a Kubernetes cluster ## Synopsis: Telepresence can connect to a cluster and route all outbound traffic from your workstation to that cluster so that software running locally can communicate as if it executed remotely, inside the cluster. This is achieved using the command: ```bash telepresence connect ``` Telepresence can also intercept traffic intended for a specific service in a cluster and redirect it to your local workstation: ```bash telepresence intercept <name of service> ``` Telepresence uses a background process, the user daemon, to manage the cluster session, and a system service, the root daemon, to modify the workstation's network and DNS so that the cluster's services become available locally. If Telepresence was installed as a standalone binary, the system service will not be present. A root daemon must then be started using sudo, which may result in a password prompt. ### Usage: ``` telepresence [command] [flags] ``` ### Available Commands: | Command | Description | |---------|-------------| | [completion](telepresence_completion) | Generate a shell completion script | | [compose](telepresence_compose) | Define and run multi-container applications with Telepresence and Docker | | [config](telepresence_config) | Telepresence configuration commands | | [connect](telepresence_connect) | Connect to a cluster | | [curl](telepresence_curl) | curl with daemon network | | [docker-run](telepresence_docker-run) | Docker run with daemon network | | [gather-logs](telepresence_gather-logs) | Gather logs from traffic-manager, traffic-agent, user and root daemons, and export them into a zip file. | | [genyaml](telepresence_genyaml) | Generate YAML for use in kubernetes manifests. | | [helm](telepresence_helm) | Helm commands using the embedded Telepresence Helm chart. | | [ingest](telepresence_ingest) | Ingest a container | | [intercept](telepresence_intercept) | Intercept a service | | [leave](telepresence_leave) | Remove existing intercept | | [list](telepresence_list) | List current intercepts | | [list-contexts](telepresence_list-contexts) | Show all contexts | | [list-namespaces](telepresence_list-namespaces) | Show all namespaces | | [loglevel](telepresence_loglevel) | Temporarily change the log-level of the traffic-manager, traffic-agent, and user and root daemons | | [mcp](telepresence_mcp) | MCP server management | | [quit](telepresence_quit) | Tell telepresence daemons to quit | | [replace](telepresence_replace) | Replace a container | | [revoke](telepresence_revoke) | Revoke an intercept by intercept ID. The intercept ID must be in the format <session_id>:<intercept_name> | | [serve](telepresence_serve) | Start the browser on a remote service | | [status](telepresence_status) | Show connectivity status | | [uninstall](telepresence_uninstall) | Uninstall telepresence agents | | [version](telepresence_version) | Show version | | [wiretap](telepresence_wiretap) | Wiretap a Service | ### Flags: ``` -h, --help help for telepresence ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` Use `telepresence [command] --help` for more information about a command. ================================================ FILE: docs/reference/cli/telepresence_completion.md ================================================ --- title: telepresence completion description: Generate a shell completion script hide_table_of_contents: true --- Generate a shell completion script ## Synopsis: To load completions: ### Bash: ```bash $ source <(telepresence completion bash) # To load completions for each session, execute once: # Linux: $ telepresence completion bash > /etc/bash_completion.d/telepresence # macOS: $ telepresence completion bash > $(brew --prefix)/etc/bash_completion.d/telepresence ``` ### Zsh: ```zsh # If shell completion is not already enabled in your environment, # you will need to enable it. You can execute the following once: $ echo "autoload -U compinit; compinit" >> ~/.zshrc # To load completions for each session, execute once: $ telepresence completion zsh > "${fpath[1]}/_telepresence" # You will need to start a new shell for this setup to take effect. ``` ### fish: ```fish $ telepresence completion fish | source # To load completions for each session, execute once: $ telepresence completion fish > ~/.config/fish/completions/telepresence.fish ``` ### PowerShell: ```powershell PS> telepresence completion powershell | Out-String | Invoke-Expression # To load completions for every new session, run: PS> telepresence completion powershell > telepresence.ps1 # and source this file from your PowerShell profile. ``` ### Usage: ``` telepresence completion [flags] ``` ### Flags: ``` -h, --help help for completion ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose.md ================================================ --- title: telepresence compose description: Define and run multi-container applications with Telepresence and Docker hide_table_of_contents: true --- Define and run multi-container applications with Telepresence and Docker ### Usage: ``` telepresence compose [command] [flags] ``` ### Available Commands: | Command | Description | |---------|-------------| | [attach](telepresence_compose_attach) | Attach local standard input, output, and error streams to a service's running container | | [bridge](telepresence_compose_bridge) | Convert compose files into another model | | [build](telepresence_compose_build) | Build or rebuild services | | [commit](telepresence_compose_commit) | Create a new image from a service container's changes | | [config](telepresence_compose_config) | Parse, resolve and render compose file in canonical format | | [cp](telepresence_compose_cp) | docker compose cp [OPTIONS] SRC_PATH|- SERVICE:DEST_PATH | | [create](telepresence_compose_create) | Creates containers for a service | | [down](telepresence_compose_down) | Stop and remove containers, networks | | [events](telepresence_compose_events) | Receive real time events from containers | | [exec](telepresence_compose_exec) | Execute a command in a running container | | [export](telepresence_compose_export) | Export a service container's filesystem as a tar archive | | [images](telepresence_compose_images) | List images used by the created containers | | [kill](telepresence_compose_kill) | Force stop service containers | | [logs](telepresence_compose_logs) | View output from containers | | [ls](telepresence_compose_ls) | List running compose projects | | [pause](telepresence_compose_pause) | Pause services | | [port](telepresence_compose_port) | Print the public port for a port binding | | [ps](telepresence_compose_ps) | List containers | | [publish](telepresence_compose_publish) | Publish compose application | | [pull](telepresence_compose_pull) | Pull service images | | [push](telepresence_compose_push) | Push service images | | [restart](telepresence_compose_restart) | Restart service containers | | [rm](telepresence_compose_rm) | Removes stopped service containers | | [run](telepresence_compose_run) | Run a one-off command on a service | | [scale](telepresence_compose_scale) | Scale services | | [start](telepresence_compose_start) | Start services | | [stats](telepresence_compose_stats) | Display a live stream of container(s) resource usage statistics | | [stop](telepresence_compose_stop) | Stop services | | [top](telepresence_compose_top) | Display the running processes | | [unpause](telepresence_compose_unpause) | Unpause services | | [up](telepresence_compose_up) | Create and start containers | | [version](telepresence_compose_version) | Show the Docker Compose version information | | [volumes](telepresence_compose_volumes) | List volumes | | [wait](telepresence_compose_wait) | Block until containers of all (or specified) services stop. | | [watch](telepresence_compose_watch) | Watch build context for service and rebuild/refresh containers when files are updated | ### Flags: ``` -h, --help help for compose ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` Use `telepresence compose [command] --help` for more information about a command. ================================================ FILE: docs/reference/cli/telepresence_compose_attach.md ================================================ --- title: telepresence compose attach description: Attach local standard input, output, and error streams to a service's running container hide_table_of_contents: true --- Attach local standard input, output, and error streams to a service's running container ### Usage: ``` telepresence compose attach [flags] [services] ``` ### Flags: ``` -h, --help help for attach ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose attach flags: ``` --detach-keys string Override the key sequence for detaching from a container. --index int index of the container if service has multiple replicas. --no-stdin Do not attach STDIN --sig-proxy Proxy all received signals to the process (default true) ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_bridge.md ================================================ --- title: telepresence compose bridge description: Convert compose files into another model hide_table_of_contents: true --- Convert compose files into another model ### Usage: ``` telepresence compose bridge [flags] [services] ``` ### Flags: ``` -h, --help help for bridge ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose bridge flags: ``` ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_build.md ================================================ --- title: telepresence compose build description: Build or rebuild services hide_table_of_contents: true --- Build or rebuild services ### Usage: ``` telepresence compose build [flags] [services] ``` ### Flags: ``` -h, --help help for build ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose build flags: ``` --build-arg stringArray Set build-time variables for services --builder string Set builder to use --check Check build configuration -m, --memory bytes Set memory limit for the build container. Not supported by BuildKit. --no-cache Do not use cache when building the image --print Print equivalent bake file --provenance string Add a provenance attestation --pull Always attempt to pull a newer version of the image --push Push service images -q, --quiet Suppress the build output --sbom string Add a SBOM attestation --ssh string Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent) --with-dependencies Also build dependencies (transitively) ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_commit.md ================================================ --- title: telepresence compose commit description: Create a new image from a service container's changes hide_table_of_contents: true --- Create a new image from a service container's changes ### Usage: ``` telepresence compose commit [flags] [services] ``` ### Flags: ``` -h, --help help for commit ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose commit flags: ``` -a, --author string Author (e.g., "John Hannibal Smith <hannibal@a-team.com>") -c, --change list Apply Dockerfile instruction to the created image --index int index of the container if service has multiple replicas. -m, --message string Commit message -p, --pause Pause container during commit (default true) ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_config.md ================================================ --- title: telepresence compose config description: Parse, resolve and render compose file in canonical format hide_table_of_contents: true --- Parse, resolve and render compose file in canonical format ### Usage: ``` telepresence compose config [flags] [services] ``` ### Flags: ``` -h, --help help for config ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose config flags: ``` --environment Print environment used for interpolation. --format string Format the output. Values: [yaml | json] --hash string Print the service config hash, one per line. --images Print the image names, one per line. --lock-image-digests Produces an override file with image digests --models Print the model names, one per line. --networks Print the network names, one per line. --no-consistency Don't check model consistency - warning: may produce invalid Compose output --no-env-resolution Don't resolve service env files --no-interpolate Don't interpolate environment variables --no-normalize Don't normalize compose model --no-path-resolution Don't resolve file paths -o, --output string Save to file (default to stdout) --profiles Print the profile names, one per line. -q, --quiet Only validate the configuration, don't print anything --resolve-image-digests Pin image tags to digests --services Print the service names, one per line. --variables Print model variables and default values. --volumes Print the volume names, one per line. ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_cp.md ================================================ --- title: telepresence compose cp description: docker compose cp [OPTIONS] SRC_PATH|- SERVICE:DEST_PATH hide_table_of_contents: true --- docker compose cp [OPTIONS] SRC_PATH|- SERVICE:DEST_PATH ### Usage: ``` telepresence compose cp [flags] [services] ``` ### Flags: ``` -h, --help help for cp ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose cp flags: ``` --all Include containers created by the run command -a, --archive Archive mode (copy all uid/gid information) -L, --follow-link Always follow symbol link in SRC_PATH --index int Index of the container if service has multiple replicas ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_create.md ================================================ --- title: telepresence compose create description: Creates containers for a service hide_table_of_contents: true --- Creates containers for a service ### Usage: ``` telepresence compose create [flags] [services] ``` ### Flags: ``` -h, --help help for create ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose create flags: ``` --build Build images before starting containers --force-recreate Recreate containers even if their configuration and image haven't changed --no-build Don't build an image, even if it's policy --no-recreate If containers already exist, don't recreate them. Incompatible with --force-recreate. --pull string Pull image before running ("always"|"missing"|"never"|"build") (default "policy") --quiet-pull Pull without printing progress information --remove-orphans Remove containers for services not defined in the Compose file --scale uint32 Scale SERVICE to NUM instances. Overrides the scale setting in the Compose file if present. -y, --yes Assume "yes" as answer to all prompts and run non-interactively ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_down.md ================================================ --- title: telepresence compose down description: Stop and remove containers, networks hide_table_of_contents: true --- Stop and remove containers, networks ### Usage: ``` telepresence compose down [flags] [services] ``` ### Flags: ``` -h, --help help for down ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose down flags: ``` --remove-orphans Remove containers for services not defined in the Compose file --rmi string Remove images used by services. "local" remove only images that don't have a custom tag ("local"|"all") -t, --timeout int Specify a shutdown timeout in seconds -v, --volumes Remove named volumes declared in the "volumes" section of the Compose file and anonymous volumes attached to containers ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_events.md ================================================ --- title: telepresence compose events description: Receive real time events from containers hide_table_of_contents: true --- Receive real time events from containers ### Usage: ``` telepresence compose events [flags] [services] ``` ### Flags: ``` -h, --help help for events ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose events flags: ``` --json Output events as a stream of json objects --since string Show all events created since timestamp --until string Stream events until this timestamp ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_exec.md ================================================ --- title: telepresence compose exec description: Execute a command in a running container hide_table_of_contents: true --- Execute a command in a running container ### Usage: ``` telepresence compose exec [flags] [services] ``` ### Flags: ``` -h, --help help for exec ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose exec flags: ``` -d, --detach Detached mode: Run command in the background -e, --env stringArray Set environment variables --index int Index of the container if service has multiple replicas -T, --no-tty Disable pseudo-TTY allocation. By default 'docker compose exec' allocates a TTY. (default true) --privileged Give extended privileges to the process -u, --user string Run the command as this user -w, --workdir string Path to workdir directory for this command ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_export.md ================================================ --- title: telepresence compose export description: Export a service container's filesystem as a tar archive hide_table_of_contents: true --- Export a service container's filesystem as a tar archive ### Usage: ``` telepresence compose export [flags] [services] ``` ### Flags: ``` -h, --help help for export ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose export flags: ``` --index int index of the container if service has multiple replicas. -o, --output string Write to a file, instead of STDOUT ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_images.md ================================================ --- title: telepresence compose images description: List images used by the created containers hide_table_of_contents: true --- List images used by the created containers ### Usage: ``` telepresence compose images [flags] [services] ``` ### Flags: ``` -h, --help help for images ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose images flags: ``` --format string Format the output. Values: [table | json] (default "table") -q, --quiet Only display IDs ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_kill.md ================================================ --- title: telepresence compose kill description: Force stop service containers hide_table_of_contents: true --- Force stop service containers ### Usage: ``` telepresence compose kill [flags] [services] ``` ### Flags: ``` -h, --help help for kill ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose kill flags: ``` --remove-orphans Remove containers for services not defined in the Compose file -s, --signal string SIGNAL to send to the container (default "\"SIGKILL\"") ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_logs.md ================================================ --- title: telepresence compose logs description: View output from containers hide_table_of_contents: true --- View output from containers ### Usage: ``` telepresence compose logs [flags] [services] ``` ### Flags: ``` -h, --help help for logs ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose logs flags: ``` -f, --follow Follow log output --index int index of the container if service has multiple replicas --no-color Produce monochrome output --no-log-prefix Don't print prefix in logs --since string Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes) -n, --tail string Number of lines to show from the end of the logs for each container (default "all") -t, --timestamps Show timestamps --until string Show logs before a timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes) ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_ls.md ================================================ --- title: telepresence compose ls description: List running compose projects hide_table_of_contents: true --- List running compose projects ### Usage: ``` telepresence compose ls [flags] [services] ``` ### Flags: ``` -h, --help help for ls ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose ls flags: ``` -a, --all Show all stopped Compose projects --filter string Filter output based on conditions provided --format string Format the output. Values: [table | json] (default "table") -q, --quiet Only display project names ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_pause.md ================================================ --- title: telepresence compose pause description: Pause services hide_table_of_contents: true --- Pause services ### Usage: ``` telepresence compose pause [flags] [services] ``` ### Flags: ``` -h, --help help for pause ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose pause flags: ``` ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_port.md ================================================ --- title: telepresence compose port description: Print the public port for a port binding hide_table_of_contents: true --- Print the public port for a port binding ### Usage: ``` telepresence compose port [flags] [services] ``` ### Flags: ``` -h, --help help for port ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose port flags: ``` --index int Index of the container if service has multiple replicas --protocol string tcp or udp (default "\"tcp\"") ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_ps.md ================================================ --- title: telepresence compose ps description: List containers hide_table_of_contents: true --- List containers ### Usage: ``` telepresence compose ps [flags] [services] ``` ### Flags: ``` -h, --help help for ps ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose ps flags: ``` -a, --all Show all stopped containers (including those created by the run command) --filter string Filter services by a property (supported filters: status) --format string Format output using a custom template: 'table': Print output in table format with column headers (default) 'table TEMPLATE': Print output in table format using the given Go template 'json': Print in JSON format 'TEMPLATE': Print output using the given Go template. Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates (default "table") --no-trunc Don't truncate output --orphans Include orphaned services (not declared by project) (default true) -q, --quiet Only display IDs --services Display services --status stringArray Filter services by status. Values: [paused | restarting | removing | running | dead | created | exited] ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_publish.md ================================================ --- title: telepresence compose publish description: Publish compose application hide_table_of_contents: true --- Publish compose application ### Usage: ``` telepresence compose publish [flags] [services] ``` ### Flags: ``` -h, --help help for publish ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose publish flags: ``` --app Published compose application (includes referenced images) --oci-version string OCI image/artifact specification version (automatically determined by default) --resolve-image-digests Pin image tags to digests --with-env Include environment variables in the published OCI artifact -y, --yes Assume "yes" as answer to all prompts ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_pull.md ================================================ --- title: telepresence compose pull description: Pull service images hide_table_of_contents: true --- Pull service images ### Usage: ``` telepresence compose pull [flags] [services] ``` ### Flags: ``` -h, --help help for pull ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose pull flags: ``` --ignore-buildable Ignore images that can be built --ignore-pull-failures Pull what it can and ignores images with pull failures --include-deps Also pull services declared as dependencies --policy string Apply pull policy ("missing"|"always") -q, --quiet Pull without printing progress information ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_push.md ================================================ --- title: telepresence compose push description: Push service images hide_table_of_contents: true --- Push service images ### Usage: ``` telepresence compose push [flags] [services] ``` ### Flags: ``` -h, --help help for push ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose push flags: ``` --ignore-push-failures Push what it can and ignores images with push failures --include-deps Also push images of services declared as dependencies -q, --quiet Push without printing progress information ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_restart.md ================================================ --- title: telepresence compose restart description: Restart service containers hide_table_of_contents: true --- Restart service containers ### Usage: ``` telepresence compose restart [flags] [services] ``` ### Flags: ``` -h, --help help for restart ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose restart flags: ``` --no-deps Don't restart dependent services -t, --timeout int Specify a shutdown timeout in seconds ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_rm.md ================================================ --- title: telepresence compose rm description: Removes stopped service containers hide_table_of_contents: true --- Removes stopped service containers ### Usage: ``` telepresence compose rm [flags] [services] ``` ### Flags: ``` -h, --help help for rm ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose rm flags: ``` -f, --force Don't ask to confirm removal -s, --stop Stop the containers, if required, before removing -v, --volumes Remove any anonymous volumes attached to containers ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_run.md ================================================ --- title: telepresence compose run description: Run a one-off command on a service hide_table_of_contents: true --- Run a one-off command on a service ### Usage: ``` telepresence compose run [flags] [services] ``` ### Flags: ``` -h, --help help for run ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose run flags: ``` --build Build image before starting container --cap-add list Add Linux capabilities --cap-drop list Drop Linux capabilities -d, --detach Run container in background and print container ID --entrypoint string Override the entrypoint of the image -e, --env stringArray Set environment variables --env-from-file stringArray Set environment variables from file -i, --interactive Keep STDIN open even if not attached (default true) -l, --label stringArray Add or override a label --name string Assign a name to the container -T, --no-TTY Disable pseudo-TTY allocation (default: auto-detected) (default true) --no-deps Don't start linked services -p, --publish stringArray Publish a container's port(s) to the host --pull string Pull image before running ("always"|"missing"|"never") (default "policy") -q, --quiet Don't print anything to STDOUT --quiet-build Suppress progress output from the build process --quiet-pull Pull without printing progress information --remove-orphans Remove containers for services not defined in the Compose file --rm Automatically remove the container when it exits -P, --service-ports Run command with all service's ports enabled and mapped to the host --use-aliases Use the service's network useAliases in the network(s) the container connects to -u, --user string Run as specified username or uid -v, --volume stringArray Bind mount a volume -w, --workdir string Working directory inside the container ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_scale.md ================================================ --- title: telepresence compose scale description: Scale services hide_table_of_contents: true --- Scale services ### Usage: ``` telepresence compose scale [flags] [services] ``` ### Flags: ``` -h, --help help for scale ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose scale flags: ``` --no-deps Don't start linked services ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_start.md ================================================ --- title: telepresence compose start description: Start services hide_table_of_contents: true --- Start services ### Usage: ``` telepresence compose start [flags] [services] ``` ### Flags: ``` -h, --help help for start ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose start flags: ``` --wait Wait for services to be running|healthy. Implies detached mode. --wait-timeout int Maximum duration in seconds to wait for the project to be running|healthy ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_stats.md ================================================ --- title: telepresence compose stats description: Display a live stream of container(s) resource usage statistics hide_table_of_contents: true --- Display a live stream of container(s) resource usage statistics ### Usage: ``` telepresence compose stats [flags] [services] ``` ### Flags: ``` -h, --help help for stats ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose stats flags: ``` -a, --all Show all containers (default shows just running) --format string Format output using a custom template: 'table': Print output in table format with column headers (default) 'table TEMPLATE': Print output in table format using the given Go template 'json': Print in JSON format 'TEMPLATE': Print output using the given Go template. Refer to https://docs.docker.com/engine/cli/formatting/ for more information about formatting output with templates --no-stream Disable streaming stats and only pull the first result --no-trunc Do not truncate output ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_stop.md ================================================ --- title: telepresence compose stop description: Stop services hide_table_of_contents: true --- Stop services ### Usage: ``` telepresence compose stop [flags] [services] ``` ### Flags: ``` -h, --help help for stop ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose stop flags: ``` -t, --timeout int Specify a shutdown timeout in seconds ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_top.md ================================================ --- title: telepresence compose top description: Display the running processes hide_table_of_contents: true --- Display the running processes ### Usage: ``` telepresence compose top [flags] [services] ``` ### Flags: ``` -h, --help help for top ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose top flags: ``` ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_unpause.md ================================================ --- title: telepresence compose unpause description: Unpause services hide_table_of_contents: true --- Unpause services ### Usage: ``` telepresence compose unpause [flags] [services] ``` ### Flags: ``` -h, --help help for unpause ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose unpause flags: ``` ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_up.md ================================================ --- title: telepresence compose up description: Create and start containers hide_table_of_contents: true --- Create and start containers ### Usage: ``` telepresence compose up [flags] [services] ``` ### Flags: ``` -h, --help help for up ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose up flags: ``` --abort-on-container-exit Stops all containers if any container was stopped. Incompatible with -d --abort-on-container-failure Stops all containers if any container exited with failure. Incompatible with -d --always-recreate-deps Recreate dependent containers. Incompatible with --no-recreate. --attach stringArray Restrict attaching to the specified services. Incompatible with --attach-dependencies. --attach-dependencies Automatically attach to log output of dependent services --build Build images before starting containers -d, --detach Detached mode: Run containers in the background --exit-code-from string Return the exit code of the selected service container. Implies --abort-on-container-exit --force-recreate Recreate containers even if their configuration and image haven't changed --menu Enable interactive shortcuts when running attached. Incompatible with --detach. Can also be enable/disable by setting COMPOSE_MENU environment var. --no-attach stringArray Do not attach (stream logs) to the specified services --no-build Don't build an image, even if it's policy --no-color Produce monochrome output --no-deps Don't start linked services --no-log-prefix Don't print prefix in logs --no-recreate If containers already exist, don't recreate them. Incompatible with --force-recreate. --no-start Don't start the services after creating them --pull string Pull image before running ("always"|"missing"|"never") (default "policy") --quiet-build Suppress the build output --quiet-pull Pull without printing progress information --remove-orphans Remove containers for services not defined in the Compose file -V, --renew-anon-volumes Recreate anonymous volumes instead of retrieving data from the previous containers --scale uint32 Scale SERVICE to NUM instances. Overrides the scale setting in the Compose file if present. -t, --timeout int Use this timeout in seconds for container shutdown when attached or when containers are already running --timestamps Show timestamps --wait Wait for services to be running|healthy. Implies detached mode. --wait-timeout int Maximum duration in seconds to wait for the project to be running|healthy -w, --watch Watch source code and rebuild/refresh containers when files are updated. -y, --yes Assume "yes" as answer to all prompts and run non-interactively ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_version.md ================================================ --- title: telepresence compose version description: Show the Docker Compose version information hide_table_of_contents: true --- Show the Docker Compose version information ### Usage: ``` telepresence compose version [flags] [services] ``` ### Flags: ``` -h, --help help for version ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose version flags: ``` -f, --format string Format the output. Values: [pretty | json]. (Default: pretty) --short Shows only Compose's version number ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_volumes.md ================================================ --- title: telepresence compose volumes description: List volumes hide_table_of_contents: true --- List volumes ### Usage: ``` telepresence compose volumes [flags] [services] ``` ### Flags: ``` -h, --help help for volumes ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose volumes flags: ``` --format string Format output using a custom template: 'table': Print output in table format with column headers (default) 'table TEMPLATE': Print output in table format using the given Go template 'json': Print in JSON format 'TEMPLATE': Print output using the given Go template. Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates (default "table") -q, --quiet Only display volume names ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_wait.md ================================================ --- title: telepresence compose wait description: Block until containers of all (or specified) services stop. hide_table_of_contents: true --- Block until containers of all (or specified) services stop. ### Usage: ``` telepresence compose wait [flags] [services] ``` ### Flags: ``` -h, --help help for wait ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose wait flags: ``` --down-project Drops project when the first container stops ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_compose_watch.md ================================================ --- title: telepresence compose watch description: Watch build context for service and rebuild/refresh containers when files are updated hide_table_of_contents: true --- Watch build context for service and rebuild/refresh containers when files are updated ### Usage: ``` telepresence compose watch [flags] [services] ``` ### Flags: ``` -h, --help help for watch ``` ### Compose flags: ``` --env-file stringArray Optional environment files -f, --file stringArray Compose configuration files --profile stringArray Profile to enable --project-directory string Specify an alternate working directory (default: the path of the, first specified, Compose file) --project-name string Project name ``` ### Compose watch flags: ``` --no-up Do not build & start services before watching --prune Prune dangling images on rebuild (default true) --quiet hide build output ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_config.md ================================================ --- title: telepresence config description: Telepresence configuration commands hide_table_of_contents: true --- Telepresence configuration commands ### Usage: ``` telepresence config [command] [flags] ``` ### Available Commands: | Command | Description | |---------|-------------| | [view](telepresence_config_view) | View current Telepresence configuration | ### Flags: ``` -h, --help help for config ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` Use `telepresence config [command] --help` for more information about a command. ================================================ FILE: docs/reference/cli/telepresence_config_view.md ================================================ --- title: telepresence config view description: View current Telepresence configuration hide_table_of_contents: true --- View current Telepresence configuration ### Usage: ``` telepresence config view [flags] ``` ### Flags: ``` -c, --client-only Only view config from client file. -h, --help help for view ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_connect.md ================================================ --- title: telepresence connect description: Connect to a cluster hide_table_of_contents: true --- Connect to a cluster ### Usage: ``` telepresence connect [flags] [-- <command to run while connected>] ``` ### Flags: ``` --allow-conflicting-subnets strings Comma separated list of CIDR that will be allowed to conflict with local subnets --also-proxy strings Additional comma separated list of CIDR to proxy --docker Start, or connect to, daemon in a docker container --expose stringArray Port that a containerized daemon will expose. See docker run -p for more info. Can be repeated -h, --help help for connect --hostname string Hostname used by a containerized daemon --manager-namespace string The namespace where the traffic manager is to be found. Overrides any other manager namespace set in config --mapped-namespaces strings Comma separated list of namespaces considered by DNS resolver and NAT for outbound connections. Defaults to all namespaces --name string Optional name to use for the connection -n, --namespace string If present, the namespace scope for this CLI request --never-proxy strings Comma separated list of CIDR to never proxy --proxy-via strings Use Network Address Translation to create virtual IPs for the given CIDR, and route via WORKLOAD. Must be in the form CIDR=WORKLOAD. CIDR can be substituted for the symblic name "service", "pods", "also", or "all". --reroute-local strings Reroute port on local host to remote host. Format is <local port>:<host>:<port>[/{tcp,udp}]. <port> can be symbolic when <host> is a service name. --reroute-remote strings Reroute port on remote host. Format is <host>:<port>:<new port>[/{tcp,udp}]. <port> can be symbolic when <host> is a service name. --vnat strings Use Network Address Translation to create virtual IPs for the given CIDR. CIDR can be substituted for the symblic name "service", "pods", "also", or "all". ``` ### Kubernetes flags: ``` --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. --as-uid string UID to impersonate for the operation. --as-user-extra stringArray User extras to impersonate for the operation, this flag can be repeated to specify multiple values for the same key. --cache-dir string Default cache directory (default "$HOME/.kube/cache") --certificate-authority string Path to a cert file for the certificate authority --client-certificate string Path to a client certificate file for TLS --client-key string Path to a client key file for TLS --cluster string The name of the kubeconfig cluster to use --context string The name of the kubeconfig context to use --disable-compression If true, opt-out of response compression for all requests to the server --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure --kubeconfig string Path to the kubeconfig file to use for CLI requests. --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") -s, --server string The address and port of the Kubernetes API server --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used --token string Bearer token for authentication to the API server --user string The name of the kubeconfig user to use ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_curl.md ================================================ --- title: telepresence curl description: curl with daemon network hide_table_of_contents: true --- curl with daemon network ### Usage: ``` telepresence curl ``` ### Flags: ``` -h, --help help for curl ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_docker-run.md ================================================ --- title: telepresence docker-run description: Docker run with daemon network hide_table_of_contents: true --- Docker run with daemon network ### Usage: ``` telepresence docker-run ``` ### Flags: ``` -h, --help help for docker-run ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_gather-logs.md ================================================ --- title: telepresence gather-logs description: Gather logs from traffic-manager, traffic-agent, user and root daemons, and export them into a zip file. hide_table_of_contents: true --- Gather logs from traffic-manager, traffic-agent, user and root daemons, and export them into a zip file. ## Synopsis: Gather logs from traffic-manager, traffic-agent, user and root daemons, and export them into a zip file. Useful if you are opening a Github issue or asking someone to help you debug Telepresence. ### Usage: ``` telepresence gather-logs [flags] ``` ### Examples: ``` Here are a few examples of how you can use this command: # Get all logs and export to a given file telepresence gather-logs -o /tmp/telepresence_logs.zip # Get all logs and pod yaml manifests for components in the kubernetes cluster telepresence gather-logs -o /tmp/telepresence_logs.zip --get-pod-yaml # Get all logs for the daemons only telepresence gather-logs --traffic-agents=None --traffic-manager=False # Get all logs for pods that have "echo-easy" in the name, useful if you have multiple replicas telepresence gather-logs --traffic-manager=False --traffic-agents=echo-easy # Get all logs for a specific pod telepresence gather-logs --traffic-manager=False --traffic-agents=echo-easy-6848967857-tw4jw # Get logs from everything except the daemons telepresence gather-logs --daemons=None ``` ### Flags: ``` -a, --anonymize To anonymize pod names + namespaces from the logs --daemons string Comma separated list of daemons you want logs from: all, root, user, kubeauth, None (default "all") -y, --get-pod-yaml Get the yaml of any pods you are getting logs for -h, --help help for gather-logs -o, --output-file string The file you want to output the logs to. --traffic-agents string Traffic-agents to collect logs from: all, name substring, None (default "all") --traffic-manager If you want to collect logs from the traffic-manager (default true) ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_genyaml.md ================================================ --- title: telepresence genyaml description: Generate YAML for use in kubernetes manifests. hide_table_of_contents: true --- Generate YAML for use in kubernetes manifests. ## Synopsis: Generate traffic-agent yaml for use in kubernetes manifests. This allows the traffic agent to be injected by hand into existing kubernetes manifests. For your modified workload to be valid, you'll have to manually inject annotations, a container, and a volume into the workload; you can do this by running "genyaml config", "genyaml container", "genyaml initcontainer", "genyaml annotations", and "genyaml volume". NOTE: It is recommended that you not do this unless strictly necessary. Instead, we suggest letting telepresence's webhook injector configure the traffic agents on demand. ### Usage: ``` telepresence genyaml [command] [flags] ``` ### Available Commands: | Command | Description | |---------|-------------| | [annotations](telepresence_genyaml_annotations) | Generate YAML for the pod template metadata annotations. | | [config](telepresence_genyaml_config) | Generate YAML for the agent's entry in the telepresence-agents configmap. | | [container](telepresence_genyaml_container) | Generate YAML for the traffic-agent container. | | [initcontainer](telepresence_genyaml_initcontainer) | Generate YAML for the traffic-agent init container. | | [volume](telepresence_genyaml_volume) | Generate YAML for the traffic-agent volume. | ### Flags: ``` -h, --help help for genyaml -o, --output string Path to the file to place the output in. Defaults to '-' which means stdout. (default "-") ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` Use `telepresence genyaml [command] --help` for more information about a command. ================================================ FILE: docs/reference/cli/telepresence_genyaml_annotations.md ================================================ --- title: telepresence genyaml annotations description: Generate YAML for the pod template metadata annotations. hide_table_of_contents: true --- Generate YAML for the pod template metadata annotations. ## Synopsis: Generate YAML for the pod template metadata annotations. See genyaml for more info on what this means ### Usage: ``` telepresence genyaml annotations [flags] ``` ### Flags: ``` -a, --agent string Path to the yaml containing the generated agent config -h, --help help for annotations -o, --output string Path to the file to place the output in. Defaults to '-' which means stdout. (default "-") ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_genyaml_config.md ================================================ --- title: telepresence genyaml config description: Generate YAML for the agent's entry in the telepresence-agents configmap. hide_table_of_contents: true --- Generate YAML for the agent's entry in the telepresence-agents configmap. ## Synopsis: Generate YAML for the agent's entry in the telepresence-agents configmap. See genyaml for more info on what this means ### Usage: ``` telepresence genyaml config [flags] ``` ### Flags: ``` --agent-image string The qualified name of the agent image (default "ghcr.io/telepresenceio/tel2:<current version>") --agent-port uint16 The port number you wish the agent to listen on. (default 9900) -h, --help help for config -i, --input string Path to the yaml containing the workload definition (i.e. Deployment, StatefulSet, etc). Pass '-' for stdin.. Mutually exclusive to --workload --loglevel The loglevel for the generated traffic-agent sidecar (default INFO) --manager-namespace string The traffic-manager namespace (default "ambassador") --manager-port uint16 The traffic-manager API port (default 8081) -n, --namespace string If present, the namespace scope for this CLI request -o, --output string Path to the file to place the output in. Defaults to '-' which means stdout. (default "-") -w, --workload string Name of the workload. If given, the workload will be retrieved from the cluster, mutually exclusive to --input ``` ### Kubernetes flags: ``` --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. --as-uid string UID to impersonate for the operation. --as-user-extra stringArray User extras to impersonate for the operation, this flag can be repeated to specify multiple values for the same key. --cache-dir string Default cache directory (default "$HOME/.kube/cache") --certificate-authority string Path to a cert file for the certificate authority --client-certificate string Path to a client certificate file for TLS --client-key string Path to a client key file for TLS --cluster string The name of the kubeconfig cluster to use --context string The name of the kubeconfig context to use --disable-compression If true, opt-out of response compression for all requests to the server --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure --kubeconfig string Path to the kubeconfig file to use for CLI requests. --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") -s, --server string The address and port of the Kubernetes API server --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used --token string Bearer token for authentication to the API server --user string The name of the kubeconfig user to use ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_genyaml_container.md ================================================ --- title: telepresence genyaml container description: Generate YAML for the traffic-agent container. hide_table_of_contents: true --- Generate YAML for the traffic-agent container. ## Synopsis: Generate YAML for the traffic-agent container. See genyaml for more info on what this means ### Usage: ``` telepresence genyaml container [flags] ``` ### Flags: ``` -a, --agent string Path to the yaml containing the generated agent config -h, --help help for container -i, --input string Optional path to the yaml containing the workload definition (i.e. Deployment, StatefulSet, etc). Pass '-' for stdin. Loaded from cluster by default -n, --namespace string If present, the namespace scope for this CLI request -o, --output string Path to the file to place the output in. Defaults to '-' which means stdout. (default "-") ``` ### Kubernetes flags: ``` --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. --as-uid string UID to impersonate for the operation. --as-user-extra stringArray User extras to impersonate for the operation, this flag can be repeated to specify multiple values for the same key. --cache-dir string Default cache directory (default "$HOME/.kube/cache") --certificate-authority string Path to a cert file for the certificate authority --client-certificate string Path to a client certificate file for TLS --client-key string Path to a client key file for TLS --cluster string The name of the kubeconfig cluster to use --context string The name of the kubeconfig context to use --disable-compression If true, opt-out of response compression for all requests to the server --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure --kubeconfig string Path to the kubeconfig file to use for CLI requests. --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") -s, --server string The address and port of the Kubernetes API server --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used --token string Bearer token for authentication to the API server --user string The name of the kubeconfig user to use ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_genyaml_initcontainer.md ================================================ --- title: telepresence genyaml initcontainer description: Generate YAML for the traffic-agent init container. hide_table_of_contents: true --- Generate YAML for the traffic-agent init container. ## Synopsis: Generate YAML for the traffic-agent init container. See genyaml for more info on what this means ### Usage: ``` telepresence genyaml initcontainer [flags] ``` ### Flags: ``` -a, --agent string Path to the yaml containing the generated agent config -h, --help help for initcontainer -n, --namespace string If present, the namespace scope for this CLI request -o, --output string Path to the file to place the output in. Defaults to '-' which means stdout. (default "-") ``` ### Kubernetes flags: ``` --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. --as-uid string UID to impersonate for the operation. --as-user-extra stringArray User extras to impersonate for the operation, this flag can be repeated to specify multiple values for the same key. --cache-dir string Default cache directory (default "$HOME/.kube/cache") --certificate-authority string Path to a cert file for the certificate authority --client-certificate string Path to a client certificate file for TLS --client-key string Path to a client key file for TLS --cluster string The name of the kubeconfig cluster to use --context string The name of the kubeconfig context to use --disable-compression If true, opt-out of response compression for all requests to the server --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure --kubeconfig string Path to the kubeconfig file to use for CLI requests. --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") -s, --server string The address and port of the Kubernetes API server --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used --token string Bearer token for authentication to the API server --user string The name of the kubeconfig user to use ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_genyaml_volume.md ================================================ --- title: telepresence genyaml volume description: Generate YAML for the traffic-agent volume. hide_table_of_contents: true --- Generate YAML for the traffic-agent volume. ## Synopsis: Generate YAML for the traffic-agent volume. See genyaml for more info on what this means ### Usage: ``` telepresence genyaml volume [flags] ``` ### Flags: ``` -a, --agent string Path to the yaml containing the generated agent config -h, --help help for volume -i, --input string Optional path to the yaml containing the workload definition (i.e. Deployment, StatefulSet, etc). Pass '-' for stdin. Loaded from cluster by default -n, --namespace string If present, the namespace scope for this CLI request -o, --output string Path to the file to place the output in. Defaults to '-' which means stdout. (default "-") ``` ### Kubernetes flags: ``` --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. --as-uid string UID to impersonate for the operation. --as-user-extra stringArray User extras to impersonate for the operation, this flag can be repeated to specify multiple values for the same key. --cache-dir string Default cache directory (default "$HOME/.kube/cache") --certificate-authority string Path to a cert file for the certificate authority --client-certificate string Path to a client certificate file for TLS --client-key string Path to a client key file for TLS --cluster string The name of the kubeconfig cluster to use --context string The name of the kubeconfig context to use --disable-compression If true, opt-out of response compression for all requests to the server --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure --kubeconfig string Path to the kubeconfig file to use for CLI requests. --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") -s, --server string The address and port of the Kubernetes API server --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used --token string Bearer token for authentication to the API server --user string The name of the kubeconfig user to use ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_helm.md ================================================ --- title: telepresence helm description: Helm commands using the embedded Telepresence Helm chart. hide_table_of_contents: true --- Helm commands using the embedded Telepresence Helm chart. ### Usage: ``` telepresence helm [command] [flags] ``` ### Available Commands: | Command | Description | |---------|-------------| | [install](telepresence_helm_install) | Install telepresence traffic manager | | [lint](telepresence_helm_lint) | Verify the embedded telepresence Helm chart | | [uninstall](telepresence_helm_uninstall) | Uninstall telepresence traffic manager | | [upgrade](telepresence_helm_upgrade) | Upgrade telepresence traffic manager | | [version](telepresence_helm_version) | Print the version of the Helm client | ### Flags: ``` -h, --help help for helm ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` Use `telepresence helm [command] --help` for more information about a command. ================================================ FILE: docs/reference/cli/telepresence_helm_install.md ================================================ --- title: telepresence helm install description: Install telepresence traffic manager hide_table_of_contents: true --- Install telepresence traffic manager ### Usage: ``` telepresence helm install [flags] ``` ### Flags: ``` --allow-conflicting-subnets strings Comma separated list of CIDR that will be allowed to conflict with local subnets --also-proxy strings Additional comma separated list of CIDR to proxy --create-namespace create a namespace for the traffic-manager if not present (default true) --docker Start, or connect to, daemon in a docker container --expose stringArray Port that a containerized daemon will expose. See docker run -p for more info. Can be repeated -h, --help help for install --hostname string Hostname used by a containerized daemon --manager-namespace string The namespace where the traffic manager is to be found. Overrides any other manager namespace set in config --mapped-namespaces strings Comma separated list of namespaces considered by DNS resolver and NAT for outbound connections. Defaults to all namespaces --name string Optional name to use for the connection -n, --namespace string If present, the namespace scope for this CLI request --never-proxy strings Comma separated list of CIDR to never proxy --no-hooks prevent hooks from running during install --proxy-via strings Use Network Address Translation to create virtual IPs for the given CIDR, and route via WORKLOAD. Must be in the form CIDR=WORKLOAD. CIDR can be substituted for the symblic name "service", "pods", "also", or "all". --reroute-local strings Reroute port on local host to remote host. Format is <local port>:<host>:<port>[/{tcp,udp}]. <port> can be symbolic when <host> is a service name. --reroute-remote strings Reroute port on remote host. Format is <host>:<port>:<new port>[/{tcp,udp}]. <port> can be symbolic when <host> is a service name. --set stringArray specify a value as a.b=v (can specify multiple or separate values with commas: a.b=v1,a.c=v2) --set-file stringArray set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) --set-json stringArray set JSON values on the command line (can specify multiple or separate values with commas: a.b=jsonval1,a.c=jsonval2) --set-string stringArray set STRING values on the command line (can specify multiple or separate values with commas: a.b=val1,a.c=val2) -f, --values stringArray specify values in a YAML file or a URL (can specify multiple) --version string the telepresence version if different from the client's version. May be a range (e.g. ^2.21.0) --vnat strings Use Network Address Translation to create virtual IPs for the given CIDR. CIDR can be substituted for the symblic name "service", "pods", "also", or "all". ``` ### Kubernetes flags: ``` --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. --as-uid string UID to impersonate for the operation. --as-user-extra stringArray User extras to impersonate for the operation, this flag can be repeated to specify multiple values for the same key. --cache-dir string Default cache directory (default "$HOME/.kube/cache") --certificate-authority string Path to a cert file for the certificate authority --client-certificate string Path to a client certificate file for TLS --client-key string Path to a client key file for TLS --cluster string The name of the kubeconfig cluster to use --context string The name of the kubeconfig context to use --disable-compression If true, opt-out of response compression for all requests to the server --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure --kubeconfig string Path to the kubeconfig file to use for CLI requests. --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") -s, --server string The address and port of the Kubernetes API server --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used --token string Bearer token for authentication to the API server --user string The name of the kubeconfig user to use ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_helm_lint.md ================================================ --- title: telepresence helm lint description: Verify the embedded telepresence Helm chart hide_table_of_contents: true --- Verify the embedded telepresence Helm chart ### Usage: ``` telepresence helm lint [flags] ``` ### Flags: ``` --allow-conflicting-subnets strings Comma separated list of CIDR that will be allowed to conflict with local subnets --also-proxy strings Additional comma separated list of CIDR to proxy --docker Start, or connect to, daemon in a docker container --expose stringArray Port that a containerized daemon will expose. See docker run -p for more info. Can be repeated -h, --help help for lint --hostname string Hostname used by a containerized daemon --manager-namespace string The namespace where the traffic manager is to be found. Overrides any other manager namespace set in config --mapped-namespaces strings Comma separated list of namespaces considered by DNS resolver and NAT for outbound connections. Defaults to all namespaces --name string Optional name to use for the connection -n, --namespace string If present, the namespace scope for this CLI request --never-proxy strings Comma separated list of CIDR to never proxy --proxy-via strings Use Network Address Translation to create virtual IPs for the given CIDR, and route via WORKLOAD. Must be in the form CIDR=WORKLOAD. CIDR can be substituted for the symblic name "service", "pods", "also", or "all". --reroute-local strings Reroute port on local host to remote host. Format is <local port>:<host>:<port>[/{tcp,udp}]. <port> can be symbolic when <host> is a service name. --reroute-remote strings Reroute port on remote host. Format is <host>:<port>:<new port>[/{tcp,udp}]. <port> can be symbolic when <host> is a service name. --set stringArray specify a value as a.b=v (can specify multiple or separate values with commas: a.b=v1,a.c=v2) --set-file stringArray set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) --set-json stringArray set JSON values on the command line (can specify multiple or separate values with commas: a.b=jsonval1,a.c=jsonval2) --set-string stringArray set STRING values on the command line (can specify multiple or separate values with commas: a.b=val1,a.c=val2) -f, --values stringArray specify values in a YAML file or a URL (can specify multiple) --version string the telepresence version if different from the client's version. May be a range (e.g. ^2.21.0) --vnat strings Use Network Address Translation to create virtual IPs for the given CIDR. CIDR can be substituted for the symblic name "service", "pods", "also", or "all". ``` ### Kubernetes flags: ``` --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. --as-uid string UID to impersonate for the operation. --as-user-extra stringArray User extras to impersonate for the operation, this flag can be repeated to specify multiple values for the same key. --cache-dir string Default cache directory (default "$HOME/.kube/cache") --certificate-authority string Path to a cert file for the certificate authority --client-certificate string Path to a client certificate file for TLS --client-key string Path to a client key file for TLS --cluster string The name of the kubeconfig cluster to use --context string The name of the kubeconfig context to use --disable-compression If true, opt-out of response compression for all requests to the server --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure --kubeconfig string Path to the kubeconfig file to use for CLI requests. --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") -s, --server string The address and port of the Kubernetes API server --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used --token string Bearer token for authentication to the API server --user string The name of the kubeconfig user to use ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_helm_uninstall.md ================================================ --- title: telepresence helm uninstall description: Uninstall telepresence traffic manager hide_table_of_contents: true --- Uninstall telepresence traffic manager ### Usage: ``` telepresence helm uninstall [flags] ``` ### Flags: ``` --allow-conflicting-subnets strings Comma separated list of CIDR that will be allowed to conflict with local subnets --also-proxy strings Additional comma separated list of CIDR to proxy --docker Start, or connect to, daemon in a docker container --expose stringArray Port that a containerized daemon will expose. See docker run -p for more info. Can be repeated -h, --help help for uninstall --hostname string Hostname used by a containerized daemon --manager-namespace string The namespace where the traffic manager is to be found. Overrides any other manager namespace set in config --mapped-namespaces strings Comma separated list of namespaces considered by DNS resolver and NAT for outbound connections. Defaults to all namespaces --name string Optional name to use for the connection -n, --namespace string If present, the namespace scope for this CLI request --never-proxy strings Comma separated list of CIDR to never proxy --no-hooks prevent hooks from running during uninstallation --proxy-via strings Use Network Address Translation to create virtual IPs for the given CIDR, and route via WORKLOAD. Must be in the form CIDR=WORKLOAD. CIDR can be substituted for the symblic name "service", "pods", "also", or "all". --reroute-local strings Reroute port on local host to remote host. Format is <local port>:<host>:<port>[/{tcp,udp}]. <port> can be symbolic when <host> is a service name. --reroute-remote strings Reroute port on remote host. Format is <host>:<port>:<new port>[/{tcp,udp}]. <port> can be symbolic when <host> is a service name. --vnat strings Use Network Address Translation to create virtual IPs for the given CIDR. CIDR can be substituted for the symblic name "service", "pods", "also", or "all". ``` ### Kubernetes flags: ``` --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. --as-uid string UID to impersonate for the operation. --as-user-extra stringArray User extras to impersonate for the operation, this flag can be repeated to specify multiple values for the same key. --cache-dir string Default cache directory (default "$HOME/.kube/cache") --certificate-authority string Path to a cert file for the certificate authority --client-certificate string Path to a client certificate file for TLS --client-key string Path to a client key file for TLS --cluster string The name of the kubeconfig cluster to use --context string The name of the kubeconfig context to use --disable-compression If true, opt-out of response compression for all requests to the server --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure --kubeconfig string Path to the kubeconfig file to use for CLI requests. --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") -s, --server string The address and port of the Kubernetes API server --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used --token string Bearer token for authentication to the API server --user string The name of the kubeconfig user to use ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_helm_upgrade.md ================================================ --- title: telepresence helm upgrade description: Upgrade telepresence traffic manager hide_table_of_contents: true --- Upgrade telepresence traffic manager ### Usage: ``` telepresence helm upgrade [flags] ``` ### Flags: ``` --allow-conflicting-subnets strings Comma separated list of CIDR that will be allowed to conflict with local subnets --also-proxy strings Additional comma separated list of CIDR to proxy --create-namespace create the release namespace if not present (default true) --docker Start, or connect to, daemon in a docker container --expose stringArray Port that a containerized daemon will expose. See docker run -p for more info. Can be repeated -h, --help help for upgrade --hostname string Hostname used by a containerized daemon --manager-namespace string The namespace where the traffic manager is to be found. Overrides any other manager namespace set in config --mapped-namespaces strings Comma separated list of namespaces considered by DNS resolver and NAT for outbound connections. Defaults to all namespaces --name string Optional name to use for the connection -n, --namespace string If present, the namespace scope for this CLI request --never-proxy strings Comma separated list of CIDR to never proxy --no-hooks disable pre/post upgrade hooks --proxy-via strings Use Network Address Translation to create virtual IPs for the given CIDR, and route via WORKLOAD. Must be in the form CIDR=WORKLOAD. CIDR can be substituted for the symblic name "service", "pods", "also", or "all". --reroute-local strings Reroute port on local host to remote host. Format is <local port>:<host>:<port>[/{tcp,udp}]. <port> can be symbolic when <host> is a service name. --reroute-remote strings Reroute port on remote host. Format is <host>:<port>:<new port>[/{tcp,udp}]. <port> can be symbolic when <host> is a service name. --reset-values when upgrading, reset the values to the ones built into the chart --reuse-values when upgrading, reuse the last release's values and merge in any overrides from the command line via --set and -f --set stringArray specify a value as a.b=v (can specify multiple or separate values with commas: a.b=v1,a.c=v2) --set-file stringArray set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) --set-json stringArray set JSON values on the command line (can specify multiple or separate values with commas: a.b=jsonval1,a.c=jsonval2) --set-string stringArray set STRING values on the command line (can specify multiple or separate values with commas: a.b=val1,a.c=val2) -f, --values stringArray specify values in a YAML file or a URL (can specify multiple) --version string the telepresence version if different from the client's version. May be a range (e.g. ^2.21.0) --vnat strings Use Network Address Translation to create virtual IPs for the given CIDR. CIDR can be substituted for the symblic name "service", "pods", "also", or "all". ``` ### Kubernetes flags: ``` --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. --as-uid string UID to impersonate for the operation. --as-user-extra stringArray User extras to impersonate for the operation, this flag can be repeated to specify multiple values for the same key. --cache-dir string Default cache directory (default "$HOME/.kube/cache") --certificate-authority string Path to a cert file for the certificate authority --client-certificate string Path to a client certificate file for TLS --client-key string Path to a client key file for TLS --cluster string The name of the kubeconfig cluster to use --context string The name of the kubeconfig context to use --disable-compression If true, opt-out of response compression for all requests to the server --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure --kubeconfig string Path to the kubeconfig file to use for CLI requests. --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") -s, --server string The address and port of the Kubernetes API server --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used --token string Bearer token for authentication to the API server --user string The name of the kubeconfig user to use ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_helm_version.md ================================================ --- title: telepresence helm version description: Print the version of the Helm client hide_table_of_contents: true --- Print the version of the Helm client ### Usage: ``` telepresence helm version [flags] ``` ### Flags: ``` -h, --help help for version ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_ingest.md ================================================ --- title: telepresence ingest description: Ingest a container hide_table_of_contents: true --- Ingest a container ### Usage: ``` telepresence ingest [flags] <name> [-- [[docker run flags] <image name>] OR [<command>]] args...] ``` ### Flags: ``` -c, --container string Name of container that provides the environment and mounts for the ingest --docker-build string Build a Docker container from the given docker-context (path or URL), and run it with ingested environment and volume mounts, by passing arguments after -- to 'docker run', e.g. '--docker-build /path/to/docker/context -- -it IMAGE /bin/bash' --docker-build-opt stringArray Options to docker-build in the form key=value, e.g. --docker-build-opt tag=mytag. --docker-debug string Like --docker-build, but allows a debugger to run inside the container with relaxed security --docker-mount string The volume mount point in docker. Defaults to same as "--mount" --docker-run Run a Docker container with ingested environment, volume mount, by passing arguments after -- to 'docker run', e.g. '--docker-run -- -it --rm ubuntu:20.04 /bin/bash' -e, --env-file string Also emit the remote environment to an file. The syntax used in the file can be determined using flag --env-syntax -j, --env-json string Also emit the remote environment to a file as a JSON blob. --env-syntax string Syntax used for env-file. One of "docker", "compose", "sh", "csh", "cmd", "json", and "ps"; where "sh", "csh", and "ps" can be suffixed with ":export" (default "docker") -h, --help help for ingest --local-mount-port uint16 Do not mount remote directories. Instead, expose this port on localhost to an external mounter --mount string The absolute path for the root directory where volumes will be mounted, $TELEPRESENCE_ROOT. Use "true" to have Telepresence pick a random mount point (default). Use "false" to disable filesystem mounting entirely. (default "true") --to-pod strings An additional port to forward from the ingested pod, will be made available at localhost:PORT Use this to, for example, access proxy/helper sidecars in the ingested pod. The default protocol is TCP. Use <port>/UDP for UDP ports --wait-message string Message to print when ingest handler has started ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_intercept.md ================================================ --- title: telepresence intercept description: Intercept a service hide_table_of_contents: true --- Intercept a service ### Usage: ``` telepresence intercept [flags] <name> [-- [[docker run flags] <image name>] OR [<command>]] args...] ``` ### Flags: ``` --address string Local address to forward to, e.g. '--address 10.0.0.2' (default "127.0.0.1" or name of container) --container string Name of container that provides the environment and mounts for the intercept. Defaults to the container matching the first intercepted port. --detailed-output Provide very detailed info about the intercept when used together with --output=json or --output=yaml' --docker-build string Build a Docker container from the given docker-context (path or URL), and run it with intercepted environment and volume mounts, by passing arguments after -- to 'docker run', e.g. '--docker-build /path/to/docker/context -- -it IMAGE /bin/bash' --docker-build-opt stringArray Options to docker-build in the form key=value, e.g. --docker-build-opt tag=mytag. --docker-debug string Like --docker-build, but allows a debugger to run inside the container with relaxed security --docker-mount string The volume mount point in docker. Defaults to same as "--mount" --docker-run Run a Docker container with intercepted environment, volume mount, by passing arguments after -- to 'docker run', e.g. '--docker-run -- -it --rm ubuntu:20.04 /bin/bash' -e, --env-file string Also emit the remote environment to an file. The syntax used in the file can be determined using flag --env-syntax -j, --env-json string Also emit the remote environment to a file as a JSON blob. --env-syntax string Syntax used for env-file. One of "docker", "compose", "sh", "csh", "cmd", "json", and "ps"; where "sh", "csh", and "ps" can be suffixed with ":export" (default "docker") -h, --help help for intercept --http-header strings HTTP header filters. Only requests with matching headers will be intercepted. Supports both formats: --http-header "X-User-ID=dev123" or --http-header "X-User-ID: dev123" (curl -H compatible). Multiple headers use AND logic. --http-path-equal strings HTTP path filters. Only requests with matching paths will be intercepted. Exact path matching. --http-path-prefix strings HTTP path prefix filters. Only requests with matching path prefixes will be intercepted. --http-path-regex strings HTTP path regex filters. Only requests with paths matching the regex will be intercepted. --local-mount-port uint16 Do not mount remote directories. Instead, expose this port on localhost to an external mounter --mechanism mechanism Which extension mechanism to use (default "tcp") --metadata strings Metadata to attach to the intercept. Use --metadata key=value to set a single key/value pair, or --metadata key1=value1 --metadata key2=value2 to set multiple key/value pairs. The metadata can be retrieved using the Telepresence API server. --mount string The absolute path for the root directory where volumes will be mounted, $TELEPRESENCE_ROOT. Use "true" to have Telepresence pick a random mount point (default). Use "false" to disable filesystem mounting entirely. Append ":ro" to mount everything read-only. (default "true") --plaintext Use plaintext instead of TLS when communicating with the intercept handler -p, --port strings Local ports to forward to. Use <local port>:<identifier> to uniquely identify service ports, where the <identifier> is the port name or number. With --docker-run and a daemon that doesn't run in docker', use <local port>:<container port> or <local port>:<container port>:<identifier>. --replace Indicates if the traffic-agent should replace application containers in workload pods. The default behavior is for the agent sidecar to be installed alongside existing containers. (DEPRECATED: Use the replace command.) --service string Optional name of service to intercept. Sometimes needed to uniquely identify the intercepted port. --to-pod strings Additional ports to forward to the intercepted pod, will available for connections to localhost:PORT. Use this to, for example, access proxy/helper sidecars in the intercepted pod. The default protocol is TCP. Use <port>/UDP for UDP ports --wait-message string Message to print when intercept handler has started -w, --workload string Name of workload (Deployment, ReplicaSet, StatefulSet, Rollout) to intercept, if different from <name> ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_leave.md ================================================ --- title: telepresence leave description: Remove existing intercept hide_table_of_contents: true --- Remove existing intercept ### Usage: ``` telepresence leave [flags] <intercept_name> ``` ### Flags: ``` -c, --container string Container name -h, --help help for leave ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_list-contexts.md ================================================ --- title: telepresence list-contexts description: Show all contexts hide_table_of_contents: true --- Show all contexts ### Usage: ``` telepresence list-contexts [flags] ``` ### Flags: ``` --allow-conflicting-subnets strings Comma separated list of CIDR that will be allowed to conflict with local subnets --also-proxy strings Additional comma separated list of CIDR to proxy --docker Start, or connect to, daemon in a docker container --expose stringArray Port that a containerized daemon will expose. See docker run -p for more info. Can be repeated -h, --help help for list-contexts --hostname string Hostname used by a containerized daemon --manager-namespace string The namespace where the traffic manager is to be found. Overrides any other manager namespace set in config --mapped-namespaces strings Comma separated list of namespaces considered by DNS resolver and NAT for outbound connections. Defaults to all namespaces --name string Optional name to use for the connection -n, --namespace string If present, the namespace scope for this CLI request --never-proxy strings Comma separated list of CIDR to never proxy --proxy-via strings Use Network Address Translation to create virtual IPs for the given CIDR, and route via WORKLOAD. Must be in the form CIDR=WORKLOAD. CIDR can be substituted for the symblic name "service", "pods", "also", or "all". --reroute-local strings Reroute port on local host to remote host. Format is <local port>:<host>:<port>[/{tcp,udp}]. <port> can be symbolic when <host> is a service name. --reroute-remote strings Reroute port on remote host. Format is <host>:<port>:<new port>[/{tcp,udp}]. <port> can be symbolic when <host> is a service name. --vnat strings Use Network Address Translation to create virtual IPs for the given CIDR. CIDR can be substituted for the symblic name "service", "pods", "also", or "all". ``` ### Kubernetes flags: ``` --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. --as-uid string UID to impersonate for the operation. --as-user-extra stringArray User extras to impersonate for the operation, this flag can be repeated to specify multiple values for the same key. --cache-dir string Default cache directory (default "$HOME/.kube/cache") --certificate-authority string Path to a cert file for the certificate authority --client-certificate string Path to a client certificate file for TLS --client-key string Path to a client key file for TLS --cluster string The name of the kubeconfig cluster to use --context string The name of the kubeconfig context to use --disable-compression If true, opt-out of response compression for all requests to the server --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure --kubeconfig string Path to the kubeconfig file to use for CLI requests. --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") -s, --server string The address and port of the Kubernetes API server --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used --token string Bearer token for authentication to the API server --user string The name of the kubeconfig user to use ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_list-namespaces.md ================================================ --- title: telepresence list-namespaces description: Show all namespaces hide_table_of_contents: true --- Show all namespaces ### Usage: ``` telepresence list-namespaces [flags] ``` ### Flags: ``` --allow-conflicting-subnets strings Comma separated list of CIDR that will be allowed to conflict with local subnets --also-proxy strings Additional comma separated list of CIDR to proxy --docker Start, or connect to, daemon in a docker container --expose stringArray Port that a containerized daemon will expose. See docker run -p for more info. Can be repeated -h, --help help for list-namespaces --hostname string Hostname used by a containerized daemon --manager-namespace string The namespace where the traffic manager is to be found. Overrides any other manager namespace set in config --mapped-namespaces strings Comma separated list of namespaces considered by DNS resolver and NAT for outbound connections. Defaults to all namespaces --name string Optional name to use for the connection -n, --namespace string If present, the namespace scope for this CLI request --never-proxy strings Comma separated list of CIDR to never proxy --proxy-via strings Use Network Address Translation to create virtual IPs for the given CIDR, and route via WORKLOAD. Must be in the form CIDR=WORKLOAD. CIDR can be substituted for the symblic name "service", "pods", "also", or "all". --reroute-local strings Reroute port on local host to remote host. Format is <local port>:<host>:<port>[/{tcp,udp}]. <port> can be symbolic when <host> is a service name. --reroute-remote strings Reroute port on remote host. Format is <host>:<port>:<new port>[/{tcp,udp}]. <port> can be symbolic when <host> is a service name. --vnat strings Use Network Address Translation to create virtual IPs for the given CIDR. CIDR can be substituted for the symblic name "service", "pods", "also", or "all". ``` ### Kubernetes flags: ``` --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. --as-uid string UID to impersonate for the operation. --as-user-extra stringArray User extras to impersonate for the operation, this flag can be repeated to specify multiple values for the same key. --cache-dir string Default cache directory (default "$HOME/.kube/cache") --certificate-authority string Path to a cert file for the certificate authority --client-certificate string Path to a client certificate file for TLS --client-key string Path to a client key file for TLS --cluster string The name of the kubeconfig cluster to use --context string The name of the kubeconfig context to use --disable-compression If true, opt-out of response compression for all requests to the server --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure --kubeconfig string Path to the kubeconfig file to use for CLI requests. --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") -s, --server string The address and port of the Kubernetes API server --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used --token string Bearer token for authentication to the API server --user string The name of the kubeconfig user to use ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_list.md ================================================ --- title: telepresence list description: List current intercepts hide_table_of_contents: true --- List current intercepts ### Usage: ``` telepresence list [flags] ``` ### Flags: ``` -a, --agents with installed agents only --debug include debugging information -h, --help help for list -g, --ingests ingests -i, --intercepts intercepts -n, --namespace string If present, the namespace scope for this CLI request -r, --replacements replacements -t, --wiretaps wiretaps ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_loglevel.md ================================================ --- title: telepresence loglevel description: Temporarily change the log-level of the traffic-manager, traffic-agent, and user and root daemons hide_table_of_contents: true --- Temporarily change the log-level of the traffic-manager, traffic-agent, and user and root daemons ### Usage: ``` telepresence loglevel <ERROR,WARN,INFO,DEBUG,TRACE> [flags] ``` ### Flags: ``` -d, --duration duration The time that the log-level will be in effect (0s means indefinitely) (default 30m0s) -h, --help help for loglevel -l, --local-only Only affect the user and root daemons -r, --remote-only Only affect the traffic-manager and traffic-agents ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_mcp.md ================================================ --- title: telepresence mcp description: MCP server management hide_table_of_contents: true --- MCP server management ## Synopsis: Manage MCP servers for AI assistants and code editors ### Usage: ``` telepresence mcp [command] [flags] ``` ### Available Commands: | Command | Description | |---------|-------------| | [claude](telepresence_mcp_claude) | Manage Claude Desktop MCP servers | | [cursor](telepresence_mcp_cursor) | Manage Cursor MCP servers | | [start](telepresence_mcp_start) | Start the MCP server | | [stream](telepresence_mcp_stream) | Stream the MCP server over HTTP | | [tools](telepresence_mcp_tools) | Export tools as JSON | | [vscode](telepresence_mcp_vscode) | Manage VSCode MCP servers | ### Flags: ``` -h, --help help for mcp ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` Use `telepresence mcp [command] --help` for more information about a command. ================================================ FILE: docs/reference/cli/telepresence_mcp_claude.md ================================================ --- title: telepresence mcp claude description: Manage Claude Desktop MCP servers hide_table_of_contents: true --- Manage Claude Desktop MCP servers ## Synopsis: Manage MCP server configuration for Claude Desktop ### Usage: ``` telepresence mcp claude [command] [flags] ``` ### Available Commands: | Command | Description | |---------|-------------| | [disable](telepresence_mcp_claude_disable) | Remove server from Claude config | | [enable](telepresence_mcp_claude_enable) | Add server to Claude config | | [list](telepresence_mcp_claude_list) | Show Claude MCP servers | ### Flags: ``` -h, --help help for claude ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` Use `telepresence mcp claude [command] --help` for more information about a command. ================================================ FILE: docs/reference/cli/telepresence_mcp_claude_disable.md ================================================ --- title: telepresence mcp claude disable description: Remove server from Claude config hide_table_of_contents: true --- Remove server from Claude config ## Synopsis: Remove this application from Claude Desktop MCP servers ### Usage: ``` telepresence mcp claude disable [flags] ``` ### Flags: ``` --config-path string Path to Claude config file -h, --help help for disable --server-name string Name of the MCP server to remove (default: derived from executable name) ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_mcp_claude_enable.md ================================================ --- title: telepresence mcp claude enable description: Add server to Claude config hide_table_of_contents: true --- Add server to Claude config ## Synopsis: Add this application as an MCP server in Claude Desktop ### Usage: ``` telepresence mcp claude enable [flags] ``` ### Flags: ``` --config-path string Path to Claude config file -e, --env stringToString Environment variables (e.g., --env KEY1=value1 --env KEY2=value2) (default []) -h, --help help for enable --log-level string Log level (debug, info, warn, error) --server-name string Name for the MCP server (default: derived from executable name) ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_mcp_claude_list.md ================================================ --- title: telepresence mcp claude list description: Show Claude MCP servers hide_table_of_contents: true --- Show Claude MCP servers ## Synopsis: Show all MCP servers configured in Claude Desktop ### Usage: ``` telepresence mcp claude list [flags] ``` ### Flags: ``` --config-path string Path to Claude config file -h, --help help for list ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_mcp_cursor.md ================================================ --- title: telepresence mcp cursor description: Manage Cursor MCP servers hide_table_of_contents: true --- Manage Cursor MCP servers ## Synopsis: Manage MCP server configuration for Cursor ### Usage: ``` telepresence mcp cursor [command] [flags] ``` ### Available Commands: | Command | Description | |---------|-------------| | [disable](telepresence_mcp_cursor_disable) | Remove server from Cursor config | | [enable](telepresence_mcp_cursor_enable) | Add server to Cursor config | | [list](telepresence_mcp_cursor_list) | Show Cursor MCP servers | ### Flags: ``` -h, --help help for cursor ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` Use `telepresence mcp cursor [command] --help` for more information about a command. ================================================ FILE: docs/reference/cli/telepresence_mcp_cursor_disable.md ================================================ --- title: telepresence mcp cursor disable description: Remove server from Cursor config hide_table_of_contents: true --- Remove server from Cursor config ## Synopsis: Remove this application from Cursor MCP servers ### Usage: ``` telepresence mcp cursor disable [flags] ``` ### Flags: ``` --config-path string Path to Cursor config file -h, --help help for disable --server-name string Name of the MCP server to remove (default: derived from executable name) --workspace Remove from workspace settings (.cursor/mcp.json) instead of user settings ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_mcp_cursor_enable.md ================================================ --- title: telepresence mcp cursor enable description: Add server to Cursor config hide_table_of_contents: true --- Add server to Cursor config ## Synopsis: Add this application as an MCP server in Cursor ### Usage: ``` telepresence mcp cursor enable [flags] ``` ### Flags: ``` --config-path string Path to Cursor config file -e, --env stringToString Environment variables (e.g., --env KEY1=value1 --env KEY2=value2) (default []) -h, --help help for enable --log-level string Log level (debug, info, warn, error) --server-name string Name for the MCP server (default: derived from executable name) --workspace Add to workspace settings (.cursor/mcp.json) instead of user settings ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_mcp_cursor_list.md ================================================ --- title: telepresence mcp cursor list description: Show Cursor MCP servers hide_table_of_contents: true --- Show Cursor MCP servers ## Synopsis: Show all MCP servers configured in Cursor ### Usage: ``` telepresence mcp cursor list [flags] ``` ### Flags: ``` --config-path string Path to Cursor config file -h, --help help for list --workspace List from workspace settings (.cursor/mcp.json) instead of user settings ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_mcp_start.md ================================================ --- title: telepresence mcp start description: Start the MCP server hide_table_of_contents: true --- Start the MCP server ## Synopsis: Start stdio server to expose CLI commands to AI assistants ### Usage: ``` telepresence mcp start [flags] ``` ### Flags: ``` -h, --help help for start --log-level string Log level (debug, info, warn, error) ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_mcp_stream.md ================================================ --- title: telepresence mcp stream description: Stream the MCP server over HTTP hide_table_of_contents: true --- Stream the MCP server over HTTP ## Synopsis: Start HTTP server to expose CLI commands to AI assistants ### Usage: ``` telepresence mcp stream [flags] ``` ### Flags: ``` -h, --help help for stream --host string host to listen on --log-level string Log level (debug, info, warn, error) --port int port number to listen on (default 8080) ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_mcp_tools.md ================================================ --- title: telepresence mcp tools description: Export tools as JSON hide_table_of_contents: true --- Export tools as JSON ## Synopsis: Export available MCP tools to mcp-tools.json for inspection ### Usage: ``` telepresence mcp tools [flags] ``` ### Flags: ``` -h, --help help for tools --log-level string Log level (debug, info, warn, error) ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_mcp_vscode.md ================================================ --- title: telepresence mcp vscode description: Manage VSCode MCP servers hide_table_of_contents: true --- Manage VSCode MCP servers ## Synopsis: Manage MCP server configuration for Visual Studio Code ### Usage: ``` telepresence mcp vscode [command] [flags] ``` ### Available Commands: | Command | Description | |---------|-------------| | [disable](telepresence_mcp_vscode_disable) | Remove server from VSCode config | | [enable](telepresence_mcp_vscode_enable) | Add server to VSCode config | | [list](telepresence_mcp_vscode_list) | Show VSCode MCP servers | ### Flags: ``` -h, --help help for vscode ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` Use `telepresence mcp vscode [command] --help` for more information about a command. ================================================ FILE: docs/reference/cli/telepresence_mcp_vscode_disable.md ================================================ --- title: telepresence mcp vscode disable description: Remove server from VSCode config hide_table_of_contents: true --- Remove server from VSCode config ## Synopsis: Remove this application from VSCode MCP servers ### Usage: ``` telepresence mcp vscode disable [flags] ``` ### Flags: ``` --config-path string Path to VSCode config file -h, --help help for disable --server-name string Name of the MCP server to remove (default: derived from executable name) --workspace Remove from workspace settings (.vscode/mcp.json) instead of user settings ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_mcp_vscode_enable.md ================================================ --- title: telepresence mcp vscode enable description: Add server to VSCode config hide_table_of_contents: true --- Add server to VSCode config ## Synopsis: Add this application as an MCP server in VSCode ### Usage: ``` telepresence mcp vscode enable [flags] ``` ### Flags: ``` --config-path string Path to VSCode config file -e, --env stringToString Environment variables (e.g., --env KEY1=value1 --env KEY2=value2) (default []) -h, --help help for enable --log-level string Log level (debug, info, warn, error) --server-name string Name for the MCP server (default: derived from executable name) --workspace Add to workspace settings (.vscode/mcp.json) instead of user settings ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_mcp_vscode_list.md ================================================ --- title: telepresence mcp vscode list description: Show VSCode MCP servers hide_table_of_contents: true --- Show VSCode MCP servers ## Synopsis: Show all MCP servers configured in VSCode ### Usage: ``` telepresence mcp vscode list [flags] ``` ### Flags: ``` --config-path string Path to VSCode config file -h, --help help for list --workspace List from workspace settings (.vscode/mcp.json) instead of user settings ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_quit.md ================================================ --- title: telepresence quit description: Tell telepresence daemons to quit hide_table_of_contents: true --- Tell telepresence daemons to quit ### Usage: ``` telepresence quit [flags] ``` ### Flags: ``` -h, --help help for quit -s, --stop-daemons stop all local telepresence daemons ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_replace.md ================================================ --- title: telepresence replace description: Replace a container hide_table_of_contents: true --- Replace a container ### Usage: ``` telepresence replace [flags] <name> [-- [[docker run flags] <image name>] OR [<command>]] args...] ``` ### Flags: ``` --address string Local address to forward to, e.g. '--address 10.0.0.2' (default "127.0.0.1" or name of container) --container string Name of container that should be replaced. Can be omitted if the workload only has one container. --detailed-output Provide very detailed info about the replace when used together with --output=json or --output=yaml' --docker-build string Build a Docker container from the given docker-context (path or URL), and run it with replaced environment and volume mounts, by passing arguments after -- to 'docker run', e.g. '--docker-build /path/to/docker/context -- -it IMAGE /bin/bash' --docker-build-opt stringArray Options to docker-build in the form key=value, e.g. --docker-build-opt tag=mytag. --docker-debug string Like --docker-build, but allows a debugger to run inside the container with relaxed security --docker-mount string The volume mount point in docker. Defaults to same as "--mount" --docker-run Run a Docker container with replaced environment, volume mount, by passing arguments after -- to 'docker run', e.g. '--docker-run -- -it --rm ubuntu:20.04 /bin/bash' -e, --env-file string Also emit the remote environment to an file. The syntax used in the file can be determined using flag --env-syntax -j, --env-json string Also emit the remote environment to a file as a JSON blob. --env-syntax string Syntax used for env-file. One of "docker", "compose", "sh", "csh", "cmd", "json", and "ps"; where "sh", "csh", and "ps" can be suffixed with ":export" (default "docker") -h, --help help for replace --local-mount-port uint16 Do not mount remote directories. Instead, expose this port on localhost to an external mounter --mount string The absolute path for the root directory where volumes will be mounted, $TELEPRESENCE_ROOT. Use "true" to have Telepresence pick a random mount point (default). Use "false" to disable filesystem mounting entirely. Append ":ro" to mount everything read-only. (default "true") -p, --port strings 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. (default [all]) --to-pod strings Additional ports to forward to the pod containing the replaced container, will available for connections to localhost:PORT. Use this to, for example, access proxy/helper sidecars in the pod. The default protocol is TCP. Use <port>/UDP for UDP ports --wait-message string Message to print when replace handler has started ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_revoke.md ================================================ --- title: telepresence revoke description: Revoke an intercept by intercept ID. The intercept ID must be in the format <session_id>:<intercept_name> hide_table_of_contents: true --- Revoke an intercept by intercept ID. The intercept ID must be in the format <session_id>:<intercept_name> ## Synopsis: Revoke an intercept by intercept ID. This is an administrative operation that requires RBAC permissions to modify the "traffic-manager" configmap. ### Usage: ``` telepresence revoke <intercept_id> [flags] ``` ### Flags: ``` -h, --help help for revoke ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_serve.md ================================================ --- title: telepresence serve description: Start the browser on a remote service hide_table_of_contents: true --- Start the browser on a remote service ### Usage: ``` telepresence serve <name of remote service> [flags] ``` ### Flags: ``` -h, --help help for serve -p, --port uint16 service port (default 80) ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_status.md ================================================ --- title: telepresence status description: Show connectivity status hide_table_of_contents: true --- Show connectivity status ### Usage: ``` telepresence status [flags] ``` ### Flags: ``` -h, --help help for status --multi-daemon always use multi-daemon output format, even if there's only one daemon connected ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_uninstall.md ================================================ --- title: telepresence uninstall description: Uninstall telepresence agents hide_table_of_contents: true --- Uninstall telepresence agents ### Usage: ``` telepresence uninstall [flags] <workloads...> ``` ### Flags: ``` -a, --all-agents uninstall intercept agent on all workloads -h, --help help for uninstall ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_version.md ================================================ --- title: telepresence version description: Show version hide_table_of_contents: true --- Show version ### Usage: ``` telepresence version [flags] ``` ### Flags: ``` -h, --help help for version ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cli/telepresence_wiretap.md ================================================ --- title: telepresence wiretap description: Wiretap a Service hide_table_of_contents: true --- Wiretap a Service ### Usage: ``` telepresence wiretap [flags] <wiretap_base_name> [-- <command with arguments...>] ``` ### Flags: ``` --address string Local address to forward to, e.g. '--address 10.0.0.2' (default "127.0.0.1" or name of container) --container string Name of container that provides the environment and mounts for the wiretap. Defaults to the container matching the first wiretapped port. --detailed-output Provide very detailed info about the wiretap when used together with --output=json or --output=yaml' --docker-build string Build a Docker container from the given docker-context (path or URL), and run it with wiretapped environment and volume mounts, by passing arguments after -- to 'docker run', e.g. '--docker-build /path/to/docker/context -- -it IMAGE /bin/bash' --docker-build-opt stringArray Options to docker-build in the form key=value, e.g. --docker-build-opt tag=mytag. --docker-debug string Like --docker-build, but allows a debugger to run inside the container with relaxed security --docker-mount string The volume mount point in docker. Defaults to same as "--mount" --docker-run Run a Docker container with wiretapped environment, volume mount, by passing arguments after -- to 'docker run', e.g. '--docker-run -- -it --rm ubuntu:20.04 /bin/bash' -e, --env-file string Also emit the remote environment to an file. The syntax used in the file can be determined using flag --env-syntax -j, --env-json string Also emit the remote environment to a file as a JSON blob. --env-syntax string Syntax used for env-file. One of "docker", "compose", "sh", "csh", "cmd", "json", and "ps"; where "sh", "csh", and "ps" can be suffixed with ":export" (default "docker") -h, --help help for wiretap --http-header strings HTTP header filters. Only requests with matching headers will be wiretapped. Supports both formats: --http-header "X-User-ID=dev123" or --http-header "X-User-ID: dev123" (curl -H compatible). Multiple headers use AND logic. --http-path-equal strings HTTP path filters. Only requests with matching paths will be wiretapped. Exact path matching. --http-path-prefix strings HTTP path prefix filters. Only requests with matching path prefixes will be wiretapped. --http-path-regex strings HTTP path regex filters. Only requests with paths matching the regex will be wiretapped. --local-mount-port uint16 Do not mount remote directories. Instead, expose this port on localhost to an external mounter --mechanism mechanism Which extension mechanism to use (default "tcp") --mount string The absolute path for the root directory where volumes will be mounted, $TELEPRESENCE_ROOT. Use "true" to have Telepresence pick a random mount point (default). Use "false" to disable filesystem mounting entirely. Append ":ro" to mount everything read-only. (default "true") --plaintext Use plaintext instead of TLS when communicating with the intercept handler -p, --port strings Local ports to forward to. Use <local port>:<identifier> to uniquely identify service ports, where the <identifier> is the port name or number. With --docker-run and a daemon that doesn't run in docker', use <local port>:<container port> or <local port>:<container port>:<identifier>. --service string Optional name of service to wiretap. Sometimes needed to uniquely identify the intercepted port. --wait-message string Message to print when wiretap handler has started -w, --workload string Name of workload (Deployment, ReplicaSet, StatefulSet, Rollout) to wiretap, if different from <name> ``` ### Global Flags: ``` --config string Path to the Telepresence configuration file (default "$HOME/.config/telepresence/config.yml") --output string Set the output format, supported values are 'json', 'yaml', and 'default' (default "default") --progress string Set type of progress output (auto, tty, plain, json, quiet) (default "auto") --use string Match expression that uniquely identifies the daemon container ``` ================================================ FILE: docs/reference/cluster-config.md ================================================ --- title: Cluster-side configuration --- # Cluster-side configuration For the most part, Telepresence doesn't require any special configuration in the cluster and can be used right away in any cluster (as long as the user has adequate [RBAC permissions](rbac.md). ## Helm Chart configuration Some cluster specific configuration can be provided when installing or upgrading the Telepresence cluster installation using Helm. Once installed, the Telepresence client will configure itself from values that it receives when connecting to the Traffic manager. See the Helm chart [README](https://artifacthub.io/packages/helm/telepresence-oss/telepresence-oss/$version$) for a full list of available configuration settings. ### Values To add configuration, create a yaml file with the configuration values and then use it executing `telepresence helm install [--upgrade] --values ` ## Client Configuration It is possible for the Traffic Manager to automatically push config to all connecting clients. To learn more about this, please see the [client config docs](config.md#global-configuration). ## gRCP connections All traffic to and from the cluster is tunneled via gRPC over a kubernetes port-forward connection. Both the traffic-manager and the traffic-agent have gRPC servers that the clients connect to. They are configured using the `grpc` structure. | Field | Description | Type | Default | |-------------------|----------------------------------------------------------------------------------------------|---------------------------------------------------|----------| | `connectionTTL` | Max time that the traffic-manager or traffic-agent will keep an idle client connection alive | [duration](https://pkg.go.dev/time#ParseDuration) | `24h` | | `maxReceiveSize` | Max size of a gRCP message received by the traffic-manager or traffic-agent | [quantity](../common/quantity.md) | `4Mi` | ## Traffic Manager Configuration The `trafficManager` structure of the Helm chart configures the behavior of the Telepresence traffic manager. ## Agent Configuration The `agent` structure of the Helm chart configures the behavior of the Telepresence agents. ### Image Configuration The `agent.image` structure contains the following values: | Setting | Meaning | |------------|-----------------------------------------------------------------------------| | `registry` | Registry used when downloading the image. Defaults to "docker.io/datawire". | | `name` | The name of the image. Defaults to "tel2" | | `tag` | The tag of the image. Defaults to $version$ | ### Log level The `agent.LogLevel` controls the log level of the traffic-agent. See [Log Levels](config.md#log-levels) for more info. ### Resources The `agent.resources` and `agent.initResources` will be used as the `resources` element when injecting traffic-agents and init-containers. ## Mutating Webhook Telepresence uses a Mutating Webhook to inject the [Traffic Agent](architecture.md#traffic-agent) sidecar container and update the port definitions. This means that an engaged workload (Deployment, StatefulSet, ReplicaSet, ArgoRollout) will remain untouched and in sync as far as GitOps workflows (such as ArgoCD) are concerned. The injection will happen on demand the first time an attempt is made to replace, ingest, intercept, or wiretap the workload. If you want to prevent that the injection ever happens, simply add the `telepresence.io/inject-traffic-agent: disabled` annotation to your workload template's annotations: ```diff spec: template: metadata: labels: service: your-service + annotations: + telepresence.io/inject-traffic-agent: disabled spec: containers: ``` ### Service Name and Port Annotations Telepresence will automatically find all services and all ports that will connect to a workload and make them available for an intercept, but you can explicitly define that only one service and/or port can be intercepted. ```diff spec: template: metadata: labels: service: your-service annotations: + telepresence.io/inject-service-name: my-service + telepresence.io/inject-service-ports: https spec: containers: ``` ### Control Volume Sharing Telepresence enables control over what volumes that will be shared with connecting clients using mount policies. A policy can be declared for a volume name, or for paths matching a path prefix, and can be added either as a Helm chart value using `agents.mountPolicies` or using the workload annotation `telepresence.io/mount-policies`. Possible Mount Policies are: | Policy | Meaning | |----------------|------------------------------------------------------------------------------------------------------| | Ignore | Do not share this volume with engaging clients | | Local | Do not share this volume with engaging clients, instead Mount it using the client's local filesystem | | Remote | Share this volume, and give engaging clients read and write access to it | | RemoteReadOnly | Like Remote, but with read-only access | Example Helm chart value: ```yaml agents: mountPolicies: '/tmp': Local certs: RemoteReadOnly private: Ignore ``` Example using the `telepresence.io/mount-policies` annotation: ```yaml spec: template: metadata: annotations: 'telepresence.io/mount-policies': '{"/tmp":"Local","certs":"RemoteReadOnly","private":"Ignore"}' ``` The annotation `telepresence.io/inject-ignore-volume-mounts` can be used if the objective is to just ignore volume mounts, but it's recommended to always use the `telepresence.io/mount-policies` annotation. Example using the `telepresence.io/inject-ignore-volume-mounts` annotation: ```yaml spec: template: metadata: annotations: telepresence.io/inject-ignore-volume-mounts: "private" ``` The example is equivalent to: ```yaml spec: template: metadata: annotations: telepresence.io/mount-policies: '{"private":"Ignore"}' ``` ### Note on Numeric Ports If the `targetPort` of your intercepted service is pointing at a port number, in addition to injecting the Traffic Agent sidecar, Telepresence will also inject an `initContainer` that will reconfigure the pod's firewall rules to redirect traffic to the Traffic Agent. > [!IMPORTANT] > Note that this `initContainer` requires `NET_ADMIN` capabilities. If your cluster administrator has disabled them, you will be unable to use numeric ports with the agent injector. For example, the following service is using a numeric port, so Telepresence would inject an initContainer into it: ```yaml apiVersion: v1 kind: Service metadata: name: your-service spec: type: ClusterIP selector: service: your-service ports: - port: 80 targetPort: 8080 --- apiVersion: apps/v1 kind: Deployment metadata: name: your-service labels: service: your-service spec: replicas: 1 selector: matchLabels: service: your-service template: metadata: annotations: telepresence.io/inject-traffic-agent: enabled labels: service: your-service spec: containers: - name: your-container image: jmalloc/echo-server ports: - containerPort: 8080 ``` ## Intercept Configuration ### Restricting Global Intercepts In shared development environments, you may want to prevent developers from creating global TCP/UDP intercepts that would block others from intercepting the same service. Telepresence allows you to restrict intercepts to only HTTP-based intercepts with header or path filters. When installing your traffic-manager through helm, use the `--set` flag: `telepresence helm install --set intercept.allowGlobalIntercepts=false` This also applies when upgrading: `telepresence helm upgrade --set intercept.allowGlobalIntercepts=false` Alternatively, add it to your custom `values.yaml`: ```yaml intercept: allowGlobalIntercepts: false ``` When this setting is `false`: - Standard intercepts without HTTP filters (e.g., `telepresence intercept myservice --port 8080`) will be rejected - Replace operations (e.g., `telepresence replace myservice`) will be rejected - HTTP intercepts with filters (e.g., `telepresence intercept myservice --http-header X-User-ID=dev123 --port 8080`) will work normally - Wiretap operations continue to work (they don't block other users) - Multiple developers can create personal HTTP intercepts on the same service simultaneously - The error message will guide users to use `--http-header`, `--http-path-prefix`, `--http-path-equal`, or `--http-path-regex` flags The setting defaults to `true` to maintain backward compatibility with existing deployments. ### Excluding Envrionment Variables If your pod contains sensitive variables like a database password, or third party API Key, you may want to exclude those from being propagated through an intercept. Telepresence allows you to configure this through a ConfigMap that is then read and removes the sensitive variables. This can be done in two ways: When installing your traffic-manager through helm you can use the `--set` flag and pass a comma separated list of variables: `telepresence helm install --set intercept.environment.excluded="{DATABASE_PASSWORD,API_KEY}"` This also applies when upgrading: `telepresence helm upgrade --set intercept.environment.excluded="{DATABASE_PASSWORD,API_KEY}"` Once this is completed, the environment variables will no longer be in the environment file created by an Intercept. The other way to complete this is in your custom `values.yaml`. Customizing your traffic-manager through a values file can be viewed [here](../install/manager.md). ```yaml intercept: environment: excluded: ['DATABASE_PASSWORD', 'API_KEY'] ``` You can exclude any number of variables, they just need to match the `key` of the variable within a pod to be excluded. ================================================ FILE: docs/reference/compose.md ================================================ --- title: Telepresence Docker Compose Extension hide_table_of_contents: true --- # Telepresence Docker Compose Extension The `x-tele` extension is added either at the top level or to a service. E.g. ```yaml x-tele: connections: - namespace: services: some-name: x-tele: type: ... ``` Those extensions are recognized by the `telepresence compose`, which acts as an extended `docker compose` command. Telepresence will create connections, engagements, and proxies based on the extensions and then modify the docker compose file with the necessary networks, mounts, and environment variables to make the extended services work. ## States - `telepresence compose up` will ensure that the extended services are in the correct state. - `telepresence compose create` is like `up`, but it will not start the containers, and therefore end any existing engagements once all the containers are created. - `telepresence compose stop` ends the engagements, but it keeps telepresence connected, because the existing containers use the `teleroute` network backed by that connection. - `telepresence compose down` will end the engagements, terminate the network, and quit telepresence. - `telepresence config` will detect if the project is started, if so, produce the extended project file. Otherwise, it will produce the original project file's canonical form. - `telepresence quit` will detect if a `telepresence compose` is running and, if so, issue a `telepresence compose down`. ## Top-level Extension The Top-level extension describes the connection and mount configurations that are used by the service extensions: | Name | Description | Type | Default Value | |-------------------|------------------------------------------------------------------------------------|--------------------|---------------| | connections | Connection configurations. | connection configs | empty | | mounts | Mount configurations. | mount configs | empty | ### Connection Configuration The `connections` field is a list of connection configurations. A single connection using the defaults from the current Kubernetes context is created when the list is empty. Each connection configuration is an object with the following fields: | Name | Description | Type | Default Value | |-------------------|------------------------------------------------------------------------------------|---------|-----------------------------------------------------------------| | name | The connection name | string | generated based on the current Kubernetes context and namespace | | namespace | The namespace to connect to | string | The namespace configured in the current Kubernetes context | | also-proxy | Subnets that Telepresence should proxy in addition to the service and pod subnets. | CIDRs | empty | | never-proxy | Subnets that Telepresence should never proxy. | CIDRs | empty | | manager-namespace | The namespace in which the Telepresence Traffic Manager is running. | string | The name specified in the client config or "ambassador" | | mapped-namespaces | Namespaces that Telepresence should map as DNS domains. | strings | empty | ### Mount Configuration The `mounts` field is a list of mount configurations that controls how the service extensions handle the volumes shared by the traffic-agent that the extended service will engage with. The mount configuration is an object with the following fields: | Name | Description | Type | Default Value | |---------------|----------------------------------------------------------------------------------------------------------|--------|---------------------------------| | volume | Name of a Docker Compose volume. Mutually exclusive to volumePattern. | string | empty | | volumePattern | Regular expression pattern matching one or several Docker Compose volumes. Mutually exclusive to volume. | string | empty | | policy | "local", "remote", or "remoteReadOnly" | string | determined by the traffic-agent | The mount policy determines how the volume is mounted by Docker Compose.
local
The Docker Compose volume is not modified.
remote
The Docker Compose volume is modified to mount a remote volume without a read-only restriction. It might still be restricted by the remote volume's permissions.
remoteReadOnly
The Docker Compose volume is modified to mount a remote volume read-only restriction.
## Service Extensions The `x-tele` extension can be added to a Docker Compose service to extend the service's behavior. The extension must have a `type` field. Available types are: | Type | Local service Behavior | Similar to | |-------------------------|-----------------------------------------------------------------|--------------------------| | [connect](#connect) | Service has access to the cluster's resources (DNS and routing) | `telepresence connect` | | [proxy](#proxy) | Replaced with a proxy for a service in the cluster | N/A | | [ingest](#ingest) | Acts as the handler for an ingested container the cluster | `telepresence ingest` | | [intercept](#intercept) | Acts as the handler for an intercepted service the cluster | `telepresence intercept` | | [replace](#replace) | Replaces a remote container in the cluster | `telepresence replace` | | [wiretap](#wiretap) | Receives wiretapped data from a service in the cluster | `telepresence wiretap` | ### connect The `connect` extension can be used as is, but it is also the base for all the other extensions. It will ensure that the extended docker compose service has access to the cluster's network and DNS by injecting a [teleroute](plugins.md#teleroute-network-plugin) network. The extension can have the following fields set: | Name | Description | Type | Default Value | |------------|-----------------------------------------------------------------------|--------|---------------| | connection | The name of a connection declared in the top-level `x-tele` extension | string | empty | The `connection` field is required only when more than one connection is declared in the top-level `x-tele` extension. ### proxy The `proxy` extension replaces the extended docker compose service with a proxy that redirects all traffic to a workload in the cluster. The extension can have the following fields set: | Name | Description | Type | Default Value | |------------|-----------------------------------------------------------------------|---------|-------------------------------| | connection | The name of a connection declared in the top-level `x-tele` extension | string | empty | | name | Name of the proxied remote service | string | name of the compose service | | ports | List of <service-port>:<remote service-port>> mappings | strings | empty (route all ports as-is) | ### ingest The `ingest` ensures that the docker compose service shares the environment and volumes of a remote container in the cluster. The extension can have the following fields set: | Name | Description | Type | Default Value | |------------|-------------------------------------------------------------------------------|---------|-----------------------------| | connection | The name of a connection declared in the top-level `x-tele` extension | string | empty | | name | Name of the remote workload (typically the deployment) | string | name of the compose service | | container | Name of the remote container | string | empty | | to-pod | Ports to forward from the local compose service to the remote pod's localhost | strings | empty | The `container` field is required when more than one container is declared in the remote workload. ### intercept The `intercept` ensures that the docker compose service receives traffic from, and shares the environment and volumes of, a remote container in the cluster. The extension can have the following fields set: | Name | Description | Type | Default Value | |------------------|------------------------------------------------------------------------------------------------|---------|-----------------------------| | connection | The name of a connection declared in the top-level `x-tele` extension | string | empty | | name | Name of the intercept engagement | string | name of the compose service | | httpFilters | HTTP header filters. Only requests with matching headers will be intercepted | object | empty | | httpPaths | HTTP path filters. Only requests with matching paths will be intercepted. Exact path matching. | strings | empty | | httpPathPrefixes | HTTP path prefix filters. Only requests with matching path prefixes will be intercepted. | strings | empty | | httpPathRegexps | HTTP path regexp filters. Only requests with paths matching the regexp will be intercepted. | strings | empty | | workload | Name of the remote workload (typically the deployment) | string | name of the engagement | | ports | Service <local port>:<service port> to intercept | strings | empty | | service | Name of the remote service | string | empty | | to-pod | Ports to forward from the local compose service to the remote pod's localhost | strings | empty | The `service` field is optional as long as the given `ports` are unique. The `workload` is useful when doing multiple intercepts on the same workload using different intercept names. ### replace The `replace` ensures that the docker compose service receives traffic from, and shares the environment and volumes of, a remote container in the cluster. The extension can have the following fields set: | Name | Description | Type | Default Value | |------------|-------------------------------------------------------------------------------|---------|-----------------------------| | connection | The name of a connection declared in the top-level `x-tele` extension | string | empty | | name | Name of the remote workload (typically the deployment) | string | name of the compose service | | container | Name of the remote container | string | empty | | ports | Service <local port>:<container port> to replace | strings | empty | | to-pod | Ports to forward from the local compose service to the remote pod's localhost | strings | empty | ### wiretap The `wiretap` ensures that the docker compose service receives wiretapped traffic from a remote service, and shares the environment and volumes (read-only) of, a remote container. The extension can have the following fields set: | Name | Description | Type | Default Value | |------------------|-----------------------------------------------------------------------------------------------|---------|-----------------------------| | connection | The name of a connection declared in the top-level `x-tele` extension | string | empty | | name | Name of the wiretapped workload (typically the deployment) | string | name of the compose service | | httpFilters | HTTP header filters. Only requests with matching headers will be wiretapped | object | empty | | httpPaths | HTTP path filters. Only requests with matching paths will be wiretapped. Exact path matching. | strings | empty | | httpPathPrefixes | HTTP path prefix filters. Only requests with matching path prefixes will be wiretapped. | strings | empty | | httpPathRegexps | HTTP path regexp filters. Only requests with paths matching the regexp will be wiretapped. | strings | empty | | ports | Service <local port>:<service port> to wiretap | strings | empty | | service | Name of the remote service | string | empty | | to-pod | Ports to forward from the local compose service to the remote pod's localhost | strings | empty | The `service` field is optional as long as the given `service ports` are unique. ================================================ FILE: docs/reference/config.md ================================================ --- title: Laptop-side configuration --- # Laptop-side configuration There are a number of configuration values that can be tweaked to change how Telepresence behaves. These can be set in three ways: globally, by a platform engineer with powers to deploy the Telepresence Traffic Manager, or locally by any user, either in the Telepresence configuration file `config.yml`, or as a Telepresence extension the Kubernetes configuration. One important exception is the configuration of the of the traffic manager namespace, which, if it's different from the default of `ambassador`, [must be set](#manager) locally to be able to connect. ## Global Configuration Global configuration is set at the Traffic Manager level and applies to any user connecting to that Traffic Manager. To set it, simply pass in a `client` dictionary to the `telepresence helm install` command, with any config values you wish to set. The `client` config supports values for [cluster](#cluster), [dns](#dns), [docker](#docker), [grpc](#grpc), [helm](#helm), [intercept](#intercept), [images](#images), [logLevels](#log-levels), [routing](#routing), and [timeouts](#timeouts). Here is an example configuration to show you the conventions of how Telepresence is configured: **note: This config shouldn't be used verbatim, since the registry `privateRepo` used doesn't exist** ```yaml client: timeouts: agentInstall: 1m intercept: 10s logLevels: userDaemon: debug images: registry: privateRepo # This overrides the default docker.io/datawire repo agentImage: tel2:$version$ # This overrides the agent image to inject when engaging with a workload grpc: maxReceiveSize: 10Mi connectionTTL: 48h dns: includeSuffixes: [.private] excludeSuffixes: [.se, .com, .io, .net, .org, .ru] lookupTimeout: 30s routing: alsoProxySubnets: - 1.2.3.4/32 neverProxySubnets: - 1.2.3.4/32 ``` ### Cluster Values for `client.cluster` controls aspects on how client's connection to the traffic-manager. | Field | Description | Type | Default | |---------------------------|---------------------------------------------------------------------------------------|---------------------------------------------|-----------------| | `defaultManagerNamespace` | The default namespace where the Traffic Manager will be installed. | [string][yaml-str] | ambassador | | `mappedNamespaces` | Namespaces that will be mapped by default. | [sequence][yaml-seq] of [strings][yaml-str] | `[]` | | `connectFromRootDaeamon` | Make connections to the cluster directly from the root daemon. | [boolean][yaml-bool] | `true` | | `agentPortForward` | Let telepresence-client use port-forwards directly to agents | [boolean][yaml-bool] | `true` | ### Docker Values for the `client.docker` provides docker specific options. | Field | Description | Type | Default | |------------------|---------------------------------------------------------------------------------------|-----------------------|-----------------| | `addHostGateway` | Add `--add-host host.docker.internal:host-gateway` when starting the daemon in docker | [boolean][yaml-bool] | `true` on linux | | `telemount` | Configuration of the image containing the telemount Docker volume plugin | Image | | | `teleroute` | Configuration of the image containing the teleroute Docker network plugin | Image | | | `enableIPv4` | Enable support for IPv4 networking | [boolean][yaml-bool] | `true` | | `enableIPv6` | Enable support for IPv6 networking | [boolean][yaml-bool] | `true` | #### Image The Image objects for `client.docker.telemount` and `client.docker.teleroute` provides information on how to download the image from a registry. The repositoryAPI must be capable of listing available tags if the tag is set to an empty string. The `ghcr.io/v2` API does not support this for anonymous users, hence the default tags. | Field | Description | Type | Default | |---------------|--------------------------------------------------|--------------------|-----------------------| | `registryAPI` | The URL used when connecting to the registry API | [string][yaml-str] | `ghcr.io/v2` | | `registry` | The name of the registry | [string][yaml-str] | `ghcr.io` | | `namespace` | The namespace of the component | [string][yaml-str] | `telepresenceio` | | `repository` | The name of the component registry | [string][yaml-str] | `telemount/teleroute` | | `tag` | The component tag | [string][yaml-str] | `0.1.6/0.3.0` | ### DNS The `client.dns` configuration offers options for configuring the DNS resolution behavior in a client application or system. Here is a summary of the available fields: The fields for `client.dns` are: `localIP`, `excludeSuffixes`, `includeSuffixes`, and `lookupTimeout`. | Field | Description | Type | Default | |--------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------|----------------------------------------------------| | `localIP` | The address of the local DNS server. This entry is only used on Linux systems that are not configured to use systemd-resolved. | IP address [string][yaml-str] | first `nameserver` mentioned in `/etc/resolv.conf` | | `excludeSuffixes` | Suffixes for which the DNS resolver will always fail (or fallback in case of the overriding resolver). Can be globally configured in the Helm chart. | [sequence][yaml-seq] of [strings][yaml-str] | `[".arpa", ".com", ".io", ".net", ".org", ".ru"]` | | `includeSuffixes` | Suffixes for which the DNS resolver will always attempt to do a lookup. Includes have higher priority than excludes. Can be globally configured in the Helm chart. | [sequence][yaml-seq] of [strings][yaml-str] | `[]` | | `excludes` | Names to be excluded by the DNS resolver | `[]` | | | `mappings` | Names to be resolved to other names (CNAME records) or to explicit IP addresses | `[]` | | | `lookupTimeout` | Maximum time to wait for a cluster side host lookup. | [duration][go-duration] [string][yaml-str] | 4 seconds | | `recursionCheck` | Enable DNS lookup recursion detection and avoidance. | boolean | false | | `useComplexLookup` | Disable use of simplified but efficient A and AAAA lookups. | [boolean][yaml-bool] | `false` | Here is an example values.yaml: ```yaml client: dns: includeSuffixes: [.private] excludeSuffixes: [.se, .com, .io, .net, .org, .ru] localAddress: 172.12.0.53 lookupTimeout: 30s ``` #### Mappings Allows you to map hostnames to aliases or to IP addresses. This is useful when you want to use an alternative name for a service in the cluster, or when you want the DNS resolver to map a name to an IP address of your choice. In the given cluster, the service named `postgres` is located within a separate namespace titled `big-data`, and it's referred to as `psql` : ```yaml dns: mappings: - name: postgres aliasFor: psql.big-data - name: my.own.domain aliasFor: 192.168.0.15 ``` #### Exclude Lists service names to be excluded from the Telepresence DNS server. This is useful when you want your application to interact with a local service instead of a cluster service. In this example, "redis" will not be resolved by the cluster, but locally. ```yaml dns: excludes: - redis ``` #### recursionCheck The `recursionCheck` is useful in situations when the cluster runs locally on the client and might have access to the client's DNS server. This is often the case when using clusters like Minikube or Kind, or have a Docker Desktop with Kubernetes enabled. The scenario is as follows: 1. A request to resolve a name arrives to the Telepresence DNS server. 2. Telepresence sends this request to the cluster's DNS server. 3. The cluster's DNS server is unable to resolve a name, and in an attempt to resolve it globally, it sends it to its host. 4. The host sends the request to the Telepresence DNS server. 5. Telepresence, already in progress of resolving this name, will wait for the previous attempt to succeed. 6. The original DNS request times out. The final timeout is fairly slow. Enabling the `recursionCheck` will make it significantly faster because Telepresence will give up after a short timeout in step 5. The draw-back is that if multiple requests to resolve the same name arrive within a very short period of time, then there's a risk that Telepresence will not be able to distinguish the recursive calls from normal calls and respond with false timeouts. ### Helm The `client.helm` object contains options for the `telepresence helm` commands | Field | Description | Type | Default | |-------------|--------------------------------------------------------------------------------------------------------------|--------------------|-------------------------------------------------| | `chartURL` | The URL used when downloading the Telepresence Helm chart when the version differs from the embedded version | [string][yaml-str] | `oci://ghcr.io/telepresenceio/telepresence-oss` | ### Grpc All traffic to and from the cluster is tunneled via gRPC over a kubernetes port-forward connection. | Field | Description | Type | Default | |-------------------|-----------------------------------------------------------------------------------------------|-------------------------------------------|----------| | `connectionTTL` | Max time that the traffic-manager or traffic-agent will keep an idle client connection alive | [duration][go-duration] | `24h` | | `maxReceiveSize` | Max size of a gRCP message. | Docker registry name [quantity][quantity] | `4Mi` | ### Images Values for `client.images` are strings. These values affect the objects that are deployed in the cluster, so it's important to ensure users have the same configuration. These are the valid fields for the `client.images` key: | Field | Description | Type | Default | |---------------|------------------------------------------------------------------------------------------|------------------------------------------------|-------------------------------------| | `registry` | Docker registry to be used for installing the Traffic Manager and default Traffic Agent. | Docker registry name [string][yaml-str] | `docker.io/datawire` | | `agentImage` | `$registry/$imageName:$imageTag` to use when installing the Traffic Agent. | qualified Docker image name [string][yaml-str] | (unset) | | `clientImage` | `$registry/$imageName:$imageTag` to use locally when connecting with `--docker`. | qualified Docker image name [string][yaml-str] | `$registry/ambassador-telepresence` | ### Intercept The `intercept` controls applies to how Telepresence will intercept the communications to replaced containers and intercepted services. | Field | Description | Type | Default | |---------------|-----------------------------------------------------------------------------------------------------------------------|----------------------|------------| | `defaultPort` | controls which port is selected when no `--port` flag is given to the `telepresence intercept` command | [int][yaml-int] | 8080 | | `useFtp` | Use fuseftp instead of sshfs when mounting remote file systems | [boolean][yaml-bool] | false | | `mountsRoot` | Directory that will be used as the root for all automatically generated mount directories (not applicable on windows) | [string][yaml-str] | env:TMPDIR | ### Log Levels Values for the `client.logLevels` fields are one of the following strings, case-insensitive: - `trace` - `debug` - `info` - `warning` or `warn` - `error` For whichever log-level you select, you will get logs labeled with that level and of higher severity. (e.g. if you use `info`, you will also get logs labeled `error`. You will NOT get logs labeled `debug`. These are the valid fields for the `client.logLevels` key: | Field | Description | Type | Default | |------------------|-----------------------------------------------------------------------------------------|---------------------------------------------|---------| | `userDaemon` | Logging level to be used by the User Daemon (logs to connector.log) | [loglevel][slog-level] [string][yaml-str] | info | | `rootDaemon` | Logging level to be used for the Root Daemon (logs to daemon.log) | [loglevel][slog-level] [string][yaml-str] | info | | `kubeAuthDaemon` | Logging level to be used by the Kubernetes Authentication Daemon (logs to kubeauth.log) | [loglevel][slog-level] [string][yaml-str] | info | | `cli` | Logging level to be used by the CLI frontend (logs to cli.log) | [loglevel][slog-level] [string][yaml-str] | info | ### Routing #### AlsoProxySubnets When using `alsoProxySubnets`, you provide a list of subnets to be added to the TUN device. All connections to addresses that the subnet spans will be dispatched to the cluster Here is an example values.yaml for the subnet `1.2.3.4/32`: ```yaml client: routing: alsoProxySubnets: - 1.2.3.4/32 ``` #### NeverProxySubnets When using `neverProxySubnets` you provide a list of subnets. These will never be routed via the TUN device, even if they fall within the subnets (pod or service) for the cluster. Instead, whatever route they have before telepresence connects is the route they will keep. Here is an example kubeconfig for the subnet `1.2.3.4/32`: ```yaml client: routing: neverProxySubnets: - 1.2.3.4/32 ``` #### Using AlsoProxy together with NeverProxy Never proxy and also proxy are implemented as routing rules, meaning that when the two conflict, regular routing routes apply. Usually this means that the most specific route will win. So, for example, if an `alsoProxySubnets` subnet falls within a broader `neverProxySubnets` subnet: ```yaml neverProxySubnets: [10.0.0.0/16] alsoProxySubnets: [10.0.5.0/24] ``` Then the specific `alsoProxySubnets` of `10.0.5.0/24` will be proxied by the TUN device, whereas the rest of `10.0.0.0/16` will not. Conversely, if a `neverProxySubnets` subnet is inside a larger `alsoProxySubnets` subnet: ```yaml alsoProxySubnets: [10.0.0.0/16] neverProxySubnets: [10.0.5.0/24] ``` Then all of the `alsoProxySubnets` of `10.0.0.0/16` will be proxied, with the exception of the specific `neverProxySubnets` of `10.0.5.0/24` These are the valid fields for the `client.routing` key: | Field | Description | Type | Default | |---------------------------|----------------------------------------------------------------------------------------|-------------------------|--------------------| | `alsoProxySubnets` | Proxy these subnets in addition to the service and pod subnets | [CIDR][cidr] | | | `neverProxySubnets` | Do not proxy these subnets | [CIDR][cidr] | | | `allowConflictingSubnets` | Give Telepresence precedence when these subnets conflict with other network interfaces | [CIDR][cidr] | | | `recursionBlockDuration` | Prevent recursion in VIF for this duration after a connect | [duration][go-duration] | | | `virtualSubnet` | The CIDR to use when generating virtual IPs | [CIDR][cidr] | platform dependent | | `autoResolveConflicts` | Auto resolve conflicts using a virtual subnet | [bool][yaml-bool] | true | ### Timeouts Values for `client.timeouts` are all durations either as a number of seconds or as a string with a unit suffix of `ms`, `s`, `m`, or `h`. Strings can be fractional (`1.5h`) or combined (`2h45m`). These are the valid fields for the `timeouts` key: | Field | Description | Type | Default | |-------------------------|------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------|-----------------| | `agentInstall` | Waiting for Traffic Agent to be installed | [int][yaml-int] or [float][yaml-float] number of seconds, or [duration][go-duration] [string][yaml-str] | 2 minutes | | `apply` | Waiting for a Kubernetes manifest to be applied | [int][yaml-int] or [float][yaml-float] number of seconds, or [duration][go-duration] [string][yaml-str] | 1 minute | | `clusterConnect` | Waiting for cluster to be connected | [int][yaml-int] or [float][yaml-float] number of seconds, or [duration][go-duration] [string][yaml-str] | 20 seconds | | `connectivityCheck` | Timeout used when checking if cluster is already proxied on the workstation | [int][yaml-int] or [float][yaml-float] number of seconds, or [duration][go-duration] [string][yaml-str] | 500 ms (max 5s) | | `endpointDial` | Waiting for a Dial to a service for which the IP is known | [int][yaml-int] or [float][yaml-float] number of seconds, or [duration][go-duration] [string][yaml-str] | 3 seconds | | `roundtripLatency` | How much to add to the endpointDial timeout when establishing a remote connection | [int][yaml-int] or [float][yaml-float] number of seconds, or [duration][go-duration] [string][yaml-str] | 2 seconds | | `intercept` | Waiting for an intercept to become active | [int][yaml-int] or [float][yaml-float] number of seconds, or [duration][go-duration] [string][yaml-str] | 30 seconds | | `proxyDial` | Waiting for an outbound connection to be established | [int][yaml-int] or [float][yaml-float] number of seconds, or [duration][go-duration] [string][yaml-str] | 5 seconds | | `trafficManagerConnect` | Waiting for the Traffic Manager API to connect for port forwards | [int][yaml-int] or [float][yaml-float] number of seconds, or [duration][go-duration] [string][yaml-str] | 60 seconds | | `trafficManagerAPI` | Waiting for connection to the gPRC API after `trafficManagerConnect` is successful | [int][yaml-int] or [float][yaml-float] number of seconds, or [duration][go-duration] [string][yaml-str] | 15 seconds | | `helm` | Waiting for Helm operations (e.g. `install`) on the Traffic Manager | [int][yaml-int] or [float][yaml-float] number of seconds, or [duration][go-duration] [string][yaml-str] | 30 seconds | ## Local Overrides In addition, it is possible to override each of these variables at the local level by setting up new values in local config files. There are two types of config values that can be set locally: those that apply to all clusters, which are set in a single `config.yml` file, and those that only apply to specific clusters, which are set as extensions to the `$KUBECONFIG` file. ### Client Config Telepresence uses a `config.yml` file to store and change those configuration values that will be used by the Telepresence client. The location of this file varies based on your OS: * macOS: `$HOME/Library/Application Support/telepresence/config.yml` * Linux: `$XDG_CONFIG_HOME/telepresence/config.yml` or, if that variable is not set, `$HOME/.config/telepresence/config.yml` * Windows: `%APPDATA%\telepresence\config.yml` ### Values The definitions of the values in the `config.yml` are identical to those values in the `client` config above, but without the top level `client` key. Here is an example configuration to show you the conventions of how Telepresence is configured: **note: This config shouldn't be used verbatim, since the registry `privateRepo` used doesn't exist** ```yaml timeouts: agentInstall: 1m intercept: 10s logLevels: userDaemon: debug images: registry: privateRepo # This overrides the default docker.io/datawire repo agentImage: tel2:$version$ # This overrides the agent image to inject when engaging with a workload grpc: maxReceiveSize: 10Mi ``` ## Workstation Per-Cluster Configuration Configuration that is specific to a cluster connection can also be overriden per-workstation by modifying your `$KUBECONFIG` file. It is recommended that you do not do this, and instead rely on upstream values provided to the Traffic Manager. This ensures that all users that connect to the Traffic Manager will behave the same. An important exception to this is the [`cluster.defaultManagerNamespace` configuration](#manager) which must be set locally. ### Values The definitions of the values in the Telepresence kubeconfig extension are identical to those values in the `config.yml` config. The values will be merged into the config and have higher priority when Telepresence is connected to the extended cluster. Example kubeconfig: ```yaml apiVersion: v1 clusters: - cluster: server: https://127.0.0.1 extensions: - name: telepresence.io extension: cluster: defaultManagerNamespace: staging dns: includeSuffixes: [.private] excludeSuffixes: [.se, .com, .io, .net, .org, .ru] routing: neverProxy: [10.0.0.0/16] alsoProxy: [10.0.5.0/24] name: example-cluster ``` #### Manager This is the one cluster configuration that cannot be set using the Helm chart because it defines how Telepresence connects to the Traffic manager. When not default, that setting needs to be configured in the workstation's kubeconfig for the cluster. The `cluster.defaultManagerNamespace` key contains configuration for finding the `traffic-manager` that telepresence will connect to. Here is an example kubeconfig that will instruct telepresence to connect to a manager in namespace `staging`. The setting can be overridden using the Telepresence connect flag `--manager-namespace`. Please note that the `cluster.defaultManagerNamespace` can be set in the `config.yml` too, but will then not be unique per cluster. ```yaml apiVersion: v1 clusters: - cluster: server: https://127.0.0.1 extensions: - name: telepresence.io extension: cluster: defaultManagerNamespace: staging name: example-cluster ``` [yaml-bool]: https://yaml.org/type/bool.html [yaml-float]: https://yaml.org/type/float.html [yaml-int]: https://yaml.org/type/int.html [yaml-seq]: https://yaml.org/type/seq.html [yaml-str]: https://yaml.org/type/str.html [quantity]: ../common/quantity.md [go-duration]: https://pkg.go.dev/time#ParseDuration [slog-level]: https://pkg.go.dev/log/slog#Level [cidr]: https://www.geeksforgeeks.org/classless-inter-domain-routing-cidr/ ================================================ FILE: docs/reference/dns.md ================================================ --- title: DNS resolution hide_table_of_contents: true --- # DNS resolution The Telepresence DNS resolver is dynamically configured to resolve names using the namespaces currently managed by the Traffic Manager. Processes running locally on the desktop will have network access to all services in the currently connected namespace by service-name only, and to other managed namespaces using service-name.namespace. See this demonstrated below, using the [quick start's](../quick-start.md) sample app services. We'll connect to a namespace in the cluster and list the services that can be intercepted. ``` $ telepresence connect --namespace default Connecting to traffic manager... Connected to context default, namespace default (https://) $ telepresence list deployment web-app: ready to engage (traffic-agent not yet installed) deployment emoji : ready to engage (traffic-agent not yet installed) deployment web : ready to engage (traffic-agent not yet installed) $ curl web-app:80 Emoji Vote ... ``` The DNS resolver will also be able to resolve services using `.` regardless of what namespace the client is connected to as long as the given namespace is among the set managed by the Traffic Manager. ### Supported Query Types The Telepresence DNS resolver is now capable of resolving queries of type `A`, `AAAA`, `CNAME`, `MX`, `NS`, `PTR`, `SRV`, and `TXT`. See [Outbound connectivity](routing.md#dns-resolution) for details on DNS lookups. ================================================ FILE: docs/reference/docker-run.md ================================================ --- title: Using Docker for engagements description: How a Telepresence engagement can run a Docker container with configured environment and volume mounts. toc_min_heading_level: 2 toc_max_heading_level: 2 --- # Using Docker when engaging with workloads ## Using command flags ### The docker flag You can start the Telepresence daemon in a Docker container on your laptop using the command: ```console $ telepresence connect --docker ``` ### The telepresence curl command The network interface that is added when connecting using `telepresence connect --docker` will not be accessible directly from the host computer. It is confined to the telepresence daemon container. You can use the `telepresence curl` command to curl your cluster resources. It will run curl in a docker container that shares the network and DNS of the daemon container. ### The telepresence docker-run command You can use the `telepresence docker-run` command to start a container that shares the network and DNS of the daemon container. ### The replace/ingest/intercept/wiretap --docker-run flag You can use the `--docker-run` flag if you want your `replace`, `ingest`, `intercept`, or `wiretap` to use a local handler that runs in a container. It will establish the engagement, run your container in the foreground, and then automatically end the engagement when the container exits. It will also ensure that the container shares the network and DNS of the daemon container. Please note that there your flags are divided into three groups when using `--docker-run` - General flags and arguments passed to the telepresence `replace`, `ingest`, `intercept`, or `wiretap` such as the workload name or the port to intercept. The `--docker-run` flag is in itself an example of a general flag. - Flags and arguments passed to the `docker run` command such as `--env A=B`. - Flags and arguments passed to the container that is started. The syntax of the command is ``` $ telepresence replace -- ``` In essence, everything after the stand-alone double dash `--` is sent to the `docker run`. The `--` separates flags intended for `telepresence replace/ingest/intercept/wiretap` from flags intended for `docker run`. It's recommended that you always use the `--docker-run` in combination with a connection started with the `telepresence connect --docker`, because that makes everything less intrusive: - No admin user access is needed. Network modifications are confined to a Docker network. - There's no need for special filesystem mount software like MacFUSE or WinFSP. The volume mounts happen in the Docker engine. The following happens under the hood when both flags are in use: - The local container will use a network controlled by the Teleroute network driver. This guarantees that the handler can access the Telepresence VIF, and hence access the cluster. - The local container is configured to use DNS provided by the daemon container. - Volume mounts will be automatic and made using the Telemount Docker volume plugin so that all volumes exposed by the targeted remote container are mounted on the local handler container. - The environment of the remote container becomes the environment of the local handler container. ### The docker-build flag The `--docker-build ` and the repeatable `docker-build-opt key=value` flags enable containers to be built on the fly by the replace/ingest/intercept/wiretap command. When using `--docker-build`, the image name used in the argument list must be verbatim `IMAGE`. The word acts as a placeholder and will be replaced by the ID of the image that is built. The presence of the word `IMAGE` is hence what separates the flags and arguments sent to `docker run` from the ones sent to the container. The `--docker-build` flag implies `--docker-run`. ### The docker-debug flag This flag is just like --docker-build, but allows a debugger to run inside the container with relaxed security. ## Using docker-run flag without docker It is possible to use `--docker-run` with a daemon running on your host, which is the default behavior of Telepresence. However, it isn't recommended since you'll be in a hybrid mode: while your handler runs in a container, the daemon will modify the host network, and if remote mounts are desired, they may require extra software. The ability to use this special combination is retained for backward compatibility reasons. It might be removed in a future release of Telepresence. The `--port` flag has slightly different semantics and can be used in situations when the local and container port must be different. This is done using `--port :`. The container port will default to the local port when using the `--port ` syntax. ## Examples Imagine you are working on a new version of your frontend service. It is running in your cluster as a Deployment called `frontend-v1`. You use Docker on your laptop to build an improved version of the container called `frontend-v2`. To test it out, use this command to run the new container on your laptop and start an intercept of the cluster service to your local container. ```console $ telepresence connect --docker $ telepresence replace frontend-v1 --docker-run -- frontend-v2 ``` Now, imagine that the `frontend-v2` image is built by a `Dockerfile` that resides in the directory `images/frontend-v2`. You can build and replace directly. ```console $ telepresence replace frontend-v1 --docker-build images/frontend-v2 --docker-build-opt tag=mytag -- IMAGE ``` ## Automatic flags Telepresence will automatically pass some relevant flags to Docker to connect the container with the remote container. Those flags are combined with the arguments given after `--` on the command line. - `--env-file ` Loads the remote environment - `--name intercept--` Names the Docker container, this flag is omitted if explicitly given on the command line - `-v ` Volume mount specification, see CLI help for `--docker-mount` flags for more info When used with a container based daemon: - `--rm` Mandatory, because the volume mounts cannot be removed until the container is removed. - `-v :` Volume mount specifications propagated from the engaged container - `--network ` Network is shared with the containerized daemon When used with a daemon that isn't container based: - `--dns-search tel2-search` Enables single label name lookups in the connected namespace - `-p ` The local port for the intercept and the container port ================================================ FILE: docs/reference/engagements/cli.md ================================================ --- title: Configure workload engagements using CLI --- # Configuring workload engagements using CLI ## Specifying a namespace for an engagement The namespace of the engaged workload is specified during connect using the `--namespace` option. ```shell telepresence connect --namespace myns telepresence replace/ingest/intercept/wiretap hello ``` ## Importing environment variables Telepresence can import the environment variables from the pod that is being engaged, see [this doc](../environment.md) for more details. ## Creating an intercept The following command will intercept HTTP requests using the header 'X-User: susan' bound to the service and proxy it to your laptop. This includes traffic coming through your ingress controller, so use this option carefully as to not disrupt production environments. ```shell telepresence intercept --http-header x-user=susan --port= ``` Run `telepresence list` to see the list of active intercepts. ```console $ telepresence list deployment dataprocessingnodeservice: intercepted Intercept name: State : ACTIVE Workload kind : Deployment Intercepting : 10.244.0.13 -> 127.0.0.1 8080 -> 8080 TCP Intercepting : Intercepting : HTTP requests with header 'X-User: susan' ``` Start a service on your laptop that will receive the intercepted traffic on port 8080, for instance: ```console $ python3 -m http.server 8080 ``` Use curl to send requests to the intercepted service using the header 'X-User: susan'. The service running locally should be the one to respond to the request. ```console $ curl -H "X-User: susan" http:/// Reply from service running on your local host ``` Run the same curl command again, but this time without the header 'X-User: susan'. Now the server running in the cluster should respond to the request. ```console $ curl http:/// Reply from service running in your cluster ``` Finally, run `telepresence leave ` to stop the intercept. If you want to change which port has been intercepted, you can create a new intercept the same way you did above, and it will change which service port is being intercepted. ## Creating an intercept when multiple services match your workload Oftentimes, there's a 1-to-1 relationship between a service and a workload, so telepresence is able to auto-detect which service it should intercept based on the workload you are trying to intercept. But if you use something like [Argo](https://www.getambassador.io/docs/argo/latest/), there may be two services (that use the same labels) to manage traffic between a canary and a stable service. Fortunately, if you know which service you want to use when intercepting a workload, you can use the `--service` flag. So in the aforementioned example, if you wanted to use the `echo-stable` service when intercepting your workload, your command would look like this: ```console $ telepresence intercept echo-rollout- --port --service echo-stable Using ReplicaSet echo-rollout- intercepted Intercept name : echo-rollout- State : ACTIVE Workload kind : ReplicaSet Destination : 127.0.0.1:3000 Volume Mount Point: /var/folders/cp/2r22shfd50d9ymgrw14fd23r0000gp/T/telfs-921196036 Intercepting : all TCP connections ``` ## Intercepting multiple ports It is possible to intercept more than one service and/or service port that are using the same workload. You do this by repeating the `--port` flag. Let's assume that we have a service `multi-echo` with the two ports `http` and `grpc`. They are both targeting the same `multi-echo` deployment. ```console $ telepresence intercept multi-echo-http --workload multi-echo --port 8080:http --port 8443:grpc Using Deployment multi-echo intercepted Intercept name : multi-echo-http State : ACTIVE Workload kind : Deployment Intercepting : 10.1.54.120 -> 127.0.0.1 8080 -> 8080 TCP 8443 -> 8443 TCP Volume Mount Point : /tmp/telfs-893700837 ``` ## Port-forwarding an intercepted container's sidecars Sidecars are containers that sit in the same pod as an application container; they usually provide auxiliary functionality to an application, and can usually be reached at `localhost:${SIDECAR_PORT}`. For example, a common use case for a sidecar is to proxy requests to a database, your application would connect to `localhost:${SIDECAR_PORT}`, and the sidecar would then connect to the database, perhaps augmenting the connection with TLS or authentication. When intercepting a container that uses sidecars, you might want those sidecars' ports to be available to your local application at `localhost:${SIDECAR_PORT}`, exactly as they would be if running in-cluster. Telepresence's `--to-pod ${PORT}` flag implements this behavior, adding port-forwards for the port given. ```console $ telepresence intercept --port=: --to-pod= Using Deployment intercepted Intercept name : State : ACTIVE Workload kind : Deployment Destination : 127.0.0.1: Service Port Identifier: Intercepting : all TCP connections ``` If there are multiple ports that you need forwarded, simply repeat the flag (`--to-pod= --to-pod=`). ## Intercepting headless services Kubernetes supports creating [services without a ClusterIP](https://kubernetes.io/docs/concepts/services-networking/service/#headless-services), which, when they have a pod selector, serve to provide a DNS record that will directly point to the service's backing pods. Telepresence supports intercepting these `headless` services as it would a regular service with a ClusterIP. So, for example, if you have the following service: ```yaml --- apiVersion: v1 kind: Service metadata: name: my-headless spec: type: ClusterIP clusterIP: None selector: service: my-headless ports: - port: 8080 targetPort: 8080 --- apiVersion: apps/v1 kind: StatefulSet metadata: name: my-headless labels: service: my-headless spec: replicas: 1 serviceName: my-headless selector: matchLabels: service: my-headless template: metadata: labels: service: my-headless spec: containers: - name: my-headless image: jmalloc/echo-server ports: - containerPort: 8080 resources: {} ``` You can intercept it like any other: ```console $ telepresence intercept my-headless --port 8080 Using StatefulSet my-headless intercepted Intercept name : my-headless State : ACTIVE Workload kind : StatefulSet Destination : 127.0.0.1:8080 Volume Mount Point: /var/folders/j8/kzkn41mx2wsd_ny9hrgd66fc0000gp/T/telfs-524189712 Intercepting : all TCP connections ``` > [!IMPORTANT] > This utilizes an `initContainer` that requires `NET_ADMIN` capabilities. > If your cluster administrator has disabled them, you will be unable to use numeric ports with the agent injector. ## Intercepting without a service You can intercept a workload without a service by adding an annotation that informs Telepresence what container ports that are eligable for intercepts. Telepresence will then inject a traffic-agent when the workload is deployed, and you will be able to intercept the given ports as if they were service ports. The annotation is: ```yaml annotations: telepresence.io/inject-container-ports: http ``` The annotation value is a comma separated list of port identifiers consisting of either the name or the port number of a container port, optionally suffixed with `/TCP` or `/UDP` ### Let's try it out! 1. Deploy an annotation similar to this one to your cluster: ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: echo-no-svc labels: app: echo-no-svc spec: replicas: 1 selector: matchLabels: app: echo-no-svc template: metadata: labels: app: echo-no-svc annotations: telepresence.io/inject-container-ports: http spec: automountServiceAccountToken: false containers: - name: echo-server image: ghcr.io/telepresenceio/echo-server:latest ports: - name: http containerPort: 8080 env: - name: PORT value: "8080" resources: limits: cpu: 50m memory: 8Mi ``` 2. Connect telepresence: ```console $ telepresence connect Launching Telepresence User Daemon Launching Telepresence Root Daemon Connected to context kind-dev, namespace default (https://127.0.0.1:36767) ``` 3. List your intercept eligible workloads. If the annotation is correct, the deployment should show up in the list: ```console $ telepresence list deployment echo-no-svc: ready to engage (traffic-agent not yet installed) ``` 4. Start an intercept handler locally that will receive the incoming traffic. Here's an example using a simple python http service: ```console $ python3 -m http.server 8080 ``` 5. Create an intercept: ```console $ telepresence intercept echo-no-svc Using Deployment echo-no-svc Intercept name : echo-no-svc State : ACTIVE Workload kind : Deployment Destination : 127.0.0.1:8080 Volume Mount Point: /tmp/telfs-3306285526 Intercepting : all TCP connections Address : 10.244.0.13:8080 ``` Note that the response contains an "Address" that you can curl to reach the intercepted pod. You will not be able to curl the name "echo-no-svc". Since there's no service by that name, there's no DNS entry for it either. 6. Curl the intercepted workload: ```console $ curl 10.244.0.13:8080 < output from your local service> ``` > [!IMPORTANT] > A service-less intercept utilizes an `initContainer` that requires `NET_ADMIN` capabilities. > If your cluster administrator has disabled them, you will only be able to intercept services using symbolic target ports. ## Specifying the engagement traffic target By default, it's assumed that your local app is reachable on `127.0.0.1` or on the IP of the local container that is running that app, and intercepted traffic will be sent to that address at the port given by `--port`. If you wish to change this behavior and send traffic to a different address, you can use the `--address` parameter to `telepresence intercept/replace/wiretap`. Say your machine is configured to respond to HTTP requests for an intercept on a container named "stoic_galois". You would run this as: ```console $ telepresence intercept my-service --address stoic-galois --port 8080 Using Deployment my-service Intercept name: my-service State : ACTIVE Workload kind : Deployment Intercepting : 127.0.0.1 -> stoic-galois 8080 -> 8080 TCP ``` ## Replacing a running workload By default, your application container continues to run while Telepresence intercepts its traffic. This can cause issues for applications with ongoing background activities, such as consuming from a message queue. To address this, the `telepresence intercept` command provides the `--replace` flag. When used, the Traffic Agent replaces the application container within the pod. This ensures that the application itself is not running and avoids unintended side effects. The original application container is automatically restored once the intercept session ends. ```console $ telepresence intercept my-service --port 8080 --replace Intercept name : my-service State : ACTIVE Workload kind : Deployment Destination : 127.0.0.1:8080 Service Port Identifier: proxied Volume Mount Point : /var/folders/j8/kzkn41mx2wsd_ny9hrgd66fc0000gp/T/telfs-517018422 Intercepting : all TCP connections ``` > [!NOTE] > Sidecars will not be stopped. Only the targeted container will be removed from the pod. ================================================ FILE: docs/reference/engagements/conflicts.md ================================================ --- title: Dealing With Conflicting Engagements --- # Dealing With Conflicting Engagements ## The Problem An organization may have several developers working on the same project, which in turn means that they may be engaging with the same workloads at the same time. Intercepting with unique http-header filters is often a good way to deal with this, but in some cases it may be necessary to use a global intercept or even to replace the entire container. Also, in some cases, perhaps a user intercepts using an http-header filter that is too broad and therefore causes conflicts with other users. Sometimes the conflict is unavoidable. The user owning the first intercept must simply finish their work in order for others to continue. However, in other cases, perhaps that user has gone home for the day or got distracted by other tasks, not realizing that their intercept is still active and might cause problems for others. ## The Solution Telepresence handles this situation in three ways: 1. An inactive client has a limited time to live before the session is terminated. The termination is performed by the Traffic Manager but is similar to a user disconnecting using `telepresence quit`. This timeout is controlled by Helm chart value `grpc.connectionTTL` and defaults to 24 hours. 2. An inactive client will normally retain ongoing intercepts for the duration of the `grpc.connectionTTL`, but the right to block other intercept attempts will be lost after a shorter timeout period controlled by Helm chart value `intercept.inactiveBlockTimeout`. This defaults to 10 minutes. 3. In some cases, a client may be considered active even though no user is behind the keyboard (see [Active Client Semantics](#active-client-semantics) below). The command `telepresence revoke ` can be used to terminate such an intercept. Since this command is somewhat intrusive, it can only be performed by users that have RBAC permissions to get and update the "traffic-manager" configmap in the traffic-manager's namespace. This means that a user may well close the lid on their laptop and come back the next day to continue working on their intercept, but only if that intercept didn't cause conflicts with other users. If it did, then it will be flagged as a conflict and the other user's attempt will instead succeed. ## Active Client Semantics A client is considered active as long as the user interacts with Telepresence. Either by using the CLI or by issuing TCP network requests that trigger the creation of new tunnels to the cluster. Neither UDP network requests - which often originate from DNS requests that aren't related to Telepresence - nor inbound connections initiated from an intercepted pod, will make the client active. > [!NOTE] > The client will remain active if it runs processes that periodically use the Telepresence VIF to send TCP traffic to the cluster. ================================================ FILE: docs/reference/engagements/container.md ================================================ --- title: Target a specific container --- # Target a specific container A `telepresence replace` or `telepresence ingest` will always target a specific container, and the `--container` flag is mandatory when the workload has more than one container. A `telepresence intercept` or `telepresence wiretap` will ultimately target a specific port within a container. The port is usually determined by examining the relationship between the service's `targetPort` and the container's `containerPort`. In certain scenarios, the container owning the intercepted port differs from the container the intercept targets. This container's sole purpose is to route traffic from the service to the intended container, often using a direct localhost connection. Use the `--container` flag with the intercept in these scenarios. ## No intercept Consider the following scenario: ![no-intercept](../../images/secondary-no-intercept.png) ## Standard Intercept During a replace, the Telepresence traffic-agent will redirect all traffic intended for the replaced container to the workstation. It will also make the environment and mounts for the **Nginx container** available, because it is considered to be the one targeted by the intercept. During an intercept, the Telepresence traffic-agent will redirect the `http` port to the workstation. It will also make the environment and mounts for the **Nginx container** available, because it is considered to be the one targeted by the intercept. ```console $ telepresence intercept myservice --port http ``` ![normal-intercept](../../images/secondary-normal-intercept.png) ## Intercept With --container The `--container ` intercept flag is useful when the objective is to work with the App container locally. While this option doesn't influence the port selection, it guarantees that the environment variables and mounts propagated to the workstation originate from the specified container. ```console $ telepresence intercept myservice --port http --container app ``` ![container-intercept](../../images/secondary-container-intercept.png) ================================================ FILE: docs/reference/engagements/sidecar.md ================================================ --- title: Traffic Agent Sidecar --- # Traffic Agent Sidecar When replacing a container or intercepting a service, the Telepresence Traffic Manager ensures that a Traffic Agent has been injected into the targeted workload. The injection is triggered by a Kubernetes Mutating Webhook and will only happen once. The Traffic Agent is responsible for making the environment and volumes available on the developer's workstation, and also for redirecting traffic to it. When replacing a workload container, all traffic intended for it will be rerouted to the local workstation, unless limited using the `--port` flag. When intercepting, all `tcp` and/or `udp` traffic to the targeted port is sent to the developer's workstation. This means that both a `replace` and an `intercept` will affect all users of the targeted workload. ## Supported workloads Kubernetes has various [workloads](https://kubernetes.io/docs/concepts/workloads/). Currently, Telepresence supports installing a Traffic Agent container on `Deployments`, `ReplicaSets`, `StatefulSets`, and `ArgoRollouts`. A Traffic Agent is installed the first time a user makes a `telepresence replace WORKLOAD`, `telepresence ingest WORKLOAD`, `telepresence intercept WORKLOAD`, `telepresence wiretap WORKLOAD`, or a `telepresence connect --proxy-via CIDR=WORKLAOD`. A Traffic Agent may also be installed up front by adding a `telepresence.io/inject-traffic-agent: enabled` annotation to the WORKLOADS pod template. ### Sidecar injection The actual installation of the Traffic Agent is performed by a mutating admission webhook that calls the agent-injector service in the Traffic Manager's namespace. The configuration for the sidecar, which is automatically generated, resides in the configmap `telepresence-agents`. ### Uninstalling the Traffic Agent A Traffic Agent will normally remain in the workload's pods once it has been installed. It can be explicitly removed by issuing the command `telepresence uninstall WORKLOAD`. It will also be removed if its configuration is removed from the `telepresence-agents` configmap. Removing the `telepresence-agents` configmap will effectively uninstall all injected Traffic Agents from the same namespace. > [!NOTE] > Uninstalling will not work if the Traffic Agent is installed using the pod template annotation. ### Disable Traffic Agent in a workload The Traffic Agent installation can be completely disabled by adding a `telepresence.io/inject-traffic-agent: disabled` annotation to the WORKLOADS pod template. This will prevent all attempts to do anything with the workload that will require a Traffic Agent. ### Disable workloads By default, traffic-manager will observe `Deployments`, `ReplicaSets` and `StatefulSets`. Each workload used today adds certain overhead. If you are not engaging a specific workload type, you can disable it to reduce that overhead. That can be achieved by setting the Helm chart values `workloads..enabled=false` when installing the traffic-manager. The following are the Helm chart values to disable the workload types: - `workloads.deployments.enabled=false` for `Deployments`, - `workloads.replicaSets.enabled=false` for `ReplicaSets`, - `workloads.statefulSets.enabled=false` for `StatefulSets`. ### Enable ArgoRollouts In order to use `ArgoRollouts`, you must pass the Helm chart value `workloads.argoRollouts.enabled=true` when installing the traffic-manager. It is recommended to set the pod template annotation `telepresence.io/inject-traffic-agent: enabled` to avoid creation of unwanted revisions. > [!NOTE] > While many of our examples use Deployments, they would also work on other supported workload types. ================================================ FILE: docs/reference/environment.md ================================================ --- title: Environment variables description: "How Telepresence can import environment variables from your Kubernetes cluster to use with code running on your laptop." hide_table_of_contents: true --- # Environment variables Telepresence will import environment variables from the cluster container when engaging with it. You can use these variables with the code running on your laptop. There are several options available to do this: 1. `telepresence replace --container --env-file ` This will write the environment variables to a file. This file can be used when starting containers locally. The option `--env-syntax` will allow control over the syntax of the file. Valid syntaxes are "docker", "compose", "sh", "csh", "cmd", and "ps" where "sh", "csh", and "ps" can be suffixed with ":export". 2. `telepresence replace --container --env-file --env-syntax=json` This will write the environment variables to a JSON file. This file can be injected into other build processes. 3. `telepresence replace --container -- ` This will run a command locally with the pod's environment variables set on your laptop. Once the command quits the `replace` is stopped (as if `telepresence leave ` was run). This can be used in conjunction with a local server command, such as `python [FILENAME]` or `node [FILENAME]` to run a service locally while using the environment variables that were set on the pod via a ConfigMap or other means. Another use would be running a subshell, Bash for example: 4. `telepresence replace -- /bin/bash` This would start the `replace` and then launch the subshell on your laptop with all the same variables set as on the pod. 5. `telepresence replace --docker-run -- ` This will ensure that the environment is propagated to the container. Will also work for `--docker-build` and `--docker-debug`. ## Telepresence Environment Variables Telepresence adds some useful environment variables in addition to the ones imported from the engaged container: ### TELEPRESENCE_ROOT Directory where all remote volumes mounts are rooted. See [Volume Mounts](volume.md) for more info. ### TELEPRESENCE_MOUNTS Colon separated list of remotely mounted directories. ### TELEPRESENCE_CONTAINER The name of the targeted container. Useful when a pod has several containers, and you want to know which one that was engaged by Telepresence. ================================================ FILE: docs/reference/inside-container.md ================================================ --- title: Running Telepresence inside a container hide_table_of_contents: true --- # Running Telepresence inside a container ## Run with the daemon and engagement handler in containers The `telepresence connect` command now has the option `--docker`. This option tells telepresence to start the Telepresence daemon in a docker container. Running the daemon in a container brings many advantages. The daemon will no longer make modifications to the host's network or DNS, and it will not mount files in the host's filesystem. Consequently, it will not need admin privileges to run, nor will it need special software like macFUSE or WinFSP to mount the remote file systems. The engagement handler (the process that runs locally and optionally will receive intercepted traffic) must also be a docker container, because that is the only way to access the cluster network that the daemon makes available, and to mount the docker volumes needed. ## Run everything in a container Environments like [GitHub Codespaces](https://docs.github.com/en/codespaces/overview) runs everything in a container. Your shell, the telepresence CLI, and both its daemons. This means that the container must be configured so that it allows Telepresence to set up its Virtual Network Interface before you issue a `telepresence connect`. There are several conditions that must be met. - Access to the `/dev/net/tun` device - The `NET_ADMIN` capability - If you're using IPv6, then you also need sysctl `net.ipv6.conf.all.disable_ipv6=0` The Codespaces `devcontainer.json` will typically need to include: ```json "runArgs": [ "--privileged", "--cap-add=NET_ADMIN", ], ``` ## Kubernetes auth plugins If Kubernetes auth plugins are needed, they must be installed into the same container as Telepresence. Each auth plugin will need a different approach. ### AWS IAM Authenticator 1. Install the AWS IAM Authenticator Go binary. ```dockerfile FROM golang:alpine AS auth-builder RUN go install sigs.k8s.io/aws-iam-authenticator/cmd/aws-iam-authenticator@latest # Dockerfile with telepresence and its prerequisites FROM alpine # Install Telepresence prerequisites RUN apk add --no-cache curl iproute2 sshfs # Download and install the telepresence binary RUN curl -fL https://github.com/telepresenceio/telepresence/releases/download/v$version$/telepresence-linux-amd64 -o telepresence && \ install -o root -g root -m 0755 telepresence /usr/local/bin/telepresence COPY --from=auth-builder /go/bin/aws-iam-authenticator ./aws-iam-authenticator RUN install -o root -g root -m 0755 aws-iam-authenticator /usr/local/bin/aws-iam-authenticator && \ rm aws-iam-authenticator ``` 2. Ensure that the authenticator can reach your kubconfig and AWS configuration by mounting them into the container: ```console $ docker run \ --cap-add=NET_ADMIN \ --device /dev/net/tun:/dev/net/tun \ --network=host \ -v ~/.kube/config:/root/.kube/config \ -v ~/.aws:/root/.aws \ -it --rm tp-in-docker ``` ================================================ FILE: docs/reference/monitoring.md ================================================ --- title: Monitoring --- # Monitoring Telepresence offers powerful monitoring capabilities to help you keep a close eye on your telepresence activities and traffic manager metrics. ## Prometheus Integration One of the key features of Telepresence is its seamless integration with Prometheus, which allows you to access real-time metrics and gain insights into your system's performance. With Prometheus, you can monitor various aspects of your traffic manager, including the number of active intercepts and users. Additionally, you can track consumption-related information, such as the number of intercepts used by your developers and how long they stayed connected. To enable Prometheus metrics for your traffic manager, follow these steps: 1. **Configure Prometheus Port** First, you'll need to specify the Prometheus port by setting a new environment variable called `PROMETHEUS_PORT` for your traffic manager. You can do this by running the following command: ```shell telepresence helm upgrade --set-string prometheus.port=9090 ``` 2. **Validate the Prometheus Exposure** After configuring the Prometheus port, you can validate its exposure by port-forwarding the port using Kubernetes: ```shell kubectl port-forward deploy/traffic-manager 9090:9090 -n ambassador ``` 3. **Access Prometheus Dashboard** Once the port-forwarding is set up, you can access the Prometheus dashboard by navigating to `http://localhost:9090` in your web browser: Here, you will find a wealth of built-in metrics, as well as custom metrics (see below) that we have added to enhance your tracking capabilities. | **Name** | **Type** | **Description** | **Labels** | |-----------------------------|----------|-------------------------------------------------------------------------------|------------------------------------------| | `telepresence_agent_count` | Gauge | Number of connected traffic agents. | | | `telepresence_client_count` | Gauge | Number of connected clients. | | | `telepresence_active_intercept_count` | Gauge | Number of active intercepts. | | | `telepresence_session_count` | Gauge | Number of sessions. | | | `telepresence_tunnel_count` | Gauge | Number of tunnels. | | | `telepresence_tunnel_ingress_bytes` | Counter | Number of bytes tunnelled from clients. | | | `telepresence_tunnel_egress_bytes` | Counter | Number of bytes tunnelled to clients. | | | `telepresence_active_http_request_count` | Gauge | Number of currently served HTTP requests. | | | `telepresence_active_grpc_request_count` | Gauge | Number of currently served gRPC requests. | | | `telepresence_connect_count` | Counter | The total number of connects by user. | `client`, `install_id` | | `telepresence_connect_active_status` | Gauge | Flag to indicate when a connect is active. 1 for active, 0 for not active. | `client`, `install_id` | | `telepresence_intercept_count` | Counter | The total number of intercepts by user. | `client`, `install_id`, `intercept_type` | | `telepresence_intercept_active_status` | Gauge | Flag to indicate when an intercept is active. 1 for active, 0 for not active. | `client`, `install_id`, `workload` | 4. **Enable Scraping for Traffic Manager Metrics** To ensure that these metrics are collected regularly by your Prometheus server and to maintain a historical record, it's essential to enable scraping. If you're using the default Prometheus configuration, you can achieve this by specifying specific pod annotations as follows: ```yaml template: metadata: annotations: prometheus.io/path: / prometheus.io/port: "9090" prometheus.io/scrape: "true" ``` These annotations instruct Prometheus to scrape metrics from the Traffic Manager pod, allowing you to track consumption metrics and other important data over time. ## Grafana Integration Grafana plays a crucial role in enhancing Telepresence's monitoring capabilities. While the step-by-step instructions for Grafana integration are not included in this documentation, you have the option to explore the integration process. By doing so, you can create visually appealing and interactive dashboards that provide deeper insights into your telepresence activities and traffic manager metrics. Moreover, we've developed a dedicated Grafana dashboard for your convenience. Below, you can find sample screenshots of the dashboard, and you can access the JSON model for configuration: **JSON Model:** This dashboard is designed to provide you with comprehensive monitoring and visualization tools to effectively manage your Telepresence environment. ```json { "__inputs": [ { "name": "DS_PROMETHEUS", "label": "Prometheus", "description": "", "type": "datasource", "pluginId": "prometheus", "pluginName": "Prometheus" } ], "__elements": {}, "__requires": [ { "type": "panel", "id": "barchart", "name": "Bar chart", "version": "" }, { "type": "grafana", "id": "grafana", "name": "Grafana", "version": "10.1.5" }, { "type": "panel", "id": "piechart", "name": "Pie chart", "version": "" }, { "type": "datasource", "id": "prometheus", "name": "Prometheus", "version": "1.0.0" }, { "type": "panel", "id": "stat", "name": "Stat", "version": "" } ], "annotations": { "list": [ { "builtIn": 1, "datasource": { "type": "grafana", "uid": "-- Grafana --" }, "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "type": "dashboard" } ] }, "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, "id": null, "links": [], "liveNow": false, "panels": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 7, "w": 6, "x": 0, "y": 0 }, "id": 5, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "10.1.5", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "exemplar": false, "expr": "telepresence_agent_count", "instant": true, "legendFormat": "__auto", "range": false, "refId": "A" } ], "title": "Number of connected traffic agents", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 7, "w": 6, "x": 6, "y": 0 }, "id": 6, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "10.1.5", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "exemplar": false, "expr": "telepresence_client_count", "instant": true, "legendFormat": "__auto", "range": false, "refId": "A" } ], "title": "Number of connected clients", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 7, "w": 6, "x": 12, "y": 0 }, "id": 7, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "10.1.5", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "exemplar": false, "expr": "telepresence_active_intercept_count", "instant": true, "legendFormat": "__auto", "range": false, "refId": "A" } ], "title": "Number of active intercepts", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 7, "w": 6, "x": 18, "y": 0 }, "id": 8, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "10.1.5", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "exemplar": false, "expr": "telepresence_session_count", "instant": true, "legendFormat": "__auto", "range": false, "refId": "A" } ], "title": "Number of sessions", "type": "stat" } ], "refresh": "", "schemaVersion": 38, "style": "dark", "tags": [], "templating": { "list": [] }, "time": { "from": "now-30d", "to": "now" }, "timepicker": {}, "timezone": "", "title": "Telepresence", "uid": "d99c884a-8f4f-43f8-bd4e-bd68e47f100d", "version": 5, "weekStart": "" } ``` ================================================ FILE: docs/reference/plugins.md ================================================ --- title: Telepresence Docker Plugins toc_min_heading_level: 2 toc_max_heading_level: 2 --- # Telepresence Docker Plug-ins A Telepresence client started with `telepresence connect --docker` will run in a Docker container. This is great because it means that the network that it creates, and the volumes that it mounts, will not interfere with the network and mounts on the workstation. Thanks to the Docker plugins Teleroute and Telemount, it also means that those resources are available to other containers in the form of a Docker network and as Docker volumes. ## Teleroute Network Plugin The Telepresence Teleroute Docker network plugin is installed on demand and ensures that the cluster networks made available by the daemon's virtual network interface (VIF) can be reached from other containers without interfering with those container's network mode. ### Technical brief This is the sequence of events that occur when the user runs `telepresence connect --docker`: 1. The Telepresence CLI checks if the Teleroute network driver plugin is present and installs it automatically[^1] if it's not found. 2. The Telepresence CLI starts the Telepresence daemon container using the default bridge network, so that it's assigned an IP address that the Teleroute network plugin can connect to. 3. The Telepresence daemon starts its `teleroute` gRPC service, and creates the "br-daemon" network bridge. 4. The Telepresence CLI creates a network that uses the same name as the telepresence connection[^1]. It is configured to use the teleroute network driver and to connect to the Telepresence daemon's `teleroute` gRPC service using the daemon's IP address on the default bridge[^3]. 5. The Telepresence CLI connects the daemon to the new network. The network driver forwards the join request to the daemon's `teleroute` service. 6. In response, the `teleroute` service creates a veth-pair, sets the "br-daemon" bridge as `master` over one end of that pair and moves the other end (the peer) to the network plugin's network namespace. 7. The network plugin then moves the veth peer again. This time to the connecting container's namespace (in this particular case, back to the daemon container). Steps similar to 5, 6, and 7 will happen when another container connects to the teleroute network. The only difference is that now, the plugin also adds routes to the container, enabling it to connect to the subnets exposed by the daemon's VIF: 1. The network driver forwards the join request to the daemon's `teleroute` service. 2. In response, the `teleroute` service creates a veth-pair, sets the "br-daemon" bridge as `master` over one end of that pair and moves the other end (the peer) to the network plugin's network namespace. 3. The network plugin then moves the veth peer again. This time to the connecting container's namespace. 4. The network plugin adds the routes necessary to reach the daemon's VIF device via the daemon's IP to the connecting container. #### DNS A container will get all routes necessary to reach cluster resources by connecting to the teleroute network. It will also be configured to use the Docker internal DNS so that all containers on the same network can find each other. It is, however, not configured with a DNS that resolves names on the cluster side. One additional step is needed in order for that to happen. The container must be started with `--dns ` where the `` is the IP that the daemon container was assigned when it connected to the teleroute network. The container IP can be retrieved and then used together with the network like this: ```console $ telepresence connect --docker --name blue $ DAEMON_IP=$(telepresence status --output json | jq -r .daemon.dns.local_address | sed -e 's/:53//') $ docker run --network blue --dns $DAEMON_IP --rm -it jonlabelle/network-tools ``` which is equivalent to using the Telepresence built-in `docker-run` command like this: ```console $ telepresence connect --docker --name blue $ telepresence docker-run --rm -it jonlabelle/network-tools ``` When Telepresence starts a docker container, either by using `telepresence curl`, `telepresence docker-run` commands, or by using the `--docker-{run|build|debug}` flag in an engagement command, the following happens: 1. The container will automatically be connected to the teleroute network. 2. The container will get its DNS configured to use the DNS server exposed by the Telepresence daemon. Picture showing a Teleroute network connected to a daemon container and three local services ![Architecture](../images/teleroute.svg) ## Telemount Volume Plugin The Telepresence Telemount Docker volume plugin is installed on demand and ensures that remote directories that are made available by SFTP-servers in the traffic-agents can be mounted as Docker volumes. The driver is configured when a containerized Telepresence daemon is started, so that volumes can be created when Telepresence engages with a remote container using `telepresence {ingest|intercept|replace|wiretap}`. These commands make a port available in the daemon that connects the network driver with the remote SFTP-server. This is the sequence of events that occur when the user runs `telepresence connect --docker`: 1. A check is made if the Telemount volume plugin is present. If not, it is installed automatically[^4]. 2. The daemon configures itself to use a bridge mounter. This mounter is just a proxy that makes the port used by a remote traffic-agent's SFTP server available on the daemon containers localhost. When Telepresence engages a container using the `--docker-{run|build|debug}` flag in an engagement command, the following happens: 1. A full list of volumes to be mounted is established. The command options are scanned for `-v`, `--volume` and `--mount` flags, and those flags are then merged with the mounts propagated from the traffic-agent of the engaged pod. The command options are given priority in this merge. 2. A `docker create volume --driver=telemount` is executed for each volume in the list, passing the port number of the proxied SFTP server to the volume plugin. 3. The container is started with `-v` flags appointing the newly created volumes. 4. The telemount performs SFTP mounts of the volumes, as needed. When the container engagement ends, the volumes are unmounted and removed, and the telepresence daemon closes the SFTP proxy. [^1]: The plugin registry, name, and tag can be fully configured using the `docker.teleroute` in the `config.yml` configuration file. [^2]: The connection name is either given by the user using the `--name ` flag, or automatically generated from the kubernetes context and cluster namespace, e.g. "docker-desktop-default" [^3]: The default port used for this connection is 4039, but it can be reconfigured using the `grpc.teleroutePort` in the `config.yml` configuration file. [^4]: The plugin registry, name, and tag can be fully configured using the `docker.telemount` in the `config.yml` configuration file. ================================================ FILE: docs/reference/rbac.md ================================================ --- title: RBAC toc_min_heading_level: 2 toc_max_heading_level: 2 --- # Telepresence RBAC The intention of this document is to provide a template for securing and limiting the permissions of Telepresence. This documentation covers the full extent of permissions necessary to administrate Telepresence components in a cluster. There are two general categories for cluster permissions with respect to Telepresence. There are RBAC settings for a User and for an Administrator described above. The User is expected to only have the minimum cluster permissions necessary to create a Telepresence [engagement](../howtos/engage.md), and otherwise be unable to affect Kubernetes resources. In addition to the above, there is also a consideration of how to manage Users and Groups in Kubernetes which is outside of the scope of the document. This document will use Service Accounts to assign Roles and Bindings. Other methods of RBAC administration and enforcement can be found on the [Kubernetes RBAC documentation](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) page. ## Requirements - Kubernetes version 1.16+ - Cluster admin privileges to apply RBAC ## Editing your kubeconfig This guide also assumes that you are utilizing a kubeconfig file that is specified by the `KUBECONFIG` environment variable. This is a `yaml` file that contains the cluster's API endpoint information as well as the user data being supplied for authentication. The Service Account name used in the example below is called tp-user. This can be replaced by any value (i.e. John or Jane) as long as references to the Service Account are consistent throughout the `yaml`. After an administrator has applied the RBAC configuration, a user should create a `config.yaml` in your current directory that looks like the following: ```yaml apiVersion: v1 kind: Config clusters: - name: my-cluster # Must match the cluster value in the contexts config cluster: ## The cluster field is highly cloud-dependent. contexts: - name: my-context context: cluster: my-cluster # Must match the name field in the clusters config user: tp-user users: - name: tp-user # Must match the name of the Service Account created by the cluster admin user: token: # See note below ``` The Service Account token will be obtained by the cluster administrator after they create the user's Service Account. Creating the Service Account will create an associated Secret in the same namespace with the format `-token-`. This token can be obtained by your cluster administrator by running `kubectl get secret -n ambassador -o jsonpath='{.data.token}' | base64 -d`. After creating `config.yaml` in your current directory, export the file's location to KUBECONFIG by running `export KUBECONFIG=$(pwd)/config.yaml`. You should then be able to switch to this context by running `kubectl config use-context my-context`. ## Administrating Telepresence Telepresence administration requires permissions for creating the `traffic-manager` [deployment](architecture.md#traffic-manager) which is typically done by a full cluster administrator. Once installed, the Telepresence Traffic Manager will run using the `traffic-manager` ServiceAccount. This account is set up differently depending on if the manager is installed using a dynamic or a static namespace selector. ### Installation without, or with dynamic, namespace selection The Traffic Manager will require cluster wide access to several resources when it lacks a namespace selector, or when it is configured with a dynamic namespace selector. ### Traffic Manager Permissions These are the permissions required by the `traffic-manager` account in such a configuration: ```yaml --- apiVersion: v1 kind: ServiceAccount metadata: name: traffic-manager namespace: ambassador --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: traffic-manager rules: - apiGroups: ["apps"] resources: ["deployments", "replicasets", "statefulsets"] verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["namespaces"] verbs: ["get", "list"] - apiGroups: ["events.k8s.io"] resources: ["events"] verbs: ["get", "watch"] - apiGroups: [""] resources: ["pods"] verbs: ["get", "list", "watch", "patch"] # patch not needed when agentInjector.enabled is set to false - apiGroups: ["networking.k8s.io"] resources: ["servicecidrs"] verbs: ["list"] # If argoRollouts.enabled is set to true - apiGroups: ["argoproj.io"] resources: ["rollouts"] verbs: ["get", "list", "watch"] # When using podCIDRStrategy nodePodCIDRs - apiGroups: [""] resources: ["nodes"] verbs: ["get", "list", "watch"] # The following is not needed when agentInjector.enabled is set to false - apiGroups: [""] resources: ["pods"] verbs: ["patch"] - apiGroups: ["apps"] resources: ["deployments", "replicasets", "statefulsets"] verbs: ["patch"] # If argoRollouts.enabled is set to true - apiGroups: ["argoproj.io"] resources: ["rollouts"] verbs: ["patch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: traffic-manager subjects: - name: traffic-manager kind: ServiceAccount namespace: default roleRef: apiGroup: rbac.authorization.k8s.io name: traffic-manager kind: ClusterRole ``` ### Installation with static namespace selection The permissions required by the `traffic-manager` account in a statically namespaced configuration is very similar to the ones used in a dynamic configuration, but a `Role`/`RoleBinding` will be installed in each managed namespace instead of the `ClusterRole`/`ClusterRoleBinding` pair. > [!NOTE] > One `ClusterRole/ClusterRoleBinding` will still be present that permits the traffic-manager to list the `servicecidr` resource. > That resource is cluster wide, so the following cluster wide rule is required: > > ```yaml > - apiGroups: ["networking.k8s.io"] > resources: ["servicecidrs"] > verbs: ["list"] > ``` ## Telepresence Client Access A Telepresence client requires just a small set of RBAC permissions. The bare minimum to connect is the ability to create a port-forward to the traffic-manager. The following configuration assumes that a ServiceAccount "tp-user" has been created in the traffic-manager's default "ambassador" namespace. In order to connect, the client must resolve the traffic-manager service name into a pod-IP and set up a port-forward. This requires the following Role/RoleBinding in the Traffic Manager's namespace. ```yaml kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: name: traffic-manager-connect namespace: ambassador rules: - apiGroups: [""] resources: ["pods"] verbs: ["get", "list"] - apiGroups: [""] resources: ["services"] resourceNames: ["traffic-manager"] verbs: ["get"] - apiGroups: [""] resources: ["pods/portforward"] verbs: ["create"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: traffic-manager-connect namespace: ambassador subjects: - kind: ServiceAccount name: tp-user namespace: ambassador roleRef: kind: Role name: traffic-manager-connect apiGroup: rbac.authorization.k8s.io ``` Once connected, it is desirable, but not necessary that the client can create port-forwards directly to Traffic Agents in the namespace that it is connected to. The lack of this permission will cause all traffic to be routed via the Traffic Manager, which will have a slightly negative impact on throughput. It's recommended that the client also has the following permissions in a dynamic namespaces installation: ```yaml kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: telepresence-ambassador rules: - apiGroups: - "" resources: ["namespaces"] verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["pods"] verbs: ["get"] # Necessary if the client should be able to gather the pod logs - apiGroups: [""] resources: ["pods"] verbs: ["list"] - apiGroups: [""] resources: ["pods/log"] verbs: ["get"] # All traffic will be routed via the traffic-manager unless a portforward can be created directly to a pod - apiGroups: [""] resources: ["pods/portforward"] verbs: ["create"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: telepresence-ambassador subjects: - kind: ServiceAccount name: tp-user namespace: ambassador roleRef: kind: ClusterRole name: telepresence-ambassador apiGroup: rbac.authorization.k8s.io ``` The corresponding configuration for a static namespace installation, for each namespaece that the client should be able to access: ```yaml kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: name: telepresence-client rules: - apiGroups: [""] resources: ["pods"] verbs: ["get"] # Necessary if the client should be able to gather the pod logs - apiGroups: [""] resources: ["pods"] verbs: ["list"] - apiGroups: [""] resources: ["pods/log"] verbs: ["get"] # All traffic will be routed via the traffic-manager unless a portforward can be created directly to a pod - apiGroups: [""] resources: ["pods/portforward"] verbs: ["create"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: telepresence-client subjects: - kind: ServiceAccount name: tp-user namespace: ambassador roleRef: kind: Role name: telepresence-client apiGroup: rbac.authorization.k8s.io ``` The user will also need the [Traffic Manager connect permission](#traffic-manager-permissions) described above. ================================================ FILE: docs/reference/restapi.md ================================================ # Telepresence RESTful API server Telepresence can run a RESTful API server on the local host, both on the local workstation and in a pod that contains a `traffic-agent`. The server currently has three endpoints. The standard `healthz` endpoint, the `consume-here` endpoint, and the `intercept-info` endpoint. ## Enabling the server The server is enabled by setting the `telepresenceAPI.port` to a valid port number in the [Telepresence Helm Chart](https://github.com/telepresenceio/telepresence/tree/release/v2/charts/telepresence). The value may be passed explicitly to Helm during the installation. ## Querying the server On the cluster's side, it's the `traffic-agent` of potentially intercepted pods that runs the server. The server can be accessed using `http://:/` from the application container. Telepresence ensures that the container has the `TELEPRESENCE_API_HOST` and `TELEPRESENCE_API_PORT` environment variable set when the `traffic-agent` is installed. On the workstation, it is the `user-daemon` that runs the server. It uses the `TELEPRESENCE_API_PORT` that is conveyed in the environment of the intercept. This means that the server can be accessed the exact same way locally if the environment is propagated correctly to the interceptor process. ## Endpoints The `consume-here` and `intercept-info` endpoints are both intended to be queried with an optional path query and a set of headers, typically obtained from a Kafka message or similar. Telepresence provides the ID of the intercept in the environment variable `TELEPRESENCE_INTERCEPT_ID` during an intercept. This ID must be provided in a `x-telepresence-caller-intercept-id: = ` header. Telepresence needs this to identify the caller correctly. The `` will be empty when running in the cluster, but it's harmless to provide it there too, so there's no need for conditional code. There are three prerequisites to fulfill before testing The `consume-here` and `intercept-info` endpoints using `curl -v` on the workstation: 1. An intercept must be active 2. The "/healthz" endpoint must respond with OK 3. The ID of the intercept must be known. It will be visible as `ID` in the output of `telepresence list --debug`. ### healthz The `http://localhost:/healthz` endpoint should respond with status code 200 OK. If it doesn't, then something isn't configured correctly. Check that the `traffic-agent` container is present and that the `TELEPRESENCE_API_PORT` has been added to the environment of the application container and/or in the environment that is propagated to the interceptor that runs on the local workstation. #### test endpoint using curl A `curl -v` call can be used to test the endpoint when an intercept is active. This example assumes that the API port is configured to be 9980. ```console $ curl -v localhost:9980/healthz * Host localhost:9980 was resolved. * IPv6: ::1 * IPv4: 127.0.0.1 * Trying [::1]:9980... * Connected to localhost (::1) port 9980 * using HTTP/1.x > GET /healthz HTTP/1.1 > Host: localhost:9980 > User-Agent: curl/8.11.1 > Accept: */* > * Request completely sent off < HTTP/1.1 200 OK < Date: Fri, 03 Oct 2025 05:39:47 GMT < Content-Length: 0 < * Connection #0 to host localhost left intact ``` ### consume-here `http://:/consume-here` will respond with "true" (consume the message) or "false" (leave the message on the queue). When running in the cluster, this endpoint will respond with `false` if the headers match an ongoing intercept for the same workload because it's assumed that it's up to the intercept to consume the message. When running locally, the response is inverted. Matching headers means that the message should be consumed. #### test endpoint using curl Assuming that the API-server runs on port 9980, that the intercept was started with `--http-header x=y`, we can now check that the "/consume-here" returns "true" for given headers. Use `telepresence list --debug` to find the ID of the intercept. ```console $ curl -v localhost:9980/consume-here -H 'x-telepresence-caller-intercept-id:9dbb0afa-38ec-48e2-a975-e664e579e197:apitest' -H 'x:y' * Host localhost:9980 was resolved. * IPv6: ::1 * IPv4: 127.0.0.1 * Trying [::1]:9980... * Connected to localhost (::1) port 9980 * using HTTP/1.x > GET /consume-here HTTP/1.1 > Host: localhost:9980 > User-Agent: curl/8.11.1 > Accept: */* > x-telepresence-caller-intercept-id:9dbb0afa-38ec-48e2-a975-e664e579e197:apitest > x:y > * Request completely sent off < HTTP/1.1 200 OK < Content-Type: application/json < Date: Fri, 03 Oct 2025 05:45:16 GMT < Content-Length: 4 < * Connection #0 to host localhost left intact true ``` If you can run curl from the pod, you can try the exact same URL. The result should be "false" when there's an ongoing intercept. The `x-telepresence-caller-intercept-id` is not needed when the call is made from the pod. ### intercept-info `http://:/intercept-info` is intended to be queried with an optional path query and a set of headers, typically obtained from a Kafka message or similar, and will respond with a JSON structure containing the two booleans `clientSide` and `intercepted`, and a `metadata` map which corresponds to the `--metadata` key pairs used when the intercept was created. This field is always omitted in case `intercepted` is `false`. #### test endpoint using curl Assuming that the API-server runs on port 9980, that the intercept was started with `--http-header x=y --metadata a=b --metadata b=c`, we can now check that the "/intercept-info" returns information for the given path and headers. ```console $ curl -v localhost:9980/intercept-info -H 'x-telepresence-caller-intercept-id:9dbb0afa-38ec-48e2-a975-e664e579e197:apitest' -H 'x:y' * Host localhost:9980 was resolved. * IPv6: ::1 * IPv4: 127.0.0.1 * Trying [::1]:9980... * Connected to localhost (::1) port 9980 * using HTTP/1.x > GET /intercept-info HTTP/1.1 > Host: localhost:9980 > User-Agent: curl/8.11.1 > Accept: */* > x-telepresence-caller-intercept-id:9dbb0afa-38ec-48e2-a975-e664e579e197:echo-easy > x:y > * Request completely sent off < HTTP/1.1 200 OK < Content-Type: application/json < Date: Fri, 03 Oct 2025 05:48:35 GMT < Content-Length: 38 < * Connection #0 to host localhost left intact {"intercepted":true,"clientSide":true},"metadata":{"a":"b","b":"c"}} ``` ================================================ FILE: docs/reference/route-controller.md ================================================ --- title: Route Controller description: Prevent routing loops on local clusters by installing iptables FORWARD DROP rules for the service CIDR. --- # Route Controller ## Overview On local Kubernetes clusters (Kind, minikube, k3d, Docker Desktop), deleted services can cause routing loops when Telepresence is connected: 1. A workstation process reaches a stale service ClusterIP through the TUN device. 2. The TUN device forwards the connection to the traffic-agent. 3. The traffic-agent dials the same stale ClusterIP. 4. No kube-proxy rule exists for it, so the packet follows the node's default route and leaves the cluster. 5. The packet returns to the workstation's TUN device and the loop repeats. The root cause is that local-cluster nodes have no blackhole or reject route for the service CIDR. Deleted (or never-assigned) service IPs fall through to the node's default route and escape to the external network. The **route-controller** is a DaemonSet that prevents these loops. It runs on every node with host networking and `NET_ADMIN` privileges. At startup it discovers the cluster's service CIDRs and inserts a `DROP` rule into the iptables `FORWARD` chain for each CIDR. Any forwarded packet (i.e. pod traffic flowing through the host's network stack) destined for an IP that has no active kube-proxy DNAT rule is dropped rather than escaping via the default route. ### Why iptables FORWARD and not a kernel blackhole route? A kernel `RTN_BLACKHOLE` route for the service subnet would cause `connect()` and `sendmsg()` to fail at the socket level before any iptables hook can fire. This would break locally-generated traffic such as the kube-apiserver calling the mutating webhook, or the route-controller itself reaching the Kubernetes API server. An iptables `FORWARD` rule is only evaluated for traffic that has already been accepted into the host's forwarding path. kube-proxy's PREROUTING DNAT rewrites the destination to a pod IP *before* the FORWARD chain is reached, so rules for active services are completely unaffected. ## Enabling the route controller The route-controller is automatically enabled when the traffic-manager Helm chart is installed with `image.registry` set to `"local"` or a `"localhost:*"` address (the convention for local clusters). It can also be force-enabled or force-disabled explicitly: ```bash # Force-enable (e.g. on a remote cluster that exhibits the same problem) helm upgrade --install traffic-manager charts/telepresence-oss \ --set routeController.enabled=true # Force-disable (e.g. to opt out on a local cluster) helm upgrade --install traffic-manager charts/telepresence-oss \ --set routeController.enabled=false ``` ### Helm values | Value | Default | Description | |---|---|---| | `routeController.enabled` | `null` | `null` = auto-detect, `true` = always enable, `false` = always disable | | `routeController.image.registry` | `""` | Image registry (inherits `image.registry` when empty) | | `routeController.image.name` | `route-controller` | Image name | | `routeController.image.pullPolicy` | `""` | Image pull policy (inherits `image.pullPolicy` when empty) | | `routeController.logLevel` | `""` | Log level (inherits `logLevel` when empty) | | `routeController.serviceCIDRs` | `[]` | Explicit service CIDRs (see [Service CIDR discovery](#service-cidr-discovery)) | ## Service CIDR discovery The route-controller needs to know the cluster's service CIDRs to install the iptables rules. It discovers them in priority order: 1. **`SERVICE_CIDRS` environment variable** — set via `routeController.serviceCIDRs` in Helm values (comma-separated list, e.g. `10.96.0.0/12`). This takes priority over all other methods. 2. **Kubernetes ServiceCIDR API** (`networking.k8s.io/v1`, available in Kubernetes 1.33+) — the controller lists `ServiceCIDR` objects from the cluster automatically. 3. **Fallback** — if neither source is available the controller logs a warning and no iptables rules are installed. Set `routeController.serviceCIDRs` explicitly in that case. For clusters older than Kubernetes 1.33, set the service CIDR explicitly: ```bash helm upgrade --install traffic-manager charts/telepresence-oss \ --set routeController.enabled=true \ --set 'routeController.serviceCIDRs={10.96.0.0/12}' ``` To find the service CIDR of an existing cluster you can inspect the kube-apiserver flags: ```bash kubectl -n kube-system get pod kube-apiserver-$(kubectl get node -o jsonpath='{.items[0].metadata.name}') \ -o jsonpath='{.spec.containers[0].command}' | tr ' ' '\n' | grep service-cluster-ip-range ``` ## Verifying the route controller After enabling, confirm the DaemonSet is running and the rules are in place: ```bash # Check DaemonSet status kubectl -n ambassador get daemonset route-controller # View logs from one node kubectl -n ambassador logs -l app=route-controller --tail=50 # On a Kind node: inspect iptables FORWARD rules docker exec kind-control-plane iptables -L FORWARD -n | grep DROP ``` ## RBAC The route-controller uses a dedicated `ServiceAccount` with a `ClusterRole` that grants: - `get`, `list` on `networking.k8s.io/servicecidrs` (to auto-discover CIDRs on Kubernetes 1.33+) Both resources are created automatically when the route-controller is enabled. ================================================ FILE: docs/reference/routing.md ================================================ --- title: Connection Routing toc_min_heading_level: 2 toc_max_heading_level: 2 --- # Connection Routing ## DNS resolution When requesting a connection to a host, the IP of that host must be determined. Telepresence provides DNS resolvers to help with this task. There are currently four types of resolvers but only one of them will be used on a workstation at any given time. Common for all of them is that they will propagate a selection of the host lookups to be performed in the cluster. The selection normally includes all names ending with `.cluster.local` or a currently mapped namespace but more entries can be added to the list using the `includeSuffixes` or `mappings` option in the [cluster DNS configuration](config.md#dns) ### Cluster side DNS lookups The cluster side host lookup will be performed by a traffic-agent in the connected namespace, or by the traffic-manager if no such agent exists. ### macOS resolver This resolver hooks into the macOS DNS system by creating files under `/etc/resolver`. Those files correspond to some domain and contain the port number of the Telepresence resolver. Telepresence creates one such file for each of the currently mapped namespaces and `include-suffixes` option. The file `telepresence.local` contains a search path that is configured based on currently connected namespace so that single label names can be resolved correctly. ### Linux systemd-resolved resolver This resolver registers itself as part of telepresence's [VIF](tun-device.md) using `systemd-resolved` and uses the DBus API to configure domains and routes that corresponds to the connected namespace and the namespaces managed by the Traffic Manager. ### Linux overriding resolver Linux systems that aren't configured with `systemd-resolved` will use this resolver. A Typical case is when running Telepresence [inside a docker container](inside-container.md). During initialization, the resolver will first establish a _fallback_ connection to the IP passed as `--dns`, the one configured as `local-ip` in the [local DNS configuration](config.md#dns), or the primary `nameserver` registered in `/etc/resolv.conf`. It will then use iptables to actually override that IP so that requests to it instead end up in the overriding resolver, which unless it succeeds on its own, will use the _fallback_. ### Windows resolver This resolver uses the DNS resolution capabilities of the [win-tun](https://www.wintun.net/) device in conjunction with [Win32_NetworkAdapterConfiguration SetDNSDomain](https://docs.microsoft.com/en-us/powershell/scripting/samples/performing-networking-tasks?view=powershell-7.2#assigning-the-dns-domain-for-a-network-adapter). ### DNS caching The Telepresence DNS resolver often changes its configuration. Telepresence will not flush the host's DNS caches. Instead, all records will have a short Time To Live (TTL) so that such caches evict the entries quickly. This causes increased load on the Telepresence resolver (shorter TTL means more frequent queries) and to cater for that, telepresence now has an internal cache to minimize the number of DNS queries that it sends to the cluster. This cache is flushed as needed without causing instabilities. ## Routing ### Subnets The Telepresence `traffic-manager` service is responsible for discovering the cluster's service subnet and all subnets used by the pods. In order to do this, it needs permission to create a dummy service[^1] in its own namespace, and the ability to list, get, and watch nodes and pods. Most clusters will expose the pod subnets as `podCIDR` in the `Node` while others, like Amazon EKS, don't. Telepresence will then fall back to deriving the subnets from the IPs of all pods. If you'd like to choose a specific method for discovering subnets, or want to provide the list yourself, you can use the `podCIDRStrategy` configuration value in the [helm](../install/manager.md) chart to do that. The complete set of subnets that the [VIF](tun-device.md) will be configured with is dynamic and may change during a connection's life cycle as new nodes arrive or disappear from the cluster. The set consists of what that the traffic-manager finds in the cluster, and the subnets configured using the [also-proxy](config.md#alsoproxysubnets) configuration option. Telepresence will remove subnets that are equal to, or completely covered by, other subnets. ### Connection origin A request to connect to an IP-address that belongs to one of the subnets of the [VIF](tun-device.md) will cause a connection request to be made in the cluster. As with host name lookups, the request will originate from a traffic-agent in the connected namespace, of by the traffic-manager when no agent is present. There are multiple reasons for doing this. One is that it is important that the request originates from the correct namespace. Example: ```bash curl some-host ``` results in a http request with header `Host: some-host`. Now, if a service-mesh like Istio performs header-based routing, then it will fail to find that host unless the request originates from the same namespace as the host resides in. Another reason is that the configuration of a service mesh can contain very strict rules. If the request then originates from the wrong pod, it will be denied. ## Recursion detection It is common that clusters used in development, such as Minikube, Minishift or k3s, run on the same host as the Telepresence client, often in a Docker container. Such clusters may have access to host network, which means that both DNS and L4 routing may be subjected to recursion. ### DNS recursion When a local cluster's DNS-resolver fails to resolve a hostname, it may fall back to querying the local host network. This means that the Telepresence resolver will be asked to resolve a query that was issued from the cluster. Telepresence must check if such a query is recursive because there is a chance that it actually originated from the Telepresence DNS resolver and was dispatched to the `traffic-manager`, or a `traffic-agent`. Telepresence handles this by sending one initial DNS-query to resolve the hostname "tel2-recursion-check.kube-system". If the cluster runs locally, and has access to the local host's network, then that query will recurse back into the Telepresence resolver. Telepresence remembers this and alters its own behavior so that queries that are believed to be recursions are detected and respond with an NXNAME record. Telepresence performs this solution to the best of its ability, but may not be completely accurate in all situations. There's a chance that the DNS-resolver will yield a false negative for the second query if the same hostname is queried more than once in rapid succession, that is when the second query is made before the first query has received a response from the cluster. ##### Footnotes: [^1]: The error message from an attempt to create a service in a bad subnet contains the service subnet. The trick of creating a dummy service is currently the only way to get Kubernetes to expose that subnet. ================================================ FILE: docs/reference/tun-device.md ================================================ --- title: Networking through Virtual Network Interface hide_table_of_contents: true --- # Networking through Virtual Network Interface The Telepresence daemon process creates a Virtual Network Interface (VIF) when Telepresence connects to the cluster. The VIF ensures that the cluster's subnets are available to the workstation. It also intercepts DNS requests and forwards them to the traffic-manager which in turn forwards them to engaged agents, if any, or performs a host lookup by itself. ### TUN-Device The VIF is a TUN-device, which means that it communicates with the workstation in terms of L3 IP-packets. The router will recognize UDP and TCP packets and tunnel their payload to the traffic-manager via its encrypted gRPC API. The traffic-manager will then establish corresponding connections in the cluster. All protocol negotiation takes place in the client because the VIF takes care of the L3 to L4 translation (i.e. the tunnel is L4, not L3). ## Gains when using the VIF ### Both TCP and UDP The TUN-device is capable of routing both TCP and UDP traffic. ### No SSH required The VIF approach is somewhat similar to using `sshuttle` but without any requirements for extra software, configuration or connections. Using the VIF means that only one single connection needs to be forwarded through the Kubernetes apiserver (à la `kubectl port-forward`), using only one single port. There is no need for `ssh` in the client nor for `sshd` in the traffic-manager. This also means that the traffic-manager container can run as the default user. #### sshfs without ssh encryption When a POD is engaged, and its volumes are mounted on the local machine, this mount is performed by [sshfs](https://github.com/libfuse/sshfs). Telepresence will run `sshfs -o slave` which means that instead of using `ssh` to establish an encrypted communication to an `sshd`, which in turn terminates the encryption and forwards to `sftp`, the `sshfs` will talk `sftp` directly on its `stdin/stdout` pair. Telepresence tunnels that directly to an `sftp` in the agent using its already encrypted gRPC API. As a result, no `sshd` is needed in client nor in the traffic-agent, and the traffic-agent container can run as the default user. ### No Firewall rules With the VIF in place, there's no longer any need to tamper with firewalls in order to establish IP routes. The VIF makes the cluster subnets available during connect, and the kernel will perform the routing automatically. When the session ends, the kernel is also responsible for cleaning up. ================================================ FILE: docs/reference/volume.md ================================================ --- title: Volume mounts hide_table_of_contents: true --- # Volume mounts Volume mounts are achieved using a Docker Volume plug-in and Docker volume mounts when connecting using `--docker` and using `--docker-run`. This page describes how mounts are achieved when running directly on the host. Telepresence supports locally mounting of volumes that are mounted to your Pods. You can specify a command to run when starting the engagement, this could be a subshell or local server such as Python or Node. ``` telepresence replace --mount=/tmp/ -- /bin/bash ``` In this case, Telepresence replaces the remote container, mounts the Pod's volumes to locally to `/tmp`, and starts a Bash subshell. Telepresence can set a random mount point for you by using `--mount=true` instead, you can then find the mount point in the output of `telepresence list` or using the `$TELEPRESENCE_ROOT` variable. ``` $ telepresence replace --mount=true -- /bin/bash Using Deployment replaced Container name : State : ACTIVE Workload kind : Deployment Destination : 127.0.0.1: Volume Mount Point: /var/folders/cp/2r22shfd50d9ymgrw14fd23r0000gp/T/telfs-988349784 bash-3.2$ echo $TELEPRESENCE_ROOT /var/folders/cp/2r22shfd50d9ymgrw14fd23r0000gp/T/telfs-988349784 ``` > [!NOTE] > `--mount=true` is the default if a mount option is not specified, use `--mount=false` to disable mounting volumes. With either method, the code you run locally either from the subshell or from the `replace` command will need to be prepended with the `$TELEPRESENCE_ROOT` environment variable to utilize the mounted volumes. For example, Kubernetes mounts secrets to `/var/run/secrets/kubernetes.io` (even if no `mountPoint` for it exists in the Pod spec). Once mounted, to access these you would need to change your code to use `$TELEPRESENCE_ROOT/var/run/secrets/kubernetes.io`. > [!NOTE] > If using `--mount=true` without a command, you can use either [environment variable](environment.md) flag to retrieve the variable. ================================================ FILE: docs/reference/vpn.md ================================================ --- title: Telepresence and VPNs --- import Platform from '@site/src/components/Platform'; # Telepresence and VPNs Telepresence creates a virtual network interface (VIF) when it connects. This VIF is configured to route the cluster's service and pod subnets so that the user can access resources in the cluster. It's not uncommon that the workstation where Telepresence runs already has network interfaces that route subnets that will overlap. Such conflicts must be resolved deterministically. Unless configured otherwise, Telepresence will resolve subnet conflicts by moving the cluster's subnet out of the way using network address translation. For a majority of use-cases, this will be enough, but there are some [caveats](#caveats-when-using-vnat) to be aware of. For more info, see the section on how to [avoid the conflict](#avoiding-the-conflict) below. ## VPN Configuration Let's begin by reviewing what a VPN does and imagining a sample configuration that might come to conflict with Telepresence. Usually, a VPN client adds two kinds of routes to your machine when you connect. The first serves to override your default route; in other words, it makes sure that packets you send out to the public internet go through the private tunnel instead of your ethernet or wifi adapter. We'll call this a `public VPN route`. The second kind of route is a `private VPN route`. These are the routes that allow your machine to access hosts inside the VPN that are not accessible to the public internet. Generally speaking, this is a more circumscribed route that will connect your machine only to reachable hosts on the private network, such as your Kubernetes API server. This diagram represents what happens when you connect to a VPN, supposing that your private network spans the CIDR range: `10.0.0.0/8`. ![VPN routing](../images/vpn-routing.jpg) ## Kubernetes configuration One of the things a Kubernetes cluster does for you is assign IP addresses to pods and services. This is one of the key elements of Kubernetes networking, as it allows applications on the cluster to reach each other. When Telepresence connects you to the cluster, it will try to connect you to the IP addresses that your cluster assigns to services and pods. Cluster administrators can configure, on cluster creation, the CIDR ranges that the Kubernetes cluster will place resources in. Let's imagine your cluster is configured to place services in `10.130.0.0/16` and pods in `10.132.0.0/16`: ![VPN Kubernetes config](../images/vpn-k8s-config.jpg) # Telepresence conflicts When you run `telepresence connect` to connect to a cluster, it talks to the API server to figure out what pod and service CIDRs it needs to map in your machine. If it detects that these CIDR ranges are already mapped by a VPN's `private route`, it will produce an error and inform you of the conflicting subnets: ```console $ telepresence connect telepresence connect: error: connector.Connect: failed to connect to root daemon: rpc error: code = Unknown desc = subnet 10.43.0.0/16 overlaps with existing route "10.0.0.0/8 via 10.0.0.0 dev utun4, gw 10.0.0.1" ``` Telepresence offers three different ways to resolve this: - [Avoid the conflict](#avoiding-the-conflict) using the `--proxy-via` connect flag - [Allow the conflict](#allowing-the-conflict) in a controlled manner - [Use docker](#using-docker) to make telepresence run in a container with its own network config ## Avoiding the conflict Telepresence can perform Virtual Network Address Translation (henceforth referred to as VNAT) of the cluster's subnets when routing them from the workstation, thus moving those subnets so that conflicts are avoided. Unless configured not to, Telepresence will use VNAT by default when it detects conflicts. VNAT is enabled by passing a `--vnat` flag (introduced in Telepresence 2.21) to`teleprence connect`. When using this flag, Telepresence will take the following actions: - The local DNS-server will translate any IP contained in a VNAT subnet to a virtual IP. - All access to a virtual IP will be translated back to its original when routed to the cluster. - The container environment retrieved when using `replace`, `ingest`, `intercept`, or `wiretap` will be mangled, so that all IPs contained in VNAT subnets are replaced with corresponding virtual IPs. The `--vnat` flag can be repeated to make Telepresence translate more than one subnet. ```console $ telepresence connect --vnat CIDR ``` The CIDR can also be a symbolic name that identifies a well-known subnet or list of subnets: | Symbol | Meaning | |-----------|-------------------------------------| | `also` | All subnets added with --also-proxy | | `service` | The cluster's service subnet | | `pods` | The cluster's pod subnets. | | `all` | All of the above. | ### Virtual Subnet Configuration Telepresence will use a special subnet when it generates the virtual IPs that are used locally. On a Linux or macOS workstation, this subnet will be a class E subnet (not normally used for any other purposes). On Windows, the class E is not routed, and Telepresence will instead default to `211.55.48.0/20`. The default subnet used can be overridden in the client configuration. In `config.yml` on the workstation: ```yaml routing: virtualSubnet: 100.10.20.0/24 ``` Or as a Helm chart value to be applied on all clients: ```yaml client: routing: virtualSubnet: 100.10.20.0/24 ``` #### Example Let's assume that we have a conflict between the cluster's subnets, all covered by the CIDR `10.124.0.0/9` and a VPN using `10.0.0.0/9`. We avoid the conflict using: ```console $ telepresence connect --vnat all ``` The cluster's subnets are now hidden behind a virtual subnet, and the resulting configuration will look like this: ![VPN Telepresence](../images/vpn-vnat.jpg) ### Proxying via a specific workload Telepresence is capable of routing all traffic to a VNAT to a specific workload. This is particularly useful when the cluster's DNS is configured with domains that resolve to loop-back addresses. This is sometimes the case when the cluster uses a mesh configured to listen to a loopback address and then reroute from there. The `--proxy-via` flag (introduced in Telepresenc 2.19) is similar to `--vnat`, but the argument must be in the form CIDR=WORKLOAD. When using this flag, all traffic to the given CIDR will be routed via the given workstation. The WORKLOAD is the deployment, replicaset, statefulset, or argo-rollout in the cluster whose traffic-agent will be used for targeting the routed subnets. #### Example Let's assume that we have a conflict between the cluster's subnets, all covered by the CIDR `10.124.0.0/9` and a VPN using `10.0.0.0/9`. We avoid the conflict using: ```console $ telepresence connect --proxy-via all=echo ``` The cluster's subnets are now hidden behind a virtual subnet, and all traffic is routed to the echo workload. ### Caveats when using VNAT Telepresence may not accurately detect cluster-side IP addresses being used by services running locally on a workstation in certain scenarios. This limitation arises when local services obtain IP addresses from remote sources such as databases or configmaps, or when IP addresses are sent to it in API calls. ### Disabling default VNAT The default behavior of using VNAT to resolve conflicts can be disabled by adding the following to the client config. In `config.yml` on the workstation: ```yaml routing: autoResolveConflicts: false ``` Or as a Helm chart value to be applied on all clients: ```yaml client: routing: autoResolveConflicts: false ``` Explicitly allowing all conflicts will also effectively prevent the default VNAT behavior. ## Allowing the conflict A conflict can be resolved by carefully considering what your network layout looks like, and then allow Telepresence to override the conflicting subnets. Telepresence is refusing to map them, because mapping them could render certain hosts that are inside the VPN completely unreachable. However, you (or your network admin) know better than anyone how hosts are spread out inside your VPN. Even if the private route routes ALL of `10.0.0.0/8`, it's possible that hosts are only being spun up in one of the sub-blocks of the `/8` space. Let's say, for example, that you happen to know that all your hosts in the VPN are bunched up in the first half of the space -- `10.0.0.0/9` (and that you know that any new hosts will only be assigned IP addresses from the `/9` block). In this case you can configure Telepresence to override the other half of this CIDR block, which is where the services and pods happen to be. To do this, all you have to do is configure the `client.routing.allowConflictingSubnets` flag in the Telepresence helm chart. You can do this directly via `telepresence helm upgrade`: In `config.yml` on the workstation: ```yaml routing: allowConflictingSubnets: 10.128.0.0/9 ``` Or as a Helm chart configuration value to be applied on all clients: ```yaml client: routing: allowConflictingSubnets: 10.128.0.0/9 ``` Or pass the Helm chart configuration using the `--set` flag ```console $ telepresence helm upgrade --set client.routing.allowConflictingSubnets="{10.128.0.0/9}" ``` The end result of this (assuming an allowlist of `/9`) will be a configuration like this: ![VPN Telepresence](../images/vpn-with-tele.jpg) ### Using docker Use `telepresence connect --docker` to make the Telepresence daemon containerized, which means that it has its own network configuration and therefore no conflict with a VPN. Read more about docker [here](docker-run.md). ## Some helpful hints when dealing with conflicts When resolving a conflict by allowing it, you might want to validate that the routing is correct during the time when Telepresence is connected. One way of doing this is to retrieve the route for an IP in a conflicting subnet. This example assumes that Telepresence detected a conflict with a VPN using subnet `100.124.0.0/16`, and that we then decided to allow a conflict in a small portion of that using allowConflictingSubnets `100.124.150.0/24`. Without telepresence being connected, we check the route for the IP `100.124.150.45`, and discover that it's running through a Tailscale device. ```console $ route -n get 100.124.150.45 route to: 100.64.2.3 destination: 100.64.0.0 mask: 255.192.0.0 interface: utun4 flags: recvpipe sendpipe ssthresh rtt,msec rttvar hopcount mtu expire 0 0 0 0 0 0 1280 0 ``` Note that in macOS it's difficult to determine what software the name of a virtual interface corresponds to -- `utun4` doesn't indicate that it was created by Tailscale. One option is to look at the output of `ifconfig` before and after connecting to your VPN to see if the interface in question is being added upon connection ```console $ ip route get 100.124.150.45 100.64.2.3 dev tailscale0 table 52 src 100.111.250.89 uid 0 ``` ```console $ Find-NetRoute -RemoteIPAddress 100.124.150.45 IPAddress : 100.102.111.26 InterfaceIndex : 29 InterfaceAlias : Tailscale AddressFamily : IPv4 Type : Unicast PrefixLength : 32 PrefixOrigin : Manual SuffixOrigin : Manual AddressState : Preferred ValidLifetime : Infinite ([TimeSpan]::MaxValue) PreferredLifetime : Infinite ([TimeSpan]::MaxValue) SkipAsSource : False PolicyStore : ActiveStore Caption : Description : ElementName : InstanceID : ;::8;;;8 Now, run the same command with telepresence connected. The output should differ and instead show that the same IP Is routed via the Telepresence Virtual Network. This should always be the case for an allowed conflict. > [!NOTE] > If you instead choose to avoid the conflict using VNAT, then the IP will be unaffected and still get routed via > Tailscale. The cluster resource using that IP will be available to you from another subnet, using another IP. ================================================ FILE: docs/release-notes.md ================================================ [comment]: # (Code generated by relnotesgen. DO NOT EDIT.) # Telepresence Release Notes ## Version 2.27.2 (March 9) ##
bugfix
[Fix duplicate Section field in .deb package causing dpkg install failure](https://github.com/telepresenceio/telepresence/issues/4073)
The .deb package control file contained a duplicate Section field, which caused dpkg -i to fail with a parsing error. The Section field was specified both as a top-level nfpm default and explicitly under deb.fields. Moved to the top-level section key and removed the deb.fields block entirely.
## Version 2.27.1 (March 8) ##
bugfix
[Fix duplicate Priority field in .deb package causing dpkg install failure](https://github.com/telepresenceio/telepresence/issues/4070)
The .deb package control file contained a duplicate Priority field, which caused dpkg -i to fail with a parsing error. This was caused by nfpm adding Priority: optional by default while the build configuration also specified it explicitly under deb.fields.
##
bugfix
[Fix ingest command lookup when container name is not specified](https://github.com/telepresenceio/telepresence/issues/4067)
The traffic manager now properly handles ingest lookups when using telepresence ingest <workload> -- command without specifying a container name. Previously, this would fail for workloads because the lookup couldn't find ingests with empty container names. The traffic manager now provides clearer error messages when a container name is required but not specified, and correctly resolves ingests for single-container workloads automatically.
## Version 2.27.0 (February 28) ##
feature
[Add macOS package installer with root daemon as a system service](install/client)
A new macOS package installer (.pkg) is now available that installs Telepresence with the root daemon configured as a launchd service. This eliminates the need for elevated privileges when using Telepresence, as the daemon starts automatically at boot and runs in the background. Available for both Intel (amd64) and Apple Silicon (arm64) Macs.
##
feature
[Add Linux package installers with root daemon as a system service](install/client)
New Linux package installers (.deb for Debian/Ubuntu and .rpm for Fedora/RHEL) are now available that install Telepresence with the root daemon configured as a systemd service. This eliminates the need for elevated privileges when using Telepresence, as the service is enabled and started automatically during installation. Available for both amd64 and arm64 architectures.
##
feature
[Add Windows installer with root daemon as a system service](install/client)
A new Windows installer (.exe) is now available that installs Telepresence with the root daemon configured as a Windows service. The installer bundles WinFSP and SSHFS-Win dependencies for volume mount support, adds Telepresence to the system PATH, and optionally installs the TelepresenceDaemon service. This eliminates the need for elevated privileges when using Telepresence. Currently available for amd64 architecture only due to dependency constraints.
##
feature
[Add route-controller DaemonSet to prevent routing loops on local clusters](reference/route-controller)
A new optional route-controller DaemonSet can be deployed alongside the traffic-manager on local Kubernetes clusters (Kind, minikube, k3d, Docker Desktop) to prevent routing loops caused by deleted or non-existent service ClusterIPs. It installs an iptables FORWARD chain DROP rule for the service CIDR on every node, and adds per-IP kernel blackhole routes when a Service is deleted. Enable it with routeController.enabled=true in the Helm chart.
##
feature
Automatic cache cleanup on version change
Telepresence now tracks its version in a version.json file in the cache directory. When the CLI detects that the major.minor version differs from the running binary, it automatically quits running daemons and clears stale cache entries (preserving logs). This prevents issues caused by leftover cache files from a previous version. Patch and pre-release version changes do not trigger a cache cleanup.
##
bugfix
[Cluster DNS not injected into containers started by telepresence compose](https://github.com/telepresenceio/telepresence/issues/4053)
When using telepresence compose up, cluster hostnames did not resolve inside the compose container because the daemon DNS IP and the tel2-search DNS search domain were not being added to the generated compose spec. DNS and dns_search are now correctly set for all engaged compose services.
## Version 2.26.2 (February 14) ##
bugfix
[Dial 127.0.0.1 instead of 0.0.0.0 when connecting to local daemons](https://github.com/telepresenceio/telepresence/issues/4048)
When a VPN routes all private network addresses, dialing 0.0.0.0 gets routed through the VPN instead of reaching the locally running daemon. This causes Telepresence to report that no daemon is running even though the processes are active and listening. The client now dials 127.0.0.1 explicitly, and the Docker-published daemon port is bound to 127.0.0.1 to match.
##
bugfix
Fix HTTP intercepts via telepresence compose failing when httpFilters or httpPaths are set
The compose extension set HeaderFilters and PathFilters on the intercept spec but left the mechanism as "tcp". This caused the traffic manager to reject the intercept with "global TCP/UDP intercepts are disabled". The mechanism is now correctly switched to "http" when any HTTP filters are specified, matching the behavior of the CLI intercept command.
##
bugfix
[Fix "root daemon is embedded" error on Windows elevated terminals](https://github.com/telepresenceio/telepresence/issues/4049)
When running Telepresence in an elevated (administrator) terminal on Windows, commands like connect and loglevel failed with "root daemon is embedded". The user daemon now correctly delegates to the in-process root daemon session instead of returning an error.
##
bugfix
[Fix h2c (HTTP/2 cleartext) prior knowledge not working on transport](https://github.com/telepresenceio/telepresence/issues/4056)
Since Go 1.24, having both HTTP1 and UnencryptedHTTP2 enabled on an HTTP transport causes Go to default to HTTP/1.1 instead of using h2c prior knowledge. The transport now explicitly disables HTTP1 when UnencryptedHTTP2 is enabled, fixing h2c communication for both the default forwarding handler and intercepted traffic.
## Version 2.26.1 (January 26) ##
bugfix
[Add support for "warning" as an alias for "warn" in log levels](https://github.com/telepresenceio/telepresence/issues/4043)
The "warning" alias for "warn" was the only acceptable value in the Helm chart, yet the traffic manager didn't accept it. This is now fixed so that both names are accepted by the Helm chart and by the traffic manager.
## Version 2.26.0 (January 23) ##
feature
[Add ability for cluster admins to revoke other users' intercepts.](reference/engagements/conflicts)
The traffic-manager can now execute commands defined in the traffic-manager ConfigMap. As a result, clients that are authorized to update this ConfigMap can issue those commands. This mechanism lays the groundwork for distinguishing administrators from regular users in Telepresence. Administrators can be granted RBAC permissions that allow them to update the traffic-manager ConfigMap, while regular users cannot. The new command, `telepresence revoke `, uses this mechanism to revoke the intercept associated with the specified ID.
##
feature
[Add support for overriding intercepts owned by inactive clients](reference/engagements/conflicts)
Introduce the Helm chart setting `intercept.inactiveBlockTimeout` that controls the maximum amount of time an intercept may be held by a client that is unreachable or inactive. Once this timeout is exceeded, the intercept no longer blocks conflicting intercepts and may be automatically removed when another client attempts to create a conflicting intercept.
##
feature
Add support for sudo-rs
Telepresence now supports running the root daemon using `sudo-rs`. This is necessary on Ubuntu where `sudo-rs` has become the default for `sudo`.
##
feature
[Add configuration to disable global TCP/UDP intercepts](reference/cluster_config#restricting_global_intercepts)
A new Helm chart configuration option `intercept.allowGlobalIntercepts` has been added to control whether global TCP/UDP intercepts are permitted. When set to `false`, only HTTP intercepts with header or path filters are allowed, preventing users from creating global intercepts that block other developers from intercepting the same port. This is particularly useful in shared development environments where multiple developers need to work on the same service simultaneously. The setting defaults to `true` to maintain full backward compatibility with existing deployments. When a user attempts to create a global intercept while the setting is disabled, they receive a helpful error message suggesting the use of `--http-header` or `--http-path-*` flags for HTTP-filtered intercepts.
##
feature
Enhanced Traffic Manager Startup Reliability
The Traffic Manager deployment now includes a `startupProbe` that accurately detects when the service is fully initialized and ready to handle traffic. This enhancement brings the following benefits: - **Prevents Premature Traffic Routing**: The manager only reports itself as ready after all configurations are loaded, eliminating potential race conditions - **Smoother Upgrades and Rollouts**: Deployment orchestration tools can reliably determine when the Traffic Manager is operational, improving overall installation stability This change is particularly beneficial in large clusters or complex networking environments where initialization may take longer than expected.
##
feature
Support customizable daemon config file
The config file for Telepresence is now configurable through the command-line flag `--config`. The `--config ` flag of the `telepresence genyaml` command was renamed to `--agent` to avoid confusion.
##
feature
Support customizable daemon log file paths
Log file paths for the Telepresence daemons are now configurable through the command-line flag `--logfile` that denotes a custom log file location or redirect of the log output to stdout/stderr. Two new log-level configuration entries for `cli` and `kubeAuthDaemon` are also introduced, expanding the existing log-level controls beyond just `userDaemon` and `rootDaemon`.
##
feature
Add ability to exclude or include modifications made by other injectors when injecting the traffic agent.
The Traffic Agent now has a new configuration option `agentInjector.mutationAware` that can be set to `false` to exclude modifications made by other injectors when injecting the traffic agent. Setting `agentInjector.mutationAware=true` requires `agentInjector.webhook.reinvocationPolicy=IfNeeded`. The default setting is `true`.
##
feature
Add ability to disable the Traffic Agent's HTTP2/Clear-Text probing.
The Traffic Agent now has a new configuration option `agent.enableH2cProbing` that can be set to `false` to disable the HTTP2/Clear-Text probing. The default setting is `true` to preserve backwards compatibility, but this will be changed to `false` in a future release.
##
feature
Add ability to configure the Traffic Agent's retry interval for watching intercepts.
The Traffic Agent's retry interval when it establishes its watcher for intercepts is now configurable using the Helm chart value `agent.watchRetryInterval`. The default retry interval was also increased from 2 seconds to 10 seconds to improve resilience when connections to the traffic manager are lost.
##
feature
Make traffic-agent consumption metrics reporting optional.
Metrics reporting for the traffic-agent can now be optionally enabled or disabled via the `agent.enableConsumptionMetrics` Helm chart value. The traffic-agent metrics will also be completely disabled when the Helm chart value `prometheus.port` is unset or zero. This change is intended to reduce the amount of unnecessary traffic generated by the traffic-agent when consumption metrics reporting is of no interest to the user.
##
feature
Improved efficiency of traffic manager map updates.
The watchable map has been refactored into a client/server model that supports delta-based updates. Where supported, gRPC now transmits only incremental changes instead of full snapshots, significantly reducing payload sizes. This improvement is especially important for large clusters, where full snapshots can be sizable and costly to transmit. Full snapshot streaming remains available as a backward-compatible fallback for clients that do not support delta methods.
##
change
Use TCP/IP instead of Unix sockets for all communication between local processes.
Telepresence now uses TCP/IP sockets for all communication between local processes. This change reduces the risk of socket-related errors caused by lingering sockets from previous Telepresence sessions. It also eliminates the difficulties of using Unix sockets for communication between a system service and user processes on Windows.
##
change
Don't allow connect with --docker when client is configured with intercept.useFtp=true
The `--docker` flag is not allowed when the client is configured with `intercept.useFtp=true` and an error is now generated instantly by the `telepresence connect --docker` command. The docker volume plugin cannot use FTP because it requires two ports: a fixed control port that Telepresence can proxy, and a dynamic data port (randomly chosen during connection) that Telepresence cannot proxy on-demand. The port-forwarder only forwards pre-configured ports and doesn't understand FTP's protocol. Consequently, FTP isn't allowed in this scenario. If it was, then when an FTP server tells the client to use an unpredictable second port for file transfers, Telepresence would block it—causing the connection to fail every time.
##
change
Better names for the Telepresence Daemons
Using the name xxx-foreground isn't very intuitive when talking about daemon processes. Yes, the command, when issued in a terminal, will start the daemon in foreground so the names of the commands does have some logic to them, but then again, starting in the foreground is the default behavior of any command. And when the same command is started from the CLI, it will be started in the background, despite its name. The daemons are therefore now renamed: - connector-foreground => userd - daemon-foreground => rootd - kubeauth-foreground => kubeauthd This also affects the Telepresence hidden CLI commands `telepresence daemon-foreground` and `telepresence connector-foreground`.
##
bugfix
Add retry logic for tunnel connection attempts
The tunnel dialer now uses a backoff-based retry mechanism when establishing connections. This ensures that dialing attempts for both TCP and UDP protocols persist if the target IP is not immediately ready to receive requests.
##
bugfix
Retry mechanism for client tunnel creation
The traffic agent now includes a backoff-based retry mechanism when establishing tunnel streams to a client. This prevents "no dial watcher" connection failures caused by a race condition where the tunnel request arrives before the client has fully initialized its communication channel.
##
bugfix
Fix "close of closed channel" panic in the root daemon process.
The root daemon process would sometimes panic with "close of closed channel" due to a race condition in the DNS cache logic. This issue has been fixed.
## Version 2.25.2 (December 26) ##
bugfix
Ensure that the exit code from a docker command becomes the exit code of the Telepresence command.
When running a Docker command using `telepresence docker-run` or `telepresence curl`, the exit code would be 1 for all non-zero exit codes from the Docker command. This has been fixed so that the exit code from the Docker command becomes the exit code of the Telepresence command.
##
bugfix
Fix a bug causing truncation of command text when generating external command help.
The Telepresence CLI would truncate the command text when generating help for external commands such as `docker compose` that had text spanning more than one line. This has been fixed so that the full command text is displayed.
##
bugfix
Fix schema for agent.image.pullSecrets
The `agent.image.pullSecrets` is referenced by the helm chart's deployment.yaml but was previously disallowed by the schema file.
## Version 2.25.1 (November 10) ##
bugfix
Volumes did not mount correctly when using `telepresence connect --docker` when Docker had IPv6 enabled.
Telepresence failed to mount volumes after connecting with `telepresence connect --docker` when Docker Engine had IPv6 enabled in its default bridge network. Disabling IPv6 in the Telepresence client configuration did not resolve the issue. This was fixed in Telepresence Volume Plugin "telemount" version 0.3.2, which circumvented a [bug in sshfs](https://github.com/libfuse/sshfs/issues/335). Additionally, the volume plugin will no longer use IPv6 when the client configuration `docker.enableIPv6` is set to `false`.
##
bugfix
Remove unnecessary setcap from traffic binary
The setcap capability (cap_net_bind_service) was removed from the traffic binary build process. This capability was originally added to allow the binary to bind to privileged ports, specifically port 443 for the mutating webhook. Since version 2.24.0, the default mutating webhook port was changed to 8443 (a non-privileged port), making this capability unnecessary. Removing it simplifies the build process and reduces the security surface area.
## Version 2.25.0 (October 16) ##
feature
[HTTP Intercepts with HTTP header and path filtering](howtos/engage#intercept-your-application)
Telepresence now supports HTTP Intercepts, enabling fine-grained HTTP traffic filtering for intercepts. Users can intercept only specific HTTP requests based on headers and URL paths using the new `--http-header`, `--http-path-prefix`, `--http-path-equal`, and `--http-path-regex` flags. This allows multiple developers to work on the same service simultaneously by intercepting only their specific traffic patterns, rather than intercepting all traffic to a service. **Routing Precedence Model**: Header-based intercepts take priority over path-only intercepts. When multiple intercepts are active on the same workload, requests are evaluated against header-based filters first, then path-only filters. This enables different developers to use header-based personal intercepts (e.g., `x-user=alice`) while others use path-based intercepts (e.g., `/admin/*`) without conflicts. **Conflict Detection**: Intercepts conflict only when their filters would route the same traffic to different destinations. Key rules: - Different header values (e.g., `X-User=adam` vs `X-User=bertil`) do NOT conflict - Header filters use subset logic: `X-User=adam` conflicts with `X-User=adam, X-Session=123` (first is subset) - Same headers with different paths do NOT conflict: `X-User=adam + /api/*` vs `X-User=adam + /admin/*` - Path-only intercepts operate at a lower priority tier than header-based intercepts The feature maintains full backward compatibility with existing TCP intercepts.
##
feature
[TLS/mTLS Intercept Support](howtos/mtls)
Support was added for HTTP-filtered intercepts on applications using TLS/mTLS encryption. The new functionality enables Telepresence to decrypt and inspect encrypted traffic by accessing TLS certificates, facilitating debugging and testing of secure applications. Certificates can be accessed via mounted volumes or Kubernetes secrets in the same namespace. New annotations (`telepresence.io/downstream-cert-path` and `telepresence.io/downstream-cert-secret`) allow configuration of certificate paths or secrets for decrypting traffic on specified ports. For mTLS, the `telepresence.io/upstream-cert-` annotation prefix supports re-encryption of upstream traffic using client-side certificates. For self-signed certificates, the `telepresence.io/upstream-insecure-skip-verify` annotation bypasses verification, enabling HTTP-filtered intercepts in development environments. The `--plaintext` option allows unencrypted traffic during intercepts or wiretaps.
##
feature
Add MCP server to Telepresence CLI
The Telepresence CLI now includes a lightweight MCP server that can be used to allow local AI agents to execute some CLI commands, such as connecting to a traffic manager, listing interceptable apps, and creating an intercept. The server can be enabled using the new `telepresence mcp claude enable` or `telepresence mcp vscode enable` commands.
##
feature
Enhance Resilience of Engagements During Traffic-Manager Redeploys
The telepresence client and traffic-agent now automatically reconnect to the traffic-manager after a restart. Upon reconnection, they share their current state, ensuring ongoing engagements remain uninterrupted. This improvement minimizes user impact during traffic-manager upgrades.
##
feature
Add support for IPv6 and dual-stack when using `telepresence connect --docker`
Telepresence now supports using `telepresence connect --docker` together with Kubernetes single-stack IPv4 networking, single-stack IPv6 networking, or dual-stack networking with both network families. Both are enabled by default, but can be disabled by setting the `client.docker.enableIPv4` or `client.docker.enableIPv6` to false in the Helm chart, or by using the corresponding settings `docker.enableIPv4` or `docker.enableIPv6` the client configuration file. The new dual-stack support requires the teleroute network plugin 0.4.0 or later. The client will install this version automatically unless you work in an air-gapped environment.
##
feature
[RESTful API Service Reintroduced with HTTP Filtering Support](reference/restapi)
The Telepresence RESTful API service has been restored with enhanced support for HTTP header and path filtering. This service enables workloads to programmatically query whether they should handle requests based on active intercepts. Added `--metadata` flag allows attaching custom metadata to intercepts that can be retrieved through the API endpoints. The API server is now accessible via `TELEPRESENCE_API_HOST` and `TELEPRESENCE_API_PORT` environment variables in both cluster pods and local intercept handlers.
##
feature
More efficient DNS handling in the traffic-manager
Telepresence will no longer send DNS queries for A and AAAA records to the traffic-manager. Instead, it will send a single query for the name and then derive the record type from the type of IP address (IPv4 or IPv6) in the response. This reduces the number of DNS queries sent to the cluster's DNS server and makes the behavior more consistent with the `net.LookupNetIP` function in the Go standard library, which the traffic-manager ultimately uses. The lookups will now be performed exclusively by the traffic-manager, never by the traffic-agent. This means that traffic-agents with special DNS configurations might stop working. If this is a problem, the old behavior can be restored by setting the `client.dns.useComplexLookup` parameter in the Helm chart or the `dns.useComplexLookup` parameter in the client configuration file.
##
feature
Updated Helm chart to include keywords and the source repository URL.
Improves the Helm chart's discoverability on platforms like Artifact Hub and automatically adds a direct link to the source code for users, providing better context.
##
change
Telepresence client now requires a traffic manager version of at least 2.21.0.
The traffic manager is now required to be at least version 2.21.0. Versions earlier than 2.21.0 will no longer work. The reason for this is that implementing the new reconnect behavior would require too much conditional code with older traffic-managers, and a lot of functionality wouldn't work anyway.
##
change
Build binaries and docker images that are stripped from dwarf and debug info.
The Telepresence binaries and docker images are now built with the `-w -s` flags, which strip the debug symbols and the DWARF information. This reduces the size of the binaries and docker images by about 50MB. Debug binaries can be built using `DEBUG=1 make build`.
## Version 2.24.1 (September 5) ##
bugfix
[Fix invalid filename generated by telepresence gather-logs command](https://github.com/telepresenceio/telepresence/issues/3956)
The `telepresence gather-logs` command would generate a filename that was invalid on Windows unless the user specified the filename explicitly using the `--output-file` flag. This was fixed using a more condensed format for the timestamp in the filename.
##
bugfix
A `telepresence connect --docker` fails kubeconfig points to a port-forwarded localhost
Telepresence would fail to connect to a cluster from within a container if the kubeconfig pointed to a port-forwarded localhost because that localhost is not reachable from within the container. This situation is now detected so that the address used from within the container has "localhost" replaced with "host.docker.internal", or an alternative alias configured by the user using the new `docker.hostGateway` parameter in the client configuration.
##
bugfix
A `telepresence connect --docker` would fail with some k3s configurations
Telepresence would fail to connect to a k3s cluster unless the cluster's IP was a loopback address. This is now changed so that any IP:port combination is accepted as long as a container can be found that defines a mapping for it.
##
bugfix
[Restore default value for agent-state.yaml in the traffic-manager configmap](https://github.com/telepresenceio/telepresence/pull/3953)
The value was previously an empty string which caused problems when when Argo CD tried to synchronize it.
## Version 2.24.0 (August 25) ##
feature
[Support for Docker Compose](howtos/docker-compose)
Telepresence now supports integration with Docker Compose. It connects to and interacts with cluster resources by utilizing `x-tele` extensions within a Docker Compose specification. These extensions configure your local services to effectively act as handlers for Telepresence connections, providing them with the necessary access to the traffic, volumes, and environment of the engaged container.
##
feature
[Serve up a web-page with telepresence serve.](reference/cli/telepresence_serve)
A new `telepresence serve ` command was added that starts a web browser on the specified service. The command is especially useful when used in combination with `telepresence connect --docker` because it will then expose the given service on a random port on localhost.
##
feature
Add ability to optionally clean up sidecars that have been idle above a specified duration
Added configuration parameter `agent.maxIdleTime` to the Helm Chart, to control how long a sidecar can be idle before it is cleaned up. The updating of latestEngagementTime is done every 1m in the Remain Call, which is now called every 1min, compared to every 5s in the past. The agent state (containing latestEngagementTime) is updated in memory, and lazily persisted to the traffic-manager configmap every 2min. Removal of the sidecar is also done in the Remain Call, if the sidecar has been idle for longer than the configured `agent.maxIdleTime`.
##
feature
[Add option to drop client label in prometheus metrics for GDPR compliance](https://github.com/telepresenceio/telepresence/issues/3491)
The Helm Chart now has a `prometheus.dropClientLabel` option that can be set to true to drop the client label from the prometheus metrics. This is useful for GDPR compliance, as the client label contains personal data, which can be potentially problematic, i.e allowing the ability to track the working times of an individual.
##
feature
[Prefix metrics with "telepresence_"](https://github.com/telepresenceio/telepresence/issues/3920)
Avoids metric conflicts and makes these more explicit to improve search in observability stacks.
##
feature
[CLI documentation in markdown format](reference/cli/telepresence)
The Telepresence CLI is now capable of generating its own documentation in markdown format using the new `telepresence man-pages` command. The generated documentation is included under the heading "Telepresence CLI" in the the Telepresence reference documentation.
##
feature
Service Port Rerouting
The telepresence connect command introduces a new `--reroute-remote ::[/{tcp|udp}]` flag, allowing users to remap service ports. This feature redirects requests sent to `:` to `:` within the Telepresence VIF. The flag can be repeated.
##
feature
Local Port Rerouting
The telepresence connect command introduces a new `--reroute-local ::[/{tcp|udp}]` flag, allowing users to redirect requests sent to ports on localhost to arbitrary service ports. This feature enables requests sent to `localhost:` to be redirected to ``. The flag can be repeated.
##
feature
[Add information about using Kubernetes auth plugins when using Telepresence CLI in a container](reference/inside-container#kubernetes-auth-plugins)
Kubernetes, and hence the Telepresence CLI, must have access to auth plugins declared in the kubeconfig. A section was added to the documentation explaining how to achieve this when using the Telepresence CLI in a container.
##
feature
Add log directory to the output of `telepresence config view`
The `telepresence config view` command now includes the path to the directory where the Telepresence logs are stored.
##
change
The default port for the mutating webhook is now 8443. It used to be 443
Port numbers below 1000 are reserved for privileged processes and are often restricted by firewalls. Consequently, the default port for the mutating webhook was changed from 443 to 8443. You can override this default port using the agentInjector.webhook.port value in the Helm Chart. This change is particularly significant for clusters using Telepresence, where firewall rules limit the admission webhook's access to worker nodes, such as in an Amazon EKS cluster.
## Version 2.23.6 (July 23) ##
bugfix
Public DNS names aren't resolved from local docker application started by Telepresence
A container running using `telepresence docker-run` or `telepresence --docker-run` was not able to resolve public DNS names such as "google.com". The problem is caused by an undocumented behavior in Docker's internal DNS-resolver when the `--dns` flag is used in conjunction with the teleroute network, where the DNS-server no longer finds public names even thought another bridge network is connected. The problem was solved by using a fallback resolver in the Telepresence DNS resolver.
## Version 2.23.5 (July 20) ##
bugfix
Let docker.Start pass on --interactive to docker start.
An `-i` or `--interactive` flag given when the user runs a container with telepresence must be propagated to `docker start` to attach `stdin`.
## Version 2.23.4 (July 18) ##
bugfix
Never truncate meaningful output from a command
The new human-friendly output using a progress reporter would sometimes truncate error output. This is no longer the case. Instead, all output will be wrapped.
##
bugfix
Typo in client mount-policy "RemoteReadonly" should be "RemoteReadOnly"
The Helm Chart correctly expects the remote read-only mount policy to be `RemoteReadOnly`, but the client expected it to be `RemoteReadonly` (without a leading capital letter in the word "only").
##
bugfix
[DNS server does not respect semicolons as comments in resolv.conf files](https://github.com/telepresenceio/telepresence/issues/3908)
Telepresence does not work correctly if `/etc/resolv.conf` contains semicolons, which are valid comments as of [linux manpage](https://man7.org/linux/man-pages/man5/resolv.conf.5.html).
##
bugfix
[Use NXDOMAIN instead SERVFAIL for DNS recursion errors (timeouts)](reference/config#recursioncheck)
A DNS for a single label name that fails in a minikube - or another type of local cluster - will sometimes result in a recursive lookup on the host. Without any type of recursion detection, this lookup will timeout waiting for itself. Previously, this resulted in a `SERVFAIL` from the cluster DNS, which triggered renewed lookup attempts that never stopped. This is now changed so that the same type of timeouts instead results in an `NXDOMAIN` error that doesn't trigger renewed attempts. Also, the recursion check now handles that the cluster's DNS adds suffixes from its search-path.
## Version 2.23.3 (July 7) ##
bugfix
Fix tunnel channel reuse in traffic-agent to prevent connection failures
Previously, when a tunnel became active, the map maintained by the traffic-agent containing channel values for agent-to-client tunnels wasn't cleared. This resulted in closed channels remaining in the map. When port numbers were eventually reused, these stale entries would be discovered and their closed channels would cause immediate stream termination, leading to data loss.
##
bugfix
The -p flags would have no effect in combination with --docker-run
When using `telepresence --docker-run` with a `-p ` flag, the Docker driver silently ignored the port specification. This occurred because the `--network=` flag disabled both additional network directives and the default bridge network (which is normally used when no network is specified). This has been resolved by: 1. Adding the teleroute network after container creation instead of using a flag 2. Replacing the single `docker run` command with a sequence of: - `docker create` - Network addition - `docker start`
##
bugfix
Requests lost when using wiretap
Wiretap connection `Close` and `Write` could sometimes be out of sync, so that the `Close` would be executed before the `Write`, causing a "read/write on closed pipe" error and loss of data.
## Version 2.23.2 (June 27) ##
bugfix
Adding an alsoProxy subnet with 32-bit mask no longer works on macOS
Routing improvements introduced in 2.23.0 surfaced a problem when using special handling of submets with 32-bit masks. The special handling now causes problems on macOS. The logic is now conditioned to only run under linux since it's no longer needed on other operating systems.
##
bugfix
The gather-logs command produces no cluster-side logs when connected with --docker
The `telepresence gather-logs` command did not include cluster-side logs when connected using `--docker`, because it tried to store such logs in a temporary directory created by the CLI. The directory was not mounted in the daemon container. This is now changed so that the temporary directory is created under the users cache directory, which is guaranteed to be mounted on the container.
##
bugfix
Docker volume mounts failing when connected using both --docker --proxy-via flags
The volume mounter would fail when doing a `telepresence connect --docker --proxy-via all=` followed by an intercept using `--docker-run`, because the bridge that the daemon created would try to access the intercepted pod using its proxied IP. Now, the bridge will instead use the pod's real IP.
## Version 2.23.1 (June 24) ##
feature
New telepresence helm version command.
The new `telepresence helm version` command prints the version of the helm client that is embedded in the telepresence binary.
##
bugfix
[Engagement disconnects after certain amount of time](https://github.com/telepresenceio/telepresence/issues/3861)
The configuration parameter `connectionTTL`, controlling how long a client could be completely idle before the traffic-manager or traffic-agent would consider it dead and disconnect (default 24 hours), had no effect. Instead, an engagement would disconnect after 2 hours (the default gRPC `keepAlive.Time` duration). The default of 24 hours is now reinstated. The Helm value `client.connectionTTL` was moved to `grpc.connectionTTL` because it is a server configuration. The old value will still work, but it is deprecated and will be removed eventually.
##
bugfix
[Telepresence breaks if config.yml exists but is empty](https://github.com/telepresenceio/telepresence/issues/3887)
Telepresence would refuse to connect with a misleading error if the `config.yml` file containing the client's configuration parameters existed but was empty.
## Version 2.23.0 (June 17) ##
feature
New telepresence wiretap command
The new `telepresence wiretap` command introduces a read-only form of an `intercept` where the original container will run unaffected while a copy of the wiretapped traffic is sent to the client. Similar to an `ingest`, a `wiretap` will always enforce read-only status on all volume mounts, and since that makes the `wiretap` completely read-only, there's no limit to how many simultaneous wiretaps that can be served. In fact, a `wiretap` and an `intercept` on the same port can run simultaneously.
##
feature
[Add Telepresence Docker Network Plugin "Teleroute"](reference/teleroute)
The new Teleroute plugin makes it possible for containers to use the Telepresence daemon's VIF without having to change their network mode, i.e. a `--network container:` is no longer needed. Instead, a container can use a custom network created when the Telepresence daemon connects to the cluster. This network uses the new driver "teleroute" which is provided by Telepresence. With the Teleroute Docker network plugin in place, there's no longer a need for special handling of network related docker flags, and the following changes have been made: 1. The Teleroute Docker network driver will be installed unless it is already present. 2. A Teleroute network will be created when starting the Telepresence daemon as a container. This network will then communicate with that container and expose the same CIDRs as the daemon's VIF. 3. A container started with `telepresence curl`, or `telepresence {ingest|intercept|replace|wiretap} --docker-{run|build|debug}` will no longer change its network mode using `--network container:`, instead it will use `--network `. 4. As a consequence of #3, published ports and other networks that are added no longer need special handling using socat containers, so all of that has been removed.
##
feature
Control whether the initContainer injection is enabled/disabled
The initContainer injection can be optionally disabled by setting the `agent.initContainer.enabled` parameter to false in the `values.yaml` file of the Helm chart. This feature was added to improve compatibility with systems like OpenShift where the initContainer injection cannot be used due to inability to give initContainer NET_ADMIN permissions.
##
feature
Human friendly progress reporting
Telepresence now uses a progress reporter that is very similar to the one used by Docker compose. The implementation is a variation of that reporter's source code, so big thanks to the Docker compose CLI authors for making it available as OSS. A new global `--progress ` flag was added. It defaults to "auto" which means that the style is chosen depending on whether the command runs from a tty type terminal. Other possible values are "plain", "quiet", and "json". `--progress quiet` is implied when formatted output is chosen using `--output json|yaml`.
##
feature
Add the ability to use a name for the target host, and defer its resolution
Knowing the IP of the local service that acts as the handler service for an intercept, replace, or wiretap is not possible until that service has been started, and telepresence will therefore now accept a name for the `--address` flag. The name is not resolved by the daemon until a request is made to the engaged container on a port that is routed to the local service.
##
feature
Add intercept.mountsRoot to the client configuration
The new `intercept.mountsRoot` can be set to a directory that will be used as the root for all automatically generated mount directories. The default is to use the platforms temp directory. The setting is not used on windows, where the mounts use drive letters.
##
feature
Add docker.addHostGateway to the client configuration.
When `docker.addHostGateway` is set to `true`, the `docker run` that starts the containerized Telepresence daemon will include the flag `--add-host host.docker.internal:host-gateway`. The flag is set to `true` by default on linux platforms and `false` on other platforms.
##
feature
Client configuration to override the Helm download URL
The default download URL `oci://ghcr.io/telepresenceio/telepresence-oss` used when installing Helm charts with versions that differ from the version of the embedded Helm chart can now be overridden using the client config value `helm.chartURL`.
##
change
Dropped support for Telepresence legacy flags
The `telepresence` CLI command will no longer support legacy flags such as: - `--swap-deployment` - `--new-deployment` - `--docker-mount` - `--method` A "Legacy Telepresence command used" warning has been printed for several years now, and the mapping for the `--swap-deployment` was the `intercept` command, which is very confusing today since we now have the `replace` command.
##
bugfix
Let containerized daemon consistently use the same port for gRPC
The port used for the containerized gRPC was randomly selected using the hosts network namespace. This is now changed so that the port used by the container is preset and configurable and then mapped to a random port on the host. The port number can be configured using `grpc.daemonPort` and defaults to `4038`.
##
bugfix
[Telepresence fails to start the root daemon on Windows unless current user is the administrator](https://github.com/telepresenceio/telepresence/issues/3875)
The telepresence CLI starts a user daemon and a root daemon. The latter is started using administrator privileges. On a Windows box, this means that the root daemon runs using a different user account (typically "Administrator") unless the current user can run processes with elevated privileges. The socket used for communication with the root daemon was assumed to reside in `%USERPROFILE%\AppData\Local\telepresence` and was therefore not found by the CLI and the user daemon. The location will henceforth always be based on the `%USERDATA` of the CLI user.
##
bugfix
[Telepresence DNS Fallback stripping CNAME information from DNS Records.](https://github.com/telepresenceio/telepresence/issues/3873)
The fallback DNS server used on Linux systems without a systemd.resolved configuration, would assume that suffixes belonging to the `search` defined in the `/etc/resolved` had been added by the caller. Since this search path was assumed to be intended for the local machine only, the suffix was stripped off prior to sending the name to the cluster for resolution. This made queries fail that relied on the qualified name to resolve CNAME records. The logic stripping the suffix was therefore removed.
## Version 2.22.6 (June 3) ##
bugfix
Regression causing "unexpected slice size" with older traffic-managers.
Older traffic-managers have a different way of reporting the service-subnet. The new way, using a list of subnets reused a proto slice in the GRPC message that was expected to be empty, but older traffic-managers will pass the IP of the kube-dns here. It cannot be parsed as a list of subnets. A check that remedies this mismatch was inserted.
## Version 2.22.5 (May 29) ##
bugfix
Unable to correctly determine service CIDR with Kubernetes >= 1.33
Starting with Kubernetes 1.33, the strategy of extracting the cluster's service CIDR from an error message no longer works because the error message has changed. The root cause for this is that Kubernetes introduced the ability to use [Multiple Service CIDRs](https://gist.github.com/aojea/c20eb117bf1c1214f8bba26c495be9c7). Since `ServiceCIDR` is now a resource, it can be easily retrieved (and modified) using standard Kubernetes client API calls, and this is what the traffic manager will use going forward. The fix required an addition to the traffic-manager's RBAC, granting it sufficient permissions to list `networking.k8s.io/servicecidrs`. A future enhancement will allow the traffic manager to watch for service CIDR changes.
##
bugfix
Helm chart schema type for nodeSelector was incorrect
The Helm chart schema for the `nodeSelector` value was incorrect. Kubernetes defines different types for nodeSelector (inside PodSpec objects) and NodeSelector (inside NodeAffinity, VolumeNodeAffinity and a bunch of other places). The schema was changed to use the correct type. The name `nodeSelector` is still used in the Helm chart so this change is backwards compatible.
##
bugfix
Pods with container ports named the same caused intercept to fail
Intercept container ports now have numbers appended to them if there are multiple ports from multiple containers with the same name. This bugfix works around an issue where Kubernetes allows multiple port definitions in a pod spec to have the same name.
##
bugfix
Don't include k8s-defs.json to chart package
The k8s-defs.json was unnecessarily included to the Helm chart package and this increased the Helm release secret size so much that it could prevent installation of the Helm chart depending on k8s settings. To fix this k8s-defs.json is not included to the Helm chart anymore.
## Version 2.22.4 (April 26) ##
bugfix
Don't require internet access when installing the traffic-manager using Helm
A regression occurred with the introduction of Helm validation in version 2.22.0. The schema relied on external HTTPS links, which inadvertently created a requirement for internet accessibility. To resolve this, we have embedded these resources within the schema, thus removing the need for an internet connection.
##
bugfix
Client failed connect with "failed to exit idle mode" in the connector.log after being idle
The port-forward connections used for connecting the daemon to the traffic-agents were using an incorrect context, causing them to fail after being idle for some time.
##
bugfix
Fix deadlock in Telepresence daemon
A deadlock would sometimes occur in the Telepresence daemon that prevented it from doing a clean exit during `telepresence quit`.
##
bugfix
Don't log error message when a pod watcher ends due to cancellation
Errors printed in the daemon log during normal cancellation of the WatchAgentPods goroutine are now removed.
## Version 2.22.3 (April 8) ##
change
The Windows install script will now install Telepresence to "%ProgramFiles%\telepresence"
Telepresence is now installed into "%ProgramFiles%\telepresence" instead of "C:\telepresence". The directory and the Path entry for `C:\telepresence` are not longer used and should be removed.
##
bugfix
[The Windows install script didn't handle upgrades properly](https://github.com/telepresenceio/telepresence/issues/3827)
The following changes were made: - The script now requires administrator privileges - The Path environment is only updated when there's a need for it
##
bugfix
[The Telepresence Helm chart could not be used as a dependency in another chart.](https://github.com/telepresenceio/telepresence/issues/3833)
The JSON schema validation implemented in Telepresence 2.22.0 had a defect: it rejected the `global` object. This object, a Helm-managed construct, facilitates the propagation of arbitrary configurations from a parent chart to its dependencies. Consequently, charts intended for dependency use must permit the presence of the `global` object.
##
bugfix
[Recreating namespaces was not possible when using a dynamically namespaced Traffic Manager](https://github.com/telepresenceio/telepresence/issues/3831)
A shared informer was sometimes reused when namespaces were removed and then later added again, leading to errors like "handler ... was not added to shared informer because it has stopped already".
##
bugfix
Single label name DNS lookups didn't work unless at least one traffic-agent was installed
A problem with incorrect handling of single label names in the traffic-manager's DNS resolver was fixed. The problem would cause lookups like `curl echo` to fail, even though telepresence was connected to a namespace containing an "echo" service, unless at least one of the workloads in the connected namespace had a traffic-agent.
## Version 2.22.2 (March 28) ##
bugfix
[Panic when using telepresence replace in a IPv6-only cluster](https://github.com/telepresenceio/telepresence/issues/3828)
A "slice bounds out of range" would occur when the targeted Pod's Traffic Agent requested a local dialer to be created on the client. This was due to a glitch in the VPN-tunnel implementation that got triggered when a remote IPv6-address was combined with a local IPv4-address.
## Version 2.22.1 (March 27) ##
bugfix
Only restore inactive traffic-agent after a replace.
A regression in the 2.20.0 release would cause the traffic-agent to be replaced with a dormant version that didn't touch any ports when an intercept ended. This terminated other ongoing intercepts on the same pod. This is now changed so that the traffic-agent remains unaffected for this use-case.
## Version 2.22.0 (March 14) ##
feature
New telepresence replace command.
The new `telepresence replace` command simplifies and clarifies container replacement. Previously, the `--replace` flag within the `telepresence intercept` command was used to replace containers. However, this approach introduced inconsistencies and limitations: * **Confusion:** Using a flag to modify the core function of a command designed for traffic interception led to ambiguity. * **Inaccurate Behavior:** Replacement was not possible when no incoming traffic was intercepted, as the command's design focused on traffic routing. To address these issues, the `--replace` flag within `telepresence intercept` has been deprecated. The new `telepresence replace` command provides a dedicated and consistent method for replacing containers, enhancing clarity and reliability. Key differences between `replace` and `intercept`: 1. **Scope:** The `replace` command targets and affects an entire container, impacting all its traffic, while an `intercept` targets specific services and/or service/container ports. 2. **Port Declarations:** Remote ports specified using the `--port` flag are container ports. 3. **No Default Port:** A `replace` can occur without intercepting any ports. 4. **Container State:** During a `replace`, the original container is no longer active within the cluster. The deprecated `--replace` flag still works, but is hidden from the `telepresence intercept` command help, and will print a deprecation warning when used.
##
feature
Add json-schema for the Telepresence Helm Chart
Helm can validate a chart using a json-schema using the command `helm lint`, and this schema can be part of the actual Helm chart. The telepresence-oss Helm chart now includes such a schema, and a new `telepresence helm lint` command was added so that linting can be performed using the embedded chart.
##
feature
No dormant container present during replace.
Telepresence will no longer inject a dormant container during a `telepresence replace` operation. Instead, the Traffic Agent now directly serves as the replacement container, eliminating the need to forward traffic to the original application container. This simplification offers several advantages when using the `--replace` flag: - **Removal of the init-container:** The need for a separate init-container is no longer necessary. - **Elimination of port renames:** Port renames within the intercepted pod are no longer required.
##
feature
One single invocation of the Telepresence intercept command can now intercept multiple ports.
It is now possible to intercept multiple ports with one single invocation of `telepresence intercept` by just repeating the `--port` flag.
##
feature
[Unify how Traffic Manager selects namespaces](install/manager#static-versus-dynamic-namespace-selection)
The definition of what namespaces that a Traffic Manager would manage use was scattered into several Helm chart values, such as `manager.Rbac.namespaces`, `client.Rbac.namespaces`, and `agentInjector.webhook.namespaceSelector`. The definition is now unified to the mutual exclusive top-level Helm chart values `namespaces` and `namespaceSelector`. The `namespaces` value is just for convenience and a short form of expressing: ```yaml namespaceSelector: matchExpressions: - key: kubernetes.io/metadata.name operator: in values: . ```
##
feature
Improved control over how remote volumes are mounted using mount policies
Mount policies, that affects how the telepresence traffic-agent shares the pod's volumes, and also how the client will mount them, can now be provided using the Helm chart value `agent.mountPolicies` or as JSON object in the workload annotation `telepresence.io/mount-policies`. A mount policy is applied to a volume or to all paths matching a path-prefix (distinguished by checking if first character is a '/'), and can be one of `Ignore`, `Local`, `Remote`, or `RemoteReadOnly`.
##
feature
List output includes workload kind.
The output of the `telepresence list` command will now include the workload kind (deployment, replicaset, statefulset, or rollout) in all entries.
##
feature
Add ability to override the default securityContext for the Telepresence init-container
Users can now use the Helm value `agent.initSecurityContext` to override the default securityContext for the Telepresence init-container.
##
change
Let download page use direct links to GitHub
The download links on the release page now points directly to the assets on the download page, instead of using being routed from getambassador.io/download/tel2oss/releases.
##
change
Use telepresence.io as annotation prefix instead of telepresence.getambassador.io
The workload and pod annotations used by Telepresence will now use the prefix `telepresence.io` instead of `telepresence.getambassador.io`. The new prefix is consistent with the prefix used by labels, and it also matches the host name of the documentation site. Annotations using the old name will still work, but warnings will be logged when they are encountered.
##
change
Make the DNS recursion check configurable and turn it off by default.
Very few systems experience a DNS recursion lookup problem. It can only occur when the cluster runs locally and the cluster's DNS is configured to somehow use DNS server that is started by Telepresence. The check is therefore now configurable through the client setting `dns.recursionCheck`, and it is `false` by default.
##
change
Trigger the mutating webhook with Kubernetes eviction objects instead of patching workloads.
Telepresence will now attempt to evict pods in order to trigger the traffic-agent's injection or removal, and revert to patching workloads if evictions are prevented by the pod's disruption budget. This causes a slight change in the traffic-manager RBAC, as the traffic-manager must be able to create "pod/eviction" objects.
##
change
The telepresence-agents configmap is no longer used.
The traffic-agent configuration was moved into a pod-annotation. This avoids sync problems between the telepresence-agents (which is no no longer present) and the pods.
##
change
Drop deprecated current-cluster-id command.
The clusterID was deprecated some time ago, and replaced by the ID of the namespace where the traffic-manager is installed.
##
bugfix
Make telepresence connect --docker work with Rancher Desktop
Rancher Desktop will start a K3s control-plane and typically expose the Kubernetes API server at `127.0.0.1:6443`. Telepresence can connect to this cluster when running on the host, but the address is not available when connecting in docker mode. The problem is solved by ensuring that the Kubernetes API server address used when doing a `telepresence connect --docker` is swapped from 127.0.0.1 to the internal address of the control-plane node. This works because that address is available to other docker containers, and the Kubernetes API server is configured with a certificate that accepts it.
##
bugfix
Rename charts/telepresence to charts/telepresence-oss.
The Helm chart name "telepresence-oss" was inconsistent with its contained folder "telepresence". As a result, attempts to install the chart using an argo ApplicationSet failed. The contained folder was renamed to match the chart name.
##
bugfix
[Conflict detection between namespaced and cluster-wide install.](install/manager#namespace-collision-detection)
The namespace conflict detection mechanism would only discover conflicts between two _namespaced_ Traffic Managers trying to manage the same namespace. This is now fixed so that all types conflicts are discovered.
##
bugfix
Don't dispatch DNS discovery queries to the cluster.
macOS based systems will often PTR queries using nameslike `b._dns-sd._udp`, lb._dns-sd._udp`, or `db-dns-sd._udp`. Those queries are no longer dispatched to the cluster.
##
bugfix
Using the --namespace option with telepresence causes a deadlock.
Using `telepresence list --namespace ` with a namespace different from the one that telepresence was connected to, would cause a deadlock, and then produce an empty list.
##
bugfix
Fix problem with exclude-suffix being hidden by DNS search path.
In some situations, a name ending with an exclude-suffix like "xyz.com" would be expanded by a search path into "xyz.com.<connected namespace>" and therefore not be excluded. Instead, the name was sent to the cluster to be resolved, causing an unnecessary load on its DNS server.
## Version 2.21.3 (February 6) ##
bugfix
Using the --proxy-via flag would sometimes cause connection timeouts.
Typically, a `telepresence connect --proxy-via =` would fail with a "deadline exceeded" message when several workloads were present in the namespace, the one targeted by the proxy-via didn't yet have an agent installed, and other workloads had an agent. This was due to a race condition in the logic for the agent-based port-forwards in the root daemon. The conditions causing this race are now eliminated.
##
bugfix
Fix panic in root daemon when using the "allow conflicting subnets" feature on macOS.
A regression was introduced in version 2.21.0, causing a panic due to an unimplemented method in the TUN-device on macOS based clients.
##
bugfix
Ensure that annotation enabled traffic-agents are uninstall when uninstalling the traffic-manager.
A traffic-agent injected because the workload had the inject annotation enabled would sometimes not get uninstalled when the traffic-manager was uninstalled.
## Version 2.21.2 (January 26) ##
bugfix
Fix panic when agentpf.client creates a Tunnel
A race could occur where several requests where made to `agentpf.client.Tunnel` on a client that had errored when creating its port-forward to the agent. The implementation could handle one such requests but not several, resulting in a panic in situations where multiple simultaneous requests were made to the same client during a very short time period,
##
bugfix
Fix goroutine leak in dialer.
The context passed to the `Tunnel` call that creates a stream for a dialer, was not cancelled when the dialer was finished, so the stream was never properly closed, leading to one dormant goroutine for each stream.
## Version 2.21.1 (December 17) ##
bugfix
[Allow ingest of serverless deployments without specifying an inject-container-ports annotation](https://github.com/telepresenceio/telepresence/issues/3741)
The ability to intercept a workload without a service is built around the `telepresence.getambassador.io/inject-container-ports` annotation, and it was also required in order to ingest such a workload. This was counterintuitive and the requirement was removed. An ingest doesn't use a port.
##
bugfix
Upgrade module dependencies to get rid of critical vulnerability.
Upgrade module dependencies to latest available stable. This includes upgrading golang.org/x/crypto, which had critical issues, from 0.30.0 to 0.31.0 where those issues are resolved.
## Version 2.21.0 (December 13) ##
feature
[Automatic VPN conflict avoidance](reference/vpn)
Telepresence not only detects subnet conflicts between the cluster and workstation VPNs but also resolves them by performing network address translation to move conflicting subnets out of the way.
##
feature
[Virtual Address Translation (VNAT).](reference/vpn)
It is now possible to use a virtual subnet without routing the affected IPs to a specific workload. A new `telepresence connect --vnat CIDR` flag was added that will perform virtual network address translation of cluster IPs. This flag is very similar to the `--proxy-via CIDR=WORKLOAD` introduced in 2.19, but without the need to specify a workload.
##
feature
[Intercepts targeting a specific container](reference/engagements/container)
In certain scenarios, the container owning the intercepted port differs from the container the intercept targets. This port owner's sole purpose is to route traffic from the service to the intended container, often using a direct localhost connection. This update introduces a `--container ` option to the intercept command. While this option doesn't influence the port selection, it guarantees that the environment variables and mounts propagated to the client originate from the specified container. Additionally, if the `--replace` option is used, it ensures that this container is replaced.
##
feature
[New telepresence ingest command](howtos/intercepts#ingest-your-service)
The new `telepresence ingest` command, similar to `telepresence intercept`, provides local access to the volume mounts and environment variables of a targeted container. However, unlike `telepresence intercept`, `telepresence ingest` does not redirect traffic to the container and ensures that the mounted volumes are read-only. An ingest requires a traffic-agent to be installed in the pods of the targeted workload. Beyond that, it's a client-side operation. This allows developers to have multiple simultaneous ingests on the same container.
##
feature
[New telepresence curl command](reference/docker-run#the-telepresence-curl-command)
The new `telepresence curl` command runs curl from within a container. The command requires that a connection has been established using `telepresence connect --docker`, and the container that runs `curl` will share the same network as the containerized telepresence daemon.
##
feature
[New telepresence docker-run command](reference/docker-run#the-telepresence-docker-run-command)
The new `telepresence docker-run ` requires that a connection has been established using `telepresence connect --docker` It will perform a `docker run ` and add the flag necessary to ensure that started container shares the same network as the containerized telepresence daemon.
##
feature
Mount everything read-only during intercept
It is now possible to append ":ro" to the intercept `--mount` flag value. This ensures that all remote volumes that the intercept mounts are read-only.
##
feature
[Unify client configuration](reference/config)
Previously, client configuration was divided between the config.yml file and a Kubernetes extension. DNS and routing settings were initially found only in the extension. However, the Helm client structure allowed entries from both. To simplify this, we've now aligned the config.yml and Kubernetes extension with the Helm client structure. This means DNS and routing settings are now included in both. The Kubernetes extension takes precedence over the config.yml and Helm client object. While the old-style Kubernetes extension is still supported for compatibility, it cannot be used with the new style.
##
feature
Use WebSockets for port-forward instead of the now deprecated SPDY.
Telepresence will now use WebSockets instead of SPDY when creating port-forwards to the Kubernetes Cluster, and will fall back to SPDY when connecting to clusters that don't support SPDY. Use of the deprecated SPDY can be forced by setting `cluster.forceSPDY=true` in the `config.yml`. See [Streaming Transitions from SPDY to WebSockets](https://kubernetes.io/blog/2024/08/20/websockets-transition/) for more information about this transition.
##
feature
Make usage data collection configurable using an extension point, and default to no-ops
The OSS code-base will no longer report usage data to the proprietary collector at Ambassador Labs. The actual calls to the collector remain, but will be no-ops unless a proper collector client is installed using an extension point.
##
feature
[Add deployments, statefulSets, replicaSets to workloads Helm chart value](reference/engagements/sidecar#disable-workloads)
The Helm chart value `workloads` now supports the kinds `deployments.enabled`, `statefulSets.enabled`, `replicaSets.enabled`. and `rollouts.enabled`. All except `rollouts` are enabled by default. The traffic-manager will ignore workloads, and Telepresence will not be able to intercept them, if the `enabled` of the corresponding kind is set to `false`.
##
feature
Improved command auto-completion
The auto-completion of namespaces, services, and containers have been added where appropriate, and the default file auto completion has been removed from most commands.
##
feature
[Docker run flags --publish, --expose, and --network now work with docker mode connections](reference/docker-run#the-telepresence-docker-run-command)
After establishing a connection to a cluster using `telepresence connect --docker`, you can run new containers that share the same network as the containerized daemon that maintains the connection. This enables seamless communication between your local development environment and the remote services. Normally, Docker has a limitation that prevents combining a shared network configuration with custom networks and exposing ports. However, Telepresence now elegantly circumvents this limitation so that a container started with `telepresence docker-run`, `telepresence intercept --docker-run`, or `telepresence ingest --docker-run` can use flags like `--network`, `--publish`, or `--expose`. To achieve this, Telepresence temporarily adds the necessary network to the containerized daemon. This allows the new container to join the same network. Additionally, Telepresence starts extra socat containers to handle port mapping, ensuring that the desired ports are exposed to the local environment.
##
feature
[Prevent recursion in the Telepresence Virtual Network Interface (VIF)](howtos/cluster-in-vm)
Network problems may arise when running Kubernetes locally (e.g., Docker Desktop, Kind, Minikube, k3s), because the VIF on the host is also accessible from the cluster's nodes. A request that isn't handled by a cluster resource might be routed back into the VIF and cause a recursion. These recursions can now be prevented by setting the client configuration property `routing.recursionBlockDuration` so that new connection attempts are temporarily blocked for a specific IP:PORT pair immediately after an initial attempt, thereby effectively ending the recursion.
##
feature
Allow Helm chart to be included as a sub-chart
The Helm chart previously had the unnecessary restriction that the .Release.Name under which telepresence is installed is literally called "traffic-manager". This restriction was preventing telepresence from being included as a sub-chart in a parent chart called anything but "traffic-manager". This restriction has been lifted.
##
feature
Add Windows arm64 client build
Telepresence client is now available for Windows ARM64. Updated the release workflow files in github actions to build and publish the Windows ARM64 client.
##
change
The --agents flag to telepresence uninstall is now the default.
The `telepresence uninstall` was once capable of uninstalling the traffic-manager as well as traffic-agents. This behavior has been deprecated for some time now and in this release, the command is all about uninstalling the agents. Therefore the `--agents` flag was made redundant and whatever arguments that are given to the command must be name of workloads that have an agent installed unless the `--all-agents` is used, in which case no arguments are allowed.
##
change
Performance improvement for the telepresence list command
The `telepresence list` command will now retrieve its data from the traffic-manager, which significantly improves its performance when used on namespaces that have a lot of workloads.
##
change
During an intercept, the local port defaults to the targeted port of the intercepted container instead of 8080.
Telepresence mimics the environment of a target container during an intercept, so it's only natural that the default for the local port is determined by the targeted container port rather than just defaulting to 8080. A default can still be explicitly defined using the `config.intercept.defaultPort` setting.
##
change
Move the telepresence-intercept-env configmap data into traffic-manager configmap.
There's no need for two configmaps that store configuration data for the traffic manager. The traffic-manager configmap is also watched, so consolidating the configuration there saves some k8s API calls.
##
change
Tracing was removed.
The ability to collect trace has been removed along with the `telepresence gather-traces` and `telepresence upload-traces` commands. The underlying code was complex and has not been well maintained since its inception in 2022. We have received no feedback on it and seen no indication that it has ever been used.
##
bugfix
Remove obsolete code checking the Docker Bridge for DNS
The DNS resolver checked the Docker bridge for messages on Linux. This code was obsolete and caused problems when running in Codespaces.
##
bugfix
Fix telepresence connect confusion caused by /.dockerenv file
A `/.dockerenv` will be present when running in a GitHub Codespaces environment. That doesn't mean that telepresence cannot use docker, or that the root daemon shouldn't start.
##
bugfix
Cap timeouts.connectivityCheck at 5 seconds.
The timeout value of `timeouts.connectivityCheck` is used when checking if a cluster is already reachable without Telepresence setting up an additional network route. If it is, this timeout should be high enough to cover the delay when establishing a connection. If this delay is higher than a second, then chances are very low that the cluster already is reachable, and if it can, that all accesses to it will be very slow. In such cases, Telepresence will create its own network interface and do perform its own tunneling. The default timeout for the check remains at 500 millisecond, which is more than sufficient for the majority of cases.
##
bugfix
Prevent that traffic-manager injects a traffic-agent into itself.
The traffic-manager can never be a subject for an intercept, ingest, or proxy-via, because that means that it injects the traffic-agent into itself, and it is not designed to do that. A user attempting this will now see a meaningful error message.
##
bugfix
Don't include pods in the kube-system namespace when computing pod-subnets from pod IPs
A user would normally never access pods in the `kube-system` namespace directly, and automatically including pods included there when computing the subnets will often lead to problems when running the cluster locally. This namespace is therefore now excluded in situations when the pod subnets are computed from the IPs of pods. Services in this namespace will still be available through the service subnet. If a user should require the pod-subnet to be mapped, it can be added to the `client.routing.alsoProxy` list in the helm chart.
##
bugfix
Let routes belonging to an allowed conflict be added as a static route on Linux.
The `allowConflicting` setting didn't always work on Linux because the conflicting subnet was just added as a link to the TUN device, and therefore didn't get subjected to routing rule used to assign priority to the given subnet.
## Version 2.20.3 (November 18) ##
bugfix
[Ensure that Telepresence works with GitHub Codespaces](https://github.com/telepresenceio/telepresence/issues/3722)
GitHub Codespaces runs in a container, but not as root. Telepresence didn't handle this situation correctly and only started the user daemon. The root daemon was never started.
##
bugfix
[Mounts not working correctly when connected with --proxy-via](https://github.com/telepresenceio/telepresence/issues/3715)
A mount would try to connect to the sftp/ftp server using the original (cluster side) IP although that IP was translated into a virtual IP when using `--proxy-via`.
## Version 2.20.2 (October 21) ##
bugfix
Crash in traffic-manager configured with agentInjector.enabled=false
A traffic-manager that was installed with the Helm value `agentInjector.enabled=false` crashed when a client used the commands `telepresence version` or `telepresence status`. Those commands would call a method on the traffic-manager that panicked if no traffic-agent was present. This method will now instead return the standard `Unavailable` error code, which is expected by the caller.
## Version 2.20.1 (October 10) ##
bugfix
Some workloads missing in the telepresence list output (typically replicasets owned by rollouts).
Version 2.20.0 introduced a regression in the `telepresence list` command, resulting in the omission of all workloads that were owned by another workload. The correct behavior is to just omit those workloads that are owned by the supported workload kinds `Deployment`, `ReplicaSet`, `StatefulSet`, and `Rollout`. Furthermore, the `Rollout` kind must only be considered supported when the Argo Rollouts feature is enabled in the traffic-manager.
##
bugfix
Allow comma separated list of daemons for the gather-logs command.
The name of the `telepresence gather-logs` flag `--daemons` suggests that the argument can contain more than one daemon, but prior to this fix, it couldn't. It is now possible to use a comma separated list, e.g. `telepresence gather-logs --daemons root,user`.
## Version 2.20.0 (October 3) ##
feature
Add timestamp to telepresence_logs.zip filename.
Telepresence is now capable of easily find telepresence gather-logs by certain timestamp.
##
feature
[Enable intercepts of workloads that have no service.](https://telepresence.io/docs/reference/engagements/cli#intercepting-without-a-service)
Telepresence is now capable of intercepting workloads that have no associated service. The intercept will then target container port instead of a service port. The new behavior is enabled by adding a telepresence.getambassador.io/inject-container-ports annotation where the value is a comma separated list of port identifiers consisting of either the name or the port number of a container port, optionally suffixed with `/TCP` or `/UDP`.
##
feature
[Publish the OSS version of the telepresence Helm chart](https://artifacthub.io/packages/helm/telepresence-oss/telepresence-oss)
The OSS version of the telepresence helm chart is now available at ghcr.io/telepresenceio/telepresence-oss, and can be installed using the command:
helm install traffic-manager oci://ghcr.io/telepresenceio/telepresence-oss --namespace ambassador --version 2.20.0 The chart documentation is published at ArtifactHUB.
##
feature
[Control the syntax of the environment file created with the intercept flag --env-file](https://telepresence.io/docs/reference/environment)
A new --env-syntax <syntax> was introduced to allow control over the syntax of the file created when using the intercept flag --env-file <file>. Valid syntaxes are "docker", "compose", "sh", "csh", "cmd", and "ps"; where "sh", "csh", and "ps" can be suffixed with ":export".
##
feature
Add support for Argo Rollout workloads.
Telepresence now has an opt-in support for Argo Rollout workloads. The behavior is controlled by `workloads.argoRollouts.enabled` Helm chart value. It is recommended to set the following annotation telepresence.getambassador.io/inject-traffic-agent: enabled to avoid creation of unwanted revisions.
##
bugfix
Enable intercepts of containers that bind to podIP
In previous versions, the traffic-agent would route traffic to localhost during periods when an intercept wasn't active. This made it impossible for an application to bind to the pod's IP, and it also meant that service meshes binding to the podIP would get bypassed, both during and after an intercept had been made. This is now changed, so that the traffic-agent instead forwards non intercepted requests to the pod's IP, thereby enabling the application to either bind to localhost or to that IP.
##
change
Use ghcr.io/telepresenceio instead of docker.io/datawire for OSS images and the telemount Docker volume plugin.
All OSS telepresence images and the telemount Docker plugin are now published at the public registry ghcr.io/telepresenceio and all references from the client and traffic-manager has been updated to use this registry instead of the one at docker.io/datawire.
##
change
Use nftables instead of iptables-legacy
Some time ago, we introduced iptables-legacy because users had problems using Telepresence with Fly.io where nftables wasn't supported by the kernel. Fly.io has since fixed this, so Telepresence will now use nftables again. This in turn, ensures that modern systems that lack support iptables-legacy will work.
##
bugfix
Root daemon wouldn't start when sudo timeout was zero.
The root daemon refused to start when sudo was configured with a timestamp_timeout=0. This was due to logic that first requested root privileges using a sudo call, and then relied on that these privileges were cached, so that a subsequent call using --non-interactive was guaranteed to succeed. This logic will now instead do one single sudo call, and rely solely on sudo to print an informative prompt and start the daemon in the background.
##
bugfix
Detect minikube network when connecting with --docker
A telepresence connect --docker failed when attempting to connect to a minikube that uses a docker driver because the containerized daemon did not have access to the minikube docker network. Telepresence will now detect an attempt to connect to that network and attach it to the daemon container as needed.
## Version 2.19.1 (July 12) ##
feature
[Add brew support for the OSS version of Telepresence.](https://github.com/telepresenceio/telepresence/issues/3609)
The Open-Source Software version of Telepresence can now be installed using the brew formula via brew install telepresenceio/telepresence/telepresence-oss.
##
feature
Add --create-namespace flag to the telepresence helm install command.
A --create-namespace (default true) flag was added to the telepresence helm install command. No attempt will be made to create a namespace for the traffic-manager if it is explicitly set to false. The command will then fail if the namespace is missing.
##
feature
Introduce DNS fallback on Windows.
A network.defaultDNSWithFallback config option has been introduced on Windows. It will cause the DNS-resolver to fall back to the resolver that was first in the list prior to when Telepresence establishes a connection. The option is default true since it is believed to give the best experience but can be set to false to restore the old behavior.
##
feature
[Brew now supports MacOS (amd64/arm64) / Linux (amd64)](https://github.com/datawire/homebrew-blackbird/issues/19)
The brew formula can now dynamically support MacOS (amd64/arm64) / Linux (amd64) in a single formula
##
feature
Add ability to provide an externally-provisioned webhook secret
Added supplied as a new option for agentInjector.certificate.method. This fully disables the generation of the Mutating Webhook's secret, allowing the chart to use the values of a pre-existing secret named agentInjector.secret.name. Previously, the install would fail when it attempted to create or update the externally-managed secret.
##
feature
Let PTR query for DNS server return the cluster domain.
The nslookup program on Windows uses a PTR query to retrieve its displayed "Server" property. This Telepresence DNS resolver will now return the cluster domain on such a query.
##
feature
Add scheduler name to PODs templates.
A new Helm chart value schedulerName has been added. With this feature, we are able to define some particular schedulers from Kubernetes to apply some different strategies to allocate telepresence resources, including the Traffic Manager and hooks pods.
##
bugfix
Race in traffic-agent injector when using inject annotation
Applying multiple deployments that used the telepresence.getambassador.io/inject-traffic-agent: enabled would cause a race condition, resulting in a large number of created pods that eventually had to be deleted, or sometimes in pods that didn't contain a traffic agent.
##
bugfix
Fix configuring custom agent security context
-> The traffic-manager helm chart will now correctly use a custom agent security context if one is provided.
## Version 2.19.0 (June 15) ##
feature
Warn when an Open Source Client connects to an Enterprise Traffic Manager.
The difference between the OSS and the Enterprise offering is not well understood, and OSS users often install a traffic-manager using the Helm chart published at getambassador.io. This Helm chart installs an enterprise traffic-manager, which is probably not what the user would expect. Telepresence will now warn when an OSS client connects to an enterprise traffic-manager and suggest switching to an enterprise client, or use telepresence helm install to install an OSS traffic-manager.
##
feature
Add scheduler name to PODs templates.
A new Helm chart value schedulerName has been added. With this feature, we are able to define some particular schedulers from Kubernetes to apply some different strategies to allocate telepresence resources, including the Traffic Manager and hooks pods.
##
bugfix
Improve traffic-manager performance in very large clusters.
-> The traffic-manager will now use a shared-informer when keeping track of deployments. This will significantly reduce the load on the Kublet in large clusters and therefore lessen the risk for the traffic-manager being throttled, which can lead to other problems.
##
bugfix
Kubeconfig exec authentication failure when connecting with --docker from a WSL linux host
Clusters like Amazon EKS often use a special authentication binary that is declared in the kubeconfig using an exec authentication strategy. This binary is normally not available inside a container. Consequently, a modified kubeconfig is used when telepresence connect --docker executes, appointing a kubeauth binary which instead retrieves the authentication from a port on the Docker host that communicates with another process outside of Docker. This process then executes the original exec command to retrieve the necessary credentials. This setup was problematic when using WSL, because even though telepresence connect --docker was executed on a Linux host, the Docker host available from host.docker.internal that the kubeauth connected to was the Windows host running Docker Desktop. The fix for this was to use the local IP of the default route instead of host.docker.internal when running under WSL..
##
bugfix
Fix bug in workload cache, causing endless recursion when a workload uses the same name as its owner.
The workload cache was keyed by name and namespace, but not by kind, so a workload named the same as its owner workload would be found using the same key. This led to the workload finding itself when looking up its owner, which in turn resulted in an endless recursion when searching for the topmost owner.
##
bugfix
FailedScheduling events mentioning node availability considered fatal when waiting for agent to arrive.
The traffic-manager considers some events as fatal when waiting for a traffic-agent to arrive after an injection has been initiated. This logic would trigger on events like "Warning FailedScheduling 0/63 nodes are available" although those events indicate a recoverable condition and kill the wait. This is now fixed so that the events are logged but the wait continues.
##
bugfix
Improve how the traffic-manager resolves DNS when no agent is installed.
The traffic-manager is typically installed into a namespace different from the one that clients are connected to. It's therefore important that the traffic-manager adds the client's namespace when resolving single label names in situations where there are any agents to dispatch the DNS query to.
##
change
Removal of ability import legacy artifact into Helm.
A helm install would make attempts to find manually installed artifacts and make them managed by Helm by adding the necessary labels and annotations. This was important when the Helm chart was first introduced but is far less so today, and this legacy import was therefore removed.
##
bugfix
[Docker aliases deprecation caused failure to detect Kind cluster.](https://docs.docker.com/engine/deprecated/#container-short-id-in-network-aliases-field)
The logic for detecting if a cluster is a local Kind cluster, and therefore needs some special attention when using telepresence connect --docker, relied on the presence of Aliases in the Docker network that a Kind cluster sets up. In Docker versions from 26 and up, this value is no longer used, but the corresponding info can instead be found in the new DNSNames field.
##
bugfix
[Include svc as a top-level domain in the DNS resolver.](https://github.com/telepresenceio/telepresence/issues/2814)
It's not uncommon that use-cases involving Kafka or other middleware use FQNs that end with "svc". The core-DNS resolver in Kubernetes can resolve such names. With this bugfix, the Telepresence DNS resolver will also be able to resolve them, and thereby remove the need to add ".svc" to the include-suffix list.
##
feature
Add ability to enable/disable the mutating webhook.
A new Helm chart boolean value agentInjector.enable has been added that controls the agent-injector service and its associated mutating webhook. If set to false, the service, the webhook, and the secrets and certificates associated with it, will no longer be installed.
##
feature
Add ability to mount a webhook secret.
A new Helm chart value agentInjector.certificate.accessMethod which can be set to watch (the default) or mount has been added. The mount setting is intended for clusters with policies that prevent containers from doing a get, list or watch of a Secret, but where a latency of up to 90 seconds is acceptable between the time the secret is regenerated and the agent-injector picks it up.
##
feature
Make it possible to specify ignored volume mounts using path prefix.
Volume mounts like /var/run/secrets/kubernetes.io are not declared in the workload. Instead, they are injected during pod-creation and their names are generated. It is now possible to ignore such mounts using a matching path prefix.
##
feature
Make the telemount Docker Volume plugin configurable
A telemount object was added to the intercept object in config.yml (or Helm value client.intercept), so that the automatic download and installation of this plugin can be fully customised.
##
feature
Add option to load the kubeconfig yaml from stdin during connect.
This allows another process with a kubeconfig already loaded in memory to directly pass it to telepresence connect without needing a separate file. Simply use a dash "-" as the filename for the --kubeconfig flag.
##
feature
Add ability to specify agent security context.
A new Helm chart value agent.securityContext that will allow configuring the security context of the injected traffic agent. The value can be set to a valid Kubernetes securityContext object, or can be set to an empty value ({}) to ensure the agent has no defined security context. If no value is specified, the traffic manager will set the agent's security context to the same as the first container's of the workload being injected into.
##
change
Tracing is no longer enabled by default.
Tracing must now be enabled explicitly in order to use the telepresence gather-traces command.
##
change
Removal of timeouts that are no longer in use
The config.yml values timeouts.agentInstall and timeouts.apply haven't been in use since versions prior to 2.6.0, when the client was responsible for installing the traffic-agent. These timeouts are now removed from the code-base, and a warning will be printed when attempts are made to use them.
##
bugfix
Search all private subnets to find one open for dnsServerSubnet
This resolves a bug that did not test all subnets in a private range, sometimes resulting in the warning, "DNS doesn't seem to work properly."
##
bugfix
Docker aliases deprecation caused failure to detect Kind cluster.
The logic for detecting if a cluster is a local Kind cluster, and therefore needs some special attention when using telepresence connect --docker, relied on the presence of Aliases in the Docker network that a Kind cluster sets up. In Docker versions from 26 and up, this value is no longer used, but the corresponding info can instead be found in the new DNSNames field.
##
bugfix
Creation of individual pods was blocked by the agent-injector webhook.
An attempt to create a pod was blocked unless it was provided by a workload. Hence, commands like kubectl run -i busybox --rm --image=curlimages/curl --restart=Never -- curl echo-easy.default would be blocked from executing.
##
bugfix
Fix panic due to root daemon not running.
If a telepresence connect was made at a time when the root daemon was not running (an abnormal condition) and a subsequent intercept was then made, a panic would occur when the port-forward to the agent was set up. This is now fixed so that the initial telepresence connect is refused unless the root daemon is running.
##
bugfix
Get rid of telemount plugin stickiness
The datawire/telemount that is automatically downloaded and installed, would never be updated once the installation was made. Telepresence will now check for the latest release of the plugin and cache the result of that check for 24 hours. If a new version arrives, it will be installed and used.
##
bugfix
Use route instead of address for CIDRs with masks that don't allow "via"
A CIDR with a mask that leaves less than two bits (/31 or /32 for IPv4) cannot be added as an address to the VIF, because such addresses must have bits allowing a "via" IP. The logic was modified to allow such CIDRs to become static routes, using the VIF base address as their "via", rather than being VIF addresses in their own right.
##
bugfix
Containerized daemon created cache files owned by root
When using telepresence connect --docker to create a containerized daemon, that daemon would sometimes create files in the cache that were owned by root, which then caused problems when connecting without the --docker flag.
##
bugfix
Remove large number of requests when traffic-manager is used in large clusters.
The traffic-manager would make a very large number of API requests during cluster start-up or when many services were changed for other reasons. The logic that did this was refactored and the number of queries were significantly reduced.
##
bugfix
Don't patch probes on replaced containers.
A container that is being replaced by a telepresence intercept --replace invocation will have no liveness-, readiness, nor startup-probes. Telepresence didn't take this into consideration when injecting the traffic-agent, but now it will refrain from patching symbolic port names of those probes.
##
bugfix
Don't rely on context name when deciding if a kind cluster is used.
The code that auto-patches the kubeconfig when connecting to a kind cluster from within a docker container, relied on the context name starting with "kind-", but although all contexts created by kind have that name, the user is still free to rename it or to create other contexts using the same connection properties. The logic was therefore changed to instead look for a loopback service address.
## Version 2.18.0 (February 9) ##
feature
Include the image for the traffic-agent in the output of the version and status commands.
The version and status commands will now output the image that the traffic-agent will be using when injected by the agent-injector.
##
feature
Custom DNS using the client DNS resolver.

A new telepresence connect --proxy-via CIDR=WORKLOAD flag was introduced, allowing Telepresence to translate DNS responses matching specific subnets into virtual IPs that are used locally. Those virtual IPs are then routed (with reverse translation) via the pod's of a given workload. This makes it possible to handle custom DNS servers that resolve domains into loopback IPs. The flag may also be used in cases where the cluster's subnets are in conflict with the workstation's VPN.

The CIDR can also be a symbolic name that identifies a subnet or list of subnets:

alsoAll subnets added with --also-proxy
serviceThe cluster's service subnet
podsThe cluster's pod subnets.
allAll of the above.
##
bugfix
Ensure that agent.appProtocolStrategy is propagated correctly.
The agent.appProtocolStrategy was inadvertently dropped when moving license related code fromm the OSS repository the repository for the Enterprise version of Telepresence. It has now been restored.
##
bugfix
Include non-default zero values in output of telepresence config view.
The telepresence config view command will now print zero values in the output when the default for the value is non-zero.
##
bugfix
Restore ability to run the telepresence CLI in a docker container.
The improvements made to be able to run the telepresence daemon in docker using telepresence connect --docker made it impossible to run both the CLI and the daemon in docker. This commit fixes that and also ensures that the user- and root-daemons are merged in this scenario when the container runs as root.
##
bugfix
Remote mounts when intercepting with the --replace flag.
A telepresence intercept --replace did not correctly mount all volumes, because when the intercepted container was removed, its mounts were no longer visible to the agent-injector when it was subjected to a second invocation. The container is now kept in place, but with an image that just sleeps infinitely.
##
bugfix
Intercepting with the --replace flag will no longer require all subsequent intercepts to use --replace.
A telepresence intercept --replace will no longer switch the mode of the intercepted workload, forcing all subsequent intercepts on that workload to use --replace until the agent is uninstalled. Instead, --replace can be used interchangeably just like any other intercept flag.
##
bugfix
Kubeconfig exec authentication with context names containing colon didn't work on Windows
The logic added to allow the root daemon to connect directly to the cluster using the user daemon as a proxy for exec type authentication in the kube-config, didn't take into account that a context name sometimes contains the colon ":" character. That character cannot be used in filenames on windows because it is the drive letter separator.
##
bugfix
Provide agent name and tag as separate values in Helm chart
The AGENT_IMAGE was a concatenation of the agent's name and tag. This is now changed so that the env instead contains an AGENT_IMAGE_NAME and AGENT_INAGE_TAG. The AGENT_IMAGE is removed. Also, a new env REGISTRY is added, where the registry of the traffic- manager image is provided. The AGENT_REGISTRY is no longer required and will default to REGISTRY if not set.
##
bugfix
Environment interpolation expressions were prefixed twice.
Telepresence would sometimes prefix environment interpolation expressions in the traffic-agent twice so that an expression that looked like $(SOME_NAME) in the app-container, ended up as $(_TEL_APP_A__TEL_APP_A_SOME_NAME) in the corresponding expression in the traffic-agent.
##
bugfix
Panic in root-daemon on darwin workstations with full access to cluster network.
A darwin machine with full access to the cluster's subnets will never create a TUN-device, and a check was missing if the device actually existed, which caused a panic in the root daemon.
##
bugfix
Show allow-conflicting-subnets in telepresence status and telepresence config view.
The telepresence status and telepresence config view commands didn't show the allowConflictingSubnets CIDRs because the value wasn't propagated correctly to the CLI.
##
feature
It is now possible use a host-based connection and containerized connections simultaneously.
Only one host-based connection can exist because that connection will alter the DNS to reflect the namespace of the connection. but it's now possible to create additional connections using --docker while retaining the host-based connection.
##
feature
Ability to set the hostname of a containerized daemon.
The hostname of a containerized daemon defaults to be the container's ID in Docker. You now can override the hostname using telepresence connect --docker --hostname <a name>.
##
feature
New --multi-daemonflag to enforce a consistent structure for the status command output.
The output of the telepresence status when using --output json or --output yaml will either show an object where the user_daemon and root_daemon are top level elements, or when multiple connections are used, an object where a connections list contains objects with those daemons. The flag --multi-daemon will enforce the latter structure even when only one daemon is connected so that the output can be parsed consistently. The reason for keeping the former structure is to retain backward compatibility with existing parsers.
##
bugfix
Make output from telepresence quit more consistent.
A quit (without -s) just disconnects the host user and root daemons but will quit a container based daemon. The message printed was simplified to remove some have/has is/are errors caused by the difference.
##
bugfix
Fix "tls: bad certificate" errors when refreshing the mutator-webhook secret
The agent-injector service will now refresh the secret used by the mutator-webhook each time a new connection is established, thus preventing the certificates to go out-of-sync when the secret is regenerated.
##
bugfix
Keep telepresence-agents configmap in sync with pod states.
An intercept attempt that resulted in a timeout due to failure of injecting the traffic-agent left the telepresence-agents configmap in a state that indicated that an agent had been added, which caused problems for subsequent intercepts after the problem causing the first failure had been fixed.
##
bugfix
The telepresence status command will now report the status of all running daemons.
A telepresence status, issued when multiple containerized daemons were active, would error with "multiple daemons are running, please select one using the --use <match> flag". This is now fixed so that the command instead reports the status of all running daemons.
##
bugfix
The telepresence version command will now report the version of all running daemons.
A telepresence version, issued when multiple containerized daemons were active, would error with "multiple daemons are running, please select one using the --use <match> flag". This is now fixed so that the command instead reports the version of all running daemons.
##
bugfix
Multiple containerized daemons can now be disconnected using telepresence quit -s
A telepresence quit -s, issued when multiple containerized daemons were active, would error with "multiple daemons are running, please select one using the --use <match> flag". This is now fixed so that the command instead quits all daemons.
##
bugfix
The DNS search path on Windows is now restored when Telepresence quits
The DNS search path that Telepresence uses to simulate the DNS lookup functionality in the connected cluster namespace was not removed by a telepresence quit, resulting in connectivity problems from the workstation. Telepresence will now remove the entries that it has added to the search list when it quits.
##
bugfix
The user-daemon would sometimes get killed when used by multiple simultaneous CLI clients.
The user-daemon would die with a fatal "fatal error: concurrent map writes" error in the connector.log, effectively killing the ongoing connection.
##
bugfix
Multiple services ports using the same target port would not get intercepted correctly.
Intercepts didn't work when multiple service ports were using the same container port. Telepresence would think that one of the ports wasn't intercepted and therefore disable the intercept of the container port.
##
bugfix
Root daemon refuses to disconnect.
The root daemon would sometimes hang forever when attempting to disconnect due to a deadlock in the VIF-device.
##
bugfix
Fix panic in user daemon when traffic-manager was unreachable
The user daemon would panic if the traffic-manager was unreachable. It will now instead report a proper error to the client.
##
change
Removal of backward support for versions predating 2.6.0
The telepresence helm installer will no longer discover and convert workloads that were modified by versions prior to 2.6.0. The traffic manager will and no longer support the muxed tunnels used in versions prior to 2.5.0.
## Version 2.17.0 (November 14) ##
feature
Additional Prometheus metrics to track intercept/connect activity
This feature adds the following metrics to the Prometheus endpoint: connect_count, connect_active_status, intercept_count, and intercept_active_status. These are labeled by client/install_id. Additionally, the intercept_count metric has been renamed to active_intercept_count for clarity.
##
feature
Make the Telepresence client docker image configurable.
The docker image used when running a Telepresence intercept in docker mode can now be configured using the setting images.clientImage and will default first to the value of the environment TELEPRESENCE_CLIENT_IMAGE, and then to the value preset by the telepresence binary. This configuration setting is primarily intended for testing purposes.
##
feature
Use traffic-agent port-forwards for outbound and intercepted traffic.
The telepresence TUN-device is now capable of establishing direct port-forwards to a traffic-agent in the connected namespace. That port-forward is then used for all outbound traffic to the device, and also for all traffic that arrives from intercepted workloads. Getting rid of the extra hop via the traffic-manager improves performance and reduces the load on the traffic-manager. The feature can only be used if the client has Kubernetes port-forward permissions to the connected namespace. It can be disabled by setting cluster.agentPortForward to false in config.yml.
##
feature
Improve outbound traffic performance.
The root-daemon now communicates directly with the traffic-manager instead of routing all outbound traffic through the user-daemon. The root-daemon uses a patched kubeconfig where exec configurations to obtain credentials are dispatched to the user-daemon. This to ensure that all authentication plugins will execute in user-space. The old behavior of routing everything through the user-daemon can be restored by setting cluster.connectFromRootDaemon to false in config.yml.
##
feature
New networking CLI flag --allow-conflicting-subnets
telepresence connect (and other commands that kick off a connect) now accepts an --allow-conflicting-subnets CLI flag. This is equivalent to client.routing.allowConflictingSubnets in the helm chart, but can be specified at connect time. It will be appended to any configuration pushed from the traffic manager.
##
change
Warn if large version mismatch between traffic manager and client.
Print a warning if the minor version diff between the client and the traffic manager is greater than three.
##
change
The authenticator binary was removed from the docker image.
The authenticator binary, used when serving proxied exec kubeconfig credential retrieval, has been removed. The functionality was instead added as a subcommand to the telepresence binary.
## Version 2.16.1 (October 12) ##
feature
Add --docker-debug flag to the telepresence intercept command.
This flag is similar to --docker-build but will start the container with more relaxed security using the docker run flags --security-opt apparmor=unconfined --cap-add SYS_PTRACE.
##
feature
Add a --export option to the telepresence connect command.
In some situations it is necessary to make some ports available to the host from a containerized telepresence daemon. This commit adds a repeatable --expose <docker port exposure> flag to the connect command.
##
feature
Prevent agent-injector webhook from selecting from kube-xxx namespaces.
The kube-system and kube-node-lease namespaces should not be affected by a global agent-injector webhook by default. A default namespaceSelector was therefore added to the Helm Chart agentInjector.webhook that contains a NotIn preventing those namespaces from being selected.
##
bugfix
Backward compatibility for pod template TLS annotations.
Users of Telepresence < 2.9.0 that make use of the pod template TLS annotations were unable to upgrade because the annotation names have changed (now prefixed by "telepresence."), and the environment expansion of the annotation values was dropped. This fix restores support for the old names (while retaining the new ones) and the environment expansion.
##
security
Built with go 1.21.3
Built Telepresence with go 1.21.3 to address CVEs.
##
bugfix
Match service selector against pod template labels
When listing intercepts (typically by calling telepresence list) selectors of services are matched against workloads. Previously the match was made against the labels of the workload, but now they are matched against the labels pod template of the workload. Since the service would actually be matched against pods this is more correct. The most common case when this makes a difference is that statefulsets now are listed when they should.
## Version 2.16.0 (October 2) ##
bugfix
The helm sub-commands will no longer start the user daemon.
The telepresence helm install/upgrade/uninstall commands will no longer start the telepresence user daemon because there's no need to connect to the traffic-manager in order for them to execute.
##
bugfix
Routing table race condition
A race condition would sometimes occur when a Telepresence TUN device was deleted and another created in rapid succession that caused the routing table to reference interfaces that no longer existed.
##
bugfix
Stop lingering daemon container
When using telepresence connect --docker, a lingering container could be present, causing errors like "The container name NN is already in use by container XX ...". When this happens, the connect logic will now give the container some time to stop and then call docker stop NN to stop it before retrying to start it.
##
bugfix
Add file locking to the Telepresence cache
Files in the Telepresence cache are accesses by multiple processes. The processes will now use advisory locks on the files to guarantee consistency.
##
change
Lock connection to namespace
The behavior changed so that a connected Telepresence client is bound to a namespace. The namespace can then not be changed unless the client disconnects and reconnects. A connection is also given a name. The default name is composed from <kube context name>-<namespace> but can be given explicitly when connecting using --name. The connection can optionally be identified using the option --use <name match> (only needed when docker is used and more than one connection is active).
##
change
Deprecation of global --context and --docker flags.
The global flags --context and --docker will now be considered deprecated unless used with commands that accept the full set of Kubernetes flags (e.g. telepresence connect).
##
change
Deprecation of the --namespace flag for the intercept command.
The --namespace flag is now deprecated for telepresence intercept command. The flag can instead be used with all commands that accept the full set of Kubernetes flags (e.g. telepresence connect).
##
change
Legacy code predating version 2.6.0 was removed.
The telepresence code-base still contained a lot of code that would modify workloads instead of relying on the mutating webhook installer when a traffic-manager version predating version 2.6.0 was discovered. This code has now been removed.
##
feature
Add `telepresence list-namespaces` and `telepresence list-contexts` commands
These commands can be used to check accessible namespaces and for automation.
##
change
Implicit connect warning
A deprecation warning will be printed if a command other than telepresence connect causes an implicit connect to happen. Implicit connects will be removed in a future release.
## Version 2.15.1 (September 6) ##
security
Rebuild with go 1.21.1
Rebuild Telepresence with go 1.21.1 to address CVEs.
##
security
Set security context for traffic agent
Openshift users reported that the traffic agent injection was failing due to a missing security context.
## Version 2.15.0 (August 29) ##
security
Add ASLR to telepresence binaries
ASLR hardens binary sercurity against fixed memory attacks.
##
feature
[Added client builds for arm64 architecture.](https://github.com/telepresenceio/telepresence/issues/3259)
Updated the release workflow files in github actions to including building and publishing the client binaries for arm64 architecture.
##
bugfix
[KUBECONFIG env var can now be used with the docker mode.](https://github.com/telepresenceio/telepresence/pull/3300)
If provided, the KUBECONFIG environment variable was passed to the kubeauth-foreground service as a parameter. However, since it didn't exist, the CLI was throwing an error when using telepresence connect --docker.
##
bugfix
[Fix deadlock while watching workloads](https://github.com/telepresenceio/telepresence/pull/3298)
The telepresence list --output json-stream wasn't releasing the session's lock after being stopped, including with a telepresence quit. The user could be blocked as a result.
##
bugfix
Change json output of telepresence list command
Replace deprecated info in the JSON output of the telepresence list command.
## Version 2.14.4 (August 21) ##
bugfix
[Nil pointer exception when upgrading the traffic-manager.](https://github.com/telepresenceio/telepresence/issues/3313)
Upgrading the traffic-manager using telepresence helm upgrade would sometimes result in a helm error message executing "telepresence/templates/intercept-env-configmap.yaml" at <.Values.intercept.environment.excluded>: nil pointer evaluating interface {}.excluded"
## Version 2.14.2 (July 26) ##
bugfix
[Telepresence now use the OSS agent in its latest version by default.](https://github.com/telepresenceio/telepresence/issues/3271)
The traffic manager admin was forced to set it manually during the chart installation.
## Version 2.14.1 (July 7) ##
feature
Envoy's http idle timout is now configurable.
A new agent.helm.httpIdleTimeout setting was added to the Helm chart that controls the proprietary Traffic agent's http idle timeout. The default of one hour, which in some situations would cause a lot of resource consuming and lingering connections, was changed to 70 seconds.
##
feature
Add more gauges to the Traffic manager's Prometheus client.
Several gauges were added to the Prometheus client to make it easier to monitor what the Traffic manager spends resources on.
##
feature
Agent Pull Policy
Add option to set traffic agent pull policy in helm chart.
##
bugfix
Resource leak in the Traffic manager.
Fixes a resource leak in the Traffic manager caused by lingering tunnels between the clients and Traffic agents. The tunnels are now closed correctly when terminated from the side that created them.
##
bugfix
[Fixed problem setting traffic manager namespace using the kubeconfig extension.](https://www.telepresence.io/docs/reference/config#manager)
Fixes a regression introduced in version 2.10.5, making it impossible to set the traffic-manager namespace using the telepresence.io kubeconfig extension.
## Version 2.14.0 (June 12) ##
feature
[DNS configuration now supports excludes and mappings.](https://github.com/telepresenceio/telepresence/pull/3172)
The DNS configuration now supports two new fields, excludes and mappings. The excludes field allows you to exclude a given list of hostnames from resolution, while the mappings field can be used to resolve a hostname with another.
##
feature
Added the ability to exclude environment variables
Added a new config map that can take an array of environment variables that will then be excluded from an intercept that retrieves the environment of a pod.
##
bugfix
Fixed traffic-agent backward incompatibility issue causing lack of remote mounts
A traffic-agent of version 2.13.3 (or 1.13.15) would not propagate the directories under /var/run/secrets when used with a traffic manager older than 2.13.3.
##
bugfix
[Fixed race condition causing segfaults on rare occasions when a tunnel stream timed out.](https://github.com/telepresenceio/telepresence/pull/2963)
A context cancellation could sometimes be trapped in a stream reader, causing it to incorrectly return an undefined message which in turn caused the parent reader to panic on a nil pointer reference.
##
change
Routing conflict reporting.
Telepresence will now attempt to detect and report routing conflicts with other running VPN software on client machines. There is a new configuration flag that can be tweaked to allow certain CIDRs to be overridden by Telepresence.
##
change
test-vpn command deprecated
Running telepresence test-vpn will now print a deprecation warning and exit. The command will be removed in a future release. Instead, please configure telepresence for your VPN's routes.
## Version 2.13.3 (May 25) ##
feature
[Add imagePullSecrets to hooks](https://github.com/telepresenceio/telepresence/pull/3079)
Add .Values.hooks.curl.imagePullSecrets and .Values.hooks curl.imagePullSecrets to Helm values.
##
change
Change reinvocation policy to Never for the mutating webhook
The default setting of the reinvocationPolicy for the mutating webhook dealing with agent injections changed from Never to IfNeeded.
##
bugfix
[Fix mounting fail of IAM roles for service accounts web identity token](https://github.com/telepresenceio/telepresence/issues/3166)
The eks.amazonaws.com/serviceaccount volume injected by EKS is now exported and remotely mounted during an intercept.
##
bugfix
[Correct namespace selector for cluster versions with non-numeric characters](https://github.com/telepresenceio/telepresence/pull/3184)
The mutating webhook now correctly applies the namespace selector even if the cluster version contains non-numeric characters. For example, it can now handle versions such as Major:"1", Minor:"22+".
##
bugfix
[Enable IPv6 on the telepresence docker network](https://github.com/telepresenceio/telepresence/issues/3179)
The "telepresence" Docker network will now propagate DNS AAAA queries to the Telepresence DNS resolver when it runs in a Docker container.
##
bugfix
[Fix the crash when intercepting with --local-only and --docker-run](https://github.com/telepresenceio/telepresence/issues/3171)
Running telepresence intercept --local-only --docker-run no longer results in a panic.
##
bugfix
[Fix incorrect error message with local-only mounts](https://github.com/telepresenceio/telepresence/issues/3171)
Running telepresence intercept --local-only --mount false no longer results in an incorrect error message saying "a local-only intercept cannot have mounts".
##
bugfix
[specify port in hook urls](https://github.com/telepresenceio/telepresence/pull/3161)
The helm chart now correctly handles custom agentInjector.webhook.port that was not being set in hook URLs.
##
bugfix
Fix wrong default value for disableGlobal and agentArrival
Params .intercept.disableGlobal and .timeouts.agentArrival are now correctly honored.
================================================ FILE: docs/release-notes.mdx ================================================ --- title: Release Notes --- import { Note, Title, Body } from '@site/src/components/ReleaseNotes' [comment]: # (Code generated by relnotesgen. DO NOT EDIT.) # Telepresence Release Notes ## Version 2.27.2 (March 9) Fix duplicate Section field in .deb package causing dpkg install failure The .deb package control file contained a duplicate Section field, which caused dpkg -i to fail with a parsing error. The Section field was specified both as a top-level nfpm default and explicitly under deb.fields. Moved to the top-level section key and removed the deb.fields block entirely. ## Version 2.27.1 (March 8) Fix duplicate Priority field in .deb package causing dpkg install failure The .deb package control file contained a duplicate Priority field, which caused dpkg -i to fail with a parsing error. This was caused by nfpm adding Priority: optional by default while the build configuration also specified it explicitly under deb.fields. Fix ingest command lookup when container name is not specified The traffic manager now properly handles ingest lookups when using telepresence ingest <workload> -- command without specifying a container name. Previously, this would fail for workloads because the lookup couldn't find ingests with empty container names. The traffic manager now provides clearer error messages when a container name is required but not specified, and correctly resolves ingests for single-container workloads automatically. ## Version 2.27.0 (February 28) Add macOS package installer with root daemon as a system service A new macOS package installer (.pkg) is now available that installs Telepresence with the root daemon configured as a launchd service. This eliminates the need for elevated privileges when using Telepresence, as the daemon starts automatically at boot and runs in the background. Available for both Intel (amd64) and Apple Silicon (arm64) Macs. Add Linux package installers with root daemon as a system service New Linux package installers (.deb for Debian/Ubuntu and .rpm for Fedora/RHEL) are now available that install Telepresence with the root daemon configured as a systemd service. This eliminates the need for elevated privileges when using Telepresence, as the service is enabled and started automatically during installation. Available for both amd64 and arm64 architectures. Add Windows installer with root daemon as a system service A new Windows installer (.exe) is now available that installs Telepresence with the root daemon configured as a Windows service. The installer bundles WinFSP and SSHFS-Win dependencies for volume mount support, adds Telepresence to the system PATH, and optionally installs the TelepresenceDaemon service. This eliminates the need for elevated privileges when using Telepresence. Currently available for amd64 architecture only due to dependency constraints. Add route-controller DaemonSet to prevent routing loops on local clusters A new optional route-controller DaemonSet can be deployed alongside the traffic-manager on local Kubernetes clusters (Kind, minikube, k3d, Docker Desktop) to prevent routing loops caused by deleted or non-existent service ClusterIPs. It installs an iptables FORWARD chain DROP rule for the service CIDR on every node, and adds per-IP kernel blackhole routes when a Service is deleted. Enable it with routeController.enabled=true in the Helm chart. Automatic cache cleanup on version change Telepresence now tracks its version in a version.json file in the cache directory. When the CLI detects that the major.minor version differs from the running binary, it automatically quits running daemons and clears stale cache entries (preserving logs). This prevents issues caused by leftover cache files from a previous version. Patch and pre-release version changes do not trigger a cache cleanup. Cluster DNS not injected into containers started by telepresence compose When using telepresence compose up, cluster hostnames did not resolve inside the compose container because the daemon DNS IP and the tel2-search DNS search domain were not being added to the generated compose spec. DNS and dns_search are now correctly set for all engaged compose services. ## Version 2.26.2 (February 14) Dial 127.0.0.1 instead of 0.0.0.0 when connecting to local daemons When a VPN routes all private network addresses, dialing 0.0.0.0 gets routed through the VPN instead of reaching the locally running daemon. This causes Telepresence to report that no daemon is running even though the processes are active and listening. The client now dials 127.0.0.1 explicitly, and the Docker-published daemon port is bound to 127.0.0.1 to match. Fix HTTP intercepts via telepresence compose failing when httpFilters or httpPaths are set The compose extension set HeaderFilters and PathFilters on the intercept spec but left the mechanism as "tcp". This caused the traffic manager to reject the intercept with "global TCP/UDP intercepts are disabled". The mechanism is now correctly switched to "http" when any HTTP filters are specified, matching the behavior of the CLI intercept command. Fix "root daemon is embedded" error on Windows elevated terminals When running Telepresence in an elevated (administrator) terminal on Windows, commands like connect and loglevel failed with "root daemon is embedded". The user daemon now correctly delegates to the in-process root daemon session instead of returning an error. Fix h2c (HTTP/2 cleartext) prior knowledge not working on transport Since Go 1.24, having both HTTP1 and UnencryptedHTTP2 enabled on an HTTP transport causes Go to default to HTTP/1.1 instead of using h2c prior knowledge. The transport now explicitly disables HTTP1 when UnencryptedHTTP2 is enabled, fixing h2c communication for both the default forwarding handler and intercepted traffic. ## Version 2.26.1 (January 26) Add support for "warning" as an alias for "warn" in log levels The "warning" alias for "warn" was the only acceptable value in the Helm chart, yet the traffic manager didn't accept it. This is now fixed so that both names are accepted by the Helm chart and by the traffic manager. ## Version 2.26.0 (January 23) Add ability for cluster admins to revoke other users' intercepts. The traffic-manager can now execute commands defined in the traffic-manager ConfigMap. As a result, clients that are authorized to update this ConfigMap can issue those commands. This mechanism lays the groundwork for distinguishing administrators from regular users in Telepresence. Administrators can be granted RBAC permissions that allow them to update the traffic-manager ConfigMap, while regular users cannot. The new command, `telepresence revoke `, uses this mechanism to revoke the intercept associated with the specified ID. Add support for overriding intercepts owned by inactive clients Introduce the Helm chart setting `intercept.inactiveBlockTimeout` that controls the maximum amount of time an intercept may be held by a client that is unreachable or inactive. Once this timeout is exceeded, the intercept no longer blocks conflicting intercepts and may be automatically removed when another client attempts to create a conflicting intercept. Add support for sudo-rs Telepresence now supports running the root daemon using `sudo-rs`. This is necessary on Ubuntu where `sudo-rs` has become the default for `sudo`. Add configuration to disable global TCP/UDP intercepts A new Helm chart configuration option `intercept.allowGlobalIntercepts` has been added to control whether global TCP/UDP intercepts are permitted. When set to `false`, only HTTP intercepts with header or path filters are allowed, preventing users from creating global intercepts that block other developers from intercepting the same port. This is particularly useful in shared development environments where multiple developers need to work on the same service simultaneously. The setting defaults to `true` to maintain full backward compatibility with existing deployments. When a user attempts to create a global intercept while the setting is disabled, they receive a helpful error message suggesting the use of `--http-header` or `--http-path-*` flags for HTTP-filtered intercepts. Enhanced Traffic Manager Startup Reliability The Traffic Manager deployment now includes a `startupProbe` that accurately detects when the service is fully initialized and ready to handle traffic. This enhancement brings the following benefits: - **Prevents Premature Traffic Routing**: The manager only reports itself as ready after all configurations are loaded, eliminating potential race conditions - **Smoother Upgrades and Rollouts**: Deployment orchestration tools can reliably determine when the Traffic Manager is operational, improving overall installation stability This change is particularly beneficial in large clusters or complex networking environments where initialization may take longer than expected. Support customizable daemon config file The config file for Telepresence is now configurable through the command-line flag `--config`. The `--config ` flag of the `telepresence genyaml` command was renamed to `--agent` to avoid confusion. Support customizable daemon log file paths Log file paths for the Telepresence daemons are now configurable through the command-line flag `--logfile` that denotes a custom log file location or redirect of the log output to stdout/stderr. Two new log-level configuration entries for `cli` and `kubeAuthDaemon` are also introduced, expanding the existing log-level controls beyond just `userDaemon` and `rootDaemon`. Add ability to exclude or include modifications made by other injectors when injecting the traffic agent. The Traffic Agent now has a new configuration option `agentInjector.mutationAware` that can be set to `false` to exclude modifications made by other injectors when injecting the traffic agent. Setting `agentInjector.mutationAware=true` requires `agentInjector.webhook.reinvocationPolicy=IfNeeded`. The default setting is `true`. Add ability to disable the Traffic Agent's HTTP2/Clear-Text probing. The Traffic Agent now has a new configuration option `agent.enableH2cProbing` that can be set to `false` to disable the HTTP2/Clear-Text probing. The default setting is `true` to preserve backwards compatibility, but this will be changed to `false` in a future release. Add ability to configure the Traffic Agent's retry interval for watching intercepts. The Traffic Agent's retry interval when it establishes its watcher for intercepts is now configurable using the Helm chart value `agent.watchRetryInterval`. The default retry interval was also increased from 2 seconds to 10 seconds to improve resilience when connections to the traffic manager are lost. Make traffic-agent consumption metrics reporting optional. Metrics reporting for the traffic-agent can now be optionally enabled or disabled via the `agent.enableConsumptionMetrics` Helm chart value. The traffic-agent metrics will also be completely disabled when the Helm chart value `prometheus.port` is unset or zero. This change is intended to reduce the amount of unnecessary traffic generated by the traffic-agent when consumption metrics reporting is of no interest to the user. Improved efficiency of traffic manager map updates. The watchable map has been refactored into a client/server model that supports delta-based updates. Where supported, gRPC now transmits only incremental changes instead of full snapshots, significantly reducing payload sizes. This improvement is especially important for large clusters, where full snapshots can be sizable and costly to transmit. Full snapshot streaming remains available as a backward-compatible fallback for clients that do not support delta methods. Use TCP/IP instead of Unix sockets for all communication between local processes. Telepresence now uses TCP/IP sockets for all communication between local processes. This change reduces the risk of socket-related errors caused by lingering sockets from previous Telepresence sessions. It also eliminates the difficulties of using Unix sockets for communication between a system service and user processes on Windows. Don't allow connect with --docker when client is configured with intercept.useFtp=true The `--docker` flag is not allowed when the client is configured with `intercept.useFtp=true` and an error is now generated instantly by the `telepresence connect --docker` command. The docker volume plugin cannot use FTP because it requires two ports: a fixed control port that Telepresence can proxy, and a dynamic data port (randomly chosen during connection) that Telepresence cannot proxy on-demand. The port-forwarder only forwards pre-configured ports and doesn't understand FTP's protocol. Consequently, FTP isn't allowed in this scenario. If it was, then when an FTP server tells the client to use an unpredictable second port for file transfers, Telepresence would block it—causing the connection to fail every time. Better names for the Telepresence Daemons Using the name xxx-foreground isn't very intuitive when talking about daemon processes. Yes, the command, when issued in a terminal, will start the daemon in foreground so the names of the commands does have some logic to them, but then again, starting in the foreground is the default behavior of any command. And when the same command is started from the CLI, it will be started in the background, despite its name. The daemons are therefore now renamed: - connector-foreground => userd - daemon-foreground => rootd - kubeauth-foreground => kubeauthd This also affects the Telepresence hidden CLI commands `telepresence daemon-foreground` and `telepresence connector-foreground`. Add retry logic for tunnel connection attempts The tunnel dialer now uses a backoff-based retry mechanism when establishing connections. This ensures that dialing attempts for both TCP and UDP protocols persist if the target IP is not immediately ready to receive requests. Retry mechanism for client tunnel creation The traffic agent now includes a backoff-based retry mechanism when establishing tunnel streams to a client. This prevents "no dial watcher" connection failures caused by a race condition where the tunnel request arrives before the client has fully initialized its communication channel. Fix "close of closed channel" panic in the root daemon process. The root daemon process would sometimes panic with "close of closed channel" due to a race condition in the DNS cache logic. This issue has been fixed. ## Version 2.25.2 (December 26) Ensure that the exit code from a docker command becomes the exit code of the Telepresence command. When running a Docker command using `telepresence docker-run` or `telepresence curl`, the exit code would be 1 for all non-zero exit codes from the Docker command. This has been fixed so that the exit code from the Docker command becomes the exit code of the Telepresence command. Fix a bug causing truncation of command text when generating external command help. The Telepresence CLI would truncate the command text when generating help for external commands such as `docker compose` that had text spanning more than one line. This has been fixed so that the full command text is displayed. Fix schema for agent.image.pullSecrets The `agent.image.pullSecrets` is referenced by the helm chart's deployment.yaml but was previously disallowed by the schema file. ## Version 2.25.1 (November 10) Volumes did not mount correctly when using `telepresence connect --docker` when Docker had IPv6 enabled. Telepresence failed to mount volumes after connecting with `telepresence connect --docker` when Docker Engine had IPv6 enabled in its default bridge network. Disabling IPv6 in the Telepresence client configuration did not resolve the issue. This was fixed in Telepresence Volume Plugin "telemount" version 0.3.2, which circumvented a [bug in sshfs](https://github.com/libfuse/sshfs/issues/335). Additionally, the volume plugin will no longer use IPv6 when the client configuration `docker.enableIPv6` is set to `false`. Remove unnecessary setcap from traffic binary The setcap capability (cap_net_bind_service) was removed from the traffic binary build process. This capability was originally added to allow the binary to bind to privileged ports, specifically port 443 for the mutating webhook. Since version 2.24.0, the default mutating webhook port was changed to 8443 (a non-privileged port), making this capability unnecessary. Removing it simplifies the build process and reduces the security surface area. ## Version 2.25.0 (October 16) HTTP Intercepts with HTTP header and path filtering Telepresence now supports HTTP Intercepts, enabling fine-grained HTTP traffic filtering for intercepts. Users can intercept only specific HTTP requests based on headers and URL paths using the new `--http-header`, `--http-path-prefix`, `--http-path-equal`, and `--http-path-regex` flags. This allows multiple developers to work on the same service simultaneously by intercepting only their specific traffic patterns, rather than intercepting all traffic to a service. **Routing Precedence Model**: Header-based intercepts take priority over path-only intercepts. When multiple intercepts are active on the same workload, requests are evaluated against header-based filters first, then path-only filters. This enables different developers to use header-based personal intercepts (e.g., `x-user=alice`) while others use path-based intercepts (e.g., `/admin/*`) without conflicts. **Conflict Detection**: Intercepts conflict only when their filters would route the same traffic to different destinations. Key rules: - Different header values (e.g., `X-User=adam` vs `X-User=bertil`) do NOT conflict - Header filters use subset logic: `X-User=adam` conflicts with `X-User=adam, X-Session=123` (first is subset) - Same headers with different paths do NOT conflict: `X-User=adam + /api/*` vs `X-User=adam + /admin/*` - Path-only intercepts operate at a lower priority tier than header-based intercepts The feature maintains full backward compatibility with existing TCP intercepts. TLS/mTLS Intercept Support Support was added for HTTP-filtered intercepts on applications using TLS/mTLS encryption. The new functionality enables Telepresence to decrypt and inspect encrypted traffic by accessing TLS certificates, facilitating debugging and testing of secure applications. Certificates can be accessed via mounted volumes or Kubernetes secrets in the same namespace. New annotations (`telepresence.io/downstream-cert-path` and `telepresence.io/downstream-cert-secret`) allow configuration of certificate paths or secrets for decrypting traffic on specified ports. For mTLS, the `telepresence.io/upstream-cert-` annotation prefix supports re-encryption of upstream traffic using client-side certificates. For self-signed certificates, the `telepresence.io/upstream-insecure-skip-verify` annotation bypasses verification, enabling HTTP-filtered intercepts in development environments. The `--plaintext` option allows unencrypted traffic during intercepts or wiretaps. Add MCP server to Telepresence CLI The Telepresence CLI now includes a lightweight MCP server that can be used to allow local AI agents to execute some CLI commands, such as connecting to a traffic manager, listing interceptable apps, and creating an intercept. The server can be enabled using the new `telepresence mcp claude enable` or `telepresence mcp vscode enable` commands. Enhance Resilience of Engagements During Traffic-Manager Redeploys The telepresence client and traffic-agent now automatically reconnect to the traffic-manager after a restart. Upon reconnection, they share their current state, ensuring ongoing engagements remain uninterrupted. This improvement minimizes user impact during traffic-manager upgrades. Add support for IPv6 and dual-stack when using `telepresence connect --docker` Telepresence now supports using `telepresence connect --docker` together with Kubernetes single-stack IPv4 networking, single-stack IPv6 networking, or dual-stack networking with both network families. Both are enabled by default, but can be disabled by setting the `client.docker.enableIPv4` or `client.docker.enableIPv6` to false in the Helm chart, or by using the corresponding settings `docker.enableIPv4` or `docker.enableIPv6` the client configuration file. The new dual-stack support requires the teleroute network plugin 0.4.0 or later. The client will install this version automatically unless you work in an air-gapped environment. RESTful API Service Reintroduced with HTTP Filtering Support The Telepresence RESTful API service has been restored with enhanced support for HTTP header and path filtering. This service enables workloads to programmatically query whether they should handle requests based on active intercepts. Added `--metadata` flag allows attaching custom metadata to intercepts that can be retrieved through the API endpoints. The API server is now accessible via `TELEPRESENCE_API_HOST` and `TELEPRESENCE_API_PORT` environment variables in both cluster pods and local intercept handlers. More efficient DNS handling in the traffic-manager Telepresence will no longer send DNS queries for A and AAAA records to the traffic-manager. Instead, it will send a single query for the name and then derive the record type from the type of IP address (IPv4 or IPv6) in the response. This reduces the number of DNS queries sent to the cluster's DNS server and makes the behavior more consistent with the `net.LookupNetIP` function in the Go standard library, which the traffic-manager ultimately uses. The lookups will now be performed exclusively by the traffic-manager, never by the traffic-agent. This means that traffic-agents with special DNS configurations might stop working. If this is a problem, the old behavior can be restored by setting the `client.dns.useComplexLookup` parameter in the Helm chart or the `dns.useComplexLookup` parameter in the client configuration file. Updated Helm chart to include keywords and the source repository URL. Improves the Helm chart's discoverability on platforms like Artifact Hub and automatically adds a direct link to the source code for users, providing better context. Telepresence client now requires a traffic manager version of at least 2.21.0. The traffic manager is now required to be at least version 2.21.0. Versions earlier than 2.21.0 will no longer work. The reason for this is that implementing the new reconnect behavior would require too much conditional code with older traffic-managers, and a lot of functionality wouldn't work anyway. Build binaries and docker images that are stripped from dwarf and debug info. The Telepresence binaries and docker images are now built with the `-w -s` flags, which strip the debug symbols and the DWARF information. This reduces the size of the binaries and docker images by about 50MB. Debug binaries can be built using `DEBUG=1 make build`. ## Version 2.24.1 (September 5) Fix invalid filename generated by telepresence gather-logs command The `telepresence gather-logs` command would generate a filename that was invalid on Windows unless the user specified the filename explicitly using the `--output-file` flag. This was fixed using a more condensed format for the timestamp in the filename. A `telepresence connect --docker` fails kubeconfig points to a port-forwarded localhost Telepresence would fail to connect to a cluster from within a container if the kubeconfig pointed to a port-forwarded localhost because that localhost is not reachable from within the container. This situation is now detected so that the address used from within the container has "localhost" replaced with "host.docker.internal", or an alternative alias configured by the user using the new `docker.hostGateway` parameter in the client configuration. A `telepresence connect --docker` would fail with some k3s configurations Telepresence would fail to connect to a k3s cluster unless the cluster's IP was a loopback address. This is now changed so that any IP:port combination is accepted as long as a container can be found that defines a mapping for it. Restore default value for agent-state.yaml in the traffic-manager configmap The value was previously an empty string which caused problems when when Argo CD tried to synchronize it. ## Version 2.24.0 (August 25) Support for Docker Compose Telepresence now supports integration with Docker Compose. It connects to and interacts with cluster resources by utilizing `x-tele` extensions within a Docker Compose specification. These extensions configure your local services to effectively act as handlers for Telepresence connections, providing them with the necessary access to the traffic, volumes, and environment of the engaged container. Serve up a web-page with telepresence serve. A new `telepresence serve ` command was added that starts a web browser on the specified service. The command is especially useful when used in combination with `telepresence connect --docker` because it will then expose the given service on a random port on localhost. Add ability to optionally clean up sidecars that have been idle above a specified duration Added configuration parameter `agent.maxIdleTime` to the Helm Chart, to control how long a sidecar can be idle before it is cleaned up. The updating of latestEngagementTime is done every 1m in the Remain Call, which is now called every 1min, compared to every 5s in the past. The agent state (containing latestEngagementTime) is updated in memory, and lazily persisted to the traffic-manager configmap every 2min. Removal of the sidecar is also done in the Remain Call, if the sidecar has been idle for longer than the configured `agent.maxIdleTime`. Add option to drop client label in prometheus metrics for GDPR compliance The Helm Chart now has a `prometheus.dropClientLabel` option that can be set to true to drop the client label from the prometheus metrics. This is useful for GDPR compliance, as the client label contains personal data, which can be potentially problematic, i.e allowing the ability to track the working times of an individual. Prefix metrics with "telepresence_" Avoids metric conflicts and makes these more explicit to improve search in observability stacks. CLI documentation in markdown format The Telepresence CLI is now capable of generating its own documentation in markdown format using the new `telepresence man-pages` command. The generated documentation is included under the heading "Telepresence CLI" in the the Telepresence reference documentation. Service Port Rerouting The telepresence connect command introduces a new `--reroute-remote ::[/{tcp|udp}]` flag, allowing users to remap service ports. This feature redirects requests sent to `:` to `:` within the Telepresence VIF. The flag can be repeated. Local Port Rerouting The telepresence connect command introduces a new `--reroute-local ::[/{tcp|udp}]` flag, allowing users to redirect requests sent to ports on localhost to arbitrary service ports. This feature enables requests sent to `localhost:` to be redirected to ``. The flag can be repeated. Add information about using Kubernetes auth plugins when using Telepresence CLI in a container Kubernetes, and hence the Telepresence CLI, must have access to auth plugins declared in the kubeconfig. A section was added to the documentation explaining how to achieve this when using the Telepresence CLI in a container. Add log directory to the output of `telepresence config view` The `telepresence config view` command now includes the path to the directory where the Telepresence logs are stored. The default port for the mutating webhook is now 8443. It used to be 443 Port numbers below 1000 are reserved for privileged processes and are often restricted by firewalls. Consequently, the default port for the mutating webhook was changed from 443 to 8443. You can override this default port using the agentInjector.webhook.port value in the Helm Chart. This change is particularly significant for clusters using Telepresence, where firewall rules limit the admission webhook's access to worker nodes, such as in an Amazon EKS cluster. ## Version 2.23.6 (July 23) Public DNS names aren't resolved from local docker application started by Telepresence A container running using `telepresence docker-run` or `telepresence --docker-run` was not able to resolve public DNS names such as "google.com". The problem is caused by an undocumented behavior in Docker's internal DNS-resolver when the `--dns` flag is used in conjunction with the teleroute network, where the DNS-server no longer finds public names even thought another bridge network is connected. The problem was solved by using a fallback resolver in the Telepresence DNS resolver. ## Version 2.23.5 (July 20) Let docker.Start pass on --interactive to docker start. An `-i` or `--interactive` flag given when the user runs a container with telepresence must be propagated to `docker start` to attach `stdin`. ## Version 2.23.4 (July 18) Never truncate meaningful output from a command The new human-friendly output using a progress reporter would sometimes truncate error output. This is no longer the case. Instead, all output will be wrapped. Typo in client mount-policy "RemoteReadonly" should be "RemoteReadOnly" The Helm Chart correctly expects the remote read-only mount policy to be `RemoteReadOnly`, but the client expected it to be `RemoteReadonly` (without a leading capital letter in the word "only"). DNS server does not respect semicolons as comments in resolv.conf files Telepresence does not work correctly if `/etc/resolv.conf` contains semicolons, which are valid comments as of [linux manpage](https://man7.org/linux/man-pages/man5/resolv.conf.5.html). Use NXDOMAIN instead SERVFAIL for DNS recursion errors (timeouts) A DNS for a single label name that fails in a minikube - or another type of local cluster - will sometimes result in a recursive lookup on the host. Without any type of recursion detection, this lookup will timeout waiting for itself. Previously, this resulted in a `SERVFAIL` from the cluster DNS, which triggered renewed lookup attempts that never stopped. This is now changed so that the same type of timeouts instead results in an `NXDOMAIN` error that doesn't trigger renewed attempts. Also, the recursion check now handles that the cluster's DNS adds suffixes from its search-path. ## Version 2.23.3 (July 7) Fix tunnel channel reuse in traffic-agent to prevent connection failures Previously, when a tunnel became active, the map maintained by the traffic-agent containing channel values for agent-to-client tunnels wasn't cleared. This resulted in closed channels remaining in the map. When port numbers were eventually reused, these stale entries would be discovered and their closed channels would cause immediate stream termination, leading to data loss. The -p flags would have no effect in combination with --docker-run When using `telepresence --docker-run` with a `-p ` flag, the Docker driver silently ignored the port specification. This occurred because the `--network=` flag disabled both additional network directives and the default bridge network (which is normally used when no network is specified). This has been resolved by: 1. Adding the teleroute network after container creation instead of using a flag 2. Replacing the single `docker run` command with a sequence of: - `docker create` - Network addition - `docker start` Requests lost when using wiretap Wiretap connection `Close` and `Write` could sometimes be out of sync, so that the `Close` would be executed before the `Write`, causing a "read/write on closed pipe" error and loss of data. ## Version 2.23.2 (June 27) Adding an alsoProxy subnet with 32-bit mask no longer works on macOS Routing improvements introduced in 2.23.0 surfaced a problem when using special handling of submets with 32-bit masks. The special handling now causes problems on macOS. The logic is now conditioned to only run under linux since it's no longer needed on other operating systems. The gather-logs command produces no cluster-side logs when connected with --docker The `telepresence gather-logs` command did not include cluster-side logs when connected using `--docker`, because it tried to store such logs in a temporary directory created by the CLI. The directory was not mounted in the daemon container. This is now changed so that the temporary directory is created under the users cache directory, which is guaranteed to be mounted on the container. Docker volume mounts failing when connected using both --docker --proxy-via flags The volume mounter would fail when doing a `telepresence connect --docker --proxy-via all=` followed by an intercept using `--docker-run`, because the bridge that the daemon created would try to access the intercepted pod using its proxied IP. Now, the bridge will instead use the pod's real IP. ## Version 2.23.1 (June 24) New telepresence helm version command. The new `telepresence helm version` command prints the version of the helm client that is embedded in the telepresence binary. Engagement disconnects after certain amount of time The configuration parameter `connectionTTL`, controlling how long a client could be completely idle before the traffic-manager or traffic-agent would consider it dead and disconnect (default 24 hours), had no effect. Instead, an engagement would disconnect after 2 hours (the default gRPC `keepAlive.Time` duration). The default of 24 hours is now reinstated. The Helm value `client.connectionTTL` was moved to `grpc.connectionTTL` because it is a server configuration. The old value will still work, but it is deprecated and will be removed eventually. Telepresence breaks if config.yml exists but is empty Telepresence would refuse to connect with a misleading error if the `config.yml` file containing the client's configuration parameters existed but was empty. ## Version 2.23.0 (June 17) New telepresence wiretap command The new `telepresence wiretap` command introduces a read-only form of an `intercept` where the original container will run unaffected while a copy of the wiretapped traffic is sent to the client. Similar to an `ingest`, a `wiretap` will always enforce read-only status on all volume mounts, and since that makes the `wiretap` completely read-only, there's no limit to how many simultaneous wiretaps that can be served. In fact, a `wiretap` and an `intercept` on the same port can run simultaneously. Add Telepresence Docker Network Plugin "Teleroute" The new Teleroute plugin makes it possible for containers to use the Telepresence daemon's VIF without having to change their network mode, i.e. a `--network container:` is no longer needed. Instead, a container can use a custom network created when the Telepresence daemon connects to the cluster. This network uses the new driver "teleroute" which is provided by Telepresence. With the Teleroute Docker network plugin in place, there's no longer a need for special handling of network related docker flags, and the following changes have been made: 1. The Teleroute Docker network driver will be installed unless it is already present. 2. A Teleroute network will be created when starting the Telepresence daemon as a container. This network will then communicate with that container and expose the same CIDRs as the daemon's VIF. 3. A container started with `telepresence curl`, or `telepresence {ingest|intercept|replace|wiretap} --docker-{run|build|debug}` will no longer change its network mode using `--network container:`, instead it will use `--network `. 4. As a consequence of #3, published ports and other networks that are added no longer need special handling using socat containers, so all of that has been removed. Control whether the initContainer injection is enabled/disabled The initContainer injection can be optionally disabled by setting the `agent.initContainer.enabled` parameter to false in the `values.yaml` file of the Helm chart. This feature was added to improve compatibility with systems like OpenShift where the initContainer injection cannot be used due to inability to give initContainer NET_ADMIN permissions. Human friendly progress reporting Telepresence now uses a progress reporter that is very similar to the one used by Docker compose. The implementation is a variation of that reporter's source code, so big thanks to the Docker compose CLI authors for making it available as OSS. A new global `--progress ` flag was added. It defaults to "auto" which means that the style is chosen depending on whether the command runs from a tty type terminal. Other possible values are "plain", "quiet", and "json". `--progress quiet` is implied when formatted output is chosen using `--output json|yaml`. Add the ability to use a name for the target host, and defer its resolution Knowing the IP of the local service that acts as the handler service for an intercept, replace, or wiretap is not possible until that service has been started, and telepresence will therefore now accept a name for the `--address` flag. The name is not resolved by the daemon until a request is made to the engaged container on a port that is routed to the local service. Add intercept.mountsRoot to the client configuration The new `intercept.mountsRoot` can be set to a directory that will be used as the root for all automatically generated mount directories. The default is to use the platforms temp directory. The setting is not used on windows, where the mounts use drive letters. Add docker.addHostGateway to the client configuration. When `docker.addHostGateway` is set to `true`, the `docker run` that starts the containerized Telepresence daemon will include the flag `--add-host host.docker.internal:host-gateway`. The flag is set to `true` by default on linux platforms and `false` on other platforms. Client configuration to override the Helm download URL The default download URL `oci://ghcr.io/telepresenceio/telepresence-oss` used when installing Helm charts with versions that differ from the version of the embedded Helm chart can now be overridden using the client config value `helm.chartURL`. Dropped support for Telepresence legacy flags The `telepresence` CLI command will no longer support legacy flags such as: - `--swap-deployment` - `--new-deployment` - `--docker-mount` - `--method` A "Legacy Telepresence command used" warning has been printed for several years now, and the mapping for the `--swap-deployment` was the `intercept` command, which is very confusing today since we now have the `replace` command. Let containerized daemon consistently use the same port for gRPC The port used for the containerized gRPC was randomly selected using the hosts network namespace. This is now changed so that the port used by the container is preset and configurable and then mapped to a random port on the host. The port number can be configured using `grpc.daemonPort` and defaults to `4038`. Telepresence fails to start the root daemon on Windows unless current user is the administrator The telepresence CLI starts a user daemon and a root daemon. The latter is started using administrator privileges. On a Windows box, this means that the root daemon runs using a different user account (typically "Administrator") unless the current user can run processes with elevated privileges. The socket used for communication with the root daemon was assumed to reside in `%USERPROFILE%\AppData\Local\telepresence` and was therefore not found by the CLI and the user daemon. The location will henceforth always be based on the `%USERDATA` of the CLI user. Telepresence DNS Fallback stripping CNAME information from DNS Records. The fallback DNS server used on Linux systems without a systemd.resolved configuration, would assume that suffixes belonging to the `search` defined in the `/etc/resolved` had been added by the caller. Since this search path was assumed to be intended for the local machine only, the suffix was stripped off prior to sending the name to the cluster for resolution. This made queries fail that relied on the qualified name to resolve CNAME records. The logic stripping the suffix was therefore removed. ## Version 2.22.6 (June 3) Regression causing "unexpected slice size" with older traffic-managers. Older traffic-managers have a different way of reporting the service-subnet. The new way, using a list of subnets reused a proto slice in the GRPC message that was expected to be empty, but older traffic-managers will pass the IP of the kube-dns here. It cannot be parsed as a list of subnets. A check that remedies this mismatch was inserted. ## Version 2.22.5 (May 29) Unable to correctly determine service CIDR with Kubernetes >= 1.33 Starting with Kubernetes 1.33, the strategy of extracting the cluster's service CIDR from an error message no longer works because the error message has changed. The root cause for this is that Kubernetes introduced the ability to use [Multiple Service CIDRs](https://gist.github.com/aojea/c20eb117bf1c1214f8bba26c495be9c7). Since `ServiceCIDR` is now a resource, it can be easily retrieved (and modified) using standard Kubernetes client API calls, and this is what the traffic manager will use going forward. The fix required an addition to the traffic-manager's RBAC, granting it sufficient permissions to list `networking.k8s.io/servicecidrs`. A future enhancement will allow the traffic manager to watch for service CIDR changes. Helm chart schema type for nodeSelector was incorrect The Helm chart schema for the `nodeSelector` value was incorrect. Kubernetes defines different types for nodeSelector (inside PodSpec objects) and NodeSelector (inside NodeAffinity, VolumeNodeAffinity and a bunch of other places). The schema was changed to use the correct type. The name `nodeSelector` is still used in the Helm chart so this change is backwards compatible. Pods with container ports named the same caused intercept to fail Intercept container ports now have numbers appended to them if there are multiple ports from multiple containers with the same name. This bugfix works around an issue where Kubernetes allows multiple port definitions in a pod spec to have the same name. Don't include k8s-defs.json to chart package The k8s-defs.json was unnecessarily included to the Helm chart package and this increased the Helm release secret size so much that it could prevent installation of the Helm chart depending on k8s settings. To fix this k8s-defs.json is not included to the Helm chart anymore. ## Version 2.22.4 (April 26) Don't require internet access when installing the traffic-manager using Helm A regression occurred with the introduction of Helm validation in version 2.22.0. The schema relied on external HTTPS links, which inadvertently created a requirement for internet accessibility. To resolve this, we have embedded these resources within the schema, thus removing the need for an internet connection. Client failed connect with "failed to exit idle mode" in the connector.log after being idle The port-forward connections used for connecting the daemon to the traffic-agents were using an incorrect context, causing them to fail after being idle for some time. Fix deadlock in Telepresence daemon A deadlock would sometimes occur in the Telepresence daemon that prevented it from doing a clean exit during `telepresence quit`. Don't log error message when a pod watcher ends due to cancellation Errors printed in the daemon log during normal cancellation of the WatchAgentPods goroutine are now removed. ## Version 2.22.3 (April 8) The Windows install script will now install Telepresence to "%ProgramFiles%\telepresence" Telepresence is now installed into "%ProgramFiles%\telepresence" instead of "C:\telepresence". The directory and the Path entry for `C:\telepresence` are not longer used and should be removed. The Windows install script didn't handle upgrades properly The following changes were made: - The script now requires administrator privileges - The Path environment is only updated when there's a need for it The Telepresence Helm chart could not be used as a dependency in another chart. The JSON schema validation implemented in Telepresence 2.22.0 had a defect: it rejected the `global` object. This object, a Helm-managed construct, facilitates the propagation of arbitrary configurations from a parent chart to its dependencies. Consequently, charts intended for dependency use must permit the presence of the `global` object. Recreating namespaces was not possible when using a dynamically namespaced Traffic Manager A shared informer was sometimes reused when namespaces were removed and then later added again, leading to errors like "handler ... was not added to shared informer because it has stopped already". Single label name DNS lookups didn't work unless at least one traffic-agent was installed A problem with incorrect handling of single label names in the traffic-manager's DNS resolver was fixed. The problem would cause lookups like `curl echo` to fail, even though telepresence was connected to a namespace containing an "echo" service, unless at least one of the workloads in the connected namespace had a traffic-agent. ## Version 2.22.2 (March 28) Panic when using telepresence replace in a IPv6-only cluster A "slice bounds out of range" would occur when the targeted Pod's Traffic Agent requested a local dialer to be created on the client. This was due to a glitch in the VPN-tunnel implementation that got triggered when a remote IPv6-address was combined with a local IPv4-address. ## Version 2.22.1 (March 27) Only restore inactive traffic-agent after a replace. A regression in the 2.20.0 release would cause the traffic-agent to be replaced with a dormant version that didn't touch any ports when an intercept ended. This terminated other ongoing intercepts on the same pod. This is now changed so that the traffic-agent remains unaffected for this use-case. ## Version 2.22.0 (March 14) New telepresence replace command. The new `telepresence replace` command simplifies and clarifies container replacement. Previously, the `--replace` flag within the `telepresence intercept` command was used to replace containers. However, this approach introduced inconsistencies and limitations: * **Confusion:** Using a flag to modify the core function of a command designed for traffic interception led to ambiguity. * **Inaccurate Behavior:** Replacement was not possible when no incoming traffic was intercepted, as the command's design focused on traffic routing. To address these issues, the `--replace` flag within `telepresence intercept` has been deprecated. The new `telepresence replace` command provides a dedicated and consistent method for replacing containers, enhancing clarity and reliability. Key differences between `replace` and `intercept`: 1. **Scope:** The `replace` command targets and affects an entire container, impacting all its traffic, while an `intercept` targets specific services and/or service/container ports. 2. **Port Declarations:** Remote ports specified using the `--port` flag are container ports. 3. **No Default Port:** A `replace` can occur without intercepting any ports. 4. **Container State:** During a `replace`, the original container is no longer active within the cluster. The deprecated `--replace` flag still works, but is hidden from the `telepresence intercept` command help, and will print a deprecation warning when used. Add json-schema for the Telepresence Helm Chart Helm can validate a chart using a json-schema using the command `helm lint`, and this schema can be part of the actual Helm chart. The telepresence-oss Helm chart now includes such a schema, and a new `telepresence helm lint` command was added so that linting can be performed using the embedded chart. No dormant container present during replace. Telepresence will no longer inject a dormant container during a `telepresence replace` operation. Instead, the Traffic Agent now directly serves as the replacement container, eliminating the need to forward traffic to the original application container. This simplification offers several advantages when using the `--replace` flag: - **Removal of the init-container:** The need for a separate init-container is no longer necessary. - **Elimination of port renames:** Port renames within the intercepted pod are no longer required. One single invocation of the Telepresence intercept command can now intercept multiple ports. It is now possible to intercept multiple ports with one single invocation of `telepresence intercept` by just repeating the `--port` flag. Unify how Traffic Manager selects namespaces The definition of what namespaces that a Traffic Manager would manage use was scattered into several Helm chart values, such as `manager.Rbac.namespaces`, `client.Rbac.namespaces`, and `agentInjector.webhook.namespaceSelector`. The definition is now unified to the mutual exclusive top-level Helm chart values `namespaces` and `namespaceSelector`. The `namespaces` value is just for convenience and a short form of expressing: ```yaml namespaceSelector: matchExpressions: - key: kubernetes.io/metadata.name operator: in values: . ``` Improved control over how remote volumes are mounted using mount policies Mount policies, that affects how the telepresence traffic-agent shares the pod's volumes, and also how the client will mount them, can now be provided using the Helm chart value `agent.mountPolicies` or as JSON object in the workload annotation `telepresence.io/mount-policies`. A mount policy is applied to a volume or to all paths matching a path-prefix (distinguished by checking if first character is a '/'), and can be one of `Ignore`, `Local`, `Remote`, or `RemoteReadOnly`. List output includes workload kind. The output of the `telepresence list` command will now include the workload kind (deployment, replicaset, statefulset, or rollout) in all entries. Add ability to override the default securityContext for the Telepresence init-container Users can now use the Helm value `agent.initSecurityContext` to override the default securityContext for the Telepresence init-container. Let download page use direct links to GitHub The download links on the release page now points directly to the assets on the download page, instead of using being routed from getambassador.io/download/tel2oss/releases. Use telepresence.io as annotation prefix instead of telepresence.getambassador.io The workload and pod annotations used by Telepresence will now use the prefix `telepresence.io` instead of `telepresence.getambassador.io`. The new prefix is consistent with the prefix used by labels, and it also matches the host name of the documentation site. Annotations using the old name will still work, but warnings will be logged when they are encountered. Make the DNS recursion check configurable and turn it off by default. Very few systems experience a DNS recursion lookup problem. It can only occur when the cluster runs locally and the cluster's DNS is configured to somehow use DNS server that is started by Telepresence. The check is therefore now configurable through the client setting `dns.recursionCheck`, and it is `false` by default. Trigger the mutating webhook with Kubernetes eviction objects instead of patching workloads. Telepresence will now attempt to evict pods in order to trigger the traffic-agent's injection or removal, and revert to patching workloads if evictions are prevented by the pod's disruption budget. This causes a slight change in the traffic-manager RBAC, as the traffic-manager must be able to create "pod/eviction" objects. The telepresence-agents configmap is no longer used. The traffic-agent configuration was moved into a pod-annotation. This avoids sync problems between the telepresence-agents (which is no no longer present) and the pods. Drop deprecated current-cluster-id command. The clusterID was deprecated some time ago, and replaced by the ID of the namespace where the traffic-manager is installed. Make telepresence connect --docker work with Rancher Desktop Rancher Desktop will start a K3s control-plane and typically expose the Kubernetes API server at `127.0.0.1:6443`. Telepresence can connect to this cluster when running on the host, but the address is not available when connecting in docker mode. The problem is solved by ensuring that the Kubernetes API server address used when doing a `telepresence connect --docker` is swapped from 127.0.0.1 to the internal address of the control-plane node. This works because that address is available to other docker containers, and the Kubernetes API server is configured with a certificate that accepts it. Rename charts/telepresence to charts/telepresence-oss. The Helm chart name "telepresence-oss" was inconsistent with its contained folder "telepresence". As a result, attempts to install the chart using an argo ApplicationSet failed. The contained folder was renamed to match the chart name. Conflict detection between namespaced and cluster-wide install. The namespace conflict detection mechanism would only discover conflicts between two _namespaced_ Traffic Managers trying to manage the same namespace. This is now fixed so that all types conflicts are discovered. Don't dispatch DNS discovery queries to the cluster. macOS based systems will often PTR queries using nameslike `b._dns-sd._udp`, lb._dns-sd._udp`, or `db-dns-sd._udp`. Those queries are no longer dispatched to the cluster. Using the --namespace option with telepresence causes a deadlock. Using `telepresence list --namespace ` with a namespace different from the one that telepresence was connected to, would cause a deadlock, and then produce an empty list. Fix problem with exclude-suffix being hidden by DNS search path. In some situations, a name ending with an exclude-suffix like "xyz.com" would be expanded by a search path into "xyz.com.<connected namespace>" and therefore not be excluded. Instead, the name was sent to the cluster to be resolved, causing an unnecessary load on its DNS server. ## Version 2.21.3 (February 6) Using the --proxy-via flag would sometimes cause connection timeouts. Typically, a `telepresence connect --proxy-via =` would fail with a "deadline exceeded" message when several workloads were present in the namespace, the one targeted by the proxy-via didn't yet have an agent installed, and other workloads had an agent. This was due to a race condition in the logic for the agent-based port-forwards in the root daemon. The conditions causing this race are now eliminated. Fix panic in root daemon when using the "allow conflicting subnets" feature on macOS. A regression was introduced in version 2.21.0, causing a panic due to an unimplemented method in the TUN-device on macOS based clients. Ensure that annotation enabled traffic-agents are uninstall when uninstalling the traffic-manager. A traffic-agent injected because the workload had the inject annotation enabled would sometimes not get uninstalled when the traffic-manager was uninstalled. ## Version 2.21.2 (January 26) Fix panic when agentpf.client creates a Tunnel A race could occur where several requests where made to `agentpf.client.Tunnel` on a client that had errored when creating its port-forward to the agent. The implementation could handle one such requests but not several, resulting in a panic in situations where multiple simultaneous requests were made to the same client during a very short time period, Fix goroutine leak in dialer. The context passed to the `Tunnel` call that creates a stream for a dialer, was not cancelled when the dialer was finished, so the stream was never properly closed, leading to one dormant goroutine for each stream. ## Version 2.21.1 (December 17) Allow ingest of serverless deployments without specifying an inject-container-ports annotation The ability to intercept a workload without a service is built around the `telepresence.getambassador.io/inject-container-ports` annotation, and it was also required in order to ingest such a workload. This was counterintuitive and the requirement was removed. An ingest doesn't use a port. Upgrade module dependencies to get rid of critical vulnerability. Upgrade module dependencies to latest available stable. This includes upgrading golang.org/x/crypto, which had critical issues, from 0.30.0 to 0.31.0 where those issues are resolved. ## Version 2.21.0 (December 13) Automatic VPN conflict avoidance Telepresence not only detects subnet conflicts between the cluster and workstation VPNs but also resolves them by performing network address translation to move conflicting subnets out of the way. Virtual Address Translation (VNAT). It is now possible to use a virtual subnet without routing the affected IPs to a specific workload. A new `telepresence connect --vnat CIDR` flag was added that will perform virtual network address translation of cluster IPs. This flag is very similar to the `--proxy-via CIDR=WORKLOAD` introduced in 2.19, but without the need to specify a workload. Intercepts targeting a specific container In certain scenarios, the container owning the intercepted port differs from the container the intercept targets. This port owner's sole purpose is to route traffic from the service to the intended container, often using a direct localhost connection. This update introduces a `--container ` option to the intercept command. While this option doesn't influence the port selection, it guarantees that the environment variables and mounts propagated to the client originate from the specified container. Additionally, if the `--replace` option is used, it ensures that this container is replaced. New telepresence ingest command The new `telepresence ingest` command, similar to `telepresence intercept`, provides local access to the volume mounts and environment variables of a targeted container. However, unlike `telepresence intercept`, `telepresence ingest` does not redirect traffic to the container and ensures that the mounted volumes are read-only. An ingest requires a traffic-agent to be installed in the pods of the targeted workload. Beyond that, it's a client-side operation. This allows developers to have multiple simultaneous ingests on the same container. New telepresence curl command The new `telepresence curl` command runs curl from within a container. The command requires that a connection has been established using `telepresence connect --docker`, and the container that runs `curl` will share the same network as the containerized telepresence daemon. New telepresence docker-run command The new `telepresence docker-run ` requires that a connection has been established using `telepresence connect --docker` It will perform a `docker run ` and add the flag necessary to ensure that started container shares the same network as the containerized telepresence daemon. Mount everything read-only during intercept It is now possible to append ":ro" to the intercept `--mount` flag value. This ensures that all remote volumes that the intercept mounts are read-only. Unify client configuration Previously, client configuration was divided between the config.yml file and a Kubernetes extension. DNS and routing settings were initially found only in the extension. However, the Helm client structure allowed entries from both. To simplify this, we've now aligned the config.yml and Kubernetes extension with the Helm client structure. This means DNS and routing settings are now included in both. The Kubernetes extension takes precedence over the config.yml and Helm client object. While the old-style Kubernetes extension is still supported for compatibility, it cannot be used with the new style. Use WebSockets for port-forward instead of the now deprecated SPDY. Telepresence will now use WebSockets instead of SPDY when creating port-forwards to the Kubernetes Cluster, and will fall back to SPDY when connecting to clusters that don't support SPDY. Use of the deprecated SPDY can be forced by setting `cluster.forceSPDY=true` in the `config.yml`. See [Streaming Transitions from SPDY to WebSockets](https://kubernetes.io/blog/2024/08/20/websockets-transition/) for more information about this transition. Make usage data collection configurable using an extension point, and default to no-ops The OSS code-base will no longer report usage data to the proprietary collector at Ambassador Labs. The actual calls to the collector remain, but will be no-ops unless a proper collector client is installed using an extension point. Add deployments, statefulSets, replicaSets to workloads Helm chart value The Helm chart value `workloads` now supports the kinds `deployments.enabled`, `statefulSets.enabled`, `replicaSets.enabled`. and `rollouts.enabled`. All except `rollouts` are enabled by default. The traffic-manager will ignore workloads, and Telepresence will not be able to intercept them, if the `enabled` of the corresponding kind is set to `false`. Improved command auto-completion The auto-completion of namespaces, services, and containers have been added where appropriate, and the default file auto completion has been removed from most commands. Docker run flags --publish, --expose, and --network now work with docker mode connections After establishing a connection to a cluster using `telepresence connect --docker`, you can run new containers that share the same network as the containerized daemon that maintains the connection. This enables seamless communication between your local development environment and the remote services. Normally, Docker has a limitation that prevents combining a shared network configuration with custom networks and exposing ports. However, Telepresence now elegantly circumvents this limitation so that a container started with `telepresence docker-run`, `telepresence intercept --docker-run`, or `telepresence ingest --docker-run` can use flags like `--network`, `--publish`, or `--expose`. To achieve this, Telepresence temporarily adds the necessary network to the containerized daemon. This allows the new container to join the same network. Additionally, Telepresence starts extra socat containers to handle port mapping, ensuring that the desired ports are exposed to the local environment. Prevent recursion in the Telepresence Virtual Network Interface (VIF) Network problems may arise when running Kubernetes locally (e.g., Docker Desktop, Kind, Minikube, k3s), because the VIF on the host is also accessible from the cluster's nodes. A request that isn't handled by a cluster resource might be routed back into the VIF and cause a recursion. These recursions can now be prevented by setting the client configuration property `routing.recursionBlockDuration` so that new connection attempts are temporarily blocked for a specific IP:PORT pair immediately after an initial attempt, thereby effectively ending the recursion. Allow Helm chart to be included as a sub-chart The Helm chart previously had the unnecessary restriction that the .Release.Name under which telepresence is installed is literally called "traffic-manager". This restriction was preventing telepresence from being included as a sub-chart in a parent chart called anything but "traffic-manager". This restriction has been lifted. Add Windows arm64 client build Telepresence client is now available for Windows ARM64. Updated the release workflow files in github actions to build and publish the Windows ARM64 client. The --agents flag to telepresence uninstall is now the default. The `telepresence uninstall` was once capable of uninstalling the traffic-manager as well as traffic-agents. This behavior has been deprecated for some time now and in this release, the command is all about uninstalling the agents. Therefore the `--agents` flag was made redundant and whatever arguments that are given to the command must be name of workloads that have an agent installed unless the `--all-agents` is used, in which case no arguments are allowed. Performance improvement for the telepresence list command The `telepresence list` command will now retrieve its data from the traffic-manager, which significantly improves its performance when used on namespaces that have a lot of workloads. During an intercept, the local port defaults to the targeted port of the intercepted container instead of 8080. Telepresence mimics the environment of a target container during an intercept, so it's only natural that the default for the local port is determined by the targeted container port rather than just defaulting to 8080. A default can still be explicitly defined using the `config.intercept.defaultPort` setting. Move the telepresence-intercept-env configmap data into traffic-manager configmap. There's no need for two configmaps that store configuration data for the traffic manager. The traffic-manager configmap is also watched, so consolidating the configuration there saves some k8s API calls. Tracing was removed. The ability to collect trace has been removed along with the `telepresence gather-traces` and `telepresence upload-traces` commands. The underlying code was complex and has not been well maintained since its inception in 2022. We have received no feedback on it and seen no indication that it has ever been used. Remove obsolete code checking the Docker Bridge for DNS The DNS resolver checked the Docker bridge for messages on Linux. This code was obsolete and caused problems when running in Codespaces. Fix telepresence connect confusion caused by /.dockerenv file A `/.dockerenv` will be present when running in a GitHub Codespaces environment. That doesn't mean that telepresence cannot use docker, or that the root daemon shouldn't start. Cap timeouts.connectivityCheck at 5 seconds. The timeout value of `timeouts.connectivityCheck` is used when checking if a cluster is already reachable without Telepresence setting up an additional network route. If it is, this timeout should be high enough to cover the delay when establishing a connection. If this delay is higher than a second, then chances are very low that the cluster already is reachable, and if it can, that all accesses to it will be very slow. In such cases, Telepresence will create its own network interface and do perform its own tunneling. The default timeout for the check remains at 500 millisecond, which is more than sufficient for the majority of cases. Prevent that traffic-manager injects a traffic-agent into itself. The traffic-manager can never be a subject for an intercept, ingest, or proxy-via, because that means that it injects the traffic-agent into itself, and it is not designed to do that. A user attempting this will now see a meaningful error message. Don't include pods in the kube-system namespace when computing pod-subnets from pod IPs A user would normally never access pods in the `kube-system` namespace directly, and automatically including pods included there when computing the subnets will often lead to problems when running the cluster locally. This namespace is therefore now excluded in situations when the pod subnets are computed from the IPs of pods. Services in this namespace will still be available through the service subnet. If a user should require the pod-subnet to be mapped, it can be added to the `client.routing.alsoProxy` list in the helm chart. Let routes belonging to an allowed conflict be added as a static route on Linux. The `allowConflicting` setting didn't always work on Linux because the conflicting subnet was just added as a link to the TUN device, and therefore didn't get subjected to routing rule used to assign priority to the given subnet. ## Version 2.20.3 (November 18) Ensure that Telepresence works with GitHub Codespaces GitHub Codespaces runs in a container, but not as root. Telepresence didn't handle this situation correctly and only started the user daemon. The root daemon was never started. Mounts not working correctly when connected with --proxy-via A mount would try to connect to the sftp/ftp server using the original (cluster side) IP although that IP was translated into a virtual IP when using `--proxy-via`. ## Version 2.20.2 (October 21) Crash in traffic-manager configured with agentInjector.enabled=false A traffic-manager that was installed with the Helm value `agentInjector.enabled=false` crashed when a client used the commands `telepresence version` or `telepresence status`. Those commands would call a method on the traffic-manager that panicked if no traffic-agent was present. This method will now instead return the standard `Unavailable` error code, which is expected by the caller. ## Version 2.20.1 (October 10) Some workloads missing in the telepresence list output (typically replicasets owned by rollouts). Version 2.20.0 introduced a regression in the `telepresence list` command, resulting in the omission of all workloads that were owned by another workload. The correct behavior is to just omit those workloads that are owned by the supported workload kinds `Deployment`, `ReplicaSet`, `StatefulSet`, and `Rollout`. Furthermore, the `Rollout` kind must only be considered supported when the Argo Rollouts feature is enabled in the traffic-manager. Allow comma separated list of daemons for the gather-logs command. The name of the `telepresence gather-logs` flag `--daemons` suggests that the argument can contain more than one daemon, but prior to this fix, it couldn't. It is now possible to use a comma separated list, e.g. `telepresence gather-logs --daemons root,user`. ## Version 2.20.0 (October 3) Add timestamp to telepresence_logs.zip filename. Telepresence is now capable of easily find telepresence gather-logs by certain timestamp. Enable intercepts of workloads that have no service. Telepresence is now capable of intercepting workloads that have no associated service. The intercept will then target container port instead of a service port. The new behavior is enabled by adding a telepresence.getambassador.io/inject-container-ports annotation where the value is a comma separated list of port identifiers consisting of either the name or the port number of a container port, optionally suffixed with `/TCP` or `/UDP`. Publish the OSS version of the telepresence Helm chart The OSS version of the telepresence helm chart is now available at ghcr.io/telepresenceio/telepresence-oss, and can be installed using the command:
helm install traffic-manager oci://ghcr.io/telepresenceio/telepresence-oss --namespace ambassador --version 2.20.0 The chart documentation is published at ArtifactHUB.
Control the syntax of the environment file created with the intercept flag --env-file A new --env-syntax <syntax> was introduced to allow control over the syntax of the file created when using the intercept flag --env-file <file>. Valid syntaxes are "docker", "compose", "sh", "csh", "cmd", and "ps"; where "sh", "csh", and "ps" can be suffixed with ":export". Add support for Argo Rollout workloads. Telepresence now has an opt-in support for Argo Rollout workloads. The behavior is controlled by `workloads.argoRollouts.enabled` Helm chart value. It is recommended to set the following annotation telepresence.getambassador.io/inject-traffic-agent: enabled to avoid creation of unwanted revisions. Enable intercepts of containers that bind to podIP In previous versions, the traffic-agent would route traffic to localhost during periods when an intercept wasn't active. This made it impossible for an application to bind to the pod's IP, and it also meant that service meshes binding to the podIP would get bypassed, both during and after an intercept had been made. This is now changed, so that the traffic-agent instead forwards non intercepted requests to the pod's IP, thereby enabling the application to either bind to localhost or to that IP. Use ghcr.io/telepresenceio instead of docker.io/datawire for OSS images and the telemount Docker volume plugin. All OSS telepresence images and the telemount Docker plugin are now published at the public registry ghcr.io/telepresenceio and all references from the client and traffic-manager has been updated to use this registry instead of the one at docker.io/datawire. Use nftables instead of iptables-legacy Some time ago, we introduced iptables-legacy because users had problems using Telepresence with Fly.io where nftables wasn't supported by the kernel. Fly.io has since fixed this, so Telepresence will now use nftables again. This in turn, ensures that modern systems that lack support iptables-legacy will work. Root daemon wouldn't start when sudo timeout was zero. The root daemon refused to start when sudo was configured with a timestamp_timeout=0. This was due to logic that first requested root privileges using a sudo call, and then relied on that these privileges were cached, so that a subsequent call using --non-interactive was guaranteed to succeed. This logic will now instead do one single sudo call, and rely solely on sudo to print an informative prompt and start the daemon in the background. Detect minikube network when connecting with --docker A telepresence connect --docker failed when attempting to connect to a minikube that uses a docker driver because the containerized daemon did not have access to the minikube docker network. Telepresence will now detect an attempt to connect to that network and attach it to the daemon container as needed. ## Version 2.19.1 (July 12) Add brew support for the OSS version of Telepresence. The Open-Source Software version of Telepresence can now be installed using the brew formula via brew install telepresenceio/telepresence/telepresence-oss. Add --create-namespace flag to the telepresence helm install command. A --create-namespace (default true) flag was added to the telepresence helm install command. No attempt will be made to create a namespace for the traffic-manager if it is explicitly set to false. The command will then fail if the namespace is missing. Introduce DNS fallback on Windows. A network.defaultDNSWithFallback config option has been introduced on Windows. It will cause the DNS-resolver to fall back to the resolver that was first in the list prior to when Telepresence establishes a connection. The option is default true since it is believed to give the best experience but can be set to false to restore the old behavior. Brew now supports MacOS (amd64/arm64) / Linux (amd64) The brew formula can now dynamically support MacOS (amd64/arm64) / Linux (amd64) in a single formula Add ability to provide an externally-provisioned webhook secret Added supplied as a new option for agentInjector.certificate.method. This fully disables the generation of the Mutating Webhook's secret, allowing the chart to use the values of a pre-existing secret named agentInjector.secret.name. Previously, the install would fail when it attempted to create or update the externally-managed secret. Let PTR query for DNS server return the cluster domain. The nslookup program on Windows uses a PTR query to retrieve its displayed "Server" property. This Telepresence DNS resolver will now return the cluster domain on such a query. Add scheduler name to PODs templates. A new Helm chart value schedulerName has been added. With this feature, we are able to define some particular schedulers from Kubernetes to apply some different strategies to allocate telepresence resources, including the Traffic Manager and hooks pods. Race in traffic-agent injector when using inject annotation Applying multiple deployments that used the telepresence.getambassador.io/inject-traffic-agent: enabled would cause a race condition, resulting in a large number of created pods that eventually had to be deleted, or sometimes in pods that didn't contain a traffic agent. Fix configuring custom agent security context -> The traffic-manager helm chart will now correctly use a custom agent security context if one is provided. ## Version 2.19.0 (June 15) Warn when an Open Source Client connects to an Enterprise Traffic Manager. The difference between the OSS and the Enterprise offering is not well understood, and OSS users often install a traffic-manager using the Helm chart published at getambassador.io. This Helm chart installs an enterprise traffic-manager, which is probably not what the user would expect. Telepresence will now warn when an OSS client connects to an enterprise traffic-manager and suggest switching to an enterprise client, or use telepresence helm install to install an OSS traffic-manager. Add scheduler name to PODs templates. A new Helm chart value schedulerName has been added. With this feature, we are able to define some particular schedulers from Kubernetes to apply some different strategies to allocate telepresence resources, including the Traffic Manager and hooks pods. Improve traffic-manager performance in very large clusters. -> The traffic-manager will now use a shared-informer when keeping track of deployments. This will significantly reduce the load on the Kublet in large clusters and therefore lessen the risk for the traffic-manager being throttled, which can lead to other problems. Kubeconfig exec authentication failure when connecting with --docker from a WSL linux host Clusters like Amazon EKS often use a special authentication binary that is declared in the kubeconfig using an exec authentication strategy. This binary is normally not available inside a container. Consequently, a modified kubeconfig is used when telepresence connect --docker executes, appointing a kubeauth binary which instead retrieves the authentication from a port on the Docker host that communicates with another process outside of Docker. This process then executes the original exec command to retrieve the necessary credentials. This setup was problematic when using WSL, because even though telepresence connect --docker was executed on a Linux host, the Docker host available from host.docker.internal that the kubeauth connected to was the Windows host running Docker Desktop. The fix for this was to use the local IP of the default route instead of host.docker.internal when running under WSL.. Fix bug in workload cache, causing endless recursion when a workload uses the same name as its owner. The workload cache was keyed by name and namespace, but not by kind, so a workload named the same as its owner workload would be found using the same key. This led to the workload finding itself when looking up its owner, which in turn resulted in an endless recursion when searching for the topmost owner. FailedScheduling events mentioning node availability considered fatal when waiting for agent to arrive. The traffic-manager considers some events as fatal when waiting for a traffic-agent to arrive after an injection has been initiated. This logic would trigger on events like "Warning FailedScheduling 0/63 nodes are available" although those events indicate a recoverable condition and kill the wait. This is now fixed so that the events are logged but the wait continues. Improve how the traffic-manager resolves DNS when no agent is installed. The traffic-manager is typically installed into a namespace different from the one that clients are connected to. It's therefore important that the traffic-manager adds the client's namespace when resolving single label names in situations where there are any agents to dispatch the DNS query to. Removal of ability import legacy artifact into Helm. A helm install would make attempts to find manually installed artifacts and make them managed by Helm by adding the necessary labels and annotations. This was important when the Helm chart was first introduced but is far less so today, and this legacy import was therefore removed. Docker aliases deprecation caused failure to detect Kind cluster. The logic for detecting if a cluster is a local Kind cluster, and therefore needs some special attention when using telepresence connect --docker, relied on the presence of Aliases in the Docker network that a Kind cluster sets up. In Docker versions from 26 and up, this value is no longer used, but the corresponding info can instead be found in the new DNSNames field. Include svc as a top-level domain in the DNS resolver. It's not uncommon that use-cases involving Kafka or other middleware use FQNs that end with "svc". The core-DNS resolver in Kubernetes can resolve such names. With this bugfix, the Telepresence DNS resolver will also be able to resolve them, and thereby remove the need to add ".svc" to the include-suffix list. Add ability to enable/disable the mutating webhook. A new Helm chart boolean value agentInjector.enable has been added that controls the agent-injector service and its associated mutating webhook. If set to false, the service, the webhook, and the secrets and certificates associated with it, will no longer be installed. Add ability to mount a webhook secret. A new Helm chart value agentInjector.certificate.accessMethod which can be set to watch (the default) or mount has been added. The mount setting is intended for clusters with policies that prevent containers from doing a get, list or watch of a Secret, but where a latency of up to 90 seconds is acceptable between the time the secret is regenerated and the agent-injector picks it up. Make it possible to specify ignored volume mounts using path prefix. Volume mounts like /var/run/secrets/kubernetes.io are not declared in the workload. Instead, they are injected during pod-creation and their names are generated. It is now possible to ignore such mounts using a matching path prefix. Make the telemount Docker Volume plugin configurable A telemount object was added to the intercept object in config.yml (or Helm value client.intercept), so that the automatic download and installation of this plugin can be fully customised. Add option to load the kubeconfig yaml from stdin during connect. This allows another process with a kubeconfig already loaded in memory to directly pass it to telepresence connect without needing a separate file. Simply use a dash "-" as the filename for the --kubeconfig flag. Add ability to specify agent security context. A new Helm chart value agent.securityContext that will allow configuring the security context of the injected traffic agent. The value can be set to a valid Kubernetes securityContext object, or can be set to an empty value ({}) to ensure the agent has no defined security context. If no value is specified, the traffic manager will set the agent's security context to the same as the first container's of the workload being injected into. Tracing is no longer enabled by default. Tracing must now be enabled explicitly in order to use the telepresence gather-traces command. Removal of timeouts that are no longer in use The config.yml values timeouts.agentInstall and timeouts.apply haven't been in use since versions prior to 2.6.0, when the client was responsible for installing the traffic-agent. These timeouts are now removed from the code-base, and a warning will be printed when attempts are made to use them. Search all private subnets to find one open for dnsServerSubnet This resolves a bug that did not test all subnets in a private range, sometimes resulting in the warning, "DNS doesn't seem to work properly." Docker aliases deprecation caused failure to detect Kind cluster. The logic for detecting if a cluster is a local Kind cluster, and therefore needs some special attention when using telepresence connect --docker, relied on the presence of Aliases in the Docker network that a Kind cluster sets up. In Docker versions from 26 and up, this value is no longer used, but the corresponding info can instead be found in the new DNSNames field. Creation of individual pods was blocked by the agent-injector webhook. An attempt to create a pod was blocked unless it was provided by a workload. Hence, commands like kubectl run -i busybox --rm --image=curlimages/curl --restart=Never -- curl echo-easy.default would be blocked from executing. Fix panic due to root daemon not running. If a telepresence connect was made at a time when the root daemon was not running (an abnormal condition) and a subsequent intercept was then made, a panic would occur when the port-forward to the agent was set up. This is now fixed so that the initial telepresence connect is refused unless the root daemon is running. Get rid of telemount plugin stickiness The datawire/telemount that is automatically downloaded and installed, would never be updated once the installation was made. Telepresence will now check for the latest release of the plugin and cache the result of that check for 24 hours. If a new version arrives, it will be installed and used. Use route instead of address for CIDRs with masks that don't allow "via" A CIDR with a mask that leaves less than two bits (/31 or /32 for IPv4) cannot be added as an address to the VIF, because such addresses must have bits allowing a "via" IP. The logic was modified to allow such CIDRs to become static routes, using the VIF base address as their "via", rather than being VIF addresses in their own right. Containerized daemon created cache files owned by root When using telepresence connect --docker to create a containerized daemon, that daemon would sometimes create files in the cache that were owned by root, which then caused problems when connecting without the --docker flag. Remove large number of requests when traffic-manager is used in large clusters. The traffic-manager would make a very large number of API requests during cluster start-up or when many services were changed for other reasons. The logic that did this was refactored and the number of queries were significantly reduced. Don't patch probes on replaced containers. A container that is being replaced by a telepresence intercept --replace invocation will have no liveness-, readiness, nor startup-probes. Telepresence didn't take this into consideration when injecting the traffic-agent, but now it will refrain from patching symbolic port names of those probes. Don't rely on context name when deciding if a kind cluster is used. The code that auto-patches the kubeconfig when connecting to a kind cluster from within a docker container, relied on the context name starting with "kind-", but although all contexts created by kind have that name, the user is still free to rename it or to create other contexts using the same connection properties. The logic was therefore changed to instead look for a loopback service address. ## Version 2.18.0 (February 9) Include the image for the traffic-agent in the output of the version and status commands. The version and status commands will now output the image that the traffic-agent will be using when injected by the agent-injector. Custom DNS using the client DNS resolver.

A new telepresence connect --proxy-via CIDR=WORKLOAD flag was introduced, allowing Telepresence to translate DNS responses matching specific subnets into virtual IPs that are used locally. Those virtual IPs are then routed (with reverse translation) via the pod's of a given workload. This makes it possible to handle custom DNS servers that resolve domains into loopback IPs. The flag may also be used in cases where the cluster's subnets are in conflict with the workstation's VPN.

The CIDR can also be a symbolic name that identifies a subnet or list of subnets:

alsoAll subnets added with --also-proxy
serviceThe cluster's service subnet
podsThe cluster's pod subnets.
allAll of the above.
Ensure that agent.appProtocolStrategy is propagated correctly. The agent.appProtocolStrategy was inadvertently dropped when moving license related code fromm the OSS repository the repository for the Enterprise version of Telepresence. It has now been restored. Include non-default zero values in output of telepresence config view. The telepresence config view command will now print zero values in the output when the default for the value is non-zero. Restore ability to run the telepresence CLI in a docker container. The improvements made to be able to run the telepresence daemon in docker using telepresence connect --docker made it impossible to run both the CLI and the daemon in docker. This commit fixes that and also ensures that the user- and root-daemons are merged in this scenario when the container runs as root. Remote mounts when intercepting with the --replace flag. A telepresence intercept --replace did not correctly mount all volumes, because when the intercepted container was removed, its mounts were no longer visible to the agent-injector when it was subjected to a second invocation. The container is now kept in place, but with an image that just sleeps infinitely. Intercepting with the --replace flag will no longer require all subsequent intercepts to use --replace. A telepresence intercept --replace will no longer switch the mode of the intercepted workload, forcing all subsequent intercepts on that workload to use --replace until the agent is uninstalled. Instead, --replace can be used interchangeably just like any other intercept flag. Kubeconfig exec authentication with context names containing colon didn't work on Windows The logic added to allow the root daemon to connect directly to the cluster using the user daemon as a proxy for exec type authentication in the kube-config, didn't take into account that a context name sometimes contains the colon ":" character. That character cannot be used in filenames on windows because it is the drive letter separator. Provide agent name and tag as separate values in Helm chart The AGENT_IMAGE was a concatenation of the agent's name and tag. This is now changed so that the env instead contains an AGENT_IMAGE_NAME and AGENT_INAGE_TAG. The AGENT_IMAGE is removed. Also, a new env REGISTRY is added, where the registry of the traffic- manager image is provided. The AGENT_REGISTRY is no longer required and will default to REGISTRY if not set. Environment interpolation expressions were prefixed twice. Telepresence would sometimes prefix environment interpolation expressions in the traffic-agent twice so that an expression that looked like $(SOME_NAME) in the app-container, ended up as $(_TEL_APP_A__TEL_APP_A_SOME_NAME) in the corresponding expression in the traffic-agent. Panic in root-daemon on darwin workstations with full access to cluster network. A darwin machine with full access to the cluster's subnets will never create a TUN-device, and a check was missing if the device actually existed, which caused a panic in the root daemon. Show allow-conflicting-subnets in telepresence status and telepresence config view. The telepresence status and telepresence config view commands didn't show the allowConflictingSubnets CIDRs because the value wasn't propagated correctly to the CLI. It is now possible use a host-based connection and containerized connections simultaneously. Only one host-based connection can exist because that connection will alter the DNS to reflect the namespace of the connection. but it's now possible to create additional connections using --docker while retaining the host-based connection. Ability to set the hostname of a containerized daemon. The hostname of a containerized daemon defaults to be the container's ID in Docker. You now can override the hostname using telepresence connect --docker --hostname <a name>. New <code>--multi-daemon</code>flag to enforce a consistent structure for the status command output. The output of the telepresence status when using --output json or --output yaml will either show an object where the user_daemon and root_daemon are top level elements, or when multiple connections are used, an object where a connections list contains objects with those daemons. The flag --multi-daemon will enforce the latter structure even when only one daemon is connected so that the output can be parsed consistently. The reason for keeping the former structure is to retain backward compatibility with existing parsers. Make output from telepresence quit more consistent. A quit (without -s) just disconnects the host user and root daemons but will quit a container based daemon. The message printed was simplified to remove some have/has is/are errors caused by the difference. Fix "tls: bad certificate" errors when refreshing the mutator-webhook secret The agent-injector service will now refresh the secret used by the mutator-webhook each time a new connection is established, thus preventing the certificates to go out-of-sync when the secret is regenerated. Keep telepresence-agents configmap in sync with pod states. An intercept attempt that resulted in a timeout due to failure of injecting the traffic-agent left the telepresence-agents configmap in a state that indicated that an agent had been added, which caused problems for subsequent intercepts after the problem causing the first failure had been fixed. The <code>telepresence status</code> command will now report the status of all running daemons. A telepresence status, issued when multiple containerized daemons were active, would error with "multiple daemons are running, please select one using the --use <match> flag". This is now fixed so that the command instead reports the status of all running daemons. The <code>telepresence version</code> command will now report the version of all running daemons. A telepresence version, issued when multiple containerized daemons were active, would error with "multiple daemons are running, please select one using the --use <match> flag". This is now fixed so that the command instead reports the version of all running daemons. Multiple containerized daemons can now be disconnected using <code>telepresence quit -s</code> A telepresence quit -s, issued when multiple containerized daemons were active, would error with "multiple daemons are running, please select one using the --use <match> flag". This is now fixed so that the command instead quits all daemons. The DNS search path on Windows is now restored when Telepresence quits The DNS search path that Telepresence uses to simulate the DNS lookup functionality in the connected cluster namespace was not removed by a telepresence quit, resulting in connectivity problems from the workstation. Telepresence will now remove the entries that it has added to the search list when it quits. The user-daemon would sometimes get killed when used by multiple simultaneous CLI clients. The user-daemon would die with a fatal "fatal error: concurrent map writes" error in the connector.log, effectively killing the ongoing connection. Multiple services ports using the same target port would not get intercepted correctly. Intercepts didn't work when multiple service ports were using the same container port. Telepresence would think that one of the ports wasn't intercepted and therefore disable the intercept of the container port. Root daemon refuses to disconnect. The root daemon would sometimes hang forever when attempting to disconnect due to a deadlock in the VIF-device. Fix panic in user daemon when traffic-manager was unreachable The user daemon would panic if the traffic-manager was unreachable. It will now instead report a proper error to the client. Removal of backward support for versions predating 2.6.0 The telepresence helm installer will no longer discover and convert workloads that were modified by versions prior to 2.6.0. The traffic manager will and no longer support the muxed tunnels used in versions prior to 2.5.0. ## Version 2.17.0 (November 14) Additional Prometheus metrics to track intercept/connect activity This feature adds the following metrics to the Prometheus endpoint: connect_count, connect_active_status, intercept_count, and intercept_active_status. These are labeled by client/install_id. Additionally, the intercept_count metric has been renamed to active_intercept_count for clarity. Make the Telepresence client docker image configurable. The docker image used when running a Telepresence intercept in docker mode can now be configured using the setting images.clientImage and will default first to the value of the environment TELEPRESENCE_CLIENT_IMAGE, and then to the value preset by the telepresence binary. This configuration setting is primarily intended for testing purposes. Use traffic-agent port-forwards for outbound and intercepted traffic. The telepresence TUN-device is now capable of establishing direct port-forwards to a traffic-agent in the connected namespace. That port-forward is then used for all outbound traffic to the device, and also for all traffic that arrives from intercepted workloads. Getting rid of the extra hop via the traffic-manager improves performance and reduces the load on the traffic-manager. The feature can only be used if the client has Kubernetes port-forward permissions to the connected namespace. It can be disabled by setting cluster.agentPortForward to false in config.yml. Improve outbound traffic performance. The root-daemon now communicates directly with the traffic-manager instead of routing all outbound traffic through the user-daemon. The root-daemon uses a patched kubeconfig where exec configurations to obtain credentials are dispatched to the user-daemon. This to ensure that all authentication plugins will execute in user-space. The old behavior of routing everything through the user-daemon can be restored by setting cluster.connectFromRootDaemon to false in config.yml. New networking CLI flag --allow-conflicting-subnets telepresence connect (and other commands that kick off a connect) now accepts an --allow-conflicting-subnets CLI flag. This is equivalent to client.routing.allowConflictingSubnets in the helm chart, but can be specified at connect time. It will be appended to any configuration pushed from the traffic manager. Warn if large version mismatch between traffic manager and client. Print a warning if the minor version diff between the client and the traffic manager is greater than three. The authenticator binary was removed from the docker image. The authenticator binary, used when serving proxied exec kubeconfig credential retrieval, has been removed. The functionality was instead added as a subcommand to the telepresence binary. ## Version 2.16.1 (October 12) Add --docker-debug flag to the telepresence intercept command. This flag is similar to --docker-build but will start the container with more relaxed security using the docker run flags --security-opt apparmor=unconfined --cap-add SYS_PTRACE. Add a --export option to the telepresence connect command. In some situations it is necessary to make some ports available to the host from a containerized telepresence daemon. This commit adds a repeatable --expose <docker port exposure> flag to the connect command. Prevent agent-injector webhook from selecting from kube-xxx namespaces. The kube-system and kube-node-lease namespaces should not be affected by a global agent-injector webhook by default. A default namespaceSelector was therefore added to the Helm Chart agentInjector.webhook that contains a NotIn preventing those namespaces from being selected. Backward compatibility for pod template TLS annotations. Users of Telepresence < 2.9.0 that make use of the pod template TLS annotations were unable to upgrade because the annotation names have changed (now prefixed by "telepresence."), and the environment expansion of the annotation values was dropped. This fix restores support for the old names (while retaining the new ones) and the environment expansion. Built with go 1.21.3 Built Telepresence with go 1.21.3 to address CVEs. Match service selector against pod template labels When listing intercepts (typically by calling telepresence list) selectors of services are matched against workloads. Previously the match was made against the labels of the workload, but now they are matched against the labels pod template of the workload. Since the service would actually be matched against pods this is more correct. The most common case when this makes a difference is that statefulsets now are listed when they should. ## Version 2.16.0 (October 2) The helm sub-commands will no longer start the user daemon. The telepresence helm install/upgrade/uninstall commands will no longer start the telepresence user daemon because there's no need to connect to the traffic-manager in order for them to execute. Routing table race condition A race condition would sometimes occur when a Telepresence TUN device was deleted and another created in rapid succession that caused the routing table to reference interfaces that no longer existed. Stop lingering daemon container When using telepresence connect --docker, a lingering container could be present, causing errors like "The container name NN is already in use by container XX ...". When this happens, the connect logic will now give the container some time to stop and then call docker stop NN to stop it before retrying to start it. Add file locking to the Telepresence cache Files in the Telepresence cache are accesses by multiple processes. The processes will now use advisory locks on the files to guarantee consistency. Lock connection to namespace The behavior changed so that a connected Telepresence client is bound to a namespace. The namespace can then not be changed unless the client disconnects and reconnects. A connection is also given a name. The default name is composed from <kube context name>-<namespace> but can be given explicitly when connecting using --name. The connection can optionally be identified using the option --use <name match> (only needed when docker is used and more than one connection is active). Deprecation of global --context and --docker flags. The global flags --context and --docker will now be considered deprecated unless used with commands that accept the full set of Kubernetes flags (e.g. telepresence connect). Deprecation of the --namespace flag for the intercept command. The --namespace flag is now deprecated for telepresence intercept command. The flag can instead be used with all commands that accept the full set of Kubernetes flags (e.g. telepresence connect). Legacy code predating version 2.6.0 was removed. The telepresence code-base still contained a lot of code that would modify workloads instead of relying on the mutating webhook installer when a traffic-manager version predating version 2.6.0 was discovered. This code has now been removed. Add `telepresence list-namespaces` and `telepresence list-contexts` commands These commands can be used to check accessible namespaces and for automation. Implicit connect warning A deprecation warning will be printed if a command other than telepresence connect causes an implicit connect to happen. Implicit connects will be removed in a future release. ## Version 2.15.1 (September 6) Rebuild with go 1.21.1 Rebuild Telepresence with go 1.21.1 to address CVEs. Set security context for traffic agent Openshift users reported that the traffic agent injection was failing due to a missing security context. ## Version 2.15.0 (August 29) Add ASLR to telepresence binaries ASLR hardens binary sercurity against fixed memory attacks. Added client builds for arm64 architecture. Updated the release workflow files in github actions to including building and publishing the client binaries for arm64 architecture. KUBECONFIG env var can now be used with the docker mode. If provided, the KUBECONFIG environment variable was passed to the kubeauth-foreground service as a parameter. However, since it didn't exist, the CLI was throwing an error when using telepresence connect --docker. Fix deadlock while watching workloads The telepresence list --output json-stream wasn't releasing the session's lock after being stopped, including with a telepresence quit. The user could be blocked as a result. Change json output of telepresence list command Replace deprecated info in the JSON output of the telepresence list command. ## Version 2.14.4 (August 21) Nil pointer exception when upgrading the traffic-manager. Upgrading the traffic-manager using telepresence helm upgrade would sometimes result in a helm error message executing "telepresence/templates/intercept-env-configmap.yaml" at <.Values.intercept.environment.excluded>: nil pointer evaluating interface {}.excluded" ## Version 2.14.2 (July 26) Telepresence now use the OSS agent in its latest version by default. The traffic manager admin was forced to set it manually during the chart installation. ## Version 2.14.1 (July 7) Envoy's http idle timout is now configurable. A new agent.helm.httpIdleTimeout setting was added to the Helm chart that controls the proprietary Traffic agent's http idle timeout. The default of one hour, which in some situations would cause a lot of resource consuming and lingering connections, was changed to 70 seconds. Add more gauges to the Traffic manager's Prometheus client. Several gauges were added to the Prometheus client to make it easier to monitor what the Traffic manager spends resources on. Agent Pull Policy Add option to set traffic agent pull policy in helm chart. Resource leak in the Traffic manager. Fixes a resource leak in the Traffic manager caused by lingering tunnels between the clients and Traffic agents. The tunnels are now closed correctly when terminated from the side that created them. Fixed problem setting traffic manager namespace using the kubeconfig extension. Fixes a regression introduced in version 2.10.5, making it impossible to set the traffic-manager namespace using the telepresence.io kubeconfig extension. ## Version 2.14.0 (June 12) DNS configuration now supports excludes and mappings. The DNS configuration now supports two new fields, excludes and mappings. The excludes field allows you to exclude a given list of hostnames from resolution, while the mappings field can be used to resolve a hostname with another. Added the ability to exclude environment variables Added a new config map that can take an array of environment variables that will then be excluded from an intercept that retrieves the environment of a pod. Fixed traffic-agent backward incompatibility issue causing lack of remote mounts A traffic-agent of version 2.13.3 (or 1.13.15) would not propagate the directories under /var/run/secrets when used with a traffic manager older than 2.13.3. Fixed race condition causing segfaults on rare occasions when a tunnel stream timed out. A context cancellation could sometimes be trapped in a stream reader, causing it to incorrectly return an undefined message which in turn caused the parent reader to panic on a nil pointer reference. Routing conflict reporting. Telepresence will now attempt to detect and report routing conflicts with other running VPN software on client machines. There is a new configuration flag that can be tweaked to allow certain CIDRs to be overridden by Telepresence. test-vpn command deprecated Running telepresence test-vpn will now print a deprecation warning and exit. The command will be removed in a future release. Instead, please configure telepresence for your VPN's routes. ## Version 2.13.3 (May 25) Add imagePullSecrets to hooks Add .Values.hooks.curl.imagePullSecrets and .Values.hooks curl.imagePullSecrets to Helm values. Change reinvocation policy to Never for the mutating webhook The default setting of the reinvocationPolicy for the mutating webhook dealing with agent injections changed from Never to IfNeeded. Fix mounting fail of IAM roles for service accounts web identity token The eks.amazonaws.com/serviceaccount volume injected by EKS is now exported and remotely mounted during an intercept. Correct namespace selector for cluster versions with non-numeric characters The mutating webhook now correctly applies the namespace selector even if the cluster version contains non-numeric characters. For example, it can now handle versions such as Major:"1", Minor:"22+". Enable IPv6 on the telepresence docker network The "telepresence" Docker network will now propagate DNS AAAA queries to the Telepresence DNS resolver when it runs in a Docker container. Fix the crash when intercepting with --local-only and --docker-run Running telepresence intercept --local-only --docker-run no longer results in a panic. Fix incorrect error message with local-only mounts Running telepresence intercept --local-only --mount false no longer results in an incorrect error message saying "a local-only intercept cannot have mounts". specify port in hook urls The helm chart now correctly handles custom agentInjector.webhook.port that was not being set in hook URLs. Fix wrong default value for disableGlobal and agentArrival Params .intercept.disableGlobal and .timeouts.agentArrival are now correctly honored. ================================================ FILE: docs/troubleshooting.md ================================================ --- title: Troubleshooting description: "Learn how to troubleshoot common issues related to Telepresence, including intercept issues, cluster connection issues, and errors related to Ambassador Cloud." --- # Troubleshooting ## Connecting to a cluster via VPN doesn't work. There are a few different issues that could arise when working with a VPN. Please see the [dedicated page](reference/vpn.md) on Telepresence and VPNs to learn more on how to fix these. ## Connecting to a cluster hosted in a Docker Container or a VM on the workstation doesn't work The cluster probably has access to the host's network and gets confused when it is mapped by Telepresence. Please check the [cluster in hosted container or vm](howtos/cluster-in-vm.md) for more details. ## Volume mounts are not working on macOS It's necessary to have `sshfs` installed in order for volume mounts to work correctly during intercepts. Lately there's been some issues using `brew install sshfs` a macOS workstation because the required component `osxfuse` (now named `macfuse`) isn't open source and hence, no longer supported. As a workaround, you can now use `gromgit/fuse/sshfs-mac` instead. Follow these steps: 1. Remove old sshfs, macfuse, osxfuse using `brew uninstall` 2. `brew install --cask macfuse` 3. `brew install gromgit/fuse/sshfs-mac` 4. `brew link --overwrite sshfs-mac` Now sshfs -V shows you the correct version, e.g.: ``` $ sshfs -V SSHFS version 2.10 FUSE library version: 2.9.9 fuse: no mount point ``` 5. Next, try a mount (or an replace/ingest/intercept that performs a mount). It will fail because you need to give permission to “Benjamin Fleischer” to execute a kernel extension (a pop-up appears that takes you to the system preferences). 6. Approve the needed permission 7. Reboot your computer. ## Volume mounts are not working on Linux It's necessary to have `sshfs` installed in order for volume mounts to work correctly when Telepresence engages with remote containers. After you've installed `sshfs`, if mounts still aren't working: 1. Uncomment `user_allow_other` in `/etc/fuse.conf` 2. Add your user to the "fuse" group with: `sudo usermod -a -G fuse ` 3. Restart your computer after uncommenting `user_allow_other` ## DNS is broken on macOS Commands like `dig` cannot find cluster resources even though Telepresence is connected to the cluster, but it works with `curl`. This is because `dig`, and some other utilities on macOS have their own built-in DNS client which bypasses the macOS native DNS system and use the libc resolver directly. Here's an excerpt from the `dig` command's man-page: > Mac OS X NOTICE > > The nslookup command does not use the host name and address resolution or the DNS query routing > mechanisms used by other processes running on Mac OS X. The results of name or address queries > printed by nslookup may differ from those found by other processes that use the Mac OS X native > name and address resolution mechanisms. The results of DNS queries may also differ from queries > that use the Mac OS X DNS routing library. A command that should always work is: ```console $ dscacheutil -q host -a name ``` ## DNS does not resolve in GitLab pipeline If services are not resolving after running `telepresence connect` in a GitLab pipeline, this may be because the `resolv.conf` file is bind-mounted, which prevents it from being copied, deleted, or moved. However, you can still replace its contents. ```yaml job: ... script: - telepresence connect - echo "nameserver 127.0.0.1" > /etc/resolv.conf # Telepresence runs a DNS server on port 53 but cannot update the bind-mounted resolv.conf file ``` ## Helm install failes with "uncomparable type" error An attempt to install the traffic-manager using the `helm` command ends with an error similar to: ``` Error: INSTALLATION FAILED: template: telepresence-oss/templates/deployment.yaml:172:22: executing "telepresence-oss/templates/deployment.yaml" at : error calling eq: uncomparable type map[string]interface {}: map[capabilities:map[add:[NET_ADMIN]]] ``` This will happen when you are using `helm` directly (as opposed to `telepresence helm`) and your helm version is older than 3.11.3. To resolve this, you can upgrade your helm to a more recent version. ## No Sidecar Injected in GKE private clusters An attempt to `telepresence intercept` results in a timeout, and upon examination of the pods (`kubectl get pods`) it's discovered that the intercept command did not inject a sidecar into the workload's pods: ```bash $ kubectl get pod NAME READY STATUS RESTARTS AGE echo-easy-7f6d54cff8-rz44k 1/1 Running 0 5m5s $ telepresence intercept echo-easy -p 8080 telepresence: error: connector.CreateIntercept: request timed out while waiting for agent echo-easy.default to arrive $ kubectl get pod NAME READY STATUS RESTARTS AGE echo-easy-d8dc4cc7c-27567 1/1 Running 0 2m9s # Notice how 1/1 containers are ready. ``` If this is occurring in a GKE cluster with private networking enabled, it is likely due to firewall rules blocking the Traffic Manager's webhook injector from the API server. To fix this, add a firewall rule allowing your cluster's master nodes to access TCP port `8443` in your cluster's pods, or change the port number that Telepresence is using for the agent injector by providing the number of an allowed port using the Helm chart value `agentInjector.webhook.port`. Please refer to the [telepresence install instructions](install/cloud#gke) or the [GCP docs](https://cloud.google.com/kubernetes-engine/docs/how-to/private-clusters#add_firewall_rules) for information to resolve this. ## Injected init-container doesn't function properly The init-container is injected to insert `iptables` rules that redirects port numbers from the app container to the traffic-agent sidecar. This is necessary when the service's `targetPort` is numeric. It requires elevated privileges (`NET_ADMIN` capabilities), and the inserted rules may get overridden by `iptables` rules inserted by other vendors, such as Istio or Linkerd. Injection of the init-container can often be avoided by using a `targetPort` _name_ instead of a number, and ensure that the corresponding container's `containerPort` is also named. This example uses the name "http", but any valid name will do: ```yaml apiVersion: v1 kind: Pod metadata: ... spec: containers: - ports: - name: http containerPort: 8080 --- apiVersion: v1 kind: Service metadata: ... spec: ports: - port: 80 targetPort: http ``` Telepresence injects an init-container into the pods of a workload, only if at least one service specifies a numeric `tagertPort` that references a `containerPort` in the workload. When this isn't the case, it will instead do the following during the injection of the traffic-agent: 1. Rename the designated container's port by prefixing it (i.e., containerPort: http becomes containerPort: tm-http). 2. Let the container port of our injected traffic-agent use the original name (i.e., containerPort: http). Kubernetes takes care of the rest and will now associate the service's `targetPort` with our traffic-agent's `containerPort`. > [!IMPORTANT] > If the service is "headless" (using `ClusterIP: None`), then using named ports won't help because the `targetPort` will > not get remapped. A headless service will always require the init-container. ## EKS, Calico, and Traffic Agent injection timeouts When using EKS with Calico CNI, the Kubernetes API server cannot reach the mutating webhook used for triggering the traffic agent injection. To solve this problem, try providing the Helm chart value `"hostNetwork=true"` when installing or upgrading the traffic-manager. More information can be found in this [blog post](https://medium.com/@denisstortisilva/kubernetes-eks-calico-and-custom-admission-webhooks-a2956b49bd0d). ## Error connecting to GKE or EKS cluster GKE and EKS require a plugin that utilizes their resepective IAM providers. You will need to install the [gke](install/cloud#gke-authentication-plugin) or [eks](install/cloud#eks-authentication-plugin) plugins for Telepresence to connect to your cluster. ## Routing loops when accessing deleted or non-existent service IPs on local clusters On local Kubernetes clusters (Kind, minikube, k3d, Docker Desktop), accessing a deleted or never-assigned service ClusterIP through the Telepresence TUN device can cause a routing loop. The packet is forwarded to the traffic-agent, which re-dials the same IP; with no kube-proxy rule in place the packet escapes the cluster via the node's default route, returns to the workstation, and the cycle repeats until the connection times out. Enable the **route-controller** DaemonSet to prevent this. It installs an iptables `FORWARD` chain `DROP` rule for the service CIDR on every node, ensuring that packets bound for non-existent ClusterIPs are silently dropped rather than escaping the cluster. See the [Route Controller reference](reference/route-controller.md) for installation and configuration instructions. ## `too many files open` error when running `telepresence connect` on Linux If `telepresence connect` on linux fails with a message in the logs `too many files open`, then check if `fs.inotify.max_user_instances` is set too low. Check the current settings with `sysctl fs.inotify.max_user_instances` and increase it temporarily with `sudo sysctl -w fs.inotify.max_user_instances=512`. For more information about permanently increasing it see [Kernel inotify watch limit reached](https://unix.stackexchange.com/a/13757/514457). ================================================ FILE: docs/variables.yml ================================================ version: "2.27.2" dlVersion: "v2.27.2" ================================================ FILE: examples/compose/proxy-voting.override.yaml ================================================ x-tele: connections: - namespace: emojivoto mounts: - volumePattern: .* policy: local services: voting: x-tele: type: proxy ================================================ FILE: examples/compose/proxy-web.override.yaml ================================================ x-tele: connections: - namespace: emojivoto mounts: - volumePattern: .* policy: local services: web: x-tele: type: proxy ports: - 8080:80 ================================================ FILE: examples/compose/replace.override.yaml ================================================ x-tele: connections: - namespace: emojivoto services: emoji: x-tele: type: replace voting: x-tele: type: replace vote-bot: profiles: - notEnabled ================================================ FILE: examples/docker/iam-authenticator/Dockerfile ================================================ FROM golang:alpine AS auth-builder RUN go install sigs.k8s.io/aws-iam-authenticator/cmd/aws-iam-authenticator@latest # Dockerfile with telepresence and its prerequisites FROM alpine # Install Telepresence prerequisites RUN apk add --no-cache curl iproute2 sshfs # Download and install the telepresence binary RUN curl -fL https://github.com/telepresenceio/telepresence/releases/download/v2.24.0-rc.0/telepresence-linux-amd64 -o telepresence && \ install -o root -g root -m 0755 telepresence /usr/local/bin/telepresence COPY --from=auth-builder /go/bin/aws-iam-authenticator ./aws-iam-authenticator RUN install -o root -g root -m 0755 aws-iam-authenticator /usr/local/bin/aws-iam-authenticator && \ rm aws-iam-authenticator ================================================ FILE: go.mod ================================================ module github.com/telepresenceio/telepresence/v2 go 1.26 require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/alexflint/go-filemutex v1.3.0 github.com/blang/semver/v4 v4.0.0 github.com/caarlos0/env/v11 v11.4.0 github.com/cenkalti/backoff/v4 v4.3.0 github.com/compose-spec/compose-go/v2 v2.10.1 github.com/containerd/errdefs v1.0.0 github.com/coreos/go-iptables v0.8.0 github.com/datawire/argo-rollouts-go-client v0.0.0-20241216133646-cb1073556c99 github.com/docker/docker v28.5.2+incompatible github.com/docker/go-units v0.5.0 github.com/fsnotify/fsnotify v1.9.0 github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 github.com/godbus/dbus/v5 v5.2.2 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb github.com/miekg/dns v1.1.72 github.com/mitchellh/go-wordwrap v1.0.1 github.com/moby/term v0.5.2 github.com/morikuni/aec v1.1.0 github.com/njayp/ophis v1.1.4 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pkg/sftp v1.13.10 github.com/prometheus/client_golang v1.23.2 github.com/puzpuzpuz/xsync/v4 v4.4.0 github.com/rogpeppe/go-internal v1.14.1 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 github.com/spf13/afero v1.15.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 github.com/telepresenceio/clog v0.0.0-20260114221933-287514cf9831 github.com/telepresenceio/go-ftpserver v1.2.1 github.com/telepresenceio/go-fuseftp v1.0.1 github.com/telepresenceio/go-fuseftp/rpc v1.0.1 github.com/telepresenceio/telepresence/cmd/cobraparser/v2 v2.0.0-20260228142840-e19ac5d889d5 github.com/telepresenceio/telepresence/rpc/v2 v2.27.2 github.com/vishvananda/netlink v1.3.1 golang.org/x/net v0.51.0 golang.org/x/sync v0.19.0 golang.org/x/sys v0.41.0 golang.org/x/term v0.40.0 golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb golang.zx2c4.com/wireguard/windows v0.5.3 google.golang.org/grpc v1.79.1 google.golang.org/protobuf v1.36.11 gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 helm.sh/helm/v3 v3.20.0 k8s.io/api v0.35.2 k8s.io/apimachinery v0.35.2 k8s.io/cli-runtime v0.35.2 k8s.io/client-go v0.35.2 k8s.io/kubectl v0.35.2 sigs.k8s.io/yaml v1.6.0 ) require ( dario.cat/mergo v1.0.2 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/BurntSushi/toml v1.6.0 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.3 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/containerd/containerd v1.7.30 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect github.com/fatih/camelcase v1.0.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fclairamb/ftpserverlib v0.30.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.22.4 // indirect github.com/go-openapi/jsonreference v0.21.4 // indirect github.com/go-openapi/swag v0.25.4 // indirect github.com/go-openapi/swag/cmdutils v0.25.4 // indirect github.com/go-openapi/swag/conv v0.25.4 // indirect github.com/go-openapi/swag/fileutils v0.25.4 // indirect github.com/go-openapi/swag/jsonname v0.25.4 // indirect github.com/go-openapi/swag/jsonutils v0.25.4 // indirect github.com/go-openapi/swag/loading v0.25.4 // indirect github.com/go-openapi/swag/mangling v0.25.4 // indirect github.com/go-openapi/swag/netutils v0.25.4 // indirect github.com/go-openapi/swag/stringutils v0.25.4 // indirect github.com/go-openapi/swag/typeutils v0.25.4 // indirect github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.1 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jlaffaye/ftp v0.2.0 // indirect github.com/jmoiron/sqlx v1.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.4 // indirect github.com/kr/fs v0.1.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lib/pq v1.11.2 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.20 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/spdystream v0.5.0 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/modelcontextprotocol/go-sdk v1.4.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.20.0 // indirect github.com/rubenv/sql-migrate v1.8.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/segmentio/asm v1.2.1 // indirect github.com/segmentio/encoding v0.5.3 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/vishvananda/netns v0.0.5 // indirect github.com/winfsp/cgofuse v1.6.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect golang.org/x/mod v0.33.0 // indirect golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/text v0.34.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.42.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.35.2 // indirect k8s.io/apiserver v0.35.2 // indirect k8s.io/component-base v0.35.2 // indirect k8s.io/component-helpers v0.35.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 // indirect k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect oras.land/oras-go/v2 v2.6.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/kustomize/api v0.21.1 // indirect sigs.k8s.io/kustomize/kyaml v0.21.1 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect ) replace github.com/telepresenceio/telepresence/rpc/v2 => ./rpc replace github.com/telepresenceio/telepresence/cmd/cobraparser/v2 => ./cmd/cobraparser // Awaits https://github.com/caarlos0/env/pull/401 replace github.com/caarlos0/env/v11 => github.com/thallgren/env/v11 v11.0.0-20260107112108-5d5593a09332 ================================================ FILE: go.sum ================================================ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/alexflint/go-filemutex v1.3.0 h1:LgE+nTUWnQCyRKbpoceKZsPQbs84LivvgwUymZXdOcM= github.com/alexflint/go-filemutex v1.3.0/go.mod h1:U0+VA/i30mGBlLCrFPGtTe9y6wGQfNAWPBTekHQ+c8A= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80= github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/compose-spec/compose-go/v2 v2.10.1 h1:mFbXobojGRFIVi1UknrvaDAZ+PkJfyjqkA1yseh+vAU= github.com/compose-spec/compose-go/v2 v2.10.1/go.mod h1:Ohac1SzhO/4fXXrzWIztIVB6ckmKBv1Nt5Z5mGVESUg= github.com/containerd/containerd v1.7.30 h1:/2vezDpLDVGGmkUXmlNPLCCNKHJ5BbC5tJB5JNzQhqE= github.com/containerd/containerd v1.7.30/go.mod h1:fek494vwJClULlTpExsmOyKCMUAbuVjlFsJQc4/j44M= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc= github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/datawire/argo-rollouts-go-client v0.0.0-20241216133646-cb1073556c99 h1:a+yPIx3r59bp9OnMM/CMgCleWGreM9bfIHPUatvlMJk= github.com/datawire/argo-rollouts-go-client v0.0.0-20241216133646-cb1073556c99/go.mod h1:2O7ijdMXY8T19xQXtgMYQYJwWgudnB9n358O00YqSms= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM= github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fclairamb/ftpserverlib v0.30.0 h1:caB9sDn1Au//q0j2ev/icPn388qPuk4k1ajSvglDcMQ= github.com/fclairamb/ftpserverlib v0.30.0/go.mod h1:QmogtltTOgkihyKza0GNo37Mu4AEzbJ+sH6W9Y0MBIQ= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 h1:vymEbVwYFP/L05h5TKQxvkXoKxNvTpjxYKdF1Nlwuao= github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb h1:PGufWXXDq9yaev6xX1YQauaO1MV90e6Mpoq1I7Lz/VM= github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb/go.mod h1:QiyDdbZLaJ/mZP4Zwc9g2QsfaEA4o7XvvgZegSci5/E= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg= github.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uTnspI= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modelcontextprotocol/go-sdk v1.4.0 h1:u0kr8lbJc1oBcawK7Df+/ajNMpIDFE41OEPxdeTLOn8= github.com/modelcontextprotocol/go-sdk v1.4.0/go.mod h1:Nxc2n+n/GdCebUaqCOhTetptS17SXXNu9IfNTaLDi1E= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/njayp/ophis v1.1.4 h1:6aq3UdU6MlGIjpg//IzKc2yIxHYMf6Rg1A5MLovM9gg= github.com/njayp/ophis v1.1.4/go.mod h1:F9KEnfeCJzCWo3e0JP2QqSCN04bXptXl1ico5lcLeYg= github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q= github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo= github.com/puzpuzpuz/xsync/v4 v4.4.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0= github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo= github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w= github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/telepresenceio/clog v0.0.0-20260114221933-287514cf9831 h1:STun1hGf7oDZqRChINrQekn2EfypqhB8z67WlCZGHoU= github.com/telepresenceio/clog v0.0.0-20260114221933-287514cf9831/go.mod h1:UNeuJUkHiI0y2x59WmgSNMTTCYLMU12U5AUHhFi3BOA= github.com/telepresenceio/go-ftpserver v1.2.1 h1:iv33eT7r2yptzfVU9ONj5BbZkZ3HStN6NaHZN99dBIQ= github.com/telepresenceio/go-ftpserver v1.2.1/go.mod h1:PWBrLTzbsYc4hi1mleS3MJmVyJkHMSc4Q5EAzNqB+qQ= github.com/telepresenceio/go-fuseftp v1.0.1 h1:Wrf/eefwBGlrIRXDS/fJy/PUYNLdOe8ZmYi7PsydjdM= github.com/telepresenceio/go-fuseftp v1.0.1/go.mod h1:9boAs9FH5+11hAaSMqQmuXPfFd6MpZPwfqIwWXj2868= github.com/telepresenceio/go-fuseftp/rpc v1.0.1 h1:gX85jmaIZRCzprFikoZYKTZEQMHNXmYOG0TwnLHAhwk= github.com/telepresenceio/go-fuseftp/rpc v1.0.1/go.mod h1:3mOKgPhyX0r+NGG9QK7VPM0Cf3M52V9AeL9vpZpvZ+U= github.com/thallgren/env/v11 v11.0.0-20260107112108-5d5593a09332 h1:0f3badT5ei3YJLU2oiDMWJzsD1xj3/Zy+figN91tFwA= github.com/thallgren/env/v11 v11.0.0-20260107112108-5d5593a09332/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/winfsp/cgofuse v1.6.0 h1:re3W+HTd0hj4fISPBqfsrwyvPFpzqhDu8doJ9nOPDB0= github.com/winfsp/cgofuse v1.6.0/go.mod h1:uxjoF2jEYT3+x+vC2KJddEGdk/LU8pRowXmyVMHSV5I= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w= go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk= go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU= go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190529164535-6a60838ec259/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto v0.0.0-20241209162323-e6fa225c2576 h1:k48HcZ4FE6in0o8IflZCkc1lTc2u37nhGd8P+fo4r24= google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 h1:Zy8IV/+FMLxy6j6p87vk/vQGKcdnbprwjTxc8UiUtsA= gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q= helm.sh/helm/v3 v3.20.0 h1:2M+0qQwnbI1a2CxN7dbmfsWHg/MloeaFMnZCY56as50= helm.sh/helm/v3 v3.20.0/go.mod h1:rTavWa0lagZOxGfdhu4vgk1OjH2UYCnrDKE2PVC4N0o= k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw= k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60= k8s.io/apiextensions-apiserver v0.35.2 h1:iyStXHoJZsUXPh/nFAsjC29rjJWdSgUmG1XpApE29c0= k8s.io/apiextensions-apiserver v0.35.2/go.mod h1:OdyGvcO1FtMDWQ+rRh/Ei3b6X3g2+ZDHd0MSRGeS8rU= k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8= k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/apiserver v0.35.2 h1:rb52v0CZGEL0FkhjS+I6jHflAp7fZ4MIaKcEHX7wmDk= k8s.io/apiserver v0.35.2/go.mod h1:CROJUAu0tfjZLyYgSeBsBan2T7LUJGh0ucWwTCSSk7g= k8s.io/cli-runtime v0.35.2 h1:3DNctzpPNXavqyrm/FFiT60TLk4UjUxuUMYbKOE970E= k8s.io/cli-runtime v0.35.2/go.mod h1:G2Ieu0JidLm5m1z9b0OkFhnykvJ1w+vjbz1tR5OFKL0= k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o= k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g= k8s.io/component-base v0.35.2 h1:btgR+qNrpWuRSuvWSnQYsZy88yf5gVwemvz0yw79pGc= k8s.io/component-base v0.35.2/go.mod h1:B1iBJjooe6xIJYUucAxb26RwhAjzx0gHnqO9htWIX+0= k8s.io/component-helpers v0.35.2 h1:7Ea4CDgHnyOGrl3ZhD8e46SdTyf1itTONnreJ2Q52UM= k8s.io/component-helpers v0.35.2/go.mod h1:ybIoc8i92FG7xJFrBcEMzB8ul1wlZgfF0I4Z9w0V6VQ= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 h1:HhDfevmPS+OalTjQRKbTHppRIz01AWi8s45TMXStgYY= k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/kubectl v0.35.2 h1:aSmqhSOfsoG9NR5oR8OD5eMKpLN9x8oncxfqLHbJJII= k8s.io/kubectl v0.35.2/go.mod h1:+OJC779UsDJGxNPbHxCwvb4e4w9Eh62v/DNYU2TlsyM= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= sigs.k8s.io/kustomize/api v0.21.1/go.mod h1:f3wkKByTrgpgltLgySCntrYoq5d3q7aaxveSagwTlwI= sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI= sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= ================================================ FILE: integration_test/agent_injector_disabled_test.go ================================================ package integration_test import ( "os" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/agentconfig" ) type agentInjectorDisabledSuite struct { itest.Suite itest.NamespacePair logName string } func (s *agentInjectorDisabledSuite) SuiteName() string { return "AgentInjectorDisabled" } func init() { itest.AddNamespacePairSuite("", func(h itest.NamespacePair) itest.TestingSuite { return &agentInjectorDisabledSuite{Suite: itest.Suite{Harness: h}, NamespacePair: h} }) } func (s *agentInjectorDisabledSuite) SetupSuite() { s.Suite.SetupSuite() s.logName = s.TelepresenceHelmInstallOK(s.Context(), false, "--set", "agentInjector.enabled=false") } func (s *agentInjectorDisabledSuite) TearDownSuite() { s.UninstallTrafficManager(s.Context(), s.ManagerNamespace()) } func (s *agentInjectorDisabledSuite) Test_AgentInjectorDisabled() { const svc = "echo-easy" ctx := s.Context() s.ApplyApp(ctx, svc, "deploy/"+svc) defer s.DeleteSvcAndWorkload(ctx, "deploy", svc) s.TelepresenceConnect(ctx) _, sErr, err := itest.Telepresence(ctx, "intercept", svc) s.Error(err) s.Contains(sErr, "agent-injector is disabled") itest.TelepresenceQuitOk(ctx) logData, err := os.ReadFile(s.logName) s.Require().NoError(err) logs := string(logData) s.NotContains(logs, "Using traffic-agent image") s.Contains(logs, "Cluster domain derived from /etc/resolv.conf") } func (s *agentInjectorDisabledSuite) Test_VersionWithAgentInjectorDisabled() { ctx := s.Context() rq := s.Require() restartCount := func() int { pods := itest.RunningPods(ctx, agentconfig.ManagerAppName, s.ManagerNamespace()) if len(pods) == 1 { for _, cs := range pods[0].Status.ContainerStatuses { if cs.Name == agentconfig.ManagerAppName { return int(cs.RestartCount) } } } return -1 } oldRestartCount := restartCount() rq.GreaterOrEqual(oldRestartCount, 0) s.TelepresenceConnect(ctx) sr, err := itest.TelepresenceStatus(ctx) itest.TelepresenceQuitOk(ctx) rq.NoError(err) tm := sr.TrafficManager rq.NotNil(tm) rq.Empty(tm.TrafficAgent) // Verify that traffic-manager didn't crash rq.Equal(oldRestartCount, restartCount()) } func (s *agentInjectorDisabledSuite) Test_ManualAgent() { testManualAgent(&s.Suite, s.NamespacePair) } ================================================ FILE: integration_test/also_proxy_test.go ================================================ package integration_test import ( "bufio" "fmt" "io" "os" "path/filepath" "strings" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" ) func (s *notConnectedSuite) Test_AlsoProxy32() { const ipToTest = "10.10.74.1" ctx := s.Context() s.TelepresenceConnect(ctx, "--also-proxy", ipToTest+"/32", "--name", "ax") itest.TelepresenceOk(ctx, "loglevel", "trace") defer itest.TelepresenceQuitOk(ctx) defer itest.TelepresenceOk(ctx, "loglevel", "debug") rq := s.Require() logFile := filepath.Join(filelocation.AppUserLogDir(s.Context()), "daemon.log") rootLog, err := os.Open(logFile) rq.NoError(err) defer rootLog.Close() // Figure out where the current end of the logfile is. This must be done before any // of the tests run because the queries that the DNS resolver receives are dependent // on how the system's DNS resolver handles search paths and caching. st, err := rootLog.Stat() rq.NoError(err) pos := st.Size() // Make an attempt to curl the also-proxied IP. The attempt will fail (there's nothing at the // other end), and that's OK. We're just interested in seeing it logged. _, _ = itest.Output(ctx, "curl", "--silent", "--max-time", "1", ipToTest) //nolint:dogsled // X // Verify that the attempt is visible in the root log. _, err = rootLog.Seek(pos, io.SeekStart) rq.NoError(err) scn := bufio.NewScanner(rootLog) found := false // mustHaveWanted caters for cases where the default behavior from the system's resolver // is to not send unwanted queries to our resolver at all (based on search and routes). // It is forced to true for inclusion tests. strToFind := fmt.Sprintf("%s:80, code STREAM_INFO", ipToTest) for scn.Scan() { txt := scn.Text() if strings.Contains(txt, strToFind) { found = true break } } s.Truef(found, "Unable to find %q", strToFind) } ================================================ FILE: integration_test/argo_rollouts_test.go ================================================ package integration_test import ( "context" "fmt" "io" "net/http" "os" "path/filepath" "runtime" "strings" "time" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" ) type argoRolloutsSuite struct { itest.Suite itest.TrafficManager } func (s *argoRolloutsSuite) SuiteName() string { return "ArgoRollouts" } func init() { itest.AddTrafficManagerSuite("-argo-rollouts", func(h itest.TrafficManager) itest.TestingSuite { return &argoRolloutsSuite{Suite: itest.Suite{Harness: h}, TrafficManager: h} }) } func (s *argoRolloutsSuite) SetupSuite() { s.Suite.SetupSuite() ctx := s.Context() rq := s.Require() itest.CreateNamespaces(ctx, "argo-rollouts") arExe := filepath.Join(itest.BuildOutput(ctx), "bin", "kubectl-argo-rollouts") if runtime.GOOS == "windows" { arExe += ".exe" } if _, err := os.Stat(arExe); err != nil { rq.ErrorIs(err, os.ErrNotExist) rq.NoError(downloadKubectlArgoRollouts(ctx, arExe)) } out, err := itest.KubectlOut(ctx, "", "argo", "rollouts", "version") rq.NoError(err) clog.Info(ctx, out) rq.NoError(itest.Kubectl(ctx, "argo-rollouts", "apply", "-f", "https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml")) } func (s *argoRolloutsSuite) TearDownSuite() { itest.DeleteNamespaces(s.Context(), "argo-rollouts") } func downloadKubectlArgoRollouts(ctx context.Context, arExe string) error { du := fmt.Sprintf( "https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-%s-%s", runtime.GOOS, runtime.GOARCH) clog.Infof(ctx, "Downloading %s", du) resp, err := http.Get(du) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("expected status 200 OK, got %v", resp.Status) } arExeFile, err := os.OpenFile(arExe, os.O_WRONLY|os.O_CREATE, 0o755) if err != nil { return err } _, err = io.Copy(arExeFile, resp.Body) _ = arExeFile.Close() return err } func (s *argoRolloutsSuite) Test_SuccessfullyInterceptsArgoRollout() { ctx := s.Context() require := s.Require() s.TelepresenceHelmInstallOK(ctx, true, "--set", "workloads.argoRollouts.enabled=true") defer s.RollbackTM(ctx) tp, svc, port := "Rollout", "echo-argo-rollout", "9094" s.ApplyApp(ctx, svc, strings.ToLower(tp)+"/"+svc) defer s.DeleteSvcAndWorkload(ctx, "rollout", svc) s.TelepresenceConnect(ctx) defer itest.TelepresenceDisconnectOk(ctx) require.Eventually( func() bool { stdout, _, err := itest.Telepresence(ctx, "list") return err == nil && strings.Contains(stdout, svc) }, 6*time.Second, // waitFor 2*time.Second, // polling interval ) stdout := itest.TelepresenceOk(ctx, "intercept", "--mount", "false", "--port", port, svc) require.Contains(stdout, "Using "+tp+" "+svc) require.Eventually(func() bool { stdout = itest.TelepresenceOk(ctx, "list", "--intercepts") return strings.Contains(stdout, svc+": intercepted") && !strings.Contains(stdout, "Volume Mount Point") }, 14*time.Second, 2*time.Second) s.CapturePodLogs(ctx, svc, "traffic-agent", s.AppNamespace()) itest.TelepresenceOk(ctx, "leave", svc) stdout = itest.TelepresenceOk(ctx, "list", "--intercepts") require.NotContains(stdout, svc+": intercepted") if !s.ClientIsVersion(">2.21.x") && s.ManagerIsVersion(">2.21.x") { // An <2.22.0 client will not be able to uninstall an agent when the traffic-manager is >=2.22.0 // because the client will attempt to remove the entry in the telepresence-agents configmap. It // is no longer present in versions >=2.22.0 return } time.Sleep(3 * time.Second) itest.TelepresenceOk(ctx, "uninstall", svc) require.Eventually( func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--agents") return err == nil && !strings.Contains(stdout, svc) }, 180*time.Second, // waitFor 6*time.Second, // polling interval ) } func (s *argoRolloutsSuite) Test_ListsReplicaSetWhenRolloutDisabled() { ctx := s.Context() require := s.Require() tp, svc := "Rollout", "echo-argo-rollout" s.ApplyApp(ctx, svc, strings.ToLower(tp)+"/"+svc) defer s.DeleteSvcAndWorkload(ctx, "rollout", svc) s.TelepresenceConnect(ctx) defer itest.TelepresenceDisconnectOk(ctx) require.Eventually( func() bool { stdout, _, err := itest.Telepresence(ctx, "list") clog.Info(ctx, stdout) return err == nil && strings.Contains(stdout, svc+"-") }, 6*time.Second, // waitFor 2*time.Second, // polling interval ) } ================================================ FILE: integration_test/bind_to_podip_test.go ================================================ package integration_test import ( "fmt" "path/filepath" "strconv" "time" core "k8s.io/api/core/v1" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/annotation" ) func (s *connectedSuite) Test_BindToPodIP() { const svcPfx = "echo-server" tplPath := filepath.Join("testdata", "k8s", "generic.goyaml") for i, targetPort := range []string{"8080", "http"} { s.Run("TargetPort:"+targetPort, func() { ctx := s.Context() svc := fmt.Sprintf("%s-%d", svcPfx, i) svcPort, cancel := itest.StartLocalHttpEchoServer(ctx, svc) defer cancel() tpl := &itest.Generic{ Name: svc, TargetPort: targetPort, Registry: "ghcr.io/telepresenceio", Image: "echo-server:latest", Environment: []core.EnvVar{ { Name: "PORTS", Value: "8080", }, { Name: "LISTEN_ADDRESS", ValueFrom: &core.EnvVarSource{ FieldRef: &core.ObjectFieldSelector{ FieldPath: "status.podIP", }, }, }, }, Annotations: map[string]string{ annotation.InjectTrafficAgent: "enabled", }, } s.ApplyTemplate(ctx, tplPath, tpl) rq := s.Require() rq.NoError(s.RolloutStatusWait(ctx, "deploy/"+svc)) defer s.DeleteTemplate(ctx, tplPath, tpl) stdout := itest.TelepresenceOk(ctx, "intercept", "--mount", "false", svc, "--port", strconv.Itoa(svcPort)) rq.Contains(stdout, "Using Deployment "+svc) itest.PingInterceptedEchoServer(ctx, svc, "80") itest.TelepresenceOk(ctx, "leave", svc) // Ensure that we now reach the original app again. s.Eventually(func() bool { out, err := itest.Output(ctx, "curl", "--verbose", "--max-time", "0.5", svc) clog.Infof(ctx, "Received %s", out) if err != nil { clog.Errorf(ctx, "curl error %v", err) return false } return true }, 30*time.Second, 2*time.Second, "Pod app is not reachable after ending the intercept") }) } } ================================================ FILE: integration_test/cidr_conflict_test.go ================================================ package integration_test import ( "fmt" "net" "net/netip" "os" "path/filepath" "runtime" "github.com/go-json-experiment/json" core "k8s.io/api/core/v1" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/annotation" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/slice" ) type cidrConflictSuite struct { itest.Suite itest.TrafficManager vipSubnet netip.Prefix subnets []netip.Prefix scripts string } func (s *cidrConflictSuite) SuiteName() string { return "CIDRConflict" } func init() { itest.AddTrafficManagerSuite("", func(h itest.TrafficManager) itest.TestingSuite { return &cidrConflictSuite{Suite: itest.Suite{Harness: h}, TrafficManager: h} }) } func (s *cidrConflictSuite) SetupSuite() { if runtime.GOOS != "linux" { s.T().Skip("we can only create veth interfaces on linux") } const svc = "echo" s.Suite.SetupSuite() tpl := &itest.Generic{ Name: svc, Registry: "ghcr.io/telepresenceio", Image: "echo-server:latest", Environment: []core.EnvVar{ { Name: "PORTS", Value: "8080", }, { Name: "LISTEN_ADDRESS", ValueFrom: &core.EnvVarSource{ FieldRef: &core.ObjectFieldSelector{ FieldPath: "status.podIP", }, }, }, }, Annotations: map[string]string{ annotation.InjectTrafficAgent: "enabled", }, } s.ApplyTemplate(s.Context(), filepath.Join("testdata", "k8s", "generic.goyaml"), &tpl) s.NoError(s.RolloutStatusWait(s.Context(), "deploy/echo")) ctx := s.Context() s.TelepresenceConnect(ctx) st := itest.TelepresenceStatusOk(ctx) itest.TelepresenceQuit(ctx) s.subnets = st.RootDaemon.Subnets if len(s.subnets) < 2 { s.T().Skip("Test cannot run unless client maps at least two subnets") } var err error s.scripts, err = filepath.Abs(filepath.Join("testdata", "scripts")) if s.NoError(err) { // Create an interface that will be in conflict with the service and pod subnets. s.NoError(itest.Run(ctx, "sudo", filepath.Join(s.scripts, "veth-up.sh"), s.subnets[0].String(), s.subnets[1].String())) s.NoError(err) } s.vipSubnet = client.GetConfig(ctx).Routing().VirtualSubnet } func (s *cidrConflictSuite) TearDownSuite() { ctx := s.Context() s.NoError(itest.Run(ctx, "sudo", filepath.Join(s.scripts, "veth-down.sh"), s.subnets[0].String(), s.subnets[1].String())) s.DeleteSvcAndWorkload(ctx, "deploy", "echo") } func (s *cidrConflictSuite) Test_AutoConflictResolution() { ctx := s.Context() s.TelepresenceConnect(ctx) st := itest.TelepresenceStatusOk(ctx) defer itest.TelepresenceQuit(ctx) sns := st.RootDaemon.Subnets rq := s.Require() rq.Less(len(sns), len(s.subnets), "pod and service subnets should be combined into one virtual subnet") // The first subnet must now be virtual. viSn := sns[0] rq.Equalf(s.vipSubnet, viSn, "expected %s to be a virtual CIDR", viSn) // Ingest to get a container environment. envFile := filepath.Join(s.T().TempDir(), "echo.env") itest.TelepresenceOk(ctx, "ingest", "echo", "--env-file", envFile, "--env-syntax", "json") itest.TelepresenceOk(ctx, "leave", "echo") var env map[string]string envData, err := os.ReadFile(envFile) rq.NoError(err) err = json.Unmarshal(envData, &env) rq.NoError(err) // Verify that these IPs in the environment have been translated into virtual IPs. for _, key := range []string{"LISTEN_ADDRESS", "ECHO_SERVICE_HOST"} { addrVal, ok := env[key] rq.True(ok) addr, err := netip.ParseAddr(addrVal) rq.NoError(err) rq.Truef(viSn.Contains(addr), "virtual subnet %s does not contain %s %s", viSn, key, addr) } } func (s *cidrConflictSuite) Test_AutoConflictAvoidance() { ctx := s.Context() s.TelepresenceConnect(ctx, "--allow-conflicting-subnets", fmt.Sprintf("%s,%s", s.subnets[0], s.subnets[1])) st := itest.TelepresenceStatusOk(ctx) defer itest.TelepresenceQuitOk(ctx) sns := st.RootDaemon.Subnets s.Require().Equal(slice.AsStrings(s.subnets), slice.AsStrings(sns), "subnet conflict should not be resolved using VNAT") } func (s *cidrConflictSuite) Test_AutoConflictResolution_CloudDisable() { ctx := s.Context() s.TelepresenceHelmInstallOK(ctx, true, "--set", "client.routing.autoResolveConflicts=false") defer s.RollbackTM(ctx) _, err := s.TelepresenceTryConnect(ctx) s.Require().Error(err) } func (s *cidrConflictSuite) Test_AutoConflictResolution_ClientDisable() { ctx := itest.WithConfig(s.Context(), func(cfg client.Config) { cfg.Routing().AutoResolveConflicts = false }) _, err := s.TelepresenceTryConnect(ctx) s.Require().Error(err) } func (s *cidrConflictSuite) Test_AllowConflictResolution() { ctx := itest.WithConfig(s.Context(), func(cfg client.Config) { cfg.Routing().AutoResolveConflicts = false cfg.Routing().AllowConflicting = s.subnets }) testIP := net.IP(s.subnets[0].Addr().AsSlice()) testIP[len(testIP)-1] = 37 // Verify that a route in the conflicting subnet is routed via brm out, err := itest.Output(ctx, "ip", "route", "get", testIP.String()) rq := s.Require() rq.NoError(err) rq.Contains(out, "dev brm") s.TelepresenceConnect(ctx) defer itest.TelepresenceQuitOk(ctx) st := itest.TelepresenceStatusOk(ctx) defer itest.TelepresenceQuitOk(ctx) sns := st.RootDaemon.Subnets rq.Equal(sns, s.subnets, "Subnets should not change but %v != %v", sns, s.subnets) // Verify that a route in the conflicting subnet is routed via Telepresence out, err = itest.Output(ctx, "ip", "route", "get", testIP.String()) rq.NoError(err) rq.Contains(out, "dev tel0") // tel0 is OK, we only run this on linux } ================================================ FILE: integration_test/cli_test.go ================================================ package integration_test import ( "context" "fmt" "regexp" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/client" ) type cliSuite struct { itest.Suite } func (s *cliSuite) SuiteName() string { return "CLI" } func init() { itest.AddClusterSuite(func(ctx context.Context) itest.TestingSuite { return &cliSuite{Suite: itest.Suite{Harness: itest.NewContextHarness(ctx)}} }) } func (s *cliSuite) Test_Version() { stdout, stderr, err := itest.Telepresence(s.Context(), "version") if err != nil { s.SetGeneralError(fmt.Errorf("bailing out. If telepresence version isn't working, nothing will: %w", err)) s.Require().NoError(err) } s.Empty(stderr) s.Regexp(fmt.Sprintf(`Client\s*: v%s`, regexp.QuoteMeta(s.ClientVersion().String())), stdout) } func (s *cliSuite) Test_VersionWithInvalidKubeContext() { stdout, _, err := itest.Telepresence(itest.WithEnv(s.Context(), map[string]string{ "KUBECONFIG": "file-that-does-not-exist", }), "version") if err != nil { s.Require().NoError(err) } s.Regexp(fmt.Sprintf(`Client\s*: v%s`, regexp.QuoteMeta(s.ClientVersion().String())), stdout) } func (s *cliSuite) Test_Help() { // TODO: Fix these tests s.T().Skip("these tests don't work") const ( helpHead = `Telepresence can connect to a cluster and route all outbound traffic` usageHead = `Usage:` ) stdout, stderr, err := itest.Telepresence(s.Context(), "help") if err != nil { s.SetGeneralError(fmt.Errorf("bailing out. If telepresence help isn't working, nothing will: %w", err)) s.Require().NoError(err) } s.Empty(stderr) s.Contains(stdout, helpHead) s.Contains(stdout, usageHead) stdout, stderr, err = itest.Telepresence(s.Context(), "--help") if err != nil { s.SetGeneralError(fmt.Errorf("bailing out. If telepresence --help isn't working, nothing will: %w", err)) s.Require().NoError(err) } s.Empty(stderr) s.Contains(stdout, helpHead) s.Contains(stdout, usageHead) stdout, stderr, err = itest.Telepresence(s.Context(), "-h") if err != nil { s.SetGeneralError(fmt.Errorf("bailing out. If telepresence --help isn't working, nothing will: %w", err)) s.Require().NoError(err) } s.Empty(stderr) s.Contains(stdout, helpHead) s.Contains(stdout, usageHead) stdout, stderr, err = itest.Telepresence(s.Context(), "") if err != nil { s.SetGeneralError(fmt.Errorf("bailing out. If telepresence --help isn't working, nothing will: %w", err)) s.Require().NoError(err) } s.Empty(stderr) s.Contains(stdout, helpHead) s.Contains(stdout, usageHead) } func (s *cliSuite) Test_Status() { itest.TelepresenceQuitOk(s.Context()) stdout, stderr, err := itest.Telepresence(s.Context(), "status") if err != nil { s.SetGeneralError(fmt.Errorf("bailing out. If telepresence status isn't working, nothing will: %w", err)) s.Require().NoError(err) } s.NoError(err) s.Empty(stderr) s.Contains(stdout, "Root Daemon: Not running") s.Contains(stdout, "User Daemon: Not running") } func (s *cliSuite) Test_StatusWithJSON() { itest.TelepresenceQuitOk(s.Context()) status, err := itest.TelepresenceStatus(s.Context()) if err != nil { s.SetGeneralError(fmt.Errorf("bailing out. If telepresence status isn't working, nothing will: %w", err)) s.Require().NoError(err) } s.False(status.RootDaemon.Running) s.False(status.UserDaemon.Running) } func (s *cliSuite) Test_ConfigViewClientOnly() { ctx := itest.WithConfig(s.Context(), func(c client.Config) { c.Timeouts().PrivateConnectivityCheck = 0 }) out := itest.TelepresenceOk(ctx, "config", "view", "--client-only") // Ensure that zero (but not default) values are included in output s.Regexp(regexp.MustCompile(`\sconnectivityCheck\s*:\s*0s\n`), out) } ================================================ FILE: integration_test/cloud_config_test.go ================================================ package integration_test import ( "bufio" "errors" "fmt" "io/fs" "log/slog" "net/netip" "os" "path/filepath" "regexp" "runtime" "strconv" "strings" "time" "k8s.io/client-go/tools/clientcmd" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" "github.com/telepresenceio/telepresence/v2/pkg/json" ) func (s *notConnectedSuite) Test_CloudNeverProxy() { require := s.Require() ctx := s.Context() svcName := "echo-never-proxy" itest.ApplyEchoService(ctx, svcName, s.AppNamespace(), 8080) defer itest.DeleteSvcAndWorkload(ctx, "deploy", svcName, s.AppNamespace()) ipStr, err := itest.Output(ctx, "kubectl", "--namespace", s.AppNamespace(), "get", "svc", svcName, "-o", "jsonpath={.spec.clusterIP}") require.NoError(err) ip, err := netip.ParseAddr(ipStr) require.NoError(err) if ip.IsLoopback() { s.T().Skipf("test can't run on host with a loopback cluster IP %s", ip) } mask := 32 if s.IsIPv6() { mask = 128 } kc := itest.KubeConfig(ctx) cfg, err := clientcmd.LoadFromFile(kc) require.NoError(err) ktx := cfg.Contexts[cfg.CurrentContext] require.NotNil(ktx, "unable to get current context from config") cluster := cfg.Clusters[ktx.Cluster] require.NotNil(cluster, "unable to get %s cluster from config", ktx.Cluster) ips, err := getClusterIPs(cluster) require.NoError(err) s.TelepresenceHelmInstallOK(ctx, true, "--set", fmt.Sprintf("client.routing.neverProxySubnets={%s/%d}", ip, mask)) defer s.RollbackTM(ctx) timeout := 20 * time.Second if runtime.GOOS == "windows" { timeout *= 5 } s.Eventuallyf(func() bool { defer func() { stdout, stderr, err := itest.Telepresence(ctx, "quit") clog.Infof(ctx, "stdout: %q", stdout) clog.Infof(ctx, "stderr: %q", stderr) if err != nil { clog.Error(ctx, err) } }() stdout, stderr, err := itest.Telepresence(ctx, "connect", "--namespace", s.AppNamespace(), "--manager-namespace", s.ManagerNamespace()) clog.Infof(ctx, "stdout: %q", stdout) clog.Infof(ctx, "stderr: %q", stderr) if err != nil { clog.Error(ctx, err) return false } neverProxiedCount := 1 // The cluster's IP address will be never proxied unless it's a loopback, so we gotta account for that. for _, cip := range ips { if !cip.IsLoopback() { neverProxiedCount++ } } stdout, stderr, err = itest.Telepresence(ctx, "status") clog.Infof(ctx, "stdout: %q", stdout) clog.Infof(ctx, "stderr: %q", stderr) if err != nil { clog.Error(ctx, err) return false } m := regexp.MustCompile(`Never Proxy\s*:\s*\((\d+) subnets\)`).FindStringSubmatch(stdout) npcOk := false if m != nil { npc, _ := strconv.Atoi(m[1]) npcOk = npc > 0 && npc <= neverProxiedCount } if !npcOk { clog.Errorf(ctx, "did not find 1-%d never-proxied subnets", neverProxiedCount) return false } view, err := s.configView() require.NoError(err) npc := len(view.Config.Routing().NeverProxy) npcOk = npc > 0 && npc <= neverProxiedCount if !npcOk { clog.Errorf(ctx, "did not find 1-%d never-proxied subnets in json status", neverProxiedCount) return false } if itest.Run(ctx, "curl", "--silent", "--max-time", "0.5", ip.String()) == nil { clog.Errorf(ctx, "never-proxied IP %s is reachable", ip) return false } clog.Infof(ctx, "Success! Never-proxied IP %s is not reachable", ip) return true }, timeout, 5*time.Second, "never-proxy not updated in %s", timeout) } func (s *notConnectedSuite) Test_CloudAllowConflicting() { require := s.Require() ctx := s.Context() acs, err := netip.ParsePrefix("10.88.2.4/30") require.NoError(err) s.TelepresenceHelmInstallOK(ctx, true, "--set", fmt.Sprintf("client.routing.allowConflictingSubnets={%s}", acs)) defer s.RollbackTM(ctx) timeout := 20 * time.Second if runtime.GOOS == "windows" { timeout *= 5 } s.Eventuallyf(func() bool { defer func() { stdout, stderr, err := itest.Telepresence(ctx, "quit") clog.Infof(ctx, "stdout: %q", stdout) clog.Infof(ctx, "stderr: %q", stderr) if err != nil { clog.Error(ctx, err) } }() s.TelepresenceConnect(ctx) sr, err := itest.TelepresenceStatus(ctx) if err != nil { return false } ac := sr.RootDaemon.AllowConflicting if len(ac) != 1 { return false } return len(ac) == 1 && netip.MustParsePrefix(ac[0].String()) == acs }, timeout, 5*time.Second, "allow-conflicting-subnets not updated in %s", timeout) } func (s *notConnectedSuite) configView() (*client.SessionConfig, error) { stdout, _, err := itest.Telepresence(s.Context(), "config", "view", "--output", "json") if err != nil { return nil, err } var view client.SessionConfig err = json.Unmarshal([]byte(stdout), &view, false) return &view, err } func (s *notConnectedSuite) Test_CloudAgentArrival() { ctx := s.Context() const agentArrivalTimeout = 2 * time.Minute s.TelepresenceHelmInstallOK(ctx, true, "--set", fmt.Sprintf("timeouts.agentArrival=%s", agentArrivalTimeout)) defer s.RollbackTM(ctx) timeout := 20 * time.Second if runtime.GOOS == "windows" { timeout *= 5 } s.Eventuallyf(func() bool { defer func() { stdout, stderr, err := itest.Telepresence(ctx, "quit") clog.Infof(ctx, "stdout: %q", stdout) clog.Infof(ctx, "stderr: %q", stderr) if err != nil { clog.Error(ctx, err) } }() s.TelepresenceConnect(ctx) view, err := s.configView() if err != nil { return false } return view.Timeouts().Get(client.TimeoutIntercept) == agentArrivalTimeout }, timeout, 5*time.Second, "timeouts.intercept not updated by changing traffic-manager's timeouts.agentArrival in %s", timeout) } func (s *notConnectedSuite) Test_RootdCloudLogLevel() { require := s.Require() ctx := s.Context() rootLogName := filepath.Join(filelocation.AppUserLogDir(ctx), "daemon.log") // Figure out where the current end of the logfile is. pos := int64(0) st, err := os.Stat(rootLogName) if err != nil { if !errors.Is(err, fs.ErrNotExist) { s.T().Fatalf("Unexpected error stat'ing %s: %v", rootLogName, err) } } else { pos = st.Size() } s.TelepresenceHelmInstallOK(ctx, true, "--set", "logLevel=debug,agent.logLevel=debug,client.logLevels.rootDaemon=trace") defer s.RollbackTM(ctx) ctx = itest.WithConfig(ctx, func(cfg client.Config) { cfg.LogLevels().RootDaemon = slog.LevelInfo }) s.Eventually(func() bool { _, _, err = itest.Telepresence(ctx, "connect", "--namespace", s.AppNamespace(), "--manager-namespace", s.ManagerNamespace()) if err != nil { return false } itest.TelepresenceDisconnectOk(ctx) rootLog, err := os.Open(rootLogName) require.NoError(err) defer rootLog.Close() _, err = rootLog.Seek(pos, 0) require.NoError(err) levelSet := false scn := bufio.NewScanner(rootLog) for scn.Scan() && !levelSet { line := scn.Text() levelSet = strings.Contains(line, `Logging at this level "TRACE"`) pos += int64(len(line)) + 1 } return levelSet }, 60*time.Second, 5*time.Second, "Root log level not updated in 20 seconds") // Make sure the log level was set back after disconnect s.Eventually(func() bool { rootLog, err := os.Open(rootLogName) require.NoError(err) defer rootLog.Close() _, err = rootLog.Seek(pos, 0) require.NoError(err) levelSet := false scn := bufio.NewScanner(rootLog) for scn.Scan() && !levelSet { line := scn.Text() levelSet = strings.Contains(line, `Logging at this level "INFO"`) pos += int64(len(line)) + 1 } return levelSet }, 5*time.Second, time.Second, "Root log level not reset after disconnect") // Set it to a "real" value to see that the client-side wins ctx = itest.WithConfig(ctx, func(config client.Config) { config.LogLevels().RootDaemon = slog.LevelDebug }) s.TelepresenceConnect(ctx) rootLog, err := os.Open(rootLogName) require.NoError(err) defer rootLog.Close() _, err = rootLog.Seek(pos, 0) require.NoError(err) levelSet := false scn := bufio.NewScanner(rootLog) for scn.Scan() && !levelSet { levelSet = strings.Contains(scn.Text(), `Logging at this level "TRACE"`) } itest.TelepresenceDisconnectOk(ctx) require.False(levelSet, "Root log level not respected when set in config file") view, err := s.configView() require.NoError(err) require.Equal(view.LogLevels().RootDaemon, slog.LevelDebug) } func (s *notConnectedSuite) Test_UserdCloudLogLevel() { require := s.Require() ctx := s.Context() userLogName := filepath.Join(filelocation.AppUserLogDir(ctx), "connector.log") // Figure out where the current end of the logfile is. pos := int64(0) st, err := os.Stat(userLogName) if err != nil { if !errors.Is(err, fs.ErrNotExist) { s.T().Fatalf("Unexpected error stat'ing %s: %v", userLogName, err) } } else { pos = st.Size() } s.TelepresenceHelmInstallOK(ctx, true, "--set", "logLevel=debug,agent.logLevel=debug,client.logLevels.userDaemon=trace") defer s.RollbackTM(ctx) ctx = itest.WithConfig(ctx, func(cfg client.Config) { cfg.LogLevels().UserDaemon = slog.LevelInfo }) s.Eventually(func() bool { so, se, err := itest.Telepresence(ctx, "connect", "--manager-namespace", s.ManagerNamespace(), "--namespace", s.AppNamespace()) clog.Infof(ctx, "stdout %s", so) clog.Infof(ctx, "stderr %s", se) if err != nil { clog.Error(ctx, err) return false } itest.TelepresenceDisconnectOk(ctx) logF, err := os.Open(userLogName) require.NoError(err) defer logF.Close() _, err = logF.Seek(pos, 0) require.NoError(err) scn := bufio.NewScanner(logF) levelSet := false for scn.Scan() && !levelSet { line := scn.Text() levelSet = strings.Contains(line, `Logging at this level "TRACE"`) pos += int64(len(line)) + 1 } return levelSet }, 20*time.Second, 5*time.Second, "Connector log level not updated in 20 seconds") // Make sure the log level was set back after disconnect logF, err := os.Open(userLogName) require.NoError(err) _, err = logF.Seek(pos, 0) if !s.NoError(err) { logF.Close() return } scn := bufio.NewScanner(logF) levelSet := false for scn.Scan() && !levelSet { levelSet = strings.Contains(scn.Text(), `Logging at this level "INFO"`) } logF.Close() require.True(levelSet, "Connector log level not reset after disconnect") // Set it to a "real" value to see that the client-side wins ctx = itest.WithConfig(ctx, func(config client.Config) { config.LogLevels().UserDaemon = slog.LevelDebug }) s.TelepresenceConnect(ctx) itest.TelepresenceDisconnectOk(ctx) logF, err = os.Open(userLogName) require.NoError(err) _, err = logF.Seek(pos, 0) if !s.NoError(err) { logF.Close() return } levelSet = false for scn.Scan() && !levelSet { levelSet = strings.Contains(scn.Text(), `Logging at this level "TRACE"`) } logF.Close() require.False(levelSet, "Connector log level not respected when set in config file") } ================================================ FILE: integration_test/compose_test.go ================================================ package integration_test import ( "context" "os" "path/filepath" goRuntime "runtime" "strings" "time" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/client" ) type composeSuite struct { itest.Suite itest.TrafficManager ctx context.Context } func (s *composeSuite) SuiteName() string { return "Compose" } func init() { itest.AddTrafficManagerSuite("", func(h itest.TrafficManager) itest.TestingSuite { return &composeSuite{Suite: itest.Suite{Harness: h}, TrafficManager: h} }) } func (s *composeSuite) SetupSuite() { if s.IsCI() && !(goRuntime.GOOS == "linux" && goRuntime.GOARCH == "amd64") { s.T().Skip("CI can't run linux docker containers inside non-linux runners") return } s.Suite.SetupSuite() s.ctx = itest.WithConfig(s.HarnessContext(), func(cfg client.Config) { cfg.Intercept().UseFtp = false }) } func (s *composeSuite) TearDownTest() { itest.TelepresenceQuitOk(s.Context()) } func (s *composeSuite) Context() context.Context { return itest.WithT(s.ctx, s.T()) } func (s *composeSuite) Test_ComposeDNS() { ctx := s.Context() require := s.Require() const svc = "echo-easy" s.ApplyEchoService(ctx, svc, 80) defer s.DeleteSvcAndWorkload(ctx, "deploy", svc) const svc2 = "echo-other" s.ApplyEchoService(ctx, svc2, 80) defer s.DeleteSvcAndWorkload(ctx, "deploy", svc2) // Write a docker-compose.yml that replaces echo-other and sleeps. composeDir := itest.TempDir(ctx) composeFile := filepath.Join(composeDir, "docker-compose.yml") ns := s.AppNamespace() composeContent := strings.Join([]string{ "x-tele:", " connections:", " - namespace: " + ns, " manager-namespace: " + s.ManagerNamespace(), "services:", " " + svc2 + ":", " x-tele:", " type: replace", " image: busybox", " command: sleep infinity", }, "\n") require.NoError(os.WriteFile(composeFile, []byte(composeContent), 0o644)) _, _, err := itest.Telepresence(ctx, "compose", "-f", composeFile, "up", "-d") require.NoError(err, "compose up failed") defer func() { _, _, _ = itest.Telepresence(ctx, "compose", "-f", composeFile, "down") }() // Verify that cluster DNS resolves inside the compose container. s.Assert().EventuallyContext(ctx, func() bool { so, se, err := itest.Telepresence(ctx, "compose", "-f", composeFile, "exec", svc2, "nslookup", svc) if err == nil && strings.Contains(so, "Address:") { return true } clog.Info(ctx, "nslookup", "sdtout", so, "stderr", se, "err", err) return false }, 30*time.Second, 3*time.Second, "nslookup of %s in compose container should succeed", svc) } func (s *composeSuite) Test_ComposeConnect() { ctx := s.Context() require := s.Require() const svc = "echo-easy" s.ApplyEchoService(ctx, svc, 80) defer s.DeleteSvcAndWorkload(ctx, "deploy", svc) composeDir := itest.TempDir(ctx) composeFile := filepath.Join(composeDir, "docker-compose.yml") ns := s.AppNamespace() composeContent := strings.Join([]string{ "x-tele:", " connections:", " - namespace: " + ns, " manager-namespace: " + s.ManagerNamespace(), "services:", " tester:", " x-tele:", " type: connect", " image: busybox", " command: sleep infinity", }, "\n") require.NoError(os.WriteFile(composeFile, []byte(composeContent), 0o644)) _, _, err := itest.Telepresence(ctx, "compose", "-f", composeFile, "up", "-d") require.NoError(err, "compose up failed") defer func() { _, _, _ = itest.Telepresence(ctx, "compose", "-f", composeFile, "down") }() // Verify HTTP access to a cluster service from the connect container using its single-label name. // The tel2-search DNS search domain causes the resolver to first try "echo-easy.tel2-search", // which Docker's embedded DNS forwards to the Telepresence daemon DNS. The daemon strips the // tel2-search suffix and resolves the bare name against the cluster. s.Assert().EventuallyContext(ctx, func() bool { so, se, err := itest.Telepresence(ctx, "compose", "-f", composeFile, "exec", "tester", "wget", "-qO-", "http://"+svc) if err == nil && len(so) > 0 { return true } clog.Info(ctx, "wget", "stdout", so, "stderr", se, "err", err) return false }, 30*time.Second, 3*time.Second, "wget of %s from connect container should succeed", svc) } func (s *composeSuite) Test_ComposeProxy() { ctx := s.Context() require := s.Require() const svc = "echo-easy" s.ApplyEchoService(ctx, svc, 80) defer s.DeleteSvcAndWorkload(ctx, "deploy", svc) composeDir := itest.TempDir(ctx) composeFile := filepath.Join(composeDir, "docker-compose.yml") ns := s.AppNamespace() composeContent := strings.Join([]string{ "x-tele:", " connections:", " - namespace: " + ns, " manager-namespace: " + s.ManagerNamespace(), "services:", " " + svc + ":", " x-tele:", " type: proxy", " tester:", " x-tele:", " type: connect", " image: busybox", " command: sleep infinity", }, "\n") require.NoError(os.WriteFile(composeFile, []byte(composeContent), 0o644)) _, _, err := itest.Telepresence(ctx, "compose", "-f", composeFile, "up", "-d") require.NoError(err, "compose up failed") defer func() { _, _, _ = itest.Telepresence(ctx, "compose", "-f", composeFile, "down") }() // Verify that HTTP requests are routed through the proxy to the cluster service. s.Assert().EventuallyContext(ctx, func() bool { so, se, err := itest.Telepresence(ctx, "compose", "-f", composeFile, "exec", "tester", "wget", "-qO-", "http://"+svc) if err == nil && len(so) > 0 { return true } clog.Info(ctx, "wget via proxy", "stdout", so, "stderr", se, "err", err) return false }, 30*time.Second, 3*time.Second, "wget of %s through proxy should succeed", svc) } func (s *composeSuite) Test_ComposeIngest() { ctx := s.Context() require := s.Require() const ingestSvc = "ingest-app" itest.ApplyAppTemplate(ctx, s.AppNamespace(), &itest.AppData{ AppName: ingestSvc, Image: "ghcr.io/telepresenceio/echo-server:0.3.1", Ports: []itest.AppPort{ {ServicePortNumber: 80, TargetPortNumber: 8080}, }, Env: map[string]string{ "INGEST_TEST_VAR": "hello-from-cluster", }, }) defer s.DeleteSvcAndWorkload(ctx, "deploy", ingestSvc) composeDir := itest.TempDir(ctx) composeFile := filepath.Join(composeDir, "docker-compose.yml") ns := s.AppNamespace() composeContent := strings.Join([]string{ "x-tele:", " connections:", " - namespace: " + ns, " manager-namespace: " + s.ManagerNamespace(), "services:", " " + ingestSvc + ":", " x-tele:", " type: ingest", " image: busybox", " command: sleep infinity", }, "\n") require.NoError(os.WriteFile(composeFile, []byte(composeContent), 0o644)) _, _, err := itest.Telepresence(ctx, "compose", "-f", composeFile, "up", "-d") require.NoError(err, "compose up failed") defer func() { _, _, _ = itest.Telepresence(ctx, "compose", "-f", composeFile, "down") }() // Verify that the ingest container inherits the environment variable from the cluster workload. s.Assert().EventuallyContext(ctx, func() bool { so, se, err := itest.Telepresence(ctx, "compose", "-f", composeFile, "exec", ingestSvc, "env") if err == nil && strings.Contains(so, "INGEST_TEST_VAR=hello-from-cluster") { return true } clog.Info(ctx, "env check", "stdout", so, "stderr", se, "err", err) return false }, 60*time.Second, 5*time.Second, "ingest container should inherit INGEST_TEST_VAR from cluster workload") } func (s *composeSuite) Test_ComposeIntercept() { ctx := s.Context() require := s.Require() const svc = "echo-easy" s.ApplyEchoService(ctx, svc, 80) defer s.DeleteSvcAndWorkload(ctx, "deploy", svc) composeDir := itest.TempDir(ctx) composeFile := filepath.Join(composeDir, "docker-compose.yml") ns := s.AppNamespace() composeContent := strings.Join([]string{ "x-tele:", " connections:", " - namespace: " + ns, " manager-namespace: " + s.ManagerNamespace(), "services:", " " + svc + ":", " x-tele:", " type: intercept", " ports:", ` - "80:80"`, " image: hashicorp/http-echo", ` command: ["-text=hello-from-compose", "-listen=:80"]`, " tester:", " x-tele:", " type: connect", " image: busybox", " command: sleep infinity", }, "\n") require.NoError(os.WriteFile(composeFile, []byte(composeContent), 0o644)) _, _, err := itest.Telepresence(ctx, "compose", "-f", composeFile, "up", "-d") require.NoError(err, "compose up failed") defer func() { _, _, _ = itest.Telepresence(ctx, "compose", "-f", composeFile, "down") }() // Verify that cluster traffic is intercepted and served by the local compose container. s.Assert().EventuallyContext(ctx, func() bool { so, se, err := itest.Telepresence(ctx, "compose", "-f", composeFile, "exec", "tester", "wget", "-qO-", "http://"+svc) if err == nil && strings.Contains(so, "hello-from-compose") { return true } clog.Info(ctx, "wget intercept", "stdout", so, "stderr", se, "err", err) return false }, 60*time.Second, 5*time.Second, "intercept should redirect cluster traffic to the compose container") } func (s *composeSuite) Test_ComposeReplace() { ctx := s.Context() require := s.Require() const svc = "echo-easy" s.ApplyEchoService(ctx, svc, 80) defer s.DeleteSvcAndWorkload(ctx, "deploy", svc) composeDir := itest.TempDir(ctx) composeFile := filepath.Join(composeDir, "docker-compose.yml") ns := s.AppNamespace() composeContent := strings.Join([]string{ "x-tele:", " connections:", " - namespace: " + ns, " manager-namespace: " + s.ManagerNamespace(), "services:", " " + svc + ":", " x-tele:", " type: replace", " ports:", ` - "80:8080"`, // local port 80 -> cluster container port 8080 (echo-server listens on 8080) " image: hashicorp/http-echo", ` command: ["-text=hello-from-compose", "-listen=:80"]`, " tester:", " x-tele:", " type: connect", " image: busybox", " command: sleep infinity", }, "\n") require.NoError(os.WriteFile(composeFile, []byte(composeContent), 0o644)) _, _, err := itest.Telepresence(ctx, "compose", "-f", composeFile, "up", "-d") require.NoError(err, "compose up failed") defer func() { _, _, _ = itest.Telepresence(ctx, "compose", "-f", composeFile, "down") }() // Verify that the local compose container serves traffic in place of the cluster pod. s.Assert().EventuallyContext(ctx, func() bool { so, se, err := itest.Telepresence(ctx, "compose", "-f", composeFile, "exec", "tester", "wget", "-qO-", "http://"+svc) if err == nil && strings.Contains(so, "hello-from-compose") { return true } clog.Info(ctx, "wget replace", "stdout", so, "stderr", se, "err", err) return false }, 60*time.Second, 5*time.Second, "replace should serve traffic from the compose container instead of the cluster pod") } func (s *composeSuite) Test_ComposeWiretap() { ctx := s.Context() require := s.Require() const svc = "echo-easy" s.ApplyEchoService(ctx, svc, 80) defer s.DeleteSvcAndWorkload(ctx, "deploy", svc) composeDir := itest.TempDir(ctx) composeFile := filepath.Join(composeDir, "docker-compose.yml") ns := s.AppNamespace() composeContent := strings.Join([]string{ "x-tele:", " connections:", " - namespace: " + ns, " manager-namespace: " + s.ManagerNamespace(), "services:", " " + svc + ":", " x-tele:", " type: wiretap", " ports:", ` - "80:80"`, " image: hashicorp/http-echo", ` command: ["-text=hello-from-compose", "-listen=:80"]`, " tester:", " x-tele:", " type: connect", " image: busybox", " command: sleep infinity", }, "\n") require.NoError(os.WriteFile(composeFile, []byte(composeContent), 0o644)) _, _, err := itest.Telepresence(ctx, "compose", "-f", composeFile, "up", "-d") require.NoError(err, "compose up failed") defer func() { _, _, _ = itest.Telepresence(ctx, "compose", "-f", composeFile, "down") }() // Use the namespace-qualified name to reach the cluster service rather than the local compose // container. Docker's embedded DNS resolves bare "echo-easy" to the local wiretap-receiver // container, which would bypass the cluster and never trigger the traffic-agent. A // namespace-qualified name is not known to Docker's embedded DNS, so it is forwarded to the // Telepresence daemon DNS which routes it to the cluster. clusterSvcName := svc + "." + ns // Verify that the cluster service still serves the original traffic (not the local container). s.Assert().EventuallyContext(ctx, func() bool { so, se, err := itest.Telepresence(ctx, "compose", "-f", composeFile, "exec", "tester", "wget", "-qO-", "http://"+clusterSvcName) if err == nil && len(so) > 0 && !strings.Contains(so, "hello-from-compose") { return true } clog.Info(ctx, "wget wiretap cluster check", "stdout", so, "stderr", se, "err", err) return false }, 60*time.Second, 5*time.Second, "cluster service should still serve original traffic during wiretap") // Make several requests via the namespace-qualified name so the traffic-agent wiretap fires // and sends copies to the local container. for range 3 { _, _, _ = itest.Telepresence(ctx, "compose", "-f", composeFile, "exec", "tester", "wget", "-qO-", "http://"+clusterSvcName) } // Verify that the wiretap container received copies of the traffic via its logs. s.Assert().EventuallyContext(ctx, func() bool { so, se, err := itest.Telepresence(ctx, "compose", "-f", composeFile, "logs", svc) if err == nil && strings.Contains(so, "GET") { return true } clog.Info(ctx, "compose logs wiretap", "stdout", so, "stderr", se, "err", err) return false }, 30*time.Second, 3*time.Second, "wiretap container should receive copies of traffic") } ================================================ FILE: integration_test/config_test.go ================================================ package integration_test import ( "os" "path/filepath" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" ) func (s *notConnectedSuite) Test_EmptyConfigFile() { ctx := s.Context() cfgDir := itest.TempDir(ctx) ctx = filelocation.WithAppUserConfigDir(ctx, cfgDir) f, err := os.Create(filepath.Join(cfgDir, "config.yml")) s.Require().NoError(err) f.Close() s.TelepresenceConnect(ctx) itest.TelepresenceQuitOk(ctx) } ================================================ FILE: integration_test/connected_test.go ================================================ package integration_test import ( "fmt" "regexp" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/agentconfig" ) type connectedSuite struct { itest.Suite itest.TrafficManager } func (s *connectedSuite) SuiteName() string { return "Connected" } func init() { itest.AddConnectedSuite("", func(h itest.TrafficManager) itest.TestingSuite { return &connectedSuite{Suite: itest.Suite{Harness: h}, TrafficManager: h} }) } func (s *connectedSuite) Test_ListExcludesTM() { stdout := itest.TelepresenceOk(s.Context(), "list", "-n", s.ManagerNamespace()) s.NotContains(stdout, agentconfig.ManagerAppName) } func (s *connectedSuite) Test_ReportsAllVersions() { stdout := itest.TelepresenceOk(s.Context(), "version") rxVer := regexp.QuoteMeta(s.ClientVersion().String()) s.Regexp(fmt.Sprintf(`Client\s*: v%s`, rxVer), stdout) s.Regexp(fmt.Sprintf(`Root Daemon\s*: v%s`, rxVer), stdout) s.Regexp(fmt.Sprintf(`User Daemon\s*: v%s`, rxVer), stdout) mgrVer := regexp.QuoteMeta(s.ManagerVersion().String()) s.Regexp(fmt.Sprintf(`Traffic Manager\s*: v%s`, mgrVer), stdout) } func (s *connectedSuite) Test_Status() { stdout := itest.TelepresenceOk(s.Context(), "status") s.Contains(stdout, "Root Daemon: Running") s.Contains(stdout, "User Daemon: Running") s.Contains(stdout, "Kubernetes context:") s.Regexp(`Manager namespace\s+: `+s.ManagerNamespace(), stdout) } func (s *connectedSuite) Test_StatusWithJSON() { status := itest.TelepresenceStatusOk(s.Context()) s.True(status.RootDaemon.Running) s.True(status.UserDaemon.Running) s.NotEmpty(status.UserDaemon.KubernetesContext) s.NotEmpty(status.UserDaemon.InstallID) s.Equal(status.UserDaemon.ManagerNamespace, s.ManagerNamespace()) s.Require().NotNil(status.TrafficManager) s.NotEmpty(status.TrafficManager.Version) s.NotEmpty(status.TrafficManager.TrafficAgent) } ================================================ FILE: integration_test/container_test.go ================================================ package integration_test import ( "context" "os" "path/filepath" "slices" "strconv" "strings" "time" "github.com/go-json-experiment/json" core "k8s.io/api/core/v1" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/intercept" ) func (s *connectedSuite) Test_InterceptsContainer() { ctx, cancel := context.WithCancel(s.Context()) defer cancel() const svc = "echo-secondary" svcPort, svcCancel := itest.StartLocalHttpEchoServer(ctx, svc) defer svcCancel() s.ApplyApp(ctx, svc, "deploy/"+svc) defer s.DeleteSvcAndWorkload(ctx, "deployment", svc) defer func() { s.NoError(s.Kubectl(ctx, "delete", "configmaps", "socat-data", "echo-data")) }() require := s.Require() dir := s.T().TempDir() envFile := filepath.Join(dir, "env.json") stdout := itest.TelepresenceOk(ctx, "intercept", svc, "--output", "json", "--detailed-output", "--container", "echo", "--env-json", envFile, "--port", strconv.Itoa(svcPort)) defer itest.TelepresenceOk(ctx, "leave", svc) var iInfo intercept.Info require.NoError(json.Unmarshal([]byte(stdout), &iInfo)) s.CapturePodLogs(ctx, svc, "traffic-agent", s.AppNamespace()) require.Eventually( func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--intercepts") return err == nil && strings.Contains(stdout, svc+": intercepted") }, 30*time.Second, // waitFor 3*time.Second, // polling interval `intercepted workload never show up in list`) itest.PingInterceptedEchoServer(ctx, svc, "80") // Check that the env stems from the targeted container s.Equal("echo-server", iInfo.Environment["TAG"]) mountPoint := iInfo.Mount.LocalDir dataDir := filepath.Join(mountPoint, "usr", "share", "data") st, err := os.Stat(dataDir) require.NoError(err, "mount of %s should be successful", dataDir) require.True(st.IsDir()) dataFile := filepath.Join(dataDir, "text") content, err := os.ReadFile(dataFile) require.NoError(err, "unable to read", dataFile) s.Equal("Hello from echo\n", string(content)) // Intercept again, this time without the --container flag itest.TelepresenceOk(ctx, "leave", svc) stdout = itest.TelepresenceOk(ctx, "intercept", svc, "--output", "json", "--detailed-output", "--env-json", envFile, "--port", strconv.Itoa(svcPort)) iInfo = intercept.Info{} require.NoError(json.Unmarshal([]byte(stdout), &iInfo)) s.Equal(iInfo.Environment["TAG"], "socat") itest.PingInterceptedEchoServer(ctx, svc, "80") mountPoint = iInfo.Mount.LocalDir dataFile = filepath.Join(mountPoint, "usr", "share", "data", "text") content, err = os.ReadFile(dataFile) require.NoError(err, "unable to read", dataFile) s.Equal("Hello from socat\n", string(content)) } func (s *connectedSuite) Test_InterceptsContainerAndReplace() { ctx, cancel := context.WithCancel(s.Context()) defer cancel() const svc = "echo-secondary" svcPort, svcCancel := itest.StartLocalHttpEchoServer(ctx, svc) defer svcCancel() s.ApplyApp(ctx, svc, "deploy/"+svc) defer s.DeleteSvcAndWorkload(ctx, "deployment", svc) defer func() { s.NoError(s.Kubectl(ctx, "delete", "configmaps", "socat-data", "echo-data")) }() require := s.Require() dir := s.T().TempDir() envFile := filepath.Join(dir, "env.json") stdout := itest.TelepresenceOk(ctx, "intercept", svc, "--output", "json", "--detailed-output", "--container", "echo", "--replace", "--env-json", envFile, "--port", strconv.Itoa(svcPort)) defer itest.TelepresenceOk(ctx, "leave", svc) var iInfo intercept.Info require.NoError(json.Unmarshal([]byte(stdout), &iInfo)) s.CapturePodLogs(ctx, svc, "traffic-agent", s.AppNamespace()) itest.PingInterceptedEchoServer(ctx, svc, "80") // Check that the env stems from the targeted container s.Equal(iInfo.Environment["TAG"], "echo-server") mountPoint := iInfo.Mount.LocalDir dataDir := filepath.Join(mountPoint, "usr", "share", "data") st, err := os.Stat(dataDir) require.NoError(err, "mount of %s should be successful", dataDir) require.True(st.IsDir()) dataFile := filepath.Join(dataDir, "text") content, err := os.ReadFile(dataFile) require.NoErrorf(err, "unable to read %s", dataFile) s.Equal("Hello from echo\n", string(content)) // Verify that the container is replaced. stdout, err = s.KubectlOut(ctx, "get", "pod", "-l", "app="+svc, "-o", "json") require.NoError(err) items := struct { ApiVersion string `json:"apiVersion"` Items []core.Pod `json:"items"` }{} require.NoError(json.Unmarshal([]byte(stdout), &items)) pods := items.Items var echoContainer *core.Container for pi := range pods { cns := pods[pi].Spec.Containers for ci := range cns { container := &cns[ci] if container.Name == "echo" { echoContainer = container break } } } if s.ManagerIsVersion(">2.21.x") { require.Nil(echoContainer) } else { require.NotNil(echoContainer) require.Equal(echoContainer.Image, "alpine:latest") require.True(slices.Equal(echoContainer.Args, []string{"sleep", "infinity"})) } // Intercept again, this time without the --container flag itest.TelepresenceOk(ctx, "leave", svc) stdout = itest.TelepresenceOk(ctx, "intercept", svc, "--output", "json", "--detailed-output", "--env-json", envFile, "--port", strconv.Itoa(svcPort)) iInfo = intercept.Info{} require.NoError(json.Unmarshal([]byte(stdout), &iInfo)) s.Equal(iInfo.Environment["TAG"], "socat") itest.PingInterceptedEchoServer(ctx, svc, "80") mountPoint = iInfo.Mount.LocalDir dataFile = filepath.Join(mountPoint, "usr", "share", "data", "text") content, err = os.ReadFile(dataFile) require.NoError(err, "unable to read", dataFile) s.Equal("Hello from socat\n", string(content)) } ================================================ FILE: integration_test/docker_daemon_test.go ================================================ package integration_test import ( "bufio" "context" "fmt" "io" "os" "path/filepath" goRuntime "runtime" "slices" "strings" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/client/logging" "github.com/telepresenceio/telepresence/v2/pkg/dos" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" ) type dockerDaemonSuite struct { itest.Suite itest.TrafficManager ctx context.Context } func (s *dockerDaemonSuite) SuiteName() string { return "DockerDaemon" } func init() { itest.AddTrafficManagerSuite("", func(h itest.TrafficManager) itest.TestingSuite { return &dockerDaemonSuite{Suite: itest.Suite{Harness: h}, TrafficManager: h} }) } func (s *dockerDaemonSuite) SetupSuite() { if s.IsCI() && !(goRuntime.GOOS == "linux" && goRuntime.GOARCH == "amd64") { s.T().Skip("CI can't run linux docker containers inside non-linux runners") return } s.Suite.SetupSuite() s.ctx = itest.WithConfig(s.HarnessContext(), func(cfg client.Config) { cfg.Intercept().UseFtp = false }) } func (s *dockerDaemonSuite) TearDownTest() { itest.TelepresenceQuitOk(s.Context()) } func (s *dockerDaemonSuite) Context() context.Context { return itest.WithT(s.ctx, s.T()) } func (s *dockerDaemonSuite) Test_DockerDaemon_status() { ctx := s.Context() s.TelepresenceConnect(ctx, "--docker") status := itest.TelepresenceStatusOk(ctx) ud := status.UserDaemon s.True(ud.Running) s.True(strings.HasSuffix(ud.Name, s.AppNamespace()+"-cn"), "ends with suffix -cn") s.Equal(ud.Status, "Connected") } func (s *dockerDaemonSuite) Test_DockerDaemon_hostDaemonNoConflict() { ctx := s.Context() s.TelepresenceConnect(ctx) _, _, err := itest.Telepresence(ctx, "connect", "--docker", "--namespace", s.AppNamespace(), "--manager-namespace", s.ManagerNamespace()) s.NoError(err) } func (s *dockerDaemonSuite) Test_DockerDaemon_alsoProxy32() { const ipToTest = "10.10.74.1" ctx := s.Context() s.TelepresenceConnect(ctx, "--docker", "--also-proxy", ipToTest+"/32", "--name", "ax") itest.TelepresenceOk(ctx, "loglevel", "trace") defer itest.TelepresenceOk(ctx, "loglevel", "debug") rq := s.Require() logFile := filepath.Join(filelocation.AppUserLogDir(s.Context()), "connector.log") rootLog, err := os.Open(logFile) rq.NoError(err) defer rootLog.Close() // Figure out where the current end of the logfile is. This must be done before any // of the tests run because the queries that the DNS resolver receives are dependent // on how the system's DNS resolver handles search paths and caching. st, err := rootLog.Stat() rq.NoError(err) pos := st.Size() // Make an attempt to curl the also-proxied IP. The attempt will fail (there's nothing at the // other end), and that's OK. We're just interested in seeing it logged. _, _, _ = itest.Telepresence(ctx, "curl", "--silent", "--max-time", "1", ipToTest) //nolint:dogsled // X // Verify that the attempt is visible in the root log. _, err = rootLog.Seek(pos, io.SeekStart) rq.NoError(err) scn := bufio.NewScanner(rootLog) found := false // mustHaveWanted caters for cases where the default behavior from the system's resolver // is to not send unwanted queries to our resolver at all (based on search and routes). // It is forced to true for inclusion tests. strToFind := fmt.Sprintf("%s:80, code STREAM_INFO", ipToTest) for scn.Scan() { txt := scn.Text() if strings.Contains(txt, strToFind) { found = true break } } s.Truef(found, "Unable to find %q", strToFind) } func (s *dockerDaemonSuite) Test_DockerDaemon_daemonHostNotConflict() { ctx := s.Context() s.TelepresenceConnect(ctx, "--docker") s.TelepresenceConnect(ctx) } func (s *dockerDaemonSuite) Test_DockerDaemon_singleNameLookup() { ctx := s.Context() const svc = "echo-easy" s.ApplyApp(ctx, svc, "deploy/"+svc) defer s.DeleteSvcAndWorkload(ctx, "deploy", svc) out := s.TelepresenceConnect(ctx, "--docker", "--", itest.GetExecutable(ctx), "curl", "--silent", "--max-time", "1", svc) s.Contains(out, "Request served by "+svc) so, err := itest.TelepresenceStatus(ctx) s.NoError(err) s.Nil(so.ContainerizedDaemon) s.False(so.UserDaemon.Running) } func (s *dockerDaemonSuite) Test_DockerDaemon_cacheFiles() { ctx := s.Context() rq := s.Require() cache := filelocation.AppUserCacheDir(ctx) // Create a random file, just to get a dos-file handle with our own UID/GID rf, err := dos.Create(ctx, filepath.Join(s.T().TempDir(), "random.file")) rq.NoError(err) rs, err := logging.FStat(rf) _ = rf.Close() rq.NoError(err) lv := filepath.Join(cache, client.UserDaemonName+".loglevel") ctx = dos.WithLockedFs(ctx) _ = dos.Remove(ctx, lv) s.TelepresenceConnect(ctx, "--docker") itest.TelepresenceOk(ctx, "loglevel", "trace") defer itest.TelepresenceOk(ctx, "loglevel", "debug") df, err := dos.Open(ctx, lv) rq.NoError(err) st, err := logging.FStat(df) _ = df.Close() rq.NoError(err) rq.True(st.HaveSameOwnerAndGroup(rs)) } func (s *dockerDaemonSuite) Test_GatherLogsTrafficManager() { ctx := s.Context() outputDir := itest.TempDir(ctx) outputFile := filepath.Join(outputDir, "allLogs.zip") s.TelepresenceConnect(ctx, "--docker") itest.TelepresenceOk(ctx, "gather-logs", "--daemons", "None", "--traffic-agents", "None", "--output-file", outputFile) foundManager, _, _, fileNames := getZipData(s.Require(), outputFile, s.AppNamespace(), s.ManagerNamespace(), "echo-easy") s.Require().True(foundManager) s.Require().True(slices.ContainsFunc(fileNames, func(name string) bool { return strings.HasPrefix(name, "traffic-manager") })) } ================================================ FILE: integration_test/docker_run_test.go ================================================ package integration_test import ( "bytes" "context" "os" "path/filepath" "regexp" goRuntime "runtime" "strings" "time" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" ) func runDockerRun(ctx context.Context, name, svc, port, appDir, tag string, rq *itest.Requirements, wch chan<- struct{}) *os.Process { _ = itest.Run(ctx, "docker", "container", "stop", name) args := []string{"intercept", "--mount", "false", svc, "--docker-run"} if port != "" { args = append(args, "--port", port) } args = append(args, "--", "--rm", "-v", appDir+":/usr/src/app") if name != "" { args = append(args, "--name", name) } args = append(args, tag) cmd := itest.TelepresenceCmd(ctx, args...) so := &bytes.Buffer{} se := &bytes.Buffer{} cmd.Stdout = so cmd.Stderr = se rq.NoError(cmd.Start()) proc := cmd.Process go func() { if wch != nil { defer close(wch) } err := cmd.Wait() clog.Info(ctx, so.String()) if ses := se.String(); ses != "" { clog.Error(ctx, ses) } if err != nil { clog.Error(ctx, err.Error()) } }() return proc } func (s *singleServiceSuite) Test_DockerRun_HostDaemon() { if s.IsCI() && !(goRuntime.GOOS == "linux" && goRuntime.GOARCH == "amd64") { s.T().Skip("CI can't run linux docker containers inside non-linux runners") } require := s.Require() ctx := s.Context() svc := s.ServiceName() tag := "telepresence/echo-test" testDir := "testdata/echo-server" _, err := itest.Output(ctx, "docker", "build", "-t", tag, testDir) require.NoError(err) abs, err := filepath.Abs(testDir) require.NoError(err) assertInterceptResponse := func(ctx context.Context) { assert := s.Assert() assert.EventuallyContext(ctx, func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--intercepts") return err == nil && strings.Contains(stdout, svc+": intercepted") }, 30*time.Second, 3*time.Second) // Response contains env variables TELEPRESENCE_CONTAINER and TELEPRESENCE_INTERCEPT_ID expectedOutput := regexp.MustCompile(`Intercept id [0-9a-f-]+:` + svc) assert.EventuallyContext(ctx, func() bool { out, err := itest.Output(ctx, "curl", "--silent", "--max-time", "1", "http://"+svc) clog.Info(ctx, out) if err != nil { clog.Error(ctx, err) return false } return expectedOutput.MatchString(out) }, 30*time.Second, // waitFor 2*time.Second, // polling interval `body of %q matches %q`, "http://"+svc, expectedOutput, ) } assertNotIntercepted := func(ctx context.Context) { assert := s.Assert() assert.EventuallyContext(ctx, func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--intercepts") if err != nil { clog.Error(ctx, err) return false } if strings.Contains(stdout, svc+": intercepted") { clog.Debugf(ctx, "stdout: %q", stdout) return false } return true }, 30*time.Second, 2*time.Second) } port := "9070:8080" s.Run("-C", func() { // Use a soft context to send a -c to telepresence in order to end it ctx := s.Context() wch := make(chan struct{}) proc := runDockerRun(ctx, "", svc, port, abs, tag, s.Require(), wch) assertInterceptResponse(ctx) _ = proc.Signal(os.Interrupt) select { case <-wch: case <-time.After(10 * time.Second): s.Fail("interceptor did not terminate") } assertNotIntercepted(ctx) }) s.Run("leave", func() { // End the intercept from another telepresence invocation ctx := s.Context() wch := make(chan struct{}) runDockerRun(ctx, "", svc, port, abs, tag, s.Require(), wch) assertInterceptResponse(ctx) itest.TelepresenceOk(ctx, "leave", svc) select { case <-wch: case <-time.After(10 * time.Second): s.Fail("interceptor did not terminate") } assertNotIntercepted(ctx) }) s.Run("disconnect", func() { // End the intercept from another telepresence invocation ctx := s.Context() wch := make(chan struct{}) runDockerRun(ctx, "", svc, port, abs, tag, s.Require(), wch) assertInterceptResponse(ctx) itest.TelepresenceDisconnectOk(ctx) select { case <-wch: case <-time.After(10 * time.Second): s.Fail("interceptor did not terminate") } s.TelepresenceConnect(ctx) assertNotIntercepted(ctx) }) s.Run("quit", func() { // End the intercept from another telepresence invocation ctx := s.Context() wch := make(chan struct{}) runDockerRun(ctx, "", svc, port, abs, tag, s.Require(), wch) assertInterceptResponse(ctx) itest.TelepresenceQuitOk(ctx) select { case <-wch: case <-time.After(10 * time.Second): s.Fail("interceptor did not terminate") } s.TelepresenceConnect(ctx) assertNotIntercepted(ctx) }) } func (s *dockerDaemonSuite) Test_DockerRun_DockerDaemon() { svc := "echo" ctx := s.Context() s.ApplyEchoService(ctx, svc, 80) defer s.DeleteSvcAndWorkload(ctx, "deploy", svc) require := s.Require() stdout := s.TelepresenceConnect(ctx, "--docker") defer itest.TelepresenceQuitOk(ctx) match := regexp.MustCompile(`Connected to context ?(.+),\s*namespace (\S+)\s+\(`).FindStringSubmatch(stdout) require.Len(match, 3) tag := "telepresence/echo-test" testDir := "testdata/echo-server" _, err := itest.Output(ctx, "docker", "build", "-t", tag, testDir) require.NoError(err) abs, err := filepath.Abs(testDir) require.NoError(err) assertInterceptResponse := func(ctx context.Context) { s.Eventually(func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--intercepts") clog.Info(ctx, stdout) return err == nil && strings.Contains(stdout, svc+": intercepted") }, 30*time.Second, 3*time.Second) expectedOutput := regexp.MustCompile(`Intercept id [0-9a-f-]+:` + svc) s.Eventually( // condition func() bool { so, _, err := itest.Telepresence(ctx, "curl", "--silent", "--max-time", "2", "http://"+svc) clog.Info(ctx, so) if err != nil { clog.Error(ctx, err) return false } return expectedOutput.MatchString(so) }, 60*time.Second, // A docker container reuses IPs, but MAC-address changes. It takes time for the network to learn about this. 5*time.Second, // polling interval `body of %q matches %q`, "http://"+svc, expectedOutput, ) } assertNotIntercepted := func(ctx context.Context) { s.Eventually(func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--intercepts") return err == nil && !strings.Contains(stdout, svc+": intercepted") }, 15*time.Second, 2*time.Second) } s.Run("-C", func() { // Use a soft context to send a -c to telepresence in order to end it ctx := s.Context() wch := make(chan struct{}) proc := runDockerRun(ctx, "adam", svc, "", abs, tag, s.Require(), wch) s.CapturePodLogs(ctx, svc, "traffic-agent", s.AppNamespace()) assertInterceptResponse(ctx) _ = proc.Signal(os.Interrupt) select { case <-wch: case <-time.After(10 * time.Second): s.Fail("interceptor did not terminate") } assertNotIntercepted(ctx) }) s.Run("leave", func() { // End the intercept from another telepresence invocation ctx := s.Context() wch := make(chan struct{}) runDockerRun(ctx, "bruce", svc, "", abs, tag, s.Require(), wch) s.CapturePodLogs(ctx, svc, "traffic-agent", s.AppNamespace()) assertInterceptResponse(ctx) itest.TelepresenceOk(ctx, "leave", svc) select { case <-wch: case <-time.After(10 * time.Second): s.Fail("interceptor did not terminate") } assertNotIntercepted(ctx) }) s.Run("disconnect", func() { // End the intercept from another telepresence invocation ctx := s.Context() wch := make(chan struct{}) runDockerRun(ctx, "chris", svc, "", abs, tag, s.Require(), wch) s.CapturePodLogs(ctx, svc, "traffic-agent", s.AppNamespace()) assertInterceptResponse(ctx) itest.TelepresenceDisconnectOk(ctx) select { case <-wch: case <-time.After(10 * time.Second): s.Fail("interceptor did not terminate") } s.TelepresenceConnect(ctx, "--docker") assertNotIntercepted(ctx) }) s.Run("quit", func() { // End the intercept from another telepresence invocation ctx := s.Context() wch := make(chan struct{}) runDockerRun(ctx, "david", svc, "", abs, tag, s.Require(), wch) assertInterceptResponse(ctx) itest.TelepresenceQuitOk(ctx) select { case <-wch: case <-time.After(10 * time.Second): s.Fail("interceptor did not terminate") } s.TelepresenceConnect(ctx, "--docker") assertNotIntercepted(ctx) }) } func (s *dockerDaemonSuite) Test_DockerRun_VolumePresent() { if !s.ClientIsVersion(">2.24.x") { s.T().Skip("Not part of compatibility tests. Docker volume plugin is unstable for versions < 2.25.0") } ctx := s.Context() s.ApplyTemplate(ctx, filepath.Join("testdata", "k8s", "hello-w-volumes.goyaml"), nil) defer s.DeleteSvcAndWorkload(ctx, "deploy", "hello") s.TelepresenceConnect(ctx, "--docker") defer itest.TelepresenceQuitOk(ctx) stdout, _, err := itest.Telepresence(ctx, "intercept", "--docker-run", "hello", "-p", "8080:http", "--", "--rm", "busybox", "ls", "/var/run/secrets/datawire.io/auth") s.NoError(err) clog.Infof(ctx, "stdout = %s", stdout) s.True(strings.HasSuffix(stdout, "\nusername")) } func (s *dockerDaemonSuite) Test_DockerRunCommand() { ctx := s.Context() require := s.Require() s.TelepresenceConnect(ctx, "--docker", "--hostname", "cicero") defer itest.TelepresenceQuitOk(ctx) stdout, _, err := itest.Telepresence(ctx, "docker-run", "--rm", "busybox", "ip", "r") require.NoError(err) clog.Infof(ctx, "stdout = %s", stdout) if s.ClientIsVersion(">=2.23.0") { s.Contains(stdout, "dev tpd-0") } } func (s *dockerDaemonSuite) Test_DockerRunExternalDNS() { ctx := s.Context() require := s.Require() s.TelepresenceConnect(ctx, "--docker") defer itest.TelepresenceQuitOk(ctx) stdout, _, err := itest.Telepresence(ctx, "docker-run", "--rm", "busybox", "nslookup", "google.com") require.NoError(err) clog.Infof(ctx, "stdout = %s", stdout) s.Contains(stdout, "Address: ") } ================================================ FILE: integration_test/env_interpolate_test.go ================================================ package integration_test import ( "github.com/go-json-experiment/json" core "k8s.io/api/core/v1" "github.com/telepresenceio/telepresence/v2/integration_test/itest" ) func (s *connectedSuite) Test_PrefixInterpolated() { ctx := s.Context() svc := "echo-interpolate" rq := s.Require() s.ApplyApp(ctx, svc, "deploy/"+svc) defer func() { s.DeleteSvcAndWorkload(ctx, "deploy", svc) s.NoError(s.Kubectl(ctx, "delete", "configmap", "interpolate-config")) }() itest.TelepresenceOk(ctx, "intercept", "--mount", "false", svc) defer itest.TelepresenceOk(ctx, "leave", svc) out, err := s.KubectlOut(ctx, "get", "pod", "-o", "json", "-l", "app="+svc) rq.NoError(err) var pods core.PodList err = json.Unmarshal([]byte(out), &pods) rq.NoError(err) var ag *core.Container outer: for _, pod := range pods.Items { cns := pod.Spec.Containers for ci := range cns { cn := &cns[ci] if cn.Name == "traffic-agent" { ag = cn break outer } } } rq.NotNil(ag) for _, vm := range ag.VolumeMounts { if vm.Name == "my-volume" { rq.Equal("$(_TEL_APP_A_SOME_NAME)_$(_TEL_APP_A_OTHER_NAME)", vm.SubPathExpr) break } } } ================================================ FILE: integration_test/gather_logs_test.go ================================================ package integration_test import ( "archive/zip" "context" "fmt" "io" "path/filepath" "regexp" "strings" "time" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/labels" ) func (s *multipleInterceptsSuite) TestGatherLogs_AllLogs() { require := s.Require() outputDir := s.T().TempDir() ctx := s.Context() outputFile := filepath.Join(outputDir, "allLogs.zip") s.cleanLogDir(ctx) itest.TelepresenceOk(ctx, "gather-logs", "--get-pod-yaml", "--output-file", outputFile) foundManager, foundAgents, yamlCount, fileNames := s.getZipData(outputFile) require.True(foundManager) require.Equal(s.ServiceCount(), foundAgents, fileNames) // One for each agent + one for the traffic manager require.Equal(s.ServiceCount()+1, yamlCount, fileNames) } func (s *multipleInterceptsSuite) TestGatherLogs_ManagerOnly() { require := s.Require() outputDir := s.T().TempDir() ctx := s.Context() outputFile := filepath.Join(outputDir, "allLogs.zip") s.cleanLogDir(ctx) itest.TelepresenceOk(ctx, "gather-logs", "--output-file", outputFile, "--get-pod-yaml", "--traffic-agents=None") foundManager, foundAgents, yamlCount, fileNames := s.getZipData(outputFile) require.True(foundManager) require.Equal(0, foundAgents, fileNames) require.GreaterOrEqual(yamlCount, 1, fileNames) } func (s *multipleInterceptsSuite) TestGatherLogs_AgentsOnly() { require := s.Require() outputDir := s.T().TempDir() ctx := s.Context() outputFile := filepath.Join(outputDir, "allLogs.zip") s.cleanLogDir(ctx) itest.TelepresenceOk(ctx, "gather-logs", "--output-file", outputFile, "--get-pod-yaml", "--traffic-manager=False") foundManager, foundAgents, yamlCount, fileNames := s.getZipData(outputFile) require.False(foundManager) require.GreaterOrEqual(foundAgents, s.ServiceCount(), fileNames) require.GreaterOrEqual(yamlCount, s.ServiceCount(), fileNames) } func (s *multipleInterceptsSuite) TestGatherLogs_OneAgentOnly() { require := s.Require() outputDir := s.T().TempDir() ctx := s.Context() outputFile := filepath.Join(outputDir, "allLogs.zip") s.cleanLogDir(ctx) itest.TelepresenceOk(ctx, "gather-logs", "--output-file", outputFile, "--get-pod-yaml", "--traffic-manager=False", "--traffic-agents=hello-1") foundManager, foundAgents, yamlCount, fileNames := s.getZipData(outputFile) require.False(foundManager) require.GreaterOrEqual(foundAgents, 1, fileNames) require.GreaterOrEqual(yamlCount, 1, fileNames) } func (s *multipleInterceptsSuite) TestGatherLogs_NoPodYamlUnlessLogs() { require := s.Require() outputDir := s.T().TempDir() ctx := s.Context() outputFile := filepath.Join(outputDir, "allLogs.zip") s.cleanLogDir(ctx) itest.TelepresenceOk(ctx, "gather-logs", "--output-file", outputFile, "--get-pod-yaml", "--traffic-manager=False", "--traffic-agents=None") foundManager, foundAgents, yamlCount, fileNames := s.getZipData(outputFile) require.False(foundManager) require.Equal(0, foundAgents, fileNames) require.Equal(0, yamlCount, fileNames) } func (s *multipleInterceptsSuite) TestGatherLogs_NoK8sLogs() { require := s.Require() outputDir := s.T().TempDir() ctx := s.Context() outputFile := filepath.Join(outputDir, "allLogs.zip") s.cleanLogDir(ctx) itest.TelepresenceOk(ctx, "gather-logs", "--output-file", outputFile, "--get-pod-yaml", "--traffic-manager=False", "--traffic-agents=None") foundManager, foundAgents, yamlCount, fileNames := s.getZipData(outputFile) require.False(foundManager) require.Equal(0, foundAgents, fileNames) require.Equal(0, yamlCount, fileNames) } func (s *connectedSuite) TestGatherLogs_OnlyMappedLogs() { const svc = "echo" ctx := s.Context() itest.TelepresenceDisconnectOk(ctx) otherOne := fmt.Sprintf("other-one-%s", s.Suffix()) itest.CreateNamespaces(ctx, otherOne) defer itest.DeleteNamespaces(ctx, otherOne) otherTwo := fmt.Sprintf("other-two-%s", s.Suffix()) itest.CreateNamespaces(ctx, otherTwo) defer itest.DeleteNamespaces(ctx, otherTwo) s.TelepresenceHelmInstallOK(itest.WithNamespaces(ctx, &itest.Namespaces{ Namespace: s.ManagerNamespace(), Selector: labels.SelectorFromNames(otherOne, otherTwo), }), true) require := s.Require() defer func() { so, se, err := itest.Telepresence(ctx, "quit") clog.Debug(ctx, "stdout", so, "stderr", se, "err", err) s.RollbackTM(ctx) stdout := s.TelepresenceConnect(ctx) require.Contains(stdout, "Connected to context") }() itest.TelepresenceDisconnectOk(ctx) itest.ApplyEchoService(ctx, svc, otherOne, 8083) itest.ApplyEchoService(ctx, svc, otherTwo, 8084) itest.TelepresenceOk(ctx, "connect", "--namespace", otherOne, "--manager-namespace", s.ManagerNamespace()) itest.TelepresenceOk(ctx, "intercept", "--mount", "false", svc) s.Eventually( func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--intercepts") return err == nil && strings.Contains(stdout, svc+": intercepted") }, 10*time.Second, 2*time.Second, ) s.CapturePodLogs(ctx, svc, "traffic-agent", otherOne) itest.TelepresenceDisconnectOk(ctx) itest.TelepresenceOk(ctx, "connect", "--namespace", otherTwo, "--manager-namespace", s.ManagerNamespace()) itest.TelepresenceOk(ctx, "intercept", "--mount", "false", svc) s.Eventually( func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--intercepts") return err == nil && strings.Contains(stdout, svc+": intercepted") }, 10*time.Second, 2*time.Second, ) s.CapturePodLogs(ctx, svc, "traffic-agent", otherTwo) itest.TelepresenceOk(ctx, "leave", svc) bothNsRx := fmt.Sprintf("(?:%s|%s)", otherOne, otherTwo) outputDir := s.T().TempDir() outputFile := filepath.Join(outputDir, "allLogs.zip") itest.CleanLogDir(ctx, require, bothNsRx, s.ManagerNamespace(), svc) itest.TelepresenceOk(ctx, "gather-logs", "--output-file", outputFile, "--traffic-manager=False") _, foundAgents, _, fileNames := getZipData(require, outputFile, bothNsRx, s.ManagerNamespace(), svc) require.GreaterOrEqual(foundAgents, 2, fileNames) // Connect using mapped-namespaces itest.TelepresenceDisconnectOk(ctx) stdout := itest.TelepresenceOk(ctx, "connect", "--namespace", otherOne, "--manager-namespace", s.ManagerNamespace(), "--mapped-namespaces", otherOne) require.Contains(stdout, "Connected to context") itest.CleanLogDir(ctx, require, bothNsRx, s.ManagerNamespace(), svc) itest.TelepresenceOk(ctx, "list") // To ensure that the mapped namespaces are active itest.TelepresenceOk(ctx, "gather-logs", "--output-file", outputFile, "--traffic-manager=False") _, foundAgents, _, fileNames = getZipData(require, outputFile, bothNsRx, s.ManagerNamespace(), svc) require.GreaterOrEqual(foundAgents, 1, fileNames) } func (s *multipleInterceptsSuite) cleanLogDir(ctx context.Context) { itest.CleanLogDir(ctx, s.Require(), s.AppNamespace(), s.ManagerNamespace(), s.svcRegex()) } func (s *multipleInterceptsSuite) svcRegex() string { if s.ServiceCount() >= 10 { return `hello-\d+` } return fmt.Sprintf("hello-[0-%d]", s.ServiceCount()) } func (s *multipleInterceptsSuite) getZipData(outputFile string) (bool, int, int, []string) { return getZipData(s.Require(), outputFile, s.AppNamespace(), s.ManagerNamespace(), s.svcRegex()) } func getZipData(require *itest.Requirements, outputFile, appNamespace, mgrNamespace, svcNameRx string) (bool, int, int, []string) { zipReader, err := zip.OpenReader(outputFile) require.NoError(err) defer func() { require.NoError(zipReader.Close()) }() // we collect and return the fileNames so that it makes it easier // to debug if tests fail helloMatch := regexp.MustCompile(fmt.Sprintf(`^%s-[0-9a-z-]+\.%s\.(?:log|yaml)$`, svcNameRx, appNamespace)) tmMatch := regexp.MustCompile(fmt.Sprintf(`^traffic-manager-[0-9a-z-]+\.%s\.(?:log|yaml)$`, mgrNamespace)) tmHdrMatch := regexp.MustCompile(`Traffic Manager v\d+\.\d+\.\d+`) agHdrMatch := regexp.MustCompile(`Traffic Agent v\d+\.\d+\.\d+`) foundManager, foundAgents, yamlCount := false, 0, 0 fileNames := make([]string, len(zipReader.File)) for i, f := range zipReader.File { fileNames[i] = f.Name if tmMatch.MatchString(f.Name) { if strings.HasSuffix(f.Name, ".yaml") { yamlCount++ continue } fileContent := readZip(require, f) // We can be fairly certain we actually got a traffic-manager log // if we see the following if tmHdrMatch.Match(fileContent) { foundManager = true } } if helloMatch.MatchString(f.Name) { if strings.HasSuffix(f.Name, ".yaml") { yamlCount++ continue } fileContent := readZip(require, f) // We can be fairly certain we actually got a traffic-agent log // if we see the following if agHdrMatch.Match(fileContent) { foundAgents++ } } } return foundManager, foundAgents, yamlCount, fileNames } // readZip reads a zip file and returns the []byte string. Used in tests for // checking that a zipped file's contents are correct. Exported since it is // also used in telepresence_test.go. func readZip(require *itest.Requirements, zippedFile *zip.File) []byte { fileReader, err := zippedFile.Open() require.NoError(err) fileContent, err := io.ReadAll(fileReader) require.NoError(err) return fileContent } ================================================ FILE: integration_test/h2c_intercept_test.go ================================================ package integration_test import ( "context" "fmt" "io" "net" "net/http" "strconv" "strings" "time" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/iputil" ) type h2cInterceptSuite struct { itest.Suite itest.TrafficManager } func (s *h2cInterceptSuite) SuiteName() string { return "H2CIntercept" } func init() { itest.AddConnectedSuite("", func(h itest.TrafficManager) itest.TestingSuite { return &h2cInterceptSuite{Suite: itest.Suite{Harness: h}, TrafficManager: h} }) } func (s *h2cInterceptSuite) SetupSuite() { if !(s.ManagerIsVersion(">2.26.x") && s.ClientIsVersion(">2.26.x")) { s.T().Skip("H2C intercepts require Telepresence 2.27.0 or later") } s.Suite.SetupSuite() } // startLocalH2CEchoServer starts a local HTTP server that supports h2c (HTTP/2 cleartext) // and echoes a line with the given name and the current URL path. func startLocalH2CEchoServer(ctx context.Context, name string) (int, context.CancelFunc) { ctx, cancel := context.WithCancel(ctx) lc := net.ListenConfig{} l, err := lc.Listen(ctx, "tcp", "localhost:0") if err != nil { cancel() panic(fmt.Sprintf("failed to listen: %v", err)) } pr := new(http.Protocols) pr.SetHTTP1(true) pr.SetUnencryptedHTTP2(true) sc := &http.Server{ Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "%s from intercept at %s", name, r.URL.Path) }), Protocols: pr, } go func() { _ = sc.Serve(l) }() go func() { <-ctx.Done() sdCtx, sdCancel := context.WithTimeout(context.WithoutCancel(ctx), time.Second) defer sdCancel() _ = sc.Shutdown(sdCtx) }() return l.Addr().(*net.TCPAddr).Port, cancel } func (s *h2cInterceptSuite) Test_H2CInterceptPreservesProtocol() { ctx := s.Context() require := s.Require() const svc = "echo-h2c" // Deploy echo-server with appProtocol: kubernetes.io/h2c itest.ApplyAppTemplate(ctx, s.AppNamespace(), &itest.AppData{ AppName: svc, Ports: []itest.AppPort{ { ServicePortNumber: 80, TargetPortNumber: 8080, AppProtocol: "kubernetes.io/h2c", }, }, Env: map[string]string{"PORTS": "8080:http"}, }) defer itest.DeleteSvcAndWorkload(ctx, "deploy", svc, s.AppNamespace()) // Start a local h2c-capable echo server to receive intercepted traffic. // The upstream handler must support the same protocol as the app. localPort, cancel := startLocalH2CEchoServer(ctx, svc) defer cancel() // Create a personal HTTP intercept with a header filter stdout, stderr, err := itest.Telepresence(ctx, "intercept", svc, "--http-header", "x-test=h2c", "--port", strconv.Itoa(localPort)+":80", "--mount=false") require.NoError(err, "stderr: %s", stderr) require.Contains(stdout, "Using Deployment") // Capture traffic-agent logs for debugging s.CapturePodLogs(ctx, svc, "traffic-agent", s.AppNamespace()) // Verify non-intercepted traffic uses HTTP/2 (h2c). // The intercept is active so traffic goes through the agent's HTTP handler. // Requests without the x-test header don't match the intercept and are forwarded // by the default reverse proxy. The fix ensures that this proxy uses h2c prior // knowledge, so the echo-server sees HTTP/2.0. require.Eventually(func() bool { ips, err := net.DefaultResolver.LookupIP(ctx, "ip", svc) if err != nil { clog.Info(ctx, err) return false } ips = iputil.UniqueSorted(ips) if len(ips) != 1 { clog.Infof(ctx, "Lookup for %s returned %v", svc, ips) return false } hc := http.Client{Timeout: 2 * time.Second} rq, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://%s", net.JoinHostPort(ips[0].String(), "80")), nil) if err != nil { clog.Info(ctx, err) return false } resp, err := hc.Do(rq) if err != nil { clog.Info(ctx, err) return false } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { clog.Info(ctx, err) return false } r := string(body) clog.Infof(ctx, "non-intercepted response: %s", r) return strings.Contains(r, "HTTP/2.0 GET /") }, 30*time.Second, 3*time.Second, "expected HTTP/2.0 in non-intercepted response") // Verify intercepted traffic reaches the local h2c handler itest.PingInterceptedEchoServer(ctx, svc, "80", "x-test=h2c") // Leave the intercept _, _, err = itest.Telepresence(ctx, "leave", svc) require.NoError(err) } ================================================ FILE: integration_test/headless_test.go ================================================ package integration_test import ( "context" "strconv" "strings" "time" "github.com/telepresenceio/telepresence/v2/integration_test/itest" ) func (s *connectedSuite) Test_SuccessfullyInterceptsHeadlessService() { if itest.GetProfile(s.Context()) == itest.GkeAutopilotProfile { s.T().Skip("GKE Autopilot does not support NET_ADMIN containers which means headless services can't be intercepted") } ctx, cancel := context.WithCancel(s.Context()) defer cancel() const svc = "echo-headless" svcPort, svcCancel := itest.StartLocalHttpEchoServer(ctx, svc) defer svcCancel() s.ApplyApp(ctx, "echo-headless", "statefulset/echo-headless") defer s.DeleteSvcAndWorkload(ctx, "statefulset", "echo-headless") require := s.Require() stdout := itest.TelepresenceOk(ctx, "intercept", "--mount", "false", svc, "--port", strconv.Itoa(svcPort)) require.Contains(stdout, "Using StatefulSet echo-headless") s.CapturePodLogs(ctx, "echo-headless", "traffic-agent", s.AppNamespace()) defer itest.TelepresenceOk(ctx, "leave", "echo-headless") require.Eventually( func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--intercepts") return err == nil && strings.Contains(stdout, "echo-headless: intercepted") }, 30*time.Second, // waitFor 3*time.Second, // polling interval `intercepted workload never show up in list`) itest.PingInterceptedEchoServer(ctx, svc, "8080") } ================================================ FILE: integration_test/helm_test.go ================================================ package integration_test import ( "fmt" "path/filepath" "strings" "time" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/labels" ) type helmSuite struct { itest.Suite itest.SingleService mgrSpace2 string appSpace2 string } func (s *helmSuite) SuiteName() string { return "Helm" } func init() { itest.AddSingleServiceSuite("", "echo", func(h itest.SingleService) itest.TestingSuite { s := &helmSuite{Suite: itest.Suite{Harness: h}, SingleService: h} suffix := itest.GetGlobalHarness(h.HarnessContext()).Suffix() s.appSpace2, s.mgrSpace2 = itest.AppAndMgrNSName(suffix + "-2") return s }) } func (s *helmSuite) SetupSuite() { s.Suite.SetupSuite() ctx := s.Context() itest.CreateNamespaces(ctx, s.appSpace2, s.mgrSpace2) itest.ApplyEchoService(ctx, s.ServiceName(), s.appSpace2, 80) } func (s *helmSuite) TearDownSuite() { itest.DeleteNamespaces(s.Context(), s.appSpace2, s.mgrSpace2) } func (s *helmSuite) Test_HelmCanInterceptInManagedNamespace() { ctx := s.Context() defer itest.TelepresenceOk(ctx, "leave", s.ServiceName()) stdout := itest.TelepresenceOk(ctx, "intercept", "--mount", "false", s.ServiceName(), "--port", "9090") s.Contains(stdout, "Using Deployment "+s.ServiceName()) stdout = itest.TelepresenceOk(ctx, "list", "--intercepts") s.Contains(stdout, s.ServiceName()+": intercepted") } func (s *helmSuite) Test_HelmCannotConnectToUnmanagedNamespace() { ctx := s.Context() defer func() { ctx := s.Context() _, _, _ = itest.Telepresence(ctx, "disconnect") //nolint:dogsled // X s.TelepresenceConnect(ctx) }() itest.TelepresenceDisconnectOk(ctx) _, stderr, err := itest.Telepresence(ctx, "connect", "--namespace", s.appSpace2, "--manager-namespace", s.ManagerNamespace()) s.Error(err) s.True(strings.Contains(stderr, fmt.Sprintf(`namespace %s is not managed`, s.appSpace2))) } func (s *helmSuite) Test_HelmWebhookInjectsInManagedNamespace() { ctx := s.Context() s.ApplyApp(ctx, "echo-auto-inject", "deploy/echo-auto-inject") defer s.DeleteSvcAndWorkload(ctx, "deploy", "echo-auto-inject") verb := "engage" if !s.ClientIsVersion(">2.21.x") { verb = "intercept" } s.Eventually(func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--agents") return err == nil && strings.Contains(stdout, fmt.Sprintf("echo-auto-inject: ready to %s (traffic-agent already installed)", verb)) }, 20*time.Second, // waitFor 2*time.Second, // polling interval ) } func (s *helmSuite) Test_HelmWebhookDoesntInjectInUnmanagedNamespace() { ctx := s.Context() itest.ApplyApp(ctx, "echo-auto-inject", s.appSpace2, "deploy/echo-auto-inject") defer itest.DeleteSvcAndWorkload(ctx, "deploy", "echo-auto-inject", s.appSpace2) verb := "engage" if !s.ClientIsVersion(">2.21.x") { verb = "intercept" } s.Never(func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--namespace", s.appSpace2, "--agents") return err == nil && strings.Contains(stdout, fmt.Sprintf("echo-auto-inject: ready to %s (traffic-agent already installed)", verb)) }, 10*time.Second, // waitFor 2*time.Second, // polling interval ) } func (s *helmSuite) Test_HelmMultipleInstalls() { svc := s.ServiceName() defer func() { ctx := s.Context() itest.TelepresenceDisconnectOk(ctx) s.TelepresenceConnect(ctx) }() s.Run("Installs Successfully", func() { ctx := itest.WithNamespaces(s.Context(), &itest.Namespaces{ Namespace: s.mgrSpace2, Selector: labels.SelectorFromNames(s.appSpace2), }) s.NoError(itest.Kubectl(ctx, s.mgrSpace2, "apply", "-f", filepath.Join("testdata", "k8s", "client_sa.yaml"))) itest.TelepresenceDisconnectOk(ctx) s.TelepresenceHelmInstallOK(ctx, false) }) s.Run("Can be connected to", func() { ctx := itest.WithUser(s.Context(), s.mgrSpace2+":"+itest.TestUser) stdout := itest.TelepresenceOk(ctx, "connect", "--namespace", s.appSpace2, "--manager-namespace", s.mgrSpace2) s.Contains(stdout, "Connected to context") s.Eventually(func() bool { return itest.Run(ctx, "curl", "--silent", "--connect-timeout", "1", fmt.Sprintf("%s.%s", svc, s.appSpace2)) == nil }, 30*time.Second, 3*time.Second) }) s.Run("Can intercept", func() { ctx := s.Context() stdout := itest.TelepresenceOk(ctx, "intercept", "--mount", "false", svc, "--port", "9090") s.Contains(stdout, "Using Deployment "+svc) stdout = itest.TelepresenceOk(ctx, "list", "--namespace", s.appSpace2, "--intercepts") s.Contains(stdout, svc+": intercepted") }) s.Run("Uninstalls Successfully", func() { defer itest.TelepresenceQuitOk(s.Context()) s.UninstallTrafficManager(s.Context(), s.mgrSpace2) }) } func (s *helmSuite) Test_CollidingInstalls() { defer func() { ctx := s.Context() itest.TelepresenceDisconnectOk(ctx) s.TelepresenceConnect(ctx) }() ctx := itest.WithNamespaces(s.Context(), &itest.Namespaces{ Namespace: s.AppNamespace(), Selector: labels.SelectorFromNames(s.appSpace2), }) _, err := s.TelepresenceHelmInstall(ctx, false) s.Error(err) } ================================================ FILE: integration_test/http_intercepts_test.go ================================================ package integration_test import ( "context" "fmt" "net/http" "os" "strconv" "strings" "sync" "testing" "time" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" ) type httpInterceptsSuite struct { itest.Suite itest.SingleService } func (s *httpInterceptsSuite) SuiteName() string { return "HTTPIntercepts" } func (s *httpInterceptsSuite) SetupSuite() { if !(s.ManagerIsVersion(">2.24.x") && s.ClientIsVersion(">2.24.x")) { s.T().Skip("HTTP intercepts require Telepresence 2.25.0 or later") } s.Suite.SetupSuite() } func (s *httpInterceptsSuite) Test_HTTPHeaderFiltering() { require := s.Require() ctx := s.Context() // Test HTTP intercept with header filters using equals format stdout, stderr, err := itest.Telepresence(ctx, "intercept", s.ServiceName(), "--http-header", "X-User-ID=dev123", "--port", "8080") require.NoError(err, "stderr: %s", stderr) require.Contains(stdout, "Using Deployment") // Clean up _, _, err = itest.Telepresence(ctx, "leave", s.ServiceName()) require.NoError(err) } func (s *httpInterceptsSuite) Test_HTTPHeaderFiltering_CurlFormat() { require := s.Require() ctx := s.Context() // Test HTTP intercept with header filters using colon format (curl -H compatible) stdout, stderr, err := itest.Telepresence(ctx, "intercept", s.ServiceName(), "--http-header", "X-User-ID: dev123", "--port", "8080") require.NoError(err, "stderr: %s", stderr) require.Contains(stdout, "Using Deployment") // Clean up _, _, err = itest.Telepresence(ctx, "leave", s.ServiceName()) require.NoError(err) } func (s *httpInterceptsSuite) Test_HTTPPathFiltering() { require := s.Require() ctx := s.Context() // Test HTTP intercept with path filters stdout, stderr, err := itest.Telepresence(ctx, "intercept", s.ServiceName(), "--http-path-prefix", "/api/", "--port", "8080") require.NoError(err, "stderr: %s", stderr) require.Contains(stdout, "Using Deployment") // Clean up _, _, err = itest.Telepresence(ctx, "leave", s.ServiceName()) require.NoError(err) } func (s *httpInterceptsSuite) Test_HTTPCombinedFiltering() { require := s.Require() ctx := s.Context() // Test HTTP intercept with both header and path filters, using mixed formats stdout, stderr, err := itest.Telepresence(ctx, "intercept", s.ServiceName(), "--http-header", "X-User-ID=dev123", "--http-header", "Authorization: Bearer token123", "--http-path-prefix", "/api/", "--port", "8080") require.NoError(err, "stderr: %s", stderr) require.Contains(stdout, "Using Deployment") // Clean up _, _, err = itest.Telepresence(ctx, "leave", s.ServiceName()) require.NoError(err) } func (s *httpInterceptsSuite) Test_BackwardCompatibility() { require := s.Require() ctx := s.Context() // Test that standard TCP intercepts still work without HTTP filters stdout, stderr, err := itest.Telepresence(ctx, "intercept", s.ServiceName(), "--port", "8080") require.NoError(err, "stderr: %s", stderr) require.Contains(stdout, "Using Deployment") // Clean up _, _, err = itest.Telepresence(ctx, "leave", s.ServiceName()) require.NoError(err) } func (s *httpInterceptsSuite) Test_HTTPInterceptCoexistence() { require := s.Require() ctx := s.Context() // This test verifies that multiple personal intercepts with different HTTP headers // can coexist on the same workload without conflicts (fixes issue #3969) // Start first personal intercept with x-user=adam stdout1, stderr1, err1 := itest.Telepresence(ctx, "intercept", "echo-one", "--workload", s.ServiceName(), "--http-header", "x-user=adam", "--port", "8080:80", "--mount", "false") require.NoError(err1, "First intercept failed - stderr: %s", stderr1) require.Contains(stdout1, "Using Deployment") // Start second personal intercept with x-user=bertil stdout2, stderr2, err2 := itest.Telepresence(ctx, "intercept", "echo-two", "--workload", s.ServiceName(), "--http-header", "x-user=bertil", "--port", "8081:80", "--mount", "false") require.NoError(err2, "Second intercept failed - stderr: %s", stderr2) require.Contains(stdout2, "Using Deployment") // Verify both intercepts are active and listed listOutput, listStderr, listErr := itest.Telepresence(ctx, "list", "--intercepts") require.NoError(listErr, "Failed to list intercepts - stderr: %s", listStderr) require.Contains(listOutput, "Intercept name: echo-one") require.Contains(listOutput, "Intercept name: echo-two") // Clean up both intercepts _, _, err3 := itest.Telepresence(ctx, "leave", "echo-one") require.NoError(err3, "Failed to leave first intercept") _, _, err4 := itest.Telepresence(ctx, "leave", "echo-two") require.NoError(err4, "Failed to leave second intercept") } func (s *httpInterceptsSuite) Test_TCPPortConflictDetection() { require := s.Require() ctx := s.Context() // This test verifies that real TCP port conflicts are still properly detected // when two intercepts try to use the same local port // Start first intercept using port 8080 stdout1, stderr1, err1 := itest.Telepresence(ctx, "intercept", "tcp-conflict-one", "--workload", s.ServiceName(), "--http-header", "x-user=adam", "--port", "8080:80", "--mount", "false") require.NoError(err1, "First intercept should succeed - stderr: %s", stderr1) require.Contains(stdout1, "Using Deployment") // Attempt second intercept using the SAME local port 8080 // This should fail with a TCP port conflict, not an agent intercept conflict _, stderr2, err2 := itest.Telepresence(ctx, "intercept", "tcp-conflict-two", "--workload", s.ServiceName(), "--http-header", "x-user=bertil", "--port", "8080:80", // Same local port as first intercept "--mount", "false") // Should fail due to real TCP port conflict require.Error(err2, "Second intercept should fail due to TCP port conflict") // Verify it's a TCP port binding error, not an agent intercept conflict require.Contains(stderr2, "127.0.0.1:8080", "Error should mention the conflicting local port") require.Contains(stderr2, "already in use", "Error should indicate port is already in use") // Should NOT contain agent intercept conflict message require.NotContains(stderr2, "Conflicts with", "Should not be an agent intercept conflict") // Clean up the successful intercept _, _, err3 := itest.Telepresence(ctx, "leave", "tcp-conflict-one") require.NoError(err3, "Failed to leave first intercept") } func init() { itest.AddSingleServiceSuite("", "echo", func(h itest.SingleService) itest.TestingSuite { return &httpInterceptsSuite{Suite: itest.Suite{Harness: h}, SingleService: h} }) } func (s *httpInterceptsSuite) Test_HTTPManySimultaneous() { if _, ok := os.LookupEnv("HTTP_INTERCEPT_STRESS_TEST"); !ok { s.T().Skip("Run this stress manually. It's too demanding for the CI infrastructure.") return } require := s.Require() ctx := s.Context() const interceptCount = 250 const pingRepeatCount = 10 localPorts := make([]int, interceptCount) httpCancels := make([]context.CancelFunc, interceptCount) responseFunc := func(name string, r *http.Request) string { return fmt.Sprintf("%s, X-Personal-Id: %s, X-Repeat-Count: %s", name, r.Header.Get("X-Personal-Id"), r.Header.Get("X-Repeat-Count")) } // Ensure that a traffic-agent is running on the workload and capture its log itest.TelepresenceOk(ctx, "intercept", "--mount", "false", s.ServiceName()) itest.TelepresenceOk(ctx, "leave", s.ServiceName()) s.CapturePodLogs(ctx, s.ServiceName(), "traffic-agent", s.AppNamespace()) for i := 0; i < interceptCount; i++ { id := strconv.Itoa(i) localPorts[i], httpCancels[i] = itest.StartLocalHttpEchoServerWithAddr(ctx, "echo-"+id, "localhost:0", responseFunc) } defer func() { for _, cancel := range httpCancels { cancel() } }() for i := 0; i < interceptCount; i++ { id := strconv.Itoa(i) hdr := "X-Personal-Id=" + id svc := "echo-" + id stdout, stderr, err := itest.Telepresence(ctx, "intercept", svc, "--workload", s.ServiceName(), "--http-header", hdr, "--port", strconv.Itoa(localPorts[i])+":80", "--mount", "false") require.NoError(err, "stderr: %s", stderr) require.Contains(stdout, "Using Deployment") } wg := &sync.WaitGroup{} wg.Add(interceptCount * pingRepeatCount) for i := 0; i < interceptCount; i++ { for n := 0; n < pingRepeatCount; n++ { go func(i int) { defer wg.Done() hdr := "X-Personal-Id=" + strconv.Itoa(i) rpt := "X-Repeat-Count=" + strconv.Itoa(n) expectedOutput := fmt.Sprintf("echo-%d, X-Personal-Id: %d, X-Repeat-Count: %d", i, i, n) itest.PingInterceptedEchoServerAndExpect(ctx, s.ServiceName(), "80", expectedOutput, hdr, rpt) }(i) } } wg.Wait() for i := 0; i < interceptCount; i++ { id := strconv.Itoa(i) _, _, err := itest.Telepresence(ctx, "leave", "echo-"+id) require.NoError(err, "Failed to leave intercept echo-"+id) } } func (s *notConnectedSuite) Test_HTTPManyClientsSimultaneous() { testHTTPManyClientsSimultaneous(s, "echo-easy", "/") } func (s *otelSuite) Test_OtelHTTPManyClientsSimultaneous() { testHTTPManyClientsSimultaneous(s, "echo-spring", "/rest/echo") } type NamespaceSuite interface { itest.NamespacePair T() *testing.T Context() context.Context Contains(actual any, expected any, msgAndArgs ...any) bool NoError(err error, msgAndArgs ...any) bool FailNow(msg string, args ...any) bool Eventually(f func() bool, timeout time.Duration, tick time.Duration, msgAndArgs ...any) bool } func testHTTPManyClientsSimultaneous(s NamespaceSuite, svc, path string) { if _, ok := os.LookupEnv("HTTP_INTERCEPT_STRESS_TEST"); !ok { s.T().Skip("Run this stress manually. It's too demanding for the CI infrastructure.") return } ctx := s.Context() // High values here will likely cause errors like "too many open files" unless the docker service is configured to allow more. // On a Linux box, this is typically done by adding a /etc/systemd/system/docker.service.d/override.conf file with the following contents: // [Service] // LimitNOFILE=infinity // // Also add the following line to /etc/docker/daemon.json: // { // "default-ulimits": { // "nofile": { // "Name": "nofile", // "Soft": 64000, // "Hard": 64000 // } // } // } const interceptCount = 16 s.ApplyApp(ctx, svc, "deploy/"+svc) defer s.DeleteSvcAndWorkload(ctx, "deploy", svc) s.TelepresenceConnect(ctx) itest.TelepresenceOk(ctx, "intercept", "--mount", "false", svc) itest.TelepresenceQuitOk(ctx) s.CapturePodLogs(ctx, svc, "traffic-agent", s.AppNamespace()) conns := make([]string, 0, interceptCount) defer func() { for _, connName := range conns { _, _, err := itest.Telepresence(ctx, "--use", connName, "quit") s.NoError(err) } }() for i := 0; i < interceptCount; i++ { id := strconv.Itoa(i) connName := "conn-" + id + "-v" _, err := s.TelepresenceTryConnect(ctx, "--docker", "--name", connName) if err != nil { s.FailNow("Failed to connect to telepresence", err) } conns = append(conns, connName) } wg := &sync.WaitGroup{} wg.Add(interceptCount) for i := 0; i < interceptCount; i++ { id := strconv.Itoa(i) hdr := "X-Personal-Id=" + id connName := conns[i] go func() { defer wg.Done() tpCtx, cancel := context.WithCancel(ctx) outCh := make(chan string) go func() { defer close(outCh) stdout, stderr, err := itest.Telepresence(tpCtx, "--use", connName, "intercept", svc, "--mount=false", "--http-header", hdr, "--docker-run", "--port", "8080:80", "--", "--name", connName+".local", "telepresenceio/echo-server") s.NoError(err, "stderr: %s", stderr) outCh <- stdout }() s.Eventually(func() bool { so, se, err := itest.Telepresence(ctx, "--use", connName, "curl", "--silent", "--max-time", "2", "-H", "X-Personal-Id: "+id, svc+path) if err != nil { clog.Error(ctx, so, se, err) return false } return strings.Contains(so, "Intercepted container") }, 10*time.Second, 1*time.Second) itest.TelepresenceOk(ctx, "--use", connName, "quit") cancel() <-outCh }() } wg.Wait() } ================================================ FILE: integration_test/ignored_mounts_test.go ================================================ package integration_test import ( "fmt" "os" "path/filepath" "strings" "github.com/go-json-experiment/json" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/annotation" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/intercept" ) func (s *mountsSuite) Test_IgnoredMounts() { type lm struct { name string svcPort int ignored []string expected []string notExpected []string } tests := []lm{ { "no ingored volumes", 80, []string{}, []string{ "var/run/secrets/kubernetes.io/serviceaccount", "var/run/secrets/datawire.io/auth", "usr/share/nginx/html", "etc/nginx/templates", }, []string{}, }, { "ignore-by-name", 80, []string{ "hello-data-volume-1", "nginx-config", }, []string{ "var/run/secrets/kubernetes.io/serviceaccount", "var/run/secrets/datawire.io/auth", }, []string{ "usr/share/nginx/html", "etc/nginx/templates", }, }, } localPort, cancel := itest.StartLocalHttpEchoServer(s.Context(), "hello") defer cancel() for _, tt := range tests { s.Run(tt.name, func() { tpl := struct { Annotations map[string]string }{ Annotations: map[string]string{ annotation.InjectIgnoreVolumeMounts: strings.Join(tt.ignored, ","), }, } ctx := s.Context() s.ApplyTemplate(ctx, filepath.Join("testdata", "k8s", "hello-w-volumes.goyaml"), &tpl) defer s.DeleteSvcAndWorkload(ctx, "deploy", "hello") require := s.Require() stdout := itest.TelepresenceOk(ctx, "intercept", "hello", "--output", "json", "--detailed-output", "--port", fmt.Sprintf("%d:%d", localPort, tt.svcPort)) defer itest.TelepresenceOk(ctx, "leave", "hello") var iInfo intercept.Info require.NoError(json.Unmarshal([]byte(stdout), &iInfo)) s.CapturePodLogs(ctx, "hello", "traffic-agent", s.AppNamespace()) mountPoint := iInfo.Mount.LocalDir for _, desired := range tt.expected { st, err := os.Stat(filepath.Join(mountPoint, desired)) if !s.NoErrorf(err, "mount of %s should be successful", desired) { s.T().FailNow() } require.True(st.IsDir()) } for _, notDesired := range tt.notExpected { st, err := os.Stat(filepath.Join(mountPoint, notDesired)) if !s.Errorf(err, "mount of %s should not be successful", notDesired) { clog.Infof(ctx, "stat gave us %s %t %s", st.Name(), st.IsDir(), st.Mode()) } } }) } } ================================================ FILE: integration_test/inactive_client_test.go ================================================ package integration_test import ( "context" "os" "path/filepath" "regexp" "time" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" ) type inactiveClientSuite struct { itest.Suite itest.NamespacePair // The name of the workload to intercept. svc string // The time a client can be inactive before it loses its right to blocks conflicting intercepts. inactiveBlockTimeout time.Duration // How often the client pings the manager to keep its right to block intercepts. pingInterval time.Duration } func (s *inactiveClientSuite) SuiteName() string { return "InactiveClient" } func init() { itest.AddNamespacePairSuite("", func(h itest.NamespacePair) itest.TestingSuite { return &inactiveClientSuite{ Suite: itest.Suite{Harness: h}, NamespacePair: h, svc: "echo-easy", inactiveBlockTimeout: 10 * time.Second, pingInterval: 2 * time.Second, } }) } func (s *inactiveClientSuite) SetupSuite() { s.Suite.SetupSuite() // Default here is 10 minutes. We don't want to wait that long for the test to complete. s.TelepresenceHelmInstallOK(s.Context(), false, "--set", "logLevel=trace,intercept.inactiveBlockTimeout="+s.inactiveBlockTimeout.String()) ctx := s.Context() s.ApplyApp(ctx, s.svc, "deploy/"+s.svc) } func (s *inactiveClientSuite) TearDownSuite() { ctx := s.Context() s.DeleteSvcAndWorkload(ctx, "deploy", s.svc) s.UninstallTrafficManager(ctx, s.ManagerNamespace()) } func (s *inactiveClientSuite) AmendSuiteContext(ctx context.Context) context.Context { return itest.WithConfig(ctx, func(cfg client.Config) { cfg.Intercept().UseFtp = false // Ping is normally one minute, we need to be faster here. cfg.Grpc().PingInterval = s.pingInterval }) } func (s *inactiveClientSuite) Test_ConflictOverrideInactive() { ctx := s.Context() s.TelepresenceConnect(ctx, "--docker", "--name", "conflict-one") defer func() { // Clean up the successful intercept itest.TelepresenceDisconnect(ctx, "--use", "conflict-one") }() require := s.Require() // This test verifies that real TCP port conflicts are still properly detected // when two intercepts try to use the same local port // Start first intercept using port 8080 stdout1, stderr1, err1 := itest.Telepresence(ctx, "--use", "conflict-one", "intercept", "conflict-one", "--workload", s.svc, "--http-header", "x-user=adam", "--port", "8080:80", "--mount", "false") require.NoError(err1, "First intercept should succeed - stderr: %s", stderr1) require.Contains(stdout1, "Using Deployment") s.CapturePodLogs(ctx, s.svc, "traffic-agent", s.AppNamespace()) // Create a second client. s.TelepresenceConnect(ctx, "--docker", "--name", "conflict-two") defer func() { // Clean up the successful intercept itest.TelepresenceDisconnect(ctx, "--use", "conflict-two") }() // Attempt second intercept using the same header. This should fail with a header conflict _, stderr2, err2 := itest.Telepresence(ctx, "--use", "conflict-two", "intercept", "conflict-two", "--workload", s.svc, "--http-header", "x-user=adam", "--port", "8081:80", "--mount", "false") // Should fail due to real TCP port conflict require.Error(err2, "Second intercept should fail due to header conflict") // Verify it's a header conflict s.Contains(stderr2, "header filters overlap") // Sleep until the first client have lost its right to block intercepts. time.Sleep(s.inactiveBlockTimeout + s.pingInterval) // Attempt the second intercept again. This should now succeed. _, _, err2 = itest.Telepresence(ctx, "--use", "conflict-two", "intercept", "conflict-two", "--workload", s.svc, "--http-header", "x-user=adam", "--port", "8081:80", "--mount", "false") s.NoError(err2, "Second intercept should succeed when the first client is sleeping") // The client that woke up should now see the intercept in an error state explaining the conflict. so, se, err := itest.Telepresence(ctx, "--use", "conflict-one", "list", "--intercepts") s.NoErrorf(err, "Failed to list intercepts - stderr: %s", se) if se != "" { clog.Error(ctx, se) } s.Regexp(regexp.MustCompile(`(?m)Intercept name: conflict-one\n.*AGENT_ERROR: conflict with intercept [\w-]+:conflict-two`), so) clog.Info(ctx, so) } func (s *inactiveClientSuite) Test_ConflictOverrideSleeping() { ctx := s.Context() s.TelepresenceConnect(ctx, "--docker", "--name", "conflict-one") defer func() { // Clean up the successful intercept itest.TelepresenceDisconnect(ctx, "--use", "conflict-one") }() require := s.Require() // This test verifies that real TCP port conflicts are still properly detected // when two intercepts try to use the same local port // Start first intercept using port 8080 stdout1, stderr1, err1 := itest.Telepresence(ctx, "--use", "conflict-one", "intercept", "conflict-one", "--workload", s.svc, "--http-header", "x-user=adam", "--port", "8080:80", "--mount", "false") require.NoError(err1, "First intercept should succeed - stderr: %s", stderr1) require.Contains(stdout1, "Using Deployment") s.CapturePodLogs(ctx, s.svc, "traffic-agent", s.AppNamespace()) // Create a second client. s.TelepresenceConnect(ctx, "--docker", "--name", "conflict-two") defer func() { // Clean up the successful intercept itest.TelepresenceDisconnect(ctx, "--use", "conflict-two") }() // Attempt second intercept using the same header. This should fail with a header conflict _, stderr2, err2 := itest.Telepresence(ctx, "--use", "conflict-two", "intercept", "conflict-two", "--workload", s.svc, "--http-header", "x-user=adam", "--port", "8081:80", "--mount", "false") // Should fail due to real TCP port conflict require.Error(err2, "Second intercept should fail due to header conflict") // Verify it's a header conflict s.Contains(stderr2, "header filters overlap") // Try again, but this time put the first client to sleep. s.withSleepingClient(ctx, "conflict-one", func(ctx context.Context) { // Sleep until the first client have lost its right to block intercepts. time.Sleep(s.inactiveBlockTimeout + s.pingInterval) // Attempt the second intercept again. This should now succeed. _, _, err2 = itest.Telepresence(ctx, "--use", "conflict-two", "intercept", "conflict-two", "--workload", s.svc, "--http-header", "x-user=adam", "--port", "8081:80", "--mount", "false") s.NoError(err2, "Second intercept should succeed when the first client is sleeping") }) // The client that woke up should now see the intercept in an error state explaining the conflict. so, se, err := itest.Telepresence(ctx, "--use", "conflict-one", "list", "--intercepts") s.NoErrorf(err, "Failed to list intercepts - stderr: %s", se) if se != "" { clog.Error(ctx, se) } s.Regexp(regexp.MustCompile(`(?m)Intercept name: conflict-one\n.*AGENT_ERROR: conflict with intercept [\w-]+:conflict-two`), so) clog.Info(ctx, so) } func (s *inactiveClientSuite) withSleepingClient(ctx context.Context, clientName string, f func(ctx context.Context)) { // Put the client to sleep but keep its daemon info file alive. We don't want // other CLI commands to remove it when it goes stale. s.Require().NoError(itest.Run(ctx, "docker", "pause", clientName)) infoFile := clientName + ".json" go func() { s.NoError(daemon.NewUserInfoLoader(ctx).KeepInfoAlive(infoFile)) }() f(ctx) // Unpause and restore the info file timestamp (because unpause reverts the timestamp). fullInfoFile := filepath.Join(filelocation.AppUserCacheDir(ctx), "userd", infoFile) st, err := os.Stat(fullInfoFile) s.NoError(err) s.NoError(itest.Run(ctx, "docker", "unpause", clientName)) s.NoError(os.Chtimes(fullInfoFile, time.Time{}, st.ModTime())) } ================================================ FILE: integration_test/ingest_test.go ================================================ package integration_test import ( "context" "os" "path/filepath" "runtime" "sync" "time" "github.com/go-json-experiment/json" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/rpc/v2/connector" "github.com/telepresenceio/telepresence/rpc/v2/daemon" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/ingest" "github.com/telepresenceio/telepresence/v2/pkg/version" ) type ingestSuite struct { itest.Suite itest.TrafficManager mounts []string } func (s *ingestSuite) SuiteName() string { return "Ingest" } func init() { itest.AddTrafficManagerSuite("", func(h itest.TrafficManager) itest.TestingSuite { return &ingestSuite{Suite: itest.Suite{Harness: h}, TrafficManager: h} }) } func (s *ingestSuite) SetupSuite() { ctx := s.Context() s.Suite.SetupSuite() wg := sync.WaitGroup{} wg.Add(3) go func() { defer wg.Done() s.TelepresenceHelmInstallOK(ctx, true, "--set", "intercept.environment.excluded={DATABASE_HOST,DATABASE_PASSWORD}") }() go func() { defer wg.Done() itest.ApplyAppTemplate(ctx, s.AppNamespace(), &itest.AppData{ AppName: "echo-env", Image: "ghcr.io/telepresenceio/echo-server:latest", Ports: []itest.AppPort{ { ServicePortNumber: 80, TargetPortName: "http", TargetPortNumber: 8080, }, }, Env: map[string]string{ "PORT": "8080", "TEST": "DATA", "INTERCEPT": "ENV", "DATABASE_HOST": "HOST_NAME", "DATABASE_PASSWORD": "SUPER_SECRET_PASSWORD", }, }) }() go func() { defer wg.Done() itest.ApplyAppTemplate(ctx, s.AppNamespace(), &itest.AppData{ AppName: "echo", Image: "ghcr.io/telepresenceio/echo-server:latest", Ports: []itest.AppPort{ { ServicePortNumber: 80, TargetPortName: "http", TargetPortNumber: 8080, }, }, Env: map[string]string{"PORT": "8080"}, }) }() wg.Wait() } func (s *ingestSuite) TearDownSuite() { ctx := s.Context() wg := sync.WaitGroup{} wg.Add(3) go func() { defer wg.Done() s.DeleteSvcAndWorkload(ctx, "deploy", "echo") }() go func() { defer wg.Done() s.DeleteSvcAndWorkload(ctx, "deploy", "echo-env") }() go func() { defer wg.Done() s.RollbackTM(ctx) }() wg.Wait() for _, mount := range s.mounts { go func() { time.Sleep(time.Second) _ = os.RemoveAll(mount) }() } } func (s *ingestSuite) mountPoint() string { switch runtime.GOOS { case "windows": return "T:" case "darwin": if s.IsCI() { // Run without mounting on darwin. Apple prevents proper install of kernel extensions return "false" } fallthrough default: mountPoint, err := os.MkdirTemp("", "mount-") // Don't use the testing.Tempdir() because deletion is delayed. s.Require().NoError(err) s.mounts = append(s.mounts, mountPoint) return mountPoint } } func (s *ingestSuite) Test_IngestCLI() { ctx := s.Context() s.TelepresenceConnect(ctx) defer itest.TelepresenceDisconnectOk(ctx) mountPoint := s.mountPoint() js := itest.TelepresenceOk(ctx, "ingest", "--mount", mountPoint, "echo-env", "--output", "json") clog.Info(ctx, js) var rsp ingest.Info s.Require().NoError(json.Unmarshal([]byte(js), &rsp)) env := rsp.Environment s.Empty(env["DATABASE_HOST"]) s.Empty(env["DATABASE_PASSWORD"]) s.Equal("DATA", env["TEST"]) s.Contains("ENV", env["INTERCEPT"]) if mountPoint != "false" { testDir := filepath.Join(rsp.Mount.LocalDir, "var") s.Eventually(func() bool { st, err := os.Stat(testDir) return err == nil && st.Mode().IsDir() }, 15*time.Second, 3*time.Second) } } func (s *ingestSuite) Test_IngestIngestConflict() { mountPoint := s.mountPoint() if mountPoint == "false" { s.T().Skip("mounts disabled on this platform") } ctx := s.Context() s.TelepresenceConnect(ctx) defer itest.TelepresenceDisconnectOk(ctx) itest.TelepresenceOk(ctx, "ingest", "--mount", mountPoint, "echo-env") so, se, err := itest.Telepresence(ctx, "ingest", "--mount", mountPoint, "echo") s.Require().Error(err) s.Empty(so) s.Contains(se, "already in use by ingest") } func (s *ingestSuite) Test_IngestInterceptConflict() { mountPoint := s.mountPoint() if mountPoint == "false" { s.T().Skip("mounts disabled on this platform") } ctx := s.Context() s.TelepresenceConnect(ctx) defer itest.TelepresenceDisconnectOk(ctx) itest.TelepresenceOk(ctx, "ingest", "--mount", mountPoint, "echo-env") so, se, err := itest.Telepresence(ctx, "intercept", "--mount", mountPoint, "echo") s.Require().Error(err) s.Empty(so) s.Contains(se, "already in use by ingest") } func (s *ingestSuite) Test_InterceptIngestConflict() { mountPoint := s.mountPoint() if mountPoint == "false" { s.T().Skip("mounts disabled on this platform") } ctx := s.Context() s.TelepresenceConnect(ctx) defer itest.TelepresenceDisconnectOk(ctx) itest.TelepresenceOk(ctx, "intercept", "--mount", mountPoint, "echo-env") so, se, err := itest.Telepresence(ctx, "ingest", "--mount", mountPoint, "echo") s.Require().Error(err) s.Empty(so) s.Contains(se, "already in use by intercept") } func (s *ingestSuite) Test_IngestRepeat() { mountPoint := s.mountPoint() ctx := s.Context() s.TelepresenceConnect(ctx) defer itest.TelepresenceDisconnectOk(ctx) i1 := itest.TelepresenceOk(ctx, "ingest", "--mount", mountPoint, "echo-env", "--output", "json") i2 := itest.TelepresenceOk(ctx, "ingest", "--mount", mountPoint, "echo-env", "--output", "json") s.Equal(i1, i2) } func (s *ingestSuite) Test_IngestFTP() { if !s.ClientVersion().EQ(version.Structured) { s.T().Skip(`Not part of compatibility tests. DoWithSession assumes compiled executable`) } mountPoint := filepath.Join(s.T().TempDir(), "mnt") rq := s.Require() rq.NoError(os.Mkdir(mountPoint, 0o755)) ctx := s.Context() rq.NoError(s.DoWithSession(ctx, s.NewConnectRequest(ctx), func(ctx context.Context, svc connector.ConnectorServer) { rsp, err := svc.Ingest(ctx, &connector.IngestRequest{ MountPoint: mountPoint, Identifier: &connector.IngestIdentifier{ WorkloadName: "echo-env", }, }) rq.NoError(err) env := rsp.Environment s.Empty(env["DATABASE_HOST"]) s.Empty(env["DATABASE_PASSWORD"]) s.Equal("DATA", env["TEST"]) s.Contains("ENV", env["INTERCEPT"]) testDir := filepath.Join(rsp.ClientMountPoint, "var") s.Eventually(func() bool { st, err := os.Stat(testDir) return err == nil && st.Mode().IsDir() }, 15*time.Second, 3*time.Second) _, err = svc.LeaveIngest(ctx, &connector.IngestIdentifier{ WorkloadName: "echo-env", }) rq.NoError(err) })) } func (s *ingestSuite) Test_IngestProxyVia() { ctx := s.Context() cr := s.NewConnectRequest(ctx) // Simulate --proxy-via all=echo-easy cr.SubnetViaWorkloads = []*daemon.SubnetViaWorkload{ { Subnet: "also", Workload: "echo-env", }, { Subnet: "service", Workload: "echo-env", }, { Subnet: "pods", Workload: "echo-env", }, } ctx = itest.WithConfig(ctx, func(cfg client.Config) { // We currently have no way to make proxy-via work with FTP, because FTP // sends an IP-address in a TCP message. cfg.Intercept().UseFtp = false }) mountPoint := filepath.Join(s.T().TempDir(), "mnt") rq := s.Require() rq.NoError(os.Mkdir(mountPoint, 0o755)) rq.NoError(s.DoWithSession(ctx, cr, func(ctx context.Context, svc connector.ConnectorServer) { rsp, err := svc.Ingest(ctx, &connector.IngestRequest{ MountPoint: mountPoint, Identifier: &connector.IngestIdentifier{ WorkloadName: "echo-env", }, }) rq.NoError(err) env := rsp.Environment s.Empty(env["DATABASE_HOST"]) s.Empty(env["DATABASE_PASSWORD"]) s.Equal("DATA", env["TEST"]) s.Contains("ENV", env["INTERCEPT"]) testDir := filepath.Join(rsp.ClientMountPoint, "var") s.Eventually(func() bool { st, err := os.Stat(testDir) return err == nil && st.Mode().IsDir() }, 15*time.Second, 3*time.Second) _, err = svc.LeaveIngest(ctx, &connector.IngestIdentifier{ WorkloadName: "echo-env", }) rq.NoError(err) })) } func (s *ingestSuite) Test_IngestWithCommand() { ctx := s.Context() s.TelepresenceConnect(ctx) defer itest.TelepresenceDisconnectOk(ctx) mountPoint := s.mountPoint() stdout := itest.TelepresenceOk(ctx, "ingest", "--mount", mountPoint, "echo", "--", "echo", "test-output") s.Contains(stdout, "test-output") } func (s *ingestSuite) Test_IngestWithContainerAndCommand() { ctx := s.Context() s.TelepresenceConnect(ctx) defer itest.TelepresenceDisconnectOk(ctx) mountPoint := s.mountPoint() stdout := itest.TelepresenceOk(ctx, "ingest", "--mount", mountPoint, "--container", "echo", "echo", "--", "echo", "explicit-container") s.Contains(stdout, "explicit-container") } func (s *ingestSuite) Test_LeaveIngestWithoutContainer() { ctx := s.Context() s.TelepresenceConnect(ctx) defer itest.TelepresenceDisconnectOk(ctx) mountPoint := s.mountPoint() itest.TelepresenceOk(ctx, "ingest", "--mount", mountPoint, "echo") itest.TelepresenceOk(ctx, "leave", "echo") stdout := itest.TelepresenceOk(ctx, "list", "--ingests") s.NotContains(stdout, "echo") } func (s *ingestSuite) Test_IngestListFormat() { ctx := s.Context() s.TelepresenceConnect(ctx) defer itest.TelepresenceDisconnectOk(ctx) mountPoint := s.mountPoint() itest.TelepresenceOk(ctx, "ingest", "--mount", mountPoint, "echo-env") stdout := itest.TelepresenceOk(ctx, "list", "--ingests") s.Contains(stdout, "echo-env") itest.TelepresenceOk(ctx, "leave", "echo-env") } ================================================ FILE: integration_test/inject_policy_test.go ================================================ package integration_test import ( "context" "fmt" "path/filepath" "runtime" "strings" "sync" "time" labels2 "k8s.io/apimachinery/pkg/labels" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/agentconfig" "github.com/telepresenceio/telepresence/v2/pkg/annotation" "github.com/telepresenceio/telepresence/v2/pkg/labels" ) func (is *installSuite) applyPolicyApp(ctx context.Context, name, namespace string, wg *sync.WaitGroup) { defer wg.Done() is.T().Helper() manifest := filepath.Join("testdata", "k8s", name+".yaml") is.NoError(itest.Kubectl(ctx, namespace, "apply", "-f", manifest), "failed to apply %s", manifest) is.NoError(itest.RolloutStatusWait(ctx, namespace, "deploy/"+name)) } func (is *installSuite) assertInjected(ctx context.Context, name, namespace string, present bool, wg *sync.WaitGroup) { defer wg.Done() is.T().Helper() out, err := itest.KubectlOut(ctx, namespace, "get", "pods", "-l", "app="+name, "-o", "jsonpath={.items.*.spec.containers[?(@.name=='traffic-agent')].image}") is.NoError(err) n := "tel2" if ai := itest.GetAgentImage(ctx); ai != nil { n = ai.Name } n = "/" + n + ":" if present { is.Contains(out, n) } else { is.NotContains(out, n) } } func (is *installSuite) injectPolicyTest(ctx context.Context, policy agentconfig.InjectPolicy) { namespace := fmt.Sprintf("%s-%s", strings.ToLower(policy.String()), is.Suffix()) itest.CreateNamespaces(ctx, namespace) defer itest.DeleteNamespaces(ctx, namespace) ctx = itest.WithNamespaces(ctx, &itest.Namespaces{ Namespace: namespace, Selector: labels.SelectorFromNames(namespace), }) is.TelepresenceHelmInstallOK(ctx, false, "--set", "agentInjector.injectPolicy="+policy.String()) defer is.UninstallTrafficManager(ctx, namespace) ctx = itest.WithUser(ctx, namespace+":"+itest.TestUser) itest.TelepresenceOk(ctx, "connect", "--namespace", namespace, "--manager-namespace", namespace) defer itest.TelepresenceOk(ctx, "quit", "-s") itest.TelepresenceOk(ctx, "loglevel", "debug") wg := sync.WaitGroup{} wg.Add(3) go is.applyPolicyApp(ctx, "pol-enabled", namespace, &wg) go is.applyPolicyApp(ctx, "pol-none", namespace, &wg) go is.applyPolicyApp(ctx, "pol-disabled", namespace, &wg) wg.Wait() if is.T().Failed() { return } // No pod should have a traffic-agent at this stage, except for the pol-enabled when the policy is WhenEnabled wg.Add(3) go is.assertInjected(ctx, "pol-enabled", namespace, true, &wg) // always injected in advance go is.assertInjected(ctx, "pol-none", namespace, false, &wg) // never injected in advance go is.assertInjected(ctx, "pol-disabled", namespace, false, &wg) // never injected wg.Wait() if is.T().Failed() { return } // An intercept on the pol-disabled must always fail wg.Add(1) go func() { _, _, err := itest.Telepresence(ctx, "intercept", "--mount", "false", "pol-disabled", "--", "true") is.Error(err) is.assertInjected(ctx, "pol-disabled", namespace, false, &wg) }() // for OnDemand, an intercept on the pol-none must succeed inject the agent if policy == agentconfig.OnDemand { wg.Add(1) _, _, err := itest.Telepresence(ctx, "intercept", "--mount", "false", "pol-none", "--", "true") is.NoError(err) is.assertInjected(ctx, "pol-none", namespace, true, &wg) } wg.Wait() } func (is *installSuite) TestInjectPolicy() { for _, policy := range []agentconfig.InjectPolicy{agentconfig.OnDemand, agentconfig.WhenEnabled} { is.Run(policy.String(), func() { is.injectPolicyTest(is.Context(), policy) }) } } func (is *installSuite) applyMultipleServices(svcCount int) { is.applyOrDeleteMultipleServices(svcCount, is.ApplyTemplate) // And check that all pods receive a traffic-agent is.Eventually(func() bool { pods := itest.RunningPodsSelector(is.Context(), is.AppNamespace(), labels2.SelectorFromSet(map[string]string{ "multi-service-test": "inject", })) clog.Infof(is.Context(), "pod count %d, expected %d", len(pods), svcCount) return len(pods) == svcCount }, 120*time.Second, 5*time.Second) } func (is *installSuite) deleteMultipleServices(svcCount int) { is.applyOrDeleteMultipleServices(svcCount, is.DeleteTemplate) } func (is *installSuite) applyOrDeleteMultipleServices(svcCount int, applyOrDelete func(context.Context, string, any)) { ctx := is.Context() wg := sync.WaitGroup{} wg.Add(svcCount) for i := range svcCount { svc := fmt.Sprintf("quote-%d", i) go func() { defer wg.Done() k8s := filepath.Join("testdata", "k8s") applyOrDelete(ctx, filepath.Join(k8s, "generic.goyaml"), &itest.Generic{ Name: svc, Registry: "datawire", Image: "quote:0.5.0", Annotations: map[string]string{ annotation.InjectTrafficAgent: "enabled", }, Labels: map[string]string{ "multi-service-test": "inject", }, }) }() } wg.Wait() } func (is *installSuite) Test_MultiOnDemandInjectOnInstall() { if !(is.ManagerIsVersion(">2.24.x") && is.ClientIsVersion(">2.24.x")) { is.T().Skip("Not part of compatibility tests.") } svcCount := 8 if runtime.GOOS != "linux" { // The GitHub runner is probably using Colima for Kubernetes and running with limited // resources. svcCount = 4 } ctx := is.Context() // First create the pods with inject annotation is.applyMultipleServices(svcCount) defer is.deleteMultipleServices(svcCount) // Then install the traffic-manager is.TelepresenceHelmInstallOK(ctx, false) defer func() { // Uninstall the traffic-manager and check that all pods traffic-agent is removed is.UninstallTrafficManager(ctx, is.ManagerNamespace()) is.Eventually(func() bool { ras := itest.RunningPodsWithAgents(ctx, "quote-", is.AppNamespace()) clog.Infof(ctx, "pod with agent count %d, expected 0", len(ras)) return len(ras) == 0 }, 120*time.Second, 5*time.Second) }() // And check that all pods receive a traffic-agent is.Eventually(func() bool { ras := itest.RunningPodsWithAgents(ctx, "quote-", is.AppNamespace()) clog.Infof(ctx, "pod with agent count %d, expected %d", len(ras), svcCount) return len(ras) == svcCount }, 120*time.Second, 5*time.Second) } func (is *installSuite) Test_MultiOnDemandInjectOnApply() { if !(is.ManagerIsVersion(">2.24.x") && is.ClientIsVersion(">2.24.x")) { is.T().Skip("Not part of compatibility tests.") } svcCount := 8 if runtime.GOOS != "linux" { // The GitHub runner is probably using Colima for Kubernetes and running with limited // resources. svcCount = 4 } ctx := is.Context() // First install the traffic-manager is.TelepresenceHelmInstallOK(ctx, false) time.Sleep(3 * time.Second) defer func() { is.UninstallTrafficManager(ctx, is.ManagerNamespace()) is.Eventually(func() bool { ras := itest.RunningPodsWithAgents(ctx, "quote-", is.AppNamespace()) clog.Infof(ctx, "pod with agent count %d, expected 0", len(ras)) return len(ras) == 0 }, 120*time.Second, 5*time.Second) }() // Then create the pods with inject annotation is.applyMultipleServices(svcCount) defer is.deleteMultipleServices(svcCount) // And check that all pods receive a traffic-agent is.Require().Eventually(func() bool { ras := itest.RunningPodsWithAgents(ctx, "quote-", is.AppNamespace()) clog.Infof(ctx, "pod with agent count %d, expected %d", len(ras), svcCount) return len(ras) == svcCount }, 120*time.Second, 5*time.Second) } ================================================ FILE: integration_test/injector_test.go ================================================ package integration_test import ( "context" "fmt" "regexp" "time" "github.com/go-json-experiment/json" core "k8s.io/api/core/v1" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" ) // Test_InterceptOperationRestoredAfterFailingInject tests that the telepresence-agents // configmap is kept in sync with installed agents after errors occurs during the actual // injection of a traffic-agent. // See ticket https://github.com/telepresenceio/telepresence/issues/3441 for more info. func (s *singleServiceSuite) Test_InterceptOperationRestoredAfterFailingInject() { if s.ClientIsVersion("<2.22.0") && s.ManagerIsVersion(">=2.22.0") { s.T().Skip("Not part of compatibility tests. Clients < 2.22.0 cannot uninstall agents with traffic-manager >= 2.22.0") } ctx := s.Context() rq := s.Require() oneContainer := func() bool { pods := itest.RunningPodNames(ctx, s.ServiceName(), s.AppNamespace()) if len(pods) != 1 { clog.Infof(ctx, "got %d pods", len(pods)) return false } podJSON, err := s.KubectlOut(ctx, "get", "pod", pods[0], "--output", "json") if err != nil { clog.Errorf(ctx, "unable to get pod %s: %v", pods[0], err) return false } var pod core.Pod err = json.Unmarshal([]byte(podJSON), &pod) if err != nil { clog.Errorf(ctx, "unable to parse json of pod %s: %v", pods[0], err) return false } nc := len(pod.Spec.Containers) if nc == 1 { return true } clog.Errorf(ctx, "pod %s has %d containers", pods[0], nc) return false } // Ensure that agent is uninstalled. so, se, err := itest.Telepresence(ctx, "uninstall", s.ServiceName()) // We don't care if it succeeds, but the output and error might be of interest when debugging. clog.Debugf(ctx, "stdout: %s, stderr %s, err: %v", so, se, err) rq.Eventually(oneContainer, 60*time.Second, 3*time.Second) // Break the TLS by temporally disabling the agent-injector service. We do this by the port of the // service that the webhook is calling. wh := "agent-injector-webhook-" + s.ManagerNamespace() pmf := `{"webhooks":[{"name": "agent-injector-%s.telepresence.io", "clientConfig": {"service": {"name": "agent-injector", "port": %d}}}]}` rq.NoError(itest.Kubectl(ctx, s.ManagerNamespace(), "patch", "mutatingwebhookconfiguration", wh, "--patch", fmt.Sprintf(pmf, s.ManagerNamespace(), 3443))) injectorSvcPort := 443 if s.ManagerIsVersion(">=2.24.0") { injectorSvcPort = 8443 } // Restore the webhook port when this test ends in case an error occurred that prevented it portRestored := false defer func() { if !portRestored { s.NoError(itest.Kubectl(ctx, s.ManagerNamespace(), "patch", "mutatingwebhookconfiguration", wh, "--patch", fmt.Sprintf(pmf, s.ManagerNamespace(), injectorSvcPort))) } }() // Now try to intercept. This attempt will timeout because the agent is never injected. _, _, err = itest.Telepresence(ctx, "intercept", s.ServiceName(), "--mount=false") // Wait for the intercept call to return. It must return an error. rq.Error(err) // Verify that the pod still has no agent rq.True(oneContainer()) // Restore mutating-webhook operation. rq.NoError(itest.Kubectl(ctx, s.ManagerNamespace(), "patch", "mutatingwebhookconfiguration", wh, "--patch", fmt.Sprintf(pmf, s.ManagerNamespace(), injectorSvcPort))) portRestored = true // Verify that intercept works OK again. stdout := itest.TelepresenceOk(ctx, "intercept", s.ServiceName(), "--mount=false") rq.Contains(stdout, "Using Deployment "+s.ServiceName()) rq.Eventually(func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--intercepts") return err == nil && regexp.MustCompile(s.ServiceName()+`\s*: intercepted`).MatchString(stdout) }, 12*time.Second, 3*time.Second) itest.TelepresenceOk(ctx, "leave", s.ServiceName()) } // Test_HelmUpgradeWebhookSecret tests that updating the webhook secret doesn't interfere with // intercept operations. // See https://github.com/telepresenceio/telepresence/issues/3442 for more info. func (s *singleServiceSuite) Test_HelmUpgradeWebhookSecret() { ctx := s.Context() rq := s.Require() stdout := itest.TelepresenceOk(ctx, "intercept", s.ServiceName(), "--mount=false") rq.Contains(stdout, "Using Deployment "+s.ServiceName()) rq.Eventually(func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--intercepts") return err == nil && regexp.MustCompile(s.ServiceName()+`\s*: intercepted`).MatchString(stdout) }, 12*time.Second, 3*time.Second) s.TelepresenceHelmInstallOK(ctx, true, "--set", "agentInjector.certificate.regenerate=true,agentInjector.certificate.accessMethod=watch,logLevel=debug") defer s.RollbackTM(ctx) time.Sleep(5 * time.Second) // Check that the intercept is still active st := itest.TelepresenceStatusOk(ctx) rq.Len(st.UserDaemon.Intercepts, 1) itest.TelepresenceOk(ctx, "leave", s.ServiceName()) // Uninstall the agent again. We want to be sure that the webhook kicks in to inject it once // we intercept. func() { defer func() { // Restore original user itest.TelepresenceDisconnectOk(ctx) s.TelepresenceConnect(ctx) }() itest.TelepresenceDisconnectOk(ctx) s.TelepresenceConnect(itest.WithUser(ctx, "default")) itest.TelepresenceOk(ctx, "uninstall", s.ServiceName()) }() stdout = itest.TelepresenceOk(ctx, "intercept", s.ServiceName(), "--mount=false") rq.Contains(stdout, "Using Deployment "+s.ServiceName()) rq.Eventually(func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--intercepts") return err == nil && regexp.MustCompile(s.ServiceName()+`\s*: intercepted`).MatchString(stdout) }, 12*time.Second, 3*time.Second) itest.TelepresenceOk(ctx, "leave", s.ServiceName()) } // Test_HelmUpgradeMountedWebhookSecret tests that updating the webhook secret does interfere with // intercept operations. func (s *singleServiceSuite) Test_HelmUpgradeMountedWebhookSecret() { if !(s.ManagerIsVersion(">2.24.x") && s.ClientIsVersion(">2.24.x")) { s.T().Skip("Not part of compatibility tests. Uninterrupted intercepts was implemented in 2.25.0") } ctx := s.Context() rq := s.Require() stdout := itest.TelepresenceOk(ctx, "intercept", s.ServiceName(), "--mount=false") rq.Contains(stdout, "Using Deployment "+s.ServiceName()) rq.Eventually(func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--intercepts") return err == nil && regexp.MustCompile(s.ServiceName()+`\s*: intercepted`).MatchString(stdout) }, 12*time.Second, 3*time.Second) s.TelepresenceHelmInstallOK(ctx, true, "--set", "agentInjector.certificate.regenerate=true,agentInjector.certificate.accessMethod=mount,logLevel=debug") time.Sleep(5 * time.Second) defer func() { itest.TelepresenceDisconnectOk(ctx) s.RollbackTM(context.WithoutCancel(ctx)) time.Sleep(5 * time.Second) s.TelepresenceConnect(ctx) }() // Using accessMethod=mount will restart the traffic-manager, but the intercept should still be active. st := itest.TelepresenceStatusOk(ctx) rq.Len(st.UserDaemon.Intercepts, 1) // Uninstall the agent again. We want to be sure that the webhook kicks in to inject it once // we intercept. func() { defer func() { // Restore original user itest.TelepresenceDisconnectOk(ctx) s.TelepresenceConnect(ctx) }() itest.TelepresenceDisconnectOk(ctx) s.TelepresenceConnect(itest.WithUser(ctx, "default")) itest.TelepresenceOk(ctx, "uninstall", s.ServiceName()) }() stdout = itest.TelepresenceOk(ctx, "intercept", s.ServiceName(), "--mount=false") rq.Contains(stdout, "Using Deployment "+s.ServiceName()) rq.Eventually(func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--intercepts") return err == nil && regexp.MustCompile(s.ServiceName()+`\s*: intercepted`).MatchString(stdout) }, 12*time.Second, 3*time.Second) itest.TelepresenceOk(ctx, "leave", s.ServiceName()) } ================================================ FILE: integration_test/install_test.go ================================================ package integration_test import ( "archive/tar" "compress/gzip" "context" "fmt" "io" "log/slog" "os" "path/filepath" "runtime" "strings" "time" "github.com/go-json-experiment/json" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/cli/values" "helm.sh/helm/v3/pkg/getter" rbac "k8s.io/api/rbac/v1" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/tools/clientcmd/api" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/agentconfig" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/helm" "github.com/telepresenceio/telepresence/v2/pkg/client/k8s" "github.com/telepresenceio/telepresence/v2/pkg/dos" "github.com/telepresenceio/telepresence/v2/pkg/k8sapi" "github.com/telepresenceio/telepresence/v2/pkg/version" ) const ManagerAppName = agentconfig.ManagerAppName type installSuite struct { itest.Suite itest.NamespacePair } func (is *installSuite) SuiteName() string { return "Install" } func init() { itest.AddNamespacePairSuite("-install", func(h itest.NamespacePair) itest.TestingSuite { return &installSuite{Suite: itest.Suite{Harness: h}, NamespacePair: h} }) } func getHelmConfig(ctx context.Context, clientGetter genericclioptions.RESTClientGetter, namespace string) (*action.Configuration, error) { helmConfig := &action.Configuration{} err := helmConfig.Init(clientGetter, namespace, "secrets", func(format string, args ...any) { ctx := clog.With(ctx, "source", "helm") clog.Infof(ctx, format, args...) }) if err != nil { return nil, err } return helmConfig, nil } func (is *installSuite) AmendSuiteContext(ctx context.Context) context.Context { if !is.ManagerVersion().EQ(is.ClientVersion()) { // Need to use the built executable because the client version doesn't handle the --version flag. exe, _ := is.Executable() ctx = itest.WithExecutable(ctx, exe) } return ctx } func (is *installSuite) Test_UpgradeRetainsValues() { ctx := is.Context() rq := is.Require() is.TelepresenceHelmInstallOK(ctx, false, "--set", "logLevel=debug") defer is.UninstallTrafficManager(ctx, is.ManagerNamespace()) kc := is.cluster(ctx, "", is.ManagerNamespace()) helmConfig, err := getHelmConfig(kc, kc.Kubeconfig, is.ManagerNamespace()) rq.NoError(err) getValues := func() (map[string]any, error) { return action.NewGetValues(helmConfig).Run(agentconfig.ManagerAppName) } containsKey := func(m map[string]any, key string) bool { _, ok := m[key] return ok } oldValues, err := getValues() rq.NoError(err) args := []string{"helm", "upgrade", "--namespace", is.ManagerNamespace()} if !is.ManagerVersion().EQ(version.Structured) { args = append(args, "--version", is.ManagerVersion().String()) } is.Run("default reuse-values", func() { itest.TelepresenceOk(is.Context(), args...) newValues, err := getValues() if is.NoError(err) { is.Equal(oldValues, newValues) } }) is.Run("default reset-values", func() { // Setting a value means that the default behavior is to reset old values. itest.TelepresenceOk(is.Context(), append(args, "--set", "apiPort=8765")...) newValues, err := getValues() if is.NoError(err) { is.Equal(8765.0, newValues["apiPort"]) is.False(containsKey(newValues, "logLevel")) // Should be back at default } }) is.Run("explicit reuse-values", func() { // Set new value and enforce merge with of old values. itest.TelepresenceOk(is.Context(), append(args, "--set", "logLevel=debug", "--reuse-values")...) newValues, err := getValues() if is.NoError(err) { is.Equal(8765.0, newValues["apiPort"]) is.Equal("debug", newValues["logLevel"]) } }) is.Run("explicit reset-values", func() { // Enforce reset of old values. itest.TelepresenceOk(is.Context(), append(args, "--reset-values")...) newValues, err := getValues() if is.NoError(err) { is.False(containsKey(newValues, "apiPort")) // Should be back at default is.False(containsKey(newValues, "logLevel")) // Should be back at default } }) } func (is *installSuite) Test_HelmTemplateInstall() { if !(is.ManagerVersion().EQ(version.Structured) && is.ClientVersion().EQ(version.Structured)) { is.T().Skip("Not part of compatibility tests. PackageHelmChart assumes current version.") } ctx := is.Context() require := is.Require() chart, err := is.PackageHelmChart(ctx) require.NoError(err) values := is.GetSetArgsForHelm(ctx, map[string]any{ "clientRbac.create": true, "clientRbac.subjects": []rbac.Subject{{ Kind: "ServiceAccount", Name: itest.TestUser, Namespace: is.ManagerNamespace(), }}, "managerRbac.create": true, }, false) require.NoError(err) values = append([]string{"template", agentconfig.ManagerAppName, chart, "-n", is.ManagerNamespace()}, values...) manifest, err := itest.Output(ctx, "helm", values...) require.NoError(err) out := clog.StdLogger(ctx, slog.LevelInfo).Writer() logCtx := dos.WithStdout(dos.WithStderr(ctx, out), out) require.NoError(itest.Kubectl(dos.WithStdin(logCtx, strings.NewReader(manifest)), "", "apply", "-f", "-")) defer func() { // Sometimes the traffic-agents configmap gets wiped, causing the delete command to fail, hence we don't require.NoError _ = itest.Kubectl(dos.WithStdin(logCtx, strings.NewReader(manifest)), "", "delete", "-f", "-") }() require.NoError(itest.RolloutStatusWait(ctx, is.ManagerNamespace(), "deploy/"+agentconfig.ManagerAppName)) is.CapturePodLogs(ctx, agentconfig.ManagerAppName, "", is.ManagerNamespace()) stdout := is.TelepresenceConnect(ctx) is.Contains(stdout, "Connected to context") itest.TelepresenceQuitOk(ctx) } func (is *installSuite) Test_FindTrafficManager_notPresent() { kc := is.cluster(is.Context(), "", is.ManagerNamespace()) // ensure that k8sapi is initialized sv := version.Version version.Version = "v0.0.0-bogus" defer func() { version.Version = sv }() _, err := k8sapi.GetDeployment(kc, ManagerAppName, is.ManagerNamespace()) is.Error(err, "expected find to not find traffic-manager deployment") } func (is *installSuite) Test_EnsureManager_toleratesFailedInstall() { if !(is.ManagerVersion().EQ(version.Structured) && is.ClientVersion().EQ(version.Structured)) { is.T().Skip("Not part of compatibility tests.") } require := is.Require() ctx := is.Context() sv := version.Version version.Version = "v0.0.0-bogus" restoreVersion := func() { version.Version = sv } // We'll call this further down, but defer it to prevent polluting other tests if we don't leave this function gracefully defer restoreVersion() defer is.UninstallTrafficManager(ctx, is.ManagerNamespace()) failCtx := itest.WithConfig(ctx, func(cfg client.Config) { cfg.Timeouts().PrivateHelm = 20 * time.Second // Give it time to discover the ImagePullbackOff error }) kc := is.cluster(failCtx, "", is.ManagerNamespace()) err := ensureTrafficManager(kc) require.Error(err) clog.Infof(ctx, "Got expected install failure: %v", err) restoreVersion() okCtx := itest.WithConfig(ctx, func(cfg client.Config) { cfg.Timeouts().PrivateHelm = 20 * time.Second // Time to wait before pending state makes us assume it's stuck. }) kc = is.cluster(okCtx, "", is.ManagerNamespace()) if !is.Eventually(func() bool { err = ensureTrafficManager(kc) if err != nil { clog.Errorf(ctx, "ensureTrafficManager failed: %v", err) } return err == nil }, time.Minute, 5*time.Second) { is.Fail(fmt.Sprintf("Unable to install proper manager after failed install: %v", err)) } } func (is *installSuite) Test_RemoveManager_canUninstall() { if !(is.ManagerVersion().EQ(version.Structured) && is.ClientVersion().EQ(version.Structured)) { is.T().Skip("Not part of compatibility tests.") } require := is.Require() ctx := is.Context() kc := is.cluster(ctx, "", is.ManagerNamespace()) require.NoError(ensureTrafficManager(kc)) require.NoError(helm.DeleteTrafficManager(ctx, kc.Kubeconfig, k8s.GetManagerNamespace(kc), true, &helm.Request{})) // We want to make sure that we can re-install the manager after it's been uninstalled, // so try to ensureManager again. require.NoError(ensureTrafficManager(kc)) // Uninstall the manager one last time -- this should behave the same way as the previous uninstall require.NoError(helm.DeleteTrafficManager(kc, kc.Kubeconfig, k8s.GetManagerNamespace(kc), true, &helm.Request{})) } func (is *installSuite) Test_No_Upgrade() { if !(is.ManagerVersion().EQ(version.Structured) && is.ClientVersion().EQ(version.Structured)) { is.T().Skip("Not part of compatibility tests.") } ctx := is.Context() require := is.Require() kc := is.cluster(ctx, "", is.ManagerNamespace()) defer is.UninstallTrafficManager(kc, is.ManagerNamespace()) // first install require.NoError(ensureTrafficManager(kc)) // errors and asks for telepresence upgrade require.Error(ensureTrafficManager(kc)) // using upgrade and --values replaces TM with values helmValues := filepath.Join("testdata", "routing-values.yaml") opts := values.Options{ValueFiles: []string{helmValues}} vp, err := opts.MergeValues(getter.Providers{}) require.NoError(err) jvp, err := json.Marshal(vp) require.NoError(err) require.NoError(helm.EnsureTrafficManager(kc, kc.Kubeconfig, k8s.GetManagerNamespace(kc), &helm.Request{ Type: helm.Upgrade, ValuesJson: jvp, })) } func (is *installSuite) Test_findTrafficManager_differentNamespace_present() { if !(is.ManagerVersion().EQ(version.Structured) && is.ClientVersion().EQ(version.Structured)) { is.T().Skip("Not part of compatibility tests.") } ctx := is.Context() customNamespace := fmt.Sprintf("custom-%d", os.Getpid()) itest.CreateNamespaces(ctx, customNamespace) defer itest.DeleteNamespaces(ctx, customNamespace) defer is.UninstallTrafficManager(ctx, customNamespace) ctx = itest.WithKubeConfigExtension(ctx, func(cluster *api.Cluster) map[string]any { return map[string]any{"manager": map[string]string{"namespace": customNamespace}} }) is.findTrafficManagerPresent(ctx, "extra", customNamespace) } func (is *installSuite) findTrafficManagerPresent(ctx context.Context, context, namespace string) { kc := is.cluster(ctx, context, namespace) require := is.Require() require.NoError(ensureTrafficManager(kc)) require.Eventually(func() bool { dep, err := k8sapi.GetDeployment(kc, ManagerAppName, namespace) if err != nil { clog.Error(ctx, err) return false } v := strings.TrimPrefix(version.Version, "v") img := dep.GetPodTemplate().Spec.Containers[0].Image clog.Infof(ctx, "traffic-manager image %s, our version %s", img, v) return strings.Contains(img, v) }, 10*time.Second, 2*time.Second, "traffic-manager deployment not found") } func (is *installSuite) cluster(ctx context.Context, context, managerNamespace string) *k8s.Cluster { cluster, err := is.GetK8SCluster(ctx, context, managerNamespace) is.Require().NoError(err) return cluster } func ensureTrafficManager(kc *k8s.Cluster) error { return helm.EnsureTrafficManager( kc, kc.Kubeconfig, k8s.GetManagerNamespace(kc), &helm.Request{Type: helm.Install}) } func unTgz(ctx context.Context, srcTgz, dstPath string) error { rd, err := os.Open(srcTgz) if err != nil { return err } defer rd.Close() err = dos.MkdirAll(ctx, dstPath, 0o755) if err != nil { return err } zrd, err := gzip.NewReader(rd) if err != nil { return err } src := tar.NewReader(zrd) for { header, err := src.Next() if err != nil { if err == io.EOF { break } return err } dst := dstPath + "/" + header.Name mode := os.FileMode(header.Mode) switch header.Typeflag { case tar.TypeDir: err = dos.MkdirAll(ctx, dst, mode) if err != nil { return err } case tar.TypeReg: err = dos.MkdirAll(ctx, filepath.Dir(dst), 0o755) if err != nil { return err } w, err := dos.OpenFile(ctx, dst, os.O_CREATE|os.O_WRONLY, mode) if err != nil { return err } _, err = io.Copy(w, src) _ = w.Close() if err != nil { return err } default: return fmt.Errorf("unable to untar type : %c in file %s", header.Typeflag, header.Name) } } return nil } func (is *installSuite) Test_HelmSubChart() { if runtime.GOOS == "windows" || !(is.ManagerVersion().EQ(version.Structured) && is.ClientVersion().EQ(version.Structured)) { is.T().Skip("Not part of compatibility tests. Need forward slashes in path, and PackageHelmChart assumes current version.") } ctx := is.Context() require := is.Require() t := is.T() subChart, err := is.PackageHelmChart(ctx) require.NoError(err) base := t.TempDir() require.NoError(unTgz(ctx, subChart, filepath.Join(base, "charts"))) chart := fmt.Sprintf(`apiVersion: v2 dependencies: - name: telepresence-oss registry: ../charts/telepresence-oss version: %s condition: enabled description: Helm chart to deploy telepresence name: parent version: 1.0.0`, is.ClientVersion()) vals := is.GetSetArgsForHelm(ctx, map[string]any{ "global": map[string]any{ "some-string": "value", "some-obj": map[string]any{ "foo": "bar", }, "some-bool": true, }, "telepresence-oss": map[string]any{ "clientRbac": map[string]any{ "create": true, "subjects": []rbac.Subject{ { Kind: "ServiceAccount", Name: itest.TestUser, Namespace: is.ManagerNamespace(), }, }, }, }, }, false) require.NoError(dos.WriteFile(ctx, filepath.Join(base, "Chart.yaml"), []byte(chart), 0o644)) vals = append([]string{"template", "parent", base, "-n", is.ManagerNamespace()}, vals...) so, err := itest.Output(ctx, "helm", vals...) require.NoError(err) require.Contains(so, "# Source: parent/charts/telepresence-oss/templates/clientRbac/connect.yaml") require.Contains(so, "name: "+itest.TestUser) } ================================================ FILE: integration_test/integration_test.go ================================================ package integration_test import ( "path/filepath" "testing" "github.com/telepresenceio/telepresence/v2/integration_test/itest" ) func Test_Integration(t *testing.T) { ossRoot, err := filepath.Abs(".") if err != nil { t.Fatalf("unable to get absolute path of .oss: %v", err) } itest.RunTests(itest.TestContext(t, ossRoot, filepath.Dir(ossRoot))) } ================================================ FILE: integration_test/intercept_env_test.go ================================================ package integration_test import ( "os" "path/filepath" "time" "github.com/telepresenceio/telepresence/v2/integration_test/itest" ) type interceptEnvSuite struct { itest.Suite itest.NamespacePair } func (s *interceptEnvSuite) SuiteName() string { return "InterceptEnv" } func init() { itest.AddNamespacePairSuite("", func(h itest.NamespacePair) itest.TestingSuite { return &interceptEnvSuite{Suite: itest.Suite{Harness: h}, NamespacePair: h} }) } func (s *interceptEnvSuite) Test_ExcludeVariables() { // given ctx := s.Context() s.TelepresenceHelmInstallOK(ctx, false, "--set", "intercept.environment.excluded={DATABASE_HOST,DATABASE_PASSWORD}") defer s.UninstallTrafficManager(ctx, s.ManagerNamespace()) s.ApplyApp(ctx, "echo_with_env", "deploy/echo-easy") defer s.DeleteSvcAndWorkload(ctx, "deploy", "echo-easy") helloEnv := filepath.Join(s.T().TempDir(), "echo.env") // when s.TelepresenceConnect(ctx) itest.TelepresenceOk(ctx, "intercept", "echo-easy", "--env-file", helloEnv) // then var file string s.Require().Eventually(func() bool { if dt, err := os.ReadFile(helloEnv); err == nil { file = string(dt) return true } return false }, 5*time.Second, 1*time.Second) s.NotContains(file, "DATABASE_HOST") s.NotContains(file, "DATABASE_PASSWORD") s.Contains(file, "TEST=DATA") s.Contains(file, "INTERCEPT=ENV") } ================================================ FILE: integration_test/intercept_flags_test.go ================================================ package integration_test import ( "context" "fmt" "os" "path/filepath" "regexp" "runtime" "time" "github.com/go-json-experiment/json" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/intercept" "github.com/telepresenceio/telepresence/v2/pkg/iputil" ) type interceptFlagSuite struct { itest.Suite itest.TrafficManager serviceName string } func (s *interceptFlagSuite) SuiteName() string { return "InterceptFlag" } func init() { itest.AddTrafficManagerSuite("-intercept-flag", func(h itest.TrafficManager) itest.TestingSuite { return &interceptFlagSuite{Suite: itest.Suite{Harness: h}, TrafficManager: h} }) } func (s *interceptFlagSuite) SetupSuite() { if s.IsCI() && runtime.GOOS == "darwin" { s.T().Skip("Mount tests don't run on darwin due to macFUSE issues") return } s.Suite.SetupSuite() ctx := s.Context() s.serviceName = "hello" s.ApplyTemplate(ctx, filepath.Join("testdata", "k8s", "hello-w-volumes.goyaml"), nil) s.TelepresenceConnect(ctx) } func (s *interceptFlagSuite) TearDownSuite() { ctx := s.Context() itest.TelepresenceQuitOk(ctx) defer s.DeleteSvcAndWorkload(ctx, "deploy", s.serviceName) } // Test_ContainerReplace tests that: // // - Two containers in a pod can be intercepted in sequence. One with replace, and one without. // - Containers can be intercepted interchangeably with or without --replace // - Volumes are mounted // - Intercept responses are produced from the intercept handlers // - Responses after the intercept ends are from the cluster func (s *interceptFlagSuite) Test_ContainerReplace() { ctx := s.Context() const ( n1 = "container_replaced" c1 = "hello-container-1" n2 = "container_kept" c2 = "hello-container-2" ) localPort1, cancel1 := itest.StartLocalHttpEchoServer(ctx, n1) defer cancel1() localPort2, cancel2 := itest.StartLocalHttpEchoServer(ctx, n2) defer cancel2() tests := []struct { name string iceptName string appContainer string replace bool localPort int port uint16 }{ { name: n1, iceptName: n1, appContainer: c1, replace: true, localPort: localPort1, port: 80, }, { name: n2, iceptName: n2, appContainer: c2, replace: false, localPort: localPort2, port: 81, }, { name: "container_replace_kept", iceptName: n1, appContainer: c1, replace: false, localPort: localPort1, port: 80, }, { name: "container_kept_replaced", iceptName: n2, appContainer: c2, replace: true, localPort: localPort2, port: 81, }, } for _, tt := range tests { s.Run(tt.name, func() { ctx := s.Context() expectedOutput := regexp.MustCompile(tt.iceptName + ` from intercept at`) args := []string{"intercept"} if tt.replace { args = append(args, "--replace") } args = append(args, "--port", fmt.Sprintf("%d:%d", tt.localPort, tt.port), "--output", "json", "--detailed-output", "--workload", s.serviceName, tt.iceptName) jsOut := itest.TelepresenceOk(ctx, args...) agentCaptureCtx, agentCaptureCancel := context.WithCancel(ctx) s.CapturePodLogs(agentCaptureCtx, s.serviceName, "traffic-agent", s.AppNamespace()) if !s.ManagerIsVersion(">2.21.x") { // Circumvent bug in 2.21.x defer func() { _, _, err := itest.Telepresence(ctx, "uninstall", s.serviceName) s.NoError(err) }() } defer func() { agentCaptureCancel() itest.TelepresenceOk(ctx, "leave", tt.iceptName) s.CapturePodLogs(ctx, s.serviceName, tt.appContainer, s.AppNamespace()) s.Eventually(func() bool { out, err := itest.Output(ctx, "curl", "--silent", "--max-time", "1", iputil.JoinHostPort(s.serviceName, tt.port)) if err != nil { clog.Error(ctx, err) return false } if !expectedOutput.MatchString(out) { return true } clog.Info(ctx, out) return false }, 1*time.Minute, 6*time.Second) }() var ii intercept.Info require := s.Require() require.NoError(json.Unmarshal([]byte(jsOut), &ii)) require.Equal(ii.Name, tt.iceptName) // Ensure that all directories are mounted. require.NotNil(ii.Mount) mounts := ii.Mount.Mounts require.True(len(mounts) > 2) clog.Infof(ctx, "Mounts = %v", mounts) require.Eventually(func() bool { for mount := range mounts { st, err := os.Stat(filepath.Join(ii.Mount.LocalDir, mount)) if !(err == nil && st.IsDir()) { return false } } return true }, 10*time.Second, 2*time.Second) require.Eventually(func() bool { out, err := itest.Output(ctx, "curl", "--silent", "--max-time", "1", iputil.JoinHostPort(s.serviceName, tt.port)) if err != nil { clog.Error(ctx, err) return false } if expectedOutput.MatchString(out) { return true } clog.Info(ctx, out) return false }, 1*time.Minute, 6*time.Second) }) } } ================================================ FILE: integration_test/intercept_localhost_test.go ================================================ package integration_test import ( "context" "fmt" "net" "net/http" "strconv" "time" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/routing" ) type interceptLocalhostSuite struct { itest.Suite itest.SingleService cancelLocal context.CancelFunc defaultRoute *routing.Route port int } func (s *interceptLocalhostSuite) SuiteName() string { return "InterceptLocalhost" } func init() { itest.AddSingleServiceSuite("", "echo", func(h itest.SingleService) itest.TestingSuite { return &interceptLocalhostSuite{Suite: itest.Suite{Harness: h}, SingleService: h} }) } func (s *interceptLocalhostSuite) SetupSuite() { s.Suite.SetupSuite() ctx := s.Context() var err error s.defaultRoute, err = routing.DefaultRoute(ctx) s.Require().NoError(err) clog.Infof(ctx, "ip: %s: route: %s", s.defaultRoute.LocalIP, s.defaultRoute) s.port, s.cancelLocal = itest.StartLocalHttpEchoServerWithAddr(ctx, s.ServiceName(), net.JoinHostPort(s.defaultRoute.LocalIP.String(), "0"), nil) } func (s *interceptLocalhostSuite) TearDownSuite() { s.cancelLocal() } func (s *interceptLocalhostSuite) TestIntercept_WithCustomLocalhost() { ctx := s.Context() doRequest := func(ctx context.Context, host, port string) error { ctx, cancel := context.WithTimeout(ctx, 1*time.Second) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://%s/", net.JoinHostPort(host, port)), nil) if err != nil { return err } resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() // If there was a response, make sure it's a 200 s.Require().Equal(http.StatusOK, resp.StatusCode) return nil } // Make sure the IP address we think will respond is actually gonna respond, and that localhost won't s.Require().NoError(doRequest(ctx, s.defaultRoute.LocalIP.String(), strconv.Itoa(s.port))) s.Require().Error(doRequest(ctx, "127.0.0.1", strconv.Itoa(s.port))) // Run the intercept stdout := itest.TelepresenceOk(ctx, "intercept", s.ServiceName(), "--port", strconv.Itoa(s.port), "--address", s.defaultRoute.LocalIP.String()) defer itest.TelepresenceOk(ctx, "leave", s.ServiceName()) s.Require().Contains(stdout, "Using Deployment "+s.ServiceName()) itest.PingInterceptedEchoServer(ctx, s.ServiceName(), "80") } ================================================ FILE: integration_test/intercept_mount_test.go ================================================ package integration_test import ( "context" "net" "net/http" "os" "path/filepath" "regexp" "runtime" "strconv" "strings" "time" "github.com/go-json-experiment/json" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/agentconfig" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/intercept" ) type interceptMountSuite struct { itest.Suite itest.SingleService mountPoint string cancelLocal context.CancelFunc } func (s *interceptMountSuite) SuiteName() string { return "InterceptMount" } func init() { itest.AddSingleServiceSuite("", "echo", func(h itest.SingleService) itest.TestingSuite { return &interceptMountSuite{Suite: itest.Suite{Harness: h}, SingleService: h} }) } func (s *interceptMountSuite) SetupSuite() { if s.IsCI() && runtime.GOOS == "darwin" { s.T().Skip("Mount tests don't run on darwin due to macFUSE issues") return } s.Suite.SetupSuite() switch runtime.GOOS { case "windows": s.mountPoint = "T:" default: var err error s.mountPoint, err = os.MkdirTemp("", "mount-") // Don't use the itest.Tempdir() because deletion is delayed. s.Require().NoError(err) } ctx := s.Context() var port int port, s.cancelLocal = itest.StartLocalHttpEchoServer(ctx, s.ServiceName()) stdout := itest.TelepresenceOk(ctx, "intercept", s.ServiceName(), "--mount", s.mountPoint, "--port", strconv.Itoa(port)) s.Contains(stdout, "Using Deployment "+s.ServiceName()) s.CapturePodLogs(ctx, "echo", "traffic-agent", s.AppNamespace()) } func (s *interceptMountSuite) TearDownSuite() { ctx := s.Context() itest.TelepresenceOk(ctx, "leave", s.ServiceName()) s.cancelLocal() s.Eventually(func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--intercepts") return err == nil && !strings.Contains(stdout, s.ServiceName()+": intercepted") }, 10*time.Second, time.Second) if runtime.GOOS != "windows" { // Delay the deletion of the mount point so that it is properly unmounted before it's removed. go func() { time.Sleep(2 * time.Second) _ = os.RemoveAll(s.mountPoint) }() } } func (s *interceptMountSuite) Test_InterceptMount() { require := s.Require() ctx := s.Context() s.Eventually(func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--intercepts") return err == nil && regexp.MustCompile(s.ServiceName()+`\s*: intercepted`).MatchString(stdout) }, 10*time.Second, time.Second) time.Sleep(200 * time.Millisecond) // List is really fast now, so give the mount some time to become effective st, err := os.Stat(s.mountPoint) require.NoError(err, "Stat on failed") require.True(st.IsDir(), "Mount point is not a directory") st, err = os.Stat(filepath.Join(s.mountPoint, "var")) require.NoError(err, "Stat on /var failed") require.True(st.IsDir(), "/var is not a directory") } func (s *singleServiceSuite) Test_InterceptMountRelative() { if s.IsCI() && runtime.GOOS == "darwin" { s.T().Skip("Mount tests don't run on darwin due to macFUSE issues") } if runtime.GOOS == "windows" { s.T().Skip("Windows mount on driver letters. Relative mounts are not possible") } require := s.Require() ctx := s.Context() port, cancel := itest.StartLocalHttpEchoServer(ctx, s.ServiceName()) defer cancel() nwd, err := os.MkdirTemp("", "mount-") // Don't use the testing.Tempdir() because deletion is delayed. require.NoError(err) ctx = itest.WithWorkingDir(ctx, nwd) stdout := itest.TelepresenceOk(ctx, "intercept", s.ServiceName(), "--mount", "rel-dir", "--port", strconv.Itoa(port)) defer func() { itest.TelepresenceOk(ctx, "leave", s.ServiceName()) }() s.Contains(stdout, "Using Deployment "+s.ServiceName()) s.Eventually(func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--intercepts") return err == nil && regexp.MustCompile(s.ServiceName()+`\s*: intercepted`).MatchString(stdout) }, 10*time.Second, time.Second) time.Sleep(200 * time.Millisecond) // List is really fast now, so give the mount some time to become effective mountPoint := filepath.Join(nwd, "rel-dir") st, err := os.Stat(mountPoint) require.NoError(err, "Stat on failed") require.True(st.IsDir(), "Mount point is not a directory") st, err = os.Stat(filepath.Join(mountPoint, "var")) require.NoError(err, "Stat on /var failed") require.True(st.IsDir(), "/var is not a directory") } func (s *singleServiceSuite) Test_InterceptDetailedOutput() { if s.IsCI() && runtime.GOOS == "darwin" { s.T().Skip("Mount tests don't run on darwin due to macFUSE issues") } ctx := s.Context() port, cancel := itest.StartLocalHttpEchoServer(ctx, s.ServiceName()) defer cancel() stdout := itest.TelepresenceOk(ctx, "intercept", "--port", strconv.Itoa(port), "--detailed-output", "--output", "json", s.ServiceName()) defer func() { itest.TelepresenceOk(ctx, "leave", s.ServiceName()) }() var iInfo intercept.Info require := s.Require() require.NoError(json.Unmarshal([]byte(stdout), &iInfo)) s.Equal(iInfo.Name, s.ServiceName()) s.Equal(iInfo.Disposition, "ACTIVE") s.Equal(iInfo.WorkloadKind, "Deployment") s.Equal(iInfo.TargetPort, int32(port)) s.Equal(iInfo.Environment["TELEPRESENCE_CONTAINER"], "echo-server") m := iInfo.Mount require.NotNil(m) s.NotNil(net.ParseIP(m.PodIP)) s.NotZero(m.Port) s.Equal(agentconfig.ExportsMountPoint+"/echo-server", m.RemoteDir) require.Len(m.Mounts, 1) s.Contains(m.Mounts, "/var/run/secrets/kubernetes.io") } func (s *singleServiceSuite) Test_NoInterceptorResponse() { if s.IsCI() && runtime.GOOS == "darwin" { s.T().Skip("Mount tests don't run on darwin due to macFUSE issues") } if runtime.GOOS == "windows" { s.T().Skip("Windows mount on driver letters. Relative mounts are not possible") } time.Sleep(2000 * time.Millisecond) // List is really fast now, so give the mount some time to become effective require := s.Require() ctx := s.Context() nwd, err := os.MkdirTemp("", "mount-") // Don't use the testing.Tempdir() because deletion is delayed. require.NoError(err) ctx = itest.WithWorkingDir(ctx, nwd) stdout := itest.TelepresenceOk(ctx, "intercept", s.ServiceName(), "--mount", "rel-dir", "--port", "8443") defer func() { itest.TelepresenceOk(ctx, "leave", s.ServiceName()) }() s.Contains(stdout, "Using Deployment "+s.ServiceName()) s.Eventually(func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--intercepts") return err == nil && regexp.MustCompile(s.ServiceName()+`\s*: intercepted`).MatchString(stdout) }, 10*time.Second, time.Second) time.Sleep(2000 * time.Millisecond) // List is really fast now, so give the mount some time to become effective s.CapturePodLogs(ctx, s.ServiceName(), "traffic-agent", s.AppNamespace()) mountPoint := filepath.Join(nwd, "rel-dir") st, err := os.Stat(mountPoint) require.NoError(err, "Stat on failed") require.True(st.IsDir(), "Mount point is not a directory") st, err = os.Stat(filepath.Join(mountPoint, "var")) require.NoError(err, "Stat on /var failed") require.True(st.IsDir(), "/var is not a directory") // Bombard the echo service with lots of traffic. It's intercepted and will redirect the // traffic to the interceptor, but there's no such process listening. This must not // result in stream congestion that kills the intercept. url := "http://" + s.ServiceName() for i := 0; i < 1000; i++ { go func() { hc := http.Client{Timeout: 100 * time.Millisecond} resp, err := hc.Get(url) if err == nil { resp.Body.Close() } }() } // Verify that we still have a functional mount st, err = os.Stat(mountPoint) require.NoError(err, "Stat on failed") require.True(st.IsDir(), "Mount point is not a directory") st, err = os.Stat(filepath.Join(mountPoint, "var")) require.NoError(err, "Stat on /var failed") require.True(st.IsDir(), "/var is not a directory") } ================================================ FILE: integration_test/itest/apply_app.go ================================================ package itest import ( "context" "path/filepath" "strconv" "strings" "time" "github.com/go-json-experiment/json" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/telepresenceio/telepresence/v2/pkg/dos" ) func ApplyEchoService(ctx context.Context, name, namespace string, port int) { ApplyService(ctx, name, namespace, "ghcr.io/telepresenceio/echo-server:0.3.1", port, 8080) } func ApplyService(ctx context.Context, name, namespace, image string, port, targetPort int) { t := getT(ctx) t.Helper() require.NoError(t, Kubectl(ctx, namespace, "create", "deploy", name, "--image", image), "failed to create deployment %s", name) require.NoError(t, Kubectl(ctx, namespace, "expose", "deploy", name, "--port", strconv.Itoa(port), "--target-port", strconv.Itoa(targetPort)), "failed to expose deployment %s", name) require.NoError(t, Kubectl(ctx, namespace, "rollout", "status", "-w", "deployment/"+name), "failed to deploy %s", name) } func DeleteSvcAndWorkload(ctx context.Context, workload, name, namespace string) { assert.NoError(getT(ctx), Kubectl(ctx, namespace, "delete", "--ignore-not-found", "--grace-period", "3", "svc,"+workload, name), "failed to delete service and %s %s", workload, name) } // ApplyApp calls kubectl apply -n -f on the given app + .yaml found in testdata/k8s relative // to the directory returned by GetWorkingDir. func ApplyApp(ctx context.Context, name, namespace, workload string) { t := getT(ctx) t.Helper() manifest := filepath.Join("testdata", "k8s", name+".yaml") require.NoError(t, Kubectl(ctx, namespace, "apply", "-f", manifest), "failed to apply %s", manifest) require.NoError(t, RolloutStatusWait(ctx, namespace, workload)) } // DeleteApp calls kubectl delete -n -f on the given app + .yaml found in testdata/k8s relative // to the directory returned by GetWorkingDir. func DeleteApp(ctx context.Context, name, namespace string) { t := getT(ctx) t.Helper() manifest := filepath.Join("testdata", "k8s", name+".yaml") require.NoError(t, Kubectl(ctx, namespace, "delete", "-f", manifest), "failed to delete %s", manifest) } type AppPort struct { ServicePortName string ServicePortNumber uint16 TargetPortName string TargetPortNumber uint16 Protocol string AppProtocol string } type AppData struct { ServiceName string DeploymentName string AppName string ContainerName string Image string PullPolicy string Ports []AppPort Env map[string]string } // ApplyAppTemplate calls kubectl apply -n -f on the given app + .yaml found in testdata/k8s relative // to the directory returned by GetWorkingDir. func ApplyAppTemplate(ctx context.Context, namespace string, app *AppData) { t := getT(ctx) t.Helper() r, err := OpenTemplate(ctx, filepath.Join(GetOSSRoot(ctx), "testdata", "k8s", "svc-deploy.goyaml"), app) require.NoError(t, err) require.NoError(t, Kubectl(dos.WithStdin(ctx, r), namespace, "apply", "-f", "-"), "failed to apply template") wl := app.DeploymentName if wl == "" { wl = app.AppName } require.NoError(t, RolloutStatusWait(ctx, namespace, "deploy/"+wl)) } func RolloutStatusWait(ctx context.Context, namespace, workload string) error { ctx, cancel := context.WithTimeout(ctx, PodCreateTimeout(ctx)) defer cancel() switch { case strings.HasPrefix(workload, "pod/"): return Kubectl(ctx, namespace, "wait", workload, "--for", "condition=ready") case strings.HasPrefix(workload, "rollout/"): return Kubectl(ctx, namespace, "argo", "rollouts", "status", strings.TrimPrefix(workload, "rollout/")) case strings.HasPrefix(workload, "replicaset/"), strings.HasPrefix(workload, "statefulset/"): for { status := struct { ReadyReplicas int `json:"readyReplicas,omitempty"` Replicas int `json:"replicas,omitempty"` }{} stdout, err := KubectlOut(ctx, namespace, "get", workload, "-o", "jsonpath={..status}") if err != nil { return err } if err = json.Unmarshal([]byte(stdout), &status); err != nil { return err } if status.ReadyReplicas == status.Replicas { return nil } time.Sleep(3 * time.Second) } } return Kubectl(ctx, namespace, "rollout", "status", "-w", workload) } ================================================ FILE: integration_test/itest/assertions.go ================================================ package itest import ( "context" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type Requirements struct { *require.Assertions } func (r *Requirements) EventuallyContext(ctx context.Context, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...any) { r.Eventually(func() bool { if ctx.Err() != nil { return true } return condition() }, waitFor, tick, msgAndArgs...) r.NoError(ctx.Err()) } type Assertions struct { *assert.Assertions } func (r *Assertions) EventuallyContext(ctx context.Context, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool { return r.Eventually(func() bool { if ctx.Err() != nil { return true } return condition() }, waitFor, tick, msgAndArgs...) || r.NoError(ctx.Err()) } ================================================ FILE: integration_test/itest/cluster.go ================================================ package itest import ( "archive/zip" "bytes" "context" "errors" "fmt" "io" "log/slog" "net" "net/http" "net/netip" "os" "os/exec" "path/filepath" "reflect" "regexp" "runtime" "slices" "strconv" "strings" "sync" "testing" "time" "unicode/utf8" "github.com/blang/semver/v4" "github.com/cenkalti/backoff/v4" "github.com/go-json-experiment/json" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" core "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/labels" k8sruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd/api" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/pkg/agentconfig" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/client/k8s" "github.com/telepresenceio/telepresence/v2/pkg/dos" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" "github.com/telepresenceio/telepresence/v2/pkg/ioutil" "github.com/telepresenceio/telepresence/v2/pkg/iputil" "github.com/telepresenceio/telepresence/v2/pkg/maps" "github.com/telepresenceio/telepresence/v2/pkg/proc" "github.com/telepresenceio/telepresence/v2/pkg/shellquote" "github.com/telepresenceio/telepresence/v2/pkg/slice" "github.com/telepresenceio/telepresence/v2/pkg/version" ) const ( purposeLabel = "tp-cli-testing" AssignPurposeLabel = "purpose=" + purposeLabel TestUser = "telepresence-test-developer" ) type Cluster interface { CapturePodLogs(ctx context.Context, app, container, ns string) string Executable() (string, error) GeneralError() error GlobalEnv(context.Context) dos.MapEnv Initialize(context.Context) context.Context IsCI() bool IsIPv6() bool LargeFileTestDisabled() bool SetGeneralError(error) Suffix() string UninstallTrafficManager(ctx context.Context, managerNamespace string, args ...string) PackageHelmChart(ctx context.Context) (string, error) GetValuesForHelm(ctx context.Context, values map[string]any, release bool) []string GetSetArgsForHelm(ctx context.Context, values map[string]any, release bool) []string GetK8SCluster(ctx context.Context, context, managerNamespace string) (*k8s.Cluster, error) TelepresenceHelmInstallOK(ctx context.Context, upgrade bool, args ...string) string TelepresenceHelmInstall(ctx context.Context, upgrade bool, args ...string) (string, error) UserdPProf() uint16 RootdPProf() uint16 AgentImage() string AgentVersion() semver.Version AgentRegistry() string ClientImage() string ClientRegistry() string ClientVersion() semver.Version // ClientIsVersion returns true if the final version of the ClientVersion is included // in the given version range. ClientIsVersion(versionRange string) bool ManagerImage() string ManagerRegistry() string ManagerVersion() semver.Version // ManagerIsVersion returns true if the final version of the ManagerVersion is included // in the given version range. ManagerIsVersion(versionRange string) bool // UseRancherLocalPath returns true if the rancher-local-path provisioner is used. // See: https://github.com/rancher/local-path-provisioner UseLocalPathProvisioner() bool } // The cluster is created once and then reused by all tests. It ensures that: // // - executable and the images are built once // - a docker repository is available // - built images are pushed to the docker repository // - a cluster is available type cluster struct { suffix string isCI bool ipv6 bool executable string kubeConfig string generalError error logCapturingPods sync.Map userdPProf uint16 rootdPProf uint16 self Cluster largeFileTestDisabled bool agentImage string clientImage string managerImage string agentRegistry string clientRegistry string managerRegistry string agentVersion semver.Version clientVersion semver.Version managerVersion semver.Version localPathProvisioner bool } //nolint:gochecknoglobals // extension point var ExtendClusterFunc = func(c Cluster) Cluster { return c } func WithCluster(ctx context.Context, f func(ctx context.Context)) { s := cluster{} s.self = &s ec := ExtendClusterFunc(&s) ctx = withGlobalHarness(ctx, ec) ctx = ec.Initialize(ctx) defer s.tearDown(ctx) t := getT(ctx) if !t.Failed() { f(s.withBasicConfig(ctx, t)) } } func (s *cluster) SetSelf(self Cluster) { s.self = self } func (s *cluster) Initialize(ctx context.Context) context.Context { s.suffix, s.isCI = dos.LookupEnv(ctx, "GITHUB_SHA") if s.isCI { // Use 7 characters of SHA to avoid busting k8s 60 character name limit if len(s.suffix) > 7 { s.suffix = s.suffix[:7] } } else { s.suffix = strconv.Itoa(os.Getpid()) } t := getT(ctx) v := dos.Getenv(ctx, "TELEPRESENCE_VERSION") require.NotEmpty(t, v, "TELEPRESENCE_VERSION must be set") var err error version.Structured, err = semver.Parse(strings.TrimPrefix(v, "v")) require.NoError(t, err) version.Version = v if v = dos.Getenv(ctx, "DEV_CLIENT_VERSION"); v != "" { s.clientVersion, err = semver.Parse(v) require.NoError(t, err) } else { s.clientVersion = version.Structured } if v = dos.Getenv(ctx, "DEV_MANAGER_VERSION"); v != "" { s.managerVersion, err = semver.Parse(v) require.NoError(t, err) } else { s.managerVersion = version.Structured } if v = dos.Getenv(ctx, "DEV_AGENT_VERSION"); v != "" { s.agentVersion, err = semver.Parse(v) require.NoError(t, err) } else { s.agentVersion = s.managerVersion } // We cannot use t.TempDir() here, because it will not mount correctly in // rancher-desktop and docker-desktop (unless they are configured to allow // mounts directly from /tmp). So we use a tempdir in BUILD_OUTPUT instead // because it's believed to be both mountable and writable. tempDir := dos.Getenv(ctx, "TELEPRESENCE_TEMP_DIR") if tempDir == "" { tempDir = filepath.Join(BuildOutput(ctx), "tmp") } _ = dos.RemoveAll(ctx, tempDir) require.NoError(t, dos.MkdirAll(ctx, tempDir, 0o777)) ctx = withTempDirBase(ctx, &tempDirBase{tempDir: tempDir}) t.Cleanup(func() { _ = os.RemoveAll(tempDir) }) registry := dos.Getenv(ctx, "TELEPRESENCE_REGISTRY") if registry == "" { registry = "ghcr.io/telepresenceio" } s.clientRegistry = dos.Getenv(ctx, "DEV_CLIENT_REGISTRY") if s.clientRegistry == "" { s.clientRegistry = registry } s.managerRegistry = dos.Getenv(ctx, "DEV_MANAGER_REGISTRY") if s.managerRegistry == "" { s.managerRegistry = registry } s.agentRegistry = dos.Getenv(ctx, "DEV_AGENT_REGISTRY") if s.agentRegistry == "" { s.agentRegistry = s.managerRegistry } s.kubeConfig = dos.Getenv(ctx, "DEV_KUBECONFIG") if s.kubeConfig == "" { lr := clientcmd.NewDefaultClientConfigLoadingRules() require.True(t, len(lr.Precedence) > 0, "Unable to figure out KUBECONFIG") s.kubeConfig = lr.Precedence[0] } s.clientImage = dos.Getenv(ctx, "DEV_CLIENT_IMAGE") if s.clientImage == "" { s.clientImage = "telepresence" } ctx = WithClientImage(ctx, &Image{ Name: s.clientImage, Tag: s.clientVersion.String(), Registry: s.clientRegistry, }) s.managerImage = dos.Getenv(ctx, "DEV_MANAGER_IMAGE") if s.managerImage == "" { s.managerImage = "tel2" } ctx = WithImage(ctx, &Image{ Name: s.managerImage, Tag: s.managerVersion.String(), Registry: s.managerRegistry, }) s.agentImage = dos.Getenv(ctx, "DEV_AGENT_IMAGE") if s.agentImage == "" { s.agentImage = s.managerImage } ctx = WithAgentImage(ctx, &Image{ Name: s.agentImage, Tag: s.agentVersion.String(), Registry: s.agentRegistry, }) if pp := dos.Getenv(ctx, "DEV_USERD_PROFILING_PORT"); pp != "" { port, err := strconv.ParseUint(pp, 10, 16) require.NoError(t, err) s.userdPProf = uint16(port) } if pp := dos.Getenv(ctx, "DEV_ROOTD_PROFILING_PORT"); pp != "" { port, err := strconv.ParseUint(pp, 10, 16) require.NoError(t, err) s.rootdPProf = uint16(port) } if pp := dos.Getenv(ctx, "DEV_LOCAL_PATH_PROVISIONER"); pp != "" { s.localPathProvisioner, _ = strconv.ParseBool(pp) } exe := "telepresence" if runtime.GOOS == "windows" { exe = "telepresence.exe" } s.executable = filepath.Join(BuildOutput(ctx), "bin", exe) var executable string if !s.clientVersion.EQ(version.Structured) { executable = s.downloadBinary(ctx, t, s.clientVersion) } else { executable = s.executable } clog.Infof(ctx, "Using binary %s", executable) ctx = WithExecutable(ctx, executable) if ipv6, err := strconv.ParseBool(dos.Getenv(ctx, "DEV_IPV6_CLUSTER")); err == nil { s.ipv6 = ipv6 } else { output, err := Output(ctx, "kubectl", "--namespace", "kube-system", "get", "svc", "kube-dns", "-o", "jsonpath={.spec.clusterIP}") if err == nil { ip, err := netip.ParseAddr(strings.TrimSpace(output)) assert.NoError(t, err) if ip.Is6() { clog.Info(ctx, "Using IPv6 because the kube-dns.kube-system has an IPv6 IP") s.ipv6 = true } } } s.ensureQuit(ctx) s.ensureNoManager(ctx) _ = Run(ctx, "kubectl", "delete", "-f", filepath.Join("testdata", "k8s", "client_rbac.yaml")) err = Run(ctx, "kubectl", "delete", "ns,svc,deploy,clusterrole,clusterrolebinding,mutatingwebhookconfiguration,pvc,pv", "-l", AssignPurposeLabel) if err != nil { clog.Errorf(ctx, "kubectl delete: %v", err) } return ctx } func (s *cluster) downloadBinary(ctx context.Context, t testing.TB, v semver.Version) string { path := filepath.Join(BuildOutput(ctx), "downloads") err := os.MkdirAll(path, 0o755) require.NoError(t, err) cdPath := filepath.Join(path, "telepresence-"+v.String()) if _, err := os.Stat(cdPath); err == nil { return cdPath } cdURL := "https://github.com/telepresenceio/telepresence/releases/download/v%s/telepresence-%s-%s" cdURL = fmt.Sprintf(cdURL, v, runtime.GOOS, runtime.GOARCH) if runtime.GOOS == "windows" { cdURL += ".zip" cdPath += ".zip" } clog.Infof(ctx, "Downloading telepresence binary from %s", cdURL) cdFile, err := os.Create(cdPath) require.NoError(t, err) rsp, err := http.Get(cdURL) require.NoError(t, err) bdy := rsp.Body _, err = io.Copy(cdFile, bdy) bdy.Close() require.NoError(t, err) if runtime.GOOS == "windows" { require.NoError(t, cdFile.Close()) unzip(t, cdFile.Name(), cdPath) return filepath.Join(cdPath, "telepresence.exe") } else { require.NoError(t, cdFile.Chmod(0o755)) require.NoError(t, cdFile.Close()) return cdFile.Name() } } func unzip(t testing.TB, zipFile, dir string) { uz, err := zip.OpenReader(zipFile) require.NoError(t, err) defer uz.Close() for _, f := range uz.File { rc, err := f.Open() require.NoError(t, err) out, err := os.OpenFile(filepath.Join(dir, f.Name), os.O_CREATE|os.O_WRONLY, f.FileInfo().Mode()) require.NoError(t, err) _, err = io.Copy(out, rc) rc.Close() out.Close() require.NoError(t, err) } } func (s *cluster) tearDown(ctx context.Context) { s.ensureQuit(ctx) if s.kubeConfig != "" { ctx = WithWorkingDir(ctx, GetOSSRoot(ctx)) _ = Run(ctx, "kubectl", "delete", "-f", filepath.Join("testdata", "k8s", "client_rbac.yaml")) _ = Run(ctx, "kubectl", "delete", "--wait=false", "all", "-l", AssignPurposeLabel) } } func (s *cluster) ensureQuit(ctx context.Context) { // Ensure that no telepresence is running when the tests start _, _, _ = Telepresence(ctx, "quit", "-s") //nolint:dogsled // don't care about any of the returns } func (s *cluster) ensureNoManager(ctx context.Context) { out, err := Output(ctx, "helm", "list", "-A", "--output", "json") t := getT(ctx) require.NoError(t, err) var es []map[string]any err = json.Unmarshal([]byte(out), &es) require.NoError(t, err) for { ix := slices.IndexFunc(es, func(v map[string]any) bool { return v["name"] == "traffic-manager" }) if ix < 0 { break } e := es[ix] es = slices.Delete(es, ix, ix+1) ns := e["namespace"].(string) if regexp.MustCompile(`^ambassador-[0-9a-f]+(-[0-9])?$`).MatchString(ns) { s.UninstallTrafficManager(ctx, ns) } else { t.Fatalf("%s is already installed in namespace %s. Please uninstall before testing.", e["chart"], ns) } } } // PodCreateTimeout will return a timeout suitable for operations that create pods. // This is longer when running against clusters that scale up nodes on demand for new pods. func PodCreateTimeout(c context.Context) time.Duration { switch GetProfile(c) { case GkeAutopilotProfile: return 5 * time.Minute case DefaultProfile: fallthrough default: // this really shouldn't be happening but hey return 180 * time.Second } } func (s *cluster) withBasicConfig(c context.Context, t *testing.T) context.Context { config := client.GetDefaultConfig() logLevels := config.LogLevels() logLevels.CLI = slog.LevelDebug logLevels.UserDaemon = slog.LevelDebug logLevels.RootDaemon = slog.LevelDebug logLevels.KubeAuthDaemon = slog.LevelDebug to := config.Timeouts() to.PrivateClusterConnect = 60 * time.Second to.PrivateEndpointDial = 10 * time.Second to.PrivateHelm = PodCreateTimeout(c) to.PrivateIntercept = 30 * time.Second to.PrivateProxyDial = 30 * time.Second to.PrivateRoundtripLatency = 5 * time.Second to.PrivateTrafficManagerAPI = 45 * time.Second to.PrivateTrafficManagerConnect = 30 * time.Second to.PrivateConnectivityCheck = 0 if s.ManagerVersion().EQ(version.Structured) { images := config.Images() images.PrivateRegistry = s.self.ManagerRegistry() if agentImage := GetAgentImage(c); agentImage != nil { images.PrivateAgentImage = agentImage.FQName() images.PrivateWebhookRegistry = agentImage.Registry } if clientImage := GetClientImage(c); clientImage != nil { images.PrivateClientImage = clientImage.FQName() } } config.Grpc().MaxReceiveSizeV, _ = resource.ParseQuantity("10Mi") config.Intercept().UseFtp = true if s.ClientIsVersion(">=2.23.0") { config.Intercept().MountsRoot = TempDir(c) } config = config.Merge(client.GetConfig(c)) configYaml, err := config.MarshalYAML() require.NoError(t, err) configYamlStr := string(configYaml) configDir := TempDir(c) c = filelocation.WithAppUserConfigDir(c, configDir) c, err = SetConfig(c, configDir, configYamlStr) require.NoError(t, err) return c } func (s *cluster) GlobalEnv(ctx context.Context) dos.MapEnv { globalEnv := dos.MapEnv{ "KUBECONFIG": s.kubeConfig, } yes := struct{}{} includeEnv := map[string]struct{}{ "HOME": yes, "PATH": yes, "LOGNAME": yes, "USER": yes, "TMPDIR": yes, "MAKELEVEL": yes, "TELEPRESENCE_MAX_LOGFILES": yes, } if runtime.GOOS == "windows" { includeEnv["APPDATA"] = yes includeEnv["AppData"] = yes includeEnv["LOCALAPPDATA"] = yes includeEnv["LocalAppData"] = yes includeEnv["OS"] = yes includeEnv["TEMP"] = yes includeEnv["TMP"] = yes includeEnv["Path"] = yes includeEnv["PATHEXT"] = yes includeEnv["ProgramFiles"] = yes includeEnv["ProgramData"] = yes includeEnv["SystemDrive"] = yes includeEnv["USERPROFILE"] = yes includeEnv["USERNAME"] = yes includeEnv["windir"] = yes } for _, env := range dos.Environ(ctx) { if eqIdx := strings.IndexByte(env, '='); eqIdx > 0 { key := env[:eqIdx] if _, ok := includeEnv[key]; ok { globalEnv[key] = env[eqIdx+1:] } } } return globalEnv } func (s *cluster) Executable() (string, error) { return s.executable, nil } func (s *cluster) GeneralError() error { return s.generalError } func (s *cluster) IsCI() bool { return s.isCI } func (s *cluster) IsIPv6() bool { return s.ipv6 } func (s *cluster) LargeFileTestDisabled() bool { return s.largeFileTestDisabled } func (s *cluster) AgentImage() string { return s.agentImage } func (s *cluster) AgentRegistry() string { return s.agentRegistry } func (s *cluster) AgentVersion() semver.Version { return s.agentVersion } func (s *cluster) ClientImage() string { return s.clientImage } func (s *cluster) ClientRegistry() string { return s.clientRegistry } func (s *cluster) ClientVersion() semver.Version { return s.clientVersion } func isFinalIncluded(vr string, v semver.Version) bool { return semver.MustParseRange(vr)(semver.MustParse(v.FinalizeVersion())) } // ClientIsVersion returns true if the final version of the ClientVersion is included // in the given version range. func (s *cluster) ClientIsVersion(vr string) bool { return isFinalIncluded(vr, s.ClientVersion()) } func (s *cluster) ManagerImage() string { return s.managerImage } func (s *cluster) ManagerRegistry() string { return s.managerRegistry } func (s *cluster) ManagerVersion() semver.Version { return s.managerVersion } // ManagerIsVersion returns true if the final version of the ManagerVersion is included // in the given version range. func (s *cluster) ManagerIsVersion(vr string) bool { return isFinalIncluded(vr, s.ManagerVersion()) } func (s *cluster) SetGeneralError(err error) { s.generalError = err } func (s *cluster) Suffix() string { return s.suffix } func (s *cluster) UserdPProf() uint16 { return s.userdPProf } func (s *cluster) RootdPProf() uint16 { return s.rootdPProf } func (s *cluster) UseLocalPathProvisioner() bool { return s.localPathProvisioner } func (s *cluster) CapturePodLogs(ctx context.Context, app, container, ns string) string { var pods []string for i := 0; ; i++ { runningPods := RunningPodNames(ctx, app, ns) if len(runningPods) > 0 { if container == "" { pods = runningPods } else { for _, pod := range runningPods { cns, err := KubectlOut(ctx, ns, "get", "pods", pod, "-o", "jsonpath={.spec.containers[*].name}") if err == nil && slice.Contains(strings.Split(cns, " "), container) { pods = append(pods, pod) } } } } if len(pods) > 0 || i == 5 { break } time.Sleep(2 * time.Second) } if len(pods) == 0 { if container == "" { clog.Errorf(ctx, "found no %s pods in namespace %s", app, ns) } else { clog.Errorf(ctx, "found no %s pods in namespace %s with a %s container", app, ns, container) } return "" } present := struct{}{} var pod string for i, key := range pods { if container != "" { key += "/" + container } if _, ok := s.logCapturingPods.LoadOrStore(key, present); !ok { pod = pods[i] break } } if pod == "" { return "" // All pods already captured } // Use another logger to avoid errors due to logs arriving after the tests complete. ctx = clog.WithLogger(ctx, slog.Default()) logName := pod if container != "" { logName = fmt.Sprintf("%s-%s", pod, container) } logFile, err := os.Create( filepath.Join(filelocation.AppUserLogDir(ctx), fmt.Sprintf("%s-%s.log", time.Now().Format("20060102T150405"), logName))) if err != nil { s.logCapturingPods.Delete(pod) clog.Errorf(ctx, "unable to create pod logfile %s: %v", logFile.Name(), err) return "" } args := []string{"--namespace", ns, "logs", "-f", pod} if container != "" { args = append(args, "-c", container) } // Let command die when the pod that it logs die cmd := Command(context.WithoutCancel(ctx), "kubectl", args...) cmd.Stdout = logFile cmd.Stderr = logFile ready := make(chan string, 1) readyClosed := false go func() { defer func() { _ = logFile.Close() s.logCapturingPods.Delete(pod) }() err := cmd.Start() if err == nil { if container == "" { clog.Infof(ctx, "Capturing logs for pod %s", pod) } else { clog.Infof(ctx, "Capturing logs for pod %s, container %s", pod, container) } ready <- logFile.Name() close(ready) readyClosed = true err = cmd.Wait() } if err != nil { if container == "" { clog.Errorf(ctx, "log capture for pod %s failed: %v", pod, err) } else { clog.Errorf(ctx, "log capture for pod %s, container %s failed: %v", pod, container, err) } if !readyClosed { close(ready) } } }() select { case <-ctx.Done(): clog.Infof(ctx, "log capture for pod %s interrupted prior to start", pod) return "" case file := <-ready: return file } } func (s *cluster) GetK8SCluster(ctx context.Context, context, managerNamespace string) (*k8s.Cluster, error) { _ = os.Setenv("KUBECONFIG", KubeConfig(ctx)) flags := map[string]string{ "namespace": managerNamespace, } if context != "" { flags["context"] = context } cfgAndFlags, err := k8s.NewKubeconfig(ctx, false, flags, managerNamespace, nil) if err != nil { return nil, err } return k8s.NewCluster(cfgAndFlags, nil) } func KubeConfig(ctx context.Context) string { kubeConf, _ := LookupEnv(ctx, "KUBECONFIG") return kubeConf } const sensitivePrefix = "--$sensitive$--" // WrapSensitive wraps an argument sent to Command so that it doesn't get logged verbatim. This can // be used for commands like "telepresence login --apikey NNNN" where the NNN shouldn't be visible // in the logs. If NNN Is wrapped using this function, it will appear as "***" in the logs. func WrapSensitive(s string) string { return sensitivePrefix + s } // Command creates and returns an exec.Cmd initialized with the global environment // from the cluster harness and any other environment that has been added using the // WithEnv() function. func Command(ctx context.Context, executable string, args ...string) *exec.Cmd { getT(ctx).Helper() // Ensure that command has a timestamp and is somewhat readable dbgArgs := args copied := false for i, a := range args { if strings.HasPrefix(a, sensitivePrefix) { if !copied { dbgArgs = make([]string, len(args)) copy(dbgArgs, args) args = make([]string, len(args)) copy(args, dbgArgs) copied = true } dbgArgs[i] = "***" args[i] = strings.TrimPrefix(a, sensitivePrefix) } } clog.Debugf(ctx, "executing %s", shellquote.ShellString(filepath.Base(executable), dbgArgs)) cmd := proc.CommandContext(ctx, executable, args...) cmd.Env = EnvironMap(ctx).Environ() cmd.Dir = GetWorkingDir(ctx) cmd.Stdin = dos.Stdin(ctx) return cmd } func EnvironMap(ctx context.Context) dos.MapEnv { env := GetGlobalHarness(ctx).GlobalEnv(ctx) maps.Merge(env, getEnv(ctx)) return env } // TelepresenceOk executes the CLI command in a new process and requires the result to be OK. func TelepresenceOk(ctx context.Context, args ...string) string { t := getT(ctx) t.Helper() stdout, stderr, err := Telepresence(ctx, args...) if !assert.NoError(t, err) { t.Fatalf("telepresence was unable to run, stdout %s", stdout) } require.NoError(t, err, "telepresence was unable to run, stdout %s", stdout) if (strings.HasPrefix(stderr, "Warning:") || strings.Contains(stderr, "has been deprecated")) && !strings.ContainsRune(stderr, '\n') { // Accept warnings, but log them. clog.Warn(ctx, stderr) } else { assert.Empty(t, stderr, "Expected stderr to be empty, but got: %s", stderr) } return stdout } // Telepresence executes the CLI command in a new process. func Telepresence(ctx context.Context, args ...string) (string, string, error) { t := getT(ctx) t.Helper() cmd := TelepresenceCmd(ctx, args...) stdout := cmd.Stdout.(*strings.Builder) stderr := cmd.Stderr.(*strings.Builder) err := cmd.Run() errStr := strings.TrimSpace(stderr.String()) if err != nil { err = RunError(err, []byte(errStr)) } return strings.TrimSpace(stdout.String()), errStr, err } // TelepresenceCmd creates an exec.Cmd using the Command function. Before the command is created, // the environment is extended with DEV_TELEPRESENCE_CONFIG_DIR from filelocation.AppUserConfigDir // and DEV_TELEPRESENCE_LOG_DIR from filelocation.AppUserLogDir. func TelepresenceCmd(ctx context.Context, args ...string) *exec.Cmd { t := getT(ctx) t.Helper() var stdout, stderr strings.Builder ctx = WithEnv(ctx, map[string]string{ "DEV_TELEPRESENCE_CONFIG_DIR": filelocation.AppUserConfigDir(ctx), "DEV_TELEPRESENCE_LOG_DIR": filelocation.AppUserLogDir(ctx), "TELEPRESENCE_PROGRESS": "plain", }) gh := GetGlobalHarness(ctx) if len(args) > 0 && (args[0] == "connect") { rest := args[1:] args = append(make([]string, 0, len(args)+3), args[0]) if user := GetUser(ctx); user != "default" { args = append(args, "--as", "system:serviceaccount:"+user) } if gh.UserdPProf() > 0 { args = append(args, "--userd-profiling-port", strconv.Itoa(int(gh.UserdPProf()))) } if gh.RootdPProf() > 0 { args = append(args, "--rootd-profiling-port", strconv.Itoa(int(gh.RootdPProf()))) } args = append(args, rest...) } cmd := Command(ctx, GetExecutable(ctx), args...) cmd.Stdout = &stdout cmd.Stderr = &stderr return cmd } // TelepresenceDisconnectOk tells telepresence to quit and asserts that the stdout contains the correct output. func TelepresenceDisconnectOk(ctx context.Context, args ...string) { AssertDisconnectOutput(ctx, TelepresenceOk(ctx, append([]string{"quit"}, args...)...)) } // TelepresenceDisconnect tells telepresence to quit and asserts that the stdout contains the correct output. func TelepresenceDisconnect(ctx context.Context, args ...string) { _, _, _ = Telepresence(ctx, append([]string{"quit"}, args...)...) //nolint:nolintlint,dogsled } // AssertDisconnectOutput asserts that the stdout contains the correct output from a telepresence quit command. func AssertDisconnectOutput(ctx context.Context, stdout string) { t := getT(ctx) assert.True(t, strings.Contains(stdout, "Disconnected") || strings.Contains(stdout, "Not connected")) if t.Failed() { t.Logf("Disconnect output was %q", stdout) } } // TelepresenceQuitOk tells telepresence to quit and asserts that the stdout contains the correct output. func TelepresenceQuitOk(ctx context.Context, args ...string) { AssertQuitOutput(ctx, TelepresenceOk(ctx, append([]string{"quit", "-s"}, args...)...)) } // TelepresenceQuit tells telepresence to quit but disregards any errors. Suitable for use in a defer statement // or when tearing down a suite. func TelepresenceQuit(ctx context.Context, args ...string) { _, _, _ = Telepresence(ctx, append([]string{"quit", "-s"}, args...)...) //nolint:nolintlint,dogsled } // AssertQuitOutput asserts that the stdout contains the correct output from a telepresence quit command. func AssertQuitOutput(ctx context.Context, stdout string) { for _, ex := range []string{"", "Quit", "Telepresence Daemons quitting...done", "Telepresence Daemons have already quit"} { if strings.Contains(stdout, ex) { return } } t := getT(ctx) t.Fail() t.Logf("Quit output was %q", stdout) } // RunError checks if the given err is a *exit.ExitError, and if so, extracts // Stderr and the ExitCode from it. func RunError(err error, out []byte) error { var ee *exec.ExitError if errors.As(err, &ee) { switch { case len(ee.Stderr) > 0: err = fmt.Errorf("%s, exit code %d", string(ee.Stderr), ee.ExitCode()) case utf8.ValidString(string(out)): err = fmt.Errorf("%s, exit code %d", string(out), ee.ExitCode()) default: err = fmt.Errorf("exit code %d", ee.ExitCode()) } } return err } // Run runs the given command and arguments and returns an error if the command failed. func Run(ctx context.Context, exe string, args ...string) error { getT(ctx).Helper() out, err := Command(ctx, exe, args...).CombinedOutput() if err != nil { return RunError(err, out) } return nil } // Output runs the given command and arguments and returns its combined output and an error if the command failed. func Output(ctx context.Context, exe string, args ...string) (string, error) { getT(ctx).Helper() cmd := Command(ctx, exe, args...) stderr := bytes.Buffer{} cmd.Stderr = &stderr out, err := cmd.Output() if err != nil { return string(out), RunError(err, stderr.Bytes()) } return string(out), nil } // Kubectl runs kubectl with the default context and the given namespace, or in the default namespace if the given // namespace is an empty string. func Kubectl(ctx context.Context, namespace string, args ...string) error { return retryKubectl(ctx, namespace, args, func(args []string) error { return Run(ctx, "kubectl", args...) }) } // KubectlOut runs kubectl with the default context and the application namespace and returns its combined output. func KubectlOut(ctx context.Context, namespace string, args ...string) (out string, err error) { err = retryKubectl(ctx, namespace, args, func(args []string) error { out, err = Output(ctx, "kubectl", args...) return err }) return out, err } func retryKubectl(ctx context.Context, namespace string, args []string, f func([]string) error) error { getT(ctx).Helper() var ks []string if namespace != "" { // Add --namespace before first flag argument ks = make([]string, 0, len(args)+2) pos := -1 for i, arg := range args { if strings.HasPrefix(arg, "-") { pos = i break } ks = append(ks, arg) } ks = append(ks, "--namespace", namespace) if pos >= 0 { ks = append(ks, args[pos:]...) } args = ks } b := backoff.NewExponentialBackOff( backoff.WithInitialInterval(2*time.Second), backoff.WithMaxInterval(7*time.Second), backoff.WithMaxElapsedTime(30*time.Second), ) return backoff.Retry(func() error { err := f(args) if err != nil && !strings.Contains(err.Error(), "(ServiceUnavailable)") { err = backoff.Permanent(err) } return err }, backoff.WithContext(b, ctx)) } func CreateNamespaces(ctx context.Context, namespaces ...string) { t := getT(ctx) t.Helper() wg := sync.WaitGroup{} wg.Add(len(namespaces)) for _, ns := range namespaces { go func(ns string) { defer wg.Done() assert.NoError(t, Kubectl(ctx, "", "create", "namespace", ns), "failed to create namespace %q", ns) assert.NoError(t, Kubectl(ctx, "", "label", "namespace", ns, AssignPurposeLabel, fmt.Sprintf("app.kubernetes.io/name=%s", ns))) }(ns) } wg.Wait() } func DeleteNamespaces(ctx context.Context, namespaces ...string) { t := getT(ctx) t.Helper() wg := sync.WaitGroup{} wg.Add(len(namespaces)) for _, ns := range namespaces { if t.Failed() { if out, err := KubectlOut(ctx, ns, "get", "events", "--field-selector", "type!=Normal"); err == nil { clog.Debugf(ctx, "Events where type != Normal from namespace %s\n%s", ns, out) } } go func(ns string) { defer wg.Done() assert.NoError(t, Kubectl(ctx, "", "delete", "namespace", "--wait=false", ns)) }(ns) } wg.Wait() } // StartLocalHttpEchoServerWithAddr is like StartLocalHttpEchoServer but binds to a specific host instead of localhost. func StartLocalHttpEchoServerWithAddr(ctx context.Context, name, addr string, responseFunc func(string, *http.Request) string) (int, context.CancelFunc) { ctx, cancel := context.WithCancel(ctx) lc := net.ListenConfig{} l, err := lc.Listen(ctx, "tcp", addr) require.NoError(getT(ctx), err, "failed to listen on localhost") if responseFunc == nil { responseFunc = func(name string, r *http.Request) string { return fmt.Sprintf("%s from intercept at %s", name, r.URL.Path) } } sc := &http.Server{ Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ioutil.Print(w, responseFunc(name, r)) }), } go func() { _ = sc.Serve(l) }() go func() { <-ctx.Done() ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), time.Second) defer cancel() err = sc.Shutdown(ctx) if err != nil { clog.Errorf(ctx, "http server on %s exited with error: %v", addr, err) } else { clog.Errorf(ctx, "http server on %s exited", addr) } }() return l.Addr().(*net.TCPAddr).Port, cancel } // StartLocalHttpEchoServer starts a local http server that echoes a line with the given name and // the current URL path. The port is returned together with a function that cancels the server. func StartLocalHttpEchoServer(ctx context.Context, name string) (int, context.CancelFunc) { return StartLocalHttpEchoServerWithAddr(ctx, name, "localhost:0", nil) } // PingInterceptedEchoServer assumes that a server has been created using StartLocalHttpEchoServer and // that an intercept is active for the given svc and svcPort that will redirect to that local server. func PingInterceptedEchoServer(ctx context.Context, svc, svcPort string, headers ...string) { wl := svc if slashIdx := strings.IndexByte(svc, '/'); slashIdx > 0 { wl = svc[slashIdx+1:] svc = svc[:slashIdx] } expectedOutput := fmt.Sprintf("%s from intercept at /", wl) PingInterceptedEchoServerAndExpect(ctx, svc, svcPort, expectedOutput, headers...) } // PingInterceptedEchoServerAndExpect assumes that a server has been created using StartLocalHttpEchoServer and // that an intercept is active for the given svc and svcPort that will redirect to that local server, and the server // will echo the given expectedOutput. func PingInterceptedEchoServerAndExpect(ctx context.Context, svc, svcPort, expectedOutput string, headers ...string) { clog.Infof(ctx, "pinging %s, expecting output: %s", net.JoinHostPort(svc, svcPort), expectedOutput) ping := func() bool { // condition ips, err := net.DefaultResolver.LookupIP(ctx, "ip", svc) if err != nil { clog.Info(ctx, err) return false } ips = iputil.UniqueSorted(ips) if len(ips) != 1 { clog.Infof(ctx, "Lookup for %s returned %v", svc, ips) return false } hc := http.Client{Timeout: 2 * time.Second} rq, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://%s", net.JoinHostPort(ips[0].String(), svcPort)), nil) if err != nil { clog.Info(ctx, err) return false } for _, h := range headers { kv := strings.SplitN(h, "=", 2) rq.Header[kv[0]] = []string{kv[1]} } resp, err := hc.Do(rq) if err != nil { clog.Info(ctx, err) return false } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { clog.Info(ctx, err) return false } r := string(body) if r != expectedOutput { clog.Infof(ctx, "body: %q != %q", r, expectedOutput) return false } return true } if ping() { return } require.Eventually(getT(ctx), ping, time.Minute, 5*time.Second, `body of %q equals %q`, "http://"+svc, expectedOutput) } func WithConfig(c context.Context, modifierFunc func(config client.Config)) context.Context { // Quit a running daemon. We're changing the directory where its config resides. TelepresenceQuitOk(c) t := getT(c) cfgVal := reflect.ValueOf(client.GetConfig(c)).Elem() cfgCopyVal := reflect.New(cfgVal.Type()) cfgCopyVal.Elem().Set(cfgVal) // By value copy configCopy := cfgCopyVal.Interface() modifierFunc(configCopy.(client.Config)) configYaml, err := configCopy.(client.Config).MarshalYAML() require.NoError(t, err) configYamlStr := string(configYaml) configDir, err := os.MkdirTemp(TempDir(c), "config") require.NoError(t, err) c, err = SetConfig(c, configDir, configYamlStr) require.NoError(t, err) return c } func WithKubeConfigExtension(ctx context.Context, extProducer func(*api.Cluster) map[string]any) context.Context { kc := KubeConfig(ctx) t := getT(ctx) cfg, err := clientcmd.LoadFromFile(kc) require.NoError(t, err, "unable to read %s", kc) cc := cfg.Contexts[cfg.CurrentContext] require.NotNil(t, cc, "unable to get current context from config") cluster := cfg.Clusters[cc.Cluster] require.NotNil(t, cluster, "unable to get current cluster from config") em := cluster.Extensions if em == nil { em = map[string]k8sruntime.Object{} } raw, err := json.Marshal(extProducer(cluster)) require.NoError(t, err, "unable to json.Marshal extension map") em["telepresence.io"] = &k8sruntime.Unknown{Raw: raw} cluster.Extensions = em kctx := *cc kctx.Cluster = "extra" cfg = &api.Config{ Kind: "Config", APIVersion: "v1", Preferences: api.Preferences{}, Clusters: map[string]*api.Cluster{"extra": cluster}, Contexts: map[string]*api.Context{"extra": &kctx}, CurrentContext: "extra", } kubeconfigFileName := filepath.Join(t.TempDir(), "kubeconfig") require.NoError(t, clientcmd.WriteToFile(*cfg, kubeconfigFileName), "unable to write modified kubeconfig") return WithEnv(ctx, map[string]string{"KUBECONFIG": strings.Join([]string{kc, kubeconfigFileName}, string([]byte{os.PathListSeparator}))}) } func WithKubeConfig(ctx context.Context, cfg *api.Config) context.Context { kubeconfigFileName := filepath.Join(TempDir(ctx), "kubeconfig") require.NoError(getT(ctx), clientcmd.WriteToFile(*cfg, kubeconfigFileName), "unable to write modified kubeconfig") return WithEnv(ctx, map[string]string{"KUBECONFIG": kubeconfigFileName}) } func RunningPods(ctx context.Context, svc, ns string) []core.Pod { return RunningPodsSelector(ctx, ns, labels.SelectorFromSet(map[string]string{"app": svc})) } func RunningPodsSelector(ctx context.Context, ns string, selector labels.Selector) []core.Pod { out, err := KubectlOut(ctx, ns, "get", "pods", "-o", "json", "--field-selector", "status.phase==Running", "-l", selector.String()) if err != nil { getT(ctx).Log(err.Error()) return nil } var pm core.PodList if err := json.UnmarshalRead(strings.NewReader(out), &pm); err != nil { getT(ctx).Log(err.Error()) return nil } return slices.DeleteFunc(pm.Items, func(pod core.Pod) bool { for _, cn := range pod.Status.ContainerStatuses { if r := cn.State.Running; r != nil && !r.StartedAt.IsZero() { // At least one container is still running. return false } } return true }) } // RunningPodNames return the names of running pods with app=. Running here means // that at least one container is still running. I.e. the pod might well be terminating // but still considered running. func RunningPodNames(ctx context.Context, svc, ns string) []string { pods := RunningPods(ctx, svc, ns) podNames := make([]string, len(pods)) for i := range pods { podNames[i] = pods[i].Name } clog.Infof(ctx, "Running pods %v", podNames) return podNames } // RunningPodsWithAgents returns the names of running pods with a matching appPrefix that // has a running traffic-agent container. func RunningPodsWithAgents(ctx context.Context, appPrefix, ns string) []string { out, err := KubectlOut(ctx, ns, "get", "pods", "-o", "json", "--field-selector", "status.phase==Running") if err != nil { getT(ctx).Log(err.Error()) return nil } var pm core.PodList if err := json.UnmarshalRead(strings.NewReader(out), &pm); err != nil { getT(ctx).Log(err.Error()) return nil } pods := make([]string, 0, len(pm.Items)) nextPod: for _, pod := range pm.Items { if !strings.HasPrefix(pod.Labels["app"], appPrefix) { continue } for _, cn := range pod.Status.ContainerStatuses { if cn.Name == agentconfig.ContainerName && cn.Ready { if r := cn.State.Running; r != nil && !r.StartedAt.IsZero() { pods = append(pods, pod.Name) continue nextPod } } } } return pods } ================================================ FILE: integration_test/itest/config.go ================================================ package itest import ( "context" "os" "path/filepath" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" ) // SetConfig creates a config from the configYml provided and assigns it to a new context which // is returned. Use this if you are testing components of the config.yml, otherwise you can use setDefaultConfig. func SetConfig(ctx context.Context, configDir, configYml string) (context.Context, error) { config, err := os.Create(filepath.Join(configDir, "config.yml")) if err != nil { return ctx, err } _, err = config.WriteString(configYml) if err != nil { return ctx, err } config.Close() // Load env if it isn't loaded already ctx = filelocation.WithAppUserConfigDir(ctx, configDir) if client.GetEnv(ctx) == nil { env, err := client.LoadEnv() if err != nil { return ctx, err } ctx = client.WithEnv(ctx, &env) } cfg, err := client.LoadConfig(ctx) if err != nil { return ctx, err } ctx = client.WithConfig(ctx, cfg) return ctx, nil } ================================================ FILE: integration_test/itest/connected.go ================================================ package itest import ( "context" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type connected struct { TrafficManager } func WithConnected(np TrafficManager, f func(ctx context.Context, ch TrafficManager)) { np.HarnessT().Run("Test_Connected", func(t *testing.T) { ctx := WithT(np.HarnessContext(), t) require.NoError(t, np.GeneralError()) ch := &connected{TrafficManager: np} ch.PushHarness(ctx, ch.setup, ch.tearDown) defer ch.PopHarness() f(ctx, ch) }) } func (ch *connected) setup(ctx context.Context) bool { t := getT(ctx) // Connect using telepresence-test-developer user stdout, _, err := Telepresence(ctx, "connect", "--namespace", ch.AppNamespace(), "--manager-namespace", ch.ManagerNamespace()) assert.NoError(t, err) assert.Contains(t, stdout, "Connected to context") if t.Failed() { return false } _, _, err = Telepresence(ctx, "loglevel", "-d30m", "debug") assert.NoError(t, err) return !t.Failed() } func (ch *connected) tearDown(ctx context.Context) { TelepresenceQuitOk(ctx) } ================================================ FILE: integration_test/itest/context.go ================================================ package itest import ( "context" "log/slog" "os" "path/filepath" "testing" "github.com/stretchr/testify/require" "github.com/telepresenceio/clog" "github.com/telepresenceio/clog/handler" "github.com/telepresenceio/clog/testutil" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/dos" "github.com/telepresenceio/telepresence/v2/pkg/maps" ) type profileKey struct{} type Profile string const ( DefaultProfile Profile = "default" GkeAutopilotProfile Profile = "gke-autopilot" ) func withProfile(ctx context.Context) context.Context { profile, ok := dos.LookupEnv(ctx, "TELEPRESENCE_TEST_PROFILE") if !ok { return context.WithValue(ctx, profileKey{}, DefaultProfile) } switch profile { case string(DefaultProfile): case string(GkeAutopilotProfile): default: panic("Invalid profile " + profile) } return context.WithValue(ctx, profileKey{}, Profile(profile)) } func GetProfile(ctx context.Context) Profile { if profile, ok := ctx.Value(profileKey{}).(Profile); ok { return profile } return DefaultProfile } type tContextKey struct{} func TestContext(t *testing.T, ossRoot, moduleRoot string) context.Context { ctx := testutil.NewContext(t, false) ctx = client.WithEnv(ctx, &client.Env{}) ctx = SetOSSRoot(ctx, ossRoot) ctx = SetModuleRoot(ctx, moduleRoot) ctx = withProfile(ctx) return WithT(ctx, t) } func WithT(ctx context.Context, t *testing.T) context.Context { ctx = context.WithValue(ctx, tContextKey{}, t) ctx, cancel := context.WithCancel( clog.WithLogger(ctx, slog.New(handler.NewText(handler.Output(t.Output()), handler.EnabledLevel(slog.LevelDebug))))) t.Cleanup(cancel) return ctx } func getT(ctx context.Context) *testing.T { if t, ok := ctx.Value(tContextKey{}).(*testing.T); ok { return t } panic("No *testing.T in context") } type ossRootKey struct{} func GetOSSRoot(ctx context.Context) string { if dir, ok := ctx.Value(ossRootKey{}).(string); ok { return dir } ossRoot, err := os.Getwd() if err != nil { panic("failed to get current directory") } return ossRoot } // SetOSSRoot sets the OSS module root for the given context to dir. func SetOSSRoot(ctx context.Context, dir string) context.Context { return context.WithValue(ctx, ossRootKey{}, dir) } // WithOSSRoot set the working directory for the Command function to the // OSS module root. func WithOSSRoot(ctx context.Context) context.Context { return WithWorkingDir(ctx, GetOSSRoot(ctx)) } type moduleRootKey struct{} func GetModuleRoot(ctx context.Context) string { if dir, ok := ctx.Value(moduleRootKey{}).(string); ok { return dir } moduleRoot, err := os.Getwd() if err != nil { panic("failed to get current directory") } return filepath.Dir(moduleRoot) } // SetModuleRoot sets the module root for the given context to dir. func SetModuleRoot(ctx context.Context, dir string) context.Context { return context.WithValue(ctx, moduleRootKey{}, dir) } // WithModuleRoot set the working directory for the Command function to the // module root. func WithModuleRoot(ctx context.Context) context.Context { return WithWorkingDir(ctx, GetModuleRoot(ctx)) } type dirContextKey struct{} // WithWorkingDir determines the working directory for the Command function. func WithWorkingDir(ctx context.Context, dir string) context.Context { return context.WithValue(ctx, dirContextKey{}, dir) } func GetWorkingDir(ctx context.Context) string { if dir, ok := ctx.Value(dirContextKey{}).(string); ok { return dir } dir, err := os.Getwd() require.NoError(getT(ctx), err, "failed to get working directory") return dir } type envContextKey struct{} // WithEnv adds environment variables to be used by the Command function. func WithEnv(ctx context.Context, env dos.MapEnv) context.Context { if prevEnv := getEnv(ctx); prevEnv != nil { merged := make(dos.MapEnv, len(prevEnv)+len(env)) maps.Merge(merged, prevEnv) maps.Merge(merged, env) env = merged } ctx = context.WithValue(ctx, envContextKey{}, env) evx, err := client.LoadEnvWith(env) if err != nil { getT(ctx).Fatal(err) } return client.WithEnv(ctx, &evx) } // WithoutEnv prevents environment variables to be used by the Command function. func WithoutEnv(ctx context.Context, keysToRemove []string) context.Context { env := getEnv(ctx) if env == nil { return ctx } env = maps.Copy(env) for _, key := range keysToRemove { delete(env, key) } ctx = context.WithValue(ctx, envContextKey{}, env) evx, err := client.LoadEnvWith(env) if err != nil { getT(ctx).Fatal(err) } return client.WithEnv(ctx, &evx) } type userContextkey struct{} func WithUser(ctx context.Context, clusterUser string) context.Context { return context.WithValue(ctx, userContextkey{}, clusterUser) } func GetUser(ctx context.Context) string { if user, ok := ctx.Value(userContextkey{}).(string); ok { return user } return "default" } func LookupEnv(ctx context.Context, key string) (value string, ok bool) { if value, ok = getEnv(ctx)[key]; !ok { value, ok = GetGlobalHarness(ctx).GlobalEnv(ctx)[key] } return value, ok } func getEnv(ctx context.Context) dos.MapEnv { if env, ok := ctx.Value(envContextKey{}).(dos.MapEnv); ok { return env } return nil } type globalHarnessContextKey struct{} func withGlobalHarness(ctx context.Context, h Cluster) context.Context { return context.WithValue(ctx, globalHarnessContextKey{}, h) } func GetGlobalHarness(ctx context.Context) Cluster { if h, ok := ctx.Value(globalHarnessContextKey{}).(Cluster); ok { return h } panic("No globalHarness in context") } type exeContextKey struct{} func WithExecutable(ctx context.Context, exe string) context.Context { return context.WithValue(ctx, exeContextKey{}, exe) } func GetExecutable(ctx context.Context) string { if user, ok := ctx.Value(exeContextKey{}).(string); ok { return user } panic("No executable in context") } ================================================ FILE: integration_test/itest/env.go ================================================ package itest import ( "context" "errors" "fmt" "io/fs" "log/slog" "os" "path/filepath" "strings" "sigs.k8s.io/yaml" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/dos" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" "github.com/telepresenceio/telepresence/v2/pkg/json" "github.com/telepresenceio/telepresence/v2/pkg/maps" ) type itestConfig struct { Env map[string]string `json:"Env,omitempty"` Config client.Config `json:"Config,omitempty"` } func LoadEnvAndConfig(ctx context.Context) context.Context { cf := filepath.Join(filelocation.AppUserConfigDir(ctx), "itest.yml") data, err := os.ReadFile(cf) var icEnv map[string]string icConfig := client.GetDefaultConfig() if err != nil { if !errors.Is(err, fs.ErrNotExist) { getT(ctx).Fatal(cf, err) } } else { var ic itestConfig data, err := yaml.YAMLToJSON(data) if err == nil { ic.Config = icConfig ic.Config.LogLevels().UserDaemon = slog.LevelDebug ic.Config.LogLevels().RootDaemon = slog.LevelDebug err = json.Unmarshal(data, &ic, true) } if err != nil { getT(ctx).Fatal(cf, err) return ctx } icEnv = ic.Env } if icEnv == nil { icEnv = make(map[string]string) } env := os.Environ() dosEnv := make(dos.MapEnv, len(env)) for _, ep := range env { if ix := strings.IndexByte(ep, '='); ix > 0 { dosEnv[ep[:ix]] = ep[ix+1:] } } maps.Merge(dosEnv, icEnv) // Ensure that build-output/bin is on the path buildBin := filepath.Join(BuildOutput(ctx), "bin") path, ok := dosEnv["PATH"] if ok { dosEnv["PATH"] = fmt.Sprintf("%s%c%s", buildBin, os.PathListSeparator, path) } else { dosEnv["PATH"] = buildBin } ctx = client.WithConfig(ctx, icConfig) return dos.WithEnv(ctx, dosEnv) } func BuildOutput(ctx context.Context) string { return filepath.Join(GetModuleRoot(ctx), "build-output") } ================================================ FILE: integration_test/itest/harness.go ================================================ package itest import ( "context" "regexp" "runtime/debug" "testing" "github.com/stretchr/testify/suite" "github.com/telepresenceio/telepresence/v2/pkg/dos" ) type Harness interface { Cluster PushHarness(ctx context.Context, setup func(ctx context.Context) bool, tearDown func(ctx context.Context)) RunSuite(TestingSuite) HarnessContext() context.Context SetupSuite() HarnessT() *testing.T PopHarness() } type upAndDown struct { setup func(ctx context.Context) bool tearDown func(ctx context.Context) setupWith context.Context wasSetup bool } type harness struct { Cluster ctx context.Context upAndDowns []upAndDown wasSetup bool } func NewContextHarness(ctx context.Context) Harness { return &harness{Cluster: GetGlobalHarness(ctx), ctx: ctx} } func (h *harness) PushHarness(ctx context.Context, setup func(ctx context.Context) bool, tearDown func(ctx context.Context)) { h.upAndDowns = append(h.upAndDowns, upAndDown{setup: setup, tearDown: tearDown, setupWith: ctx}) h.wasSetup = false } func (h *harness) HarnessContext() context.Context { if l := len(h.upAndDowns) - 1; l >= 0 { return h.upAndDowns[l].setupWith } return h.ctx } func (h *harness) RunSuite(s TestingSuite) { if suiteEnabled(h.HarnessContext(), s) { s.setContext(s.AmendSuiteContext(h.HarnessContext())) h.HarnessT().Run(s.SuiteName(), func(t *testing.T) { suite.Run(t, s) }) } } func suiteEnabled(ctx context.Context, s TestingSuite) bool { suiteRx := dos.Getenv(ctx, "TEST_SUITE") if suiteRx == "" { return true } r, err := regexp.Compile(suiteRx) if err != nil { getT(ctx).Fatal(err) } return r.MatchString(s.SuiteName()) } // SetupSuite calls all functions that has been added with AddSetup in the order they // were added. This only happens once. Repeated calls to this function on the same // instance will do nothing. // // Repeated calls are expected since this function will be called before each // test. func (h *harness) SetupSuite() { if h.wasSetup { return } h.wasSetup = true if err := h.GeneralError(); err != nil { h.HarnessT().Fatal(err) // Immediately fail the test if a general error has been set } uds := h.upAndDowns for i := range uds { upDown := &uds[i] if setup := upDown.setup; setup != nil { upDown.setup = nil // Never setup twice if upDown.wasSetup = safeSetUp(upDown.setupWith, setup); !upDown.wasSetup { getT(h.ctx).FailNow() } } } } // PopHarness calls the tearDown function that was pushed last and removes it. func (h *harness) PopHarness() { last := len(h.upAndDowns) - 1 if last < 0 { return } upDown := &h.upAndDowns[last] h.upAndDowns = h.upAndDowns[:last] if upDown.setupWith != nil { if tearDown := upDown.tearDown; tearDown != nil { upDown.tearDown = nil // Never tear down twice if upDown.wasSetup { safeTearDown(upDown.setupWith, tearDown) } } } } func safeSetUp(ctx context.Context, f func(context.Context) bool) bool { defer failOnPanic(ctx) return f(ctx) } func safeTearDown(ctx context.Context, f func(context.Context)) { defer failOnPanic(ctx) f(ctx) } func failOnPanic(ctx context.Context) { if r := recover(); r != nil { t := getT(ctx) t.Errorf("test panicked: %v\n%s", r, debug.Stack()) t.FailNow() } } func (h *harness) HarnessT() *testing.T { return getT(h.HarnessContext()) } ================================================ FILE: integration_test/itest/helm.go ================================================ package itest import ( "bytes" "context" "fmt" "os" "path/filepath" "slices" "strings" "time" "github.com/go-json-experiment/json" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" rbac "k8s.io/api/rbac/v1" meta "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/yaml" "github.com/telepresenceio/clog" telcharts "github.com/telepresenceio/telepresence/v2/charts" "github.com/telepresenceio/telepresence/v2/pkg/agentconfig" "github.com/telepresenceio/telepresence/v2/pkg/dos" "github.com/telepresenceio/telepresence/v2/pkg/labels" "github.com/telepresenceio/telepresence/v2/pkg/version" ) func (s *cluster) PackageHelmChart(ctx context.Context) (string, error) { filename := filepath.Join(getT(ctx).TempDir(), telcharts.TelepresenceChartName+"-chart.tgz") fh, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0o666) if err != nil { return "", err } if err := telcharts.WriteChart(telcharts.DirTypeTelepresence, fh, telcharts.TelepresenceChartName, s.self.ManagerVersion()); err != nil { _ = fh.Close() return "", err } if err := fh.Close(); err != nil { return "", err } return filename, nil } func (s *cluster) GetSetArgsForHelm(ctx context.Context, values map[string]any, release bool) []string { settings := s.GetValuesForHelm(ctx, values, release) args := make([]string, len(settings)*2) n := 0 for _, s := range settings { args[n] = "--set-json" n++ args[n] = s n++ } return args } func (s *cluster) GetValuesForHelm(ctx context.Context, values map[string]any, release bool) []string { nss := GetNamespaces(ctx) settings := []string{ `logLevel="debug"`, } if s.ManagerVersion().EQ(version.Structured) { reg := s.self.ManagerRegistry() if reg == "local" { settings = append(settings, `image.pullPolicy="Never"`) settings = append(settings, `agent.image.pullPolicy="Never"`) } else if strings.HasPrefix(reg, "localhost:") { settings = append(settings, `image.pullPolicy="Always"`) settings = append(settings, `agent.image.pullPolicy="Always"`) } } if nss != nil && nss.Selector != nil { j, err := json.Marshal(nss.Selector) if err != nil { clog.Errorf(ctx, "unable to marshal selector '%v': %v", nss.Selector, err) } else { settings = append(settings, `namespaceSelector=`+string(j)) } } agentImage := GetAgentImage(ctx) if agentImage != nil { settings = append(settings, fmt.Sprintf(`agent.image.name=%q`, agentImage.Name), // Prevent attempts to retrieve image from SystemA fmt.Sprintf(`agent.image.tag=%q`, agentImage.Tag), fmt.Sprintf(`agent.image.registry=%q`, agentImage.Registry)) } if !release { settings = append(settings, fmt.Sprintf(`image.registry=%q`, s.self.ManagerRegistry())) } for k, v := range values { j, err := json.Marshal(v) if err != nil { clog.Errorf(ctx, "unable to marshal value %v: %v", v, err) } else { settings = append(settings, k+"="+string(j)) } } return settings } func (s *cluster) TelepresenceHelmInstallOK(ctx context.Context, upgrade bool, settings ...string) string { logFile, err := s.self.TelepresenceHelmInstall(ctx, upgrade, settings...) require.NoError(getT(ctx), err) return logFile } func (s *cluster) TelepresenceHelmInstall(ctx context.Context, upgrade bool, settings ...string) (string, error) { nss := GetNamespaces(ctx) subjectNames := []string{TestUser} subjects := make([]rbac.Subject, len(subjectNames)) for i, s := range subjectNames { subjects[i] = rbac.Subject{ Kind: "ServiceAccount", Name: s, Namespace: nss.Namespace, } } type xRbac struct { Create bool `json:"create"` Namespaced bool `json:"namespaced,omitempty"` Subjects []rbac.Subject `json:"subjects,omitempty"` Namespaces []string `json:"namespaces,omitempty"` } type xAgent struct { Image *Image `json:"image,omitempty"` } agentImage := GetAgentImage(ctx) agent := &xAgent{Image: agentImage} type xClient struct { Routing map[string][]string `json:"routing"` } type xTimeouts struct { AgentArrival string `json:"agentArrival,omitempty"` } managerRbac := xRbac{ Create: true, } clientRbac := xRbac{ Create: true, Subjects: subjects, } vx := struct { LogLevel string `json:"logLevel"` Image *Image `json:"image,omitempty"` Agent *xAgent `json:"agent,omitempty"` ClientRbac *xRbac `json:"clientRbac"` ManagerRbac *xRbac `json:"managerRbac"` Client xClient `json:"client"` Timeouts xTimeouts `json:"timeouts,omitempty"` Namespaces []string `json:"namespaces,omitempty"` NamespaceSelector *labels.Selector `json:"namespaceSelector,omitempty"` }{ LogLevel: "debug", Agent: agent, ClientRbac: &clientRbac, ManagerRbac: &managerRbac, Client: xClient{ Routing: map[string][]string{}, }, Timeouts: xTimeouts{AgentArrival: "60s"}, } if managedNamespaces := nss.Selector.StaticNames(); len(managedNamespaces) > 0 { if s.ManagerIsVersion(">2.21.x") { vx.Namespaces = managedNamespaces } else { if !slices.Contains(managedNamespaces, nss.Namespace) { managedNamespaces = append(managedNamespaces, nss.Namespace) } svcAccArg := "--serviceaccount=" + nss.Namespace + ":" + TestUser if !s.ManagerIsVersion(">2.21.x") { clientRbac.Namespaced = true clientRbac.Namespaces = managedNamespaces managerRbac.Namespaced = true managerRbac.Namespaces = managedNamespaces role := "tele-update-config" // Agent is removed by removing its entry in the telepresence-agents configmap for _, ns := range managedNamespaces { err := Kubectl(ctx, ns, "create", "role", role, "--verb=update", "--resource=configmaps", "--resource-name=telepresence-agents") if err != nil && !strings.Contains(err.Error(), "already exists") { return "", err } err = Kubectl(ctx, ns, "create", "rolebinding", role, "--role", role, svcAccArg) if err != nil && !strings.Contains(err.Error(), "already exists") { return "", err } } } } if !s.ClientIsVersion(">2.21.x") && s.ManagerIsVersion(">2.21.x") { // Clients older than 2.22.0 need several additional permissions. role := "tele-client" for _, ns := range managedNamespaces { r := rbac.Role{ TypeMeta: meta.TypeMeta{ APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role", }, ObjectMeta: meta.ObjectMeta{ Name: role, Namespace: ns, }, Rules: []rbac.PolicyRule{ { Verbs: []string{"get", "list", "watch"}, APIGroups: []string{"apps"}, Resources: []string{"deployments", "replicasets", "statefulsets"}, }, { Verbs: []string{"get", "list", "watch"}, APIGroups: []string{"argoproj.io"}, Resources: []string{"rollouts"}, }, { Verbs: []string{"get", "list", "watch"}, APIGroups: []string{""}, Resources: []string{"services"}, }, }, } rj, err := yaml.Marshal(&r) if err != nil { return "", err } rb := rbac.RoleBinding{ TypeMeta: meta.TypeMeta{ APIVersion: "rbac.authorization.k8s.io/v1", Kind: "RoleBinding", }, ObjectMeta: meta.ObjectMeta{ Name: role, Namespace: ns, }, Subjects: subjects, RoleRef: rbac.RoleRef{ APIGroup: "rbac.authorization.k8s.io", Kind: "Role", Name: role, }, } rbj, err := yaml.Marshal(&rb) if err != nil { return "", err } rj = append(rj, []byte("\n---\n")...) rj = append(rj, rbj...) err = Kubectl(dos.WithStdin(ctx, bytes.NewReader(rj)), ns, "apply", "-f", "-") if err != nil { return "", err } } } } else { vx.NamespaceSelector = nss.Selector } vx.Image = GetImage(ctx) if s.ManagerVersion().EQ(s.ClientVersion()) { reg := s.ManagerRegistry() if reg == "local" { // Using Kind/minikube/k3d/Docker Desktop with images loaded directly into the cluster. // They are automatically present and must not be pulled. vx.Image.PullPolicy = "Never" vx.Agent.Image.PullPolicy = "Never" } else if strings.HasPrefix(reg, "localhost:") { // Using a local registry. Images must always be pulled to get the latest build. vx.Image.PullPolicy = "Always" vx.Agent.Image.PullPolicy = "Always" } } ss, err := yaml.Marshal(&vx) if err != nil { return "", err } valuesFile := filepath.Join(getT(ctx).TempDir(), "values.yaml") if err := os.WriteFile(valuesFile, ss, 0o644); err != nil { return "", err } verb := "install" if upgrade { verb = "upgrade" } args := []string{"helm", verb, "-n", nss.Namespace, "-f", valuesFile} if !s.ManagerVersion().EQ(s.ClientVersion()) { // Need to use the built executable because the client version doesn't handle the --version flag. ctx = WithExecutable(ctx, s.executable) if !s.ManagerVersion().EQ(version.Structured) { args = append(args, "--version", s.ManagerVersion().String()) } } args = append(args, settings...) ctx = WithoutEnv(ctx, []string{"TELEPRESENCE_REGISTRY", "TELEPRESENCE_VERSION"}) if _, _, err = Telepresence(WithUser(ctx, "default"), args...); err != nil { return "", err } if err = RolloutStatusWait(ctx, nss.Namespace, "deploy/"+agentconfig.ManagerAppName); err != nil { return "", err } logFileName := s.self.CapturePodLogs(ctx, agentconfig.ManagerAppName, "", nss.Namespace) if !s.ManagerIsVersion(">2.21.x") { // Give the manager time to perform rollouts, listen to telepresence-agents configmap, etc. time.Sleep(2 * time.Second) } if err != nil { return "", err } return logFileName, nil } func (s *cluster) UninstallTrafficManager(ctx context.Context, managerNamespace string, args ...string) { t := getT(ctx) ctx = WithUser(ctx, "default") TelepresenceOk(ctx, append([]string{"helm", "uninstall", "--manager-namespace", managerNamespace}, args...)...) // Helm uninstall does deletions asynchronously, so let's wait until the deployment is gone assert.Eventually(t, func() bool { return len(RunningPodNames(ctx, agentconfig.ManagerAppName, managerNamespace)) == 0 }, 60*time.Second, 4*time.Second, "traffic-manager deployment was not removed") TelepresenceQuitOk(ctx) } ================================================ FILE: integration_test/itest/image.go ================================================ package itest import ( "context" "strings" "github.com/telepresenceio/telepresence/v2/pkg/dos" ) type Image struct { Name string `json:"name,omitempty"` Tag string `json:"tag,omitempty"` Registry string `json:"registry,omitempty"` PullPolicy string `json:"pullPolicy,omitempty"` } func (img *Image) FQName() string { nb := strings.Builder{} if img.Registry != "" { nb.WriteString(img.Registry) nb.WriteByte('/') } nb.WriteString(img.Name) if img.Tag != "" { nb.WriteByte(':') nb.WriteString(img.Tag) } return nb.String() } func ImageFromEnv(ctx context.Context, env, defaultTag, defaultRegistry string) *Image { if imgQN, ok := dos.LookupEnv(ctx, env); ok { img := new(Image) i := strings.LastIndexByte(imgQN, '/') if i >= 0 { img.Registry = imgQN[:i] imgQN = imgQN[i+1:] } else { img.Registry = defaultRegistry } if i = strings.IndexByte(imgQN, ':'); i > 0 { img.Name = imgQN[:i] img.Tag = imgQN[i+1:] } else { img.Name = imgQN img.Tag = defaultTag } return img } return nil } type imageContextKey struct{} func WithImage(ctx context.Context, image *Image) context.Context { return context.WithValue(ctx, imageContextKey{}, image) } func GetImage(ctx context.Context) *Image { if image, ok := ctx.Value(imageContextKey{}).(*Image); ok { return image } return nil } type clientImageContextKey struct{} func WithClientImage(ctx context.Context, image *Image) context.Context { return context.WithValue(ctx, clientImageContextKey{}, image) } func GetClientImage(ctx context.Context) *Image { if image, ok := ctx.Value(clientImageContextKey{}).(*Image); ok { return image } return nil } type agentImageContextKey struct{} func WithAgentImage(ctx context.Context, image *Image) context.Context { return context.WithValue(ctx, agentImageContextKey{}, image) } func GetAgentImage(ctx context.Context) *Image { if image, ok := ctx.Value(agentImageContextKey{}).(*Image); ok { return image } return nil } ================================================ FILE: integration_test/itest/logdir.go ================================================ package itest import ( "context" "fmt" "os" "path/filepath" "regexp" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" ) func CleanLogDir(ctx context.Context, require *Requirements, nsRx, mgrNamespace, svcNameRx string) { logDir := filelocation.AppUserLogDir(ctx) files, err := os.ReadDir(logDir) require.NoError(err) match := regexp.MustCompile( fmt.Sprintf(`^(?:traffic-manager-[0-9a-z-]+\.%s|%s-[0-9a-z-]+\.%s)\.(?:log|yaml)$`, mgrNamespace, svcNameRx, nsRx)) for _, file := range files { if match.MatchString(file.Name()) { clog.Infof(ctx, "Deleting log-file %s", file.Name()) require.NoError(os.Remove(filepath.Join(logDir, file.Name()))) } } } ================================================ FILE: integration_test/itest/multiple_services.go ================================================ package itest import ( "context" "fmt" "sync" "testing" ) type MultipleServices interface { NamespacePair Name() string ServiceCount() int } type multipleServices struct { NamespacePair name string serviceCount int } func WithMultipleServices(np NamespacePair, name string, serviceCount int, f func(MultipleServices)) { np.HarnessT().Run(fmt.Sprintf("Test_Services_%d", serviceCount), func(t *testing.T) { ctx := WithT(np.HarnessContext(), t) ms := &multipleServices{NamespacePair: np, name: name, serviceCount: serviceCount} ms.PushHarness(ctx, ms.setup, ms.tearDown) defer ms.PopHarness() f(ms) }) } func (h *multipleServices) setup(ctx context.Context) bool { wg := sync.WaitGroup{} wg.Add(h.serviceCount) for i := 0; i < h.serviceCount; i++ { go func(i int) { defer wg.Done() h.ApplyEchoService(ctx, fmt.Sprintf("%s-%d", h.name, i), 80) }(i) } wg.Wait() return true } func (h *multipleServices) tearDown(ctx context.Context) { wg := sync.WaitGroup{} wg.Add(h.serviceCount) for i := 0; i < h.serviceCount; i++ { go func(i int) { defer wg.Done() h.DeleteSvcAndWorkload(ctx, "deploy", fmt.Sprintf("hello-%d", i)) }(i) } wg.Wait() } func (h *multipleServices) Name() string { return h.name } func (h *multipleServices) ServiceCount() int { return h.serviceCount } ================================================ FILE: integration_test/itest/namespace.go ================================================ package itest import ( "bytes" "context" "fmt" "path/filepath" "sync" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/pkg/agentconfig" "github.com/telepresenceio/telepresence/v2/pkg/dos" "github.com/telepresenceio/telepresence/v2/pkg/labels" ) type NamespacePair interface { Harness ApplyApp(ctx context.Context, name, workload string) ApplyEchoService(ctx context.Context, name string, port int) ApplyTemplate(ctx context.Context, path string, values any) DeleteApp(ctx context.Context, name string) DeleteTemplate(ctx context.Context, path string, values any) AppNamespace() string TelepresenceConnect(ctx context.Context, args ...string) string TelepresenceTryConnect(ctx context.Context, args ...string) (string, error) DeleteSvcAndWorkload(ctx context.Context, workload, name string) Kubectl(ctx context.Context, args ...string) error KubectlOk(ctx context.Context, args ...string) string KubectlOut(ctx context.Context, args ...string) (string, error) ManagerNamespace() string RollbackTM(ctx context.Context) RolloutStatusWait(ctx context.Context, workload string) error } type Namespaces struct { Namespace string `json:"namespace,omitempty"` Selector *labels.Selector `json:"managedNamespaces,omitempty"` } type namespacesContextKey struct{} func WithNamespaces(ctx context.Context, namespaces *Namespaces) context.Context { return context.WithValue(ctx, namespacesContextKey{}, namespaces) } func GetNamespaces(ctx context.Context) *Namespaces { if namespaces, ok := ctx.Value(namespacesContextKey{}).(*Namespaces); ok { return namespaces } return nil } // The namespaceSuite has no tests. It's sole purpose is to create and destroy the namespaces and // any non-namespaced resources that we, ourselves, make nsPair specific, such as the // mutating webhook configuration for the traffic-agent injection. type nsPair struct { Harness Namespaces } // TelepresenceConnect connects using the AppNamespace and ManagerNamespace and requires the result to be OK. func (s *nsPair) TelepresenceConnect(ctx context.Context, args ...string) string { return TelepresenceOk(ctx, append( []string{"connect", "--namespace", s.AppNamespace(), "--manager-namespace", s.ManagerNamespace()}, args...)...) } // TelepresenceTryConnect connects using the AppNamespace and ManagerNamespace and returns an error on failure. func (s *nsPair) TelepresenceTryConnect(ctx context.Context, args ...string) (string, error) { stdout, _, err := Telepresence(ctx, append( []string{"connect", "--namespace", s.AppNamespace(), "--manager-namespace", s.ManagerNamespace()}, args...)...) return stdout, err } func WithNamespacePair(ctx context.Context, suffix string, f func(NamespacePair)) { s := &nsPair{} var namespace string namespace, s.Namespace = AppAndMgrNSName(suffix) s.Selector = labels.SelectorFromNames(namespace) getT(ctx).Run(fmt.Sprintf("Test_Namespaces_%s", suffix), func(t *testing.T) { ctx = WithT(ctx, t) ctx = WithUser(ctx, s.Namespace+":"+TestUser) ctx = WithNamespaces(ctx, &s.Namespaces) s.Harness = NewContextHarness(ctx) s.PushHarness(ctx, s.setup, s.tearDown) defer s.PopHarness() f(s) }) } func (s *nsPair) setup(ctx context.Context) bool { CreateNamespaces(ctx, s.AppNamespace(), s.Namespace) t := getT(ctx) if t.Failed() { return false } err := Kubectl(ctx, s.Namespace, "apply", "-f", filepath.Join(GetOSSRoot(ctx), "testdata", "k8s", "client_sa.yaml")) if assert.NoError(t, err, "failed to create connect ServiceAccount") { db, err := ReadTemplate(ctx, filepath.Join(GetOSSRoot(ctx), "testdata", "k8s", "client_rancher.goyaml"), map[string]string{ "ManagerNamespace": s.Namespace, }) if assert.NoError(t, err) { assert.NoError(t, Kubectl(dos.WithStdin(ctx, bytes.NewReader(db)), s.Namespace, "apply", "-f", "-")) } } return !t.Failed() } func AppAndMgrNSName(suffix string) (appNS, mgrNS string) { mgrNS = fmt.Sprintf("ambassador-%s", suffix) appNS = fmt.Sprintf("telepresence-%s", suffix) return appNS, mgrNS } func (s *nsPair) tearDown(ctx context.Context) { wg := sync.WaitGroup{} wg.Add(1) go func() { defer wg.Done() DeleteNamespaces(ctx, s.AppNamespace(), s.Namespace) }() wg.Add(1) go func() { defer wg.Done() _ = Kubectl(ctx, "", "delete", "--wait=false", "mutatingwebhookconfiguration", "agent-injector-webhook-"+s.Namespace) }() wg.Wait() } func (s *nsPair) RollbackTM(ctx context.Context) { ctx, cancel := context.WithTimeout(ctx, 5*time.Minute) defer cancel() err := Command(ctx, "helm", "rollback", "--no-hooks", "--wait", "--namespace", s.ManagerNamespace(), agentconfig.ManagerAppName).Run() t := getT(ctx) require.NoError(t, err) require.NoError(t, RolloutStatusWait(ctx, s.Namespace, "deploy/"+agentconfig.ManagerAppName)) assert.Eventually(t, func() bool { return len(RunningPodNames(ctx, agentconfig.ManagerAppName, s.Namespace)) == 1 }, 30*time.Second, 5*time.Second) s.CapturePodLogs(ctx, agentconfig.ManagerAppName, "", s.Namespace) } func (s *nsPair) AppNamespace() string { if len(s.Selector.MatchExpressions) == 1 { m := s.Selector.MatchExpressions[0] if m.Key == labels.NameLabelKey && m.Operator == labels.OperatorIn { return m.Values[0] } } return "" } func (s *nsPair) ManagerNamespace() string { return s.Namespace } func (s *nsPair) ApplyEchoService(ctx context.Context, name string, port int) { getT(ctx).Helper() ApplyEchoService(ctx, name, s.AppNamespace(), port) } // ApplyApp calls kubectl apply -n -f on the given app + .yaml found in testdata/k8s relative // to the directory returned by GetCurrentDirectory. func (s *nsPair) ApplyApp(ctx context.Context, name, workload string) { getT(ctx).Helper() ApplyApp(ctx, name, s.AppNamespace(), workload) } // DeleteApp calls kubectl delete -n -f on the given app + .yaml found in testdata/k8s relative // to the directory returned by GetCurrentDirectory. func (s *nsPair) DeleteApp(ctx context.Context, name string) { getT(ctx).Helper() DeleteApp(ctx, name, s.AppNamespace()) } func (s *nsPair) RolloutStatusWait(ctx context.Context, workload string) error { return RolloutStatusWait(ctx, s.AppNamespace(), workload) } func (s *nsPair) DeleteSvcAndWorkload(ctx context.Context, workload, name string) { getT(ctx).Helper() DeleteSvcAndWorkload(ctx, workload, name, s.AppNamespace()) } func (s *nsPair) ApplyTemplate(ctx context.Context, path string, values any) { s.doWithTemplate(ctx, "apply", path, values) } func (s *nsPair) DeleteTemplate(ctx context.Context, path string, values any) { yml, err := ReadTemplate(ctx, path, values) require.NoError(getT(ctx), err) if err = s.Kubectl(dos.WithStdin(ctx, bytes.NewReader(yml)), "delete", "-f", "-"); err != nil { clog.Errorf(ctx, "unable to delete %q", string(yml)) getT(ctx).Fatal(err) } } func (s *nsPair) doWithTemplate(ctx context.Context, action, path string, values any) { yml, err := ReadTemplate(ctx, path, values) require.NoError(getT(ctx), err) if err = s.Kubectl(dos.WithStdin(ctx, bytes.NewReader(yml)), action, "-f", "-"); err != nil { clog.Errorf(ctx, "unable to %s %q", action, string(yml)) getT(ctx).Fatal(err) } } // Kubectl runs kubectl with the default context and the application namespace. func (s *nsPair) Kubectl(ctx context.Context, args ...string) error { getT(ctx).Helper() return Kubectl(ctx, s.AppNamespace(), args...) } // KubectlOk runs kubectl with the default context and the application namespace and returns its combined output // and fails if an error occurred. func (s *nsPair) KubectlOk(ctx context.Context, args ...string) string { out, err := KubectlOut(ctx, s.AppNamespace(), args...) require.NoError(getT(ctx), err) return out } // KubectlOut runs kubectl with the default context and the application namespace and returns its combined output. func (s *nsPair) KubectlOut(ctx context.Context, args ...string) (string, error) { getT(ctx).Helper() return KubectlOut(ctx, s.AppNamespace(), args...) } ================================================ FILE: integration_test/itest/runner.go ================================================ package itest import ( "context" "os" "path/filepath" "testing" "github.com/alexflint/go-filemutex" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) type Runner interface { AddClusterSuite(func(context.Context) TestingSuite) AddNamespacePairSuite(suffix string, f func(NamespacePair) TestingSuite) AddTrafficManagerSuite(suffix string, f func(TrafficManager) TestingSuite) AddConnectedSuite(suffix string, f func(TrafficManager) TestingSuite) AddMultipleServicesSuite(suffix, name string, svcCount int, f func(MultipleServices) TestingSuite) AddSingleServiceSuite(suffix, name string, f func(SingleService) TestingSuite) RunTests(context.Context) } type namedRunner struct { withMultipleServices []func(MultipleServices) TestingSuite withSingleService []func(SingleService) TestingSuite } type runnerKey struct { name string svcCount int } type suffixedRunner struct { withNamespace []func(NamespacePair) TestingSuite withTrafficManager []func(TrafficManager) TestingSuite withConnected []func(TrafficManager) TestingSuite withName map[runnerKey]*namedRunner } type runner struct { withCluster []func(ctx context.Context) TestingSuite withSuffix map[string]*suffixedRunner } var defaultRunner Runner = &runner{withSuffix: make(map[string]*suffixedRunner)} //nolint:gochecknoglobals // integration test config // AddClusterSuite adds a constructor for a test suite that requires a cluster to run to the default runner. func AddClusterSuite(f func(context.Context) TestingSuite) { defaultRunner.AddClusterSuite(f) } // AddClusterSuite adds a constructor for a test suite that requires a cluster to run. func (r *runner) AddClusterSuite(f func(context.Context) TestingSuite) { r.withCluster = append(r.withCluster, f) } func (r *runner) forSuffix(suffix string) *suffixedRunner { sr, ok := r.withSuffix[suffix] if !ok { sr = &suffixedRunner{withName: map[runnerKey]*namedRunner{}} r.withSuffix[suffix] = sr } return sr } // AddNamespacePairSuite adds a constructor for a test suite that requires a cluster where a namespace // pair has been initialized to the default runner. func AddNamespacePairSuite(suffix string, f func(NamespacePair) TestingSuite) { defaultRunner.AddNamespacePairSuite(suffix, f) } // AddNamespacePairSuite adds a constructor for a test suite that requires a cluster where a namespace // pair has been initialized. func (r *runner) AddNamespacePairSuite(suffix string, f func(NamespacePair) TestingSuite) { sr := r.forSuffix(suffix) sr.withNamespace = append(sr.withNamespace, f) } // AddTrafficManagerSuite adds a constructor for a test suite that requires a cluster where a namespace // pair has been initialized and a traffic manager is installed. func AddTrafficManagerSuite(suffix string, f func(manager TrafficManager) TestingSuite) { defaultRunner.AddTrafficManagerSuite(suffix, f) } // AddTrafficManagerSuite adds a constructor for a test suite that requires a cluster where a namespace // pair has been initialized and a traffic manager is installed. func (r *runner) AddTrafficManagerSuite(suffix string, f func(TrafficManager) TestingSuite) { sr := r.forSuffix(suffix) sr.withTrafficManager = append(sr.withTrafficManager, f) } func (r *suffixedRunner) forName(key runnerKey) *namedRunner { nr, ok := r.withName[key] if !ok { nr = &namedRunner{} r.withName[key] = nr } return nr } // AddConnectedSuite adds a constructor for a test suite to the default runner that requires a cluster where a namespace // pair has been initialized, and telepresence is connected. func AddConnectedSuite(suffix string, f func(TrafficManager) TestingSuite) { defaultRunner.AddConnectedSuite(suffix, f) } // AddConnectedSuite adds a constructor for a test suite to the default runner that requires a cluster where a namespace // pair has been initialized, and telepresence is connected. func (r *runner) AddConnectedSuite(suffix string, f func(TrafficManager) TestingSuite) { sr := r.forSuffix(suffix) sr.withConnected = append(sr.withConnected, f) } // AddMultipleServicesSuite adds a constructor for a test suite to the default runner that requires a cluster where a namespace // pair has been initialized, multiple services have been installed, and telepresence is connected. func AddMultipleServicesSuite(suffix, name string, svcCount int, f func(services MultipleServices) TestingSuite) { defaultRunner.AddMultipleServicesSuite(suffix, name, svcCount, f) } // AddMultipleServicesSuite adds a constructor for a test suite that requires a cluster where a namespace // pair has been initialized, multiple services have been installed, and telepresence is connected. func (r *runner) AddMultipleServicesSuite(suffix, name string, svcCount int, f func(services MultipleServices) TestingSuite) { nr := r.forSuffix(suffix).forName(runnerKey{name: name, svcCount: svcCount}) nr.withMultipleServices = append(nr.withMultipleServices, f) } // AddSingleServiceSuite adds a constructor for a test suite to the default runner that requires a cluster where a namespace // pair has been initialized, a service has been installed, and telepresence is connected. func AddSingleServiceSuite(suffix, name string, f func(services SingleService) TestingSuite) { defaultRunner.AddSingleServiceSuite(suffix, name, f) } // AddSingleServiceSuite adds a constructor for a test suite that requires a cluster where a namespace // pair has been initialized, a service has been installed, and telepresence is connected. func (r *runner) AddSingleServiceSuite(suffix, name string, f func(services SingleService) TestingSuite) { nr := r.forSuffix(suffix).forName(runnerKey{name: name}) nr.withSingleService = append(nr.withSingleService, f) } func RunTests(c context.Context) { defaultRunner.RunTests(c) } // RunTests creates all suites using the added constructors and runs them. func (r *runner) RunTests(c context.Context) { //nolint:gocognit c = LoadEnvAndConfig(c) m, err := filemutex.New(filepath.Join(os.TempDir(), "telepresence-itest.lock")) if err != nil { require.NoError(getT(c), err) } err = m.Lock() // Will block until lock can be acquired if err != nil { require.NoError(getT(c), err) } defer func() { _ = m.Unlock() }() WithCluster(c, func(c context.Context) { func() { t := getT(c) for _, f := range r.withCluster { s := f(c) if suiteEnabled(c, s) { t.Run(s.SuiteName(), func(t *testing.T) { ts := f(c) ts.setContext(ts.AmendSuiteContext(c)) suite.Run(t, ts) }) } } }() for s, sr := range r.withSuffix { WithNamespacePair(c, GetGlobalHarness(c).Suffix()+s, func(np NamespacePair) { for _, f := range sr.withNamespace { np.RunSuite(f(np)) } if len(sr.withTrafficManager)+len(sr.withConnected)+len(sr.withName) > 0 { WithTrafficManager(np, func(c context.Context, cnp TrafficManager) { for _, f := range sr.withTrafficManager { cnp.RunSuite(f(cnp)) } if len(sr.withConnected)+len(sr.withName) > 0 { WithConnected(cnp, func(c context.Context, cnp TrafficManager) { for _, f := range sr.withConnected { cnp.RunSuite(f(cnp)) } for n, nr := range sr.withName { if len(nr.withMultipleServices) > 0 { WithMultipleServices(cnp, n.name, n.svcCount, func(ms MultipleServices) { for _, f := range nr.withMultipleServices { ms.RunSuite(f(ms)) } }) } if len(nr.withSingleService) > 0 { WithSingleService(cnp, n.name, func(ss SingleService) { for _, f := range nr.withSingleService { ss.RunSuite(f(ss)) } }) } } }) } }) } }) } }) } ================================================ FILE: integration_test/itest/single_service.go ================================================ package itest import ( "context" "fmt" "testing" ) type SingleService interface { NamespacePair ServiceName() string } type singleService struct { NamespacePair serviceName string } func WithSingleService(h NamespacePair, serviceName string, f func(SingleService)) { h.HarnessT().Run(fmt.Sprintf("Test_Service_%s", serviceName), func(t *testing.T) { ctx := WithT(h.HarnessContext(), t) s := &singleService{NamespacePair: h, serviceName: serviceName} s.PushHarness(ctx, s.setup, s.tearDown) defer h.PopHarness() f(s) }) } func (h *singleService) setup(ctx context.Context) bool { h.ApplyEchoService(ctx, h.serviceName, 80) return true } func (h *singleService) tearDown(ctx context.Context) { h.DeleteSvcAndWorkload(ctx, "deploy", h.serviceName) } func (h *singleService) ServiceName() string { return h.serviceName } ================================================ FILE: integration_test/itest/status.go ================================================ package itest import ( "context" "errors" "github.com/go-json-experiment/json" "github.com/stretchr/testify/require" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/cmd" ) type StatusResponse struct { RootDaemon *cmd.RootDaemonStatus `json:"root_daemon,omitempty"` UserDaemon *cmd.UserDaemonStatus `json:"user_daemon,omitempty"` TrafficManager *cmd.TrafficManagerStatus `json:"traffic_manager,omitempty"` ContainerizedDaemon *cmd.ContainerizedDaemonStatus `json:"daemon,omitempty"` Connections []struct { RootDaemon *cmd.RootDaemonStatus `json:"root_daemon,omitempty"` UserDaemon *cmd.UserDaemonStatus `json:"user_daemon,omitempty"` TrafficManager *cmd.TrafficManagerStatus `json:"traffic_manager,omitempty"` ContainerizedDaemon *cmd.ContainerizedDaemonStatus `json:"daemon,omitempty"` } `json:"connections,omitempty"` Error string `json:"err,omitempty"` } func TelepresenceStatus(ctx context.Context, args ...string) (*StatusResponse, error) { stdout, stderr, err := Telepresence(ctx, append([]string{"status", "--output", "json"}, args...)...) var status StatusResponse jErr := json.Unmarshal([]byte(stdout), &status) if err != nil { if jErr == nil && status.Error != "" { clog.Error(ctx, status.Error) return nil, errors.New(status.Error) } clog.Error(ctx, stderr) return nil, err } if jErr != nil { return nil, jErr } if cd := status.ContainerizedDaemon; cd != nil { if cd.RoutingSnake == nil { cd.RoutingSnake = &client.RoutingSnake{} } status.UserDaemon = cd.UserDaemonStatus status.RootDaemon = &cmd.RootDaemonStatus{ Running: cd.Running, Name: cd.Name, Version: cd.Version, DNS: cd.DNS, RoutingSnake: cd.RoutingSnake, PortMappings: cd.PortMappings, } } else if status.RootDaemon == nil { status.RootDaemon = &cmd.RootDaemonStatus{RoutingSnake: &client.RoutingSnake{}} } return &status, nil } func TelepresenceStatusOk(ctx context.Context, args ...string) *StatusResponse { status, err := TelepresenceStatus(ctx, args...) require.NoError(getT(ctx), err) return status } ================================================ FILE: integration_test/itest/suite.go ================================================ package itest import ( "context" "github.com/stretchr/testify/suite" ) type TestingSuite interface { suite.TestingSuite Harness AmendSuiteContext(context.Context) context.Context Context() context.Context Assert() *Assertions Require() *Requirements SuiteName() string setContext(ctx context.Context) } type Suite struct { suite.Suite Harness ctx context.Context } func (s *Suite) AmendSuiteContext(ctx context.Context) context.Context { return ctx } //nolint:unused // Linter is confused about this one. func (s *Suite) setContext(ctx context.Context) { s.ctx = ctx } func (s *Suite) Context() context.Context { return WithT(s.ctx, s.T()) } func (s *Suite) Assert() *Assertions { return &Assertions{Assertions: s.Suite.Assert()} } func (s *Suite) Require() *Requirements { return &Requirements{Assertions: s.Suite.Require()} } ================================================ FILE: integration_test/itest/tempdir.go ================================================ package itest import ( "context" "fmt" "os" "sync/atomic" "github.com/stretchr/testify/require" ) type tempDirBase struct { tempDir string tempDirSeq uint64 } type tempDirBaseKey struct{} func withTempDirBase(ctx context.Context, td *tempDirBase) context.Context { return context.WithValue(ctx, tempDirBaseKey{}, td) } // TempDir returns a temporary directory for the test to use. // The directory is automatically removed when the test and // all its subtests complete. // Each subsequent call to t.TempDir returns a unique directory; // if the directory creation fails, TempDir terminates the test by calling Fatal. func TempDir(ctx context.Context) string { t := getT(ctx) if td, ok := ctx.Value(tempDirBaseKey{}).(*tempDirBase); ok { seq := atomic.AddUint64(&td.tempDirSeq, 1) dir := fmt.Sprintf("%s%c%03d", td.tempDir, os.PathSeparator, seq) require.NoError(t, os.Mkdir(dir, 0o777)) return dir } return t.TempDir() } ================================================ FILE: integration_test/itest/template.go ================================================ package itest import ( "bytes" "context" "io" "path/filepath" "strings" "text/template" "github.com/Masterminds/sprig/v3" core "k8s.io/api/core/v1" "sigs.k8s.io/yaml" "github.com/telepresenceio/telepresence/v2/pkg/dos" ) type TplResource interface { Apply(context context.Context, namespace string) error Delete(ctx context.Context) error } type ContainerPort struct { Number int Name string Protocol core.Protocol } type ServicePort struct { Number int Name string Protocol core.Protocol TargetPort string } type tplBase struct { yml []byte ns string } func (b *tplBase) loadAndApply(ctx context.Context, path, ns string, data any) error { yml, err := ReadTemplate(ctx, filepath.Join("testdata", "k8s", path+".goyaml"), data) if err == nil { b.yml = yml b.ns = ns err = Kubectl(dos.WithStdin(ctx, bytes.NewReader(b.yml)), b.ns, "apply", "-f", "-") } return err } func (b *tplBase) Delete(ctx context.Context) error { return Kubectl(dos.WithStdin(ctx, bytes.NewReader(b.yml)), b.ns, "delete", "-f", "-") } type Generic struct { tplBase Name string Annotations map[string]string Labels map[string]string Environment []core.EnvVar TargetPort string ServicePorts []ServicePort ContainerPort int ContainerPorts []ContainerPort Image string Registry string ServiceAccount string Volumes []core.Volume VolumeMounts []core.VolumeMount } func (g *Generic) Apply(ctx context.Context, ns string) error { return g.loadAndApply(ctx, "generic", ns, g) } type PersistentVolume struct { tplBase Name string Annotations map[string]string StorageClassName string } func (p *PersistentVolume) Apply(ctx context.Context, ns string) error { return p.loadAndApply(ctx, "pv", ns, p) } type PersistentVolumeClaim struct { tplBase Name string Annotations map[string]string StorageClassName string } func (r *PersistentVolumeClaim) Apply(ctx context.Context, ns string) error { return r.loadAndApply(ctx, "pvc", ns, r) } type DisruptionBudget struct { Name string MinAvailable int MaxUnavailable int } func OpenTemplate(ctx context.Context, path string, data any) (io.Reader, error) { b, err := ReadTemplate(ctx, path, data) if err != nil { return nil, err } return bytes.NewReader(b), nil } func ReadTemplate(ctx context.Context, path string, data any) ([]byte, error) { fnMap := sprig.FuncMap() fnMap["toYaml"] = toYAML tpl, err := template.New("").Funcs(fnMap).ParseFiles(path) if err != nil { return nil, err } wr := bytes.Buffer{} if err = tpl.ExecuteTemplate(&wr, filepath.Base(path), data); err != nil { return nil, err } return wr.Bytes(), nil } func EvalTemplate(content string, data any) ([]byte, error) { fnMap := sprig.FuncMap() fnMap["toYaml"] = toYAML tpl, err := template.New("embedded").Funcs(fnMap).Parse(content) if err != nil { return nil, err } wr := bytes.Buffer{} if err = tpl.ExecuteTemplate(&wr, "embedded", data); err != nil { return nil, err } return wr.Bytes(), nil } // toYAML is direct copy of toYaml in the helm.sh/helm/v3/pkg/engine package. func toYAML(v interface{}) string { data, err := yaml.Marshal(v) if err != nil { // Swallow errors inside of a template. return "" } return strings.TrimSuffix(string(data), "\n") } ================================================ FILE: integration_test/itest/timed.go ================================================ package itest import ( "context" "time" ) // TimedRun gives the given function maxDuration time to finish, and returns a timeout error if it doesn't. Otherwise, // it returns the error from the function. func TimedRun(ctx context.Context, maxDuration time.Duration, function func(ctx2 context.Context) error) error { ctx, cancel := context.WithTimeout(ctx, maxDuration) defer cancel() select { case <-ctx.Done(): return ctx.Err() default: return function(ctx) } } ================================================ FILE: integration_test/itest/traffic_manager.go ================================================ package itest import ( "context" "fmt" "net" "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "github.com/telepresenceio/clog" rpc "github.com/telepresenceio/telepresence/rpc/v2/connector" "github.com/telepresenceio/telepresence/rpc/v2/manager" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/client/cli" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/connect" "github.com/telepresenceio/telepresence/v2/pkg/client/docker" "github.com/telepresenceio/telepresence/v2/pkg/client/logging" "github.com/telepresenceio/telepresence/v2/pkg/client/portforward" "github.com/telepresenceio/telepresence/v2/pkg/client/userd/daemon" "github.com/telepresenceio/telepresence/v2/pkg/dos" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" tpGrpc "github.com/telepresenceio/telepresence/v2/pkg/grpc" grpcClient "github.com/telepresenceio/telepresence/v2/pkg/grpc/client" "github.com/telepresenceio/telepresence/v2/pkg/grpc/server" "github.com/telepresenceio/telepresence/v2/pkg/ioutil" "github.com/telepresenceio/telepresence/v2/pkg/k8sapi" "github.com/telepresenceio/telepresence/v2/pkg/log" ) type TrafficManager interface { NamespacePair DoWithTrafficManager(context.Context, func(context.Context, context.CancelFunc, manager.ManagerClient, *manager.SessionInfo)) error DoWithSession(context.Context, *rpc.ConnectRequest, func(context.Context, rpc.ConnectorServer)) error NewConnectRequest(context.Context) *rpc.ConnectRequest } type trafficManager struct { NamespacePair } func WithTrafficManager(np NamespacePair, f func(ctx context.Context, ch TrafficManager)) { np.HarnessT().Run("Test_TrafficManager", func(t *testing.T) { ctx := WithT(np.HarnessContext(), t) require.NoError(t, np.GeneralError()) th := &trafficManager{NamespacePair: np} th.PushHarness(ctx, th.setup, th.tearDown) defer th.PopHarness() f(ctx, th) }) } func (th *trafficManager) setup(ctx context.Context) bool { t := getT(ctx) TelepresenceQuitOk(ctx) _, err := th.TelepresenceHelmInstall(ctx, false) return assert.NoError(t, err) } func (th *trafficManager) tearDown(ctx context.Context) { th.UninstallTrafficManager(ctx, th.ManagerNamespace()) } func (th *trafficManager) trafficManagerConnection(ctx context.Context) (*grpc.ClientConn, error) { cfg, err := clientcmd.BuildConfigFromFlags("", KubeConfig(ctx)) if err != nil { return nil, err } return dialTrafficManager(ctx, cfg, th.ManagerNamespace()) } func dialTrafficManager(ctx context.Context, cfg *rest.Config, managerNamespace string) (*grpc.ClientConn, error) { k8sApi, err := kubernetes.NewForConfig(cfg) if err != nil { return nil, err } ctx = k8sapi.WithK8sInterface(ctx, k8sApi) ctx = portforward.WithRestConfig(ctx, cfg) pap, err := portforward.ResolveSvcToPod(ctx, "traffic-manager", managerNamespace, "8081") if err != nil { clog.Errorf(ctx, "cannot resolve svc/traffic-manager.%s:8081: %v", managerNamespace, err) return nil, err } return grpcClient.DialGRPC(ctx, fmt.Sprintf(portforward.K8sPFScheme+":///svc/traffic-manager.%s:8081", managerNamespace), grpc.WithResolvers(portforward.NewResolver(ctx, pap)), grpc.WithContextDialer(portforward.Dialer(ctx)), grpc.WithTransportCredentials(insecure.NewCredentials()), ) } // DoWithTrafficManager is intended to be used when testing the traffic-manager grpc. It simulates a connector client // that has a session established with the traffic-manager. func (th *trafficManager) DoWithTrafficManager(ctx context.Context, f func(context.Context, context.CancelFunc, manager.ManagerClient, *manager.SessionInfo)) error { conn, err := th.trafficManagerConnection(ctx) if err != nil { return err } defer conn.Close() mgr := manager.NewManagerClient(conn) // Retrieve the session info from the traffic-manager. This is how // a connection to a namespace is made. The traffic-manager now // associates the returned session with that namespace in subsequent // calls. clientSession, err := mgr.ArriveAsClient(ctx, &manager.ClientInfo{ Name: "telepresence@datawire.io", Namespace: th.AppNamespace(), InstallId: "xxx", Product: "telepresence", Version: th.ClientVersion().String(), }) if err != nil { return err } // Normal ticker routine to keep the client alive. ctx, cancel := context.WithCancel(ctx) defer cancel() go func() { ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: _, _ = mgr.Remain(ctx, &manager.RemainRequest{Session: clientSession}) case <-ctx.Done(): _, _ = mgr.Depart(ctx, clientSession) return } } }() f(ctx, cancel, mgr, clientSession) return nil } // NewConnectRequest returns a connector.ConnectRequest that has been initialized with default values. It's intended // to be used in together with DoWithSession to provide an ability to create and modify the request used when // connecting. func (th *trafficManager) NewConnectRequest(ctx context.Context) *rpc.ConnectRequest { flags := map[string]string{ "kubeconfig": KubeConfig(ctx), "namespace": th.AppNamespace(), } if user := GetUser(ctx); user != "default" { flags["as"] = "system:serviceaccount:" + user } return &rpc.ConnectRequest{ KubeFlags: flags, MappedNamespaces: []string{th.AppNamespace()}, ManagerNamespace: th.ManagerNamespace(), Environment: th.GlobalEnv(ctx), } } // DoWithSession is intended to be used when testing the connector GRPC directly without using the CLI. A "connect" is // made before calling the provided function, which means that the `rpc.ConnectorServer` can be used as a connected daemon. A // call to quit is guaranteed after the function ends. func (th *trafficManager) DoWithSession(ctx context.Context, cr *rpc.ConnectRequest, f func(context.Context, rpc.ConnectorServer)) error { client.ProcessName = func() string { return client.UserDaemonName } ctx = docker.EnableClient(ctx) ctx = cli.InitContext(ctx) cfg := client.GetConfig(ctx) logFile := filepath.Join(filelocation.AppUserLogDir(ctx), "connector.log") ctx, err := logging.InitContext(ctx, logFile, cfg.LogLevels().UserDaemon, logging.RotateNever, false) if err != nil { return err } ctx = dos.WithExe(ctx, th) err = connect.EnsureRootDaemonRunning(ctx) if err != nil { return err } ctx, cancel := context.WithCancel(ctx) defer cancel() g := log.NewGroup(ctx) grpcServer := grpc.NewServer() srv := daemon.NewService(ctx, cancel, client.GetConfig(ctx), grpcServer) if cfg.Intercept().UseFtp && !srv.LinkedFTP() { g.Go("fuseftp-server", func(ctx context.Context) error { if err := srv.InitFTPServer(ctx); err != nil { clog.Error(ctx, err) } <-ctx.Done() return nil }) } lc := net.ListenConfig{} fp, err := ioutil.FreePortsTCP(1) if err != nil { return err } addr := fp[0] grpcListener, err := lc.Listen(ctx, "tcp", addr.String()) if err != nil { return err } defer grpcListener.Close() srv.SetListenerAddress(addr) g.Go("server-grpc", func(ctx context.Context) error { return server.Serve(ctx, grpcServer, grpcListener) }) sv := srv.ConnectorServer() _, err = sv.Connect(ctx, cr) if err != nil { return tpGrpc.FromGRPC(err) } func() { defer func() { _, _ = sv.Quit(ctx, nil) }() f(ctx, sv) }() return g.Wait() } ================================================ FILE: integration_test/kubeauth_test.go ================================================ package integration_test import ( "bufio" "context" "errors" "os" "path/filepath" "runtime" "strings" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd/api" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" ) func (s *notConnectedSuite) Test_ConnectWithKubeconfigExec() { extContext, cfg := s.makeKubeConfigWithExec() connectWithExec := func(ctx context.Context, useDocker bool) { if useDocker && s.IsCI() { if !(runtime.GOOS == "linux" && runtime.GOARCH == "amd64") { s.T().Skip("CI can't run linux docker containers inside non-linux runners") } } // Retrieve the current size of the connector.log so that we can scan the messages that appear after connect rq := s.Require() logSize := int64(0) logName := "connector.log" if useDocker { // Authenticator runs as a separate process on the host logName = "kubeauth.log" } logFQName := filepath.Join(filelocation.AppUserLogDir(ctx), logName) st, err := os.Stat(logFQName) if err == nil { logSize = st.Size() } else if !errors.Is(err, os.ErrNotExist) { rq.FailNow(err.Error()) } ctx = itest.WithKubeConfig(ctx, cfg) var args []string if useDocker { args = []string{"--docker"} } _, err = s.TelepresenceTryConnect(ctx, args...) rq.NoError(err) defer itest.TelepresenceQuitOk(ctx) // Scan the log from its previous end. It should now contain a message indicating that the gRPC service that // it contains have served an exec request from a modified kubeconfig requesting credentials from extContext. logF, err := os.Open(logFQName) rq.NoError(err) defer logF.Close() if logSize > 0 { _, err = logF.Seek(logSize, 0) rq.NoError(err) } scn := bufio.NewScanner(logF) found := false for !found && scn.Scan() { found = strings.Contains(scn.Text(), "GetContextExecCredentials("+extContext+")") } rq.Truef(found, "unable to find expected GetContextExecCredentials in the %s", logName) } s.Run("root-daemon", func() { connectWithExec(s.Context(), false) }) s.Run("containerized-daemon", func() { ctx := itest.WithConfig(s.Context(), func(config client.Config) { config.Intercept().UseFtp = false }) connectWithExec(ctx, true) }) } func (s *notConnectedSuite) makeKubeConfigWithExec() (string, *api.Config) { ctx := s.Context() rq := s.Require() kc := itest.KubeConfig(ctx) cfg, err := clientcmd.LoadFromFile(kc) rq.NoError(err) // Create an additional context that has a user with an exec extension. The extension calls the k8screds program // to retrieve the credentials for the original context. rq.NotEmpty(cfg.CurrentContext) cc := cfg.Contexts[cfg.CurrentContext] rq.NotNil(cc) rq.NotEmpty(cc.AuthInfo) ai := cfg.AuthInfos[cc.AuthInfo] rq.NotNil(ai) if ai.Exec != nil { s.T().Skipf("this test requires a kubecontext that doesn't have an exec extension") } // Ensure that the k8screds program is built and ready. binDir := itest.TempDir(ctx) k8sCredsBinary := filepath.Join(binDir, "k8screds") if runtime.GOOS == "windows" { k8sCredsBinary += ".exe" } rq.NoError(itest.Run(ctx, "go", "build", "-o", k8sCredsBinary, filepath.Join("testdata", "k8screds", "main.go"))) // Create a new AuthInfo. extAuthInfo := cc.AuthInfo + "-exec" rq.Nil(cfg.AuthInfos[extAuthInfo]) envMap := itest.EnvironMap(ctx) envVars := make([]api.ExecEnvVar, len(envMap)) i := 0 for k, v := range envMap { envVars[i].Name = k envVars[i].Value = v i++ } cfg.AuthInfos[extAuthInfo] = &api.AuthInfo{ Exec: &api.ExecConfig{ Command: k8sCredsBinary, Args: []string{cfg.CurrentContext}, Env: envVars, APIVersion: "client.authentication.k8s.io/v1beta1", InteractiveMode: "Never", StdinUnavailable: true, }, } extCc := cc.DeepCopy() extCc.AuthInfo = extAuthInfo // Create a new Context that uses the new AuthInfo. We use a nasty name here to ensure // that it is correctly converted to a usable name. extContext := "abc:def/xyz$32-#1?efd" rq.Nil(cfg.Contexts[extContext]) cfg.Contexts[extContext] = extCc cfg.CurrentContext = extContext return extContext, cfg } ================================================ FILE: integration_test/kubeconfig_extension_test.go ================================================ package integration_test import ( "bufio" "context" "fmt" "io" "net" "net/netip" "net/url" "os" "path/filepath" "regexp" "runtime" "strconv" "strings" "time" "k8s.io/client-go/tools/clientcmd/api" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" "github.com/telepresenceio/telepresence/v2/pkg/json" "github.com/telepresenceio/telepresence/v2/pkg/routing" "github.com/telepresenceio/telepresence/v2/pkg/slice" ) func getClusterIPs(cluster *api.Cluster) ([]netip.Addr, error) { var as []netip.Addr svcUrl, err := url.Parse(cluster.Server) if err != nil { return nil, err } hostname := svcUrl.Hostname() if rawIP, err := netip.ParseAddr(hostname); err == nil { as = []netip.Addr{rawIP} } else { ips, err := net.LookupIP(hostname) if err != nil { return nil, err } as = make([]netip.Addr, len(ips)) for i, ip := range ips { as[i], _ = netip.AddrFromSlice(ip) } } return as, nil } func (s *notConnectedSuite) Test_APIServerIsProxied() { ctx := s.Context() require := s.Require() defaultGW, err := routing.DefaultRoute(ctx) require.NoError(err) var ips []netip.Addr ctx = itest.WithKubeConfigExtension(ctx, func(cluster *api.Cluster) map[string]any { var apiServers []string var err error ips, err = getClusterIPs(cluster) require.NoError(err) for _, ip := range ips { if ip.IsLoopback() { s.T().Skipf("test can't run on host with a loopback cluster IP %s", ip) } if ip.Is6() { apiServers = append(apiServers, fmt.Sprintf(`%s/96`, ip)) } else { apiServers = append(apiServers, fmt.Sprintf(`%s/24`, ip)) } if defaultGW.Routes(ip) { s.T().Skipf("test can't run on host with route %s and cluster IP %s", defaultGW.String(), ip) } } return map[string]any{"also-proxy": apiServers} }) s.TelepresenceConnect(ctx, "--context", "extra") expectedLen := len(ips) expect := regexp.MustCompile(`Also Proxy\s*:\s*\((\d+) subnets\)`) s.Eventually(func() bool { stdout, stderr, err := itest.Telepresence(ctx, "status") if err == nil { if m := expect.FindStringSubmatch(stdout); m != nil && m[1] == strconv.Itoa(expectedLen) { return true } clog.Infof(ctx, "%q does not match %q to %d subnets", stdout, expect, expectedLen) } else { clog.Errorf(ctx, "%s: %v", stderr, err) } return false }, 30*time.Second, 3*time.Second, fmt.Sprintf("did not find %d also-proxied subnets", expectedLen)) status := itest.TelepresenceStatusOk(ctx) require.Len(status.RootDaemon.AlsoProxy, expectedLen) for _, ip := range ips { rng := ip.As4() rng[len(rng)-1] = 0 expectedValue := netip.PrefixFrom(netip.AddrFrom4(rng), 24) require.Contains(status.RootDaemon.AlsoProxy, expectedValue) } } func (s *notConnectedSuite) Test_NeverProxy() { require := s.Require() ctx := s.Context() svcName := "echo-never-proxy" itest.ApplyEchoService(ctx, svcName, s.AppNamespace(), 8080) defer itest.DeleteSvcAndWorkload(ctx, "deploy", svcName, s.AppNamespace()) ip, err := itest.Output(ctx, "kubectl", "--namespace", s.AppNamespace(), "get", "svc", svcName, "-o", "jsonpath={.spec.clusterIP}") require.NoError(err) mask := 32 if s.IsIPv6() { mask = 128 } neverProxiedCount := 1 ctx = itest.WithKubeConfigExtension(ctx, func(cluster *api.Cluster) map[string]any { ips, err := getClusterIPs(cluster) require.NoError(err) for _, cip := range ips { if !cip.IsLoopback() { clog.Infof(ctx, "expect never-proxy of %s", cip) neverProxiedCount++ } } return map[string]any{"never-proxy": []string{fmt.Sprintf("%s/%d", ip, mask)}} }) s.TelepresenceConnect(ctx, "--context", "extra") // The cluster's IP address will be never proxied unless it's a loopback, so we gotta account for that. s.Eventually(func() bool { stdout, _, err := itest.Telepresence(ctx, "status") if err != nil { return false } m := regexp.MustCompile(`Never Proxy\s*:\s*\((\d+) subnets\)`).FindStringSubmatch(stdout) npcOk := false if m != nil { npc, _ := strconv.Atoi(m[1]) npcOk = npc > 0 && npc <= neverProxiedCount } if !npcOk { clog.Infof(ctx, "did not find 1-%d never-proxied subnets\nOut: %s", neverProxiedCount, stdout) return false } return true }, 5*time.Second, 1*time.Second, fmt.Sprintf("did not find 1-%d never-proxied subnets", neverProxiedCount)) s.Eventually(func() bool { status, err := itest.TelepresenceStatus(ctx) if err == nil && status.RootDaemon != nil { npc := len(status.RootDaemon.NeverProxy) return npc > 0 && npc <= neverProxiedCount } return false }, 5*time.Second, 1*time.Second, fmt.Sprintf("did not find 1-%d never-proxied subnets in json status", neverProxiedCount)) s.Eventually(func() bool { return itest.Run(ctx, "curl", "--silent", "--max-time", "0.5", ip) != nil }, 15*time.Second, 2*time.Second, fmt.Sprintf("never-proxied IP %s is reachable", ip)) } func (s *notConnectedSuite) Test_ConflictingProxies() { ctx := s.Context() s.TelepresenceConnect(ctx) st := itest.TelepresenceStatusOk(ctx) itest.TelepresenceQuitOk(ctx) rq := s.Require() rq.True(len(st.RootDaemon.Subnets) > 0) svcCIDR := st.RootDaemon.Subnets[0] ones := svcCIDR.Bits() if ones > 16 || !svcCIDR.Addr().Is4() { s.T().Skip("test requires an IPv4 service subnet with a 16 bit mask or smaller") } base := svcCIDR.Masked().Addr() largeCIDR := netip.PrefixFrom(base, 24) smallCIDR := netip.PrefixFrom(base, 28) // testIP is an IP that is covered by smallCIDR baseBytes := base.As4() testIP := netip.PrefixFrom(netip.AddrFrom4([4]byte{baseBytes[0], baseBytes[1], 0, 4}), 32) // We don't really care if we can't route this with TP disconnected provided the result is the same once we connect originalRoute, _ := routing.GetRoute(ctx, testIP) for name, t := range map[string]struct { alsoProxy []string neverProxy []string expectEq bool }{ "Never Proxy wins": { alsoProxy: []string{largeCIDR.String()}, neverProxy: []string{smallCIDR.String()}, expectEq: true, }, "Also Proxy wins": { alsoProxy: []string{smallCIDR.String()}, neverProxy: []string{largeCIDR.String()}, expectEq: false, }, } { s.Run(name, func() { ctx := itest.WithKubeConfigExtension(s.Context(), func(cluster *api.Cluster) map[string]any { return map[string]any{ "never-proxy": t.neverProxy, "also-proxy": t.alsoProxy, } }) s.TelepresenceConnect(ctx, "--context", "extra") defer itest.TelepresenceQuitOk(ctx) s.Eventually(func() bool { newRoute, err := routing.GetRoute(ctx, testIP) if err != nil { clog.Errorf(ctx, "failed to get route for %s: %v", testIP, err) return false } if t.expectEq { return originalRoute.InterfaceName == newRoute.InterfaceName } return newRoute.InterfaceName != originalRoute.InterfaceName }, 30*time.Second, 200*time.Millisecond) }) } } func (s *notConnectedSuite) Test_AlsoNeverProxyDocker() { if s.IsCI() && !(runtime.GOOS == "linux" && runtime.GOARCH == "amd64") { s.T().Skip("CI can't run linux docker containers inside non-linux runners") } alsoProxy := []string{"10.128.0.0/16"} neverProxy := []string{"10.128.0.0/24"} ctx := itest.WithKubeConfigExtension(s.Context(), func(cluster *api.Cluster) map[string]any { return map[string]any{ "never-proxy": neverProxy, "also-proxy": alsoProxy, } }) cidrsToStrings := func(cidrs []netip.Prefix) []string { ss := make([]string, len(cidrs)) for i, cidr := range cidrs { ss[i] = cidr.String() } return ss } ctx = itest.WithConfig(ctx, func(config client.Config) { config.Intercept().UseFtp = false }) s.TelepresenceConnect(ctx, "--context", "extra", "--docker") defer itest.TelepresenceQuitOk(ctx) st := itest.TelepresenceStatusOk(ctx) s.True(slice.ContainsAll(cidrsToStrings(st.ContainerizedDaemon.AlsoProxy), alsoProxy)) s.True(slice.ContainsAll(cidrsToStrings(st.ContainerizedDaemon.NeverProxy), neverProxy)) } func (s *notConnectedSuite) Test_DNSSuffixRules() { if s.IsCI() && runtime.GOOS == "linux" && runtime.GOARCH == "arm64" { s.T().Skip("The DNS on the linux-arm64 GitHub runner is not configured correctly") } defaults := client.GetDefaultConfig().DNS() const randomName = "zwslkjsdf" const randomDomain = ".xnrqj" const randomDomain2 = ".pvdar" tests := []struct { name string domainName string includeSuffixes []string excludeSuffixes []string configIncludeSuffixes []string wantedLogEntry []string mustHaveWanted bool expectedIncludeSuffixes []string expectedExcludeSuffixes []string unwantedLogEntry []string }{ { "default-exclude-com", randomName + ".com", nil, nil, nil, []string{ `Cluster DNS excluded by exclude-suffix ".com" for name "` + randomName + `.com"`, }, false, defaults.IncludeSuffixes, defaults.ExcludeSuffixes, []string{ `Lookup A "` + randomName + `.com`, }, }, { "default-exclude-random-domain", randomName + randomDomain, nil, nil, nil, []string{ `Cluster DNS excluded for name "` + randomName + randomDomain + `". No inclusion rule was matched`, }, false, defaults.IncludeSuffixes, defaults.ExcludeSuffixes, []string{ `Lookup A "` + randomName + randomDomain + `."`, }, }, { "include-random-domain", randomName + randomDomain, []string{randomDomain}, nil, nil, []string{ `Cluster DNS included by include-suffix "` + randomDomain + `" for name "` + randomName + randomDomain + `"`, `Lookup A "` + randomName + randomDomain + `."`, }, true, []string{randomDomain}, defaults.ExcludeSuffixes, nil, }, { "include-random-domain-config", randomName + randomDomain, nil, nil, []string{randomDomain}, []string{ `Cluster DNS included by include-suffix "` + randomDomain + `" for name "` + randomName + randomDomain + `"`, `Lookup A "` + randomName + randomDomain + `."`, }, true, []string{randomDomain}, defaults.ExcludeSuffixes, nil, }, { "override-random-domain", randomName + randomDomain, []string{randomDomain}, nil, []string{randomDomain2}, []string{ `Cluster DNS included by include-suffix "` + randomDomain + `" for name "` + randomName + randomDomain + `"`, `Lookup A "` + randomName + randomDomain + `."`, }, true, []string{randomDomain}, defaults.ExcludeSuffixes, nil, }, { "equally specific include overrides exclude", randomName + ".org", []string{".org"}, nil, nil, []string{ `Cluster DNS included by include-suffix ".org" (overriding exclude-suffix ".org") for name "` + randomName + `.org"`, `Lookup A "` + randomName + `.org."`, }, true, []string{".org"}, defaults.ExcludeSuffixes, nil, }, { "more specific include overrides exclude", randomName + ".my-domain.org", []string{".my-domain.org"}, defaults.ExcludeSuffixes, nil, []string{ `Cluster DNS included by include-suffix ".my-domain.org" (overriding exclude-suffix ".org") for name "` + randomName + `.my-domain.org"`, `Lookup A "` + randomName + `.my-domain.org."`, }, true, []string{".my-domain.org"}, defaults.ExcludeSuffixes, nil, }, { "more specific exclude overrides include", randomName + ".my-domain.org", []string{".org"}, []string{".com", ".my-domain.org"}, nil, []string{ `Cluster DNS excluded by exclude-suffix ".my-domain.org" for name "` + randomName + `.my-domain.org"`, }, true, []string{".org"}, []string{".com", ".my-domain.org"}, []string{ `Lookup A "` + randomName + `.my-domain.org."`, }, }, } logFile := filepath.Join(filelocation.AppUserLogDir(s.Context()), "daemon.log") for _, tt := range tests { s.Run(tt.name, func() { ctx := s.Context() if len(tt.configIncludeSuffixes) > 0 { ctx = itest.WithConfig(ctx, func(config client.Config) { config.DNS().IncludeSuffixes = tt.configIncludeSuffixes }) } ctx = itest.WithKubeConfigExtension(ctx, func(cluster *api.Cluster) map[string]any { m := map[string]any{ "logLevels": map[string]string{ "rootDaemon": "trace", }, } if len(tt.excludeSuffixes)+len(tt.includeSuffixes) > 0 { dns := map[string]any{} if len(tt.excludeSuffixes) > 0 { dns["excludeSuffixes"] = tt.excludeSuffixes } if len(tt.includeSuffixes) > 0 { dns["includeSuffixes"] = tt.includeSuffixes } m["dns"] = dns } return m }) require := s.Require() s.TelepresenceConnect(ctx, "--context", "extra") defer itest.TelepresenceQuitOk(ctx) // Check that config view -c includes the includeSuffixes var cfg client.SessionConfig stdout := itest.TelepresenceOk(ctx, "config", "view", "--client-only", "--output", "json") require.NoError(json.Unmarshal([]byte(stdout), &cfg, false)) require.Equal(tt.expectedExcludeSuffixes, cfg.DNS().ExcludeSuffixes) require.Equal(tt.expectedIncludeSuffixes, cfg.DNS().IncludeSuffixes) rootLog, err := os.Open(logFile) require.NoError(err) defer rootLog.Close() // Figure out where the current end of the logfile is. This must be done before any // of the tests run because the queries that the DNS resolver receives are dependent // on how the system's DNS resolver handle search paths and caching. st, err := rootLog.Stat() s.Require().NoError(err) pos := st.Size() short, cancel := context.WithTimeout(ctx, 20*time.Millisecond) defer cancel() _, _ = net.DefaultResolver.LookupIPAddr(short, tt.domainName) // Give query time to reach telepresence and produce a log entry time.Sleep(500 * time.Millisecond) for _, wl := range tt.wantedLogEntry { _, err = rootLog.Seek(pos, io.SeekStart) require.NoError(err) scn := bufio.NewScanner(rootLog) found := false // mustHaveWanted caters for cases where the default behavior from the system's resolver // is to not send unwanted queries to our resolver at all (based on search and routes). // It is forced to true for inclusion tests. mustHaveWanted := tt.mustHaveWanted for scn.Scan() { txt := scn.Text() if strings.Contains(txt, wl) { found = true break } if !mustHaveWanted { if strings.Contains(txt, " ServeDNS ") && strings.Contains(txt, tt.domainName) { mustHaveWanted = true } } } s.Truef(found || !mustHaveWanted, "Unable to find %q", wl) } for _, wl := range tt.unwantedLogEntry { _, err = rootLog.Seek(pos, io.SeekStart) require.NoError(err) scn := bufio.NewScanner(rootLog) found := false for scn.Scan() { if strings.Contains(scn.Text(), wl) { found = true break } } s.Falsef(found, "Found unwanted %q", wl) } }) } } ================================================ FILE: integration_test/large_files_test.go ================================================ package integration_test import ( "bufio" "context" "encoding/binary" "errors" "fmt" "math" "os" "path/filepath" "strconv" "sync" "time" "github.com/go-json-experiment/json" core "k8s.io/api/core/v1" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/intercept" "github.com/telepresenceio/telepresence/v2/pkg/ioutil" ) type largeFilesSuite struct { itest.Suite itest.TrafficManager name string manifests [][3]itest.TplResource serviceCount int mountPoint []string largeFiles []string } func (s *largeFilesSuite) SuiteName() string { return "LargeFiles" } const ( svcCount = 4 fileSize = 100 * 1024 * 1024 fileCountPerSvc = 3 ) func init() { itest.AddTrafficManagerSuite("", func(h itest.TrafficManager) itest.TestingSuite { return &largeFilesSuite{ Suite: itest.Suite{Harness: h}, TrafficManager: h, name: "hello", serviceCount: svcCount, mountPoint: make([]string, svcCount), largeFiles: make([]string, svcCount*fileCountPerSvc), } }) } func (s *largeFilesSuite) Name() string { return s.name } func (s *largeFilesSuite) ServiceCount() int { return s.serviceCount } func (s *largeFilesSuite) SetupSuite() { if !(s.ManagerIsVersion(">2.21.x") && s.ClientIsVersion(">2.24.x")) { s.T().Skip("Not part of compatibility tests. Not enough transfer stability") } s.Suite.SetupSuite() ctx := s.Context() s.manifests = make([][3]itest.TplResource, s.ServiceCount()) wg := sync.WaitGroup{} wg.Add(s.ServiceCount()) for i := 0; i < s.ServiceCount(); i++ { go func() { defer wg.Done() pvName := fmt.Sprintf("%s-pv-%d", s.Name(), i) pv := &itest.PersistentVolume{ Name: pvName, } if s.UseLocalPathProvisioner() { pv.Annotations = map[string]string{ "pv.kubernetes.io/provisioned-by": "rancher.io/local-path", } pv.StorageClassName = "local-path" } s.NoError(pv.Apply(ctx, s.AppNamespace())) pvcName := fmt.Sprintf("%s-pvc-%d", s.Name(), i) pvc := &itest.PersistentVolumeClaim{ Name: pvcName, } if s.UseLocalPathProvisioner() { pvc.Annotations = map[string]string{ "pv.kubernetes.io/provisioned-by": "rancher.io/local-path", } pvc.StorageClassName = "local-path" } s.NoError(pvc.Apply(ctx, s.AppNamespace())) svc := fmt.Sprintf("%s-%d", s.Name(), i) dep := &itest.Generic{ Name: svc, Registry: "ghcr.io/telepresenceio", Image: "echo-server:latest", Volumes: []core.Volume{ { Name: "rw-volume", VolumeSource: core.VolumeSource{ PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: pvc.Name}, }, }, }, VolumeMounts: []core.VolumeMount{ { Name: "rw-volume", MountPath: "/home/scratch", }, }, } s.NoError(dep.Apply(ctx, s.AppNamespace())) s.NoError(itest.RolloutStatusWait(ctx, s.AppNamespace(), "deploy/"+svc)) s.manifests[i] = [3]itest.TplResource{dep, pvc, pv} }() } wg.Wait() } func (s *largeFilesSuite) TearDownSuite() { ctx := s.Context() // Delete in reverse order wg := sync.WaitGroup{} wg.Add(s.ServiceCount()) for _, trs := range s.manifests { go func() { defer wg.Done() for _, tr := range trs { s.NoError(tr.Delete(ctx)) } }() } wg.Wait() itest.TelepresenceQuitOk(ctx) } func (s *largeFilesSuite) createIntercepts(ctx context.Context) { s.TelepresenceConnect(ctx) wg := sync.WaitGroup{} wg.Add(s.ServiceCount()) for i := 0; i < s.ServiceCount(); i++ { go func(i int) { defer wg.Done() svc := fmt.Sprintf("%s-%d", s.Name(), i) stdout := itest.TelepresenceOk(ctx, "intercept", "--detailed-output", "--output", "json", "--port", strconv.Itoa(8080+i), svc, ) var info intercept.Info require := s.Require() require.NoError(json.Unmarshal([]byte(stdout), &info)) require.Equal(svc, info.Name, ioutil.WriterToString(info.WriteTo)) require.NotNil(info.Mount) s.mountPoint[i] = info.Mount.LocalDir s.NoError(itest.RolloutStatusWait(ctx, s.AppNamespace(), "deploy/"+svc)) s.CapturePodLogs(ctx, svc, "traffic-agent", s.AppNamespace()) }(i) } wg.Wait() time.Sleep(7 * time.Second) } func (s *largeFilesSuite) leaveIntercepts(ctx context.Context) { for i := 0; i < s.ServiceCount(); i++ { itest.TelepresenceOk(ctx, "leave", fmt.Sprintf("%s-%d", s.Name(), i)) } } func (s *largeFilesSuite) Test_LargeFileIntercepts_fuseftp() { ctx := itest.WithConfig(s.Context(), func(cfg client.Config) { cfg.Timeouts().PrivateFtpReadWrite = 2 * time.Minute cfg.Timeouts().PrivateFtpShutdown = 3 * time.Minute cfg.Intercept().UseFtp = true }) s.largeFileIntercepts(ctx) } func (s *largeFilesSuite) Test_LargeFileIntercepts_sshfs() { ctx := itest.WithConfig(s.Context(), func(cfg client.Config) { cfg.Intercept().UseFtp = false }) s.largeFileIntercepts(ctx) } func (s *largeFilesSuite) largeFileIntercepts(ctx context.Context) { s.createIntercepts(ctx) wg := sync.WaitGroup{} // Start by creating files in the mounted filesystem from entry 1 - fileCountPerSvc for each service. // We leave the first entry empty because in the next step, we want to create a file parallel to // validating the ones we create here so that there is heavy parallel reads and writes. wg.Add(s.ServiceCount() * (fileCountPerSvc - 1)) for i := 0; i < s.ServiceCount(); i++ { for n := 1; n < fileCountPerSvc; n++ { // Leave the first entry empty for now go func(i, n int) { defer wg.Done() path, err := s.createLargeFile(filepath.Join(s.mountPoint[i], "home", "scratch"), fileSize) s.largeFiles[i*fileCountPerSvc+n] = filepath.Base(path) s.NoError(err) }(i, n) } } wg.Wait() // At this point we leave the intercepts so that all directories are unmounted. The volumes are persistent // so they will be remounted. s.leaveIntercepts(ctx) if s.T().Failed() { s.T().FailNow() } s.createIntercepts(ctx) // Parallel to creating the first entry, also validate the ones that we created in step 1. wg.Add(s.ServiceCount() * fileCountPerSvc) for i := 0; i < s.ServiceCount(); i++ { go func(i int) { defer wg.Done() path, err := s.createLargeFile(filepath.Join(s.mountPoint[i], "home", "scratch"), fileSize) s.largeFiles[i*fileCountPerSvc] = filepath.Base(path) s.NoError(err) }(i) for n := 1; n < fileCountPerSvc; n++ { // Leave the first entry empty for now go func(i, n int) { defer wg.Done() s.NoError(itest.TimedRun(ctx, 10*time.Second, func(_ context.Context) error { return validateLargeFile(filepath.Join(s.mountPoint[i], "home", "scratch", s.largeFiles[i*fileCountPerSvc+n]), fileSize) })) }(i, n) } } wg.Wait() s.leaveIntercepts(ctx) if s.T().Failed() { s.T().FailNow() } s.createIntercepts(ctx) defer s.leaveIntercepts(ctx) // Validate the first entry wg.Add(s.ServiceCount()) for i := 0; i < s.ServiceCount(); i++ { go func(i int) { defer wg.Done() s.NoError(itest.TimedRun(ctx, 10*time.Second, func(_ context.Context) error { return validateLargeFile(filepath.Join(s.mountPoint[i], "home", "scratch", s.largeFiles[i*fileCountPerSvc]), fileSize) })) }(i) } wg.Wait() } func (s *largeFilesSuite) createLargeFile(dir string, sz int) (string, error) { if sz%4 != 0 { return "", errors.New("size%4 must be zero") } qsz := sz / 4 // We'll write a sequence of uint32 values if qsz > math.MaxUint32 { return "", fmt.Errorf("size must be less than %d", math.MaxUint32*4) } f, err := os.CreateTemp(dir, "big-*.bin") if err != nil { return "", fmt.Errorf("%s: os.CreateTemp failed: %w", time.Now().Format("15:04:05.0000"), err) } defer f.Close() bf := bufio.NewWriter(f) qz := uint32(qsz) buf := make([]byte, 4) for i := uint32(0); i < qz; i++ { binary.BigEndian.PutUint32(buf, i) n, err := bf.Write(buf) if err != nil { return "", fmt.Errorf("%s: Write on %s failed: %w", time.Now().Format("15:04:05.0000"), f.Name(), err) } if n != 4 { return "", errors.New("didn't write quartet") } } if err := bf.Flush(); err != nil { return "", fmt.Errorf("%s: Flush on %s failed: %w", time.Now().Format("15:04:05.0000"), f.Name(), err) } return f.Name(), nil } func validateLargeFile(name string, sz int) error { f, err := os.Open(name) if err != nil { return err } defer f.Close() st, err := f.Stat() if err != nil { return err } if st.Size() != int64(sz) { return fmt.Errorf("file size differ. Expected %d, got %d", sz, st.Size()) } bf := bufio.NewReader(f) qz := uint32(sz / 4) buf := make([]byte, 4) for i := uint32(0); i < qz; i++ { n, err := bf.Read(buf) if err != nil { return err } if n != 4 { return errors.New("didn't read quartet") } x := binary.BigEndian.Uint32(buf) if i != x { return fmt.Errorf("content differ at position %d: expected %d, got %d", i*4, i, x) } } return nil } ================================================ FILE: integration_test/limitrange_test.go ================================================ package integration_test import ( "path/filepath" "strings" "time" "github.com/go-json-experiment/json" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" ) func (is *installSuite) limitedRangeTest() { const svc = "echo" ctx := itest.WithUser(is.Context(), is.ManagerNamespace()+":"+itest.TestUser) is.TelepresenceConnect(ctx) itest.TelepresenceOk(ctx, "loglevel", "debug") require := is.Require() itest.ApplyEchoService(ctx, svc, is.AppNamespace(), 8083) defer func() { is.NoError(itest.Kubectl(ctx, is.AppNamespace(), "delete", "svc,deploy", svc)) is.Eventually(func() bool { return len(itest.RunningPodNames(ctx, svc, is.AppNamespace())) == 0 }, 2*time.Minute, 6*time.Second) }() _, _, err := itest.Telepresence(ctx, "intercept", "--mount", "false", svc) if err != nil { clog.Error(ctx, err) if out, err := itest.KubectlOut(ctx, is.AppNamespace(), "get", "pod", "-o", "yaml", "-l", "app="+svc); err == nil { clog.Info(ctx, out) } } require.NoError(err) is.Eventually( func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--intercepts") return err == nil && strings.Contains(stdout, svc+": intercepted") }, 10*time.Second, 2*time.Second, ) itest.TelepresenceOk(ctx, "leave", svc) // Ensure that LimitRange is injected into traffic-agent out, err := itest.KubectlOut(ctx, is.AppNamespace(), "get", "pods", "-l", "app="+svc, "-o", `jsonpath={range .items.*.spec.containers[?(@.name=='traffic-agent')]}{.resources}{","}{end}`) require.NoError(err) clog.Infof(ctx, "resources = %s", out) var rrs []v1.ResourceRequirements require.NoError(json.Unmarshal([]byte("["+strings.TrimSuffix(out, ",")+"]"), &rrs)) oneGig, err := resource.ParseQuantity("100Mi") require.NoError(err) require.NotEmpty(rrs) rr := rrs[0] m := rr.Limits.Memory() require.True(m != nil && m.Equal(oneGig)) m = rr.Requests.Memory() require.True(m != nil && m.Equal(oneGig)) } func (is *installSuite) TestLimitRange() { ctx := is.Context() require := is.Require() require.NoError(itest.Kubectl(ctx, is.ManagerNamespace(), "apply", "-f", filepath.Join("testdata", "k8s", "client_sa.yaml"))) defer func() { require.NoError(itest.Kubectl(ctx, is.ManagerNamespace(), "delete", "-f", filepath.Join("testdata", "k8s", "client_sa.yaml"))) }() defer is.UninstallTrafficManager(ctx, is.ManagerNamespace()) require.NoError(itest.Kubectl(ctx, is.AppNamespace(), "apply", "-f", filepath.Join("testdata", "k8s", "memory-constraints.yaml"))) defer func() { require.NoError(itest.Kubectl(ctx, is.AppNamespace(), "delete", "-f", filepath.Join("testdata", "k8s", "memory-constraints.yaml"))) }() is.Run("Never", func() { opts := []string{"--set", "agentInjector.webhook.reinvocationPolicy=Never"} if is.ManagerIsVersion(">2.25.x") { opts = append(opts, "--set", "agentInjector.mutationAware=false") } is.TelepresenceHelmInstallOK(is.Context(), false, opts...) is.limitedRangeTest() }) is.Run("IfNeeded", func() { opts := []string{"--set", "agentInjector.webhook.reinvocationPolicy=IfNeeded"} if is.ManagerIsVersion(">2.25.x") { opts = append(opts, "--set", "agentInjector.mutationAware=true") } is.TelepresenceHelmInstallOK(is.Context(), true, opts...) is.limitedRangeTest() }) } ================================================ FILE: integration_test/list_watch_test.go ================================================ package integration_test import ( "context" "time" "github.com/telepresenceio/telepresence/v2/integration_test/itest" ) func (s *connectedSuite) Test_ListWatch() { svc := "echo-easy" s.Run("-C", func() { // Use a context to end tele list -w ctx := s.Context() cancelctx, cancel := context.WithCancel(ctx) ch := make(chan string) go func() { stdout, _, _ := itest.Telepresence(cancelctx, "list", "--output", "json-stream") ch <- stdout }() time.Sleep(time.Second) s.ApplyApp(ctx, svc, "deploy/"+svc) defer s.DeleteSvcAndWorkload(ctx, "deploy", svc) time.Sleep(time.Second) cancel() s.Contains(<-ch, svc) }) } ================================================ FILE: integration_test/loglevel_test.go ================================================ package integration_test import ( "bufio" "os" "path/filepath" "regexp" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" ) func (s *notConnectedSuite) Test_RootDaemonLogLevel() { require := s.Require() ctx := s.Context() s.TelepresenceConnect(ctx) itest.TelepresenceQuitOk(ctx) rootLogName := filepath.Join(filelocation.AppUserLogDir(ctx), "daemon.log") rootLog, err := os.Open(rootLogName) require.NoError(err) defer rootLog.Close() hasDebug := false scn := bufio.NewScanner(rootLog) match := regexp.MustCompile(` DEBUG +rootd/server`) for scn.Scan() && !hasDebug { hasDebug = match.MatchString(scn.Text()) } s.True(hasDebug, "daemon.log does not contain expected debug statements") } ================================================ FILE: integration_test/manager_grpc_test.go ================================================ package integration_test import ( "fmt" "google.golang.org/grpc" empty "google.golang.org/protobuf/types/known/emptypb" "github.com/telepresenceio/telepresence/rpc/v2/manager" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/userd/trafficmgr" ) type managerGRPCSuite struct { itest.Suite itest.TrafficManager conn *grpc.ClientConn si *manager.SessionInfo } func (m *managerGRPCSuite) SuiteName() string { return "ManagerGRPC" } func init() { itest.AddConnectedSuite("", func(h itest.TrafficManager) itest.TestingSuite { return &managerGRPCSuite{Suite: itest.Suite{Harness: h}, TrafficManager: h} }) } func (m *managerGRPCSuite) SetupSuite() { m.Suite.SetupSuite() k8sCluster, err := m.GetK8SCluster(m.Context(), "", m.ManagerNamespace()) m.Require().NoError(err) m.conn, _, _, err = k8sCluster.ConnectToManager(m.Context(), m.ManagerNamespace()) m.Require().NoError(err) _, err = manager.NewManagerClient(m.conn).Version(m.Context(), &empty.Empty{}) m.Require().NoError(err) daemonID := daemon.NewIdentifier("", k8sCluster.KubeContext, m.AppNamespace(), false) m.si, err = trafficmgr.LoadSessionInfoFromUserCache(m.Context(), daemonID) m.Require().NoError(err) } func (m *managerGRPCSuite) TearDownSuite() { if m.conn != nil { go m.conn.Close() m.conn = nil } } func (m *managerGRPCSuite) Test_ClusterInfo() { istream, err := manager.NewManagerClient(m.conn).WatchClusterInfo(m.Context(), m.si) m.Require().NoError(err) info, err := istream.Recv() m.Require().NoError(err) // We can't really legislate for the IPs, but we can make sure they're there. The rest should be the default config values. m.Require().NotNil(info.ManagerPodIp) m.Require().Equal(int32(8081), info.ManagerPodPort) m.Require().NotNil(info.InjectorSvcIp) injectorSvcPort := int32(443) if m.ManagerIsVersion(">=2.24.0") { injectorSvcPort = 8443 } m.Require().Equal(injectorSvcPort, info.InjectorSvcPort) m.Require().Equal(fmt.Sprintf("agent-injector.%s", m.ManagerNamespace()), info.InjectorSvcHost) } ================================================ FILE: integration_test/manual_agent_test.go ================================================ package integration_test import ( "fmt" "os" "path/filepath" "regexp" "strconv" "strings" "time" "github.com/go-json-experiment/json" "sigs.k8s.io/yaml" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/agentconfig" "github.com/telepresenceio/telepresence/v2/pkg/version" ) func (s *notConnectedSuite) Test_ManualAgent() { testManualAgent(&s.Suite, s) } func testManualAgent(s *itest.Suite, nsp itest.NamespacePair) { if !(s.ManagerVersion().EQ(version.Structured) && s.ClientVersion().EQ(version.Structured)) { s.T().Skip("Not part of compatibility tests. Manual setup often change between versions") } require := s.Require() ctx := s.Context() k8sDir := filepath.Join("testdata", "k8s") require.NoError(nsp.Kubectl(ctx, "apply", "-f", filepath.Join(k8sDir, "echo-manual-inject-svc.yaml"))) agentImage := fmt.Sprintf("%s/%s:%s", s.AgentRegistry(), s.AgentImage(), s.AgentVersion()) inputFile := filepath.Join(k8sDir, "echo-manual-inject-deploy.yaml") cfgEntry := itest.TelepresenceOk(ctx, "genyaml", "config", "--agent-image", agentImage, "--output", "-", "--manager-namespace", nsp.ManagerNamespace(), "--namespace", nsp.AppNamespace(), "--input", inputFile, "--loglevel", "debug") sc, err := agentconfig.UnmarshalYAML([]byte(cfgEntry)) require.NoError(err) tmpDir := s.T().TempDir() writeYaml := func(name string, data any) string { yf := filepath.Join(tmpDir, name) b, err := yaml.Marshal(data) require.NoError(err) require.NoError(os.WriteFile(yf, b, 0o666)) return yf } configFile := filepath.Join(tmpDir, sc.WorkloadName) require.NoError(os.WriteFile(configFile, []byte(cfgEntry), 0o666)) stdout := itest.TelepresenceOk(ctx, "genyaml", "container", "--output", "-", "--agent", configFile, "--input", filepath.Join(k8sDir, "echo-manual-inject-deploy.yaml")) var container map[string]any require.NoError(yaml.Unmarshal([]byte(stdout), &container)) stdout = itest.TelepresenceOk(ctx, "genyaml", "initcontainer", "--output", "-", "--agent", configFile) var initContainer map[string]any require.NoError(yaml.Unmarshal([]byte(stdout), &initContainer)) stdout = itest.TelepresenceOk(ctx, "genyaml", "volume", "--agent", configFile, "--input", inputFile) var volumes []map[string]any require.NoError(yaml.Unmarshal([]byte(stdout), &volumes)) stdout = itest.TelepresenceOk(ctx, "genyaml", "annotations", "--agent", configFile) var anns map[string]string require.NoError(yaml.Unmarshal([]byte(stdout), &anns)) b, err := os.ReadFile(filepath.Join(k8sDir, "echo-manual-inject-deploy.yaml")) require.NoError(err) var deploy map[string]any b, err = yaml.YAMLToJSON(b) require.NoError(err, string(b)) err = json.Unmarshal(b, &deploy) require.NoError(err) renameHttpPort := func(con map[string]any) { if ports, ok := con["ports"].([]map[string]any); ok { for _, port := range ports { if port["name"] == "http" { port["name"] = "tm_http" } } } } podTemplate := deploy["spec"].(map[string]any)["template"].(map[string]any) podSpec := podTemplate["spec"].(map[string]any) cons := podSpec["containers"].([]any) for _, con := range cons { renameHttpPort(con.(map[string]any)) } podSpec["containers"] = append(cons, container) podSpec["initContainers"] = []map[string]any{initContainer} podSpec["volumes"] = volumes podTemplate["metadata"].(map[string]any)["annotations"] = anns dplYaml := writeYaml("deployment.yaml", deploy) require.NoError(nsp.Kubectl(ctx, "apply", "-f", dplYaml)) defer func() { require.NoError(nsp.Kubectl(ctx, "delete", "-f", dplYaml)) }() err = nsp.RolloutStatusWait(ctx, "deploy/"+sc.WorkloadName) nsp.CapturePodLogs(ctx, sc.WorkloadName, "traffic-agent", nsp.AppNamespace()) require.NoError(err) nsp.TelepresenceConnect(ctx) defer itest.TelepresenceQuitOk(ctx) stdout = itest.TelepresenceOk(ctx, "list") require.Regexp(regexp.MustCompile(`.*`+sc.WorkloadName+`\s*:\s*ready to (engage|intercept) \(traffic-agent already installed\).*`), stdout) svcPort, svcCancel := itest.StartLocalHttpEchoServer(ctx, sc.WorkloadName) defer svcCancel() itest.TelepresenceOk(ctx, "intercept", sc.WorkloadName, "--port", strconv.Itoa(svcPort)) defer itest.TelepresenceOk(ctx, "leave", sc.WorkloadName) s.Eventually(func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--intercepts") return err == nil && strings.Contains(stdout, sc.WorkloadName+": intercepted") }, 30*time.Second, 3*time.Second) itest.PingInterceptedEchoServer(ctx, sc.WorkloadName, "80") } ================================================ FILE: integration_test/mounts_test.go ================================================ package integration_test import ( "fmt" "os" "path/filepath" "runtime" "time" core "k8s.io/api/core/v1" "github.com/telepresenceio/telepresence/v2/integration_test/itest" ) type mountsSuite struct { itest.Suite itest.TrafficManager } func (s *mountsSuite) SuiteName() string { return "Mounts" } func init() { itest.AddConnectedSuite("", func(h itest.TrafficManager) itest.TestingSuite { return &mountsSuite{ Suite: itest.Suite{Harness: h}, TrafficManager: h, } }) } func (s *mountsSuite) SetupSuite() { if s.IsCI() && runtime.GOOS == "darwin" { s.T().Skip("Mount tests don't run on darwin due to macFUSE issues") return } s.Suite.SetupSuite() } func (s *mountsSuite) createDeployment() [3]itest.TplResource { ctx := s.Context() rq := s.Require() pv := &itest.PersistentVolume{ Name: "local-pv", } if s.UseLocalPathProvisioner() { pv.Annotations = map[string]string{ "pv.kubernetes.io/provisioned-by": "rancher.io/local-path", } pv.StorageClassName = "local-path" } rq.NoError(pv.Apply(ctx, s.AppNamespace())) pvc := &itest.PersistentVolumeClaim{ Name: "local-pvc", } if s.UseLocalPathProvisioner() { pvc.Annotations = map[string]string{ "pv.kubernetes.io/provisioned-by": "rancher.io/local-path", } pvc.StorageClassName = "local-path" } rq.NoError(pvc.Apply(ctx, s.AppNamespace())) dep := &itest.Generic{ Name: "hello", Registry: "ghcr.io/telepresenceio", Image: "echo-server:latest", Volumes: []core.Volume{ { Name: "rw-volume", VolumeSource: core.VolumeSource{ PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: pvc.Name}, }, }, }, VolumeMounts: []core.VolumeMount{ { Name: "rw-volume", MountPath: "/data", }, }, } rq.NoError(dep.Apply(ctx, s.AppNamespace())) rq.NoError(s.RolloutStatusWait(ctx, "deployment/hello")) return [3]itest.TplResource{pv, pvc, dep} } func (s *mountsSuite) deleteDeployment(ts [3]itest.TplResource) { // Delete in reverse order for i := 2; i >= 0; i-- { s.Require().NoError(ts[i].Delete(s.Context())) } } func (s *mountsSuite) Test_MountWrite() { if runtime.GOOS == "windows" { s.T().SkipNow() } ts := s.createDeployment() defer s.deleteDeployment(ts) ctx := s.Context() mountPoint := filepath.Join(s.T().TempDir(), "mnt") itest.TelepresenceOk(ctx, "intercept", "hello", "--mount", mountPoint, "--port", "80:80") time.Sleep(2 * time.Second) content := "hello world\n" path := filepath.Join(mountPoint, "data", "hello.txt") rq := s.Require() rq.NoError(os.WriteFile(path, []byte(content), 0o644)) itest.TelepresenceOk(ctx, "leave", "hello") time.Sleep(2 * time.Second) mountPoint = filepath.Join(s.T().TempDir(), "data") itest.TelepresenceOk(ctx, "intercept", "hello", "--mount", mountPoint, "--port", "80:80") defer itest.TelepresenceOk(ctx, "leave", "hello") s.CapturePodLogs(ctx, "hello", "traffic-agent", s.AppNamespace()) path = filepath.Join(mountPoint, "data", "hello.txt") data, err := os.ReadFile(path) rq.NoError(err) rq.Equal(content, string(data)) } func (s *mountsSuite) Test_MountReadOnly() { if runtime.GOOS == "windows" { s.T().SkipNow() } rs := s.createDeployment() defer s.deleteDeployment(rs) ctx := s.Context() mountPoint := filepath.Join(s.T().TempDir(), "mnt") itest.TelepresenceOk(ctx, "intercept", "hello", "--mount", mountPoint+":ro", "--port", "80:80") defer itest.TelepresenceOk(ctx, "leave", "hello") time.Sleep(2 * time.Second) s.Require().Error(os.WriteFile(filepath.Join(mountPoint, "data", "hello.txt"), []byte("hello world\n"), 0o644)) } // Test_CollidingMounts tests that multiple mounts from several containers are managed correctly // by the traffic-agent and that an intercept of a container mounts the expected volumes. func (s *mountsSuite) Test_CollidingMounts() { ctx := s.Context() s.ApplyTemplate(ctx, filepath.Join("testdata", "k8s", "hello-w-volumes.goyaml"), nil) defer s.DeleteSvcAndWorkload(ctx, "deploy", "hello") type lm struct { name string svcPort int mountPoint string } var tests []lm if runtime.GOOS == "windows" { tests = []lm{ { "one", 80, "O:", }, { "two", 81, "T:", }, } } else { tempDir := s.T().TempDir() tests = []lm{ { "one", 80, filepath.Join(tempDir, "one"), }, { "two", 81, filepath.Join(tempDir, "two"), }, } } for i, tt := range tests { s.Run(tt.name, func() { ctx := s.Context() require := s.Require() stdout := itest.TelepresenceOk(ctx, "intercept", "hello", "--mount", tt.mountPoint, "--port", fmt.Sprintf("%d:%d", tt.svcPort, tt.svcPort)) defer itest.TelepresenceOk(ctx, "leave", "hello") require.Contains(stdout, "Using Deployment hello") if i == 0 { s.CapturePodLogs(ctx, "hello", "traffic-agent", s.AppNamespace()) } else { // Mounts are sometimes slow time.Sleep(3 * time.Second) } ns, err := os.ReadFile(filepath.Join(tt.mountPoint, "var", "run", "secrets", "kubernetes.io", "serviceaccount", "namespace")) require.NoError(err) require.Equal(s.AppNamespace(), string(ns)) token, err := os.ReadFile(filepath.Join(tt.mountPoint, "var", "run", "secrets", "kubernetes.io", "serviceaccount", "token")) require.NoError(err) require.True(len(token) > 0) }) } } ================================================ FILE: integration_test/multi_connect_test.go ================================================ package integration_test import ( "bytes" "context" "os" "path/filepath" "regexp" goRuntime "runtime" "strings" "sync" "time" "github.com/go-json-experiment/json" "k8s.io/client-go/tools/clientcmd" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/dos" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" "github.com/telepresenceio/telepresence/v2/pkg/ioutil" "github.com/telepresenceio/telepresence/v2/pkg/labels" ) type multiConnectSuite struct { itest.Suite itest.TrafficManager appSpace2 string mgrSpace2 string handlerTag string } func (s *multiConnectSuite) SuiteName() string { return "MultiConnect" } func init() { // This will give us one namespace pair with a traffic-manager installed. itest.AddTrafficManagerSuite("-1", func(h itest.TrafficManager) itest.TestingSuite { return &multiConnectSuite{Suite: itest.Suite{Harness: h}, TrafficManager: h} }) } func (s *multiConnectSuite) SetupSuite() { if s.IsCI() && !(goRuntime.GOOS == "linux" && goRuntime.GOARCH == "amd64") { s.T().Skip("CI can't run linux docker containers inside non-linux runners") } s.Suite.SetupSuite() // This will give us another namespace pair with a traffic-manager installed. ctx := s.Context() require := s.Require() suffix := itest.GetGlobalHarness(s.HarnessContext()).Suffix() s.appSpace2, s.mgrSpace2 = itest.AppAndMgrNSName(suffix + "-2") itest.CreateNamespaces(ctx, s.appSpace2, s.mgrSpace2) const svc = "echo" appData := itest.AppData{ AppName: svc, Image: "ghcr.io/telepresenceio/echo-server:latest", Ports: []itest.AppPort{ { ServicePortNumber: 80, TargetPortName: "http", TargetPortNumber: 8080, }, }, Env: map[string]string{"PORT": "8080"}, } wg := sync.WaitGroup{} wg.Add(2) go func() { defer wg.Done() itest.ApplyAppTemplate(ctx, s.AppNamespace(), &appData) }() go func() { defer wg.Done() itest.ApplyAppTemplate(ctx, s.appSpace2, &appData) }() ctx2 := itest.WithNamespaces(ctx, &itest.Namespaces{Namespace: s.mgrSpace2, Selector: labels.SelectorFromNames(s.appSpace2)}) err := itest.Kubectl(ctx2, s.mgrSpace2, "apply", "-f", filepath.Join(itest.GetOSSRoot(ctx2), "testdata", "k8s", "client_sa.yaml")) require.NoError(err, "failed to create connect ServiceAccount") db, err := itest.ReadTemplate(ctx, filepath.Join("testdata", "k8s", "client_rancher.goyaml"), map[string]string{ "ManagerNamespace": s.mgrSpace2, }) require.NoError(err) require.NoError(err, itest.Kubectl(dos.WithStdin(ctx, bytes.NewReader(db)), s.mgrSpace2, "apply", "-f", "-")) ctx2 = itest.WithUser(ctx2, s.mgrSpace2+":"+itest.TestUser) s.TelepresenceHelmInstallOK(ctx2, false) itest.TelepresenceQuitOk(ctx2) s.handlerTag = "telepresence/echo-test" testDir := "testdata/echo-server" _, err = itest.Output(ctx, "docker", "build", "-t", s.handlerTag, testDir) require.NoError(err) wg.Wait() if s.T().Failed() { s.T().FailNow() } } func (s *multiConnectSuite) AmendSuiteContext(ctx context.Context) context.Context { return itest.WithConfig(ctx, func(cfg client.Config) { cfg.Intercept().UseFtp = false }) } func (s *multiConnectSuite) TearDownSuite() { ctx2 := itest.WithNamespaces(s.Context(), &itest.Namespaces{Namespace: s.mgrSpace2, Selector: labels.SelectorFromNames(s.appSpace2)}) s.UninstallTrafficManager(ctx2, s.mgrSpace2) itest.DeleteNamespaces(ctx2, s.appSpace2, s.mgrSpace2) } func (s *multiConnectSuite) Test_MultipleConnect() { ctx := s.Context() itest.TelepresenceOk(ctx, "connect", "--docker", "--namespace", s.AppNamespace(), "--manager-namespace", s.ManagerNamespace()) defer itest.TelepresenceDisconnectOk(ctx, "--use", s.AppNamespace()) ctx2 := itest.WithUser(ctx, s.mgrSpace2+":"+itest.TestUser) itest.TelepresenceOk(ctx2, "connect", "--docker", "--namespace", s.appSpace2, "--manager-namespace", s.mgrSpace2) defer itest.TelepresenceDisconnectOk(ctx2, "--use", s.appSpace2) require := s.Require() kc := itest.KubeConfig(ctx) cfg, err := clientcmd.LoadFromFile(kc) require.NoError(err) ctxName := ioutil.SafeName(cfg.CurrentContext) s.doubleConnectCheck(ctx, ctx2, ctxName+"-"+s.AppNamespace()+"-cn", ctxName+"-"+s.appSpace2+"-cn", s.AppNamespace(), s.appSpace2, "") } func (s *multiConnectSuite) Test_MultipleConnect_named() { ctx := s.Context() const n1 = "primary" const n2 = "secondary" itest.TelepresenceOk(ctx, "connect", "--docker", "--name", n1, "--namespace", s.AppNamespace(), "--manager-namespace", s.ManagerNamespace()) defer itest.TelepresenceDisconnectOk(ctx, "--use", n1) ctx2 := itest.WithUser(ctx, s.mgrSpace2+":"+itest.TestUser) itest.TelepresenceOk(ctx2, "connect", "--docker", "--name", n2, "--namespace", s.appSpace2, "--manager-namespace", s.mgrSpace2) defer itest.TelepresenceDisconnectOk(ctx2, "--use", n2) s.doubleConnectCheck(ctx, ctx2, n1, n2, s.AppNamespace(), s.appSpace2, "") } // Test_MultipleConnect_sameNamespace tests that we can have multiple connects to the same namespace when using // named connections. This can be of interest when ports on localhost in different intercepted pods collide but can't // be remapped because the intercept handler expects them to be specific numbers. Using multiple connections and // docker containers means that each container have its own localhost. func (s *multiConnectSuite) Test_MultipleConnect_sameNamespace() { ctx := s.Context() const n1 = "first" const n2 = "second" s.TelepresenceConnect(ctx, "--docker", "--name", n1) defer itest.TelepresenceDisconnectOk(ctx, "--use", n1) s.TelepresenceConnect(ctx, "--docker", "--name", n2) defer itest.TelepresenceDisconnectOk(ctx, "--use", n2) itest.ApplyEchoService(ctx, "hello", s.AppNamespace(), 80) s.doubleConnectCheck(ctx, ctx, n1, n2, s.AppNamespace(), s.AppNamespace(), "hello") } func (s *multiConnectSuite) doubleConnectCheck(ctx1, ctx2 context.Context, n1, n2, ns1, ns2, svc2 string) { require := s.Require() st := itest.TelepresenceStatusOk(ctx1, "--use", n1) require.Equal(st.UserDaemon.Namespace, ns1) name1 := st.UserDaemon.Name st = itest.TelepresenceStatusOk(ctx1, "--use", n2) require.Equal(st.UserDaemon.Namespace, ns2) name2 := st.UserDaemon.Name cacheDir := filelocation.AppUserCacheDir(ctx1) var di daemon.Info info, err := os.ReadFile(filepath.Join(cacheDir, "userd", n1+".json")) require.NoError(err) require.NoError(json.Unmarshal(info, &di)) require.Equal(ns1, di.Namespace) info, err = os.ReadFile(filepath.Join(cacheDir, "userd", n2+".json")) require.NoError(err) require.NoError(json.Unmarshal(info, &di)) require.Equal(ns2, di.Namespace) ctx1, cancel1 := context.WithCancel(ctx1) defer cancel1() ctx2, cancel2 := context.WithCancel(ctx2) defer cancel2() wg := sync.WaitGroup{} runDockerRun := func(ctx context.Context, use, svc string, cancel context.CancelFunc) { defer wg.Done() defer cancel() so, se, err := itest.Telepresence(ctx, "intercept", "--use", use, "--mount", "false", svc, "--docker-run", "--port", "8080", "--", "--rm", "--name", use+"-app", s.handlerTag) if err != nil { clog.Errorf(ctx, "intercept %s ended with error: %v", svc, err) } if so != "" { clog.Infof(ctx, "intercept %s stdout: %s", svc, so) } if se != "" { clog.Infof(ctx, "intercept %s stderr: %s", svc, se) } } assertInterceptResponse := func(ctx context.Context, cn, svc string) { s.Assert().EventuallyContext(ctx, func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--use", cn, "--intercepts") if err == nil { if strings.Contains(stdout, svc+": intercepted") { return true } clog.Infof(ctx, "stdout: %s", stdout) } else { clog.Error(ctx, err) } return false }, 30*time.Second, 3*time.Second) // Response contains env variables TELEPRESENCE_CONTAINER and TELEPRESENCE_INTERCEPT_ID expectedOutput := regexp.MustCompile(`Intercept id [0-9a-f-]+:` + svc) s.Assert().EventuallyContext(ctx, // condition func() bool { ot, et, err := itest.Telepresence(ctx, "--use", cn, "curl", "--silent", "--max-time", "2", svc) if err != nil { clog.Errorf(ctx, "%s%s:%v", ot, et, err) return false } clog.Info(ctx, ot) return expectedOutput.MatchString(ot) }, 20*time.Second, // waitFor 3*time.Second, // polling interval `body of %q matches %q`, "http://"+svc, expectedOutput, ) } assertNotIntercepted := func(ctx context.Context, use, svc string) { s.Assert().EventuallyContext(ctx, func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--use", use, "--intercepts") return err == nil && !strings.Contains(stdout, svc+": intercepted") }, 10*time.Second, 2*time.Second) } svc1 := "echo" if svc2 == "" { svc2 = svc1 } wg.Add(2) ixCtx1, ixCancel1 := context.WithCancel(ctx1) defer ixCancel1() go runDockerRun(ctx1, n1, svc1, ixCancel1) ixCtx2, ixCancel2 := context.WithCancel(ctx2) defer ixCancel2() go runDockerRun(ctx2, n2, svc2, ixCancel2) assertInterceptResponse(ixCtx1, name1, svc1) assertInterceptResponse(ixCtx2, name2, svc2) itest.TelepresenceOk(ctx1, "leave", "--use", n1, svc1) assertNotIntercepted(ctx1, n1, svc1) // Other connection's intercept is still alive and kicking. assertInterceptResponse(ixCtx2, name2, svc2) itest.TelepresenceOk(ctx2, "leave", "--use", n2, svc2) assertNotIntercepted(ctx2, n2, svc2) cancel1() cancel2() wg.Wait() } ================================================ FILE: integration_test/multiple_intercepts_test.go ================================================ package integration_test import ( "context" "fmt" "io" "net" "net/http" "regexp" "strconv" "sync" "time" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" ) type multipleInterceptsSuite struct { itest.Suite itest.MultipleServices servicePort []int serviceCancel []context.CancelFunc } func (s *multipleInterceptsSuite) SuiteName() string { return "MultipleIntercepts" } func init() { itest.AddMultipleServicesSuite("", "hello", 3, func(h itest.MultipleServices) itest.TestingSuite { return &multipleInterceptsSuite{ Suite: itest.Suite{Harness: h}, MultipleServices: h, servicePort: make([]int, h.ServiceCount()), serviceCancel: make([]context.CancelFunc, h.ServiceCount()), } }) } func (s *multipleInterceptsSuite) SetupSuite() { s.Suite.SetupSuite() ctx := s.Context() for i := 0; i < s.ServiceCount(); i++ { s.servicePort[i], s.serviceCancel[i] = itest.StartLocalHttpEchoServer(ctx, fmt.Sprintf("%s-%d", s.Name(), i)) } wg := sync.WaitGroup{} wg.Add(s.ServiceCount()) for i := 0; i < s.ServiceCount(); i++ { go func(i int) { defer wg.Done() svc := fmt.Sprintf("%s-%d", s.Name(), i) stdout := itest.TelepresenceOk(ctx, "intercept", svc, "--mount", "false", "--port", strconv.Itoa(s.servicePort[i])) s.Contains(stdout, "Using Deployment "+svc) s.NoError(s.RolloutStatusWait(ctx, "deploy/"+svc)) }(i) } wg.Wait() } func (s *multipleInterceptsSuite) TearDownSuite() { ctx := s.Context() for i := 0; i < s.ServiceCount(); i++ { itest.TelepresenceOk(ctx, "leave", fmt.Sprintf("%s-%d", s.Name(), i)) } for _, cancel := range s.serviceCancel { if cancel != nil { cancel() } } // Ensure that we have OK statuses on our services after leaving the intercept. s.Eventually(func() bool { stdout := itest.TelepresenceOk(ctx, "list", "-n", s.AppNamespace()) for i := 0; i < s.ServiceCount(); i++ { rx := regexp.MustCompile(fmt.Sprintf(`%s-%d\s*: ready to (engage|intercept)`, s.Name(), i)) if !rx.MatchString(stdout) { return false } } return true }, 30*time.Second, 2*time.Second) } func (s *multipleInterceptsSuite) Test_Intercepts() { ctx := s.Context() s.Eventually(func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--intercepts") if err != nil { return false } for i := 0; i < s.ServiceCount(); i++ { if !regexp.MustCompile(fmt.Sprintf(`%s-%d\s*: intercepted`, s.Name(), i)).MatchString(stdout) { return false } } return true }, 10*time.Second, time.Second) wg := sync.WaitGroup{} wg.Add(s.ServiceCount()) for i := 0; i < s.ServiceCount(); i++ { go func(i int) { defer wg.Done() svc := fmt.Sprintf("%s-%d", s.Name(), i) expectedOutput := fmt.Sprintf("%s from intercept at /", svc) s.Require().Eventually( // condition func() bool { ip, err := net.DefaultResolver.LookupHost(ctx, svc) if err != nil { clog.Infof(ctx, "%v", err) return false } if len(ip) != 1 { clog.Infof(ctx, "Lookup for %s returned %v", svc, ip) return false } clog.Infof(ctx, "trying %q...", "http://"+svc) hc := http.Client{Timeout: 2 * time.Second} resp, err := hc.Get("http://" + svc) if err != nil { clog.Infof(ctx, "%v", err) return false } defer resp.Body.Close() clog.Infof(ctx, "status code: %v", resp.StatusCode) body, err := io.ReadAll(resp.Body) if err != nil { clog.Infof(ctx, "%v", err) return false } clog.Infof(ctx, "body: %q", body) return string(body) == expectedOutput }, time.Minute, // waitFor 3*time.Second, // polling interval `body of %q equals %q`, "http://"+svc, expectedOutput, ) }(i) } wg.Wait() } func (s *multipleInterceptsSuite) Test_ReportsPortConflict() { ctx := s.Context() svc := fmt.Sprintf("%s-%d", s.Name(), 0) itest.TelepresenceOk(ctx, "leave", svc) defer itest.TelepresenceOk(ctx, "intercept", "--mount", "false", "--port", strconv.Itoa(s.servicePort[0]), svc) _, stderr, err := itest.Telepresence(s.Context(), "intercept", "--mount", "false", "--port", strconv.Itoa(s.servicePort[1]), svc) s.Error(err) s.Contains(stderr, fmt.Sprintf("port 127.0.0.1:%d is already in use by intercept %s-1", s.servicePort[1], s.Name())) } ================================================ FILE: integration_test/multiple_port_intercept_test.go ================================================ package integration_test import ( "context" "fmt" "strconv" "sync" "time" core "k8s.io/api/core/v1" "github.com/telepresenceio/telepresence/v2/integration_test/itest" ) type multiportInterceptSuite struct { itest.Suite itest.TrafficManager servicePort [4]int serviceCancel [4]context.CancelFunc workloads [2]string ports [2][2]string } func (s *multiportInterceptSuite) SuiteName() string { return "MultiPortIntercept" } func init() { itest.AddTrafficManagerSuite("", func(h itest.TrafficManager) itest.TestingSuite { return &multiportInterceptSuite{ Suite: itest.Suite{Harness: h}, TrafficManager: h, workloads: [2]string{"echo-both", "echo-double-one-unnamed"}, ports: [2][2]string{{"one", "two"}, {"8080", "8081"}}, } }) } func (s *multiportInterceptSuite) SetupSuite() { if !(s.ManagerIsVersion(">2.21.x") && s.ClientIsVersion(">2.21.x")) { s.T().Skip("Not part of compatibility tests. Support for multiport intercept was introduced in 2.22") } s.Suite.SetupSuite() ctx := s.Context() for i := 0; i < 4; i++ { s.servicePort[i], s.serviceCancel[i] = itest.StartLocalHttpEchoServer(ctx, fmt.Sprintf("%s-%d", "echo", i)) } wg := sync.WaitGroup{} wg.Add(2) for i := 0; i < 2; i++ { go func() { defer wg.Done() dep := s.workloads[i] s.ApplyApp(ctx, dep, "deploy/"+dep) }() } s.TelepresenceConnect(ctx) wg.Wait() } func (s *multiportInterceptSuite) TearDownSuite() { ctx := s.Context() itest.TelepresenceQuitOk(ctx) for i := 0; i < 2; i++ { s.DeleteApp(ctx, s.workloads[i]) } for i := 0; i < 4; i++ { s.serviceCancel[i]() } } func (s *multiportInterceptSuite) Test_MultiPortIntercept() { ctx := s.Context() for i := 0; i < 2; i++ { itest.TelepresenceOk(ctx, "intercept", s.workloads[i], "--mount=false", "--port", fmt.Sprintf("%d:%s", s.servicePort[i*2], s.ports[i][0]), "--port", fmt.Sprintf("%d:%s", s.servicePort[i*2+1], s.ports[i][1])) } defer func() { for i := 0; i < 2; i++ { itest.TelepresenceOk(ctx, "leave", s.workloads[i]) } }() rq := s.Require() rq.Eventually(func() bool { st := itest.TelepresenceStatusOk(ctx) ics := st.UserDaemon.Intercepts if len(ics) != 2 { return false } for i := 0; i < 2; i++ { ic := ics[i] if ic.Name != s.workloads[i] { return false } } return true }, 10*time.Second, time.Second) var edoPods []core.Pod rq.Eventually(func() bool { edoPods = itest.RunningPods(ctx, s.workloads[1], s.AppNamespace()) return len(edoPods) == 1 }, 30*time.Second, 3*time.Second) edoPodIP := edoPods[0].Status.PodIP ports := [4]string{"80", "80", "8080", "8081"} for i, svc := range [4]string{"echo-one", "echo-two", edoPodIP, edoPodIP} { itest.PingInterceptedEchoServer(ctx, fmt.Sprintf("%s/echo-%d", svc, i), ports[i]) } } func (s *multiportInterceptSuite) Test_MultiPortLocalConflict() { ctx := s.Context() itest.TelepresenceOk(ctx, "intercept", s.workloads[0], "--mount=false", "--port", fmt.Sprintf("%d:%s", s.servicePort[0], s.ports[0][0]), "--port", fmt.Sprintf("%d:%s", s.servicePort[1], s.ports[0][1])) defer itest.TelepresenceOk(ctx, "leave", s.workloads[0]) _, _, err := itest.Telepresence(ctx, "intercept", s.workloads[1], "--mount=false", "--port", fmt.Sprintf("%d:%s", s.servicePort[2], s.ports[1][0]), "--port", fmt.Sprintf("%d:%s", s.servicePort[1], s.ports[1][1])) s.Require().Error(err) s.Contains(err.Error(), fmt.Sprintf("%d is already in use by intercept %s", s.servicePort[1], s.workloads[0])) } func (s *multiportInterceptSuite) Test_MultiPortRemoteConflict() { ctx := s.Context() itest.TelepresenceOk(ctx, "intercept", s.workloads[0], "--mount=false", "--port", fmt.Sprintf("%d:%s", s.servicePort[0], s.ports[0][0]), "--port", fmt.Sprintf("%d:%s", s.servicePort[1], s.ports[0][1])) defer itest.TelepresenceOk(ctx, "leave", s.workloads[0]) _, _, err := itest.Telepresence(ctx, "intercept", "--workload", s.workloads[0], s.workloads[0]+"-again", "--mount=false", "--port", strconv.Itoa(s.servicePort[1]), "--service", "echo-two") s.Require().Error(err) s.Regexp(`one intercept has no filters \(intercepts all traffic\)`, err.Error()) } ================================================ FILE: integration_test/multiple_services_test.go ================================================ package integration_test import ( "bytes" "errors" "fmt" "io" "math/rand" "net/http" "strings" "sync" "sync/atomic" "time" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" ) type multipleServicesSuite struct { itest.Suite itest.MultipleServices } func (s *multipleServicesSuite) SuiteName() string { return "MultipleServices" } func init() { itest.AddMultipleServicesSuite("", "hello", 3, func(h itest.MultipleServices) itest.TestingSuite { return &multipleServicesSuite{Suite: itest.Suite{Harness: h}, MultipleServices: h} }) } func (s *multipleServicesSuite) Test_LargeRequest() { if !(s.ManagerIsVersion(">2.21.x") && s.ClientIsVersion(">2.21.x")) { s.T().Skip("Not part of compatibility tests. TUN-device isn't stable enough in versions <2.22.0") } ctx := s.Context() itest.TelepresenceQuitOk(ctx) s.TelepresenceConnect(ctx) defer func() { // Restore the connection to what it was before the test. itest.TelepresenceQuitOk(ctx) s.TelepresenceConnect(s.Context()) }() const sendSize = 1024 * 1024 * 12 const varyMax = 1024 * 1024 * 4 // vary last 4Mi const concurrentRequests = 64 tb := [sendSize + varyMax]byte{} tb[0] = '!' tb[1] = '\n' for i := 2; i < sendSize+varyMax; i++ { tb[i] = 'A' } time.Sleep(3 * time.Second) wg := sync.WaitGroup{} wg.Add(concurrentRequests) pingPong := func(x int) { defer wg.Done() sendSize := sendSize + rand.Int()%varyMax // vary the last 64Ki to get random buffer sizes b := tb[:sendSize] // Distribute the requests over all services url := fmt.Sprintf("http://%s-%d.%s/put", s.Name(), x%s.ServiceCount(), s.AppNamespace()) req, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(b)) req.ContentLength = int64(len(b)) if !s.NoError(err) { return } client := &http.Client{Timeout: 60 * time.Second} resp, err := client.Do(req) if !s.NoError(err) { return } bdy := resp.Body defer bdy.Close() if !s.Equal(resp.StatusCode, 200) { return } cl := sendSize + 1024 buf := make([]byte, cl) i := 0 for i < cl && err == nil { var j int j, err = bdy.Read(buf[i:]) i += j } if errors.Is(err, io.EOF) { err = nil } if s.NoError(err) { ei := bytes.Index(buf, []byte{'!', '\n'}) s.GreaterOrEqual(ei, 0) // Do this instead of require.Equal(b, buf[ei:i]) so that on failure we don't print two very large buffers to the terminal s.Equal(true, bytes.Equal(b, buf[ei:i])) } } for i := 0; i < concurrentRequests; i += 4 { go func() { pingPong(i) pingPong(i + 1) pingPong(i + 2) pingPong(i + 3) }() } wg.Wait() } func (s *multipleServicesSuite) Test_List() { stdout := itest.TelepresenceOk(s.Context(), "list", "-n", s.AppNamespace()) for i := 0; i < s.ServiceCount(); i++ { s.Regexp(fmt.Sprintf(`%s-%d\s*: ready to (engage|intercept)`, s.Name(), i), stdout) } } func (s *multipleServicesSuite) Test_ListOnlyMapped() { ctx := itest.WithUser(s.Context(), "default") require := s.Require() itest.TelepresenceDisconnectOk(ctx) defer func() { ctx := s.Context() itest.TelepresenceDisconnectOk(ctx) itest.TelepresenceOk(s.Context(), "connect", "--namespace", s.AppNamespace(), "--manager-namespace", s.ManagerNamespace()) }() s.TelepresenceConnect(ctx, "--mapped-namespaces", "default") stdout := itest.TelepresenceOk(ctx, "list") require.Contains(stdout, "No Workloads") s.TelepresenceConnect(ctx, "--mapped-namespaces", "all") stdout = itest.TelepresenceOk(ctx, "list") require.NotContains(stdout, "No Workloads") } func (s *multipleServicesSuite) Test_RepeatedConnect() { totalErrCount := int64(0) for i := 0; i < s.ServiceCount(); i++ { url := fmt.Sprintf("http://%s-%d.%s", s.Name(), i, s.AppNamespace()) for v := 0; v < 30; v++ { s.Run(fmt.Sprintf("test-%d", i*30+v), func() { ctx := s.Context() s.T().Parallel() time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond) assert := s.Assert() cl := http.Client{} req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) req.Close = true assert.NoError(err) res, err := cl.Do(req) if err != nil { if atomic.AddInt64(&totalErrCount, 1) > 2 { s.Failf("failed more than 2 times: %v", err.Error()) } return } assert.Equal(res.StatusCode, http.StatusOK) _, err = io.Copy(io.Discard, res.Body) assert.NoError(err) assert.NoError(res.Body.Close()) }) } } } func (s *multipleServicesSuite) Test_ProxiesOutboundTraffic() { ctx := s.Context() for i := 0; i < s.ServiceCount(); i++ { svc := fmt.Sprintf("%s-%d.%s", s.Name(), i, s.AppNamespace()) expectedOutput := fmt.Sprintf("Request served by %s-%d", s.Name(), i) s.Require().Eventually( // condition func() bool { clog.Infof(ctx, "trying %q...", "http://"+svc) hc := http.Client{Timeout: time.Second} resp, err := hc.Get("http://" + svc) if err != nil { clog.Error(ctx, err) return false } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { clog.Error(ctx, err) return false } clog.Infof(ctx, "body: %q", body) return strings.Contains(string(body), expectedOutput) }, 15*time.Second, // waitfor 3*time.Second, // polling interval `body of %q contains %q`, "http://"+svc, expectedOutput, ) } } ================================================ FILE: integration_test/multiport_test.go ================================================ package integration_test import ( "fmt" "net" "strings" "sync" "time" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" ) // Test_MultipleUnnamedServicePorts tests the use-case where multiple services using // unnamed ports targets different container ports in the same workload and the same // container. func (s *connectedSuite) Test_MultipleUnnamedServicePorts() { if !s.ManagerIsVersion(">2.21.x") { s.T().Skip(`Not part of compatibility tests. No support for port annotation "all" in Versions < 2.22.0`) } ctx := s.Context() dep := "echo-double-one-unnamed" s.ApplyApp(ctx, dep, "deploy/"+dep) defer s.KubectlOk(ctx, "delete", "deploy", dep) require := s.Require() require.NoError(s.Kubectl(ctx, "expose", "deploy", dep, "--port", "80", "--target-port", "8080", "--name", dep+"-"+"80")) defer s.KubectlOk(ctx, "delete", "svc", dep+"-"+"80") require.NoError(s.Kubectl(ctx, "expose", "deploy", dep, "--port", "81", "--target-port", "8081", "--name", dep+"-"+"81")) defer s.KubectlOk(ctx, "delete", "svc", dep+"-"+"81") require.NoError(s.Kubectl(ctx, "expose", "deploy", dep, "--port", "82", "--target-port", "8082", "--name", dep+"-"+"82")) defer s.KubectlOk(ctx, "delete", "svc", dep+"-"+"82") portTest := func(svcPort, targetPort string) { ctx := s.Context() svc := dep + "-" + svcPort localPort, cancel := itest.StartLocalHttpEchoServer(ctx, svc) defer cancel() itest.TelepresenceOk(ctx, "intercept", "--mount", "false", "-p", fmt.Sprintf("%d:%s", localPort, svcPort), dep) defer itest.TelepresenceOk(ctx, "leave", dep) itest.PingInterceptedEchoServer(ctx, svc, svcPort) } s.Run("port 80", func() { portTest("80", "8080") }) s.Run("port 81", func() { portTest("81", "8081") }) s.Run("port 82", func() { // Must fail. The container doesn't expose port 8082 ctx := s.Context() require := s.Require() svcPort := "82" svc := dep + "-" + svcPort localPort, cancel := itest.StartLocalHttpEchoServer(ctx, svc) defer cancel() _, _, err := itest.Telepresence(ctx, "intercept", "-p", fmt.Sprintf("%d:%s", localPort, svcPort), dep) require.Error(err) }) } // Test_MultipleUnnamedServicePorts tests the use-case where multiple services using // unnamed ports targets different container ports in the same workload and the same // container. func (s *connectedSuite) Test_NoContainerPort() { ctx := s.Context() dep := "echo-no-containerport" s.ApplyApp(ctx, dep, "deploy/"+dep) defer s.KubectlOk(ctx, "delete", "deploy", dep) require := s.Require() require.NoError(s.Kubectl(ctx, "expose", "deploy", dep, "--port", "80", "--target-port", "8080", "--name", dep+"-"+"80")) defer s.KubectlOk(ctx, "delete", "svc", dep+"-"+"80") require.NoError(s.Kubectl(ctx, "expose", "deploy", dep, "--port", "81", "--target-port", "8081", "--name", dep+"-"+"81")) defer s.KubectlOk(ctx, "delete", "svc", dep+"-"+"81") portTest := func(svcPort, targetPort string) { ctx := s.Context() svc := dep + "-" + svcPort localPort, cancel := itest.StartLocalHttpEchoServer(ctx, svc) defer cancel() itest.TelepresenceOk(ctx, "intercept", "--mount", "false", "-p", fmt.Sprintf("%d:%s", localPort, svcPort), dep) defer itest.TelepresenceOk(ctx, "leave", dep) itest.PingInterceptedEchoServer(ctx, svc, svcPort) } s.Run("port 80", func() { portTest("80", "8080") }) s.Run("port 81", func() { portTest("81", "8081") }) } func (s *connectedSuite) Test_UnnamedUdpAndTcpPort() { ctx := s.Context() dep := "echo-udp-tcp-unnamed" s.ApplyApp(ctx, dep, "deploy/"+dep) defer s.KubectlOk(ctx, "delete", "deploy", dep) require := s.Require() require.NoError(s.Kubectl(ctx, "expose", "deploy", dep, "--port", "80", "--protocol", "UDP", "--target-port", "8080", "--name", "echo-udp")) defer s.KubectlOk(ctx, "delete", "svc", "echo-udp") require.NoError(s.Kubectl(ctx, "expose", "deploy", dep, "--port", "80", "--protocol", "TCP", "--target-port", "8080", "--name", "echo-tcp")) defer s.KubectlOk(ctx, "delete", "svc", "echo-tcp") svcPort := "80" s.Run("TCP port 80", func() { ctx := s.Context() localPort, cancel := itest.StartLocalHttpEchoServer(ctx, "echo-tcp") defer cancel() itest.TelepresenceOk(ctx, "intercept", "--mount", "false", "--service", "echo-tcp", "-p", fmt.Sprintf("%d:%s", localPort, svcPort), dep) s.CapturePodLogs(ctx, dep, "traffic-agent", s.AppNamespace()) defer itest.TelepresenceOk(ctx, "leave", dep) itest.PingInterceptedEchoServer(ctx, "echo-tcp", svcPort) }) s.Run("UDP port 80", func() { ctx := s.Context() require := s.Require() localPortCh := make(chan int, 1) // Start a local interceptor service that prints some relevant messages go func() { lc := net.ListenConfig{} pc, err := lc.ListenPacket(ctx, "udp", ":0") addr := pc.LocalAddr().(*net.UDPAddr) localPortCh <- addr.Port close(localPortCh) require.NoError(err) buf := [0x100]byte{} for err == nil { var rr net.Addr var n int _ = pc.SetReadDeadline(time.Now().Add(10 * time.Millisecond)) n, rr, err = pc.ReadFrom(buf[:]) if n > 0 { msg := string(buf[0:n]) clog.Infof(ctx, "Local UDP server received %q", msg) _, werr := pc.WriteTo([]byte(fmt.Sprintf("received message %q", msg)), rr) require.NoError(werr) } if err != nil && strings.Contains(err.Error(), "timeout") { err = ctx.Err() } } clog.Debug(ctx, "UDP end") }() var localPort int select { case <-ctx.Done(): return case localPort = <-localPortCh: } itest.TelepresenceOk(ctx, "loglevel", "trace") defer itest.TelepresenceOk(ctx, "loglevel", "debug") itest.TelepresenceOk(ctx, "intercept", "--mount", "false", "--service", "echo-udp", "-p", fmt.Sprintf("%d:%s", localPort, svcPort), dep) s.CapturePodLogs(ctx, dep, "traffic-agent", s.AppNamespace()) defer itest.TelepresenceOk(ctx, "leave", dep) pingPong := func(conn net.Conn, msg string) { _ = conn.SetDeadline(time.Now().Add(10 * time.Second)) bm := []byte(msg) n, err := conn.Write(bm) require.NoError(err) require.Equal(len(bm), n) buf := [0x100]byte{} n, err = conn.Read(buf[:]) require.NoError(err) require.Equal(fmt.Sprintf("received message %q", msg), string(buf[0:n])) } interact := func(i int, wg *sync.WaitGroup) { defer wg.Done() time.Sleep(time.Second) conn, err := net.Dial("udp", fmt.Sprintf("%s:80", "echo-udp")) require.NoError(err) defer conn.Close() pingPong(conn, fmt.Sprintf("%d: 12345678", i)) pingPong(conn, fmt.Sprintf("%d: a slightly longer message", i)) } wg := sync.WaitGroup{} wg.Add(5) for i := 0; i < 5; i++ { go interact(i, &wg) } wg.Wait() }) } // TestSameContainerPort tests the use-case where multiple services using // the same container port. func (s *connectedSuite) Test_SameContainerPort() { ctx := s.Context() dep := "echo-stp" s.ApplyApp(ctx, "echo-same-target-port", "deploy/"+dep) defer func() { s.KubectlOk(ctx, "delete", "deploy", dep) }() portTest := func(svcPort string) { ctx := s.Context() localPort, cancel := itest.StartLocalHttpEchoServer(ctx, dep) defer cancel() itest.TelepresenceOk(ctx, "intercept", "--mount=false", "-p", fmt.Sprintf("%d:%s", localPort, svcPort), dep) defer itest.TelepresenceOk(ctx, "leave", dep) // Both ports are now intercepted because the intercept is on the container port itest.PingInterceptedEchoServer(ctx, dep, "80") itest.PingInterceptedEchoServer(ctx, dep, "8080") } s.Run("port 80", func() { portTest("eighty") }) s.Run("port 8080", func() { portTest("eighty-eighty") }) } ================================================ FILE: integration_test/namespaces_test.go ================================================ package integration_test import ( "bytes" "context" "path/filepath" "sync" "time" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/agentconfig" "github.com/telepresenceio/telepresence/v2/pkg/dos" "github.com/telepresenceio/telepresence/v2/pkg/labels" ) type nsSuite struct { nss []namespaceData itest.Suite } func (s *nsSuite) SuiteName() string { return "Namespaces" } func init() { itest.AddClusterSuite(func(ctx context.Context) itest.TestingSuite { return &nsSuite{Suite: itest.Suite{Harness: itest.NewContextHarness(ctx)}} }) } const namespaceTpl = ` apiVersion: v1 kind: Namespace metadata: name: {{.Name}} labels: purpose: tp-cli-testing {{- with .Labels }} {{ toYaml . | nindent 4 }} {{- end }}` type namespaceData struct { Name string Labels map[string]string } func (s *nsSuite) SetupSuite() { if !(s.ClientIsVersion(">2.21.x") && s.ManagerIsVersion(">2.21.x")) { s.T().Skip("Not part of compatibility tests. Namespace selector was introduced in 2.22.0") } s.nss = []namespaceData{ { "manager", nil, }, { "alpha", map[string]string{ "phase": "alpha", "deploy": "dev", }, }, { "beta", map[string]string{ "phase": "beta", "deploy": "dev", }, }, { "gamma", map[string]string{ "phase": "beta", "deploy": "dev", }, }, { "rc", map[string]string{ "phase": "rc", "deploy": "staging", }, }, { "ga", map[string]string{ "phase": "ga", "deploy": "prod", }, }, { "patch", map[string]string{ "phase": "patch", "deploy": "prod", }, }, } ctx := s.Context() wg := new(sync.WaitGroup) wg.Add(len(s.nss)) for _, ns := range s.nss { go func() { defer wg.Done() data, err := itest.EvalTemplate(namespaceTpl, ns) s.NoError(err) s.NoError(itest.Kubectl(dos.WithStdin(ctx, bytes.NewReader(data)), "", "apply", "-f", "-")) itest.ApplyEchoService(ctx, "echo", ns.Name, 80) }() } wg.Wait() err := itest.Kubectl(ctx, s.nss[0].Name, "apply", "-f", filepath.Join(itest.GetOSSRoot(ctx), "testdata", "k8s", "client_sa.yaml")) s.NoError(err, "failed to create connect ServiceAccount") } func (s *nsSuite) AmendSuiteContext(ctx context.Context) context.Context { return itest.WithUser(ctx, "manager:"+itest.TestUser) } func (s *nsSuite) managerNamespace() string { return s.nss[0].Name } func (s *nsSuite) TearDownSuite() { ctx := s.Context() wg := new(sync.WaitGroup) wg.Add(len(s.nss)) for _, ns := range s.nss { go func() { defer wg.Done() _ = itest.Kubectl(ctx, "", "delete", "ns", ns.Name) }() } wg.Wait() } func (s *nsSuite) Test_NamespacesClusterWide() { ctx := s.Context() ctx = itest.WithNamespaces(ctx, &itest.Namespaces{ Namespace: s.managerNamespace(), }) s.TelepresenceHelmInstallOK(ctx, false) defer s.UninstallTrafficManager(ctx, "manager") itest.TelepresenceOk(ctx, "connect", "--manager-namespace", s.managerNamespace(), "--namespace", "alpha") defer itest.TelepresenceDisconnectOk(ctx) st := itest.TelepresenceStatusOk(ctx) s.Greater(len(st.UserDaemon.MappedNamespaces), len(s.nss)) } func (s *nsSuite) Test_NamespacesDynamic() { ctx := itest.WithNamespaces(s.Context(), &itest.Namespaces{ Namespace: s.managerNamespace(), Selector: &labels.Selector{ MatchExpressions: []*labels.Requirement{ { Key: "phase", Operator: labels.OperatorIn, Values: []string{"alpha"}, }, { Key: "deploy", Operator: labels.OperatorIn, Values: []string{"dev"}, }, }, }, }) s.TelepresenceHelmInstallOK(ctx, false) defer s.UninstallTrafficManager(ctx, "manager") itest.TelepresenceOk(ctx, "connect", "--manager-namespace", s.managerNamespace(), "--namespace", "alpha") defer itest.TelepresenceDisconnectOk(ctx) rq := s.Require() st := itest.TelepresenceStatusOk(ctx) rq.Len(st.UserDaemon.MappedNamespaces, 1) itest.TelepresenceDisconnectOk(ctx) _, se, err := itest.Telepresence(ctx, "connect", "--manager-namespace", s.managerNamespace(), "--namespace", "beta") rq.Error(err) rq.Contains(se, "beta is not managed") // Switch to just using label "deploy=dev" ctx = itest.WithNamespaces(s.Context(), &itest.Namespaces{ Namespace: s.managerNamespace(), Selector: &labels.Selector{ MatchExpressions: []*labels.Requirement{{ Key: "deploy", Operator: labels.OperatorIn, Values: []string{"dev"}, }}, }, }) restartCount := func() int { pods := itest.RunningPods(ctx, agentconfig.ManagerAppName, s.managerNamespace()) if len(pods) == 1 { for _, cs := range pods[0].Status.ContainerStatuses { if cs.Name == agentconfig.ManagerAppName { return int(cs.RestartCount) } } } return -1 } rq.Equal(0, restartCount()) s.TelepresenceHelmInstallOK(ctx, true) // A dynamic change of namespaces should not result in a traffic-manager restart rq.Equal(0, restartCount()) itest.TelepresenceOk(ctx, "connect", "--manager-namespace", s.managerNamespace(), "--namespace", "beta") st = itest.TelepresenceStatusOk(ctx) rq.Len(st.UserDaemon.MappedNamespaces, 3) itest.TelepresenceDisconnectOk(ctx) // Add yet another namespace that matches "deploy=dev" data, err := itest.EvalTemplate(namespaceTpl, namespaceData{ "delta", map[string]string{ "phase": "test", "deploy": "dev", }, }) rq.NoError(err) rq.NoError(itest.Kubectl(dos.WithStdin(ctx, bytes.NewReader(data)), "", "apply", "-f", "-")) defer func() { s.NoError(itest.Kubectl(ctx, "", "delete", "namespace", "delta")) }() // Upgrade, to ensure that the proper roles and rolebindings are installed. s.TelepresenceHelmInstallOK(ctx, true) rq.Equal(0, restartCount()) itest.TelepresenceOk(ctx, "connect", "--manager-namespace", s.managerNamespace(), "--namespace", "delta") st = itest.TelepresenceStatusOk(ctx) rq.Len(st.UserDaemon.MappedNamespaces, 4) itest.ApplyEchoService(ctx, "echo", "delta", 80) // Check that list output includes the service from "delta" lst := itest.TelepresenceOk(ctx, "list") rq.Contains(lst, "deployment echo:") itest.TelepresenceDisconnectOk(ctx) // Delete and recreate the namespace s.NoError(itest.Kubectl(ctx, "", "delete", "namespace", "delta")) rq.NoError(itest.Kubectl(dos.WithStdin(ctx, bytes.NewReader(data)), "", "apply", "-f", "-")) // Upgrade, to ensure that the proper roles and rolebindings are installed. s.TelepresenceHelmInstallOK(ctx, true) rq.Equal(0, restartCount()) itest.TelepresenceOk(ctx, "connect", "--manager-namespace", s.managerNamespace(), "--namespace", "delta") st = itest.TelepresenceStatusOk(ctx) rq.Len(st.UserDaemon.MappedNamespaces, 4) itest.ApplyEchoService(ctx, "echo", "delta", 80) // Check that list output still includes the service from "delta" lst = itest.TelepresenceOk(ctx, "list") rq.Contains(lst, "deployment echo:") itest.TelepresenceDisconnectOk(ctx) } func (s *nsSuite) Test_NamespacesStatic() { ctx := itest.WithNamespaces(s.Context(), &itest.Namespaces{ Namespace: s.managerNamespace(), Selector: &labels.Selector{ MatchExpressions: []*labels.Requirement{ { Key: labels.NameLabelKey, Operator: labels.OperatorIn, Values: []string{"alpha"}, }, }, }, }) s.TelepresenceHelmInstallOK(ctx, false) defer s.UninstallTrafficManager(ctx, "manager") itest.TelepresenceOk(ctx, "connect", "--manager-namespace", s.managerNamespace(), "--namespace", "alpha") defer itest.TelepresenceDisconnectOk(ctx) rq := s.Require() st := itest.TelepresenceStatusOk(ctx) rq.Len(st.UserDaemon.MappedNamespaces, 1) itest.TelepresenceDisconnectOk(ctx) _, se, err := itest.Telepresence(ctx, "connect", "--manager-namespace", s.managerNamespace(), "--namespace", "beta") rq.Error(err) rq.Contains(se, "beta is not managed") // Switch to just using both alpha and beta ctx = itest.WithNamespaces(s.Context(), &itest.Namespaces{ Namespace: s.managerNamespace(), Selector: &labels.Selector{ MatchExpressions: []*labels.Requirement{{ Key: labels.NameLabelKey, Operator: labels.OperatorIn, Values: []string{"alpha", "beta"}, }}, }, }) getPodName := func() (podName string) { rq.Eventually(func() bool { pods := itest.RunningPods(ctx, agentconfig.ManagerAppName, s.managerNamespace()) if len(pods) == 1 { podName = pods[0].Name return true } return false }, 16*time.Second, 4*time.Second) return podName } tmPodName := getPodName() s.TelepresenceHelmInstallOK(ctx, true) // A static change must force a restart of the traffic-manager, or the new permissions will not come into effect. tmPodNameAfter := getPodName() rq.NotEqual(tmPodName, tmPodNameAfter) itest.TelepresenceOk(ctx, "connect", "--manager-namespace", s.managerNamespace(), "--namespace", "beta") st = itest.TelepresenceStatusOk(ctx) rq.Len(st.UserDaemon.MappedNamespaces, 2) itest.TelepresenceDisconnectOk(ctx) } ================================================ FILE: integration_test/not_connected_test.go ================================================ package integration_test import ( "bufio" "fmt" "regexp" "runtime" "strings" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/labels" ) type notConnectedSuite struct { itest.Suite itest.TrafficManager } func (s *notConnectedSuite) SuiteName() string { return "NotConnected" } func init() { itest.AddTrafficManagerSuite("", func(h itest.TrafficManager) itest.TestingSuite { return ¬ConnectedSuite{Suite: itest.Suite{Harness: h}, TrafficManager: h} }) } func (s *notConnectedSuite) TearDownTest() { itest.TelepresenceQuitOk(s.Context()) } func (s *notConnectedSuite) Test_ConnectWithCommand() { ctx := s.Context() exe, _ := s.Executable() stdout := s.TelepresenceConnect(ctx, "--", exe, "status") s.Contains(stdout, "Connected to context") s.Contains(stdout, "Kubernetes context:") } func (s *notConnectedSuite) Test_InvalidKubeconfig() { ctx := s.Context() path := "/dev/null" if runtime.GOOS == "windows" { path = "C:\\NUL" } badEnvCtx := itest.WithEnv(ctx, map[string]string{"KUBECONFIG": path}) _, stderr, err := itest.Telepresence(badEnvCtx, "connect") s.Contains(stderr, "kubeconfig has no context definition") s.Error(err) } func (s *notConnectedSuite) Test_NonExistentContext() { ctx := s.Context() _, stderr, err := itest.Telepresence(ctx, "connect", "--context", "not-likely-to-exist") s.Error(err) s.Contains(stderr, "context was not found") } func (s *notConnectedSuite) Test_ConnectingToOtherNamespace() { ctx := s.Context() suffix := itest.GetGlobalHarness(s.HarnessContext()).Suffix() appSpace2, mgrSpace2 := itest.AppAndMgrNSName(suffix + "-2") itest.CreateNamespaces(ctx, appSpace2, mgrSpace2) defer itest.DeleteNamespaces(ctx, appSpace2, mgrSpace2) s.Run("Installs Successfully", func() { ctx := itest.WithNamespaces(s.Context(), &itest.Namespaces{ Namespace: mgrSpace2, Selector: labels.SelectorFromNames(appSpace2), }) s.TelepresenceHelmInstallOK(ctx, false) }) s.Run("Can be connected to with --manager-namespace-flag", func() { ctx := s.Context() itest.TelepresenceQuitOk(ctx) // Set the config to some nonsense to verify that the flag wins ctx = itest.WithConfig(ctx, func(cfg client.Config) { cfg.Cluster().DefaultManagerNamespace = "daffy-duck" }) ctx = itest.WithUser(ctx, mgrSpace2+":"+itest.TestUser) stdout := itest.TelepresenceOk(ctx, "connect", "--namespace", appSpace2, "--manager-namespace="+mgrSpace2) s.Contains(stdout, "Connected to context") stdout = itest.TelepresenceOk(ctx, "status") s.Regexp(`Manager namespace\s+: `+mgrSpace2, stdout) }) s.Run("Can be connected to with defaultManagerNamespace config", func() { ctx := s.Context() itest.TelepresenceQuitOk(ctx) ctx = itest.WithConfig(ctx, func(cfg client.Config) { cfg.Cluster().DefaultManagerNamespace = mgrSpace2 }) stdout := itest.TelepresenceOk(itest.WithUser(ctx, "default"), "connect", "--namespace", appSpace2) s.Contains(stdout, "Connected to context") stdout = itest.TelepresenceOk(ctx, "status") s.Regexp(`Manager namespace\s+: `+mgrSpace2, stdout) }) s.Run("Uninstalls Successfully", func() { s.UninstallTrafficManager(s.Context(), mgrSpace2) }) } func (s *notConnectedSuite) Test_ReportsNotConnected() { ctx := s.Context() s.TelepresenceConnect(ctx) itest.TelepresenceDisconnectOk(ctx) stdout := itest.TelepresenceOk(ctx, "version") rxVer := regexp.QuoteMeta(s.ClientVersion().String()) s.Regexp(fmt.Sprintf(`Client\s*: v%s`, rxVer), stdout) s.Regexp(fmt.Sprintf(`Root Daemon\s*: v%s`, rxVer), stdout) s.Regexp(fmt.Sprintf(`User Daemon\s*: v%s`, rxVer), stdout) s.Regexp(`Traffic Manager\s*: not connected`, stdout) } // Test_CreateAndRunIndividualPod tess that pods can be created without a workload. func (s *notConnectedSuite) Test_CreateAndRunIndividualPod() { out := s.KubectlOk(s.Context(), "run", "-i", "busybox", "--rm", "--image", "curlimages/curl", "--restart", "Never", "--", "ls", "/etc") lines := bufio.NewScanner(strings.NewReader(out)) hostsFound := false for lines.Scan() { if lines.Text() == "hosts" { hostsFound = true break } } s.True(hostsFound, "remote ls command did not find /etc/hosts") } ================================================ FILE: integration_test/otel_test.go ================================================ package integration_test import ( "bytes" "os" "path/filepath" "time" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/dos" ) type otelSuite struct { itest.Suite itest.NamespacePair } func (s *otelSuite) SuiteName() string { return "Otel" } func init() { itest.AddNamespacePairSuite("-otel", func(h itest.NamespacePair) itest.TestingSuite { return &otelSuite{Suite: itest.Suite{Harness: h}, NamespacePair: h} }) } func (s *otelSuite) SetupSuite() { if _, ok := os.LookupEnv("HTTP_INTERCEPT_STRESS_TEST"); !ok { s.T().Skip("Run this stress manually. It's too demanding for the CI infrastructure.") return } s.Suite.SetupSuite() ctx := s.Context() cmd := itest.Command(ctx, "helm", "upgrade", "--install", "cert-manager", "jetstack/cert-manager", "--namespace", s.AppNamespace(), "--version", "v1.8.0", "--set", "installCRDs=true") out, err := cmd.CombinedOutput() s.Require().NoError(err, "helm repo add: %s", out) defer func() { if err != nil { _ = itest.Run(ctx, "helm", "uninstall", "cert-manager", "--namespace", s.AppNamespace()) } }() otelDir := filepath.Join("testdata", "otel") cmd = itest.Command(ctx, "helm", "upgrade", "--install", "otel-operator", "open-telemetry/opentelemetry-operator", "--namespace", s.AppNamespace(), "--create-namespace", "--set", "manager.collectorImage.repository=otel/opentelemetry-collector-k8s", "--set", "admissionWebhooks.certManager.enabled=false", "--set", "admissionWebhooks.autoGenerateCert.enabled=true", "--set", "manager.extraArgs={--enable-go-instrumentation}", "--values", filepath.Join(otelDir, "helm-yamls", "otel-operator.yml"), ) out, err = cmd.CombinedOutput() s.Require().NoError(err, "helm install", string(out)) defer func() { if err != nil { _ = itest.Run(ctx, "helm", "uninstall", "otel-operator", "--namespace", s.AppNamespace()) } }() cmd = itest.Command(ctx, "helm", "upgrade", "--install", "--namespace", s.AppNamespace(), "alloy", "grafana/alloy") out, err = cmd.CombinedOutput() s.Require().NoError(err, "helm install", string(out)) defer func() { if err != nil { _ = itest.Run(ctx, "helm", "uninstall", "alloy", "--namespace", s.AppNamespace()) } }() // Wait for cert-manager to be ready. The job might have been completed already. _ = s.Kubectl(ctx, "wait", "--for=condition=complete", "--timeout=60s", "job/cert-manager-startupapicheck") _ = s.RolloutStatusWait(ctx, "deploy/cert-manager-webhook") _ = s.RolloutStatusWait(ctx, "deploy/cert-manager") time.Sleep(30 * time.Second) var instrumentationData []byte instrumentationData, err = os.ReadFile(filepath.Join(otelDir, "instrumentation.yml")) s.Require().NoError(err) instrumentationData = bytes.ReplaceAll(instrumentationData, []byte("__NAMESPACE__"), []byte(s.AppNamespace())) err = s.Kubectl(dos.WithStdin(ctx, bytes.NewReader(instrumentationData)), "apply", "-f", "-") s.Require().NoError(err) var so string so, err = s.TelepresenceHelmInstall(ctx, false, "--set", "logLevel=trace", "--set", "agentInjector.mutationAware=false") s.Require().NoError(err, "telepresence install", so) } func (s *otelSuite) TearDownSuite() { ctx := s.Context() s.UninstallTrafficManager(ctx, s.ManagerNamespace()) cmd := itest.Command(ctx, "helm", "uninstall", "otel-operator", "--namespace", s.AppNamespace()) out, err := cmd.CombinedOutput() s.NoError(err, "helm uninstall otel-operator", string(out)) cmd = itest.Command(ctx, "helm", "uninstall", "cert-manager", "--namespace", s.AppNamespace()) out, err = cmd.CombinedOutput() s.NoError(err, "helm uninstall cert-manager", string(out)) cmd = itest.Command(ctx, "helm", "uninstall", "alloy", "--namespace", s.AppNamespace()) out, err = cmd.CombinedOutput() s.NoError(err, "helm uninstall alloy", string(out)) } ================================================ FILE: integration_test/pod_cidr_test.go ================================================ package integration_test import ( "net/netip" "os" "path/filepath" "sigs.k8s.io/yaml" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" ) type podCIDRSuite struct { itest.Suite itest.NamespacePair } func (s *podCIDRSuite) SuiteName() string { return "PodCIDRStrategy" } func init() { itest.AddNamespacePairSuite("", func(h itest.NamespacePair) itest.TestingSuite { return &podCIDRSuite{Suite: itest.Suite{Harness: h}, NamespacePair: h} }) } func (s *podCIDRSuite) Test_PodCIDRStrategy() { ctx := s.Context() connected := false s.TelepresenceHelmInstallOK(ctx, false) defer func() { if connected { itest.TelepresenceQuitOk(ctx) } s.UninstallTrafficManager(ctx, s.ManagerNamespace()) }() s.TelepresenceConnect(ctx) connected = true si := itest.TelepresenceStatusOk(ctx) itest.TelepresenceQuitOk(ctx) connected = false subnetsAsStrings := func(snn []netip.Prefix) []string { sns := make([]string, len(snn)) for i, sn := range snn { sns[i] = sn.String() } return sns } clog.Infof(ctx, "subnets %v", si.RootDaemon.Subnets) podCIDRs := subnetsAsStrings(si.RootDaemon.Subnets[1:]) tests := []struct { name string values map[string]any wantSubnets []string }{ { "environment", map[string]any{ "podCIDRs": append(podCIDRs, "199.199.50.228/30"), "podCIDRStrategy": "environment", }, append(podCIDRs, "199.199.50.228/30"), }, } vDir := s.T().TempDir() vFile := filepath.Join(vDir, "values.yaml") for _, tt := range tests { s.Run(tt.name, func() { rq := s.Require() vy, err := yaml.Marshal(tt.values) rq.NoError(err) rq.NoError(os.WriteFile(vFile, vy, 0o644)) s.TelepresenceHelmInstallOK(ctx, true, "--no-hooks", "-f", vFile) s.TelepresenceConnect(ctx) connected = true si := itest.TelepresenceStatusOk(ctx) itest.TelepresenceQuitOk(ctx) connected = false rd := si.RootDaemon rq.NotNil(rd) rq.Greater(len(rd.Subnets), 1) sns := subnetsAsStrings(rd.Subnets[1:]) for _, ws := range tt.wantSubnets { s.Contains(sns, ws) } }) } } ================================================ FILE: integration_test/podscaling_test.go ================================================ package integration_test import ( "context" "fmt" "io" "net/http" "os" "path/filepath" "regexp" "strings" "time" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" ) // Test_RestartInterceptedPod build belongs to the interceptMountSuite because we want to // verify that the mount survives the scaling. func (s *interceptMountSuite) Test_RestartInterceptedPod() { assert := s.Assert() require := s.Require() ctx := s.Context() rx := regexp.MustCompile(fmt.Sprintf(`Intercept name\s*: %s\s+State\s*: ([^\n]+)\n`, s.ServiceName())) // Scale down to zero pods require.NoError(s.Kubectl(ctx, "scale", "deploy", s.ServiceName(), "--replicas", "0")) scaleUp := true defer func() { if scaleUp { assert.NoError(s.Kubectl(ctx, "scale", "deploy", s.ServiceName(), "--replicas", "1")) } }() // Wait until the pods have terminated. This might take a long time (several minutes). require.Eventually(func() bool { return len(s.runningPods(ctx)) == 0 }, 2*time.Minute, 6*time.Second) // Verify that intercept remains but that no agent is found. Use require here // to avoid a hanging os.Stat call unless this succeeds. require.Eventually(func() bool { stdout, _, err := itest.Telepresence(ctx, "list") if err != nil { return false } if match := rx.FindStringSubmatch(stdout); match != nil { return match[1] == "WAITING" || strings.Contains(match[1], `No agent found for "`+s.ServiceName()+`"`) } return false }, 30*time.Second, 3*time.Second) // Verify that volume mount is broken time.Sleep(time.Second) // avoid a stat just when the intercept became inactive as it sometimes causes a hang _, err := os.Stat(filepath.Join(s.mountPoint, "var", "run")) assert.Error(err, "Stat on /var succeeded although no agent was found") // Scale up again (start intercepted pod) assert.NoError(s.Kubectl(ctx, "scale", "deploy", s.ServiceName(), "--replicas", "1")) scaleUp = false assert.Eventually(func() bool { return len(s.runningPods(ctx)) == 1 }, itest.PodCreateTimeout(ctx), 6*time.Second) s.CapturePodLogs(ctx, s.ServiceName(), "traffic-agent", s.AppNamespace()) // Verify that intercept becomes active assert.Eventually(func() bool { stdout, _, err := itest.Telepresence(ctx, "list") if err != nil { clog.Errorf(ctx, "%s: %v", stdout, err) return false } if match := rx.FindStringSubmatch(stdout); match != nil { if match[1] == "ACTIVE" { return true } } clog.Info(ctx, stdout) return false }, 30*time.Second, 3*time.Second) // Verify that volume mount is restored time.Sleep(time.Second) // avoid a stat just when the intercept became active as it sometimes causes a hang assert.Eventually(func() bool { st, err := os.Stat(filepath.Join(s.mountPoint, "var", "run")) return err == nil && st.IsDir() }, 5*time.Second, time.Second) } // Test_StopInterceptedPodOfMany build belongs to the interceptMountSuite because we want to // verify that the mount survives the scaling. func (s *interceptMountSuite) Test_StopInterceptedPodOfMany() { assert := s.Assert() require := s.Require() ctx := s.Context() // Wait for exactly one active pod var currentPod string require.Eventually(func() bool { currentPods := s.runningPods(ctx) if len(currentPods) == 1 { currentPod = currentPods[0] return true } return false }, 20*time.Second, 2*time.Second) // Scale up to two pods require.NoError(s.Kubectl(ctx, "scale", "deploy", s.ServiceName(), "--replicas", "2")) defer func() { _ = s.Kubectl(ctx, "scale", "deploy", s.ServiceName(), "--replicas", "1") assert.Eventually( func() bool { return len(s.runningPods(ctx)) == 1 }, time.Minute, time.Second) s.CapturePodLogs(ctx, s.ServiceName(), "traffic-agent", s.AppNamespace()) }() // Wait for second pod to arrive assert.Eventually(func() bool { return len(s.runningPods(ctx)) == 2 }, itest.PodCreateTimeout(ctx), 6*time.Second) s.CapturePodLogs(ctx, s.ServiceName(), "traffic-agent", s.AppNamespace()) // Delete the currently intercepted pod require.NoError(s.Kubectl(ctx, "delete", "pod", currentPod)) // Wait for that pod to disappear and then be recreated assert.Eventually( func() bool { pods := s.runningPods(ctx) for _, zp := range pods { if zp == currentPod { return false } } return len(pods) == 2 }, time.Minute, time.Second) s.CapturePodLogs(ctx, s.ServiceName(), "traffic-agent", s.AppNamespace()) // Verify that intercept is still active rx := regexp.MustCompile(`Intercept name\s*: ` + s.ServiceName() + `\s+State\s*: ([^\n]+)\n`) assert.Eventually(func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--intercepts") if err != nil { return false } clog.Debugf(ctx, "match %q in %q", rx.String(), stdout) if match := rx.FindStringSubmatch(stdout); match != nil { return match[1] == "ACTIVE" } return false }, 15*time.Second, time.Second) // Verify response from intercepting client expect := s.ServiceName() + " from intercept at /" require.Eventually(func() bool { hc := http.Client{Timeout: time.Second} resp, err := hc.Get("http://" + s.ServiceName()) if err != nil { return false } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return false } if expect == string(body) { return true } clog.Infof(ctx, "%s != %s", expect, string(body)) return false }, 30*time.Second, time.Second) // Verify that volume mount is restored time.Sleep(3 * time.Second) // avoid a stat just when the intercept became active as it sometimes causes a hang st, err := os.Stat(filepath.Join(s.mountPoint, "var")) require.NoError(err, "Stat on /var failed") require.True(st.IsDir(), "/var is not a directory") } // Return the names of running pods with app=. Running here means // that at least one container is still running. I.e. the pod might well be terminating // but still considered running. func (s *interceptMountSuite) runningPods(ctx context.Context) []string { return itest.RunningPodNames(ctx, s.ServiceName(), s.AppNamespace()) } ================================================ FILE: integration_test/proxy_via_test.go ================================================ package integration_test import ( "bufio" "fmt" "io" "net" "net/netip" "os" "path/filepath" "regexp" "runtime" "strings" "time" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" ) type proxyViaSuite struct { itest.Suite itest.TrafficManager } func (s *proxyViaSuite) SuiteName() string { return "ProxyVia" } func init() { itest.AddTrafficManagerSuite("", func(h itest.TrafficManager) itest.TestingSuite { return &proxyViaSuite{Suite: itest.Suite{Harness: h}, TrafficManager: h} }) } const ( domain = "mydomain.local" alias = "echo-home" fqnAlias = alias + "." + domain ) func (s *proxyViaSuite) SetupSuite() { if !s.ClientIsVersion(">2.24.x") { s.T().Skip("ProxyVia have some quirks in versions <2.25.0") } s.Suite.SetupSuite() tpl := struct { AliasIP string Aliases []string }{ AliasIP: "127.0.0.1", Aliases: []string{alias, fqnAlias}, } if s.IsIPv6() { tpl.AliasIP = "::1" } s.ApplyTemplate(s.Context(), filepath.Join("testdata", "k8s", "echo-w-hostalias.goyaml"), &tpl) s.NoError(s.RolloutStatusWait(s.Context(), "deploy/echo")) } func (s *proxyViaSuite) TearDownSuite() { s.DeleteSvcAndWorkload(s.Context(), "deploy", "echo") } func (s *proxyViaSuite) Test_ProxyViaLoopBack() { ctx := s.Context() if s.IsIPv6() { ctx = itest.WithConfig(ctx, func(config client.Config) { config.Routing().VirtualSubnet = netip.MustParsePrefix("abac:0de0::/64") }) } s.TelepresenceHelmInstallOK(ctx, true, "--set", "client.dns.includeSuffixes={mydomain.local}") defer s.RollbackTM(ctx) if s.IsIPv6() { s.TelepresenceConnect(ctx, "--proxy-via", "::1/128=echo") } else { s.TelepresenceConnect(ctx, "--proxy-via", "127.0.0.1/32=echo") } defer itest.TelepresenceQuitOk(ctx) s.CapturePodLogs(ctx, "echo", "traffic-agent", s.AppNamespace()) virtualSubnet := client.GetConfig(ctx).Routing().VirtualSubnet tests := []struct { name string hostName string expectedOutput *regexp.Regexp }{ { "single-label", alias, regexp.MustCompile("Host: " + alias + ":8080"), }, { "fully-qualified", fqnAlias, regexp.MustCompile("Host: " + fqnAlias + ":8080"), }, } for _, tt := range tests { s.Run(tt.name, func() { rq := s.Require() var vip netip.Addr rq.Eventually(func() bool { // hostname will resolve to 127.0.0.1 remotely and then be translated into a virtual IP ips, err := net.LookupIP(tt.hostName) if err != nil { clog.Error(ctx, err) return false } if len(ips) != 1 { clog.Error(ctx, "LookupIP did not return one IP") return false } var ok bool vip, ok = netip.AddrFromSlice(ips[0]) return ok }, 30*time.Second, 2*time.Second) clog.Infof(ctx, "%s uses IP %s", tt.hostName, vip) rq.Truef(virtualSubnet.Contains(vip), "virtualIPSubnet %s does not contain %s", virtualSubnet, vip) rq.Eventually(func() bool { out, err := itest.Output(ctx, "curl", "--silent", "--max-time", "2", net.JoinHostPort(tt.hostName, "8080")) clog.Info(ctx, out) return err == nil && tt.expectedOutput.MatchString(out) }, 10*time.Second, 2*time.Second) }) } } func (s *proxyViaSuite) Test_ProxyViaEverything() { ctx := s.Context() s.TelepresenceConnect(ctx) st := itest.TelepresenceStatusOk(ctx) itest.TelepresenceDisconnectOk(ctx) sns := st.RootDaemon.Subnets if len(sns) == 0 { s.T().Skip("Test cannot run unless client maps at least one subnet") } if s.IsIPv6() { ctx = itest.WithConfig(ctx, func(config client.Config) { config.Routing().VirtualSubnet = netip.MustParsePrefix("abac:0de0::/64") }) } args := make([]string, 0, len(sns)*2) for _, sn := range sns { args = append(args, "--proxy-via", sn.String()+"=echo") } s.TelepresenceConnect(ctx, args...) st = itest.TelepresenceStatusOk(ctx) defer itest.TelepresenceDisconnectOk(ctx) rq := s.Require() rq.NotNil(st.RootDaemon) rq.Len(st.RootDaemon.Subnets, 1) // Virtual subnet rq.Eventually(func() bool { out, err := itest.Output(ctx, "curl", "--silent", "--max-time", "2", "echo") clog.Infof(ctx, "Output from echo service %s", out) return err == nil }, 10*time.Second, 2*time.Second) } func (s *proxyViaSuite) Test_ProxyViaAll() { ctx := s.Context() rq := s.Require() if s.IsIPv6() { ctx = itest.WithConfig(ctx, func(config client.Config) { config.Routing().VirtualSubnet = netip.MustParsePrefix("abac:0de0::/64") }) } s.TelepresenceConnect(ctx, "--proxy-via", "all=echo") st := itest.TelepresenceStatusOk(ctx) defer itest.TelepresenceDisconnectOk(ctx) rq.NotNil(st.RootDaemon) rq.Len(st.RootDaemon.Subnets, 1) // Virtual subnet rq.Eventually(func() bool { out, err := itest.Output(ctx, "curl", "--silent", "--max-time", "2", "echo") clog.Infof(ctx, "Output from echo service %s", out) return err == nil }, 30*time.Second, 2*time.Second) } func (s *proxyViaSuite) Test_ProxyViaAllAndMounts() { if s.IsCI() && runtime.GOOS == "darwin" { s.T().Skip("CI can't do user mounts on darwin") } ctx := s.Context() rq := s.Require() if s.IsIPv6() { ctx = itest.WithConfig(ctx, func(config client.Config) { config.Routing().VirtualSubnet = netip.MustParsePrefix("abac:0de0::/64") }) } s.TelepresenceConnect(ctx, "--proxy-via", "all=echo") st := itest.TelepresenceStatusOk(ctx) defer itest.TelepresenceDisconnectOk(ctx) rq.NotNil(st.RootDaemon) rq.Len(st.RootDaemon.Subnets, 1) var mountPoint string if runtime.GOOS == "windows" { mountPoint = "T:" } else { var err error mountPoint, err = os.MkdirTemp("", "mount-") // Don't use the testing.Tempdir() because deletion is delayed. s.Require().NoError(err) defer func() { time.AfterFunc(time.Second, func() { _ = os.RemoveAll(mountPoint) }) }() } itest.TelepresenceOk(ctx, "intercept", "--mount", mountPoint, "echo") // Verify that volume mount is present and functional time.Sleep(3 * time.Second) // avoid a stat just when the intercept became active as it sometimes causes a hang fst, err := os.Stat(filepath.Join(mountPoint, "var")) rq.NoError(err, "Stat on /var failed") rq.True(fst.IsDir()) } func (s *proxyViaSuite) Test_NeverProxySubnetIsOmitted() { ctx := s.Context() s.TelepresenceConnect(ctx) st := itest.TelepresenceStatusOk(ctx) itest.TelepresenceDisconnectOk(ctx) sns := st.RootDaemon.Subnets if len(sns) == 0 { s.T().Skip("Test cannot run unless client maps at least one subnet") } logFile := filepath.Join(filelocation.AppUserLogDir(s.Context()), "daemon.log") rootLog, err := os.Open(logFile) s.Require().NoError(err) defer rootLog.Close() for _, sn := range sns { s.Run("subnet "+sn.String(), func() { ctx := s.Context() rq := s.Require() fs, err := rootLog.Stat() rq.NoError(err) pos := fs.Size() s.TelepresenceConnect(ctx, "--never-proxy", sn.String()) itest.TelepresenceDisconnectOk(ctx) _, err = rootLog.Seek(pos, io.SeekStart) rq.NoError(err) scn := bufio.NewScanner(rootLog) found := false msg := fmt.Sprintf("Dropping never-proxy %q", sn.String()) for scn.Scan() { txt := scn.Text() if strings.Contains(txt, msg) { found = true break } } s.Truef(found, "Unable to find %q", msg) }) } s.Run(fmt.Sprintf("subnets %s", sns), func() { ctx := s.Context() rq := s.Require() fs, err := rootLog.Stat() rq.NoError(err) pos := fs.Size() sb := strings.Builder{} for i, sn := range sns { if i > 0 { sb.WriteByte(',') } sb.WriteString(sn.String()) } s.TelepresenceConnect(ctx, "--never-proxy", sb.String()) itest.TelepresenceDisconnectOk(ctx) for _, sn := range sns { _, err = rootLog.Seek(pos, io.SeekStart) rq.NoError(err) scn := bufio.NewScanner(rootLog) found := false msg := fmt.Sprintf("Dropping never-proxy %q", sn.String()) for scn.Scan() { txt := scn.Text() if strings.Contains(txt, msg) { found = true break } } s.Truef(found, "Unable to find %q", msg) } }) } ================================================ FILE: integration_test/reconnect_session_test.go ================================================ package integration_test import ( "net" "net/url" "strconv" "sync" "sync/atomic" "time" "k8s.io/client-go/tools/clientcmd" "github.com/telepresenceio/telepresence/v2/integration_test/itest" ) type reconnectSuite struct { itest.Suite itest.TrafficManager iptablesChain string svc string } func (s *reconnectSuite) SuiteName() string { return "Reconnect" } func init() { itest.AddTrafficManagerSuite("", func(h itest.TrafficManager) itest.TestingSuite { return &reconnectSuite{ Suite: itest.Suite{Harness: h}, TrafficManager: h, iptablesChain: "TEL2-CI", svc: "echo-easy", } }) } func (s *reconnectSuite) SetupSuite() { if !s.ClientIsVersion(">2.21.x") { s.T().Skip(`Not part of compatibility tests. Reconnect doesn't work well in Versions < 2.22.0`) } s.Suite.SetupSuite() // Add our iptables table, disable tests if we're unable to. err := itest.Run(s.Context(), "sudo", "iptables", "-t", "filter", "-N", s.iptablesChain) if err != nil { s.T().Skipf("Skipped, because sudo iptables fails: %v", err) } ctx := s.Context() s.ApplyApp(ctx, s.svc, "deploy/"+s.svc) } func (s *reconnectSuite) TearDownSuite() { ctx := s.Context() s.DeleteSvcAndWorkload(ctx, "deploy", s.svc) s.NoError(itest.Run(ctx, "sudo", "iptables", "-t", "filter", "-X", s.iptablesChain)) } func (s *reconnectSuite) Test_ReconnectAfterNetworkFailure() { ctx := s.Context() rq := s.Require() kc := itest.KubeConfig(ctx) cfg, err := clientcmd.LoadFromFile(kc) rq.NoError(err) // Front the Kubernetes API with a socat, so that we can break the connection. c, ok := cfg.Contexts[cfg.CurrentContext] rq.True(ok) cl, ok := cfg.Clusters[c.Cluster] rq.True(ok) su, err := url.Parse(cl.Server) rq.NoError(err) ips, err := net.LookupIP(su.Hostname()) rq.NoError(err) rq.Len(ips, 1) kubeIP := ips[0] // Set up a chain that will drop all tcp packages destined for the kubernetes server. rq.NoError(itest.Run(ctx, "sudo", "iptables", "-t", "filter", "-A", s.iptablesChain, "-p", "tcp", "-d", kubeIP.String(), "--dport", su.Port(), "-j", "DROP", )) defer func() { // Flush (i.e. clear) the chain s.NoError(itest.Run(ctx, "sudo", "iptables", "-t", "filter", "-F", s.iptablesChain)) }() port, cancel := itest.StartLocalHttpEchoServer(ctx, s.svc) defer cancel() s.TelepresenceConnect(ctx) defer itest.TelepresenceQuitOk(ctx) itest.TelepresenceOk(ctx, "intercept", "--port", strconv.Itoa(port), "--mount=false", s.svc) // Send all tcp-packets in the OUTPUT chain to our iptablesChain. This effectively interrupts all communication // going from the telepresence daemons to the kubernetes server. rq.NoError(itest.Run(ctx, "sudo", "iptables", "-t", "filter", "-I", "OUTPUT", "-p", "tcp", "-j", s.iptablesChain)) var restore atomic.Bool restore.Store(true) wg := &sync.WaitGroup{} wg.Add(1) restoreOutput := func() { if restore.CompareAndSwap(true, false) { wg.Done() s.NoError(itest.Run(ctx, "sudo", "iptables", "-t", "filter", "-D", "OUTPUT", "-p", "tcp", "-j", s.iptablesChain)) } } defer restoreOutput() // Restore connection after 7 seconds. This ensures that the first ping will fail. time.AfterFunc(7*time.Second, restoreOutput) // Ping the intercepted service. The first ping will certainly fail, but eventually the connection will be restored. itest.PingInterceptedEchoServer(ctx, s.svc, "80") wg.Wait() } ================================================ FILE: integration_test/replace_test.go ================================================ package integration_test import ( "fmt" "path/filepath" "time" core "k8s.io/api/core/v1" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/annotation" ) type replaceSuite struct { itest.Suite itest.TrafficManager svc string tplPath string tpl *itest.Generic } func (s *replaceSuite) SuiteName() string { return "Replace" } func init() { itest.AddConnectedSuite("", func(h itest.TrafficManager) itest.TestingSuite { return &replaceSuite{Suite: itest.Suite{Harness: h}, TrafficManager: h, svc: "echo-server"} }) } func (s *replaceSuite) SetupSuite() { if !(s.ManagerIsVersion(">2.21.x") && s.ClientIsVersion(">2.21.x")) { s.T().Skip("Not part of compatibility tests. The replace command was introduced in 2.22") } s.Suite.SetupSuite() s.tplPath = filepath.Join("testdata", "k8s", "generic.goyaml") s.tpl = &itest.Generic{ Name: s.svc, TargetPort: "http", Registry: "ghcr.io/telepresenceio", Image: "echo-server:latest", ServicePorts: []itest.ServicePort{ { Number: 80, Name: "http", TargetPort: "http-cp", }, { Number: 81, Name: "extra", TargetPort: "extra-cp", }, }, ContainerPorts: []itest.ContainerPort{ { Number: 8080, Name: "http-cp", }, { Number: 8081, Name: "extra-cp", }, }, Environment: []core.EnvVar{ { Name: "PORTS", Value: "8080,8081", }, { Name: "LISTEN_ADDRESS", ValueFrom: &core.EnvVarSource{ FieldRef: &core.ObjectFieldSelector{ FieldPath: "status.podIP", }, }, }, }, Annotations: map[string]string{ annotation.InjectTrafficAgent: "enabled", }, } ctx := s.Context() s.ApplyTemplate(ctx, s.tplPath, s.tpl) s.NoError(s.RolloutStatusWait(ctx, "deploy/"+s.svc)) } func (s *replaceSuite) TearDownSuite() { s.DeleteTemplate(s.Context(), s.tplPath, s.tpl) } func (s *replaceSuite) Test_ReplaceWithMultiContainerPorts() { ctx := s.Context() _, httpCancel := itest.StartLocalHttpEchoServerWithAddr(ctx, s.svc+"-http", "localhost:8080", nil) defer httpCancel() _, extraCancel := itest.StartLocalHttpEchoServerWithAddr(ctx, s.svc+"-extra", "localhost:8081", nil) defer extraCancel() // Use container port names here. so := itest.TelepresenceOk(ctx, "replace", s.svc) mustLeave := true defer func() { if mustLeave { itest.TelepresenceOk(ctx, "leave", s.svc) } }() rq := s.Require() rq.Contains(so, "Using Deployment "+s.svc) itest.PingInterceptedEchoServer(ctx, fmt.Sprintf("%s/%s-http", s.svc, s.svc), "80") itest.PingInterceptedEchoServer(ctx, fmt.Sprintf("%s/%s-extra", s.svc, s.svc), "81") itest.TelepresenceOk(ctx, "leave", s.svc) mustLeave = false // Ensure that we now reach the original app again. s.Eventually(func() bool { out, err := itest.Output(ctx, "curl", "--verbose", "--max-time", "1", s.svc) clog.Infof(ctx, "Received %s", out) if err != nil { clog.Errorf(ctx, "curl error %v", err) return false } return true }, 30*time.Second, 2*time.Second, "Pod app is not reachable after ending the replace") } ================================================ FILE: integration_test/restapi_test.go ================================================ package integration import ( "context" "encoding/json" "fmt" "runtime" "strconv" "sync" "time" core "k8s.io/api/core/v1" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/annotation" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/restapi" ) // request is the JSON structure used by the echo-server /forward endpoint. type request struct { URL string `json:"url"` Headers map[string]string `json:"headers,omitempty"` Method string `json:"method,omitempty"` Body string `json:"body,omitempty"` } type restAPISuite struct { itest.Suite itest.NamespacePair svc string registry string image string apiPort uint16 daemonName string } func (s *restAPISuite) SuiteName() string { return "RESTful-API" } func init() { itest.AddNamespacePairSuite("", func(h itest.NamespacePair) itest.TestingSuite { return &restAPISuite{ Suite: itest.Suite{Harness: h}, NamespacePair: h, svc: "hello", apiPort: 9980, daemonName: "alpha", registry: "ghcr.io/telepresenceio", image: "echo-server:latest", } }) } func (s *restAPISuite) SetupSuite() { if !(s.ManagerIsVersion(">2.24.x") && s.ClientIsVersion(">2.24.x")) { s.T().Skip("Not part of compatibility suite") } if s.IsCI() && runtime.GOOS != "linux" { s.T().Skip("CI can't run linux docker containers inside non-linux runners") return } s.Suite.SetupSuite() ctx := s.Context() s.TelepresenceHelmInstallOK(ctx, false, "--set", fmt.Sprintf("telepresenceAPI.port=%d", s.apiPort)) dep := &itest.Generic{ Name: s.svc, Registry: s.registry, Image: s.image, Environment: []core.EnvVar{ { Name: "TELEPRESENCE_API_PORT", Value: strconv.Itoa(int(s.apiPort)), }, { Name: "PORTS", Value: "8080,8081", }, }, Annotations: map[string]string{ annotation.InjectTrafficAgent: "enabled", }, ServicePorts: []itest.ServicePort{ { Number: 80, Name: "http", TargetPort: "http", }, { Number: 81, Name: "check", TargetPort: "check", }, }, ContainerPorts: []itest.ContainerPort{ { Name: "http", Number: 8080, }, { Name: "check", Number: 8081, }, }, } s.NoError(dep.Apply(ctx, s.AppNamespace())) s.CapturePodLogs(ctx, s.svc, "traffic-agent", s.AppNamespace()) s.CapturePodLogs(ctx, s.svc, "", s.AppNamespace()) s.TelepresenceConnect(ctx, "--docker", "--name", s.daemonName) } func (s *restAPISuite) TearDownSuite() { ctx := s.Context() itest.TelepresenceQuit(ctx) s.KubectlOk(ctx, "delete", "svc,deploy", s.svc) s.UninstallTrafficManager(ctx, s.ManagerNamespace()) } func (s *restAPISuite) AmendSuiteContext(ctx context.Context) context.Context { return itest.WithConfig(ctx, func(cfg client.Config) { cfg.Intercept().UseFtp = false }) } func (s *restAPISuite) curlAPIServer(ctx context.Context, port uint16, path string, headers map[string]string, myArgs ...string) (string, error) { apiReq := &request{ Headers: headers, URL: fmt.Sprintf("http://${TELEPRESENCE_API_HOST}:${TELEPRESENCE_API_PORT}/%s?containerPort=%d", path, port), } apiReqJSON, err := json.Marshal(apiReq) if err != nil { return "", fmt.Errorf("failed to marshal request: %w", err) } args := append([]string{ "curl", "--silent", "--max-time", "2", "-H", "Content-Type: application/json", "--request", "POST", "--data", string(apiReqJSON), fmt.Sprintf("http://%s/forward", s.svc), }, myArgs...) so, se, err := itest.Telepresence(ctx, args...) if se != "" { clog.Errorf(ctx, "stderr: %s", se) } return so, err } func (s *restAPISuite) startIntercept(ctx context.Context, meta, header string, errCh chan<- error, wg *sync.WaitGroup) { defer wg.Done() defer close(errCh) args := make([]string, 0, 15) args = append(args, "intercept", s.svc) if meta != "" { args = append(args, "--metadata", meta) } if header != "" { args = append(args, "--http-header", header) } args = append(args, "--port", "8080:80", "--mount=false", "--docker-run", "--", "--rm", "--name", s.svc+"-local", s.registry+"/"+s.image) so, se, err := itest.Telepresence(ctx, args...) if so != "" { clog.Infof(ctx, "stdout: %s", so) } if se != "" { clog.Errorf(ctx, "stderr: %s", se) } if err != nil { errCh <- err } } func (s *restAPISuite) waitForInterceptReady(ctx context.Context, errCh <-chan error) error { ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop() for { select { case <-ctx.Done(): return ctx.Err() case err := <-errCh: return err case <-ticker.C: st, err := itest.TelepresenceStatus(ctx) if err != nil { return err } for _, ic := range st.ContainerizedDaemon.Intercepts { if ic.Name == s.svc { return nil } } } } } func (s *restAPISuite) Test_RestAPI_GlobalConsume() { ctx := s.Context() rq := s.Require() so, err := s.curlAPIServer(ctx, 8080, "consume-here", nil) rq.NoError(err) clog.Infof(ctx, "stdout: %s", so) var jv any err = json.Unmarshal([]byte(so), &jv) rq.NoError(err) yes, ok := jv.(bool) rq.True(ok) rq.True(yes) // When exiting, first cancel the intercept, then wait for it to exit. wg := &sync.WaitGroup{} iCtx, cancel := context.WithCancel(ctx) defer func() { cancel() wg.Wait() }() wg.Add(1) errCh := make(chan error, 1) go s.startIntercept(iCtx, "", "", errCh, wg) rq.NoError(s.waitForInterceptReady(iCtx, errCh)) defer itest.TelepresenceOk(ctx, "leave", s.svc) so, err = s.curlAPIServer(ctx, 8080, "consume-here", map[string]string{restapi.HeaderCallerInterceptID: "${TELEPRESENCE_INTERCEPT_ID}"}) rq.NoError(err) jv = nil err = json.Unmarshal([]byte(so), &jv) rq.NoError(err) clog.Infof(ctx, "stdout: %s", so) // A global intercept will always hit the client, and the client's API server will always return true for the intercept id. yes, ok = jv.(bool) rq.True(ok) rq.True(yes) } func (s *restAPISuite) Test_RestAPI_GlobalInfo() { ctx := s.Context() rq := s.Require() so, err := s.curlAPIServer(ctx, 8080, "intercept-info", nil) rq.NoError(err) clog.Infof(ctx, "stdout: %s", so) var jv any err = json.Unmarshal([]byte(so), &jv) rq.NoError(err) info, ok := jv.(map[string]any) rq.True(ok) yes, ok := info["intercepted"].(bool) rq.True(ok) s.False(yes) wg := &sync.WaitGroup{} iCtx, cancel := context.WithCancel(ctx) defer func() { cancel() wg.Wait() }() wg.Add(1) errCh := make(chan error, 1) go s.startIntercept(iCtx, "my:data", "", errCh, wg) rq.NoError(s.waitForInterceptReady(iCtx, errCh)) defer itest.TelepresenceOk(ctx, "leave", s.svc) so, err = s.curlAPIServer(ctx, 8080, "intercept-info", map[string]string{restapi.HeaderCallerInterceptID: "${TELEPRESENCE_INTERCEPT_ID}"}) rq.NoError(err) jv = nil err = json.Unmarshal([]byte(so), &jv) rq.NoError(err) clog.Infof(ctx, "stdout: %s", so) info, ok = jv.(map[string]any) rq.True(ok) yes, ok = info["intercepted"].(bool) rq.True(ok) s.True(yes) data, ok := info["metadata"].(map[string]any) rq.True(ok) s.Equal("data", data["my"]) } func (s *restAPISuite) Test_RestAPI_FilteredConsume() { ctx := s.Context() wg := &sync.WaitGroup{} iCtx, cancel := context.WithCancel(ctx) defer func() { cancel() wg.Wait() }() wg.Add(1) errCh := make(chan error, 1) go s.startIntercept(iCtx, "", "x:y", errCh, wg) s.Require().NoError(s.waitForInterceptReady(iCtx, errCh)) defer itest.TelepresenceOk(ctx, "leave", s.svc) tts := []struct { name string containerPort uint16 apiHeaders map[string]string args []string expected bool }{ // Query the remote API server without any headers. It must respond true because this doesn't match any intercept. { name: "query-remote-without-headers", containerPort: 8080, expected: true, }, // Query the remote API server with the intercepted header. The intercept is matched, so the remote API server // should tell the remote app to not consume the request. { name: "query-remote-with-headers", containerPort: 8080, apiHeaders: map[string]string{"x": "y"}, expected: false, }, // Query the local API server without the intercepted header. The intercept is not matched, so the local API server // should tell the local app to not consume the request. { name: "query-local-without-headers", containerPort: 8080, expected: false, apiHeaders: map[string]string{restapi.HeaderCallerInterceptID: "${TELEPRESENCE_INTERCEPT_ID}"}, args: []string{"-H", "x:y"}, }, // Query the local API server with the intercepted header. The intercept is matched, so the local API server // should tell the local app to consume the request. { name: "query-local-with-headers", containerPort: 8080, apiHeaders: map[string]string{restapi.HeaderCallerInterceptID: "${TELEPRESENCE_INTERCEPT_ID}", "x": "y"}, args: []string{"-H", "x:y"}, expected: true, }, } for _, tt := range tts { s.Run(tt.name, func() { ctx := s.Context() rq := s.Require() so, err := s.curlAPIServer(ctx, tt.containerPort, "consume-here", tt.apiHeaders, tt.args...) rq.NoError(err) clog.Infof(ctx, "stdout: %s", so) var jv any err = json.Unmarshal([]byte(so), &jv) rq.NoError(err) yes, ok := jv.(bool) rq.True(ok) s.Equal(tt.expected, yes) }) } } func (s *restAPISuite) Test_RestAPI_FilteredInfo() { ctx := s.Context() wg := &sync.WaitGroup{} iCtx, cancel := context.WithCancel(ctx) defer func() { cancel() wg.Wait() }() wg.Add(1) errCh := make(chan error, 1) go s.startIntercept(iCtx, "my:data", "x:y", errCh, wg) s.Require().NoError(s.waitForInterceptReady(iCtx, errCh)) defer itest.TelepresenceOk(ctx, "leave", s.svc) tts := []struct { name string containerPort uint16 apiHeaders map[string]string args []string intercepted bool clientSide bool myMetadata string }{ // Query the remote API server without any headers. It must respond with client-side=false and intercepted=false because this doesn't match any intercept.' { name: "query-remote-without-headers", containerPort: 8080, intercepted: false, clientSide: false, myMetadata: "", }, // Query the remote API server with the intercepted header. It must respond with client-side=false and intercepted=true because the remote app is intercepted. { name: "query-remote-with-headers", containerPort: 8080, apiHeaders: map[string]string{"x": "y"}, intercepted: true, clientSide: false, myMetadata: "data", }, // Query the local API server without the intercepted header. The local API server must respond with client-side=true and intercepted=false. { name: "query-local-without-headers", containerPort: 8080, apiHeaders: map[string]string{restapi.HeaderCallerInterceptID: "${TELEPRESENCE_INTERCEPT_ID}"}, intercepted: false, clientSide: true, args: []string{"-H", "x:y"}, myMetadata: "", }, // Query the local API server with the intercepted header. The local API server must respond with client-side=true and intercepted=true { name: "query-local-with-headers", containerPort: 8080, apiHeaders: map[string]string{restapi.HeaderCallerInterceptID: "${TELEPRESENCE_INTERCEPT_ID}", "x": "y"}, intercepted: true, clientSide: true, args: []string{"-H", "x:y"}, myMetadata: "data", }, } for _, tt := range tts { s.Run(tt.name, func() { ctx := s.Context() rq := s.Require() so, err := s.curlAPIServer(ctx, tt.containerPort, "intercept-info", tt.apiHeaders, tt.args...) rq.NoError(err) clog.Infof(ctx, "stdout: %s", so) var jv any err = json.Unmarshal([]byte(so), &jv) rq.NoError(err) clog.Infof(ctx, "stdout: %s", so) info, ok := jv.(map[string]any) rq.True(ok) yes, ok := info["intercepted"].(bool) rq.True(ok) s.Equal(tt.intercepted, yes) // All intercepts have the same metadata. data, ok := info["metadata"].(map[string]any) if tt.myMetadata != "" { rq.True(ok) s.Equal(tt.myMetadata, data["my"]) } else { s.False(ok) } }) } } ================================================ FILE: integration_test/single_service_test.go ================================================ package integration_test import ( "github.com/telepresenceio/telepresence/v2/integration_test/itest" ) type singleServiceSuite struct { itest.Suite itest.SingleService } func (s *singleServiceSuite) SuiteName() string { return "SingleService" } func init() { itest.AddSingleServiceSuite("", "echo", func(h itest.SingleService) itest.TestingSuite { return &singleServiceSuite{Suite: itest.Suite{Harness: h}, SingleService: h} }) } ================================================ FILE: integration_test/subdomain_test.go ================================================ package integration_test import ( "context" "net" "time" "github.com/telepresenceio/clog" ) func (s *connectedSuite) Test_PodWithSubdomain() { c := s.Context() s.ApplyApp(c, "echo-w-subdomain", "deploy/echo-subsonic") defer func() { s.NoError(s.Kubectl(c, "delete", "svc", "subsonic")) s.NoError(s.Kubectl(c, "delete", "deploy", "echo-subsonic")) }() lookupHost := func(host string) { var err error s.Eventually(func() bool { c, cancel := context.WithTimeout(c, 1800*time.Millisecond) defer cancel() clog.Info(c, "LookupHost("+host+")") _, err = net.DefaultResolver.LookupHost(c, host) return err == nil }, 10*time.Second, 2*time.Second, "%s did not resolve: %v", host, err) } lookupHost("echo.subsonic." + s.AppNamespace()) lookupHost("echo.subsonic." + s.AppNamespace() + ".svc.cluster.local") } ================================================ FILE: integration_test/svcdomain_test.go ================================================ package integration_test import ( "context" "fmt" "net" "time" "github.com/telepresenceio/clog" ) func (s *connectedSuite) Test_SvcDomain() { c := s.Context() s.ApplyEchoService(c, "echo", 8080) defer s.DeleteSvcAndWorkload(c, "deploy", "echo") host := fmt.Sprintf("echo.%s.svc", s.AppNamespace()) s.Eventuallyf(func() bool { c, cancel := context.WithTimeout(c, 1800*time.Millisecond) defer cancel() clog.Info(c, "LookupHost("+host+")") _, err := net.DefaultResolver.LookupHost(c, host) return err == nil }, 10*time.Second, 2*time.Second, "%s did not resolve", host) } ================================================ FILE: integration_test/testdata/apiserveraccess/main.go ================================================ package main import ( "context" "fmt" "net" "net/http" "net/url" "os" "os/signal" "strconv" "strings" "github.com/go-json-experiment/json" "golang.org/x/sys/unix" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/pkg/agentconfig" "github.com/telepresenceio/telepresence/v2/pkg/log" "github.com/telepresenceio/telepresence/v2/pkg/matcher" "github.com/telepresenceio/telepresence/v2/pkg/restapi" ) // This service is meant for testing the cluster side Telepresence API service. // // Publish image to cluster: // // ko publish -B ./integration_test/testdata/apiserveraccess [--insecure-registry] // // Deploy it: // // kubectl apply -f ./k8s/apitest.yaml // // Run it locally using an intercept with -- so that TELEPRESENCE_INTERCEPT_ID is propagated in the env func main() { c, cancel := context.WithCancel(log.MakeBaseLogger(context.Background(), os.Stderr, "DEBUG")) sigs := make(chan os.Signal, 1) signal.Notify(sigs, os.Interrupt, unix.SIGTERM) defer func() { signal.Stop(sigs) cancel() }() go func() { select { case <-sigs: cancel() case <-c.Done(): } <-sigs os.Exit(1) }() if err := run(c); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func run(c context.Context) error { if lv, ok := os.LookupEnv("LOG_LEVEL"); ok { clog.SetTreeLevel(c, clog.MustParseLevel(lv)) } ap, ok := os.LookupEnv("APP_PORT") if !ok { ap = "8080" } _, err := strconv.ParseUint(ap, 10, 16) if err != nil { return fmt.Errorf("the value %q of env APP_PORT is not a valid port number", ap) } ln, err := net.Listen("tcp", "localhost:"+ap) if err != nil { return err } mux := http.NewServeMux() mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) if apiUrl, err := apiURL(); err == nil { consumeHereURL := apiUrl + "/consume-here" interceptInfoURL := apiUrl + "/intercept-info" mux.HandleFunc("/consume-here", func(w http.ResponseWriter, r *http.Request) { var b bool intercepted(c, consumeHereURL, r.FormValue("path"), w, r, &b) }) mux.HandleFunc("/intercept-info", func(w http.ResponseWriter, r *http.Request) { ii := restapi.InterceptInfo{} intercepted(c, interceptInfoURL, r.FormValue("path"), w, r, &ii) }) } else { mux.HandleFunc("/consume-here", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _ = json.MarshalWrite(w, false) }) mux.HandleFunc("/intercept-info", func(w http.ResponseWriter, r *http.Request) { ii := restapi.InterceptInfo{} _ = json.MarshalWrite(w, &ii) }) } info := fmt.Sprintf("API test server on %v", ln.Addr()) clog.Infof(c, "%s started", info) defer clog.Infof(c, "%s ended", info) svc := http.Server{Handler: mux, BaseContext: func(listener net.Listener) context.Context { return c }} go func() { _ = svc.Serve(ln) }() <-c.Done() return svc.Shutdown(context.Background()) } const interceptIdEnv = "TELEPRESENCE_INTERCEPT_ID" // apiURL creates the generic URL needed to access the service func apiURL() (string, error) { pe := os.Getenv(agentconfig.EnvAPIPort) if _, err := strconv.ParseUint(pe, 10, 16); err != nil { return "", fmt.Errorf("value %q of env %s does not represent a valid port number", pe, agentconfig.EnvAPIPort) } return "http://localhost:" + pe, nil } // doRequest calls the consume-here endpoint with the given headers and returns the result func doRequest(c context.Context, rqUrl string, path string, hm map[string]string, objTemplate any, er *restapi.ErrorResponse) (int, error) { rq, err := http.NewRequest("GET", rqUrl+"?path="+url.QueryEscape(path), nil) if err != nil { return 0, err } rq.Header = make(http.Header, len(hm)+1) rq.Header.Set("X-Telepresence-Caller-Intercept-Id", os.Getenv(interceptIdEnv)) for k, v := range hm { rq.Header.Set(k, v) } clog.Debugf(c, "%s with headers\n%s", rqUrl, matcher.HeaderStringer(rq.Header)) rs, err := http.DefaultClient.Do(rq) if err != nil { return 0, err } defer rs.Body.Close() if rs.StatusCode == http.StatusOK { err = json.UnmarshalRead(rs.Body, objTemplate) } else { // Make an attempt to decode a json error. _ = json.UnmarshalRead(rs.Body, er) } return rs.StatusCode, err } func intercepted(c context.Context, url string, path string, w http.ResponseWriter, r *http.Request, objTemplate any) { hm := make(map[string]string, len(r.Header)) // The "X-With-" prefix is used as a backdoor to avoid triggering intercepts during test. It's // stripped off here. for h := range r.Header { hm[strings.TrimPrefix(h, "X-With-")] = r.Header.Get(h) } // The "X-Without-" prefix is used when the headers that trigger an intercept must be included in // order for the intercept to take place, but should be removed in the subsequent query. Both // the "X-Without-" headers and the header they refer to are stripped off here. for h := range hm { if hw := strings.TrimPrefix(h, "X-Without-"); h != hw { delete(hm, h) delete(hm, hw) } } w.Header().Set("Content-Type", "application/json") er := restapi.ErrorResponse{} if status, err := doRequest(c, url, path, hm, objTemplate, &er); err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "failed to execute http request: %v", err) } else { w.WriteHeader(status) if status == http.StatusOK { err = json.MarshalWrite(w, objTemplate) } else if er.Error != "" { err = json.MarshalWrite(w, er) } if err != nil { clog.Error(c, err) } } } ================================================ FILE: integration_test/testdata/cli-container/Dockerfile ================================================ FROM telepresence RUN apk add --no-cache bash curl ENTRYPOINT ["/bin/bash"] ================================================ FILE: integration_test/testdata/count-server/main.go ================================================ package main import ( "fmt" "log" "net/http" "sync/atomic" ) func main() { counter := int64(0) http.HandleFunc("/count", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, atomic.LoadInt64(&counter)) }) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { atomic.AddInt64(&counter, 1) }) log.Fatal(http.ListenAndServe(":8080", nil)) } ================================================ FILE: integration_test/testdata/echo-server/Dockerfile ================================================ FROM golang:alpine AS base # Build Delve RUN go install github.com/go-delve/delve/cmd/dlv@latest WORKDIR /echo-server COPY go.mod . COPY go.sum . # Get dependencies - will also be cached if we won't change mod/sum RUN go mod download COPY main.go . FROM base AS builder RUN go build -o echo-server . FROM base AS development RUN go build -gcflags="all=-N -l" -o echo-server . EXPOSE 40000 CMD ["/go/bin/dlv", "--listen=:40000", "--headless=true", "--api-version=2", "--accept-multiclient", "exec", "/echo-server/echo-server"] FROM alpine AS production COPY --from=builder /echo-server/echo-server / COPY certs certs CMD ["/echo-server"] ================================================ FILE: integration_test/testdata/echo-server/certs/tls.crt ================================================ -----BEGIN CERTIFICATE----- MIIDZTCCAk2gAwIBAgIURwxYS26yunKI6OcJh4loNDXvguMwDQYJKoZIhvcNAQEL BQAwNDEYMBYGA1UEAwwPdGVsZXByZXNlbmNlLmlvMRgwFgYDVQQKDA90ZWxlcHJl c2VuY2UuaW8wHhcNMjUwOTI3MDEzODEwWhcNMjYwOTI3MDEzODEwWjA0MRgwFgYD VQQDDA90ZWxlcHJlc2VuY2UuaW8xGDAWBgNVBAoMD3RlbGVwcmVzZW5jZS5pbzCC ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIhAUP3zIQTmXtbJWAv/wX9u 1iFVTS/1ty8pVdS3yPWngZbd3SGDdtHkm0Lis/PDBiP2IWHjHQP8YC332J3vgiH2 nSTJxXAlJz0kpJyT0Z1NV3d1It+1QBnh5FFN5XvZYZ6poE+EMlG2nKCA9g0O8xHg eOQXPUWN1PsjZtU6bmvGuzWyzgKadZZztA9TY0nQIbGljCoYrBB7/lfcoIsz1IHc a+wUlkeKgoZJ5L+xMx5tkNlpBPtCOP1Le/xu1MHPFKTJd5G08gzb/uRGJ4tnXtR0 AYd57wTMSs7lLS+cfzySCuu10t/Emql1HtJGQ0EdzjCBYjzbGFTo8u9mS3XxIWMC AwEAAaNvMG0wHQYDVR0OBBYEFMfgpuHxQF45KO2YsOsZz1KsytlAMB8GA1UdIwQY MBaAFMfgpuHxQF45KO2YsOsZz1KsytlAMA8GA1UdEwEB/wQFMAMBAf8wGgYDVR0R BBMwEYIPdGVsZXByZXNlbmNlLmlvMA0GCSqGSIb3DQEBCwUAA4IBAQBeyH/EDsB6 7k5wVjuDwo+4USRjJ1sF6hvxBBxsRpwict84ErM3FaQK/v/Jf/hgtvFmTTlMn3EM vuzQEgsLsFXlCct2F7I6F5ywICCStMLfzvC17lU8/v4I0seDS7FeQzx133uYCA9i PJrKj+D6Ay+pp0sl6vkUlHVm9FiD+taMY8eWT2FvQSg6n2g2UEyOA9LwWA0KagBk EC1vkd+w7FZSMqFI+vdej+25EYqzAuC6Cw1rbWnURhKp8+GiUS8jLjFaSpeJmuPp 7N4mlWr9t3AatYtekjhnqFXIktQS9inKe+F4h11XieHCzM3jyDvCFFGs5tWVV4/G c7aA+ZfJ/GXY -----END CERTIFICATE----- ================================================ FILE: integration_test/testdata/echo-server/certs/tls.key ================================================ -----BEGIN PRIVATE KEY----- MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCIQFD98yEE5l7W yVgL/8F/btYhVU0v9bcvKVXUt8j1p4GW3d0hg3bR5JtC4rPzwwYj9iFh4x0D/GAt 99id74Ih9p0kycVwJSc9JKSck9GdTVd3dSLftUAZ4eRRTeV72WGeqaBPhDJRtpyg gPYNDvMR4HjkFz1FjdT7I2bVOm5rxrs1ss4CmnWWc7QPU2NJ0CGxpYwqGKwQe/5X 3KCLM9SB3GvsFJZHioKGSeS/sTMebZDZaQT7Qjj9S3v8btTBzxSkyXeRtPIM2/7k RieLZ17UdAGHee8EzErO5S0vnH88kgrrtdLfxJqpdR7SRkNBHc4wgWI82xhU6PLv Zkt18SFjAgMBAAECggEABfMrdkOEcoNuEQDlR7TcXTVYw+Dgbrk5gPI6dABIlgDb TFxihTQJkskVLKEPJnrV2eksJ2tBctFpEJwG1hksIutZjM5qdE2MpQNkh6ZLqSWQ +oySpsrYwnVjJ1fFYmV2RiiP8X9k3fBuO4x8i8pwCLZRB49D1wJs30CtadKHTIAZ wdin38ALAa+Q7ocY9oEIgIX7uypCuHEmoPKtgj3NOGh52p4RFs3t1lD4sg6iUmsM hjsjtUGASnMbjJ0vgTxLVbDaRf9jSNNoEVSn6iyKzEGLZR4cFZwp+nt4GpCmdIU5 PCYU8K9wIKHkvxLnMBcfHPGlfphCTDL45cf3SQTuwQKBgQDASZ1v/yN/NxR3r/Ln 2UHr+rSlaHw4V2mznEK5DvypUsg9lcQwV8ixMiyW8Y/aEchQyc0eXx7P+dtHqcZb 3KgUOaNjKmEkTNFOy52EFONONaNU+98MImSgHP4c53IpDhJDbGJOBaFhpc//LiSO kFUZyOZ13qL7mTg5KM8F0D7BQQKBgQC1ZYn5TNAj2M3k6GnNx3wBiGIPoY/CnliV zyeUqSegIaAvK23qnOozfPUBUmz9UlSbNqF6pJTpFb146Xp5jiAgIVpN7x4XBvot AKFMqnkQoSVhLWB5e4FLqHF/0wY9yDIjxw444l55VQspjiPiBmNI589fvQO/jkUQ C+joefHVowKBgGoLby8n5zyudLQ5Ld4dXBS3U87xG6i61ImAgO+sSz1acSI9qU/7 6auHfz3ThMEAE5gyYtQAI28RXZRdFg7tVyioTOpQofgyATDSbFE+b8lfHW+t5Gm9 wf7nXmE0ZyorH3ldma1rv3+pwVb67KBPCw/IUwjoOrxE2NP1JI8RNLrBAoGAC87C PnIbklnIfUALsxNrJQZlq7LOktKP3aCQaQLhy3Ck5q0jCISSUiuuHxnockzrqPbT aBJShyGdJcO87zCrMqw5Hp2UDdesbUV/OmhWXRjAQCUeBIpfjjc2vCVWYKspaF7K tDU4BRneEiRofYwA5nwAabD6D3wJTtQXoxpc/ZUCgYB+5UZtmULVRb78Nf3OOcEB 1kLzgDNcZcscvEc0IQOUOWWadkkJVlCqxIr1v0/rt01iQ2OOhWI8PMwHutUYoWC4 AwIfcSz5sDCvTSL3TDfaIehkoDE5WTXcGzFybdROHoADkLlInrJUnEavQ4fmhhg8 w/6eqex+/wYx0OXJtLj19w== -----END PRIVATE KEY----- ================================================ FILE: integration_test/testdata/echo-server/go.mod ================================================ module local go 1.24.0 require ( golang.org/x/sync v0.19.0 golang.org/x/sys v0.41.0 ) ================================================ FILE: integration_test/testdata/echo-server/go.sum ================================================ golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= ================================================ FILE: integration_test/testdata/echo-server/main.go ================================================ package main import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "log" "net" "net/http" "os" "os/signal" "strconv" "strings" "time" "golang.org/x/sync/errgroup" "golang.org/x/sys/unix" ) type request struct { URL string `json:"url"` Headers map[string]string `json:"headers,omitempty"` Method string `json:"method,omitempty"` Body string `json:"body,omitempty"` } func main() { errLog := log.New(os.Stderr, "", log.LstdFlags) outLog := log.New(os.Stdout, "", log.LstdFlags) addr := os.Getenv("LISTEN_ADDRESS") portsEnv := os.Getenv("PORTS") if portsEnv == "" { portsEnv = os.Getenv("PORT") } if portsEnv == "" { portsEnv = "8080:http" } ports := strings.Split(portsEnv, ",") certFile, ok := os.LookupEnv("CERT_FILE") if !ok { certFile = "/certs/tls.crt" } keyFile, ok := os.LookupEnv("KEY_FILE") if !ok { keyFile = "/certs/tls.key" } servers := make([]*http.Server, 0, len(ports)) g, ctx := errgroup.WithContext(context.Background()) shutdownCtx, quit := context.WithCancel(ctx) mux := http.NewServeMux() mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { outLog.Print(r.URL.Path) w.WriteHeader(http.StatusOK) }) mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) { outLog.Print(r.URL.Path) w.WriteHeader(http.StatusOK) }) mux.HandleFunc("/quit", func(w http.ResponseWriter, r *http.Request) { outLog.Print(r.URL.Path) w.WriteHeader(http.StatusOK) quit() }) mux.HandleFunc("/forward", func(w http.ResponseWriter, r *http.Request) { outLog.Print(r.URL.Path) forwardHandler(w, r, outLog, errLog) }) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { outLog.Print(r.URL.Path) echoHandler(w, r, outLog, errLog) }) for _, portAndProto := range ports { proto := "http" port := portAndProto if pp := strings.SplitN(portAndProto, ":", 2); len(pp) == 2 { port = pp[0] proto = pp[1] } var addrPort string if addr == "" { addrPort = ":" + port } else { addrPort = net.JoinHostPort(addr, port) } outLog.Printf("Echo server listening on %s.\n", addrPort) server := &http.Server{ Addr: addrPort, BaseContext: func(_ net.Listener) context.Context { return ctx }, Handler: mux, } prs := new(http.Protocols) prs.SetHTTP1(true) server.Protocols = prs switch proto { case "http": prs.SetUnencryptedHTTP2(true) g.Go(func() error { _ = server.ListenAndServe() return nil }) case "https": prs.SetHTTP2(true) g.Go(func() error { _ = server.ListenAndServeTLS(certFile, keyFile) return nil }) default: errLog.Fatalf("unknown protocol %q", proto) } servers = append(servers, server) } g.Go(func() error { sigstop := make(chan os.Signal, 1) signal.Notify(sigstop, os.Interrupt, unix.SIGTERM) select { case <-sigstop: case <-shutdownCtx.Done(): } sdCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Second) defer cancel() var errs error for _, server := range servers { errs = errors.Join(errs, server.Shutdown(sdCtx)) } return errs }) err := g.Wait() if err != nil && !errors.Is(err, context.Canceled) { errLog.Fatalf("Echo server exited with error: %v", err) } } func echoHandler(wr http.ResponseWriter, req *http.Request, outLog, errLog *log.Logger) { defer req.Body.Close() switch req.Method { case http.MethodHead: return case http.MethodGet: // Accepted default: wr.WriteHeader(http.StatusMethodNotAllowed) return } bf := &bytes.Buffer{} if tpID, ok := os.LookupEnv("TELEPRESENCE_INTERCEPT_ID"); ok { writeAndLogf(bf, outLog, "Intercept id %s\n", tpID) writeAndLogf(bf, outLog, "Intercepted container %q\n", os.Getenv("TELEPRESENCE_CONTAINER")) } writeAndLogf(bf, outLog, "%s %s %s\n\nHost: %s\n", req.Proto, req.Method, req.URL, req.Host) if len(req.Header) > 0 { writeAndLogf(bf, outLog, "Headers\n") for key, values := range req.Header { for _, value := range values { writeAndLogf(bf, outLog, " %s: %s\n", key, value) } } } host, err := os.Hostname() if err == nil { writeAndLogf(bf, outLog, "Request served by %s\n", host) } _, _ = io.Copy(bf, req.Body) outLog.Printf("%s | http | %d byte(s)\n", req.RemoteAddr, bf.Len()) hdr := wr.Header() hdr.Set("Content-Type", "text/plain") hdr.Set("Content-Length", strconv.Itoa(bf.Len())) _, err = bf.WriteTo(wr) if err != nil { errLog.Printf("Error serving HTTP: %v", err) } } func forwardHandler(wr http.ResponseWriter, req *http.Request, outLog, errLog *log.Logger) { if req.Method != http.MethodPost { wr.WriteHeader(http.StatusMethodNotAllowed) return } fwReq := &request{} d := json.NewDecoder(req.Body) d.DisallowUnknownFields() err := d.Decode(fwReq) if err != nil { errLog.Printf("Error parsing request: %v", err) wr.WriteHeader(http.StatusBadRequest) return } var fwReqBody io.Reader if fwReq.Body != "" { fwReqBody = strings.NewReader(fwReq.Body) } if fwReq.Method == "" { fwReq.Method = http.MethodGet } fwr, err := http.NewRequest(fwReq.Method, os.ExpandEnv(fwReq.URL), fwReqBody) if err != nil { errLog.Printf("Error creating forward request: %v", err) wr.WriteHeader(http.StatusInternalServerError) return } for k, v := range fwReq.Headers { fwr.Header.Set(k, os.ExpandEnv(v)) } fwRsp, err := http.DefaultClient.Do(fwr) if err != nil { errLog.Printf("Error forwarding request: %v", err) wr.WriteHeader(http.StatusInternalServerError) return } defer fwRsp.Body.Close() wrh := wr.Header() for key, values := range fwRsp.Header { for _, value := range values { wrh.Add(key, value) } } if fwRsp.StatusCode != http.StatusOK { wr.WriteHeader(fwRsp.StatusCode) } _, err = io.Copy(wr, fwRsp.Body) if err != nil { errLog.Printf("Error forwarding response: %v", err) } } func writeAndLogf(w *bytes.Buffer, outLog *log.Logger, format string, args ...any) { s := fmt.Sprintf(format, args...) outLog.Print(s) _, _ = w.WriteString(s) } ================================================ FILE: integration_test/testdata/k8s/client_experiment.yaml ================================================ --- --- apiVersion: v1 kind: Service metadata: name: "echo-easy" spec: type: ClusterIP selector: app: echo-easy ports: - name: proxied port: 80 targetPort: http --- apiVersion: v1 kind: ServiceAccount metadata: name: telepresence-test-developer --- kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: name: traffic-manager-connect rules: - apiGroups: [""] resources: ["services"] verbs: ["get", "list"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: traffic-manager-connect subjects: - kind: ServiceAccount name: telepresence-test-developer roleRef: apiGroup: rbac.authorization.k8s.io name: traffic-manager-connect kind: Role ================================================ FILE: integration_test/testdata/k8s/client_rancher.goyaml ================================================ --- kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: rancher-inspect labels: purpose: tp-cli-testing rules: - apiGroups: [""] resources: ["nodes"] verbs: ["get", "list"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: rancher-inspect-{{ .ManagerNamespace }} labels: purpose: tp-cli-testing subjects: - kind: ServiceAccount name: telepresence-test-developer namespace: {{ .ManagerNamespace }} roleRef: apiGroup: rbac.authorization.k8s.io name: rancher-inspect kind: ClusterRole ================================================ FILE: integration_test/testdata/k8s/client_sa.yaml ================================================ apiVersion: v1 kind: ServiceAccount metadata: name: telepresence-test-developer ================================================ FILE: integration_test/testdata/k8s/disruption-budget.goyaml ================================================ {{- /*gotype: github.com/telepresenceio/telepresence/v2/integration_test/itest.DisruptionBudget*/ -}}--- apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: {{.Name}} spec: {{- if gt .MinAvailable 0 }} minAvailable: {{ .MinAvailable }} {{- end }} {{- if gt .MaxUnavailable 0 }} maxUnavailable: {{ .MaxUnavailable }} {{- end }} selector: matchLabels: budget: {{.Name}} ================================================ FILE: integration_test/testdata/k8s/echo-argo-rollout.yaml ================================================ --- apiVersion: argoproj.io/v1alpha1 kind: Rollout metadata: name: echo-argo-rollout labels: app: echo-argo-rollout spec: replicas: 5 strategy: canary: steps: - setWeight: 20 - pause: {} - setWeight: 40 - pause: {duration: 10} - setWeight: 60 - pause: {duration: 10} - setWeight: 80 - pause: {duration: 10} revisionHistoryLimit: 2 selector: matchLabels: app: echo-argo-rollout template: metadata: labels: app: echo-argo-rollout spec: automountServiceAccountToken: false containers: - name: echo-server image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - name: http containerPort: 8080 env: - name: PORT value: "8080" resources: limits: cpu: 50m memory: 16Mi --- apiVersion: v1 kind: Service metadata: name: echo-argo-rollout spec: ports: - port: 80 targetPort: http protocol: TCP name: http selector: app: echo-argo-rollout ================================================ FILE: integration_test/testdata/k8s/echo-auto-inject.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: echo-auto-inject spec: type: ClusterIP selector: app: echo-auto-inject ports: - name: proxied port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: echo-auto-inject labels: app: echo-auto-inject spec: replicas: 1 selector: matchLabels: app: echo-auto-inject template: metadata: annotations: telepresence.io/inject-traffic-agent: enabled labels: app: echo-auto-inject spec: containers: - name: echo-auto-inject image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8080 name: http resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: integration_test/testdata/k8s/echo-both.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: echo-one spec: type: ClusterIP selector: app: echo-both ports: - name: one port: 80 targetPort: echo-one --- apiVersion: v1 kind: Service metadata: name: echo-two spec: type: ClusterIP selector: app: echo-both ports: - name: two port: 80 targetPort: echo-two --- apiVersion: apps/v1 kind: Deployment metadata: name: echo-both labels: app: echo-both spec: replicas: 1 selector: matchLabels: app: echo-both template: metadata: labels: app: echo-both spec: containers: - name: echo-one image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - name: echo-one containerPort: 8080 resources: limits: cpu: 50m memory: 128Mi - name: echo-two image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - name: echo-two containerPort: 8081 env: - name: PORT value: "8081" resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: integration_test/testdata/k8s/echo-double-one-unnamed.yaml ================================================ # The echo-double-unnamed deployment exposes two unnamed ports, 8080 and 8081 from a single container apiVersion: apps/v1 kind: Deployment metadata: name: echo-double-one-unnamed labels: app: echo-double-one-unnamed spec: replicas: 1 selector: matchLabels: app: echo-double-one-unnamed template: metadata: annotations: telepresence.io/inject-container-ports: all labels: app: echo-double-one-unnamed spec: containers: - name: echo-server image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8080 - containerPort: 8081 env: - name: PORTS value: "8080,8081" resources: limits: cpu: 50m memory: 8Mi ================================================ FILE: integration_test/testdata/k8s/echo-easy.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: "echo-easy" spec: type: ClusterIP selector: app: echo-easy ports: - name: proxied port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: "echo-easy" labels: app: echo-easy spec: replicas: 1 selector: matchLabels: app: echo-easy template: metadata: labels: app: echo-easy spec: containers: - name: echo-easy image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8080 name: http resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: integration_test/testdata/k8s/echo-extra-udp.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: echo-extra-udp spec: type: ClusterIP selector: app: echo-extra-udp ports: - name: http port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: echo-extra-udp labels: app: echo-extra-udp spec: replicas: 1 selector: matchLabels: app: echo-extra-udp template: metadata: labels: app: echo-extra-udp spec: containers: - name: echo-udp-server image: ghcr.io/telepresenceio/udp-echo:latest imagePullPolicy: IfNotPresent env: - name: PORT value: "8080" resources: limits: cpu: 50m memory: 8Mi - name: echo-server image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - name: http containerPort: 8080 env: - name: PORT value: "8080" resources: limits: cpu: 50m memory: 8Mi ================================================ FILE: integration_test/testdata/k8s/echo-headless.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: echo-headless spec: type: ClusterIP clusterIP: None selector: app: echo-headless ports: - name: http port: 8080 targetPort: 8080 --- apiVersion: apps/v1 kind: StatefulSet metadata: name: echo-headless labels: app: echo-headless spec: replicas: 1 serviceName: echo-headless selector: matchLabels: app: echo-headless template: metadata: labels: app: echo-headless spec: containers: - name: echo-headless image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8080 resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: integration_test/testdata/k8s/echo-interpolate.yaml ================================================ --- apiVersion: v1 kind: ConfigMap metadata: name: interpolate-config data: SOME_NAME: "hello" --- apiVersion: v1 kind: Service metadata: name: echo-interpolate spec: type: ClusterIP selector: app: echo-interpolate ports: - name: proxied port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: echo-interpolate labels: app: echo-interpolate spec: replicas: 1 selector: matchLabels: app: echo-interpolate template: metadata: labels: app: echo-interpolate spec: containers: - name: echo-interpolate image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent envFrom: - configMapRef: name: interpolate-config env: - name: OTHER_NAME value: "hi" ports: - containerPort: 8080 name: http volumeMounts: - mountPath: /var/log/my-volume name: my-volume subPathExpr: $(SOME_NAME)_$(OTHER_NAME) resources: limits: cpu: 50m memory: 128Mi volumes: - emptyDir: {} name: my-volume ================================================ FILE: integration_test/testdata/k8s/echo-manual-inject-deploy.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: "manual-inject" labels: app: manual-inject spec: replicas: 1 selector: matchLabels: app: manual-inject template: metadata: labels: app: manual-inject spec: containers: - name: echo-container image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - containerPort: 80 resources: {} ================================================ FILE: integration_test/testdata/k8s/echo-manual-inject-svc.yaml ================================================ apiVersion: v1 kind: Service metadata: name: "manual-inject" spec: type: ClusterIP selector: app: manual-inject ports: - port: 80 ================================================ FILE: integration_test/testdata/k8s/echo-min.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: "echo" spec: type: ClusterIP selector: app: echo ports: - name: proxied port: 80 targetPort: http --- apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: echo spec: minAvailable: 1 selector: matchLabels: app: echo --- apiVersion: apps/v1 kind: Deployment metadata: name: "echo" labels: app: echo spec: replicas: 1 selector: matchLabels: app: echo template: metadata: labels: app: echo spec: containers: - name: echo image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8080 name: http resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: integration_test/testdata/k8s/echo-no-containerport.yaml ================================================ # The echo-no-containerport deployment doesn't expose any ports but will reply on port 8080 apiVersion: apps/v1 kind: Deployment metadata: name: echo-no-containerport labels: app: echo-no-containerport spec: replicas: 1 selector: matchLabels: app: echo-no-containerport template: metadata: labels: app: echo-no-containerport spec: containers: - name: echo-server image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent env: - name: PORTS value: "8080,8081" resources: limits: cpu: 50m memory: 8Mi ================================================ FILE: integration_test/testdata/k8s/echo-no-svc-ann.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: echo-no-svc-ann labels: app: echo-no-svc-ann spec: replicas: 1 selector: matchLabels: app: echo-no-svc-ann template: metadata: labels: app: echo-no-svc-ann budget: telepresence-test annotations: telepresence.io/inject-container-ports: http spec: automountServiceAccountToken: false containers: - name: echo-server image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - name: http containerPort: 8080 env: - name: PORT value: "8080" resources: limits: cpu: 50m memory: 8Mi ================================================ FILE: integration_test/testdata/k8s/echo-no-svc.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: echo-no-svc labels: app: echo-no-svc spec: replicas: 1 selector: matchLabels: app: echo-no-svc template: metadata: labels: app: echo-no-svc spec: automountServiceAccountToken: false containers: - name: echo-server image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - name: http containerPort: 8080 env: - name: PORT value: "8080" resources: limits: cpu: 50m memory: 8Mi ================================================ FILE: integration_test/testdata/k8s/echo-no-vols.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: echo-no-vols spec: type: ClusterIP selector: app: echo-no-vols ports: - name: http port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: echo-no-vols labels: app: echo-no-vols spec: replicas: 1 selector: matchLabels: app: echo-no-vols template: metadata: labels: app: echo-no-vols budget: telepresence-test spec: automountServiceAccountToken: false containers: - name: echo-server image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - name: http containerPort: 8080 env: - name: PORT value: "8080" resources: limits: cpu: 50m memory: 8Mi ================================================ FILE: integration_test/testdata/k8s/echo-one.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: "echo-one" spec: type: ClusterIP selector: app: echo-one ports: - name: proxied port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: "echo-one" labels: app: echo-one spec: replicas: 1 selector: matchLabels: app: echo-one template: metadata: labels: app: echo-one spec: containers: - name: echo-one image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8080 name: http resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: integration_test/testdata/k8s/echo-same-target-port.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: echo-stp spec: type: ClusterIP selector: app: echo-stp ports: - name: eighty port: 80 targetPort: 8080 - name: eighty-eighty port: 8080 targetPort: 8080 --- apiVersion: apps/v1 kind: Deployment metadata: name: echo-stp labels: app: echo-stp spec: replicas: 1 selector: matchLabels: app: echo-stp template: metadata: labels: app: echo-stp spec: containers: - name: echo-stp image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8080 resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: integration_test/testdata/k8s/echo-secondary.yaml ================================================ apiVersion: v1 kind: ConfigMap metadata: name: echo-data data: text: | Hello from echo --- apiVersion: v1 kind: ConfigMap metadata: name: socat-data data: text: | Hello from socat --- apiVersion: v1 kind: Service metadata: name: "echo-secondary" spec: type: ClusterIP selector: app: echo-secondary ports: - name: proxied port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: "echo-secondary" labels: app: echo-secondary spec: replicas: 1 selector: matchLabels: app: echo-secondary template: metadata: labels: app: echo-secondary spec: volumes: - name: echo-data configMap: name: echo-data - name: socat-data configMap: name: socat-data containers: - name: socat image: alpine/socat:latest imagePullPolicy: IfNotPresent args: - "tcp-listen:9080,fork,reuseaddr" - "tcp-connect:localhost:8080" env: - name: TAG value: socat ports: - containerPort: 9080 name: http volumeMounts: - mountPath: "/usr/share/data" name: socat-data - name: echo image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent env: - name: TAG value: echo-server volumeMounts: - mountPath: "/usr/share/data" name: echo-data ================================================ FILE: integration_test/testdata/k8s/echo-spring.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: "echo-spring" spec: type: ClusterIP selector: app: echo-spring ports: - name: proxied port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: "echo-spring" labels: app: echo-spring spec: replicas: 1 selector: matchLabels: app: echo-spring template: metadata: annotations: instrumentation.opentelemetry.io/inject-java: "true" labels: app: echo-spring spec: containers: - name: echo-spring image: codelev/echo-spring:latest imagePullPolicy: IfNotPresent startupProbe: httpGet: path: /rest/echo port: 9000 initialDelaySeconds: 8 periodSeconds: 3 failureThreshold: 10 ports: - containerPort: 9000 name: http resources: limits: cpu: 500m memory: 1024Mi ================================================ FILE: integration_test/testdata/k8s/echo-tls.goyaml ================================================ --- apiVersion: v1 kind: Service metadata: name: {{ .Name }} spec: type: ClusterIP selector: app: {{ .Name }} ports: - name: http port: 80 targetPort: http - name: https port: 443 targetPort: https --- apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Name }} labels: app: {{ .Name }} spec: replicas: 1 selector: matchLabels: app: {{ .Name }} template: metadata: labels: app: {{ .Name }} {{- with .Annotations }} annotations: {{- toYaml . | nindent 8 }} {{- end}} spec: serviceAccountName: {{ .ServiceAccount }} containers: - name: backend image: "{{ .Registry }}/{{ .Image }}" ports: - name: http containerPort: 8080 - name: https containerPort: 8443 {{- with .Environment }} env: {{- toYaml . | nindent 12 }} {{- end}} ================================================ FILE: integration_test/testdata/k8s/echo-udp-tcp-unnamed.yaml ================================================ # The echo-double-unnamed deployment exposes two unnamed ports, 8080 and 8081 from a single container apiVersion: apps/v1 kind: Deployment metadata: name: echo-udp-tcp-unnamed labels: app: echo-udp-tcp-unnamed spec: replicas: 1 selector: matchLabels: app: echo-udp-tcp-unnamed template: metadata: labels: app: echo-udp-tcp-unnamed spec: containers: - name: echo-udp-server image: ghcr.io/telepresenceio/udp-echo:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8080 protocol: UDP env: - name: PORT value: "8080" resources: limits: cpu: 50m memory: 8Mi - name: echo-server image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8080 env: - name: PORT value: "8080" resources: limits: cpu: 50m memory: 8Mi ================================================ FILE: integration_test/testdata/k8s/echo-w-hostalias.goyaml ================================================ --- apiVersion: v1 kind: Service metadata: name: echo spec: type: ClusterIP selector: app: echo ports: - name: proxied port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: echo labels: app: echo spec: replicas: 1 selector: matchLabels: app: echo template: metadata: labels: app: echo spec: hostAliases: - ip: "{{ .AliasIP }}" hostnames: {{- range .Aliases }} - {{ . }} {{- end }} containers: - name: echo image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8080 name: http resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: integration_test/testdata/k8s/echo-w-sidecars.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: echo-w-sidecars spec: type: ClusterIP selector: app: echo-w-sidecars ports: - name: proxied port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: echo-w-sidecars labels: app: echo-w-sidecars spec: replicas: 1 selector: matchLabels: app: echo-w-sidecars template: metadata: labels: app: echo-w-sidecars spec: containers: - name: echo-main image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8080 name: http resources: limits: cpu: 50m memory: 128Mi - name: echo-side-one image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8081 name: http-one env: - name: PORT value: "8081" resources: limits: cpu: 50m memory: 128Mi - name: echo-side-two image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8082 name: http-two env: - name: PORT value: "8082" resources: limits: cpu: 50m memory: 128Mi - name: echo-side-three image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8083 name: http-three env: - name: PORT value: "8083" resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: integration_test/testdata/k8s/echo-w-subdomain.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: subsonic spec: selector: app: subsonic clusterIP: None ports: - name: proxied port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: echo-subsonic labels: app: subsonic spec: replicas: 1 selector: matchLabels: app: subsonic template: metadata: labels: app: subsonic spec: hostname: echo subdomain: subsonic containers: - name: echo image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8080 name: http resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: integration_test/testdata/k8s/echo_with_env.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: "echo-easy" spec: type: ClusterIP selector: app: echo-easy ports: - name: proxied port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: "echo-easy" labels: app: echo-easy spec: replicas: 1 selector: matchLabels: app: echo-easy template: metadata: labels: app: echo-easy spec: containers: - name: echo-easy image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent env: - name: TEST value: "DATA" - name: INTERCEPT value: "ENV" - name: DATABASE_HOST value: "HOST_NAME" - name: DATABASE_PASSWORD value: "SUPER_SECRET_PASSWORD" ports: - containerPort: 8080 name: http resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: integration_test/testdata/k8s/generic.goyaml ================================================ {{- /*gotype: github.com/telepresenceio/telepresence/v2/integration_test/itest.Generic*/ -}} --- apiVersion: v1 kind: Service metadata: name: {{ .Name }} spec: type: ClusterIP selector: app: {{ .Name }} ports: {{- range .ServicePorts }} - port: {{ .Number }} {{- with .Name }} name: {{ . }} {{- end}} {{- with .TargetPort }} targetPort: {{ . }} {{- end}} {{- with .Protocol }} protocol: {{ . }} {{- end}} {{- else }} - port: 80 name: http targetPort: {{ .TargetPort | default "http" }} {{- end}} --- apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Name }} labels: app: {{ .Name }} spec: replicas: 1 selector: matchLabels: app: {{ .Name }} template: metadata: labels: app: {{ .Name }} {{- with .Labels }} {{- toYaml . | nindent 8 }} {{- end}} {{- with .Annotations }} annotations: {{- toYaml . | nindent 8 }} {{- end}} spec: {{- with .Volumes }} volumes: {{- toYaml . | nindent 8 }} {{- end}} containers: - name: backend {{- if .Registry }} image: {{ .Registry }}/{{ .Image }} {{- else }} image: {{ .Image }} {{- end}} imagePullPolicy: Always ports: {{- range .ContainerPorts }} - containerPort: {{ .Number }} {{- with .Name }} name: {{ . }} {{- end}} {{- with .Protocol }} protocol: {{ . }} {{- end}} {{- else }} - containerPort: {{ .ContainerPort | default 8080 }} name: http {{- end }} {{- with .VolumeMounts }} volumeMounts: {{- toYaml . | nindent 10 }} {{- end }} {{- with .Environment }} env: {{- toYaml . | nindent 12 }} {{- end}} ================================================ FILE: integration_test/testdata/k8s/hello-w-volumes.goyaml ================================================ apiVersion: v1 kind: ConfigMap metadata: name: nginx data: default.conf.template: | server { listen ${NGINX_PORT}; server_name localhost; location / { root /usr/share/nginx/html; index index.html index.htm; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } } --- apiVersion: v1 kind: ConfigMap metadata: name: hello-data-1 data: index.html: |

Hello from volume 1!

--- apiVersion: v1 kind: ConfigMap metadata: name: hello-data-2 data: index.html: |

Hello from volume 2!

--- apiVersion: v1 kind: Secret metadata: name: hello-secret-1 type: kubernetes.io/basic-auth stringData: username: "hello-1" password: "hello-1" --- apiVersion: v1 kind: Secret metadata: name: hello-secret-2 type: kubernetes.io/basic-auth stringData: username: "hello-2" password: "hello-2" --- apiVersion: v1 kind: Service metadata: name: hello spec: type: ClusterIP selector: app: hello ports: - name: http port: 80 targetPort: http - name: http2 port: 81 appProtocol: http targetPort: http2 --- apiVersion: apps/v1 kind: Deployment metadata: name: hello labels: app: hello spec: replicas: 1 selector: matchLabels: app: hello template: metadata: labels: app: hello {{- with .Annotations }} annotations: {{- toYaml . | nindent 8 }} {{- end}} spec: volumes: - name: hello-data-volume-1 configMap: name: hello-data-1 - name: hello-data-volume-2 configMap: name: hello-data-2 - name: hello-secret-volume-1 secret: secretName: hello-secret-1 - name: hello-secret-volume-2 secret: secretName: hello-secret-2 - name: nginx-config configMap: name: nginx containers: - name: hello-container-1 image: nginx imagePullPolicy: IfNotPresent env: - name: NGINX_HOST value: "hello-1.com" - name: NGINX_PORT value: "80" ports: - containerPort: 80 name: http volumeMounts: - mountPath: "/usr/share/nginx/html" name: hello-data-volume-1 - mountPath: "/var/run/secrets/datawire.io/auth" name: hello-secret-volume-1 - mountPath: /etc/nginx/templates/ name: nginx-config livenessProbe: httpGet: path: / port: http initialDelaySeconds: 3 periodSeconds: 3 - name: hello-container-2 image: nginx imagePullPolicy: IfNotPresent env: - name: NGINX_HOST value: "hello-2.com" - name: NGINX_PORT value: "81" ports: - containerPort: 81 name: http2 volumeMounts: - mountPath: "/usr/share/nginx/html" name: hello-data-volume-2 - mountPath: "/var/run/secrets/datawire.io/auth" name: hello-secret-volume-2 - mountPath: /etc/nginx/templates/ name: nginx-config ================================================ FILE: integration_test/testdata/k8s/many-volumes.yaml ================================================ apiVersion: v1 kind: ConfigMap metadata: name: cm1 data: data.txt: | Some data 1 --- apiVersion: v1 kind: ConfigMap metadata: name: cm2 data: data.txt: | Some data 2 --- apiVersion: v1 kind: ConfigMap metadata: name: cm3 data: data.txt: | Some data 3 --- apiVersion: v1 kind: ConfigMap metadata: name: cm4 data: data.txt: | Some data 4 --- apiVersion: v1 kind: ConfigMap metadata: name: cm5 data: data.txt: | Some data 5 --- apiVersion: v1 kind: ConfigMap metadata: name: cm6 data: data.txt: | Some data 6 --- apiVersion: v1 kind: ConfigMap metadata: name: cm7 data: data.txt: | Some data 7 --- apiVersion: v1 kind: ConfigMap metadata: name: cm8 data: data.txt: | Some data 8 --- apiVersion: v1 kind: Secret metadata: name: hello-secret-1 type: kubernetes.io/basic-auth stringData: username: "hello-1" password: "hello-1" --- apiVersion: v1 kind: Secret metadata: name: hello-secret-2 type: kubernetes.io/basic-auth stringData: username: "hello-2" password: "hello-2" --- apiVersion: v1 kind: Service metadata: name: hello spec: type: ClusterIP selector: app: hello ports: - name: http port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: hello labels: app: hello spec: replicas: 1 selector: matchLabels: app: hello template: metadata: labels: app: hello spec: volumes: - name: v1 configMap: name: cm1 - name: v2 configMap: name: cm2 - name: v3 configMap: name: cm3 - name: v4 configMap: name: cm4 - name: v5 configMap: name: cm5 - name: v6 configMap: name: cm6 - name: v7 configMap: name: cm7 - name: v8 configMap: name: cm8 - name: ed1 emptyDir: {} - name: ed2 emptyDir: {} - name: ed3 emptyDir: {} - name: ed4 emptyDir: {} - name: ed5 emptyDir: {} - name: ed6 emptyDir: {} - name: ed7 emptyDir: {} - name: ed8 emptyDir: {} containers: - name: hello image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8080 name: http volumeMounts: - mountPath: "/usr/share/cm1" name: v1 - mountPath: "/usr/share/cm2" name: v2 - mountPath: "/usr/share/cm3" name: v3 - mountPath: "/usr/share/cm4" name: v4 - mountPath: "/usr/share/cm5" name: v5 - mountPath: "/usr/share/cm6" name: v6 - mountPath: "/usr/share/cm7" name: v7 - mountPath: "/usr/share/cm8" name: v8 - mountPath: "/tmp1" name: ed1 - mountPath: "/tmp2" name: ed2 - mountPath: "/tmp3" name: ed3 - mountPath: "/tmp4" name: ed4 - mountPath: "/tmp5" name: ed5 - mountPath: "/tmp6" name: ed6 - mountPath: "/tmp7" name: ed7 - mountPath: "/tmp8" name: ed8 ================================================ FILE: integration_test/testdata/k8s/memory-constraints.yaml ================================================ apiVersion: v1 kind: LimitRange metadata: name: mem-min-max-test spec: limits: - max: memory: 100Mi min: memory: 20Mi type: Container ================================================ FILE: integration_test/testdata/k8s/namespaces.yaml ================================================ --- apiVersion: v1 kind: Namespace metadata: name: alpha labels: phase: a1 scope: dev --- apiVersion: v1 kind: Namespace metadata: name: beta labels: phase: a1 scope: dev --- apiVersion: v1 kind: Namespace metadata: name: alpha-test labels: phase: none scope: unscoped --- apiVersion: v1 kind: Namespace metadata: name: alpha-rc labels: phase: a2 scope: dev --- apiVersion: v1 kind: Namespace metadata: name: beta-rc labels: phase: a2 scope: dev --- apiVersion: v1 kind: Namespace metadata: name: alpha-live labels: phase: ga scope: prod --- apiVersion: v1 kind: Namespace metadata: name: beta-live labels: phase: ga scope: prod ================================================ FILE: integration_test/testdata/k8s/pol-disabled.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: pol-disabled spec: type: ClusterIP selector: app: pol-disabled ports: - name: proxied port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: pol-disabled labels: app: pol-disabled spec: replicas: 1 selector: matchLabels: app: pol-disabled template: metadata: annotations: telepresence.io/inject-traffic-agent: disabled labels: app: pol-disabled spec: containers: - name: echo image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8080 name: http resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: integration_test/testdata/k8s/pol-enabled.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: pol-enabled spec: type: ClusterIP selector: app: pol-enabled ports: - name: proxied port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: pol-enabled labels: app: pol-enabled spec: replicas: 1 selector: matchLabels: app: pol-enabled template: metadata: annotations: telepresence.io/inject-traffic-agent: enabled labels: app: pol-enabled spec: containers: - name: echo image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8080 name: http resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: integration_test/testdata/k8s/pol-none.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: pol-none spec: type: ClusterIP selector: app: pol-none ports: - name: proxied port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: pol-none labels: app: pol-none spec: replicas: 1 selector: matchLabels: app: pol-none template: metadata: labels: app: pol-none spec: containers: - name: echo image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8080 name: http resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: integration_test/testdata/k8s/pv.goyaml ================================================ {{- /*gotype: github.com/telepresenceio/telepresence/v2/integration_test/itest.PersistentVolume*/ -}} apiVersion: v1 kind: PersistentVolume metadata: name: {{ .Name }} labels: purpose: tp-cli-testing {{- with .Annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/os operator: In values: - linux capacity: storage: 0.1Gi accessModes: - ReadWriteMany persistentVolumeReclaimPolicy: Retain hostPath: path: /tmp/{{ .Name }}-pv {{- with .StorageClassName }} storageClassName: {{ . }} {{- end }} ================================================ FILE: integration_test/testdata/k8s/pvc.goyaml ================================================ {{- /*gotype: github.com/telepresenceio/telepresence/v2/integration_test/itest.PersistentVolumeClaim*/ -}} apiVersion: v1 kind: PersistentVolumeClaim metadata: name: {{ .Name }} labels: purpose: tp-cli-testing {{- with .Annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: accessModes: - ReadWriteMany resources: requests: storage: 0.1Gi {{- with .StorageClassName }} storageClassName: {{ . }} {{- end }} ================================================ FILE: integration_test/testdata/k8s/rs-echo.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: rs-echo spec: type: ClusterIP selector: app: rs-echo ports: - name: http port: 80 targetPort: 8080 --- apiVersion: apps/v1 kind: ReplicaSet metadata: name: rs-echo labels: app: rs-echo spec: replicas: 1 selector: matchLabels: app: rs-echo template: metadata: labels: app: rs-echo budget: telepresence-test spec: containers: - name: rs-echo image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - name: http containerPort: 8080 resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: integration_test/testdata/k8s/secret-reader-rbac.goyaml ================================================ --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: secret-reader rules: - apiGroups: [""] resources: ["secrets"] verbs: ["get", "watch", "list"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: read-secrets subjects: - kind: ServiceAccount name: {{ .Name }} apiGroup: "" roleRef: kind: Role name: secret-reader apiGroup: rbac.authorization.k8s.io ================================================ FILE: integration_test/testdata/k8s/ss-echo.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: ss-echo spec: type: ClusterIP selector: app: ss-echo ports: - name: http port: 80 targetPort: 8080 --- apiVersion: apps/v1 kind: StatefulSet metadata: name: ss-echo labels: app: ss-echo spec: serviceName: "ss-echo" replicas: 2 selector: matchLabels: app: ss-echo template: metadata: labels: app: ss-echo budget: telepresence-test spec: containers: - name: ss-echo image: ghcr.io/telepresenceio/echo-server:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8080 resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: integration_test/testdata/k8s/svc-deploy.goyaml ================================================ --- apiVersion: v1 kind: Service metadata: name: {{ default .AppName .ServiceName }} labels: app: {{ .AppName }} spec: type: ClusterIP selector: app: {{ .AppName }} ports: {{- range .Ports }} - port: {{ .ServicePortNumber }} {{- if .ServicePortName }} name: {{ .ServicePortName }} {{- end }} {{- if .AppProtocol }} appProtocol: {{ .AppProtocol }} {{- end }} protocol: {{ default "TCP" .Protocol }} targetPort: {{ default .TargetPortNumber .TargetPortName }} {{- end }} --- apiVersion: apps/v1 kind: Deployment metadata: name: {{ default .AppName .DeploymentName }} labels: app: {{ .AppName }} spec: replicas: 1 selector: matchLabels: app: {{ .AppName }} template: metadata: labels: app: {{ .AppName }} spec: containers: - name: {{ default .AppName .ContainerName }} image: {{ default "ghcr.io/telepresenceio/echo-server:latest" .Image }} imagePullPolicy: {{ default "IfNotPresent" .PullPolicy }} env: {{- range $key, $value := .Env }} - name: {{ $key }} value: {{ quote $value }} {{- end }} ports: {{- range .Ports }} - containerPort: {{ .TargetPortNumber }} {{- if .TargetPortName }} name: {{ .TargetPortName }} {{- end }} protocol: {{ default "TCP" .Protocol }} {{- end }} resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: integration_test/testdata/k8s/tel-cert.yaml ================================================ apiVersion: v1 kind: Secret type: kubernetes.io/tls metadata: name: tel-cert data: tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURaVENDQWsyZ0F3SUJBZ0lVUnd4WVMyNnl1bktJNk9jSmg0bG9ORFh2Z3VNd0RRWUpLb1pJaHZjTkFRRUwKQlFBd05ERVlNQllHQTFVRUF3d1BkR1ZzWlhCeVpYTmxibU5sTG1sdk1SZ3dGZ1lEVlFRS0RBOTBaV3hsY0hKbApjMlZ1WTJVdWFXOHdIaGNOTWpVd09USTNNREV6T0RFd1doY05Nall3T1RJM01ERXpPREV3V2pBME1SZ3dGZ1lEClZRUUREQTkwWld4bGNISmxjMlZ1WTJVdWFXOHhHREFXQmdOVkJBb01EM1JsYkdWd2NtVnpaVzVqWlM1cGJ6Q0MKQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFJaEFVUDN6SVFUbVh0YkpXQXYvd1g5dQoxaUZWVFMvMXR5OHBWZFMzeVBXbmdaYmQzU0dEZHRIa20wTGlzL1BEQmlQMklXSGpIUVA4WUMzMzJKM3ZnaUgyCm5TVEp4WEFsSnowa3BKeVQwWjFOVjNkMUl0KzFRQm5oNUZGTjVYdlpZWjZwb0UrRU1sRzJuS0NBOWcwTzh4SGcKZU9RWFBVV04xUHNqWnRVNmJtdkd1eld5emdLYWRaWnp0QTlUWTBuUUliR2xqQ29ZckJCNy9sZmNvSXN6MUlIYwphK3dVbGtlS2dvWko1TCt4TXg1dGtObHBCUHRDT1AxTGUveHUxTUhQRktUSmQ1RzA4Z3piL3VSR0o0dG5YdFIwCkFZZDU3d1RNU3M3bExTK2NmenlTQ3V1MTB0L0VtcWwxSHRKR1EwRWR6akNCWWp6YkdGVG84dTltUzNYeElXTUMKQXdFQUFhTnZNRzB3SFFZRFZSME9CQllFRk1mZ3B1SHhRRjQ1S08yWXNPc1p6MUtzeXRsQU1COEdBMVVkSXdRWQpNQmFBRk1mZ3B1SHhRRjQ1S08yWXNPc1p6MUtzeXRsQU1BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0dnWURWUjBSCkJCTXdFWUlQZEdWc1pYQnlaWE5sYm1ObExtbHZNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUJleUgvRURzQjYKN2s1d1ZqdUR3bys0VVNSakoxc0Y2aHZ4QkJ4c1Jwd2ljdDg0RXJNM0ZhUUsvdi9KZi9oZ3R2Rm1UVGxNbjNFTQp2dXpRRWdzTHNGWGxDY3QyRjdJNkY1eXdJQ0NTdE1MZnp2QzE3bFU4L3Y0STBzZURTN0ZlUXp4MTMzdVlDQTlpClBKcktqK0Q2QXkrcHAwc2w2dmtVbEhWbTlGaUQrdGFNWThlV1QyRnZRU2c2bjJnMlVFeU9BOUx3V0EwS2FnQmsKRUMxdmtkK3c3RlpTTXFGSSt2ZGVqKzI1RVlxekF1QzZDdzFyYlduVVJoS3A4K0dpVVM4akxqRmFTcGVKbXVQcAo3TjRtbFdyOXQzQWF0WXRla2pobnFGWElrdFFTOWluS2UrRjRoMTFYaWVIQ3pNM2p5RHZDRkZHczV0V1ZWNC9HCmM3YUErWmZKL0dYWQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2QUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktZd2dnU2lBZ0VBQW9JQkFRQ0lRRkQ5OHlFRTVsN1cKeVZnTC84Ri9idFloVlUwdjliY3ZLVlhVdDhqMXA0R1czZDBoZzNiUjVKdEM0clB6d3dZajlpRmg0eDBEL0dBdAo5OWlkNzRJaDlwMGt5Y1Z3SlNjOUpLU2NrOUdkVFZkM2RTTGZ0VUFaNGVSUlRlVjcyV0dlcWFCUGhESlJ0cHlnCmdQWU5Edk1SNEhqa0Z6MUZqZFQ3STJiVk9tNXJ4cnMxc3M0Q21uV1djN1FQVTJOSjBDR3hwWXdxR0t3UWUvNVgKM0tDTE05U0IzR3ZzRkpaSGlvS0dTZVMvc1RNZWJaRFphUVQ3UWpqOVMzdjhidFRCenhTa3lYZVJ0UElNMi83awpSaWVMWjE3VWRBR0hlZThFekVyTzVTMHZuSDg4a2dycnRkTGZ4SnFwZFI3U1JrTkJIYzR3Z1dJODJ4aFU2UEx2ClprdDE4U0ZqQWdNQkFBRUNnZ0VBQmZNcmRrT0Vjb051RVFEbFI3VGNYVFZZdytEZ2JyazVnUEk2ZEFCSWxnRGIKVEZ4aWhUUUprc2tWTEtFUEpuclYyZWtzSjJ0QmN0RnBFSndHMWhrc0l1dFpqTTVxZEUyTXBRTmtoNlpMcVNXUQorb3lTcHNyWXduVmpKMWZGWW1WMlJpaVA4WDlrM2ZCdU80eDhpOHB3Q0xaUkI0OUQxd0pzMzBDdGFkS0hUSUFaCndkaW4zOEFMQWErUTdvY1k5b0VJZ0lYN3V5cEN1SEVtb1BLdGdqM05PR2g1MnA0UkZzM3QxbEQ0c2c2aVVtc00KaGpzanRVR0FTbk1iakowdmdUeExWYkRhUmY5alNOTm9FVlNuNml5S3pFR0xaUjRjRlp3cCtudDRHcENtZElVNQpQQ1lVOEs5d0lLSGt2eExuTUJjZkhQR2xmcGhDVERMNDVjZjNTUVR1d1FLQmdRREFTWjF2L3lOL054UjNyL0xuCjJVSHIrclNsYUh3NFYybXpuRUs1RHZ5cFVzZzlsY1F3VjhpeE1peVc4WS9hRWNoUXljMGVYeDdQK2R0SHFjWmIKM0tnVU9hTmpLbUVrVE5GT3k1MkVGT05PTmFOVSs5OE1JbVNnSFA0YzUzSXBEaEpEYkdKT0JhRmhwYy8vTGlTTwprRlVaeU9aMTNxTDdtVGc1S004RjBEN0JRUUtCZ1FDMVpZbjVUTkFqMk0zazZHbk54M3dCaUdJUG9ZL0NubGlWCnp5ZVVxU2VnSWFBdksyM3FuT296ZlBVQlVtejlVbFNiTnFGNnBKVHBGYjE0NlhwNWppQWdJVnBON3g0WEJ2b3QKQUtGTXFua1FvU1ZoTFdCNWU0RkxxSEYvMHdZOXlESWp4dzQ0NGw1NVZRc3BqaVBpQm1OSTU4OWZ2UU8vamtVUQpDK2pvZWZIVm93S0JnR29MYnk4bjV6eXVkTFE1TGQ0ZFhCUzNVODd4RzZpNjFJbUFnTytzU3oxYWNTSTlxVS83CjZhdUhmejNUaE1FQUU1Z3lZdFFBSTI4UlhaUmRGZzd0Vnlpb1RPcFFvZmd5QVREU2JGRStiOGxmSFcrdDVHbTkKd2Y3blhtRTBaeW9ySDNsZG1hMXJ2Mytwd1ZiNjdLQlBDdy9JVXdqb09yeEUyTlAxSkk4Uk5MckJBb0dBQzg3QwpQbklia2xuSWZVQUxzeE5ySlFabHE3TE9rdEtQM2FDUWFRTGh5M0NrNXEwakNJU1NVaXV1SHhub2NrenJxUGJUCmFCSlNoeUdkSmNPODd6Q3JNcXc1SHAyVURkZXNiVVYvT21oV1hSakFRQ1VlQklwZmpqYzJ2Q1ZXWUtzcGFGN0sKdERVNEJSbmVFaVJvZll3QTVud0FhYkQ2RDN3SlR0UVhveHBjL1pVQ2dZQis1VVp0bVVMVlJiNzhOZjNPT2NFQgoxa0x6Z0ROY1pjc2N2RWMwSVFPVU9XV2Fka2tKVmxDcXhJcjF2MC9ydDAxaVEyT09oV0k4UE13SHV0VVlvV0M0CkF3SWZjU3o1c0RDdlRTTDNURGZhSWVoa29ERTVXVFhjR3pGeWJkUk9Ib0FEa0xsSW5ySlVuRWF2UTRmbWhoZzgKdy82ZXFleCsvd1l4ME9YSnRMajE5dz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K ================================================ FILE: integration_test/testdata/k8s/with-probes.yaml ================================================ --- apiVersion: apps/v1 kind: Deployment metadata: name: with-probes spec: replicas: 2 selector: matchLabels: app: with-probes template: metadata: annotations: consul.hashicorp.com/connect-inject: 'false' sidecar.istio.io/inject: 'false' labels: app: with-probes budget: telepresence-test spec: containers: - name: sample-app image: gcr.io/datawire/k8s-initializer-sample-app:latest imagePullPolicy: Always env: - name: LISTEN_PORT value: '3000' livenessProbe: httpGet: path: /health port: http periodSeconds: 10 readinessProbe: httpGet: path: /health port: http periodSeconds: 10 ports: - containerPort: 3000 name: http --- apiVersion: v1 kind: Service metadata: name: with-probes labels: app: with-probes spec: ports: - port: 80 name: http targetPort: http selector: app: with-probes ================================================ FILE: integration_test/testdata/k8screds/main.go ================================================ package main import ( "bytes" "fmt" "os" "os/exec" meta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/client-go/pkg/apis/clientauthentication" "k8s.io/client-go/pkg/apis/clientauthentication/install" "k8s.io/client-go/tools/clientcmd/api" "github.com/telepresenceio/telepresence/v2/pkg/client/k8s" "github.com/telepresenceio/telepresence/v2/pkg/dos" ) func main() { if err := run(os.Args); err != nil { _, _ = fmt.Fprintln(os.Stderr, err.Error()) os.Exit(1) } } func run(args []string) error { var cn string var fm map[string]string switch len(args) { case 1: case 2: cn = os.Args[1] fm = map[string]string{"context": cn} default: return fmt.Errorf("usage %s ", args) } flags, err := k8s.ConfigFlags(fm) if err != nil { return err } config, err := flags.ToRawKubeConfigLoader().RawConfig() if err != nil { return err } if cn == "" { cn = config.CurrentContext } else { config.CurrentContext = cn } if err = api.MinifyConfig(&config); err != nil { return fmt.Errorf("unable to load context %q: %w", cn, err) } // Ensure that all certs are embedded instead of reachable using a path if err = api.FlattenConfig(&config); err != nil { return fmt.Errorf("unable to flatten context %q: %w", cn, err) } cc := config.Contexts[cn] ai, ok := config.AuthInfos[cc.AuthInfo] if !ok { return fmt.Errorf("unable to load authinfo %q for context %q", cc.AuthInfo, cn) } var data []byte if ec := ai.Exec; ec != nil { data, err = resolveExec(ec) } else { data, err = resolveCreds(ai, config.Clusters[cc.Cluster]) } if err != nil { return err } _, err = os.Stdout.Write(data) return err } func resolveCreds(ai *api.AuthInfo, cl *api.Cluster) ([]byte, error) { st := clientauthentication.ExecCredentialStatus{ Token: ai.Token, } if len(ai.ClientCertificateData) > 0 { st.ClientCertificateData = string(ai.ClientCertificateData) } if len(ai.ClientKeyData) > 0 { st.ClientKeyData = string(ai.ClientKeyData) } creds := clientauthentication.ExecCredential{ TypeMeta: meta.TypeMeta{ Kind: "ExecCredential", APIVersion: "client.authentication.k8s.io/v1beta1", }, Spec: clientauthentication.ExecCredentialSpec{ Interactive: false, }, Status: &st, } if cl != nil { creds.Spec.Cluster = &clientauthentication.Cluster{ Server: cl.Server, TLSServerName: cl.TLSServerName, InsecureSkipTLSVerify: cl.InsecureSkipTLSVerify, CertificateAuthorityData: cl.CertificateAuthorityData, ProxyURL: cl.ProxyURL, DisableCompression: cl.DisableCompression, } } scheme := runtime.NewScheme() install.Install(scheme) codecs := serializer.NewCodecFactory(scheme) return runtime.Encode(codecs.LegacyCodec(creds.GroupVersionKind().GroupVersion()), &creds) } func resolveExec(execConfig *api.ExecConfig) ([]byte, error) { var buf bytes.Buffer cmd := exec.Command(execConfig.Command, execConfig.Args...) cmd.Stdout = &buf cmd.Stderr = os.Stderr cmd.Env = os.Environ() if len(execConfig.Env) > 0 { em := dos.FromEnvPairs(cmd.Env) for _, ev := range execConfig.Env { em[ev.Name] = ev.Value } cmd.Env = em.Environ() } if err := cmd.Run(); err != nil { return nil, fmt.Errorf("failed to run host command: %w", err) } return buf.Bytes(), nil } ================================================ FILE: integration_test/testdata/otel/helm-yamls/otel-operator.yml ================================================ # Default values for opentelemetry-operator. # This is a YAML-formatted file. # Declare variables to be passed into your templates. replicaCount: 1 ## Provide a name in place of opentelemetry-operator (includes the chart's release name). ## nameOverride: "" ## Fully override the name (excludes the chart's release name). ## fullnameOverride: "" ## Reference one or more secrets to be used when pulling images from authenticated repositories. imagePullSecrets: [] ## Kubernetes cluster domain suffix clusterDomain: cluster.local # Common labels to add to all otel-operator resources. Evaluated as a template. additionalLabels: {} ## Pod Disruption Budget configuration ## pdb: ## Enable/disable a Pod Disruption Budget creation ## create: false ## Minimum number/percentage of pods that should remain scheduled ## minAvailable: 1 ## Maximum number/percentage of pods that may be made unavailable ## maxUnavailable: "" ## Provide OpenTelemetry Operator manager container image and resources. ## manager: image: repository: ghcr.io/open-telemetry/opentelemetry-operator/opentelemetry-operator tag: 0.112.0 collectorImage: repository: "" tag: 0.120.0 opampBridgeImage: repository: "" tag: "" targetAllocatorImage: repository: "" tag: "" autoInstrumentationImage: java: repository: "" tag: "" nodejs: repository: "" tag: "" python: repository: "" tag: "" dotnet: repository: "" tag: "" apacheHttpd: repository: "" tag: "" # The Go instrumentation support in the operator is disabled by default. # To enable it, use the operator.autoinstrumentation.go feature gate. go: repository: "" tag: "" # Feature Gates are a comma-delimited list of feature gate identifiers. # Prefix a gate with '-' to disable support. # Prefixing a gate with '+' or no prefix will enable support. # A full list of valid identifiers can be found here: https://github.com/open-telemetry/opentelemetry-operator/blob/main/pkg/featuregate/featuregate.go # NOTE: the featureGates value is deprecated and will be replaced by featureGatesMap in the future. featureGates: "" # The featureGatesMap will enable or disable specific feature gates in the operator as well as deploy any prerequisites for the feature gate. # If this property is not an empty map, the featureGates property will be ignored. featureGatesMap: {} # operator.targetallocator.mtls: false # operator.targetallocator.fallbackstrategy: false # operator.collector.targetallocatorcr: false # operator.sidecarcontainers.native: false # operator.observability.prometheus: false # operator.golang.flags: false # operator.collector.default.config: false ports: metricsPort: 8080 webhookPort: 9443 healthzPort: 8081 resources: {} # resources: # limits: # cpu: 100m # memory: 128Mi # ephemeral-storage: 50Mi # requests: # cpu: 100m # memory: 64Mi # ephemeral-storage: 50Mi ## Adds additional environment variables. This property will be deprecated. Please use extraEnvs instead. ## e.g ENV_VAR: env_value env: ENABLE_WEBHOOKS: "true" # Extra definitions of environment variables. extraEnvs: [] # - name: GOMEMLIMIT # valueFrom: # resourceFieldRef: # containerName: manager # resource: limits.memory # -- Create the manager ServiceAccount serviceAccount: create: true annotations: {} ## Override the default name of the serviceaccount (the name of your installation) name: "" ## Enable ServiceMonitor for Prometheus metrics scrape serviceMonitor: enabled: false # additional labels on the ServiceMonitor extraLabels: {} # add annotations on the ServiceMonitor annotations: {} metricsEndpoints: - port: metrics # Used to set relabeling and metricRelabeling configs on the ServiceMonitor # https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config relabelings: [] metricRelabelings: [] # Adds additional annotations to the manager Deployment deploymentAnnotations: {} # Adds additional annotations to the manager Service serviceAnnotations: {} podAnnotations: {} podLabels: {} prometheusRule: enabled: false groups: [] # Create default rules for monitoring the manager defaultRules: enabled: false ## Additional labels for PrometheusRule alerts additionalRuleLabels: {} ## Additional annotations for PrometheusRule alerts additionalRuleAnnotations: {} ## Alerts are considered firing once they have been returned for this long. duration: 5m # additional labels on the PrometheusRule object extraLabels: {} # add annotations on the PrometheusRule object annotations: {} # change the default runbook urls. # the alert name will get appended at the end of the url as an anchor. runbookUrl: "" # Whether the operator should create RBAC permissions for collectors. See README.md for more information. createRbacPermissions: false ## List of additional cli arguments to configure the manager ## for example: --labels, etc. extraArgs: [] ## Enable leader election mechanism for protecting against split brain if multiple operator pods/replicas are started. ## See more at https://docs.openshift.com/container-platform/4.10/operators/operator_sdk/osdk-leader-election.html leaderElection: enabled: true # Enable vertical pod autoscaler support for the manager verticalPodAutoscaler: enabled: false # List of resources that the vertical pod autoscaler can control. Defaults to cpu, memory and ephemeral-storage. controlledResources: [] # Define the max allowed resources for the pod maxAllowed: {} # cpu: 200m # memory: 100Mi # ephemeral-storage: 50Mi # Define the min allowed resources for the pod minAllowed: {} # cpu: 200m # memory: 100Mi # ephemeral-storage: 50Mi updatePolicy: # Specifies whether recommended updates are applied when a Pod is started and whether recommended updates # are applied during the life of a Pod. Possible values are "Off", "Initial", "Recreate", and "Auto". updateMode: Auto # Minimal number of replicas which need to be alive for Updater to attempt pod eviction. # Only positive values are allowed. The default is 2. minReplicas: 2 # Enable manager pod automatically rolling rolling: false ## Container specific securityContext ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container securityContext: {} # allowPrivilegeEscalation: false # capabilities: # drop: # - ALL ## Provide OpenTelemetry Operator kube-rbac-proxy container image. ## kubeRBACProxy: enabled: true image: repository: quay.io/brancz/kube-rbac-proxy tag: v0.18.1 ports: proxyPort: 8443 resources: {} # resources: # limits: # cpu: 500m # memory: 128Mi # requests: # cpu: 5m # memory: 64Mi ## List of additional cli arguments to configure the kube-rbac-proxy ## for example: --tls-cipher-suites, --tls-min-version, etc. extraArgs: [] ## Container specific securityContext ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container securityContext: {} # allowPrivilegeEscalation: false # capabilities: # drop: # - ALL ## Admission webhooks make sure only requests with correctly formatted rules will get into the Operator. ## They also enable the sidecar injection for OpenTelemetryCollector and Instrumentation CR's admissionWebhooks: create: true servicePort: 443 failurePolicy: Fail secretName: "" ## Defines the sidecar injection logic in Pods. ## - Ignore, the injection is fail-open. The pod will be created, but the sidecar won't be injected. ## - Fail, the injection is fail-close. If the webhook pod is not ready, pods cannot be created. pods: failurePolicy: Ignore ## Adds a prefix to the mutating webhook name. ## This can be used to order this mutating webhook with all your cluster's mutating webhooks. namePrefix: "" ## Customize webhook timeout duration timeoutSeconds: 10 ## Provide selectors for your objects namespaceSelector: {} objectSelector: {} ## https://github.com/open-telemetry/opentelemetry-helm-charts/blob/main/charts/opentelemetry-operator/README.md#tls-certificate-requirement ## TLS Certificate Option 1: Use certManager to generate self-signed certificate. ## certManager must be enabled. If enabled, always takes precedence over options 2 and 3. certManager: enabled: true ## Provide the issuer kind and name to do the cert auth job. ## By default, OpenTelemetry Operator will use self-signer issuer. issuerRef: {} # kind: # name: ## Annotations for the cert and issuer if cert-manager is enabled. certificateAnnotations: {} issuerAnnotations: {} # duration must be specified by a Go time.Duration (ending in s, m or h) duration: "" # renewBefore must be specified by a Go time.Duration (ending in s, m or h) # Take care when setting the renewBefore field to be very close to the duration # as this can lead to a renewal loop, where the Certificate is always in the renewal period. renewBefore: "" ## TLS Certificate Option 2: Use Helm to automatically generate self-signed certificate. ## certManager must be disabled and autoGenerateCert must be enabled. ## If true and certManager.enabled is false, Helm will automatically create a self-signed cert and secret for you. autoGenerateCert: enabled: true # If set to true, new webhook key/certificate is generated on helm upgrade. recreate: true # Cert period time in days. The default is 365 days. certPeriodDays: 365 ## TLS Certificate Option 3: Use your own self-signed certificate. ## certManager and autoGenerateCert must be disabled and certFile, keyFile, and caFile must be set. ## The chart reads the contents of the file paths with the helm .Files.Get function. ## Refer to this doc https://helm.sh/docs/chart_template_guide/accessing_files/ to understand ## limitations of file paths accessible to the chart. ## Path to your own PEM-encoded certificate. certFile: "" ## Path to your own PEM-encoded private key. keyFile: "" ## Path to the CA cert. caFile: "" # Adds additional annotations to the admissionWebhook Service serviceAnnotations: {} ## Secret annotations secretAnnotations: {} ## Secret labels secretLabels: {} ## Install CRDS with the right webhook settings ## These are installed as templates, so they will clash with existing OpenTelemetry Operator CRDs in your cluster that are not already managed by the helm chart. ## See https://github.com/open-telemetry/opentelemetry-helm-charts/blob/main/charts/opentelemetry-operator/UPGRADING.md#0560-to-0570 for more details. crds: create: true ## Create the provided Roles and RoleBindings ## role: create: true ## Create the provided ClusterRoles and ClusterRoleBindings ## clusterRole: create: true affinity: {} tolerations: [] nodeSelector: {} topologySpreadConstraints: [] hostNetwork: false # Allows for pod scheduler prioritisation priorityClassName: "" ## SecurityContext holds pod-level security attributes and common container settings. ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ securityContext: runAsGroup: 65532 runAsNonRoot: true runAsUser: 65532 fsGroup: 65532 testFramework: image: repository: busybox tag: latest ================================================ FILE: integration_test/testdata/otel/instrumentation.yml ================================================ apiVersion: opentelemetry.io/v1alpha1 kind: Instrumentation metadata: name: otel-instrumentation spec: exporter: endpoint: http://alloy.__NAMESPACE__.svc.cluster.local:4318 propagators: - tracecontext - baggage sampler: type: parentbased_traceidratio # see https://opentelemetry.io/docs/languages/erlang/sampling/#parentbasedsampler argument: ".2" env: - name: OTEL_EXPORTER_OTLP_INSECURE value: "true" - name: OTEL_LOG_LEVEL value: "debug" - name: OTEL_TRACES_EXPORTER value: "otlp" - name: OTEL_METRICS_EXPORTER value: "none" - name: OTEL_LOGS_EXPORTER value: "none" # - name: PYROSCOPE_SERVER_ADDRESS # value: "http://pyroscope.__NAMESPACE__.svc.cluster.local:4040" - name: PYROSCOPE_ENABLED value: "false" - name: OTEL_PROFILING_START_PROFILING value: "false" go: image: ghcr.io/open-telemetry/opentelemetry-go-instrumentation/autoinstrumentation-go:v0.23.0 env: - name: OTEL_EXPORTER_OTLP_ENDPOINT value: http://alloy.__NAMESPACE__.svc.cluster.local:4318 java: image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:latest env: - name: OTEL_EXPORTER_OTLP_ENDPOINT value: http://alloy.__NAMESPACE__.svc.cluster.local:4318 ================================================ FILE: integration_test/testdata/routing-values.yaml ================================================ client: routing: # add the following subnets to the client's virtual network interface # array of strings alsoProxySubnets: ["11.11.11.11/32", "12.12.12.12/32"] neverProxySubnets: ["13.11.11.11/32", "14.12.12.12/32"] ================================================ FILE: integration_test/testdata/scripts/veth-down.sh ================================================ #!/usr/bin/bash ip link set vm2 down ip link set brm down for var in "$@" do ip addr del "$(echo "$var" | sed -r 's/\.0$/.1/')" dev brm ip addr del "$(echo "$var" | sed -r 's/\.0$/.2/')" dev vm2 done ip link del brm type bridge ip link set dev tapm down ip tuntap del tapm mode tap ip link set dev vm1 down ip link del dev vm1 type veth peer name vm2 ================================================ FILE: integration_test/testdata/scripts/veth-up.sh ================================================ #!/usr/bin/bash ip link add dev vm1 type veth peer name vm2 ip link set dev vm1 up ip tuntap add tapm mode tap ip link set dev tapm up ip link add brm type bridge ip link set tapm master brm ip link set vm1 master brm for var in "$@" do ip addr add "$(echo "$var" | sed -r 's/\.0$/.1/')" dev brm ip addr add "$(echo "$var" | sed -r 's/\.0$/.2/')" dev vm2 done ip link set brm up ip link set vm2 up ================================================ FILE: integration_test/testdata/stdiotest/main.go ================================================ package main import ( "fmt" "io" "os" "github.com/spf13/cobra" ) type runner struct { useStdErr bool noNL bool } func main() { r := runner{} cmd := cobra.Command{ Use: "stdiotest", Short: "Test stdin, stdout, and stderr", Long: "stdiotest will echo either echo its arguments, or when no arguments are given, its stdin", RunE: r.run, } flags := cmd.Flags() flags.BoolVarP(&r.useStdErr, "stderr", "e", false, "send output to stderr (default is stdout)") flags.BoolVarP(&r.useStdErr, "nonl", "n", false, "don't append a newline to each echoed argument (invalid when echoing stdin)") if err := cmd.Execute(); err != nil { fmt.Fprintln(os.Stderr, err.Error()) os.Exit(1) } } func (r runner) run(cmd *cobra.Command, args []string) error { var out io.Writer if r.useStdErr { out = cmd.ErrOrStderr() } else { out = cmd.OutOrStdout() } if len(args) == 0 { _, err := io.Copy(out, cmd.InOrStdin()) return err } if r.noNL { for _, arg := range args { if _, err := out.Write([]byte(arg)); err != nil { return err } } } else { for _, arg := range args { if _, err := fmt.Fprintln(out, arg); err != nil { return err } } } return nil } ================================================ FILE: integration_test/testdata/udp-echo/Dockerfile ================================================ FROM golang:alpine3.15 AS builder WORKDIR /udp-echo COPY go.mod . COPY main.go . RUN go build -o udp-echo . FROM alpine:3.15 COPY --from=builder /udp-echo/udp-echo /usr/local/bin/ ENTRYPOINT ["/usr/local/bin/udp-echo"] ================================================ FILE: integration_test/testdata/udp-echo/go.mod ================================================ module local go 1.19 ================================================ FILE: integration_test/testdata/udp-echo/main.go ================================================ package main import ( "fmt" "net" "os" "strings" "sync" ) func main() { portsEnv := os.Getenv("PORTS") if portsEnv == "" { portsEnv = os.Getenv("PORT") } if portsEnv == "" { portsEnv = "8080" } ports := strings.Split(portsEnv, ",") wg := sync.WaitGroup{} wg.Add(len(ports)) for _, port := range ports { port := port // pin it go func() { defer wg.Done() fmt.Printf("UDP-echo server listening on port %s.\n", port) defer fmt.Printf("UDP-echo server on port %s exited.\n", port) pc, err := net.ListenPacket("udp", ":"+port) if err == nil { err = serveConnection(pc) } if err != nil { fmt.Fprintln(os.Stderr, err.Error()) } }() } wg.Wait() } func serveConnection(pc net.PacketConn) error { buf := [0x10000]byte{} pfx := []byte("Reply from UDP-echo: ") for { n, addr, err := pc.ReadFrom(buf[:]) if n > 0 { sb := string(buf[:n]) if strings.HasSuffix(sb, "\n") { fmt.Print(sb) } else { fmt.Println(sb) } if n == 5 && sb == "exit\n" { return nil } r := make([]byte, len(pfx)+n) copy(r, pfx) copy(r[len(pfx):], buf[:n]) if _, werr := pc.WriteTo(r, addr); werr != nil { fmt.Fprintln(os.Stderr, werr.Error()) } } if err != nil { return err } } } ================================================ FILE: integration_test/tls_test.go ================================================ package integration_test import ( "context" "fmt" "path/filepath" "regexp" "strings" "sync" "time" core "k8s.io/api/core/v1" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/annotation" "github.com/telepresenceio/telepresence/v2/pkg/client/docker" ) func (s *dockerDaemonSuite) Test_TLSAnnotations() { if !(s.ManagerIsVersion(">2.24.x") && s.ClientIsVersion(">2.24.x")) { s.T().Skip("Not part of compatibility tests. Versions < 2.25.0 have no support for http intercepts") } const ( svc = "hello" containerPort = 8443 image = "echo-server:latest" registry = "ghcr.io/telepresenceio" ) tlsAppTemplate := itest.Generic{ Name: svc, ServicePorts: []itest.ServicePort{ { Number: 443, Name: "https", TargetPort: "https", }, }, ContainerPorts: []itest.ContainerPort{ { Number: containerPort, Name: "https", }, }, Environment: []core.EnvVar{ { Name: "PORTS", Value: fmt.Sprintf("%d:https", containerPort), }, }, Image: image, Registry: registry, Volumes: []core.Volume{ { Name: "tls", VolumeSource: core.VolumeSource{ Secret: &core.SecretVolumeSource{SecretName: "tel-cert"}, }, }, }, VolumeMounts: []core.VolumeMount{ { Name: "tls", ReadOnly: true, MountPath: "/certs", }, }, } ctx := docker.EnableClient(s.Context()) rq := s.Require() dockerCli, err := docker.GetClient(ctx) rq.NoError(err) k8s := filepath.Join(itest.GetOSSRoot(ctx), "testdata", "k8s") genericPath := filepath.Join(k8s, "generic.goyaml") rq.NoError(itest.Kubectl(ctx, s.AppNamespace(), "apply", "-f", filepath.Join(k8s, "tel-cert.yaml"))) s.TelepresenceConnect(ctx, "--docker") defer itest.TelepresenceDisconnectOk(ctx) type testData struct { testName string plainText bool header string anns map[string]string errorPattern string } tts := []testData{ { "Client TLS and cert secret", false, "x:y", map[string]string{ annotation.DownstreamTLSSecret + fmt.Sprintf(".%d", containerPort): "tel-cert", annotation.UpstreamInsecureSkipVerify + fmt.Sprintf(".%d", containerPort): "enabled", annotation.InjectTrafficAgent: "enabled", }, "", }, { "Client plaintext and cert secret", true, "x:y", map[string]string{ annotation.DownstreamTLSSecret + fmt.Sprintf(".%d", containerPort): "tel-cert", annotation.UpstreamInsecureSkipVerify + fmt.Sprintf(".%d", containerPort): "enabled", annotation.InjectTrafficAgent: "enabled", }, "", }, { "Client TLS and cert path", false, "x:y", map[string]string{ annotation.DownstreamCertificatePath + fmt.Sprintf(".%d", containerPort): "/certs", annotation.UpstreamInsecureSkipVerify + fmt.Sprintf(".%d", containerPort): "enabled", annotation.InjectTrafficAgent: "enabled", }, "", }, { "Client TLS and cert path, not insecure", false, "x:y", map[string]string{ annotation.DownstreamCertificatePath + fmt.Sprintf(".%d", containerPort): "/certs", annotation.InjectTrafficAgent: "enabled", }, "failed to verify certificate", }, { "Client mTLS and cert path", false, "x:y", map[string]string{ annotation.DownstreamCertificatePath + fmt.Sprintf(".%d", containerPort): "/certs", annotation.UpstreamCertificatePath + fmt.Sprintf(".%d", containerPort): "/certs", annotation.UpstreamInsecureSkipVerify + fmt.Sprintf(".%d", containerPort): "enabled", annotation.InjectTrafficAgent: "enabled", }, "", }, } for i, tt := range tts { s.Run(tt.testName, func() { ctx = s.Context() ttSvc := fmt.Sprintf("%s-%d", svc, i) tpl := tlsAppTemplate tpl.Name = ttSvc tpl.Annotations = tt.anns s.ApplyTemplate(ctx, genericPath, &tpl) defer s.DeleteTemplate(ctx, genericPath, &tpl) rq := s.Require() ctx, cancel := context.WithCancel(ctx) wg := sync.WaitGroup{} wg.Add(1) defer wg.Wait() defer cancel() go func() { defer wg.Done() defer cancel() args := make([]string, 0, 15) args = append(args, "intercept", ttSvc) if tt.plainText { args = append(args, "--plaintext", "-p", "8080:https") } else { args = append(args, "-p", "8443:https") } if tt.header != "" { args = append(args, "--http-header", tt.header) } args = append(args, "--mount=false", "--docker-run", "--", "--name", ttSvc+".local") if tt.plainText { args = append(args, "-e", "PORTS=8080:http") } args = append(args, registry+"/"+image) stdout, _, err := itest.Telepresence(ctx, args...) if err != nil { clog.Errorf(ctx, "stdout: %s", stdout) clog.Error(ctx, err) } else { clog.Infof(ctx, "stdout: %s", stdout) } }() rq.EventuallyContext(ctx, func() bool { ir, err := dockerCli.ContainerInspect(ctx, ttSvc+".local") return err == nil && ir.State.Running }, 30*time.Second, 3*time.Second, "expected intercepted status never arrived") s.CapturePodLogs(ctx, ttSvc, "traffic-agent", s.AppNamespace()) si := itest.TelepresenceStatusOk(ctx) rq.True(len(si.UserDaemon.Intercepts) == 1) rq.Equal(si.UserDaemon.Intercepts[0].Name, ttSvc) rq.EventuallyContext(ctx, func() bool { args := []string{"curl", "--max-time", "2", "-s", "-w", "\nStatus: %{http_code}\n", "-k"} if tt.header != "" { args = append(args, "-H", tt.header) } args = append(args, fmt.Sprintf("https://%s", ttSvc)) so, se, err := itest.Telepresence(ctx, args...) if err != nil { if se != "" { clog.Error(ctx, se) } clog.Error(ctx, err) return false } clog.Info(ctx, so) if tt.errorPattern != "" { ok, err := regexp.MatchString(tt.errorPattern, so) return err == nil && ok } return strings.Contains(so, "HTTP/2.0 GET /") }, 10*time.Second, 3*time.Second, "expected curl response never arrived") // Terminate the ongoing intercept so, se, err := itest.Telepresence(ctx, "leave", ttSvc) if so != "" { clog.Info(ctx, so) } if se != "" { clog.Info(ctx, se) } if err != nil { clog.Error(ctx, err) } cancel() }) } } ================================================ FILE: integration_test/to_pod_test.go ================================================ package integration_test import ( "fmt" "net" "regexp" "sync" "time" "github.com/telepresenceio/telepresence/v2/integration_test/itest" ) func (s *connectedSuite) Test_ToPodPortForwarding() { const svc = "echo-w-sidecars" ctx := s.Context() s.ApplyApp(ctx, svc, "deploy/"+svc) defer s.DeleteSvcAndWorkload(ctx, "deploy", svc) require := s.Require() stdout := itest.TelepresenceOk(ctx, "intercept", "--mount", "false", svc, "--port", "8080", "--to-pod", "8081", "--to-pod", "8082") defer itest.TelepresenceOk(ctx, "leave", svc) require.Contains(stdout, "Using Deployment "+svc) s.Eventually(func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--intercepts") return err == nil && regexp.MustCompile(svc+`\s*: intercepted`).MatchString(stdout) }, 10*time.Second, time.Second) wg := sync.WaitGroup{} wg.Add(1) go func() { defer wg.Done() s.Eventually(func() bool { return itest.Run(ctx, "curl", "--silent", "--max-time", "0.5", "localhost:8081") == nil }, 30*time.Second, 2*time.Second, "Forwarded port is not reachable as localhost:8081") }() wg.Add(1) go func() { defer wg.Done() s.Eventually(func() bool { return itest.Run(ctx, "curl", "--silent", "--max-time", "0.5", "localhost:8082") == nil }, 30*time.Second, 2*time.Second, "Forwarded port is not reachable as localhost:8082") }() wg.Add(1) go func() { defer wg.Done() s.Eventually(func() bool { return itest.Run(ctx, "curl", "--silent", "--max-time", "0.5", "localhost:8083") != nil }, 30*time.Second, 2*time.Second, "Non-forwarded port is reachable") }() wg.Wait() } func (s *connectedSuite) Test_ToPodUDPPortForwarding() { const svc = "echo-extra-udp" ctx := s.Context() s.ApplyApp(ctx, svc, "deploy/"+svc) defer s.DeleteSvcAndWorkload(ctx, "deploy", svc) require := s.Require() stdout := itest.TelepresenceOk(ctx, "intercept", "--mount", "false", svc, "--port", "9080", "--to-pod", "8080/UDP") defer itest.TelepresenceOk(ctx, "leave", svc) require.Contains(stdout, "Using Deployment "+svc) stdout = itest.TelepresenceOk(ctx, "list", "--intercepts") require.Contains(stdout, svc+": intercepted") itest.TelepresenceOk(ctx, "loglevel", "trace") defer itest.TelepresenceOk(ctx, "loglevel", "debug") s.CapturePodLogs(ctx, svc, "traffic-agent", s.AppNamespace()) conn, err := net.Dial("udp", "localhost:8080") require.NoError(err) defer conn.Close() pingPong := func(msg string) { _ = conn.SetDeadline(time.Now().Add(10 * time.Second)) bm := []byte(msg) n, err := conn.Write(bm) require.NoError(err) require.Equal(len(bm), n) buf := [0x100]byte{} n, err = conn.Read(buf[:]) require.NoError(err) require.Equal(fmt.Sprintf("Reply from UDP-echo: %s", msg), string(buf[0:n])) } pingPong("12345678") pingPong("a slightly longer message") } ================================================ FILE: integration_test/udp_test.go ================================================ package integration_test import ( "errors" "fmt" "io" "net" "runtime" "strings" "time" ) func (s *connectedSuite) TestUDPEcho() { ctx := s.Context() require := s.Require() svc := "udp-echo" tag := "ghcr.io/telepresenceio/udp-echo:latest" require.NoError(s.Kubectl(ctx, "create", "deploy", svc, "--image", tag)) require.NoError(s.Kubectl(ctx, "expose", "deploy", svc, "--port", "80", "--protocol", "UDP", "--target-port", "8080")) defer func() { _ = s.Kubectl(ctx, "delete", "svc,deploy", svc) }() require.NoError(s.RolloutStatusWait(ctx, "deploy/"+svc)) s.CapturePodLogs(ctx, svc, "udp-echo", s.AppNamespace()) var conn net.Conn require.Eventually( func() bool { var err error conn, err = net.Dial("udp", fmt.Sprintf("%s.%s:80", svc, s.AppNamespace())) return err == nil }, 12*time.Second, // waitFor 3*time.Second, // polling interval `dial never succeeds`) defer conn.Close() mb := strings.Builder{} mb.WriteString("This is ") itm := "a russian doll containing " count := 1000 if runtime.GOOS == "darwin" { // Max UDP message size is 9216 bytes count = 9000 / len(itm) } for i := 0; i < count; i++ { mb.WriteString(itm) } mb.WriteString("a solid russian doll") buf := [0x10000]byte{} echoTest := func(msg string) { _, err := conn.Write([]byte(msg)) require.NoError(err) require.NoError(conn.SetReadDeadline(time.Now().Add(5 * time.Second))) n, err := conn.Read(buf[:]) require.Greater(n, 0) if errors.Is(err, io.EOF) { err = nil } require.NoError(err) rp := "Reply from UDP-echo: " pl := len(rp) require.Equal(string(buf[:pl]), rp) require.Equal(len(msg)+pl, n) require.Equal(msg, string(buf[pl:n])) } // A UDP Dial will succeed immediately because it doesn't really connect, and even though the deployment is ready, the service // might not be listening just yet (there's no readiness probe). So we sleep a bit to give the service time to start. time.Sleep(2 * time.Second) echoTest("Hello") echoTest(mb.String()) } ================================================ FILE: integration_test/uhn_dns_test.go ================================================ package integration_test import ( "fmt" "net" "time" "github.com/stretchr/testify/assert" "k8s.io/client-go/tools/clientcmd/api" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/iputil" ) type unqualifiedHostNameDNSSuite struct { itest.Suite itest.TrafficManager } func (s *unqualifiedHostNameDNSSuite) SuiteName() string { return "UnqualifiedHostNameDNS" } func init() { itest.AddTrafficManagerSuite("", func(h itest.TrafficManager) itest.TestingSuite { return &unqualifiedHostNameDNSSuite{Suite: itest.Suite{Harness: h}, TrafficManager: h} }) } func (s *unqualifiedHostNameDNSSuite) TearDownTest() { itest.TelepresenceQuitOk(s.Context()) } func (s *unqualifiedHostNameDNSSuite) Test_UHNExcludes() { // given ctx := s.Context() serviceName := "hey" port, svcCancel := itest.StartLocalHttpEchoServer(ctx, serviceName) defer svcCancel() itest.ApplyEchoService(ctx, serviceName, s.AppNamespace(), port) defer s.DeleteSvcAndWorkload(ctx, "deploy", serviceName) excludes := []string{ serviceName, fmt.Sprintf("%s.%s", serviceName, s.AppNamespace()), fmt.Sprintf("%s.%s.svc.cluster.local", serviceName, s.AppNamespace()), } ctx = itest.WithKubeConfigExtension(ctx, func(cluster *api.Cluster) map[string]any { return map[string]any{"dns": map[string][]string{ "excludes": excludes, }} }) // when s.TelepresenceConnect(ctx, "--context", "extra") // then for _, excluded := range excludes { s.Eventually(func() bool { conn, err := net.DialTimeout("tcp", iputil.JoinHostPort(excluded, uint16(port)), 2*time.Second) if err == nil { clog.Errorf(ctx, "excluded DNS name %s resolved to %s", excluded, conn.RemoteAddr()) _ = conn.Close() } return err != nil }, 10*time.Second, 1*time.Second, "should not be able to reach %s", excluded) } status := itest.TelepresenceStatusOk(s.Context()) assert.Equal(s.T(), excludes, status.RootDaemon.DNS.Excludes, "Excludes in output") } func (s *unqualifiedHostNameDNSSuite) Test_UHNMappings() { // given ctx := s.Context() serviceName := "echo" port := 80 itest.ApplyEchoService(ctx, serviceName, s.AppNamespace(), port) defer s.DeleteSvcAndWorkload(ctx, "deploy", serviceName) localPort, cancel := itest.StartLocalHttpEchoServer(ctx, "my-hello") defer cancel() aliasedService := fmt.Sprintf("%s.%s", serviceName, s.AppNamespace()) dnsMappings := client.DNSMappings{ { Name: "my-alias", AliasFor: aliasedService, }, { Name: fmt.Sprintf("my-alias.%s", s.AppNamespace()), AliasFor: aliasedService, }, { Name: "my-alias.vx-root-domain.cluster.local", AliasFor: aliasedService, }, { Name: "my-hello", AliasFor: "127.0.0.1", }, } mappings := make([]map[string]string, len(dnsMappings)) for i, dm := range dnsMappings { mappings[i] = map[string]string{"name": dm.Name, "aliasFor": dm.AliasFor} } ctx = itest.WithKubeConfigExtension(ctx, func(cluster *api.Cluster) map[string]any { return map[string]any{"dns": map[string]client.DNSMappings{ "mappings": dnsMappings, }} }) // when s.TelepresenceConnect(ctx, "--context", "extra") // then for _, mapping := range dnsMappings { dialPort := port if mapping.Name == "my-hello" { dialPort = localPort } s.Eventually(func() bool { conn, err := net.DialTimeout("tcp", iputil.JoinHostPort(mapping.Name, uint16(dialPort)), 5000*time.Millisecond) if err == nil { _ = conn.Close() } return err == nil }, 10*time.Second, 1*time.Second, "can find alias %s", mapping.Name) } status := itest.TelepresenceStatusOk(s.Context()) assert.Equal(s.T(), dnsMappings, status.RootDaemon.DNS.Mappings, "Mappings in output") } ================================================ FILE: integration_test/uninstall_test.go ================================================ package integration_test import ( "fmt" "regexp" "strings" "time" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/agentconfig" ) func (s *notConnectedSuite) Test_Uninstall() { require := s.Require() // The telepresence-test-developer will not be able to uninstall everything ctx := itest.WithUser(s.Context(), "default") s.TelepresenceConnect(ctx) names := func() (string, error) { return itest.KubectlOut(ctx, s.ManagerNamespace(), "get", "svc,deploy", agentconfig.ManagerAppName, "--ignore-not-found", "-o", "jsonpath={.items[*].metadata.name}") } stdout, err := names() require.NoError(err) require.Equal(2, len(strings.Split(stdout, " ")), "the string %q doesn't contain a service and a deployment", stdout) // Add webhook agent to test webhook uninstall jobname := "echo-auto-inject" deployname := "deploy/" + jobname s.ApplyApp(ctx, jobname, deployname) defer s.DeleteSvcAndWorkload(ctx, "deploy", jobname) verb := "engage" if !s.ClientIsVersion(">2.21.x") { verb = "intercept" } s.Eventually(func() bool { stdout, _, err = itest.Telepresence(ctx, "list", "--agents") return err == nil && strings.Contains(stdout, fmt.Sprintf("%s: ready to %s (traffic-agent already installed)", jobname, verb)) }, 30*time.Second, 3*time.Second) stdout = itest.TelepresenceOk(ctx, "helm", "uninstall", "-n", s.ManagerNamespace()) defer s.TelepresenceHelmInstallOK(ctx, false) s.Contains(stdout, "Traffic Manager uninstalled successfully") // Double check webhook agent is uninstalled require.NoError(s.RolloutStatusWait(ctx, deployname)) s.Eventually(func() bool { stdout, err = s.KubectlOut(ctx, "get", "pods") if err != nil { clog.Error(ctx, err) return false } match, err := regexp.MatchString(jobname+`-[a-z0-9]+-[a-z0-9]+\s+1/1\s+Running`, stdout) if err != nil { clog.Error(ctx, err) return false } if !match { clog.Infof(ctx, "stdout = %s", stdout) } return err == nil && match }, itest.PodCreateTimeout(ctx), 2*time.Second) require.Eventually( func() bool { stdout, _ := names() return stdout == "" }, 5*time.Second, // waitFor 500*time.Millisecond, // polling interval ) } ================================================ FILE: integration_test/webhook_test.go ================================================ package integration_test import ( "fmt" "strings" "time" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/client" ) type webhookSuite struct { itest.Suite itest.TrafficManager } func (s *webhookSuite) SuiteName() string { return "Webhook" } func init() { itest.AddConnectedSuite("", func(h itest.TrafficManager) itest.TestingSuite { return &webhookSuite{Suite: itest.Suite{Harness: h}, TrafficManager: h} }) } func (s *webhookSuite) Test_AutoInjectedAgent() { ctx := s.Context() s.ApplyApp(ctx, "echo-auto-inject", "deploy/echo-auto-inject") defer s.DeleteSvcAndWorkload(ctx, "deploy", "echo-auto-inject") require := s.Require() verb := "engage" if !s.ClientIsVersion(">2.21.x") { verb = "intercept" } require.Eventually(func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--agents") return err == nil && strings.Contains(stdout, fmt.Sprintf("echo-auto-inject: ready to %s (traffic-agent already installed)", verb)) }, 20*time.Second, // waitFor 2*time.Second, // polling interval "doesn't show up with agent installed in list output", ) stdout := itest.TelepresenceOk(ctx, "intercept", "--mount", "false", "echo-auto-inject", "--port", "9091") defer itest.TelepresenceOk(ctx, "leave", "echo-auto-inject") require.Contains(stdout, "Using Deployment echo-auto-inject") stdout = itest.TelepresenceOk(ctx, "list", "--intercepts") require.Contains(stdout, "echo-auto-inject: intercepted") } func (s *notConnectedSuite) Test_AgentImageFromConfig() { // Use a config with agentImage to validate that it's the // latter that is used in the traffic-manager ctx := itest.WithConfig(s.Context(), func(cfg client.Config) { cfg.Images().PrivateAgentImage = "imageFromConfig:0.0.1" }) s.TelepresenceHelmInstallOK(itest.WithAgentImage(ctx, &itest.Image{}), true) defer s.RollbackTM(ctx) s.TelepresenceConnect(ctx) defer itest.TelepresenceQuitOk(ctx) st := itest.TelepresenceStatusOk(ctx) s.Require().NotNil(st.TrafficManager) s.Equal(s.ManagerRegistry()+"/imageFromConfig:0.0.1", st.TrafficManager.TrafficAgent) } ================================================ FILE: integration_test/wiretap_test.go ================================================ package integration_test import ( "context" "fmt" "math/rand" "net" "net/http" "path/filepath" "strings" "sync" "sync/atomic" "time" "github.com/go-json-experiment/json" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/rpc/v2/connector" "github.com/telepresenceio/telepresence/v2/integration_test/itest" ) type wiretapSuite struct { itest.Suite itest.TrafficManager svc string tplPath string tpl *itest.Generic hitCount int32 } func (s *wiretapSuite) SuiteName() string { return "Wiretap" } func init() { itest.AddTrafficManagerSuite("", func(h itest.TrafficManager) itest.TestingSuite { return &wiretapSuite{Suite: itest.Suite{Harness: h}, TrafficManager: h, svc: "echo-wt"} }) } func (s *wiretapSuite) SetupSuite() { if !(s.ManagerIsVersion(">2.22.x") && s.ClientIsVersion(">2.22.x")) { s.T().Skip("Not part of compatibility tests. The wiretap command was introduced in 2.23") } s.Suite.SetupSuite() s.tplPath = filepath.Join("testdata", "k8s", "generic.goyaml") s.tpl = &itest.Generic{ Name: s.svc, TargetPort: "http", Registry: "ghcr.io/telepresenceio", Image: "echo-server:latest", ServicePorts: []itest.ServicePort{ { Number: 80, Name: "http", TargetPort: "http-cp", }, }, ContainerPorts: []itest.ContainerPort{ { Number: 8080, Name: "http-cp", }, }, } ctx := s.Context() s.ApplyTemplate(ctx, s.tplPath, s.tpl) s.NoError(s.RolloutStatusWait(ctx, "deploy/"+s.svc)) } func (s *wiretapSuite) TearDownSuite() { s.DeleteTemplate(s.Context(), s.tplPath, s.tpl) } func (s *wiretapSuite) startWiretapHandler(ctx context.Context, name, addr string, echoTo *chan string) (int, context.CancelFunc) { ctx, cancel := context.WithCancel(ctx) lc := net.ListenConfig{} l, err := lc.Listen(ctx, "tcp", addr) rq := s.Require() rq.NoError(err, "failed to listen on localhost") port := l.Addr().(*net.TCPAddr).Port sc := &http.Server{ Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { atomic.AddInt32(&s.hitCount, 1) if *echoTo != nil { *echoTo <- fmt.Sprintf("Request served by %s on port %d\n", name, port) } }), } go func() { _ = sc.Serve(l) }() go func() { <-ctx.Done() ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), time.Second) defer cancel() err = sc.Shutdown(ctx) if err != nil { clog.Errorf(ctx, "http server on %s exited with error: %v", addr, err) } else { clog.Errorf(ctx, "http server on %s exited", addr) } }() return port, cancel } func (s *wiretapSuite) Test_MultipleTapsOnOnePort() { //nolint:gocognit ctx := s.Context() s.TelepresenceConnect(ctx) defer itest.TelepresenceQuitOk(ctx) var out1 chan string localPort1, tap1HandlerCancel := s.startWiretapHandler(ctx, s.svc+"-wt1", ":0", &out1) defer tap1HandlerCancel() var out2 chan string localPort2, tap2HandlerCancel := s.startWiretapHandler(ctx, s.svc+"-wt2", ":0", &out2) defer tap2HandlerCancel() localPort3, ihCancel := itest.StartLocalHttpEchoServer(ctx, s.svc) defer ihCancel() s.Run("Place wiretap 1", func() { so := itest.TelepresenceOk(ctx, "wiretap", "--workload", s.svc, "--mount=false", "--port", fmt.Sprintf("%d:80", localPort1), "wt1") s.CapturePodLogs(ctx, s.svc, "traffic-agent", s.AppNamespace()) s.Contains(so, "Using Deployment "+s.svc) }) var podName string s.Run("Find pod with agent", func() { // Retrieve the name of the wiretapped pod ctx := s.Context() s.Eventually(func() bool { pods := itest.RunningPodsWithAgents(ctx, s.svc, s.AppNamespace()) clog.Infof(ctx, "%s pods with agents: %v", s.svc, pods) if len(pods) != 1 { return false } podName = pods[0] return true }, 30*time.Second, 3*time.Second) }) type listOut struct { Cmd string `json:"cmd"` Stdout []connector.WorkloadInfo `json:"stdout"` } s.Run("Place wiretap 2", func() { so := itest.TelepresenceOk(s.Context(), "wiretap", "--workload", s.svc, "--mount=false", "--port", fmt.Sprintf("%d:80", localPort2), "wt2") s.Contains(so, "Using Deployment "+s.svc) }) s.Run("Verify taps", func() { var ok1, ok2 bool out1 = make(chan string, 5) defer close(out1) out2 = make(chan string, 5) defer close(out2) s.Eventually(func() bool { so, err := itest.Output(ctx, "curl", "--silent", "--max-time", "2", s.svc) // Output must yield the standard response from the cluster's service if err != nil { clog.Errorf(ctx, "curl: %s", err) return false } clog.Infof(ctx, "curl output: %s", so) if !strings.Contains(so, `Request served by `+podName) { return false } if !ok1 { select { case out := <-out1: ok1 = strings.Contains(out, fmt.Sprintf("Request served by %s-wt1 on port %d\n", s.svc, localPort1)) default: } } if !ok2 { select { case out := <-out2: ok2 = strings.Contains(out, fmt.Sprintf("Request served by %s-wt2 on port %d\n", s.svc, localPort2)) default: } } return ok1 && ok2 }, 30*time.Second, 3*time.Second) so := itest.TelepresenceOk(ctx, "list", "--wiretaps", "--output", "json") var soj listOut s.Require().NoError(json.Unmarshal([]byte(so), &soj)) if s.Len(soj.Stdout, 1) { iis := soj.Stdout[0].InterceptInfo if s.Len(iis, 2) { s.True(iis[0].Spec.Wiretap) s.True(iis[1].Spec.Wiretap) } } so = itest.TelepresenceOk(ctx, "list", "--intercepts", "--output", "json") soj.Stdout = nil s.Require().NoError(json.Unmarshal([]byte(so), &soj)) s.Len(soj.Stdout, 0) }) s.Run("Verify tap concurrency", func() { // Perform 100 http requests spread out over a one-second period. const requestCount = int32(100) out1 = nil out2 = nil atomic.StoreInt32(&s.hitCount, 0) wg := sync.WaitGroup{} wg.Add(int(requestCount)) for i := 0; i < int(requestCount); i++ { go func() { defer wg.Done() time.Sleep(time.Duration(rand.Float64() * float64(time.Second))) _, err := itest.Output(ctx, "curl", "--silent", "--max-time", "30", s.svc) s.NoError(err) }() } wg.Wait() time.Sleep(time.Second) s.Require().Equal(requestCount*2, atomic.LoadInt32(&s.hitCount)) }) s.Run("Intercept wiretapped service", func() { out1 = make(chan string, 5) defer close(out1) out2 = make(chan string, 5) defer close(out2) so := itest.TelepresenceOk(ctx, "intercept", "--mount=false", "--port", fmt.Sprintf("%d:80", localPort3), s.svc) defer itest.TelepresenceOk(ctx, "leave", s.svc) s.Contains(so, "Using Deployment "+s.svc) itest.PingInterceptedEchoServer(ctx, s.svc, "80") // Both handlers should have produced output. var ok1, ok2 bool s.Eventually(func() bool { if !ok1 { select { case out := <-out1: ok1 = strings.Contains(out, fmt.Sprintf("Request served by %s-wt1 on port %d\n", s.svc, localPort1)) default: } } if !ok2 { select { case out := <-out2: ok2 = strings.Contains(out, fmt.Sprintf("Request served by %s-wt2 on port %d\n", s.svc, localPort2)) default: } } return ok1 && ok2 }, 5*time.Second, 3*time.Second) so = itest.TelepresenceOk(ctx, "list", "--wiretaps", "--output", "json") var soj listOut s.Require().NoError(json.Unmarshal([]byte(so), &soj)) if s.Len(soj.Stdout, 1) { iis := soj.Stdout[0].InterceptInfo if s.Len(iis, 2) { s.True(iis[0].Spec.Wiretap) s.True(iis[1].Spec.Wiretap) } } so = itest.TelepresenceOk(ctx, "list", "--intercepts", "--output", "json") soj.Stdout = nil s.Require().NoError(json.Unmarshal([]byte(so), &soj)) if s.Len(soj.Stdout, 1) { iis := soj.Stdout[0].InterceptInfo if s.Len(iis, 1) { s.False(iis[0].Spec.Wiretap) } } }) s.Run("Verify taps after intercept", func() { out1 = make(chan string, 5) defer close(out1) out2 = make(chan string, 5) defer close(out2) so, err := itest.Output(ctx, "curl", "--silent", "--max-time", "2", s.svc) s.NoError(err) // Output must yield the standard response from the cluster's service s.Contains(so, `Request served by `+podName) var ok1, ok2 bool s.Eventually(func() bool { if !ok1 { select { case out := <-out1: ok1 = strings.Contains(out, fmt.Sprintf("Request served by %s-wt1 on port %d\n", s.svc, localPort1)) default: } } if !ok2 { select { case out := <-out2: ok2 = strings.Contains(out, fmt.Sprintf("Request served by %s-wt2 on port %d\n", s.svc, localPort2)) default: } } return ok1 && ok2 }, 5*time.Second, 3*time.Second) }) s.Run("Wiretaps gone", func() { out1 = make(chan string, 5) defer close(out1) out2 = make(chan string, 5) defer close(out2) itest.TelepresenceOk(ctx, "leave", "wt1") itest.TelepresenceOk(ctx, "leave", "wt2") so, err := itest.Output(ctx, "curl", "--silent", "--max-time", "2", s.svc) s.NoError(err) // Out must yield the standard response from the cluster's service s.Contains(so, `Request served by `+podName) // Handlers should not have produced output. select { case out := <-out1: s.Failf("unexpected output from wiretap handler: %q", out) case out := <-out2: s.Failf("unexpected output from wiretap handler: %q", out) case <-time.After(2 * time.Second): } }) } ================================================ FILE: integration_test/workload_configuration_test.go ================================================ package integration_test import ( "fmt" "strings" "time" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" ) type workloadConfigurationSuite struct { itest.Suite itest.TrafficManager } func (s *workloadConfigurationSuite) SuiteName() string { return "WorkloadConfiguration" } func init() { itest.AddTrafficManagerSuite("-workload-configuration", func(h itest.TrafficManager) itest.TestingSuite { return &workloadConfigurationSuite{Suite: itest.Suite{Harness: h}, TrafficManager: h} }) } func (s *workloadConfigurationSuite) disabledWorkloadKind(tp, wl string) { ctx := s.Context() require := s.Require() s.ApplyApp(ctx, wl, strings.ToLower(tp)+"/"+wl) defer s.DeleteSvcAndWorkload(ctx, strings.ToLower(tp), wl) s.TelepresenceConnect(ctx) defer itest.TelepresenceDisconnectOk(ctx) // give it time for the workload to be detected (if it was going to be) time.Sleep(6 * time.Second) list := itest.TelepresenceOk(ctx, "list") require.Equal("No Workloads (Deployments, StatefulSets, ReplicaSets, or Rollouts)", list) _, stderr, err := itest.Telepresence(ctx, "intercept", wl) require.Error(err) require.Contains(stderr, fmt.Sprintf("connector.CreateIntercept: workload \"%s.%s\" not found", wl, s.AppNamespace())) } func (s *workloadConfigurationSuite) Test_DisabledReplicaSet() { s.TelepresenceHelmInstallOK(s.Context(), true, "--set", "workloads.replicaSets.enabled=false") defer s.TelepresenceHelmInstallOK(s.Context(), true, "--set", "workloads.replicaSets.enabled=true") s.disabledWorkloadKind("ReplicaSet", "rs-echo") } func (s *workloadConfigurationSuite) Test_DisabledStatefulSet() { s.TelepresenceHelmInstallOK(s.Context(), true, "--set", "workloads.statefulSets.enabled=false") defer s.TelepresenceHelmInstallOK(s.Context(), true, "--set", "workloads.statefulSets.enabled=true") s.disabledWorkloadKind("StatefulSet", "ss-echo") } func (s *workloadConfigurationSuite) Test_InterceptsDeploymentWithDisabledReplicaSets() { ctx := s.Context() require := s.Require() wl, tp := "echo-one", "Deployment" s.ApplyApp(ctx, wl, strings.ToLower(tp)+"/"+wl) defer s.DeleteSvcAndWorkload(ctx, strings.ToLower(tp), wl) s.TelepresenceHelmInstallOK(ctx, true, "--set", "workloads.replicaSets.enabled=false") defer s.TelepresenceHelmInstallOK(ctx, true, "--set", "workloads.replicaSets.enabled=true") s.TelepresenceConnect(ctx) defer itest.TelepresenceDisconnectOk(ctx) verb := "engage" if !s.ClientIsVersion(">2.21.x") { verb = "intercept" } require.Eventually( func() bool { stdout, _, err := itest.Telepresence(ctx, "list") return err == nil && strings.Contains(stdout, fmt.Sprintf("%s: ready to %s", wl, verb)) }, 6*time.Second, // waitFor 2*time.Second, // polling interval ) stdout := itest.TelepresenceOk(ctx, "intercept", wl) require.Contains(stdout, fmt.Sprintf("Using %s %s", tp, wl)) stdout = itest.TelepresenceOk(ctx, "list", "--intercepts") require.Contains(stdout, fmt.Sprintf("%s: intercepted", wl)) itest.TelepresenceOk(ctx, "leave", wl) } func (s *workloadConfigurationSuite) Test_InterceptsReplicaSetWithDisabledDeployments() { ctx := s.Context() require := s.Require() wl, tp := "echo-easy", "Deployment" s.ApplyApp(ctx, wl, strings.ToLower(tp)+"/"+wl) defer s.DeleteSvcAndWorkload(ctx, strings.ToLower(tp), wl) interceptableWl := s.KubectlOk(ctx, "get", "replicasets", "-l", fmt.Sprintf("app=%s", wl), "-o", "jsonpath={.items[*].metadata.name}") s.TelepresenceHelmInstallOK(ctx, true, "--set", "logLevel=trace", "--set", "workloads.deployments.enabled=false") defer s.TelepresenceHelmInstallOK(ctx, true, "--set", "workloads.deployments.enabled=true") s.TelepresenceConnect(ctx) defer itest.TelepresenceDisconnectOk(ctx) verb := "engage" if !s.ClientIsVersion(">2.21.x") { verb = "intercept" } expect := fmt.Sprintf("%s: ready to %s", interceptableWl, verb) require.Eventuallyf( func() bool { stdout, _, err := itest.Telepresence(ctx, "list") clog.Info(ctx, stdout) return err == nil && strings.Contains(stdout, expect) }, 6*time.Second, // waitFor 2*time.Second, // polling interval "expected %q was never produced", expect, ) stdout := itest.TelepresenceOk(ctx, "intercept", interceptableWl) require.Contains(stdout, fmt.Sprintf("Using %s %s", "ReplicaSet", interceptableWl)) stdout = itest.TelepresenceOk(ctx, "list", "--intercepts") require.Contains(stdout, fmt.Sprintf("%s: intercepted", interceptableWl)) itest.TelepresenceOk(ctx, "leave", interceptableWl) } ================================================ FILE: integration_test/workload_watch_test.go ================================================ package integration_test import ( "context" "strconv" "time" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/rpc/v2/manager" "github.com/telepresenceio/telepresence/v2/pkg/version" ) func (s *notConnectedSuite) createIntercept(ctx context.Context, client manager.ManagerClient, session *manager.SessionInfo) (*manager.InterceptInfo, error) { ir := &manager.CreateInterceptRequest{ Session: session, InterceptSpec: &manager.InterceptSpec{ Name: "echo-easy", Client: "telepresence@datawire.io", Agent: "echo-easy", WorkloadKind: "Deployment", Namespace: s.AppNamespace(), Mechanism: "tcp", TargetHost: "127.0.0.1", TargetPort: 8080, }, } pi, err := client.PrepareIntercept(ctx, ir) if err != nil { return nil, err } spec := ir.InterceptSpec spec.ServicePort = pi.ServicePort spec.ServicePortName = pi.ServicePortName spec.ServiceUid = pi.ServiceUid spec.ContainerPort = pi.ContainerPort spec.Protocol = pi.Protocol spec.ContainerName = pi.ContainerName if pi.ServiceUid != "" { if pi.ServicePortName != "" { spec.PortIdentifier = pi.ServicePortName } else { spec.PortIdentifier = strconv.Itoa(int(pi.ServicePort)) } } else { spec.PortIdentifier = strconv.Itoa(int(pi.ContainerPort)) } return client.CreateIntercept(ctx, ir) } func (s *notConnectedSuite) Test_WorkloadListener() { if !s.ClientVersion().EQ(version.Structured) { s.T().Skip(`Not part of compatibility tests. DoWithTrafficManager assumes compiled executable`) } s.Require().NoError(s.DoWithTrafficManager(s.Context(), func(ctx context.Context, cancel context.CancelFunc, client manager.ManagerClient, session *manager.SessionInfo) { rq := s.Require() // Perform some actions that will generate events. Here: // 1. Create a deployment // 2. Prepare an intercept on that deployment (injects the traffic-agent into the pod) // 3. Create an intercept (changes state to INTERCEPTED) // 4. Leave the intercept (state goes back to INSTALLED) // 5. Remove the deployment defer cancel() _, err := client.SetLogLevel(ctx, &manager.LogLevelRequest{LogLevel: "trace"}) if !s.NoError(err) { return } toCtx, toCancel := context.WithTimeout(ctx, time.Minute) defer toCancel() wwStream, err := client.WatchWorkloads(toCtx, &manager.WorkloadEventsRequest{ SessionInfo: session, }) rq.NoError(err) defer func() { _, _ = client.SetLogLevel(ctx, &manager.LogLevelRequest{LogLevel: "debug"}) }() s.ApplyApp(ctx, "echo-easy", "deploy/echo-easy") // This map contains a key for each expected event from the workload watcher expectations := map[string]bool{ "added": false, "progressing": false, "available": false, "agent installed": false, "agent intercepted": false, "agent installed again": false, "deleted": false, } var spec *manager.InterceptSpec var interceptingClient string for !(s.T().Failed() || expectations["deleted"]) { delta, err := wwStream.Recv() if err != nil { clog.Infof(ctx, "watcher ended with %v", err) break } for _, ev := range delta.Events { clog.Infof(ctx, "watcher event: %s %v", ev.Type, ev.Workload) switch ev.Type { case manager.WorkloadEvent_ADDED_UNSPECIFIED, manager.WorkloadEvent_MODIFIED: expectations["added"] = true switch ev.Workload.State { case manager.WorkloadInfo_PROGRESSING: expectations["progressing"] = true case manager.WorkloadInfo_AVAILABLE: if !expectations["available"] { expectations["available"] = true ii, err := s.createIntercept(ctx, client, session) if !s.NoError(err) { return } spec = ii.Spec } switch ev.Workload.AgentState { case manager.WorkloadInfo_INSTALLED: if expectations["agent intercepted"] { expectations["agent installed again"] = true s.DeleteSvcAndWorkload(ctx, "deploy", "echo-easy") } else { expectations["agent installed"] = true } case manager.WorkloadInfo_INTERCEPTED: expectations["agent installed"] = true expectations["agent intercepted"] = true if ics := ev.Workload.InterceptClients; len(ics) == 1 { interceptingClient = ics[0].Client } _, err = client.RemoveIntercept(ctx, &manager.RemoveInterceptRequest2{ Session: session, Name: spec.Name, }) s.NoError(err) } } case manager.WorkloadEvent_DELETED: expectations["deleted"] = true } } } for k, expect := range expectations { s.True(expect, k) } s.Equal("telepresence@datawire.io", interceptingClient) })) } ================================================ FILE: integration_test/workloads_test.go ================================================ package integration_test import ( "bytes" "path/filepath" "strconv" "strings" "time" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/integration_test/itest" "github.com/telepresenceio/telepresence/v2/pkg/dos" ) func (s *connectedSuite) uninstall(wl string) { ctx := s.Context() itest.TelepresenceOk(ctx, "uninstall", wl) s.Require().Eventually( func() bool { stdout, _, err := itest.Telepresence(ctx, "list", "--agents") return err == nil && !strings.Contains(stdout, wl) }, 180*time.Second, // waitFor 6*time.Second, // polling interval ) } func (s *connectedSuite) successfulIntercept(tp, wl, port string) { ctx := s.Context() s.ApplyApp(ctx, wl, strings.ToLower(tp)+"/"+wl) defer s.DeleteSvcAndWorkload(ctx, tp, wl) s.doIntercept(tp, wl, port) if !s.ClientIsVersion(">2.21.x") && s.ManagerIsVersion(">2.21.x") { // An <2.22.0 client will not be able to uninstall an agent when the traffic-manager is >=2.22.0 // because the client will attempt to remove the entry in the telepresence-agents configmap. It // is no longer present in versions >=2.22.0 return } s.uninstall(wl) tpl := itest.DisruptionBudget{ Name: "telepresence-test", MinAvailable: 1, } // Do the intercept again, this time with a disruption budget with minAvailable=1 in place. rq := s.Require() db, err := itest.ReadTemplate(ctx, filepath.Join("testdata", "k8s", "disruption-budget.goyaml"), &tpl) rq.NoError(err) rq.NoError(s.Kubectl(dos.WithStdin(ctx, bytes.NewReader(db)), "apply", "-f", "-")) defer func() { _ = s.Kubectl(dos.WithStdin(ctx, bytes.NewReader(db)), "delete", "-f", "-") }() s.doIntercept(tp, wl, port) } func (s *connectedSuite) doIntercept(tp, wl, port string) { ctx := s.Context() require := s.Require() require.Eventually( func() bool { stdout, _, err := itest.Telepresence(ctx, "list") return err == nil && strings.Contains(stdout, wl) }, 6*time.Second, // waitFor 2*time.Second, // polling interval ) out, err := s.KubectlOut(ctx, "get", strings.ToLower(tp), wl, "-o", "jsonpath={.spec.replicas}") require.NoError(err) replicas, err := strconv.Atoi(out) require.NoError(err) stdout := itest.TelepresenceOk(ctx, "intercept", "--mount", "false", "--port", port, wl) require.Contains(stdout, "Using "+tp+" "+wl) stdout = itest.TelepresenceOk(ctx, "list", "--intercepts") require.Contains(stdout, wl+": intercepted") require.NotContains(stdout, "Volume Mount Point") s.Eventually(func() bool { ras := itest.RunningPodsWithAgents(ctx, wl, s.AppNamespace()) clog.Infof(ctx, "pod with agent count %d, expected %d", len(ras), replicas) return len(ras) == replicas }, 60*time.Second, 5*time.Second) s.CapturePodLogs(ctx, wl, "traffic-agent", s.AppNamespace()) time.Sleep(10 * time.Second) itest.TelepresenceOk(ctx, "leave", wl) stdout = itest.TelepresenceOk(ctx, "list", "--intercepts") require.NotContains(stdout, wl+": intercepted") } func (s *connectedSuite) successfulIngest(tp, wl string) { ctx := s.Context() s.ApplyApp(ctx, wl, strings.ToLower(tp)+"/"+wl) defer s.DeleteSvcAndWorkload(ctx, tp, wl) require := s.Require() require.Eventually( func() bool { stdout, _, err := itest.Telepresence(ctx, "list") return err == nil && strings.Contains(stdout, wl) }, 6*time.Second, // waitFor 2*time.Second, // polling interval ) stdout := itest.TelepresenceOk(ctx, "ingest", "--mount", "false", wl) require.Contains(stdout, "Using "+tp+" "+wl) stdout = itest.TelepresenceOk(ctx, "list", "--ingests") require.Contains(stdout, wl+": ingested") require.NotContains(stdout, "Volume Mount Point") s.CapturePodLogs(ctx, wl, "traffic-agent", s.AppNamespace()) itest.TelepresenceOk(ctx, "leave", wl) stdout = itest.TelepresenceOk(ctx, "list", "--ingests") require.NotContains(stdout, wl+": ingested") if !s.ClientIsVersion(">2.21.x") && s.ManagerIsVersion(">2.21.x") { // An <2.22.0 client will not be able to uninstall an agent when the traffic-manager is >=2.22.0 // because the client will attempt to remove the entry in the telepresence-agents configmap. It // is no longer present in versions >=2.22.0 return } s.uninstall(wl) } func (s *connectedSuite) Test_SuccessfullyInterceptsDeploymentWithProbes() { s.successfulIntercept("Deployment", "with-probes", "9090") } func (s *connectedSuite) Test_SuccessfullyInterceptsReplicaSet() { s.successfulIntercept("ReplicaSet", "rs-echo", "9091") } func (s *connectedSuite) Test_SuccessfullyInterceptsStatefulSet() { if !s.ManagerIsVersion(">2.21.x") { s.T().Skip("Not part of compatibility tests. StatefulSet rollouts fail intermittently in versions < 2.22.0") } s.successfulIntercept("StatefulSet", "ss-echo", "9092") } func (s *connectedSuite) Test_SuccessfullyInterceptsDeploymentWithNoVolumes() { s.successfulIntercept("Deployment", "echo-no-vols", "9093") } func (s *connectedSuite) Test_SuccessfullyInterceptsDeploymentWithoutService() { s.successfulIntercept("Deployment", "echo-no-svc-ann", "9094") } func (s *connectedSuite) Test_SuccessfullyIngestsDeploymentWithProbes() { s.successfulIngest("Deployment", "with-probes") } func (s *connectedSuite) Test_SuccessfullyIngestsReplicaSet() { s.successfulIngest("ReplicaSet", "rs-echo") } func (s *connectedSuite) Test_SuccessfullyIngestsStatefulSet() { if !s.ManagerIsVersion(">2.21.x") { s.T().Skip("Not part of compatibility tests. StatefulSet rollouts fail intermittently in versions < 2.22.0") } s.successfulIngest("StatefulSet", "ss-echo") } func (s *connectedSuite) Test_SuccessfullyIngestsDeploymentWithNoVolumes() { s.successfulIngest("Deployment", "echo-no-vols") } func (s *connectedSuite) Test_SuccessfullyIngestsDeploymentWithoutService() { s.successfulIngest("Deployment", "echo-no-svc") } ================================================ FILE: integration_test/wpad_test.go ================================================ package integration_test import ( "bufio" "context" "fmt" "net" "os" "path/filepath" "strings" "time" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" ) // Test_WpadNotForwarded tests that DNS request aren't forwarded // to the cluster. func (s *connectedSuite) Test_WpadNotForwarded() { ctx := s.Context() logFile := filepath.Join(filelocation.AppUserLogDir(ctx), "daemon.log") tests := []struct { qn string forward bool }{ { "wpad", false, }, { fmt.Sprintf("wpad.%s", s.AppNamespace()), false, }, { "wpad.cluster.local", false, }, { "wpad.svc.cluster.local", false, }, { fmt.Sprintf("wpad.%s.svc.cluster.local", s.AppNamespace()), false, }, /* revisit after checking relevant log messages on all platforms { "wpad.bogus.nu", true, }, */ } // Figure out where the current end of the logfile is. This must be done before any // of the tests run because the queries that the DNS resolver receives are dependent // on how the system's DNS resolver handle search paths and caching. st, err := os.Stat(logFile) s.Require().NoError(err) pos := st.Size() for _, tt := range tests { s.Run(tt.qn, func() { require := s.Require() ctx := s.Context() // Make an attempt to resolve the host short, cancel := context.WithTimeout(ctx, 20*time.Millisecond) defer cancel() _, _ = net.DefaultResolver.LookupIPAddr(short, tt.qn) time.Sleep(200 * time.Millisecond) // Seek to the end of the log as it were before the lookup rootLog, err := os.Open(logFile) require.NoError(err) defer rootLog.Close() _, err = rootLog.Seek(pos, 0) require.NoError(err) // Ensure that there's an A record with an NXDOMAIN but no LookupHost call // with a "wpad." prefix. The host may not match exactly due to how the // OS handles search paths. hasNX := false hasLookup := false scn := bufio.NewScanner(rootLog) for scn.Scan() { txt := scn.Text() if strings.Contains(txt, "wpad") { if !hasLookup { if s.IsIPv6() { hasLookup = strings.Contains(txt, "Lookup AAAA ") } else { hasLookup = strings.Contains(txt, "Lookup A ") } } if !hasNX { hasNX = strings.Contains(txt, "-> NXDOMAIN") } } } if tt.qn == "wpad" && !hasNX && !hasLookup { // this is very likely OK because our DNS server never received the request. It // was filtered by the OS DNS framework. Those tests are only relevant when the overriding // DNS resolver is used. return } if tt.forward { require.Truef(hasLookup, "Missing expected Lookup A log for %s", tt.qn) } else { require.Falsef(hasLookup, "Found unexpected Lookup A log for %s", tt.qn) require.Truef(hasNX, "No NXDOMAIN record found for %s", tt.qn) } }) } } ================================================ FILE: k8s/agent-injector-rbac.yaml ================================================ --- apiVersion: v1 kind: ServiceAccount metadata: name: traffic-manager namespace: ambassador --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: traffic-manager rules: - apiGroups: - "" resources: ["services"] verbs: ["get", "list"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: traffic-manager subjects: - kind: ServiceAccount name: traffic-manager namespace: ambassador roleRef: kind: ClusterRole name: traffic-manager apiGroup: rbac.authorization.k8s.io ================================================ FILE: k8s/agent-injector-secret.yaml ================================================ --- kind: Secret apiVersion: v1 metadata: name: agent-injector-tls namespace: ambassador type: Opaque data: ## TLS certificate and key for `ambassador-injector.ambassador.svc` crt.pem: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZnekNDQTJ1Z0F3SUJBZ0lFRHY3S3NUQU5CZ2txaGtpRzl3MEJBUXNGQURBYk1Sa3dGd1lEVlFRS0V4Qm4KWlhSaGJXSmhjM05oWkc5eUxtbHZNQjRYRFRJeE1EWXdPVEEyTVRneE1Wb1hEVEl5TURZd09UQTJNVGd4TVZvdwpRekVaTUJjR0ExVUVDaE1RWjJWMFlXMWlZWE56WVdSdmNpNXBiekVtTUNRR0ExVUVBeE1kWVdkbGJuUXRhVzVxClpXTjBiM0l1WVcxaVlYTnpZV1J2Y2k1emRtTXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDRHdBd2dnSUsKQW9JQ0FRQzVDWnowbFEvNjlNWEl2TWtTckZQOElRdEM1YlRlQzRMdUd3TVZPS24wQzUydTl5aEdQYkJKY1I0dQpEZ1l1NmNvSWNIMS91RmJPMVZseGJRR0ptTjVyQXA5MFo1WUZLUSt2SnBobGpVaWFiUVZtbTZWNkgzYmNHalpsCmprYXdhQktIaVlNQ1VvOU9YNnplY0NhOTQ3RlZ1YVIvMFpndkcyNjlNNWVCL25hVExJK09iTHRYUEF4TDNiaE8KN2dxczhWb3c1Q2NDZFRaRk9yekJ3Ui9vZERqdGxKR2tTR2JkN2hXVG9UaUx1VUcrcE1Ea0lmNXlOSm5zQXorNgpQZm1Db2ZhekFuV0dDYTd5T0gvRW1UcDRNTlJKRVQ1NkFvVlVvcDFxbmdJZk1aUlZvNzJuTDAwQkpDNkhRdCt1CnVXSmlXeWJaalBiV3F0UGJFeVRGUEZoUjhLSVY3SVBXTXZ2K3dmYk02Q3JWbVFlVTcyY2E5MmRSY3VnWkVQS2wKVnkrMXZCV2pKaFk5clF1cmNDcW1IVEt6bGZuajRmbmx0SitmQ1dqQVNiTEFCU2hRWTFaN3RKbGVnY3JZZDh4NgpUZ1NyN0pLS1RLWkFVaTRZQlNZYlpWV0tjQVNuaGpSQlJSTWE1UDFjZkI1R1FqZCtyZDBSK05LOFRRanpNWis0CnhPQnJ5aUVCcUFhM0VhbERQZ3Q2RUo1cG5Kd2lxQ3oyNm1oOXg2bHo1MzB5MFQ3Yi80cWlhMjYrWG5WSFdrMlUKcHZyUFlMUG9PUjkxZFdDUkx1OGF2UzVITGtRZzRLbWlIem05SnBzT3JiaE83NFBmc1BQUmVkSVFKN21TckdvVgpSdkZpQjBpYXVvYjR5VzExaVZBUlkzT2ZDa1d6ZFJyZndvMUdMTDZUZk5Za2FkOXZWd0lEQVFBQm80R21NSUdqCk1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBZEJnTlZIU1VFRmpBVUJnZ3JCZ0VGQlFjREFnWUlLd1lCQlFVSEF3RXcKSFFZRFZSME9CQllFRkhjU1ZFTnprb3F2SjhQTXE1c0k5cmhZWExQb01GTUdBMVVkRVFSTU1FcUNEbUZuWlc1MApMV2x1YW1WamRHOXlnaGxoWjJWdWRDMXBibXBsWTNSdmNpNWhiV0poYzNOaFpHOXlnaDFoWjJWdWRDMXBibXBsClkzUnZjaTVoYldKaGMzTmhaRzl5TG5OMll6QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FnRUFZL29icjJRSjViYysKWFF2N0tVMmN2NmZnOStVZHFLSUlaN2lHNGNKN1Fyb1R3cFZLeVlHVFpOUTVyeGNUWkxQN1Z6b21iZ0VsTWVmcwpzanlLUnVOYXZSL2wrYTUreWZIYnd5bHFsK0d3MElRbSt5UFpmeEt4OCtCSzFzZmpIdFJ3Z01uNGVEYkd5M0pEClhGc3FUWHdFT1VqMkNLS0J2M1JXKzhlNUtDOW1GSDhaWi9yZzJuUTRJb1FJOFh3UFlIcXRkenU2c2xpTHIvVWwKY28xZXR6bW5FdVFKVy9RL24rSzQvUlpoUjAzbEVCNlZVZVczUzQ3ZUpScENtN0RrdlV3ZWRYSXBoV1FuYkxNeApDSkd5RTNucjdaL3ZPaDhxYlp6amFqcXNtd01kTTJYbUVVckZ5enE4bnpkZ0JRZnh4YyswUkt5Rm45RnlkTWhYCjFqajhmNjVzbjRTczhUNndNWDA3c2tDMllua3ZMTVplREIvVUcrYUhWTW44ZXROT2RleXhDU3FUZG5mR0pIWDIKaWk3Yi93WmZVaDUvemNCcmdMTHhsaU5COHFBYjVvZ2p2WmNDcjY3SWZMdndkZndBeVdRN3JSekg4ckpWOHVQZwpoZTNMcjVVYnMxZWJsd1dKZ2lpSjNjZmRMV3o3S3BFakxoTkZVdXMwTlNCd0JLUmZWWkpxbXVzYXNsN1VpZEV0CnpTYzdoZ1E2SzZ3aGkyZ1RldEJNL2FaMk5ORVpRSFY1L3NpQUpyR0ZZZncwQ0IvSlFhcFkybDNDUUNRalN1blIKK25mRFQ2ekJGWHovNVZvNGJqOFh3K1VRRzJMY0JvRzZVRU9Udlk0eXJuNTdGeUUzaW9nbUJReTQvaHUzV0dKRgoxdkF3eEZrRXgxUk1BQXBPOTBuT1FPL2RGMnlWMzc4PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== key.pem: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKSndJQkFBS0NBZ0VBdVFtYzlKVVArdlRGeUx6SkVxeFQvQ0VMUXVXMDNndUM3aHNERlRpcDlBdWRydmNvClJqMndTWEVlTGc0R0x1bktDSEI5ZjdoV3p0VlpjVzBCaVpqZWF3S2ZkR2VXQlNrUHJ5YVlaWTFJbW0wRlpwdWwKZWg5MjNCbzJaWTVHc0dnU2g0bURBbEtQVGwrczNuQW12ZU94VmJta2Y5R1lMeHR1dlRPWGdmNTJreXlQam15NwpWendNUzkyNFR1NEtyUEZhTU9RbkFuVTJSVHE4d2NFZjZIUTQ3WlNScEVobTNlNFZrNkU0aTdsQnZxVEE1Q0grCmNqU1o3QU0vdWozNWdxSDJzd0oxaGdtdThqaC94Sms2ZUREVVNSRStlZ0tGVktLZGFwNENIekdVVmFPOXB5OU4KQVNRdWgwTGZycmxpWWxzbTJZejIxcXJUMnhNa3hUeFlVZkNpRmV5RDFqTDcvc0gyek9ncTFaa0hsTzluR3ZkbgpVWExvR1JEeXBWY3Z0YndWb3lZV1BhMExxM0FxcGgweXM1WDU0K0g1NWJTZm53bG93RW15d0FVb1VHTldlN1NaClhvSEsySGZNZWs0RXEreVNpa3ltUUZJdUdBVW1HMlZWaW5BRXA0WTBRVVVUR3VUOVhId2VSa0kzZnEzZEVmalMKdkUwSTh6R2Z1TVRnYThvaEFhZ0d0eEdwUXo0TGVoQ2VhWnljSXFnczl1cG9mY2VwYytkOU10RSsyLytLb210dQp2bDUxUjFwTmxLYjZ6MkN6NkRrZmRYVmdrUzd2R3IwdVJ5NUVJT0Nwb2g4NXZTYWJEcTI0VHUrRDM3RHowWG5TCkVDZTVrcXhxRlVieFlnZEltcnFHK01sdGRZbFFFV056bndwRnMzVWEzOEtOUml5K2szeldKR25mYjFjQ0F3RUEKQVFLQ0FnQWs1aXJiaDBJNWpFaEl3SVRrYVdNU0cxRFFsVmdkRTNTWG9PRmJnUUk3UFhuRFkxd3ZyYXVTNmJEWQpCRW50WHdlLzZSYk51bWZ0TlJSeUR3ZklkYWljOUZmeDhabzBDejBxYzJyZWpXOFdCSG1ZUForaEc5Y3JDenNmCncyQ0xXeVdleTZoSmRVZEluTUd2VmZRVDErME1LRW9LaHpSdTZHeUw1RmJwUUdKSzlRN25tdjA4NXllWWxXMWsKcUFtTzlVNUVBNnRYelNIMmFrRXI3aVE3eWJsMGZ6VVA2clJBdTNLb0R2Vmt2NXZCdGI4VmYwWHJabHZ2ZWJKaQpwR3MyUnJBWWdId0pMN01vY0dYaVFhQXNTYWg3cmFtazBRM2ZrOUlLYWRvSWVHMlpLbDd1a1BzdEtCYnpPRDI4CjdSRVdoaERZUDBrV0E5amRqaEsrRmt1U2c0Yit1eEJKUzZMemUwVkp1V3dXdHVPSk45YUord1NoMlo1V2NqeisKVzFncTdNQ3VCc1BFTTVMaDFSTkd3eGxKZGJTakU2ejZOalF4Q2R1dVFkMjZHNk1XTElDRFRVcHZDOFM5QXBRZApvMkk1QkxuTHVpWkRjWDdYVGpCbVhNQVBnRzdva3VWUkRtaXI1QXFxQjRWcDNBNWpWandsS2dFcVJUOXlNNDl2Cm1NV212V1NJSVQvdmljMElqZ0pQS1Rpa21BT2ZGQzl3aTNnN1MrQmxKV1lMaWMwc2dLeVUrVnBJYW9senRLU2wKUTI3UExRY2xsNkhkVUMzcmlWVU5aSnBGRFVJM2tLWEgwSldsY0hTQjhGbE9rZGZueXh4QiswVHRXMmFBTkQvUApZaEx4Y0ZMZUlDc3dtN1cvNm5DUW1DN2tlUklsa01IZERwUlprbUFkR0U1dVVIbXNHUUtDQVFFQXhuOUhKcEE0CjBIOXhoR1dWS3ZwdjZ6bmVucmh6d3ZUelBzbGRzR2Qxb3BKRnZtV2M1SWxEdnYrcmliK3VJMWZFdVhyRXhkZjMKS2JDQm51UkpyTGc3ZEYrZ0IyNTBxRmJKblQ5NUdiUzBkeUozSUJTZlFZL1FVZEJjTmxKNW1yL1lTZXIrVUU1SQpPOHFNN0ozWGZxN21SU2p4bVJVNy8yRXZRVmJYQUVrZmc3KysydE9OYjQxR3NpYW1tcDFIMzh4U1VJNUZnV3RBCitPS1NEczhka21IeC9HLzJkWktQWXBic1hMc0ZRWTYzeVcrdzBJTEovc05xUHNJNE8wTU8zZ1RzL0c3WXA0em4KVlRCSnZpQ2RhbC94LzJBRFR5RnNYZld5MFN4aVdWRzlLM3NJUHpkNWdkN3B1d3poeTlDWllaaE9OT24rVmJETwpQOERra0F5TkgzOSsvUUtDQVFFQTdxUW9WQ3NTRkNsbFA1TXVZN1FzR1pkdGU0N1NjRTI1RXlXUTU3OFk3SzdrCi9KL0lZbHJGRnhCMW1senZMUTI1aHh0eUgzTHpkdDVya3BOMmRMTXg4NnZ2d2lIZ2JHakhpOE9YMlRXdzJBS00KTVNJVTREckJjUU85UGMvK2VCaytzbXp6MHhvUXd2ZEN4b2xIZGR4bitYTzJVZDJBemZicHZuV0ZTWTFDT2FscQpCWTNoWUlRd2czUTFscEJuanA1RjZrNks5Y0ZoVVhud1U0UEYvb0JWYXYvRHBOdDJtalFseVdiaFJoNWZqOGNECjVVaG0zVGFJeSt6bEk1dUtsLzVqYWwwaXlIelM0TGR6TFUrREVyd3lIWXdhOWxvTURPcXZ5cUU4QjM1UWJYbTUKclY5RktiSjZmR1N6YlVIMzk2VTI5eU03SzVxL0luNHNjRlViUjFHNTR3S0NBUUJCbmxmR0RzMUpWNmdPTFlxbgpYNHphQlJKc0ErdjQyZ01Ea1l2UVFoTm9QOVNnZ1hUaE0rTmFZNml3YTlaRVJzSjQwblgwTlZXMnlXdkFQQldDCjdKQnpBeDJpOTBmSFVwRnAwMDdVU2FHUzlLak95U0p3aVB0RDRJNXJRczZDY3NNc2hHdTMzbmtRa3dBTlJJeTEKaTFvQ0tPdmRRR0RLSnJWNWN4eTJNbllobHFTZ01HbEVKRDducTlGTnNZck9GL3hxTnU0UlA0U0dBbGhvWHQyVwp3NXc4YUVMZ1VTZm5YcDhhZEpUalBDdlRnb0hBSEV4c1ZPdHRmY1ViQ2lzRENEdlRvMDBwN25HVy90U1I3clE3Ck15YUwxcENoZXhvWTRaMVFlc3kwWEVvZDhwa2lWTk50TkdTdCtpODJzbW5TRW9oZ0E2NlpZMDU3VXVmOEdyb1kKWEl4OUFvSUJBSEZyZSs5aytSYTBCZmNOVU1MNll1dFcyU1ljWDFBWTRKbUZCVFhmMFV6TVl2RGVVRExPeVZXSQo4UndJaDNSMlRYTTFUQ3crU2hCNDdjK2dYbkJncXFFUldzWjlxMWhiQkZ3Yy9oS3lQZmFzWDAwSzBia3dzN2V1CiswWmhrS1FyKzJ4NTgvaWxMc283RW5XaDBXRG0vRlBHOXlRNWpucFZuQXAxZUgvWXIxMFFjOTluNjNJZjRaN1kKb0krSzJtMGlORUNFUys4NWxiTlByVFZFTDlvaHpIY3FVQ1lPV0hRNXpLdklSZEU1cGxtRFVRcXNPcGR3ejl0OApIL3VvZFZxQUFXZ1FFL1FOdjN3bU5JdVllc2R3d3JEZ1lnQXNGQVlmbEtWTnRHWXJWclp4WUJwU2FXREQvd3NZCklWOFFOM1p3QTR1Nkp1azJoeGt4dHBVOUhkWHJ0ZGtDZ2dFQUgyaDJ6QzVqTHVzWnZKRFNteHNpbmd4YTRLSUQKalQ0QVQyNUJ2Wmk1a1ZnUDBobjMrWG1HcVlVY1hSd3ViN2hDeTR3d0RQOFFEWFcvV1pvdXpleUVTQXpMQ2xXbgpKL1FUMEppYUlubUhJS2hKVWowZ2pBY3lCZ0hmeVk0eWJ6aGFFMnFFK2xJRlk5NzJ0ZnI5YUwyOEF0RUhMdjdxClc4SDhYN3JQelV0anVpejB2aXhTTlZLU2FDRHRvWTJXUkN4anVxb3hVOHpZVmFqTVFxZkUwR2NTUE9jVlhlU2YKb3JYN3BNL2pmZk1mMXdHQ3QyZS9hVWNINjZDdGtCTnRpMllUQkl2T0tBdnBPWlZBamxuOThHMUhRM24wK2JHQQpaU0hsdFZuWEZkVkYxZFYwL1NIU0REWUdWcXJwVkhmdjJIdnZmbkdIc3dScjR4cktmTG95SmZpLzRRPT0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K ================================================ FILE: k8s/agent-injector.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: agent-injector namespace: ambassador spec: type: ClusterIP selector: app: traffic-manager telepresence: manager ports: - name: agent-injector port: 8443 targetPort: https --- apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: name: agent-injector-webhook webhooks: - name: agent-injector.telepresence.io clientConfig: service: name: agent-injector namespace: ambassador path: "/traffic-agent" caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZGVENDQXYyZ0F3SUJBZ0lFRHY3S3NEQU5CZ2txaGtpRzl3MEJBUXNGQURBYk1Sa3dGd1lEVlFRS0V4Qm4KWlhSaGJXSmhjM05oWkc5eUxtbHZNQjRYRFRJeE1EWXdPVEEyTVRnd09Wb1hEVEl5TURZd09UQTJNVGd3T1ZvdwpHekVaTUJjR0ExVUVDaE1RWjJWMFlXMWlZWE56WVdSdmNpNXBiekNDQWlJd0RRWUpLb1pJaHZjTkFRRUJCUUFECmdnSVBBRENDQWdvQ2dnSUJBTjdrbzROZHMvZTdvaVU0WGM1Rm5DcUlTNDNnZ01Zd2MrR1huaW9zQk50R000K00KMEFiODgweUx2WlRQTURoSVJQbmtlSm9UeThhN3NETVRQcHdtc2tmZFNJRjNRQlVKSlc3ZGNpaWI3Y3lmTzdUWgp2UmM0YW5SSXpYYUtsc1pRQ3ZZNUVLOHJOZ3hDYmd3VGprdldoN01iOUpPdFZqcDMyTEhaUFV5Q1RKQ1diUzRjCm05MHZiWVFOclNPZUpLc0I3bEUrWGVrWnEvODI4OXpBWXBpakYxY3Z3aW1sMU83eUMyc2hKa1Urd0lUcldVSmYKd1J5SDZMVk5zSlZKaXZjWU1EN01panU0T0tPOG1lZkUrYlYxRXpjYVpmWmh4OGowZWNMZjRPdUhQUlBMS2pyVQpjUFFheFRZZm9hdlpKV0RyQU5VSnNEb01BWUZyZkh4VFlmZHZFY2RHaGN0Z1Rpb2xPbUpkenMzYWpwVWpINnhICllOTWRsb2YreHpGSlQzbWxYZ2pKMHJXc2NwQ0lkSG1vVXFWQURpR0huSTJzVEpFMkpneFBodzRYcGQ1OHRxOFQKTFBBZGdsdDF6R2VHeGl5Z3BuU0UyaGppY2grcm1DUlcrT1BReCtMQTdIQTFlTitWZmxpWTJ4S1pDaXlydmdHNApjamlPb2lsR05iOXI5M1ZqT0MrUnpVUWlwRklLVnBqVFc5V1BvQVRTajc3aUJWbmxVWSs3SHpmN2VnS0RNVTBPCjNGRFJOS0Zra1IwTmN1T2dZQTZ3ZmMwbUhJaFpucThVd0pPMTdiRGcrYm5nYUF4RmdJT0Z0ZnJ2U0IybUpXZTEKTkdoSWpkdWt6VkxkbmxjOThDQXhyZHQxbnRlU3hOYnlFTWJvZkxRNTBRUHNHM3htWk5peUMzcFJtV3ZaQWdNQgpBQUdqWVRCZk1BNEdBMVVkRHdFQi93UUVBd0lDaERBZEJnTlZIU1VFRmpBVUJnZ3JCZ0VGQlFjREFnWUlLd1lCCkJRVUhBd0V3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFkQmdOVkhRNEVGZ1FVeEplTitoYjJJdThFYWN6K2FNZVcKUHNiemhZUXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnSUJBTGZESlNxSFZlQ083cWlsaGJ2REhabjBhaW01RTMvMgpyT0FScVE1MFJPZ1hKQy9DM3RrK0Q5NG5JelI1azlMNHBNQ2xSR3prZWQ4bmltRExJMUVPa1hka3czQ0xucXBSCmppam5DV3hOczJIQS9RTGxGNHJZREJXRVk2OFF2OWo3MmFQT1plbTE1d1RXeGswSkd0c05JQ1FkQ08wcjRCWDQKdTAzUFcxVFVnRTBCZS9zNFBuRVM4TVAwdFRacmRxNVk3UDh4c1FxOGEzYjhaR3FYbmdmY3AvOHArVjBQcWhVdQpKdEpoeDhQcjNSMXZDNEFXc1lzTWlQNHZuV3gxVWkrVDUybllGZTNnVHFyc2NPYU9vVUhiOGh0VnI4NGZFOWxRClBIQ2NGTmdQdERPSTBzREgrSmdKQS9Ec3N5MHRZQ2VacGNwUTcybkxPVU5IZzdaUktOa01tVTdua1BmMStBb1cKZTFNY1kycW96dCtwRmZjdGg2d2YzYTNDZEU2ZGtqM1BFd0xrelBxL2lVZTFEbDk4anBYTjJvNGdDcThDS0t1QQo3dFYzekhJZFhwYW53UFVQVEFJQUovSDFPempIZ2ptNUo1NDdBY3dlbUQ4UkFCRTkrSGREYVJtaitCeXd4MFQxCmRDRVNyNm90WThFUVhETGtSQ1hqdnRIM2lORXhHOW9IbjZxL1lZcmtNc2kzNldwSkhpN2lGUmRwMEhXbDBLMmsKeWZFelNRUjR4TE5OWk0zaVBpYXQvZzAyaEEzTGVjbllzU3BMS1RmSFd2T3VTT3kwK1lrR1VsWXQ4Wlgyc2I5cgo5d0dGeFFTQmFrRzNSNkdTMjF2azFCdkpJd2hlcjlBRVowdWtCS282ZS9IcEN4MFFPd2dlempkV1pkUzVvWmdLCmYxczRZaE5PVUVMWAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== rules: - operations: ["CREATE"] apiGroups: [""] apiVersions: ["v1"] resources: ["pods"] sideEffects: None admissionReviewVersions: ["v1"] timeoutSeconds: 5 ================================================ FILE: k8s/apitest.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: "apitest" spec: type: ClusterIP selector: service: apitest ports: - name: proxied port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: "apitest" labels: service: apitest spec: replicas: 1 selector: matchLabels: service: apitest template: metadata: labels: service: apitest spec: containers: - name: apitest image: localhost:5000/apiserveraccess:latest ports: - containerPort: 8080 name: http env: - name: APP_PORT value: "8080" - name: LOG_LEVEL value: "DEBUG" resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: k8s/dnsutils-headless.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: dnsutils-headless spec: type: ClusterIP clusterIP: None selector: service: dnsutils-headless ports: - port: 8080 targetPort: 8080 --- apiVersion: apps/v1 kind: StatefulSet metadata: name: dnsutils-headless labels: service: dnsutils-headless spec: replicas: 1 serviceName: dnsutils-headless selector: matchLabels: service: dnsutils-headless template: metadata: labels: service: dnsutils-headless spec: containers: - name: dnsutils-headless image: gcr.io/kubernetes-e2e-test-images/dnsutils:1.3 command: - sleep - "3600" imagePullPolicy: IfNotPresent restartPolicy: Always ================================================ FILE: k8s/echo-auto-headless.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: echo-auto-headless spec: type: ClusterIP clusterIP: None selector: service: echo-auto-headless ports: - port: 8080 targetPort: 8080 --- apiVersion: apps/v1 kind: StatefulSet metadata: name: echo-auto-headless labels: service: echo-auto-headless spec: replicas: 1 serviceName: echo-auto-headless selector: matchLabels: service: echo-auto-headless template: metadata: labels: service: echo-auto-headless annotations: telepresence.io/inject-traffic-agent: enabled telepresence.io/inject-service-ports: "8080" spec: containers: - name: echo-auto-headless image: jmalloc/echo-server ports: - containerPort: 8080 resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: k8s/echo-double-one.yaml ================================================ # The echo-double-one service exposes two ports, 80 and 81 and directs them to one single container --- apiVersion: v1 kind: Service metadata: name: "echo-double-one" spec: type: ClusterIP selector: service: echo-double-one ports: - name: http port: 80 targetPort: http - name: extra port: 81 targetPort: extra --- apiVersion: apps/v1 kind: Deployment metadata: name: "echo-double-one" labels: service: echo-double-one spec: replicas: 1 selector: matchLabels: service: echo-double-one template: metadata: labels: service: echo-double-one spec: containers: - name: echo-double image: multi:5000/tel2/echo ports: - containerPort: 8080 name: http - containerPort: 8081 name: extra env: - name: PORTS value: "8080,8081" resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: k8s/echo-double.yaml ================================================ # The echo-double service exposes two ports, 80 and 81 and directs them to two separate containers --- apiVersion: v1 kind: Service metadata: name: "echo-double" spec: type: ClusterIP selector: service: echo-double ports: - name: http port: 80 targetPort: http - name: extra port: 81 targetPort: extra --- apiVersion: apps/v1 kind: Deployment metadata: name: "echo-double" labels: service: echo-double spec: replicas: 1 selector: matchLabels: service: echo-double template: metadata: labels: service: echo-double spec: containers: - name: echo-one image: ghcr.io/telepresenceio/echo-server ports: - containerPort: 8080 name: http resources: limits: cpu: 50m memory: 128Mi - name: echo-two image: ghcr.io/telepresenceio/echo-server ports: - containerPort: 8081 name: extra env: - name: PORT value: "8081" resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: k8s/echo-sc.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: "echo-sc" spec: type: ClusterIP selector: service: echo-sc ports: - name: proxied port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: "echo-sc" labels: service: echo-sc spec: replicas: 1 selector: matchLabels: service: echo-sc template: metadata: labels: service: echo-sc spec: securityContext: fsGroup: 1000 runAsUser: 1000 containers: - name: echo-sc image: ghcr.io/telepresenceio/echo-server:latest ports: - containerPort: 8080 name: http resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: k8s/ext-example.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: ext-example spec: type: ExternalName externalName: example.com ================================================ FILE: k8s/hello-w-volume.yaml ================================================ apiVersion: v1 kind: ConfigMap metadata: name: hello labels: app: hello data: index.html: |

--- apiVersion: v1 kind: Service metadata: name: hello spec: type: ClusterIP selector: service: hello ports: - name: proxied port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: hello labels: service: hello spec: replicas: 1 selector: matchLabels: service: hello template: metadata: labels: service: hello spec: volumes: - name: hello-cm-volume configMap: name: hello - name: tmp-volume emptyDir: {} containers: - name: hello-container image: nginx ports: - containerPort: 80 name: http volumeMounts: - mountPath: "/usr/share/nginx/html" name: hello-cm-volume - mountPath: "/tmp" name: tmp-volume ================================================ FILE: k8s/local-echo-easy.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: "echo-easy" spec: type: ClusterIP selector: app: echo-easy ports: - name: proxied port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: "echo-easy" labels: app: echo-easy spec: replicas: 1 selector: matchLabels: app: echo-easy template: metadata: labels: app: echo-easy spec: containers: - name: echo-easy image: localhost:5000/echo-server:latest imagePullPolicy: Always ports: - containerPort: 8080 name: http resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: k8s/local-echo-next.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: "echo-next" spec: type: ClusterIP selector: app: echo-next ports: - name: proxied port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: "echo-next" labels: app: echo-next spec: replicas: 1 selector: matchLabels: app: echo-next template: metadata: labels: app: echo-next spec: containers: - name: echo-next image: localhost:5000/echo-server:latest imagePullPolicy: Always ports: - containerPort: 8080 name: http resources: limits: cpu: 50m memory: 128Mi ================================================ FILE: k8s/manager.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: traffic-manager spec: type: ClusterIP clusterIP: None selector: app: traffic-manager telepresence: manager ports: - name: api port: 8081 targetPort: api --- apiVersion: apps/v1 kind: Deployment metadata: name: traffic-manager labels: app: traffic-manager telepresence: manager spec: replicas: 1 selector: matchLabels: app: traffic-manager telepresence: manager template: metadata: labels: app: traffic-manager telepresence: manager spec: containers: - name: traffic-manager image: ko://github.com/telepresenceio/telepresence/v2/cmd/traffic ports: - name: api containerPort: 8081 restartPolicy: Always ================================================ FILE: k8s/minikube-registry.yaml ================================================ apiVersion: v1 kind: ReplicationController metadata: name: kube-registry-ctl namespace: kube-system labels: k8s-app: kube-registry controller: yep spec: replicas: 1 selector: k8s-app: kube-registry controller: yep template: metadata: labels: k8s-app: kube-registry controller: yep spec: containers: - name: registry image: registry:2 resources: # keep request = limit to keep this container in guaranteed class limits: cpu: 100m memory: 100Mi requests: cpu: 100m memory: 100Mi env: - name: REGISTRY_HTTP_ADDR value: :5000 - name: REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY value: /var/lib/registry volumeMounts: - name: image-store mountPath: /var/lib/registry ports: - containerPort: 5000 name: registry protocol: TCP volumes: - name: image-store hostPath: path: /data/registry/ --- apiVersion: v1 kind: Service metadata: name: kube-registry namespace: kube-system labels: k8s-app: kube-registry spec: selector: k8s-app: kube-registry ports: - name: registry port: 5000 protocol: TCP --- apiVersion: apps/v1 kind: DaemonSet metadata: name: kube-registry-proxy namespace: kube-system labels: k8s-app: kube-registry kubernetes.io/cluster-service: "true" spec: selector: matchLabels: k8s-app: kube-registry-pod template: metadata: labels: k8s-app: kube-registry-pod spec: containers: - name: kube-registry-proxy image: gcr.io/google_containers/kube-registry-proxy:0.4 resources: limits: cpu: 100m memory: 50Mi env: - name: REGISTRY_HOST value: kube-registry.kube-system.svc.cluster.local - name: REGISTRY_PORT value: "5000" ports: - name: registry containerPort: 80 hostPort: 5000 ================================================ FILE: k8s/private-reg-proxy.yaml ================================================ apiVersion: apps/v1 kind: DaemonSet metadata: name: private-registry-proxy labels: app: private-registry-proxy spec: selector: matchLabels: app: private-registry-proxy template: metadata: labels: app: private-registry-proxy spec: containers: - name: tcp-proxy image: quay.io/bentoml/proxy-to-service:v2 args: - tcp - "5000" - docker-registry.default.svc.cluster.local ports: - containerPort: 5000 hostPort: 5000 name: tcp protocol: TCP ================================================ FILE: k8s/rs-echo-svc2.yaml ================================================ --- # Use a second service here that appoints the same port in the replicaset to # make things a bit more complex for the discovery mechanism, and also force # use of the --service flag when intercepting apiVersion: v1 kind: Service metadata: name: rs-echo-canary spec: type: ClusterIP selector: service: rs-echo ports: - name: http port: 80 targetPort: 8080 ================================================ FILE: packaging/artifacthub-repo.yml ================================================ repositoryID: 37605942-af09-4135-bd75-5241c570ad76 owners: - name: Thomas Hallgren email: thomas@tada.se ================================================ FILE: packaging/bundle.wxs.in ================================================  ================================================ FILE: packaging/helmpackage.go ================================================ package main import ( "flag" "log" "os" "github.com/blang/semver/v4" telcharts "github.com/telepresenceio/telepresence/v2/charts" ) type sv semver.Version func (v *sv) String() string { return (*semver.Version)(v).String() } func (v *sv) Set(s string) error { ver, err := semver.Parse(s) if err == nil { *v = sv(ver) } return err } func main() { var output string var version sv flag.StringVar(&output, "o", "", "output file") flag.Var(&version, "v", "Helm chart version") flag.Parse() err := packageHelmChart(output, semver.Version(version)) if err != nil { log.Fatal(err) } } func packageHelmChart(filename string, version semver.Version) error { fh, err := os.Create(filename) if err != nil { return err } defer fh.Close() return telcharts.WriteChart(telcharts.DirTypeTelepresence, fh, telcharts.TelepresenceChartName, version) } ================================================ FILE: packaging/homebrew-oss-formula.rb ================================================ # This script is generated automatically by the release automation code in the # Telepresence repository: class __FORMULA_NAME__ < Formula desc "Local dev environment attached to a remote Kubernetes cluster" homepage "https://telepresence.io" version "__NEW_VERSION__" BASE_URL = "https://github.com/telepresenceio/telepresence/releases/download" ARCH = Hardware::CPU.arm? ? "arm64" : "amd64" OPERATING_SYSTEM = OS.mac? ? "darwin" : "linux" PACKAGE_NAME = "telepresence-#{OPERATING_SYSTEM}-#{ARCH}" url "#{BASE_URL}/v#{version}/#{PACKAGE_NAME}" sha256 "__TARBALL_HASH_DARWIN_AMD64__" if OS.mac? && Hardware::CPU.intel? sha256 "__TARBALL_HASH_DARWIN_ARM64__" if OS.mac? && Hardware::CPU.arm? sha256 "__TARBALL_HASH_LINUX_AMD64__" if OS.linux? && Hardware::CPU.intel? # TODO support linux arm64 #sha256 "__TARBALL_HASH_LINUX_ARM64__" if OS.linux? && Hardware::CPU.arm? conflicts_with "telepresence" def install bin.install "#{PACKAGE_NAME}" => "telepresence" end test do system "#{bin}/telepresence", "--help" end end ================================================ FILE: packaging/homebrew-package.sh ================================================ #!/bin/bash set -e if [ -z "$1" ] then echo "Must set version" exit 1 fi VERSION="${1}" GITHUB_USER="${2:-$(git config get user.name)}" GITHUB_EMAIL="${3:-$(git config get user.email)}" GITHUB_TOKEN="${4}" ARCH=(amd64 arm64) OS=(darwin linux) MY_PATH=$(dirname "$0") MY_PATH=$( cd "$MY_PATH" && pwd ) WORK_DIR="$(mktemp -d)" cd "${WORK_DIR}" echo "Working in ${WORK_DIR}" BUILD_HOMEBREW_DIR=homebrew FORMULA_NAME="TelepresenceOss" FORMULA_FILE="${MY_PATH}/homebrew-oss-formula.rb" FORMULA="Formula/telepresence-oss.rb" for this_os in "${OS[@]}"; do for this_arch in "${ARCH[@]}"; do if [ "${this_arch}" == "arm64" ] && [ "${this_os}" == "linux" ]; then # TODO support linux arm64 continue fi # We should only be updating homebrew with a version of telepresence that # already exists, so let's download it DOWNLOAD_PATH="/download/v${VERSION}/telepresence-${this_os}-${this_arch}" echo "Downloading ${DOWNLOAD_PATH}" mkdir -p "${WORK_DIR}/${this_os}/${this_arch}/" curl -fL "https://github.com/telepresenceio/telepresence/releases${DOWNLOAD_PATH}" -o "${WORK_DIR}/${this_os}/${this_arch}/telepresence" declare -x "TARBALL_HASH_${this_os}_${this_arch}"="$(shasum -a 256 "${WORK_DIR}/${this_os}/${this_arch}/telepresence" | cut -f 1 -d " ")" tmp_var=TARBALL_HASH_${this_os}_${this_arch} echo "${tmp_var} == ${!tmp_var}" done done export HASH_ERRORS=0 for this_os in "${OS[@]}"; do for this_arch in "${ARCH[@]}"; do if [ "${this_arch}" == "arm64" ] && [ "${this_os}" == "linux" ]; then # TODO support linux arm64 continue fi # We don't want to update our homebrew formula if there # isn't a hash, so exit early if that's the case. tmp_var="TARBALL_HASH_${this_os}_${this_arch}" if [ -n "${!tmp_var}" ]; then echo "Telepresence binary hash: ${tmp_var} == ${!tmp_var}" else echo "Telepresence binary could not be hashed: ${tmp_var}" HASH_ERRORS=$((HASH_ERRORS++)) fi done done echo "HASH_ERRORS==${HASH_ERRORS}" if [ "${HASH_ERRORS}" -gt 0 ]; then exit 1 fi export GIT_CONFIG_GLOBAL=/dev/null export GIT_CONFIG_SYSTEM=/dev/null # Clone telepresenceio-homebrew: echo "Cloning into ${BUILD_HOMEBREW_DIR}..." if [ "${GITHUB_TOKEN}" == "" ]; then git clone "https://github.com/telepresenceio/homebrew-telepresence.git" "${BUILD_HOMEBREW_DIR}" else git clone "https://${GITHUB_TOKEN}@github.com/telepresenceio/homebrew-telepresence.git" "${BUILD_HOMEBREW_DIR}" fi cd "${BUILD_HOMEBREW_DIR}" # Update recipe mkdir -p "$(dirname "${FORMULA}")" cp "${FORMULA_FILE}" "${FORMULA}" sed -i'' -e "s/__FORMULA_NAME__/${FORMULA_NAME}/g" "${FORMULA}" sed -i'' -e "s/__NEW_VERSION__/${VERSION}/g" "${FORMULA}" for this_os in "${OS[@]}"; do for this_arch in "${ARCH[@]}"; do if [ "${this_arch}" == "arm64" ] && [ "${this_os}" == "linux" ]; then # TODO support linux arm64 continue fi tmp_var="TARBALL_HASH_${this_os}_${this_arch}" sed -i'' -e "s/__TARBALL_HASH_${this_os^^}_${this_arch^^}__/${!tmp_var}/g" "${FORMULA}" done done chmod 644 "${FORMULA}" # Use the correct machine user for committing git config --local user.email "${GITHUB_EMAIL}" git config --local user.name "${GITHUB_USER}" git add "${FORMULA}" git commit -m "Release ${VERSION}" # This cat is just so we can see the formula in case # the git permissions are incorrect and we can't publish # the change. Once we know the automation is working, we can # remove it. cat "${FORMULA}" git tag --message "Release ${VERSION}" "${VERSION}" git push origin "${VERSION}" main # Clean up the working directory rm -rf "${WORK_DIR}" ================================================ FILE: packaging/install-telepresence.ps1 ================================================ #Requires -RunAsAdministrator param ( $Path = "$env:ProgramFiles\telepresence" ) $current_directory = (Get-Location).path echo "Installing telepresence to $Path" Start-Process msiexec -Wait -verb runAs -Args "/i $current_directory\winfsp.msi /passive /qn /L*V winfsp-install.log" Start-Process msiexec -Wait -verb runAs -Args "/i $current_directory\sshfs-win.msi /passive /qn /L*V sshfs-win-install.log" if(!(test-path $Path)) { New-Item -ItemType Directory -Force -Path $Path } Copy-Item "telepresence.exe" -Destination "$Path" -Force Copy-Item "wintun.dll" -Destination "$Path" -Force # Update PATH if entries do not exist only $currentPath = [Environment]::GetEnvironmentVariable("Path", "Machine") @("$Path", "C:\Program Files\SSHFS-Win\bin") | Where-Object { $currentPath -notlike "*$_*" } | ForEach-Object { $currentPath = "$_;$currentPath" } [Environment]::SetEnvironmentVariable("Path", $currentPath, "Machine") echo "Telepresence installed to $Path" ================================================ FILE: packaging/telepresence.wxs.in ================================================  ================================================ FILE: packaging/windows-package.sh ================================================ #!/bin/bash set -e -x # This is a scrappy first attempt at a windows "installer". # It generates a zip file with all the dependencies and things required # for running telepresence in windows. We should eventually change this # to produce a msi, but for developer preview this is likely fine. if [ -z "$TELEPRESENCE_VERSION" ] then echo "Must set version" exit 1 fi if [ -z "$GOARCH" ] then echo "Must set GOARCH" exit 1 fi SCRIPT_DIR=$( dirname -- "${BASH_SOURCE[0]}") WINFSP_VERSION=1.11.22176 SSHFS_WIN_VERSION=3.7.21011 WINTUN_VERSION=0.14.1 # SHA2-256 Checksum for wintun-0.14.1.zip WINTUN_CHECKSUM=07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51 BINDIR="${BINDIR:-./build-output/bin}" rm -f "${BINDIR}/telepresence.zip" rm -f "${BINDIR}/telepresence-setup.exe" ZIPDIR="${ZIPDIR:-$BINDIR/telepresence-windows}" rm -rf "$ZIPDIR" mkdir -p "$ZIPDIR" if [[ ! "${ZIPDIR}" ]]; then echo "Could not create $ZIPDIR for windows package" exit 1 fi # Download sshfs-win.msi + winfsp.msi # ${WINFSP_VERSION%.*} will remove the last `.` and everything after it curl -L -o "${ZIPDIR}/winfsp.msi" "https://github.com/billziss-gh/winfsp/releases/download/v${WINFSP_VERSION%.*}/winfsp-${WINFSP_VERSION}.msi" curl -L -o "${ZIPDIR}/sshfs-win.msi" "https://github.com/billziss-gh/sshfs-win/releases/download/v${SSHFS_WIN_VERSION}/sshfs-win-${SSHFS_WIN_VERSION}-x64.msi" # Download wintun curl -L -o "${BINDIR}/wintun.zip" "https://www.wintun.net/builds/wintun-${WINTUN_VERSION}.zip" echo "${WINTUN_CHECKSUM} ${BINDIR}/wintun.zip" | sha256sum -c - unzip -p -C "${BINDIR}/wintun.zip" "wintun/bin/${GOARCH}/wintun.dll" > "${ZIPDIR}/wintun.dll" cp "${BINDIR}/telepresence.exe" "${ZIPDIR}/telepresence.exe" # Copy powershell install script into $ZIPDIR cp "${SCRIPT_DIR}/install-telepresence.ps1" "${ZIPDIR}/install-telepresence.ps1" powershell -Command "Compress-Archive -Path '${ZIPDIR}/*' -DestinationPath '${BINDIR}/telepresence.zip'" # Generate installer cp "${SCRIPT_DIR}/sidebar.png" "${ZIPDIR}/sidebar.png" TELEPRESENCE_PLAIN_VERSION=${TELEPRESENCE_VERSION#v} TELEPRESENCE_PLAIN_VERSION=${TELEPRESENCE_PLAIN_VERSION%-*} sed s/TELEPRESENCE_VERSION/"$TELEPRESENCE_PLAIN_VERSION"/ "${SCRIPT_DIR}/telepresence.wxs.in" > "${ZIPDIR}/telepresence.wxs" sed s/TELEPRESENCE_VERSION/"$TELEPRESENCE_PLAIN_VERSION"/ "${SCRIPT_DIR}/bundle.wxs.in" > "${ZIPDIR}/bundle.wxs" WIX_VERSION=4.0.4 dotnet tool install --global wix --version $WIX_VERSION cd "${ZIPDIR}" wix build -o telepresence.msi telepresence.wxs wix extension add -g WixToolset.Bal.wixext/$WIX_VERSION wix build -ext WixToolset.Bal.wixext/$WIX_VERSION -o "${BINDIR}/telepresence-setup.exe" bundle.wxs rm -rf "${ZIPDIR}" ================================================ FILE: pkg/agentconfig/container.go ================================================ package agentconfig import ( "context" "fmt" "regexp" "strconv" "strings" "github.com/go-json-experiment/json" core "k8s.io/api/core/v1" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/pkg/annotation" "github.com/telepresenceio/telepresence/v2/pkg/maps" "github.com/telepresenceio/telepresence/v2/pkg/types" ) type ContainerBuilder struct { MountPolicies types.MountPolicies Pod *core.PodTemplateSpec Config *Sidecar } // AgentContainer will return a configured traffic-agent. func (a *ContainerBuilder) AgentContainer(ctx context.Context) (*core.Container, map[string]string, error) { ports := make([]core.ContainerPort, 0, 5) confCns := a.configuredContainers(ctx) names := make(map[string]int) a.eachConfiguredContainer(confCns, func(app *core.Container, cc *Container) { switch cc.Replace { case ReplacePolicyContainer: // Simply inherit the ports of the replaced container ports = append(ports, app.Ports...) case ReplacePolicyIntercept: for _, ic := range PortUniqueIntercepts(cc) { name := ic.ContainerPortName // We don't want to apply duplication logic to empty strings // as - is not a valid starting character for port names. if name != "" { if n, ok := names[name]; ok { // if name already exists, append a number to it n++ names[name] = n // convert to numeric name suffix suffix := "-" + strconv.Itoa(n) // if string length of name plus number is greater than 15 if len(name)+len(suffix) > 15 { // truncate name to 15 characters name = name[:15-len(suffix)] } name += suffix } else { names[name] = 1 } } ports = append(ports, core.ContainerPort{ Name: name, ContainerPort: int32(ic.AgentPort), Protocol: core.Protocol(ic.Protocol.String()), }) } default: } }) evs := make([]core.EnvVar, 0, len(a.Config.Containers)*5) efs := make([]core.EnvFromSource, 0, len(a.Config.Containers)*3) a.eachConfiguredContainer(confCns, func(app *core.Container, cc *Container) { evs = appendAppContainerEnv(app, cc, evs) efs = appendAppContainerEnvFrom(app, cc, efs) }) if a.Config.APIPort > 0 { evs = append(evs, core.EnvVar{ Name: EnvAPIPort, Value: strconv.Itoa(int(a.Config.APIPort)), }) } evs = append(evs, core.EnvVar{ Name: EnvPrefixAgent + "POD_IP", ValueFrom: &core.EnvVarSource{ FieldRef: &core.ObjectFieldSelector{ APIVersion: "v1", FieldPath: "status.podIP", }, }, }, core.EnvVar{ Name: EnvPrefixAgent + "POD_UID", ValueFrom: &core.EnvVarSource{ FieldRef: &core.ObjectFieldSelector{ APIVersion: "v1", FieldPath: "metadata.uid", }, }, }, core.EnvVar{ Name: EnvPrefixAgent + "NAME", ValueFrom: &core.EnvVarSource{ FieldRef: &core.ObjectFieldSelector{ APIVersion: "v1", FieldPath: "metadata.name", }, }, }) mounts := make([]core.VolumeMount, 0, len(a.Config.Containers)*3) a.eachConfiguredContainer(confCns, func(app *core.Container, cc *Container) { mounts = a.appendVolumeMounts(app, cc, mounts) }) mounts = append(mounts, core.VolumeMount{ Name: PodInfoVolumeName, MountPath: PodInfoMountPath, }, core.VolumeMount{ Name: ExportsVolumeName, MountPath: ExportsMountPoint, }, core.VolumeMount{ Name: TempVolumeName, MountPath: TempMountPoint, }, ) anns := make(map[string]string) var err error mounts, err = a.mountSecrets(annotation.DownstreamTLSSecret, annotation.DownstreamCertificatePath, anns, mounts) if err != nil { return nil, nil, err } mounts, err = a.mountSecrets(annotation.UpstreamTLSSecret, annotation.UpstreamCertificatePath, anns, mounts) if err != nil { return nil, nil, err } if len(efs) == 0 { efs = nil } a.eachConfiguredContainer(confCns, func(app *core.Container, cc *Container) { if cc.Replace == ReplacePolicyContainer { cnJson, err := json.Marshal(app) if err != nil { clog.Errorf(ctx, "unable to marshal container %s.%s/%s to json: %v", a.Config.WorkloadName, a.Config.Namespace, app.Name, err) } anns[annotation.ReplaceAnnotationKey(cc.Name)] = string(cnJson) } }) cfg, _ := MarshalTight(a.Config) anns[annotation.Config] = cfg if len(ports) == 0 { ports = nil } ac := &core.Container{ Name: ContainerName, Image: a.Config.AgentImage, Args: []string{"agent"}, Ports: ports, Env: evs, EnvFrom: efs, VolumeMounts: mounts, ReadinessProbe: &core.Probe{ ProbeHandler: core.ProbeHandler{ Exec: &core.ExecAction{ Command: []string{"/bin/stat", "/tmp/agent/ready"}, }, }, }, ImagePullPolicy: core.PullPolicy(a.Config.PullPolicy), } if r := a.Config.Resources; r != nil { ac.Resources = *r } appSc := a.Config.SecurityContext if appSc == nil { // Assign the security context of the first container to the traffic agent. appSc, err = a.firstAppSecurityContext() if err != nil { return nil, nil, err } } ac.SecurityContext = appSc return ac, anns, nil } func (a *ContainerBuilder) mountSecrets(annotation, certPath string, anns map[string]string, mounts []core.VolumeMount) ([]core.VolumeMount, error) { secretsAndPorts, err := annotationPrefixedPorts(a.Pod.Annotations, annotation) if err != nil || len(secretsAndPorts) == 0 { return mounts, err } for _, secret := range maps.SortedKeys(secretsAndPorts) { volName := fmt.Sprintf("%s-vol", secret) volPath := fmt.Sprintf("%s/%s", DownstreamTLSVolumePath, secret) mounts = append(mounts, core.VolumeMount{ Name: volName, MountPath: volPath, }) for _, p := range secretsAndPorts[secret] { anns[fmt.Sprintf("%s.%d", certPath, p)] = volPath } } return mounts, nil } // annotationPrefixedPorts will return a map of secret names to a slice of ports extracted from annotations that match the given prefix. func annotationPrefixedPorts(anns map[string]string, prefix string) (m map[string][]uint16, err error) { if _, ok := anns[prefix]; ok { return nil, fmt.Errorf(`annotation %q must have a "." suffix`, prefix) } prefix += "." for k, v := range anns { if !strings.HasPrefix(k, prefix) { continue } ps := k[len(prefix):] if len(ps) == 0 { return nil, fmt.Errorf("empty port suffix for annotation %s", k) } pn, err := strconv.ParseUint(ps, 10, 16) if err != nil { return nil, fmt.Errorf("invalid port number %s for annotation %s", ps, k) } if m == nil { m = make(map[string][]uint16) } m[v] = append(m[v], uint16(pn)) } return m, nil } // Find the security context of the first container (with both intercepts and a set security context) and ensure // that any env interpolations in it are prefixed with the env-prefix of the corresponding config container. func (a *ContainerBuilder) firstAppSecurityContext() (*core.SecurityContext, error) { cns := a.Pod.Spec.Containers for _, cc := range a.Config.Containers { if len(cc.Intercepts) > 0 { for i := range cns { app := &cns[i] if app.Name != cc.Name { continue } if app.SecurityContext == nil { break } js, err := json.Marshal(app.SecurityContext) if err != nil { return nil, err } sc := core.SecurityContext{} err = json.Unmarshal([]byte(prefixInterpolated(string(js), EnvPrefixApp+cc.EnvPrefix)), &sc) if err != nil { return nil, err } return &sc, nil } } } return nil, nil } // configuredContainers will find each container in the given config and match it against a container // in the pod using its name. The returned slice is guaranteed to use the same index as the Sidecar.Containers slice. func (a *ContainerBuilder) configuredContainers(ctx context.Context) []*core.Container { cns := a.Pod.Spec.Containers result := make([]*core.Container, len(a.Config.Containers)) for ci, cc := range a.Config.Containers { for i := range cns { app := &cns[i] if app.Name == ContainerName { // The pod might hold JSON of replaced containers from an earlier patch annName := annotation.ReplacedContainerPrefix + cc.Name if appJson, ok := a.Pod.Annotations[annName]; ok { var cn core.Container err := json.Unmarshal([]byte(appJson), &cn) if err != nil { clog.Errorf(ctx, "failed to unmarshal container annotation %s: %v", annName, err) } result[ci] = &cn break } } else if app.Name == cc.Name { result[ci] = app break } } } return result } func (a *ContainerBuilder) eachConfiguredContainer(configureContainers []*core.Container, f func(*core.Container, *Container)) { for i, cn := range configureContainers { if cn != nil { f(cn, a.Config.Containers[i]) } } } // prefixInterpolated will prefix all environment variable names that are referenced using $(NAME) expressions // in the given string with the given prefix and return the result. Escaped expressions in the form $$(NAME), // unbalanced, or otherwise invalid expressions are not prefixed. func prefixInterpolated(str, pfx string) string { const ( stNormal = iota stDollarSeen stDollarParenSeen ) st := stNormal var bd, ev strings.Builder for _, c := range str { switch c { case '$': switch st { case stDollarParenSeen: // '$' is not a legal character in an environment interpolation expression so // terminate that expression without prefixing it. bd.WriteString(ev.String()) ev.Reset() st = stDollarSeen case stDollarSeen: st = stNormal default: st = stDollarSeen } bd.WriteByte('$') case '(': switch st { case stDollarParenSeen: // '(' is not a legal character in an environment interpolation expression so // terminate that expression without prefixing it. bd.WriteString(ev.String()) ev.Reset() st = stNormal case stDollarSeen: st = stDollarParenSeen default: st = stNormal } bd.WriteByte('(') case ')': if st == stDollarParenSeen && ev.Len() > 0 { bd.WriteString(pfx) bd.WriteString(ev.String()) ev.Reset() } st = stNormal bd.WriteByte(')') default: switch st { case stDollarParenSeen: ev.WriteRune(c) default: bd.WriteRune(c) st = stNormal } } } if ev.Len() > 0 { // Unbalanced interpolation. Just leave it as is. bd.WriteString(ev.String()) } return bd.String() } var envRxReplace = regexp.MustCompile(`\$\(([^)]+)\)`) func appendAppContainerEnv(app *core.Container, cc *Container, es []core.EnvVar) []core.EnvVar { pfx := EnvPrefixApp + cc.EnvPrefix pfxReplace := "$(" + pfx + "$1)" for _, e := range app.Env { e.Name = pfx + e.Name e.Value = envRxReplace.ReplaceAllString(e.Value, pfxReplace) es = append(es, e) } return es } func appendAppContainerEnvFrom(app *core.Container, cc *Container, es []core.EnvFromSource) []core.EnvFromSource { for _, e := range app.EnvFrom { e.Prefix = EnvPrefixApp + cc.EnvPrefix + e.Prefix es = append(es, e) } return es } ================================================ FILE: pkg/agentconfig/container_test.go ================================================ package agentconfig import ( "testing" ) func Test_prefixInterpolated(t *testing.T) { tests := []struct { name string arg string want string }{ { "empty", "", "", }, { "empty_ipl", "$()", "$()", }, { "alone", "$(IPL)", "$(_TEL_APP_A_IPL)", }, { "normal", "Normal $(IPL) text", "Normal $(_TEL_APP_A_IPL) text", }, { "escaped_ipl", "Escaped $$(IPL) text", "Escaped $$(IPL) text", }, { "nested_ipl", "Nested $(IP$(IPL)) text", "Nested $(IP$(_TEL_APP_A_IPL)) text", }, { "invalid_env", "Nested $(IP$) text", "Nested $(IP$) text", }, { "unbalanced", "Unbalanced $(IPL text", "Unbalanced $(IPL text", }, { "adjacent", "Adjacent $(IP1)$(IP2) text", "Adjacent $(_TEL_APP_A_IP1)$(_TEL_APP_A_IP2) text", }, { "dollar-separated", "Dollar $(IP1)$$$(IP2) separated", "Dollar $(_TEL_APP_A_IP1)$$$(_TEL_APP_A_IP2) separated", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := prefixInterpolated(tt.arg, "_TEL_APP_A_"); got != tt.want { t.Errorf("prefixInterpolated(%q) = %q, want %q", tt.arg, got, tt.want) } }) } } ================================================ FILE: pkg/agentconfig/initcontainer.go ================================================ package agentconfig import ( "fmt" core "k8s.io/api/core/v1" "github.com/telepresenceio/telepresence/v2/pkg/annotation" ) func InitContainer(config *Sidecar) *core.Container { ic := &core.Container{ Name: InitContainerName, Image: config.AgentImage, Args: []string{"agent-init"}, Env: []core.EnvVar{ { Name: "LOG_LEVEL", Value: config.LogLevel.String(), }, { Name: "AGENT_CONFIG", ValueFrom: &core.EnvVarSource{ FieldRef: &core.ObjectFieldSelector{ APIVersion: "v1", FieldPath: fmt.Sprintf("metadata.annotations['%s']", annotation.Config), }, }, }, { Name: "POD_IP", ValueFrom: &core.EnvVarSource{ FieldRef: &core.ObjectFieldSelector{ APIVersion: "v1", FieldPath: "status.podIP", }, }, }, }, SecurityContext: &core.SecurityContext{ Capabilities: &core.Capabilities{ Add: []core.Capability{"NET_ADMIN"}, }, }, } if r := config.InitResources; r != nil { ic.Resources = *r } if s := config.InitSecurityContext; s != nil { ic.SecurityContext = s } return ic } ================================================ FILE: pkg/agentconfig/injectpolicy.go ================================================ package agentconfig import ( "fmt" ) // InjectPolicy specifies when the agent injector mutating webhook will inject a traffic-agent into // a pod. type InjectPolicy int var epNames = [...]string{"OnDemand", "WhenEnabled", "Never"} //nolint:gochecknoglobals // constant names const ( // OnDemand tells the injector to inject the traffic-agent the first time someone makes an attempt // to intercept the workload, even if the telepresence.io/inject-traffic-agent is // missing. // // OnDemand has lower priority than the annotation. If the annotation is set to "enabled", then // the injector will inject the traffic-agent in advance into all pods that are created or updated. // If it is "disabled", then no injection will take place. // // This is the default setting. OnDemand InjectPolicy = iota // WhenEnabled tells the injector to inject the traffic-agent in advance into all pods that are // created or updated when the telepresence.io/inject-traffic-agent annotation is // present and set to "enabled". WhenEnabled // Never will disable the injector. Never ) func (aps InjectPolicy) String() string { return epNames[aps] } func NewEnablePolicy(s string) (InjectPolicy, error) { for i, n := range epNames { if s == n { return InjectPolicy(i), nil } } return 0, fmt.Errorf("invalid InjectPolicy: %q", s) } func (aps InjectPolicy) MarshalJSON() ([]byte, error) { return []byte(aps.String()), nil } //goland:noinspection GoMixedReceiverTypes func (aps *InjectPolicy) EnvDecode(val string) (err error) { var as InjectPolicy if val == "" { as = OnDemand } else if as, err = NewEnablePolicy(val); err != nil { return err } *aps = as return nil } //goland:noinspection GoMixedReceiverTypes func (aps *InjectPolicy) UnmarshalText(value []byte) error { return aps.EnvDecode(string(value)) } //goland:noinspection GoMixedReceiverTypes func (aps *InjectPolicy) UnmarshalJSON(value []byte) error { return aps.EnvDecode(string(value)) } ================================================ FILE: pkg/agentconfig/intercepttarget.go ================================================ package agentconfig import ( "bytes" "context" "fmt" "strconv" core "k8s.io/api/core/v1" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/rpc/v2/manager" "github.com/telepresenceio/telepresence/v2/pkg/ioutil" "github.com/telepresenceio/telepresence/v2/pkg/types" ) // InterceptTarget describes the mapping between service ports and one container port, or if no service // is used, just the container port. // All entries must be guaranteed to all have the same Protocol, ContainerPort, and AgentPort. // The slice must be considered immutable once created using NewInterceptTarget. type InterceptTarget []*Intercept func NewInterceptTarget(ics []*Intercept) InterceptTarget { // This is a parameter assertion. If it is triggered, then something is dead wrong in the caller code. ni := len(ics) if ni == 0 { panic("attempt to add intercept create an InterceptTarget with no Intercepts") } if ni > 1 { icZero := ics[0] for i := 1; i < ni; i++ { ic := ics[i] if icZero.AgentPort != ic.AgentPort || icZero.ContainerPort != ic.ContainerPort || icZero.Protocol != ic.Protocol { panic("attempt to add intercept to an InterceptTarget with different AgentPort or ContainerPort") } } } return ics } func (cp InterceptTarget) MatchForSpec(spec *manager.InterceptSpec) bool { ic := cp[0] cnPort := uint16(spec.ContainerPort) return cnPort == ic.ContainerPort && ic.Protocol == types.FromK8sProtocol(core.Protocol(spec.Protocol)) } func (cp InterceptTarget) AgentPort() uint16 { return cp[0].AgentPort } func (cp InterceptTarget) TargetPortNumeric() bool { for _, ic := range cp { if ic.TargetPortNumeric { return true } } return false } func (cp InterceptTarget) ContainerPort() uint16 { return cp[0].ContainerPort } func (cp InterceptTarget) ContainerPortName() string { return cp[0].ContainerPortName } func (cp InterceptTarget) Protocol() types.Proto { return cp[0].Protocol } func portString(ic *Intercept) (s string) { if ic.ServiceUID != "" { p := ic.ServicePortName if p == "" { p = strconv.Itoa(int(ic.ServicePort)) } return fmt.Sprintf("service port %s:%s", ic.ServiceName, p) } p := ic.ContainerPortName if p == "" { p = strconv.Itoa(int(ic.ContainerPort)) } return fmt.Sprintf("container port %s", p) } func (cp InterceptTarget) AppProtocol(ctx context.Context) (proto string) { var foundIc *Intercept for _, ic := range cp { if ic.AppProtocol == "" { continue } if foundIc == nil { foundIc = ic proto = foundIc.AppProtocol } else if foundIc.AppProtocol != ic.AppProtocol { clog.Warnf(ctx, "%s appProtocol %s differs from %s appProtocol %s. %s will be used for %s", portString(foundIc), proto, portString(ic), ic.AppProtocol, proto, portString(ic)) } } return proto } func (cp InterceptTarget) HasServicePortName(name string) bool { for _, sv := range cp { if sv.ServicePortName == name { return true } } return false } func (cp InterceptTarget) HasServicePort(port uint16) bool { for _, sv := range cp { if sv.ServicePort == port { return true } } return false } func (cp InterceptTarget) String() string { sb := bytes.Buffer{} l := len(cp) if l > 1 { sb.WriteByte('[') } for i, ic := range cp { if i > 0 { switch l { case 2: sb.WriteString(" and ") case i + 1: sb.WriteString(", and ") default: sb.WriteString(", ") } } sb.WriteString(portString(ic)) } if l > 1 { sb.WriteByte(']') } if l > 1 || cp[0].ServiceName != "" { ioutil.Printf(&sb, " => container port %d/%s", cp.ContainerPort(), cp.Protocol()) } return sb.String() } ================================================ FILE: pkg/agentconfig/sidecar.go ================================================ package agentconfig import ( "fmt" "log/slog" "time" core "k8s.io/api/core/v1" k8sTypes "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/yaml" "github.com/telepresenceio/telepresence/v2/pkg/annotation" "github.com/telepresenceio/telepresence/v2/pkg/errcat" "github.com/telepresenceio/telepresence/v2/pkg/json" "github.com/telepresenceio/telepresence/v2/pkg/k8sapi" "github.com/telepresenceio/telepresence/v2/pkg/types" ) const ( ContainerName = "traffic-agent" ManagerAppName = "traffic-manager" InitContainerName = "tel-agent-init" MountPrefixApp = "/tel_app_mounts" ExportsVolumeName = "export-volume" ExportsMountPoint = "/tel_app_exports" TempVolumeName = "tel-agent-tmp" TempMountPoint = "/tmp" EnvPrefix = "_TEL_" EnvPrefixAgent = EnvPrefix + "AGENT_" EnvPrefixApp = EnvPrefix + "APP_" PodInfoVolumeName = "pod-info" PodInfoMountPath = "/etc/podinfo" DownstreamTLSVolumeName = "downstream-tls" DownstreamTLSVolumePath = "/downstream-tls" UpstreamTLSVolumeName = "upstream-tls" UpstreamTLSVolumePath = "/upstream-tls" // EnvAgentConfig is the environment variable where the traffic-agent finds its own config. EnvAgentConfig = "AGENT_CONFIG" // EnvInterceptContainer intercepted container propagated to client during intercept. EnvInterceptContainer = "TELEPRESENCE_CONTAINER" // EnvInterceptMounts mount points propagated to client during intercept. EnvInterceptMounts = "TELEPRESENCE_MOUNTS" // EnvLocalMounts mount points that the client should mount locally (e.g. /tmp). EnvLocalMounts = "TELEPRESENCE_LOCAL_MOUNTS" // EnvAPIHost is the host name of the Telepresence API server when it is enabled. EnvAPIHost = "TELEPRESENCE_API_HOST" // EnvAPIPort is the port number of the Telepresence API server when it is enabled. EnvAPIPort = "TELEPRESENCE_API_PORT" WorkloadNameLabel = annotation.DomainPrefix + "workloadName" WorkloadKindLabel = annotation.DomainPrefix + "workloadKind" WorkloadEnabledLabel = annotation.DomainPrefix + "workloadEnabled" ) type ReplacePolicy int const ( // ReplacePolicyIntercept The traffic-agent will receive all traffic intended for the ports of the app-container and // then either route that traffic to the client or to the original app-container depending on if the port is // intercepted or not. This will require an init-container when the targetPort of the service is numeric or // when the service is headless. ReplacePolicyIntercept ReplacePolicy = iota // ReplacePolicyContainer The traffic-agent is currently replacing the app container and routes all traffic to the // client. ReplacePolicyContainer // ReplacePolicyInactive The traffic-agent is not interfering with any ports or containers. ReplacePolicyInactive ) // Intercept describes the mapping between a service port and an intercepted container port or, when // service is used, just the container port. type Intercept struct { // The name of the intercepted container port ContainerPortName string `json:"containerPortName,omitzero"` // Name of intercepted service ServiceName string `json:"serviceName,omitzero"` // UID of intercepted service ServiceUID k8sTypes.UID `json:"serviceUID,omitzero"` // Name of intercepted service port ServicePortName string `json:"servicePortName,omitzero"` // TargetPortNumeric is set to true unless the servicePort has a symbolic target port TargetPortNumeric bool `json:"targetPortNumeric,omitzero"` // L4 protocol used by the intercepted port Protocol types.Proto `json:"protocol,omitzero"` // L7 protocol used by the intercepted port AppProtocol string `json:"appProtocol,omitzero"` // True if the service is headless Headless bool `json:"headless,omitzero"` // The number of the intercepted container port ContainerPort uint16 `json:"containerPort,omitzero"` // Number of intercepted service port ServicePort uint16 `json:"servicePort,omitzero"` // The port number that the agent listens to AgentPort uint16 `json:"agentPort,omitzero"` } // Container describes one container that can have one or several intercepts. type Container struct { // Name of the intercepted container Name string `json:"name,omitempty" yaml:"name,omitzero"` // The intercepts managed by the agent Intercepts []*Intercept `json:"intercepts,omitempty"` // Prefix used for all keys in the container environment copy EnvPrefix string `json:"envPrefix,omitzero"` // Where the agent mounts its volumes MountPoint string `json:"mountPoint,omitzero"` // Mounts controls how the traffic-agent makes mounts available for this container. Each // policy is keyed with either the name of a volume or by a path prefix that matches the mounted // path. Mounts types.MountPolicies `json:"mounts,omitempty"` // MountPaths are the actual mount points that are mounted by this container // // Deprecated: Use Mounts. MountPaths []string `json:"Mounts,omitempty"` // Replace is whether the agent should replace the intercepted container, it's ports, or nothing. Replace ReplacePolicy `json:"replace,omitzero"` } // The Sidecar configures the traffic-agent sidecar. type Sidecar struct { // If Create is true, then this Config has not yet been filled in. Create bool `json:"create,omitzero"` // If Manual is true, then this Config is created manually. Manual bool `json:"manual,omitzero"` // The fully qualified name of the traffic-agent image, i.e. "ghcr.io/telepresenceio/tel2:2.5.4". AgentImage string `json:"agentImage,omitzero"` // One of "IfNotPresent", "Always", or "Never". PullPolicy string `json:"pullPolicy,omitzero"` // Secrets used when pulling the agent image from a private registry. PullSecrets []core.LocalObjectReference `json:"pullSecrets,omitempty"` // The name of the traffic-agent instance. Typically, the same as the name of the workload owner. AgentName string `json:"agentName,omitzero"` // The namespace of the intercepted pod. Namespace string `json:"namespace,omitzero"` // LogLevel used for all traffic-agent logging. LogLevel slog.Level `json:"logLevel,omitzero"` // The name of the workload that the pod originates from. WorkloadName string `json:"workloadName,omitzero"` // The kind of workload that the pod originates from. WorkloadKind k8sapi.Kind `json:"workloadKind,omitzero"` // The host used when connecting to the traffic-manager. ManagerHost string `json:"managerHost,omitzero"` // The port used when connecting to the traffic manager. ManagerPort uint16 `json:"managerPort,omitzero"` // The port used by the agents restFUL API server. APIPort uint16 `json:"apiPort,omitzero"` // Resources for the sidecar. Resources *core.ResourceRequirements `json:"resources,omitempty"` // InitResources is the resource requirements for the initContainer sidecar. InitResources *core.ResourceRequirements `json:"initResources,omitempty"` // MountPolicies controls how the agent will handle new mounts that might arrive when // the pod is created. MountPolicies types.MountPolicies `json:"mountPolicies,omitzero"` // The intercepts managed by the agent. Containers []*Container `json:"containers,omitempty"` // SecurityContext for the sidecar. SecurityContext *core.SecurityContext `json:"securityContext,omitempty"` // InitSecurityContext is the SecurityContext for the initContainer sidecar. InitSecurityContext *core.SecurityContext `json:"initSecurityContext,omitempty"` // ClientConnectionTTL is the maximum duration that the traffic-agent will keep an idle client connection alive. ClientConnectionTTL time.Duration `json:"clientConnectionTTL,omitempty,format:units"` // EnableMetrics is true if the traffic-agent should send consumption reports to the traffic-manager. EnableMetrics bool `json:"enableMetrics,omitempty"` // EnableH2cProbing is true if the traffic-agent should enable H2C probing on TCP ports that have no TLS and no appProtocol. EnableH2cProbing bool `json:"enableH2cProbing,omitempty"` // WatchRetryInterval is the interval between retries that a watcher uses when the gRPC connection to the traffic-manager is lost. WatchRetryInterval time.Duration `json:"watchRetryInterval,format:units"` } // InterceptTarget returns the container and intercepts that are parents of the given container port and protocol. func (s *Sidecar) InterceptTarget(containerPort uint16, proto types.Proto) (*Container, InterceptTarget) { for _, c := range s.Containers { for i, ic := range c.Intercepts { if ic.ContainerPort == containerPort && ic.Protocol == proto { it := InterceptTarget{ic} i++ if i < len(c.Intercepts) { for _, ic := range c.Intercepts[i:] { if ic.ContainerPort == containerPort && ic.Protocol == proto { it = append(it, ic) } } } return c, it } } } return nil, nil } // InterceptorInactivePort returns the port that the interceptor should write to when it isn't serving // an intercept. The port will be the container port unless some service uses a numeric target port // that targets the container port. // // When a numeric target port is specified, the init-container sets up an iptables NAT PREROUTING rule // to redirect all traffic destined for the container port to the corresponding port where the agent's // forwarder is listening. When no intercept is active, the forwarder routes traffic to the container // port using the pod's IP address. However, directly routing to the container port would trigger the // NAT PREROUTING rule again, causing an infinite loop. To avoid this, the forwarder uses a proxy port, // which is redirected to the container port via an iptables NAT OUTPUT rule. func (s *Sidecar) InterceptorInactivePort(containerPort uint16, proto types.Proto) uint16 { _, it := s.InterceptTarget(containerPort, proto) if it != nil && it.TargetPortNumeric() { return s.ProxyPort(it.AgentPort()) } return containerPort } // Clone returns a deep copy of the Sidecar. func (s *Sidecar) Clone() *Sidecar { cs := *s for ci, cn := range cs.Containers { ccn := *cn cs.Containers[ci] = &ccn for ii, ic := range ccn.Intercepts { cic := *ic ccn.Intercepts[ii] = &cic } } return &cs } // EachContainer will find each container and match it against a container // in the pod using its name. The given function is called once for each match. func (s *Sidecar) EachContainer(pod *core.Pod, f func(*core.Container, *Container)) { cns := pod.Spec.Containers for _, cc := range s.Containers { for i := range cns { if app := &cns[i]; app.Name == cc.Name { f(app, cc) break } } } } // FindIntercept finds the [Container] and [Intercept] configuration that matches the given service name, container name, and service- or container port. // The port will be considered a service port for intercepts that have a service UID and a container port for service less intercepts. func (s *Sidecar) FindIntercept(serviceName, containerName string, port types.PortIdentifier) (foundCN *Container, foundIC *Intercept, err error) { for _, cn := range s.Containers { for _, ic := range cn.Intercepts { if !(serviceName == "" || serviceName == ic.ServiceName) { continue } if port != "" { if ic.ServiceUID != "" { if !IsInterceptForService(port, ic) { continue } } else if !IsInterceptForContainer(port, ic) { continue } } if foundIC == nil { foundCN = cn if containerName != "" { for _, cx := range s.Containers { if cx.Name == containerName { foundCN = cx break } } } foundIC = ic continue } var msg string switch { case serviceName == "" && port == "": msg = fmt.Sprintf("%s %s.%s has multiple interceptable ports.\n"+ "Please specify the service and/or port you want to intercept "+ "by passing the --service= and/or --port= flag.", s.WorkloadKind, s.WorkloadName, s.Namespace) case serviceName == "": msg = fmt.Sprintf("%s %s.%s has multiple interceptable services with port %s.\n"+ "Please specify the service you want to intercept by passing the --service= flag.", s.WorkloadKind, s.WorkloadName, s.Namespace, port) case port == "": msg = fmt.Sprintf("%s %s.%s has multiple interceptable ports in service %s.\n"+ "Please specify the port you want to intercept by passing the --port= flag.", s.WorkloadKind, s.WorkloadName, s.Namespace, serviceName) default: msg = fmt.Sprintf("%s %s.%s intercept config is broken. Service %s, port %s is declared more than once\n", s.WorkloadKind, s.WorkloadName, s.Namespace, serviceName, port) } return nil, nil, errcat.User.New(msg) } } if foundIC != nil { return foundCN, foundIC, nil } ss := "" if serviceName != "" { if port != "" { ss = fmt.Sprintf(" matching service %s, port %s", serviceName, port) } else { ss = fmt.Sprintf(" matching service %s", serviceName) } } else if port != "" { ss = fmt.Sprintf(" matching port %s", port) } return nil, nil, errcat.User.Newf("%s %s.%s has no interceptable port%s", s.WorkloadKind, s.WorkloadName, s.Namespace, ss) } // Marshal returns YAML encoding of the Sidecar. func (s *Sidecar) Marshal() ([]byte, error) { return yaml.Marshal(s) } // UnmarshalYAML creates a new instance of the SidecarType from the given YAML data. func UnmarshalYAML(data []byte) (*Sidecar, error) { into := new(Sidecar) data, err := yaml.YAMLToJSON(data) if err != nil { return nil, err } if err := json.Unmarshal(data, into, true); err != nil { return nil, err } return into, nil } // MarshalTight marshals the given instance into JSON data, with data relating to the creation of the // container manifest stripped off. func MarshalTight(ac *Sidecar) (string, error) { // Strip things that are not needed once the container has been created. ai := ac.AgentImage pp := ac.PullPolicy ps := ac.PullSecrets ir := ac.InitResources sc := ac.SecurityContext is := ac.InitSecurityContext ac.AgentImage = "" ac.PullPolicy = "" ac.PullSecrets = nil ac.InitResources = nil ac.SecurityContext = nil ac.InitSecurityContext = nil data, err := json.Marshal(ac) ac.AgentImage = ai ac.PullPolicy = pp ac.PullSecrets = ps ac.InitResources = ir ac.SecurityContext = sc ac.InitSecurityContext = is if err != nil { return "", err } return string(data), err } // UnmarshalJSON creates a new instance of the SidecarType from the given JSON data. func UnmarshalJSON(data string) (*Sidecar, error) { into := new(Sidecar) if err := json.Unmarshal([]byte(data), into, true); err != nil { return nil, err } return into, nil } ================================================ FILE: pkg/agentconfig/util.go ================================================ package agentconfig import "github.com/telepresenceio/telepresence/v2/pkg/types" // IsInterceptForService returns true when the given PortIdentifier is equal to the // config's ServicePortName, or can be parsed to an integer equal to the config's ServicePort. func IsInterceptForService(pi types.PortIdentifier, ic *Intercept) bool { proto, name, num := pi.ProtoAndNameOrNumber() if pi.HasProto() && proto != ic.Protocol { return false } if name == "" { return num == ic.ServicePort } return name == ic.ServicePortName } // IsInterceptForContainer returns true when the given PortIdentifier is equal to the // config's ContainerPort, or can be parsed to an integer equal to the config's ContainerPort. func IsInterceptForContainer(pi types.PortIdentifier, ic *Intercept) bool { proto, name, num := pi.ProtoAndNameOrNumber() if pi.HasProto() && proto != ic.Protocol { return false } if name == "" { return num == ic.ContainerPort } return name == ic.ContainerPortName } // PortUniqueIntercepts returns a slice of intercepts for the container where each intercept // is unique with respect to the AgentPort and Protocol. // This method should always be used when iterating the intercepts, except for when an // intercept is identified via a service. func PortUniqueIntercepts(cn *Container) []*Intercept { um := make(map[types.PortAndProto]struct{}, len(cn.Intercepts)) ics := make([]*Intercept, 0, len(cn.Intercepts)) for _, ic := range cn.Intercepts { k := types.PortAndProto{Port: ic.AgentPort, Proto: ic.Protocol} if _, ok := um[k]; !ok { um[k] = struct{}{} ics = append(ics, ic) } } return ics } // ProxyPort returns a port that can be used as a proxy for a container port for the given // agentPort (the listener port for the traffic agent). // The proxy port will be the agentPort + the maximum number of possible intercepts for the sidecar. func (s *Sidecar) ProxyPort(agentPort uint16) uint16 { return agentPort + 11 + uint16(s.numberOfPossibleIntercepts()) } func (s *Sidecar) numberOfPossibleIntercepts() (count int) { for _, c := range s.Containers { count += len(c.Intercepts) } return count } ================================================ FILE: pkg/agentconfig/volumes.go ================================================ package agentconfig import ( "fmt" "strings" core "k8s.io/api/core/v1" "github.com/telepresenceio/telepresence/v2/pkg/annotation" "github.com/telepresenceio/telepresence/v2/pkg/dos" "github.com/telepresenceio/telepresence/v2/pkg/maps" "github.com/telepresenceio/telepresence/v2/pkg/types" ) func AgentVolumes(agentName string, pod *core.Pod) (volumes []core.Volume, err error) { volumes = []core.Volume{ { Name: PodInfoVolumeName, VolumeSource: core.VolumeSource{ DownwardAPI: &core.DownwardAPIVolumeSource{ Items: []core.DownwardAPIVolumeFile{ { Path: "annotations", FieldRef: &core.ObjectFieldSelector{ FieldPath: "metadata.annotations", }, }, }, DefaultMode: nil, }, }, }, { Name: ExportsVolumeName, VolumeSource: core.VolumeSource{ EmptyDir: &core.EmptyDirVolumeSource{}, }, }, { Name: TempVolumeName, VolumeSource: core.VolumeSource{ EmptyDir: &core.EmptyDirVolumeSource{}, }, }, } if pod == nil { // Called from genyaml so no pod is available to provide annotations. return volumes, nil } // The name of the TLS secret in the annotations might contain environment variable expansions. The expansions // allowed here are "$AGENT_NAME" and "$_TEL_AGENT_NAME". The latter is for backward compatibility with older // agents where this expansion happened in the traffic-agent. env := dos.MapEnv{"AGENT_NAME": agentName} volumes, err = appendSecretVolume(env, annotation.DownstreamTLSSecret, pod, volumes) if err != nil { return nil, err } return appendSecretVolume(env, annotation.UpstreamTLSSecret, pod, volumes) } func appendSecretVolume(env dos.Env, annotation string, pod *core.Pod, volumes []core.Volume) ([]core.Volume, error) { secretsAndPorts, err := annotationPrefixedPorts(pod.Annotations, annotation) if err != nil || len(secretsAndPorts) == 0 { return volumes, err } for _, secret := range maps.SortedKeys(secretsAndPorts) { volumes = append(volumes, core.Volume{ Name: fmt.Sprintf("%s-vol", secret), VolumeSource: core.VolumeSource{ Secret: &core.SecretVolumeSource{ SecretName: env.ExpandEnv(secret), }, }, }) } return volumes, nil } func (a *ContainerBuilder) appendVolumeMounts(app *core.Container, cc *Container, mounts []core.VolumeMount) []core.VolumeMount { pfx := EnvPrefixApp + cc.EnvPrefix for _, m := range app.VolumeMounts { mp := a.Config.MountPolicies.Get(m.Name, m.MountPath) switch mp { case types.MountPolicyIgnore, types.MountPolicyLocal: case types.MountPolicyRemoteReadOnly: if !m.ReadOnly { rco := core.RecursiveReadOnlyIfPossible m.ReadOnly = true m.RecursiveReadOnly = &rco } fallthrough default: m.Name = prefixInterpolated(m.Name, pfx) m.MountPath = prefixInterpolated(cc.MountPoint+"/"+strings.TrimPrefix(m.MountPath, "/"), pfx) m.SubPath = prefixInterpolated(m.SubPath, pfx) m.SubPathExpr = prefixInterpolated(m.SubPathExpr, pfx) mounts = append(mounts, m) } } return mounts } ================================================ FILE: pkg/agentmap/capsbase26.go ================================================ package agentmap // CapsBase26 converts the given number into base 26, represented using the letters 'A' to 'Z'. func CapsBase26(v uint64) string { return addBase26('A', v) } // Base26 converts the given number into base 26, represented using the letters 'a' to 'z'. func Base26(v uint64) string { return addBase26('a', v) } // Base26 converts the given number into base 26 represented using the letters 'a' to 'z'. func addBase26(c byte, v uint64) string { i := 14 // covers v == math.MaxUint64 b := make([]byte, i) for { l := v % 26 i-- b[i] = c + byte(l) if v < 26 { break } v /= 26 } return string(b[i:]) } ================================================ FILE: pkg/agentmap/capsbase26_test.go ================================================ package agentmap import ( "math" "testing" "github.com/stretchr/testify/assert" ) func TestCapsBase26(t *testing.T) { tests := []struct { name string v uint64 want string }{ { "zero", 0, "A", }, { "25", 25, "Z", }, { "26", 26, "BA", }, { "51", 26 + 25, "BZ", }, { "52", 2 * 26, "CA", }, { "1351", 2*26*26 - 1, "BZZ", }, { "1352", 2 * 26 * 26, "CAA", }, { "maxuint", math.MaxUint64, "HLHXCZMXSYUMQP", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equalf(t, tt.want, CapsBase26(tt.v), "CapsBase26(%v)", tt.v) }) } } ================================================ FILE: pkg/agentmap/discorvery.go ================================================ package agentmap import ( "bytes" "context" "fmt" "regexp" "sort" core "k8s.io/api/core/v1" k8sErrors "k8s.io/apimachinery/pkg/api/errors" meta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/util/intstr" apps "k8s.io/client-go/informers/apps/v1" argorollouts "github.com/datawire/argo-rollouts-go-client/pkg/client/informers/externalversions/rollouts/v1alpha1" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/pkg/agentconfig" "github.com/telepresenceio/telepresence/v2/pkg/annotation" "github.com/telepresenceio/telepresence/v2/pkg/informer" "github.com/telepresenceio/telepresence/v2/pkg/k8sapi" ) var ReplicaSetNameRx = regexp.MustCompile(`\A(.+)-[a-f0-9]+\z`) type WorkloadOwnerNotFoundError struct { *k8sErrors.StatusError } func FindOwnerWorkload(ctx context.Context, obj k8sapi.Object, supportedWorkloadKinds k8sapi.Kinds) (k8sapi.Workload, error) { clog.Tracef(ctx, "FindOwnerWorkload(%s,%s,%s)", obj.GetName(), obj.GetNamespace(), obj.GetKind()) lbs := obj.GetLabels() if wlName, ok := lbs[agentconfig.WorkloadNameLabel]; ok { kind, ok := lbs[agentconfig.WorkloadKindLabel] if ok && !supportedWorkloadKinds.Contains(k8sapi.Kind(kind)) { return nil, fmt.Errorf("unable to find %s owner for %s.%s (annotation controlled)", kind, obj.GetName(), obj.GetNamespace()) } return GetWorkload(ctx, wlName, obj.GetNamespace(), k8sapi.Kind(kind)) } refs := obj.GetOwnerReferences() ns := obj.GetNamespace() for i := range refs { if or := &refs[i]; or.Controller != nil && *or.Controller { kind := k8sapi.Kind(or.Kind) if kind == k8sapi.ReplicaSetKind && supportedWorkloadKinds.Contains(k8sapi.DeploymentKind) { // Try the common case first. Strip replicaset's generated hash and try to // get the deployment. If this succeeds, we have saved us a replicaset // lookup. if m := ReplicaSetNameRx.FindStringSubmatch(or.Name); m != nil { if wl, err := GetWorkload(ctx, m[1], ns, k8sapi.DeploymentKind); err == nil { return wl, nil } } } if supportedWorkloadKinds.Contains(kind) { wl, err := GetWorkload(ctx, or.Name, ns, kind) if err != nil { return nil, err } return FindOwnerWorkload(ctx, wl, supportedWorkloadKinds) } // A controller owner of unsupported workload kind is treated as "no owner". break } } if wl, ok := obj.(k8sapi.Workload); ok { return wl, nil } return nil, &WorkloadOwnerNotFoundError{StatusError: k8sErrors.NewNotFound( obj.GetGroupResource(), fmt.Sprintf("%s.%s", obj.GetName(), obj.GetNamespace()))} } func GetWorkload(ctx context.Context, name, namespace string, workloadKind k8sapi.Kind) (obj k8sapi.Workload, err error) { clog.Tracef(ctx, "GetWorkload(%s,%s,%s)", name, namespace, workloadKind) i := informer.GetFactory(ctx, namespace) if i == nil { clog.Debugf(ctx, "fetching %s %s.%s using direct API call", workloadKind, name, namespace) return k8sapi.GetWorkload(ctx, name, namespace, workloadKind) } ai, ri := i.GetK8sInformerFactory().Apps().V1(), i.GetArgoRolloutsInformerFactory().Argoproj().V1alpha1().Rollouts() return getWorkload(ai, ri, name, namespace, workloadKind) } func getWorkload(ai apps.Interface, ri argorollouts.RolloutInformer, name, namespace string, kind k8sapi.Kind) (obj k8sapi.Workload, err error) { switch kind { case k8sapi.DeploymentKind: return getDeployment(ai, name, namespace) case k8sapi.ReplicaSetKind: return getReplicaSet(ai, name, namespace) case k8sapi.StatefulSetKind: return getStatefulSet(ai, name, namespace) case k8sapi.RolloutKind: return getRollout(ri, name, namespace) case "": for _, wk := range k8sapi.KnownWorkloadKinds { if obj, err = getWorkload(ai, ri, name, namespace, wk); err == nil { return obj, nil } if !k8sErrors.IsNotFound(err) { return nil, err } } return nil, k8sErrors.NewNotFound(core.Resource("workload"), name+"."+namespace) default: return nil, k8sapi.UnsupportedWorkloadKindError(kind) } } func getDeployment(ai apps.Interface, name, namespace string) (wl k8sapi.Workload, err error) { dep, err := ai.Deployments().Lister().Deployments(namespace).Get(name) if err != nil { return nil, err } return k8sapi.Deployment(dep), nil } func getRollout(ri argorollouts.RolloutInformer, name, namespace string) (wl k8sapi.Workload, err error) { if ri == nil { return nil, k8sapi.UnsupportedWorkloadKindError("Rollout") } rollout, err := ri.Lister().Rollouts(namespace).Get(name) if err != nil { return nil, err } return k8sapi.Rollout(rollout), nil } func getReplicaSet(ai apps.Interface, name, namespace string) (k8sapi.Workload, error) { rs, err := ai.ReplicaSets().Lister().ReplicaSets(namespace).Get(name) if err != nil { return nil, err } return k8sapi.ReplicaSet(rs), nil } func getStatefulSet(ai apps.Interface, name, namespace string) (k8sapi.Workload, error) { ss, err := ai.StatefulSets().Lister().StatefulSets(namespace).Get(name) if err != nil { return nil, err } return k8sapi.StatefulSet(ss), nil } func FindServicesForPod(ctx context.Context, pod *core.PodTemplateSpec, svcName string) ([]k8sapi.Object, error) { switch { case svcName != "": var svc *core.Service var err error if f := informer.GetK8sFactory(ctx, pod.Namespace); f != nil { svc, err = f.Core().V1().Services().Lister().Services(pod.Namespace).Get(svcName) } else { // This shouldn't happen really. clog.Debugf(ctx, "fetching service %s.%s using direct API call", svcName, pod.Namespace) svc, err = k8sapi.GetK8sInterface(ctx).CoreV1().Services(pod.Namespace).Get(ctx, svcName, meta.GetOptions{}) } if err != nil { if k8sErrors.IsNotFound(err) { return nil, fmt.Errorf( "unable to find service %s specified by annotation %s declared in pod %s.%s", svcName, annotation.InjectServiceName, pod.Name, pod.Namespace) } return nil, err } return []k8sapi.Object{k8sapi.Service(svc)}, nil case len(pod.Labels) > 0: return findServicesSelecting(ctx, pod.Namespace, labels.Set(pod.Labels)) default: return nil, fmt.Errorf("unable to find a service using pod %s.%s because it has no labels", pod.Name, pod.Namespace) } } type objectsStringer []k8sapi.Object func (os objectsStringer) String() string { b := bytes.Buffer{} l := len(os) if l == 0 { return "no services" } for i, o := range os { if i > 0 { if l != 2 { b.WriteString(", ") } if i == l-1 { b.WriteString(" and ") } } b.WriteString(o.GetName()) } return b.String() } // findServicesSelecting finds all services that has a selector that matches the given labels. func findServicesSelecting(ctx context.Context, namespace string, lbs labels.Labels) ([]k8sapi.Object, error) { var ms []k8sapi.Object var scanned int if f := informer.GetK8sFactory(ctx, namespace); f != nil { ss, err := f.Core().V1().Services().Lister().Services(namespace).List(labels.Everything()) if err != nil { return nil, err } scanned = len(ss) for _, s := range ss { sel := s.Spec.Selector if len(sel) > 0 && labels.SelectorFromValidatedSet(sel).Matches(lbs) { ms = append(ms, k8sapi.Service(s)) } } } else { // This shouldn't happen really. clog.Tracef(ctx, "Fetching services in %s using direct API call", namespace) l, err := k8sapi.GetK8sInterface(ctx).CoreV1().Services(namespace).List(ctx, meta.ListOptions{}) if err != nil { return nil, err } items := l.Items scanned = len(items) for i := range items { s := &items[i] sel := s.Spec.Selector if len(sel) > 0 && labels.SelectorFromValidatedSet(sel).Matches(lbs) { ms = append(ms, k8sapi.Service(s)) } } } // Ensure predictable order of found services sort.Slice(ms, func(i, j int) bool { return ms[i].GetName() < ms[j].GetName() }) clog.Tracef(ctx, "Scanned %d services in namespace %s and found that %s selects labels %v", scanned, namespace, objectsStringer(ms), lbs) return ms, nil } // findContainerMatchingPort finds the container that matches the given ServicePort. The match is // made using Protocol, and the Name or the ContainerPort field of each port in each container // depending on if the service port is symbolic or numeric. The first container with a matching // port is returned along with the index of the container port that matched. // // The first container with no ports at all is returned together with a port index of -1, in case // no port match could be made and the service port is numeric. This enables intercepts of containers // that indeed do listen a port but lack a matching port description in the manifest, which is what // you get if you do: // // kubectl create deploy my-deploy --image my-image // kubectl expose deploy my-deploy --port 80 --target-port 8080 func findContainerMatchingPort(port *core.ServicePort, cns []core.Container) (*core.Container, int) { // The protocol of the targetPort must match the protocol of the containerPort because it is // not illegal to listen with both TCP and UDP on the same port. proto := core.ProtocolTCP if port.Protocol != "" { proto = port.Protocol } protoEqual := func(p core.Protocol) bool { return p == proto || p == "" && proto == core.ProtocolTCP } if port.TargetPort.Type == intstr.String { portName := port.TargetPort.StrVal for ci := range cns { cn := &cns[ci] for pi := range cn.Ports { p := &cn.Ports[pi] if p.Name == portName && protoEqual(p.Protocol) { return cn, pi } } } } else { portNum := port.TargetPort.IntVal if portNum == 0 { // The targetPort default is the value of the port field. portNum = port.Port } for ci := range cns { cn := &cns[ci] for pi := range cn.Ports { p := &cn.Ports[pi] if p.ContainerPort == portNum && protoEqual(p.Protocol) { return cn, pi } } } // As a last resort, also consider containers that don't expose their ports at all. Those // containers match all ports because it's unknown what they might be listening to. for ci := range cns { cn := &cns[ci] if len(cn.Ports) == 0 { return cn, -1 } } } return nil, 0 } // IsPodRunning returns true if at least one container has state Running and a non-zero StartedAt. func IsPodRunning(pod *core.Pod) bool { for _, cn := range pod.Status.ContainerStatuses { if r := cn.State.Running; r != nil && !r.StartedAt.IsZero() { // At least one container is running. return true } } return false } // AgentContainer returns the pod's traffic-agent container, or nil if the pod doesn't have a traffic-agent. func AgentContainer(pod *core.Pod) *core.Container { return containerByName(agentconfig.ContainerName, pod.Spec.Containers) } // InitContainer returns the pod's tel-agent-init init-container, or nil if the pod doesn't have a tel-agent-init. func InitContainer(pod *core.Pod) *core.Container { return containerByName(agentconfig.InitContainerName, pod.Spec.InitContainers) } func containerByName(name string, cns []core.Container) *core.Container { for i := range cns { cn := &cns[i] if cn.Name == name { return cn } } return nil } ================================================ FILE: pkg/agentmap/generator.go ================================================ package agentmap import ( "context" "fmt" "log/slog" "slices" "sort" "strconv" "strings" "time" core "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/util/intstr" "github.com/telepresenceio/telepresence/v2/pkg/agentconfig" "github.com/telepresenceio/telepresence/v2/pkg/annotation" "github.com/telepresenceio/telepresence/v2/pkg/k8sapi" "github.com/telepresenceio/telepresence/v2/pkg/types" ) var TrafficManagerSelector = labels.SelectorFromSet(map[string]string{ //nolint:gochecknoglobals // constant "app": agentconfig.ManagerAppName, "telepresence": "manager", }) type GeneratorConfig struct { ManagerPort uint16 AgentPort uint16 APIPort uint16 QualifiedAgentImage string ManagerNamespace string LogLevel slog.Level InitResources *core.ResourceRequirements Resources *core.ResourceRequirements PullPolicy string PullSecrets []core.LocalObjectReference SecurityContext *core.SecurityContext InitSecurityContext *core.SecurityContext MountPolicies types.MountPolicies ClientConnectionTTL time.Duration WatchRetryInterval time.Duration EnableH2cProbing bool EnableMetrics bool } func portsFromContainerPortsAnnotation(ctx context.Context, wl k8sapi.Workload) (ports []types.PortIdentifier, err error) { pod := wl.GetPodTemplate() cpa := annotation.GetAnnotation(ctx, pod.GetAnnotations(), annotation.InjectContainerPorts, annotation.LegacyInjectContainerPorts) switch cpa { case "": return nil, nil case "all": cns := pod.Spec.Containers for i := range cns { for _, pn := range cns[i].Ports { pi := pn.Name if pi == "" { pi = strconv.Itoa(int(pn.ContainerPort)) } if pn.Protocol != core.ProtocolTCP { pi += "/" + string(pn.Protocol) } ports = append(ports, types.PortIdentifier(pi)) } } default: ports, err = portsFromAnnotationValue(wl, annotation.InjectContainerPorts, cpa) } return ports, err } func portsFromAnnotationValue(wl k8sapi.Workload, annotation, value string) (ports []types.PortIdentifier, err error) { cps := strings.Split(value, ",") ports = make([]types.PortIdentifier, len(cps)) for i, cp := range cps { pi := types.PortIdentifier(cp) if err = pi.Validate(); err != nil { return nil, fmt.Errorf("unable to parse annotation %s of %s: %w", annotation, wl, err) } ports[i] = pi } return ports, nil } func (cfg *GeneratorConfig) Generate( ctx context.Context, wl k8sapi.Workload, existingConfig *agentconfig.Sidecar, ) (*agentconfig.Sidecar, error) { if TrafficManagerSelector.Matches(labels.Set(wl.GetLabels())) { return nil, fmt.Errorf("%s is the Telepresence Traffic Manager. It can not have a traffic-agent", wl) } pod := wl.GetPodTemplate() pod.Namespace = wl.GetNamespace() cns := pod.Spec.Containers for i := range cns { cn := &cns[i] if cn.Name == agentconfig.ContainerName { continue } ports := cn.Ports for pi := range ports { if ports[pi].ContainerPort == int32(cfg.AgentPort) { return nil, fmt.Errorf( "the %s.%s pod container %s is exposing the same port (%d) as the %s sidecar", pod.Name, pod.Namespace, cn.Name, cfg.AgentPort, agentconfig.ContainerName) } } } ann := annotation.GetAnnotation(ctx, pod.Annotations, annotation.InjectServiceName, annotation.LegacyInjectServiceName) svcs, err := FindServicesForPod(ctx, pod, ann) if err != nil { return nil, err } pns := make(map[int32]uint16) agentPortNumberFunc := func(cnPort int32) uint16 { if p, ok := pns[cnPort]; ok { // Port already mapped. Reuse that mapping return p } p := cfg.AgentPort + uint16(len(pns)) pns[cnPort] = p return p } var ports []types.PortIdentifier ann = annotation.GetAnnotation(ctx, pod.Annotations, annotation.InjectServicePorts, annotation.LegacyInjectServicePort) if ann != "" { ports, err = portsFromAnnotationValue(wl, annotation.InjectServicePorts, ann) } if err != nil { return nil, err } cfg.MountPolicies, err = cfg.MountPolicies.AddAnnotations(ctx, pod.Annotations) if err != nil { return nil, err } var ccs []*agentconfig.Container for _, svc := range svcs { svcImpl, _ := k8sapi.ServiceImpl(svc) ccs = cfg.appendAgentContainerConfigs(svcImpl, pod, ports, agentPortNumberFunc, ccs, existingConfig) } ports, err = portsFromContainerPortsAnnotation(ctx, wl) if err != nil { return nil, err } if len(ports) > 0 { if ccs, err = cfg.appendServiceLessAgentContainerConfigs(pod, ports, agentPortNumberFunc, ccs, existingConfig); err != nil { return nil, err } } // Append other containers even though they aren't directly interceptable. They might be fronted by a // dispatching container that is, or they might be candidates for `ingest`. for i := range cns { cn := &cns[i] if cn.Name == agentconfig.ContainerName { continue } if !slices.ContainsFunc(ccs, func(cc *agentconfig.Container) bool { return cc.Name == cn.Name }) { ccs = append(ccs, cfg.newContainerConfig(cn, len(ccs), nil, containerReplacePolicy(existingConfig, cn))) } } return &agentconfig.Sidecar{ AgentImage: cfg.QualifiedAgentImage, AgentName: wl.GetName(), LogLevel: cfg.LogLevel, Namespace: wl.GetNamespace(), WorkloadName: wl.GetName(), WorkloadKind: wl.GetKind(), ManagerHost: agentconfig.ManagerAppName + "." + cfg.ManagerNamespace, ManagerPort: cfg.ManagerPort, APIPort: cfg.APIPort, ClientConnectionTTL: cfg.ClientConnectionTTL, MountPolicies: cfg.MountPolicies, Containers: ccs, InitResources: cfg.InitResources, Resources: cfg.Resources, PullPolicy: cfg.PullPolicy, PullSecrets: cfg.PullSecrets, SecurityContext: cfg.SecurityContext, InitSecurityContext: cfg.InitSecurityContext, EnableH2cProbing: cfg.EnableH2cProbing, EnableMetrics: cfg.EnableMetrics, WatchRetryInterval: cfg.WatchRetryInterval, }, nil } func (cfg *GeneratorConfig) appendAgentContainerConfigs( svc *core.Service, pod *core.PodTemplateSpec, portAnnotations []types.PortIdentifier, agentPortNumberFunc func(int32) uint16, ccs []*agentconfig.Container, existingConfig *agentconfig.Sidecar, ) []*agentconfig.Container { ports := filterServicePorts(svc, portAnnotations) nextSvcPort: for _, port := range ports { cn, i := findContainerMatchingPort(&port, pod.Spec.Containers) if cn == nil || cn.Name == agentconfig.ContainerName { continue } var appPort core.ContainerPort if i < 0 { // Can only happen if the service port is numeric, so it's safe to use TargetPort.IntVal here appPort = core.ContainerPort{ Protocol: port.Protocol, ContainerPort: port.TargetPort.IntVal, } } else { appPort = cn.Ports[i] } ic := &agentconfig.Intercept{ ServiceName: svc.Name, ServiceUID: svc.UID, ServicePortName: port.Name, ServicePort: uint16(port.Port), TargetPortNumeric: port.TargetPort.Type == intstr.Int, Protocol: types.FromK8sProtocol(port.Protocol), AgentPort: agentPortNumberFunc(appPort.ContainerPort), ContainerPortName: appPort.Name, ContainerPort: uint16(appPort.ContainerPort), } if port.AppProtocol != nil { ic.AppProtocol = *port.AppProtocol } // The container might already have intercepts declared for _, cc := range ccs { if cc.Name == cn.Name { cc.Intercepts = append(cc.Intercepts, ic) continue nextSvcPort } } ccs = append(ccs, cfg.newContainerConfig(cn, len(ccs), []*agentconfig.Intercept{ic}, containerReplacePolicy(existingConfig, cn))) } return ccs } func (cfg *GeneratorConfig) newContainerConfig(cn *core.Container, index int, ics []*agentconfig.Intercept, rp agentconfig.ReplacePolicy) *agentconfig.Container { // Create the concrete MountPolicies that map mount path to policy var mps types.MountPolicies for i := range cn.VolumeMounts { vm := &cn.VolumeMounts[i] path := vm.MountPath vp := cfg.MountPolicies.Get(vm.Name, path) if vp != types.MountPolicyIgnore { if mps == nil { mps = make(types.MountPolicies, len(cn.VolumeMounts)) } mps[path] = vp } } // Legacy mounts property must list all remote mounts, no more and no less var mounts []string if len(mps) > 0 { mounts = make([]string, 0, len(mps)) for key, mp := range mps { if mp == types.MountPolicyRemote || mp == types.MountPolicyRemoteReadOnly { mounts = append(mounts, key) } } } sort.Strings(mounts) return &agentconfig.Container{ Name: cn.Name, EnvPrefix: CapsBase26(uint64(index)) + "_", MountPoint: agentconfig.MountPrefixApp + "/" + cn.Name, MountPaths: mounts, Mounts: mps, Intercepts: ics, Replace: rp, } } func findContainerPort(cns []core.Container, p types.PortIdentifier) (*core.Container, *core.ContainerPort) { proto, name, num := p.ProtoAndNameOrNumber() for n := range cns { cn := &cns[n] if cn.Name != agentconfig.ContainerName { for i := range cn.Ports { appPort := &cn.Ports[i] if (name != "" && name == appPort.Name || num == uint16(appPort.ContainerPort)) && (proto.String() == string(appPort.Protocol) || proto == types.ProtoTCP && appPort.Protocol == "") { return cn, appPort } } } } return nil, nil } func (cfg *GeneratorConfig) appendServiceLessAgentContainerConfigs( pod *core.PodTemplateSpec, portAnnotations []types.PortIdentifier, agentPortNumberFunc func(int32) uint16, ccs []*agentconfig.Container, existingConfig *agentconfig.Sidecar, ) ([]*agentconfig.Container, error) { cns := pod.Spec.Containers anonNameIndex := uint64(0) nextContainerPort: for _, p := range portAnnotations { cn, appPort := findContainerPort(cns, p) if appPort == nil { // The port is not explicitly declared as a container port, so if possible, we synthesize one. proto, name, num := p.ProtoAndNameOrNumber() if name != "" { // We can only synthesize given a numeric port. return nil, fmt.Errorf("found no container port that matches port annotation %s", p) } appPort = &core.ContainerPort{ Name: fmt.Sprintf("port-%s", Base26(anonNameIndex)), ContainerPort: int32(num), Protocol: core.Protocol(proto.String()), } anonNameIndex++ } ic := &agentconfig.Intercept{ TargetPortNumeric: true, Protocol: types.FromK8sProtocol(appPort.Protocol), AgentPort: agentPortNumberFunc(appPort.ContainerPort), ContainerPortName: appPort.Name, ContainerPort: uint16(appPort.ContainerPort), } // The container might already have intercepts declared for _, cc := range ccs { if cc.Name == cn.Name { // Don't add service-less intercept if an intercept with a service is present cnFound := false for _, eic := range cc.Intercepts { if eic.ContainerPort == ic.ContainerPort { cnFound = true break } } if !cnFound { cc.Intercepts = append(cc.Intercepts, ic) } continue nextContainerPort } } ccs = append(ccs, cfg.newContainerConfig(cn, len(ccs), []*agentconfig.Intercept{ic}, containerReplacePolicy(existingConfig, cn))) } return ccs, nil } func containerReplacePolicy(existingConfig *agentconfig.Sidecar, cn *core.Container) agentconfig.ReplacePolicy { var replaceContainer agentconfig.ReplacePolicy if existingConfig != nil { for _, cc := range existingConfig.Containers { if cc.Name == cn.Name { replaceContainer = cc.Replace break } } } return replaceContainer } // filterServicePorts iterates through a list of ports in a service and // only returns the ports that match the given nameOrNumber. All ports will // be returned if nameOrNumber is equal to the empty string. func filterServicePorts(svc *core.Service, portAnnotations []types.PortIdentifier) []core.ServicePort { ports := svc.Spec.Ports if len(portAnnotations) == 0 { return ports } svcPorts := make([]core.ServicePort, 0) for _, pi := range portAnnotations { proto, name, num := pi.ProtoAndNameOrNumber() if name != "" { for _, port := range ports { if port.Name == name { svcPorts = append(svcPorts, port) } } } else { for _, port := range ports { pn := int32(0) if port.TargetPort.Type == intstr.Int { pn = port.TargetPort.IntVal } if pn == 0 { pn = port.Port } if uint16(pn) == num && types.FromK8sProtocol(port.Protocol) == proto { svcPorts = append(svcPorts, port) } } } } return svcPorts } ================================================ FILE: pkg/annotation/annotation.go ================================================ package annotation import ( "context" "github.com/telepresenceio/clog" ) const ( DomainPrefix = "telepresence.io/" Config = DomainPrefix + "agent-config" InjectContainerPorts = DomainPrefix + "inject-container-ports" InjectIgnoreVolumeMounts = DomainPrefix + "inject-ignore-volume-mounts" InjectServiceName = DomainPrefix + "inject-service-name" InjectServicePorts = DomainPrefix + "inject-service-ports" InjectTrafficAgent = DomainPrefix + "inject-traffic-agent" ManuallyInjected = DomainPrefix + "manually-injected" ReplacedContainerPrefix = DomainPrefix + "replaced-container." RestartedAt = DomainPrefix + "restartedAt" VolumeMountPolicies = DomainPrefix + "mount-policies" DownstreamTLSSecret = DomainPrefix + "downstream-tls-secret" DownstreamCertificatePath = DomainPrefix + "downstream-tls-path" UpstreamTLSSecret = DomainPrefix + "upstream-tls-secret" UpstreamCertificatePath = DomainPrefix + "upstream-tls-path" UpstreamInsecureSkipVerify = DomainPrefix + "upstream-insecure-skip-verify" UpstreamProbeTimeout = DomainPrefix + "upstream-probe-timeout" LegacyDomainPrefix = "telepresence.getambassador.io/" LegacyInjectContainerPorts = LegacyDomainPrefix + "inject-container-ports" LegacyInjectIgnoreVolumeMounts = LegacyDomainPrefix + "inject-ignore-volume-mounts" LegacyInjectServiceName = LegacyDomainPrefix + "inject-service-name" LegacyInjectServicePort = LegacyDomainPrefix + "inject-service-port" LegacyInjectTrafficAgent = LegacyDomainPrefix + "inject-traffic-agent" LegacyManuallyInjected = LegacyDomainPrefix + "manually-injected" ) func GetAnnotation(ctx context.Context, annotations map[string]string, key, deprecatedKey string) string { value, ok := annotations[key] if !ok { value, ok = annotations[deprecatedKey] if ok { clog.Warnf(ctx, "Annotation %q is deprecated. Use %q instead", key, value) } } return value } func ReplaceAnnotationKey(cn string) string { return ReplacedContainerPrefix + cn } ================================================ FILE: pkg/authenticator/authenticator.go ================================================ package authenticator import ( "context" "fmt" clientcmd_api "k8s.io/client-go/tools/clientcmd/api" "github.com/telepresenceio/telepresence/v2/pkg/k8sapi" ) func NewService( clientConfigProvider k8sapi.ClientConfigProvider, ) *Service { return &Service{ clientConfigProvider: clientConfigProvider, } } type ExecCredentialsResolver interface { Resolve( ctx context.Context, execConfig *clientcmd_api.ExecConfig, ) ([]byte, error) } type Service struct { clientConfigProvider k8sapi.ClientConfigProvider } func (a Service) GetExecCredentials(ctx context.Context, contextName string) ([]byte, error) { execConfig, err := a.getExecConfigFromContext(contextName) if err != nil { return nil, fmt.Errorf("failed to get exec config from context %s, %w", contextName, err) } rawExecCredentials, err := ResolveExecConfig(ctx, execConfig) if err != nil { return nil, fmt.Errorf("failed to resolve credentials: %w", err) } return rawExecCredentials, nil } func (a Service) getExecConfigFromContext(contextName string) (*clientcmd_api.ExecConfig, error) { cc, err := a.clientConfigProvider.ClientConfig() if err != nil { return nil, fmt.Errorf("failed to get kubeconfig provider: %w", err) } rawConfig, err := cc.RawConfig() if err != nil { return nil, fmt.Errorf("failed to get kubeconfig: %w", err) } kubeContext, ok := rawConfig.Contexts[contextName] if !ok { return nil, fmt.Errorf("kube context %s doesn't exist", contextName) } authInfo, ok := rawConfig.AuthInfos[kubeContext.AuthInfo] if !ok { return nil, fmt.Errorf("auth info %s doesn't exist", kubeContext.AuthInfo) } if authInfo.Exec == nil { return nil, fmt.Errorf("auth info %s isn't of type exec", kubeContext.AuthInfo) } return &clientcmd_api.ExecConfig{ Command: authInfo.Exec.Command, Args: authInfo.Exec.Args, Env: authInfo.Exec.Env, }, nil } ================================================ FILE: pkg/authenticator/config.go ================================================ package authenticator import "k8s.io/client-go/tools/clientcmd" func LoadKubeConfig(kubeConfig string) clientcmd.ClientConfig { loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() loadingRules.ExplicitPath = kubeConfig return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{}) } ================================================ FILE: pkg/authenticator/exec.go ================================================ package authenticator import ( "bytes" "context" "fmt" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" "github.com/telepresenceio/telepresence/v2/pkg/dos" "github.com/telepresenceio/telepresence/v2/pkg/proc" ) func ResolveExecConfig(ctx context.Context, execConfig *clientcmdapi.ExecConfig) ([]byte, error) { var buf bytes.Buffer cmd := proc.CommandContext(ctx, execConfig.Command, execConfig.Args...) cmd.Stdout = &buf cmd.Stderr = dos.Stderr(ctx) cmd.Env = dos.Environ(ctx) if len(execConfig.Env) > 0 { em := dos.FromEnvPairs(cmd.Env) for _, ev := range execConfig.Env { em[ev.Name] = ev.Value } cmd.Env = em.Environ() } if err := cmd.Run(); err != nil { return nil, fmt.Errorf("failed to run host command: %w", err) } return buf.Bytes(), nil } ================================================ FILE: pkg/authenticator/exec_test.go ================================================ package authenticator import ( "context" "testing" "github.com/stretchr/testify/assert" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) func TestExecCredentialsNoLocalEnv(t *testing.T) { t.Setenv("GLOBAL_ENV", "global-val") config := &clientcmdapi.ExecConfig{ Command: "sh", Args: []string{"-c", "echo $GLOBAL_ENV/$LOCAL_ENV"}, } result, err := ResolveExecConfig(context.Background(), config) assert.NoError(t, err) assert.Equal(t, string(result), "global-val/\n") } func TestExecCredentialsYesLocalEnv(t *testing.T) { t.Setenv("GLOBAL_ENV", "global-val") config := &clientcmdapi.ExecConfig{ Command: "sh", Args: []string{"-c", "echo $GLOBAL_ENV/$LOCAL_ENV"}, Env: []clientcmdapi.ExecEnvVar{{Name: "LOCAL_ENV", Value: "local-val"}}, } result, err := ResolveExecConfig(context.Background(), config) assert.NoError(t, err) assert.Equal(t, string(result), "global-val/local-val\n") } ================================================ FILE: pkg/authenticator/grpc/authenticator.go ================================================ package grpc import ( "context" "fmt" "google.golang.org/grpc" "github.com/telepresenceio/clog" rpc "github.com/telepresenceio/telepresence/rpc/v2/authenticator" "github.com/telepresenceio/telepresence/v2/pkg/authenticator" "github.com/telepresenceio/telepresence/v2/pkg/k8sapi" ) func RegisterAuthenticatorServer(srv *grpc.Server, clientConfigProvider k8sapi.ClientConfigProvider) { rpc.RegisterAuthenticatorServer(srv, &AuthenticatorServer{ authenticator: authenticator.NewService(clientConfigProvider), }) } type Authenticator interface { GetExecCredentials(ctx context.Context, contextName string) ([]byte, error) } type AuthenticatorServer struct { rpc.UnsafeAuthenticatorServer authenticator Authenticator } // GetContextExecCredentials returns credentials for a particular Kubernetes context on the host machine. func (h *AuthenticatorServer) GetContextExecCredentials(ctx context.Context, request *rpc.GetContextExecCredentialsRequest) (*rpc.GetContextExecCredentialsResponse, error) { clog.Debugf(ctx, "GetContextExecCredentials(%s)", request.ContextName) rawExecCredentials, err := h.authenticator.GetExecCredentials(ctx, request.ContextName) if err != nil { return nil, fmt.Errorf("failed to resolve exec credentils: %w", err) } return &rpc.GetContextExecCredentialsResponse{ RawCredentials: rawExecCredentials, }, nil } ================================================ FILE: pkg/authenticator/patcher/patcher.go ================================================ package patcher import ( "context" "fmt" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/global" ) const ( kubeConfigStubSubCommands = "kubeauth" ) // AddressProvider is a function that returns the path to the telepresence executable and an address to a service that // implements the Authenticator gRPC. // // The function will typically start the gRPC service, and the service is therefore given // a list of files that it must listen to in order to reliably resolve requests. It is // also passed a pointer to the minified config that will be stored in a file so that it // has a chance to modify it. type ( AddressProvider func(configFiles []string) (executable, addr, configFile string, err error) Patcher func(*clientcmdapi.Config) error ) // CreateExternalKubeConfig will load the current kubeconfig and minimize it so that it just contains the current // context. Exec configs in that context are replaced by a stub binary that calls the kubeauth service. The kubeauth // service, which runs on the host with the user's credentials, will then use the original Exec config. // The minified config is stored in the /kube directory and returned. func CreateExternalKubeConfig( ctx context.Context, loader clientcmd.ClientConfig, kubeContext string, authAddressFunc AddressProvider, patcher Patcher, ) ([]byte, error) { ns, _, err := loader.Namespace() if err != nil { return nil, err } configFiles := loader.ConfigAccess().GetLoadingPrecedence() clog.Debugf(ctx, "host kubeconfig = %v", configFiles) origConfig, err := loader.RawConfig() if err != nil { return nil, err } var config clientcmdapi.Config origConfig.DeepCopyInto(&config) // Minify the config so that we only deal with the current context. if kubeContext != "" { config.CurrentContext = kubeContext } if err = clientcmdapi.MinifyConfig(&config); err != nil { return nil, err } clog.Debugf(ctx, "context = %q, namespace %q", config.CurrentContext, ns) // Minify guarantees that the CurrentContext is set, but not that it has a cluster cc := config.Contexts[config.CurrentContext] if cc.Cluster == "" { return nil, fmt.Errorf("current context %q has no cluster", config.CurrentContext) } if needsStubbedExec(&config) { executable, addr, configFile, err := authAddressFunc(configFiles) if err != nil { return nil, err } if err = replaceAuthExecWithStub(&config, executable, addr, configFile); err != nil { return nil, err } } // Ensure that all certs are embedded instead of reachable using a path if err = clientcmdapi.FlattenConfig(&config); err != nil { return nil, err } if patcher != nil { if err = patcher(&config); err != nil { return nil, err } } return clientcmd.Write(config) } // replaceAuthExecWithStub goes through the kubeconfig and replaces all uses of the Exec auth method by // an invocation of the stub binary. func replaceAuthExecWithStub(rawConfig *clientcmdapi.Config, executable, address, configFile string) error { for contextName, kubeContext := range rawConfig.Contexts { // Find related Auth. authInfo, ok := rawConfig.AuthInfos[kubeContext.AuthInfo] if !ok { return fmt.Errorf("auth info %s not found for context %s", kubeContext.AuthInfo, contextName) } // If it isn't an exec mode context, just return the default host kubeconfig. if authInfo.Exec == nil { continue } // Patch exec. authInfo.Exec = &clientcmdapi.ExecConfig{ InteractiveMode: clientcmdapi.NeverExecInteractiveMode, APIVersion: authInfo.Exec.APIVersion, Command: executable, Args: []string{kubeConfigStubSubCommands, "--" + global.FlagConfig, configFile, contextName, address}, } } return nil } // needsStubbedExec returns true if the config contains at least one user with an Exec type AuthInfo. func needsStubbedExec(rawConfig *clientcmdapi.Config) bool { for _, kubeContext := range rawConfig.Contexts { if authInfo, ok := rawConfig.AuthInfos[kubeContext.AuthInfo]; ok && authInfo.Exec != nil { return true } } return false } ================================================ FILE: pkg/cache/client.go ================================================ package cache import ( "github.com/puzpuzpuz/xsync/v4" ) type Entry[K comparable, V any] interface { Key() K Value() V } type ClientMap[K comparable, V any] struct { *xsync.Map[K, V] } func (c *ClientMap[K, V]) Watch(doneCh <-chan struct{}, deltaCh <-chan Delta[K, V], onChanges func() error) error { for { select { case <-doneCh: return nil case delta, ok := <-deltaCh: if !ok { return nil } for k, v := range delta.Upserts { c.Store(k, v) } for k := range delta.Removals { c.Delete(k) } if onChanges != nil { err := onChanges() if err != nil { return err } } } } } func NewClientMap[K comparable, V any](options ...func(config *xsync.MapConfig)) *ClientMap[K, V] { return &ClientMap[K, V]{Map: xsync.NewMap[K, V](options...)} } type Server[K comparable, V any] interface { Subscribe(done <-chan struct{}, filter func(K, V) bool) <-chan Delta[K, V] } ================================================ FILE: pkg/cache/map.go ================================================ package cache import ( "math" "sync" "sync/atomic" "time" "github.com/google/uuid" "github.com/puzpuzpuz/xsync/v4" ) type Delta[K comparable, V any] struct { Upserts map[K]V Removals map[K]V } func (delta *Delta[K, V]) Merge(other Delta[K, V]) { if len(delta.Upserts) == 0 { delta.Upserts = other.Upserts } else { for k, v := range other.Upserts { delta.Upserts[k] = v } for k := range other.Removals { delete(delta.Upserts, k) } } if len(delta.Removals) == 0 { delta.Removals = other.Removals } else { for k, v := range other.Removals { delta.Removals[k] = v } for k := range other.Upserts { delete(delta.Removals, k) } } } type subscription[K comparable, V any] struct { channel chan Delta[K, V] include func(K, V) bool initialized atomic.Bool mark atomic.Bool doneCh <-chan struct{} } type Map[K comparable, V any] struct { *xsync.Map[K, V] snapLock sync.Mutex snapshot map[K]V equal func(V, V) bool subscribers *xsync.Map[uuid.UUID, *subscription[K, V]] notifyDelay time.Duration notifier *time.Timer } // NewMap creates a new Map instance configured with the given options. func NewMap[K comparable, V any](equal func(V, V) bool, notifyDelay time.Duration, config ...func(*xsync.MapConfig)) *Map[K, V] { m := &Map[K, V]{ Map: xsync.NewMap[K, V](config...), equal: equal, subscribers: xsync.NewMap[uuid.UUID, *subscription[K, V]](), notifyDelay: notifyDelay, } m.notifier = time.AfterFunc(math.MaxInt64, m.notify) return m } // Subscribe returns a channel that will emit deltas that corresponds to modifications of the contained // values filtered by the given filter. // // The first delta is a snapshot of all values, and it is emitted immediately after the call to Subscribe(). // After that, a new Delta is emitted then whenever the map changes a value for which the filter evaluates // to true. // // The values contained in a delta will reflect actual values in the map and must be considered immutable. // Mutating them will mutate the map without the map's knowledge and hence not trigger notifications to // subscribers. // // The returned channel will be closed when the given channel is closed. func (m *Map[K, V]) Subscribe(done <-chan struct{}, includeFilter func(K, V) bool) <-chan Delta[K, V] { ch := make(chan Delta[K, V], 1) select { case <-done: close(ch) default: m.notifier.Reset(math.MaxInt64) id := uuid.New() sb := &subscription[K, V]{include: includeFilter, channel: ch, doneCh: done} sb.mark.Store(true) m.subscribers.Store(id, sb) // Fire notifier immediately to send the snapshot. m.notify() go func() { <-done m.subscribers.Delete(id) close(ch) }() } return ch } // Compute either sets the computed new value for the key or deletes the value for the key. // When the delete result of the valueFn function is set to true, the value will be deleted if it exists. // When delete is set to false, the value is updated to the newValue. The ok result indicates whether the // value was computed and stored, thus, is present in the map. The actual result contains the new value in // cases where the value was computed and stored. See the example for a few use cases. // // This call locks a hash table bucket while the compute function is executed. It means that modifications // on other entries in the bucket will be blocked until the valueFn executes. Consider this when the function // includes long-running operations. func (m *Map[K, V]) Compute(key K, f func(V, bool) (V, xsync.ComputeOp)) (V, bool) { modified := false actual, ok := m.Map.Compute(key, func(v V, loaded bool) (V, xsync.ComputeOp) { fv, op := f(v, loaded) switch op { case xsync.CancelOp: case xsync.UpdateOp: if loaded && m.equal(fv, v) { fv = v op = xsync.CancelOp } else { modified = true m.markSubscribers(key, fv) } case xsync.DeleteOp: modified = true m.markSubscribers(key, v) } return fv, op }) if modified { m.notifier.Reset(m.notifyDelay) } return actual, ok } // CompareAndSwap checks if the current value for the given key equals the oldValue, and if // so, swaps the current value for the newValue. // The swapped result reports whether the value was swapped. func (m *Map[K, V]) CompareAndSwap(key K, oldValue, newValue V) (swapped bool) { m.Compute(key, func(cur V, loaded bool) (V, xsync.ComputeOp) { if loaded && m.equal(cur, oldValue) { swapped = true return newValue, xsync.UpdateOp } return oldValue, xsync.CancelOp }) return swapped } // Delete deletes the value for a key. func (m *Map[K, V]) Delete(key K) { m.Compute(key, func(oldValue V, loaded bool) (V, xsync.ComputeOp) { if !loaded { return oldValue, xsync.CancelOp } return oldValue, xsync.DeleteOp }) } // LoadAll return a map of all entries. func (m *Map[K, V]) LoadAll() map[K]V { mr := make(map[K]V, m.Size()) m.Range(func(key K, value V) bool { mr[key] = value return true }) return mr } // LoadMatching return a map of all entries matching the given filter. func (m *Map[K, V]) LoadMatching(filter func(K, V) bool) map[K]V { mr := make(map[K]V) m.Range(func(key K, value V) bool { if filter(key, value) { mr[key] = value } return true }) return mr } // LoadAndDelete deletes the value for a key, returning the previous value if any. // The loaded result reports whether the key was present. func (m *Map[K, V]) LoadAndDelete(key K) (previous V, loaded bool) { m.Compute(key, func(oldValue V, wasLoaded bool) (V, xsync.ComputeOp) { if wasLoaded { previous = oldValue loaded = true return oldValue, xsync.DeleteOp } return oldValue, xsync.CancelOp }) return previous, loaded } // LoadAndStore stores a new value for the key and returns the existing one, if present. The loaded result is true if the // existing value was loaded, false otherwise. func (m *Map[K, V]) LoadAndStore(key K, value V) (existing V, loaded bool) { m.Compute(key, func(oldValue V, wasLoaded bool) (V, xsync.ComputeOp) { if wasLoaded { existing = oldValue loaded = true } return value, xsync.UpdateOp }) return existing, loaded } // LoadOrCompute returns the existing value for the key if present. Otherwise, it computes the value using // the provided function and returns the computed value. The loaded result is true if the value was loaded, // false if stored. // // This call locks a hash table bucket while the compute function is executed. It means that modifications // on other entries in the bucket will be blocked until the valueFn executes. Consider this when the function // includes long-running operations. func (m *Map[K, V]) LoadOrCompute(key K, valueFn func() V) (actual V, loaded bool) { actual, _ = m.Compute(key, func(oldValue V, wasLoaded bool) (V, xsync.ComputeOp) { if wasLoaded { loaded = true return oldValue, xsync.CancelOp } return valueFn(), xsync.UpdateOp }) return actual, loaded } // LoadOrStore returns the existing value for the key if present. Otherwise, it stores and returns the given value. // The loaded result is true if the value was loaded, false if stored. func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) { actual, _ = m.Compute(key, func(oldValue V, wasLoaded bool) (V, xsync.ComputeOp) { if wasLoaded { loaded = true return oldValue, xsync.CancelOp } return value, xsync.UpdateOp }) return actual, loaded } // Store stores a new value for the key. func (m *Map[K, V]) Store(key K, value V) { m.Compute(key, func(oldValue V, wasLoaded bool) (V, xsync.ComputeOp) { return value, xsync.UpdateOp }) } // markSubscribers marks all subscribers interested in the given key and value binding. func (m *Map[K, V]) markSubscribers(key K, value V) { m.subscribers.Range(func(_ uuid.UUID, sb *subscription[K, V]) bool { // Don't run the filter if the subscriber is marked already. if !sb.mark.Load() && (sb.include == nil || sb.include(key, value)) { sb.mark.Store(true) } return true }) } // notify will send a snapshot to all subscribers that have been marked. func (m *Map[K, V]) notify() { // We need to loop until all marked snapshots have been sent, because new marks may be added during sending. delta := m.makeDelta() for didSend := true; didSend; { didSend = false m.subscribers.Range(func(_ uuid.UUID, sb *subscription[K, V]) bool { if sb.mark.CompareAndSwap(true, false) { delta.send(sb) didSend = true } return true }) } } type allDelta[K comparable, V any] struct { snapshot map[K]V upserts map[K]V removals map[K]V } func filteredMap[K comparable, V any](m map[K]V, include func(K, V) bool) map[K]V { if include == nil { return m } fm := make(map[K]V) for k, v := range m { if include(k, v) { fm[k] = v } } return fm } func (ad *allDelta[K, V]) filteredDelta(initialized bool, include func(K, V) bool) Delta[K, V] { var upserts map[K]V var removals map[K]V if initialized { upserts = ad.upserts removals = ad.removals } else { upserts = ad.snapshot removals = nil } return Delta[K, V]{Upserts: filteredMap(upserts, include), Removals: filteredMap(removals, include)} } func (ad *allDelta[K, V]) send(sb *subscription[K, V]) { initialized := sb.initialized.Swap(true) fd := ad.filteredDelta(initialized, sb.include) if initialized && len(fd.Upserts) == 0 && len(fd.Removals) == 0 { return } select { case <-sb.doneCh: case prevDelta := <-sb.channel: // The previous delta was not read by the subscriber yet, so we need to merge it with the new delta // and put it back on the channel. prevDelta.Merge(fd) sb.channel <- prevDelta default: // The channel is empty, so we can just send the delta. sb.channel <- fd } } func (m *Map[K, V]) makeDelta() allDelta[K, V] { m.snapLock.Lock() previous := m.snapshot current := m.LoadAll() m.snapshot = current var upserts map[K]V for k, v := range current { if prev, ok := previous[k]; !(ok && m.equal(prev, v)) { if upserts == nil { upserts = make(map[K]V) } upserts[k] = v } } var removals map[K]V for k, v := range previous { if _, ok := current[k]; !ok { if removals == nil { removals = make(map[K]V) } removals[k] = v } } m.snapLock.Unlock() return allDelta[K, V]{ snapshot: current, upserts: upserts, removals: removals, } } ================================================ FILE: pkg/client/agentpf/clients.go ================================================ package agentpf import ( "context" "fmt" "net" "net/netip" "sync" "sync/atomic" "time" "github.com/puzpuzpuz/xsync/v4" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "k8s.io/apimachinery/pkg/types" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/rpc/v2/agent" "github.com/telepresenceio/telepresence/rpc/v2/manager" tpClient "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/client/k8s" "github.com/telepresenceio/telepresence/v2/pkg/grpc/watcher" "github.com/telepresenceio/telepresence/v2/pkg/maps" "github.com/telepresenceio/telepresence/v2/pkg/tunnel" ) type client struct { // Mutex protects the following fields (the rest is immutable) // info.intercepted // cli // cancelClient // cancelDialWatch // cli and cancelClient are both safe to use without a mutex once the ready channel is closed. *k8s.Cluster sync.RWMutex cli agent.AgentClient session *manager.SessionInfo info *manager.AgentPodInfo remove func() cancelClient context.CancelFunc cancelDialWatch context.CancelFunc connectErr error tunnelCount int32 lastActive int64 } const dormantLingerTime = 5 * time.Second func (ac *client) String() string { if ac == nil { return "" } ai := ac.info return fmt.Sprintf("%s(%s), port %d", ai.PodName, net.IP(ai.PodIp), ai.ApiPort) } func (ac *client) Tunnel(ctx context.Context, opts ...grpc.CallOption) (tunnel.Client, error) { cli, err := ac.ensureConnect(ctx) if err != nil { return nil, err } clog.Tracef(ctx, "%s(%s) creating Tunnel over gRPC", ac, net.IP(ac.info.PodIp)) tc, err := cli.Tunnel(ctx, opts...) if err != nil { clog.Tracef(ctx, "%s(%s) failed to create Tunnel over gRPC: %v", ac, net.IP(ac.info.PodIp), err) return nil, err } atomic.AddInt32(&ac.tunnelCount, 1) clog.Tracef(ctx, "%s(%s) have %d active tunnels", ac, net.IP(ac.info.PodIp), atomic.LoadInt32(&ac.tunnelCount)) go func() { <-ctx.Done() tc := atomic.LoadInt32(&ac.tunnelCount) if tc > 0 && atomic.CompareAndSwapInt32(&ac.tunnelCount, tc, tc-1) { clog.Tracef(ctx, "%s(%s) have %d active tunnels", ac, net.IP(ac.info.PodIp), tc-1) } }() atomic.StoreInt64(&ac.lastActive, time.Now().UnixNano()) return tc, nil } func (ac *client) ensureConnect(ctx context.Context) (agent.AgentClient, error) { ac.Lock() defer ac.Unlock() return ac.ensureConnectLocked(ctx) } func (ac *client) ensureConnectLocked(ctx context.Context) (agent.AgentClient, error) { if ac.connectErr != nil { return nil, ac.connectErr } if ac.cli == nil { dialCtx, dialCancel := context.WithTimeout(ctx, 5*time.Second) defer dialCancel() ai := ac.info conn, cli, _, err := ac.ConnectToAgent(dialCtx, ai.PodName, uint16(ai.ApiPort), types.UID(ai.PodId)) if err != nil { ac.connectErr = err // There's a risk for deadlock here, because of the Range iteration of the map that performs cancel. This cancel will block // because we're holding the lock now, and since the Range iteration holds a lock for the entry that we're about to delete, // that delete will block. So we let a potential cancel call continue by unlocking before we delete. ac.Unlock() ac.remove() ac.Lock() // Must of course lock again to prevent panic by the pending unlock. return nil, err } ac.cli = cli ac.cancelClient = func() { // Need to run this in a separate thread to avoid deadlock. go func() { conn.Close() ac.Lock() atomic.StoreInt32(&ac.tunnelCount, 0) ac.cancelClient = nil ac.cli = nil ac.Unlock() }() } } if ac.info.Intercepted { err := ac.startDialWatcherLocked() if err != nil { return nil, err } } atomic.StoreInt64(&ac.lastActive, time.Now().UnixNano()) return ac.cli, nil } func (ac *client) idleTime() time.Duration { return time.Duration(time.Now().UnixNano() - atomic.LoadInt64(&ac.lastActive)) } func (ac *client) dormant() bool { if atomic.LoadInt32(&ac.tunnelCount) > 0 || ac.idleTime() < dormantLingerTime { return false } ac.RLock() dormant := ac.cli != nil && !ac.info.Intercepted ac.RUnlock() return dormant } func (ac *client) connected() bool { ac.RLock() ok := ac.cli != nil ac.RUnlock() return ok } func (ac *client) intercepted() bool { ac.RLock() ret := ac.info.Intercepted ac.RUnlock() return ret } func (ac *client) cancel() bool { ac.RLock() cc := ac.cancelClient cdw := ac.cancelDialWatch ac.RUnlock() didCancel := false if cdw != nil { didCancel = true cdw() } if cc != nil { didCancel = true cc() } return didCancel } func (ac *client) refresh(ai *manager.AgentPodInfo) { var cdw context.CancelFunc defer func() { if cdw != nil { cdw() } }() ac.Lock() defer ac.Unlock() oldStatus := ac.info.Intercepted ac.info = ai if ai.Intercepted == oldStatus { return } if ai.Intercepted { clog.Debugf(ac, "Agent %s(%s) changed to intercepted", ai.PodName, net.IP(ai.PodIp)) if _, err := ac.ensureConnectLocked(ac); err != nil { clog.Errorf(ac, "failed to start client watcher for %s(%s): %v", ai.PodName, net.IP(ai.PodIp), err) } } else { // This agent is no longer intercepting. Stop the dial watcher clog.Debugf(ac, "Agent %s(%s) changed to not intercepted", ai.PodName, net.IP(ai.PodIp)) cdw = ac.cancelDialWatch } } func (ac *client) startDialWatcherLocked() error { if ac.cancelDialWatch != nil { // Already started return nil } ctx, cancel := context.WithCancel(ac) // Create the dial watcher clog.Debugf(ctx, "watching dials from agent pod %s", ac) dialStream, err := ac.cli.WatchDial(ctx, ac.session) if err != nil { cancel() return err } ac.cancelDialWatch = func() { ac.Lock() ac.info.Intercepted = false ac.cancelDialWatch = nil ac.Unlock() cancel() } go func() { err := tunnel.DialWaitLoop(ctx, tunnel.AgentToClient, tunnel.AgentProvider(ac.cli), dialStream, tunnel.SessionID(ac.session.SessionId)) if err != nil { // The traffic-agent closed the dial wait loop, which means that it's terminating. clog.Error(ctx, err) } ai := ac.info clog.Debugf(ctx, "DialWaitLoop ended for %s.%s", ai.PodName, ai.Namespace) ac.RLock() dwCancel := ac.cancelDialWatch ac.RUnlock() if dwCancel != nil { dwCancel() } }() return nil } type Clients interface { GetRandomAgent(context.Context) agent.AgentClient GetClient(netip.Addr) tunnel.Provider WatchAgentPods(rmc manager.ManagerClient) error WaitForIP(ctx context.Context, timeout time.Duration, ip netip.Addr) error WaitForWorkload(timeout time.Duration, name string) error GetWorkloadClient(workload string) (ag tunnel.Provider) SetProxyVia(workload string) } type clients struct { *k8s.Cluster session *manager.SessionInfo clients *xsync.Map[string, *client] ipWaiters *xsync.Map[netip.Addr, chan struct{}] wlWaiters *xsync.Map[string, chan struct{}] proxyVias *xsync.Map[string, struct{}] disabled atomic.Bool } func NewClients(cl *k8s.Cluster, session *manager.SessionInfo) Clients { return &clients{ Cluster: cl, session: session, clients: xsync.NewMap[string, *client](), ipWaiters: xsync.NewMap[netip.Addr, chan struct{}](), wlWaiters: xsync.NewMap[string, chan struct{}](), proxyVias: xsync.NewMap[string, struct{}](), } } // GetClient returns tunnel.Provider that opens a tunnel to a known traffic-agent. // The traffic-agent is chosen using the following rules in the order mentioned: // // 1. agent has a pod_ip that matches the given ip // 2. agent is currently intercepted by this client // 3. any agent // // The function returns nil when there are no agents in the connected namespace. func (s *clients) GetClient(ip netip.Addr) (pvd tunnel.Provider) { if s.disabled.Load() { return nil } var primary, secondary, ternary tunnel.Provider s.clients.Range(func(_ string, c *client) bool { podIP, ok := netip.AddrFromSlice(c.info.PodIp) switch { case ok && ip == podIP: primary = c case c.intercepted(): secondary = c default: ternary = c } return primary == nil }) switch { case primary != nil: pvd = primary case secondary != nil: pvd = secondary default: pvd = ternary } return pvd } // GetRandomAgent returns an active agent.AgentClient and ensures that it is kept alive // for at least 5 seconds. // // The function returns nil when there are no active agents. func (s *clients) GetRandomAgent(ctx context.Context) (aa agent.AgentClient) { var connected, waiting, other *client s.clients.Range(func(_ string, ac *client) bool { if ac.connected() { connected = ac return false } if s.isProxyVIA(ac.info) || s.hasWaiterFor(ac.info) { waiting = ac } else { other = ac } return true }) var err error switch { case connected != nil: connected.Lock() connected.lastActive = time.Now().UnixNano() aa = connected.cli connected.Unlock() case waiting != nil: aa, err = waiting.ensureConnect(ctx) case other != nil: aa, err = other.ensureConnect(ctx) } if err != nil { clog.Warn(s, err) } return aa } // GetWorkloadClient returns tunnel.Provider that opens a tunnel to a traffic-agent that // belongs to a pod created for the given workload. // // The function returns nil when there are no agents for the given workload in the connected namespace. func (s *clients) GetWorkloadClient(workload string) (pvd tunnel.Provider) { s.clients.Range(func(_ string, ac *client) bool { if ac.info.WorkloadName == workload { pvd = ac return false } return true }) return pvd } func (s *clients) SetProxyVia(workload string) { s.proxyVias.Store(workload, struct{}{}) } func (s *clients) isProxyVIA(info *manager.AgentPodInfo) bool { _, isPV := s.proxyVias.Load(info.WorkloadName) return isPV } func (s *clients) hasWaiterFor(info *manager.AgentPodInfo) bool { if podIP, ok := netip.AddrFromSlice(info.PodIp); ok { if _, isW := s.ipWaiters.Load(podIP); isW { return true } } if _, isW := s.wlWaiters.Load(info.WorkloadName); isW { return true } return false } func (s *clients) WatchAgentPods(rmc manager.ManagerClient) error { defer func() { activeCount := 0 s.clients.Range(func(_ string, ac *client) bool { if ac.cancel() { activeCount++ } return true }) clog.Debugf(s, "WatchAgentPods ending with %d clients still active", activeCount) s.disabled.Store(true) }() snapMap := make(map[string]*manager.AgentPodInfo) err := watcher.WatchWithRetry(s, "WatchAgentPodsDelta", tpClient.GetConfig(s).Grpc().WatchRetryInterval, func(ctx context.Context) (grpc.ServerStreamingClient[manager.AgentPodInfoDelta], error) { clog.Debugf(ctx, "WatchAgentPodsDelta starting") return rmc.WatchAgentPodsDelta(ctx, s.session) }, func(delta *manager.AgentPodInfoDelta) error { clog.Debugf(s, "WatchAgentPodsDelta received %d upserts, %d removals", len(delta.Upserts), len(delta.Removals)) maps.DeltaUpdate(snapMap, delta.Upserts, delta.Removals) return s.updateClients(maps.Values(snapMap)) }, func() error { clear(snapMap) return nil }) if err == nil || status.Code(err) != codes.Unimplemented { return err } // Older traffic-manager. Fall back to watching all agents. clog.Warnf(s, "WatchAgentPodsDelta is not implemented by the traffic-manager, falling back to WatchAgentPods and full snapshots") return watcher.WatchWithRetry(s, "WatchAgentPods", tpClient.GetConfig(s).Grpc().WatchRetryInterval, func(ctx context.Context) (grpc.ServerStreamingClient[manager.AgentPodInfoSnapshot], error) { clog.Debugf(ctx, "No delta support in traffic-manager, starting WatchAgentPods instead") return rmc.WatchAgentPods(ctx, s.session) }, func(snapshot *manager.AgentPodInfoSnapshot) error { return s.updateClients(snapshot.Agents) }, nil) } func (ac *client) notify(waiter chan struct{}) { // a client must be connected to be able to notify if _, err := ac.ensureConnect(ac); err != nil { clog.Errorf(ac, "notifyWaiters %s (%s), ensureConnect failed: %v", ac.info.WorkloadName, net.IP(ac.info.PodIp), err) } close(waiter) } func (s *clients) notifyWaiters() { s.clients.Range(func(name string, ac *client) bool { if podIP, ok := netip.AddrFromSlice(ac.info.PodIp); ok { if waiter, ok := s.ipWaiters.LoadAndDelete(podIP); ok { ac.notify(waiter) } } if waiter, ok := s.wlWaiters.LoadAndDelete(ac.info.WorkloadName); ok { ac.notify(waiter) } return true }) } func (s *clients) waitWithTimeout(timeout time.Duration, waitOn <-chan struct{}) error { s.notifyWaiters() ctx, cancel := context.WithTimeout(s, timeout) defer cancel() select { case <-waitOn: return nil case <-ctx.Done(): return ctx.Err() } } func (s *clients) WaitForIP(ctx context.Context, timeout time.Duration, ip netip.Addr) error { if s.disabled.Load() { return nil } var cl *client waitOn, _ := s.ipWaiters.LoadOrCompute(ip, func() (chan struct{}, bool) { s.clients.Range(func(k string, ac *client) bool { if podIP, ok := netip.AddrFromSlice(ac.info.PodIp); ok && ip == podIP { cl = ac return false } return true }) if cl != nil { return nil, true } return make(chan struct{}), false }) if cl != nil { _, err := cl.ensureConnect(ctx) return err } if err := s.waitWithTimeout(timeout, waitOn); err != nil { return err } // Ensure that the client we're waiting for is ready. s.clients.Range(func(k string, ac *client) bool { if acIP, ok := netip.AddrFromSlice(ac.info.PodIp); ok && ip == acIP { cl = ac return false } return true }) if cl == nil { return status.Error(codes.NotFound, "no client available") } _, err := cl.ensureConnect(ctx) return err } func (s *clients) WaitForWorkload(timeout time.Duration, name string) error { if s.disabled.Load() { return nil } // Create a channel to subscribe to, but only if the agent doesn't already exist. waitOn, ok := s.wlWaiters.LoadOrCompute(name, func() (chan struct{}, bool) { found := false s.clients.Range(func(k string, ac *client) bool { if ac.info.WorkloadName == name { found = true return false } return true }) if found { return nil, true } return make(chan struct{}), false }) if ok { return s.waitWithTimeout(timeout, waitOn) } // No chan created because the agent already exists return nil } func (s *clients) updateClients(ais []*manager.AgentPodInfo) error { defer s.notifyWaiters() var aim map[string]*manager.AgentPodInfo if len(ais) > 0 { aim = make(map[string]*manager.AgentPodInfo, len(ais)) for _, ai := range ais { if ai.PodName != "" { aim[ai.PodName+"."+ai.Namespace] = ai } } if len(aim) == 0 { // The current traffic-manager injects old style clients that doesn't report a pod name. clog.Debugf(s, "disabling, because traffic-agent doesn't report pod name") s.disabled.Store(true) return nil } } deleteClient := func(k string) { s.clients.Compute(k, func(oldValue *client, loaded bool) (*client, xsync.ComputeOp) { if loaded { clog.Debugf(s, "Deleting agent %s", k) oldValue.cancel() return nil, xsync.DeleteOp } return nil, xsync.CancelOp }) } // Cancel clients that no longer exist. s.clients.Range(func(k string, _ *client) bool { if _, ok := aim[k]; !ok { deleteClient(k) } return true }) // Refresh current clients for k, ai := range aim { if ac, ok := s.clients.Load(k); ok { ac.refresh(ai) } } addClient := func(k string, ai *manager.AgentPodInfo) { _, _ = s.clients.LoadOrCompute(k, func() (*client, bool) { ac := &client{ Cluster: s.Cluster, session: s.session, remove: func() { s.clients.Delete(k) }, info: ai, } clog.Debugf(s, "Adding agent pod %s (%s)", k, net.IP(ai.PodIp)) return ac, false }) } // Add clients for newly arrived agents. for k, ai := range aim { addClient(k, ai) } // Terminate all dormant agents except the last one. dormantCount := 0 s.clients.Range(func(k string, ac *client) bool { if ac.dormant() && !s.isProxyVIA(ac.info) && !s.hasWaiterFor(ac.info) { dormantCount++ if dormantCount > 1 { clog.Debugf(s, "Deleting dormant agent %s", k) ac.cancel() } } return true }) if dormantCount > 1 { clog.Debugf(s, "Cancelled %d dormant clients", dormantCount-1) } return nil } ================================================ FILE: pkg/client/bwcompat/clusterinfo.go ================================================ package bwcompat import ( "github.com/telepresenceio/telepresence/rpc/v2/manager" "github.com/telepresenceio/telepresence/v2/pkg/iputil" ) func FixLegacyClusterInfo(mgrInfo *manager.ClusterInfo) { // Older clients use index 1 in the ClusterInfo to pass the kube-dns IP. It // will manifest itself as one entry of len 4 in the servicecidrs. if len(mgrInfo.ServiceCidrs) == 1 && len(mgrInfo.ServiceCidrs[0]) == 4 { // Older client with no support for multiple service subnets if mgrInfo.ServiceSubnet != nil { cidr := iputil.RPCToPrefix(mgrInfo.ServiceSubnet) sb, _ := cidr.MarshalBinary() mgrInfo.ServiceCidrs = [][]byte{sb} } else { mgrInfo.ServiceCidrs = nil } } } ================================================ FILE: pkg/client/cache/cache.go ================================================ package cache import ( "context" "errors" "fmt" "io/fs" "os" "path/filepath" "time" "github.com/go-json-experiment/json" "github.com/telepresenceio/telepresence/v2/pkg/dos" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" ) type Permissions fs.FileMode const ( Public Permissions = 0o644 Private Permissions = 0o600 ) func SaveToUserCache(ctx context.Context, object any, file string, perm Permissions) error { ctx = dos.WithLockedFs(ctx) jsonContent, err := json.Marshal(object) if err != nil { return err } // add file path (ex. "ispec/00-00-0000.json") fullFilePath := filepath.Join(filelocation.AppUserCacheDir(ctx), file) // get dir of joined path dir := filepath.Dir(fullFilePath) if err := dos.MkdirAll(ctx, dir, 0o755); err != nil { return err } return dos.WriteFile(ctx, fullFilePath, jsonContent, (fs.FileMode(perm))) } func LoadFromUserCache(ctx context.Context, dest any, file string) error { ctx = dos.WithLockedFs(ctx) path := filepath.Join(filelocation.AppUserCacheDir(ctx), file) jsonContent, err := dos.ReadFile(ctx, path) if err != nil { return err } if err := json.Unmarshal(jsonContent, &dest); err != nil { return fmt.Errorf("failed to parse JSON from file %s: %w", path, err) } return nil } func DeleteFromUserCache(ctx context.Context, file string) error { ctx = dos.WithLockedFs(ctx) if err := dos.Remove(ctx, filepath.Join(filelocation.AppUserCacheDir(ctx), file)); err != nil && !errors.Is(err, fs.ErrNotExist) { return err } return nil } func TouchUserCache(ctx context.Context, file string) error { now := time.Now() return os.Chtimes(filepath.Join(filelocation.AppUserCacheDir(ctx), file), now, now) } func UserCacheModTime(ctx context.Context, file string) (time.Time, error) { st, err := os.Stat(filepath.Join(filelocation.AppUserCacheDir(ctx), file)) if err != nil { return time.Time{}, err } return st.ModTime(), nil } func ExistsInCache(ctx context.Context, fileName string) (bool, error) { ctx = dos.WithLockedFs(ctx) path := filepath.Join(filelocation.AppUserCacheDir(ctx), fileName) if _, err := dos.Stat(ctx, path); err != nil { if errors.Is(err, os.ErrNotExist) { return false, nil } return false, err } return true, nil } ================================================ FILE: pkg/client/cache/watcher.go ================================================ package cache import ( "context" "math" "path/filepath" "time" "github.com/fsnotify/fsnotify" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/pkg/dos" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" ) // WatchUserCache uses a file system watcher that receives events when one of the given files changes // and calls the given function when that happens. // All files in the given subDir are watched when the list of files is empty. func WatchUserCache(ctx context.Context, subDir string, onChange func(context.Context) error, files ...string) error { dir := filepath.Join(filelocation.AppUserCacheDir(ctx), subDir) // Ensure that the user cache directory exists. if err := dos.MkdirAll(ctx, dir, 0o755); err != nil { return err } watcher, err := fsnotify.NewWatcher() if err != nil { return err } defer watcher.Close() // The directory containing the files must be watched because editing a // file will typically end with renaming the original and then creating // a new file. A watcher that follows the inode will not see when the new // file is created. if err = watcher.Add(dir); err != nil { return err } // The delay timer will initially sleep forever. It's reset to a very short // delay when the file is modified. delay := time.AfterFunc(time.Duration(math.MaxInt64), func() { select { case <-ctx.Done(): return default: if err := onChange(ctx); err != nil { clog.Error(ctx, err) } } }) defer delay.Stop() isOfInterest := func(string) bool { return true } if len(files) > 0 { for i := range files { files[i] = filepath.Join(dir, files[i]) } isOfInterest = func(s string) bool { for _, file := range files { if s == file { return true } } return false } } for { select { case <-ctx.Done(): return nil case err = <-watcher.Errors: clog.Error(ctx, err) case event := <-watcher.Events: if event.Op&(fsnotify.Remove|fsnotify.Write|fsnotify.Create) != 0 && isOfInterest(event.Name) { // The file was created, modified, or removed. Let's defer the call to onChange just // a little bit in case there are more modifications to it. delay.Reset(5 * time.Millisecond) } } } } ================================================ FILE: pkg/client/cli/ann/annotations.go ================================================ package ann // -- Annotation keys const ( UserDaemon = "userD" Session = "session" VersionCheck = "versionCheck" UpdateCheckFormat = "updateCheckFormat" Progress = "progress" ) // -- Annotation values const ( Optional = "optional" Required = "required" Tel2 = "https://%s/download/tel2/%s/%s/stable.txt" ) ================================================ FILE: pkg/client/cli/cmd/completion.go ================================================ package cmd import ( "fmt" "os" "github.com/spf13/cobra" "github.com/telepresenceio/telepresence/v2/pkg/errcat" ) const completionLongPlain = `To load completions: Bash: $ source <(%[1]s completion bash) # To load completions for each session, execute once: # Linux: $ %[1]s completion bash > /etc/bash_completion.d/%[1]s # macOS: $ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s Zsh: # If shell completion is not already enabled in your environment, # you will need to enable it. You can execute the following once: $ echo "autoload -U compinit; compinit" >> ~/.zshrc # To load completions for each session, execute once: $ %[1]s completion zsh > "${fpath[1]}/_%[1]s" # You will need to start a new shell for this setup to take effect. fish: $ %[1]s completion fish | source # To load completions for each session, execute once: $ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish PowerShell: PS> %[1]s completion powershell | Out-String | Invoke-Expression # To load completions for every new session, run: PS> %[1]s completion powershell > %[1]s.ps1 # and source this file from your PowerShell profile. ` const completionLongMarkdown = `To load completions: ### Bash: ` + "```bash" + ` $ source <(%[1]s completion bash) # To load completions for each session, execute once: # Linux: $ %[1]s completion bash > /etc/bash_completion.d/%[1]s # macOS: $ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s ` + "```" + ` ### Zsh: ` + "```zsh" + ` # If shell completion is not already enabled in your environment, # you will need to enable it. You can execute the following once: $ echo "autoload -U compinit; compinit" >> ~/.zshrc # To load completions for each session, execute once: $ %[1]s completion zsh > "${fpath[1]}/_%[1]s" # You will need to start a new shell for this setup to take effect. ` + "```" + ` ### fish: ` + "```fish" + ` $ %[1]s completion fish | source # To load completions for each session, execute once: $ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish ` + "```" + ` ### PowerShell: ` + "```powershell" + ` PS> %[1]s completion powershell | Out-String | Invoke-Expression # To load completions for every new session, run: PS> %[1]s completion powershell > %[1]s.ps1 # and source this file from your PowerShell profile. ` + "```" func addCompletion(rootCmd *cobra.Command, markdown bool) { longText := completionLongPlain if markdown { longText = completionLongMarkdown } cmd := cobra.Command{ Use: "completion", Short: "Generate a shell completion script", ValidArgs: []string{ "bash", "zsh", "powershell", "fish", }, ArgAliases: []string{"ps"}, RunE: func(cmd *cobra.Command, args []string) error { var shell string if 0 < len(args) { shell = args[0] } var err error switch shell { case "zsh": err = rootCmd.GenZshCompletionNoDesc(os.Stdout) case "bash": err = rootCmd.GenBashCompletionV2(os.Stdout, false) case "fish": err = rootCmd.GenFishCompletion(os.Stdout, false) case "ps", "powershell": err = rootCmd.GenPowerShellCompletion(os.Stdout) case "": err = errcat.User.Newf("shell not specified") } return err }, Long: fmt.Sprintf(longText, rootCmd.Name()), } cmd.SetContext(rootCmd.Context()) rootCmd.AddCommand(&cmd) } ================================================ FILE: pkg/client/cli/cmd/compose.go ================================================ package cmd import ( "github.com/spf13/cobra" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/docker/compose" ) func composeCmd() *cobra.Command { cmd := &cobra.Command{ Use: "compose command [flags]", Short: "Define and run multi-container applications with Telepresence and Docker", TraverseChildren: true, } cmd.AddCommand(compose.GenerateSubCommands(cmd)...) return cmd } ================================================ FILE: pkg/client/cli/cmd/config.go ================================================ package cmd import ( "github.com/spf13/cobra" empty "google.golang.org/protobuf/types/known/emptypb" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/ann" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/connect" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/output" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/progress" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" "github.com/telepresenceio/telepresence/v2/pkg/grpc" "github.com/telepresenceio/telepresence/v2/pkg/json" ) func configCmd() *cobra.Command { cmd := &cobra.Command{ Use: "config", Short: "Telepresence configuration commands", } cmd.AddCommand(configView()) return cmd } const clientOnlyFlag = "client-only" func configView() *cobra.Command { cmd := &cobra.Command{ Use: "view", Args: cobra.NoArgs, PersistentPreRunE: output.DefaultYAML, Short: "View current Telepresence configuration", RunE: runConfigView, Annotations: map[string]string{ ann.Session: ann.Optional, }, ValidArgsFunction: cobra.NoFileCompletions, } cmd.Flags().BoolP(clientOnlyFlag, "c", false, "Only view config from client file.") return cmd } func runConfigView(cmd *cobra.Command, _ []string) error { defer func() { progress.Stop(cmd.Context()) }() var cfg client.SessionConfig clientOnly, _ := cmd.Flags().GetBool(clientOnlyFlag) if !clientOnly { cmd.Annotations = map[string]string{ ann.Session: ann.Required, } if err := connect.InitCommand(cmd); err != nil { // Unable to establish a session, so try to convey the local config instead. It // may be helpful in diagnosing the problem. cmd.Annotations = map[string]string{} clientOnly = true } } if clientOnly { if err := connect.InitCommand(cmd); err != nil { return err } kc, err := daemon.GetCommandKubeConfig(cmd) if err != nil { return err } cfg.Config = client.GetConfig(kc) cfg.ClientFile = client.GetConfigFile(kc) cfg.LogDirectory = filelocation.AppUserLogDir(kc) output.Object(cmd.Context(), &cfg, true) return nil } cmd.Annotations = map[string]string{ ann.Session: ann.Required, } if err := connect.InitCommand(cmd); err != nil { return err } ctx := cmd.Context() cc, err := daemon.MustGetUserClient(ctx).GetConfig(ctx, &empty.Empty{}) if err != nil { return grpc.FromGRPC(err) } err = json.Unmarshal(cc.Json, &cfg, false) if err != nil { return err } output.Object(ctx, &cfg, true) return nil } ================================================ FILE: pkg/client/cli/cmd/connect.go ================================================ package cmd import ( "os" "slices" "github.com/spf13/cobra" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/ann" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/connect" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" ) func connectCmd() *cobra.Command { var request *daemon.CobraRequest cmd := &cobra.Command{ Use: "connect [flags] [-- ]", Args: cobra.ArbitraryArgs, Short: "Connect to a cluster", Annotations: map[string]string{ ann.Session: ann.Required, }, RunE: func(cmd *cobra.Command, args []string) error { if err := request.CommitFlags(cmd); err != nil { return err } return connect.RunConnect(cmd, args) }, ValidArgsFunction: func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { dir := cobra.ShellCompDirectiveNoFileComp if slices.Contains(os.Args, "--") { dir = cobra.ShellCompDirectiveDefault } return nil, dir }, } request = daemon.InitRequest(cmd) return cmd } ================================================ FILE: pkg/client/cli/cmd/curl.go ================================================ package cmd import ( "slices" "github.com/spf13/cobra" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/ann" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/global" ) func curlCmd() *cobra.Command { cmd := &cobra.Command{ Use: "curl", Short: "curl with daemon network", Args: cobra.ArbitraryArgs, Annotations: map[string]string{ ann.Session: ann.Optional, }, RunE: runCurl, ValidArgsFunction: cobra.NoFileCompletions, SilenceErrors: true, SilenceUsage: true, DisableFlagParsing: true, DisableFlagsInUseLine: true, DisableSuggestions: true, } return cmd } func runCurl(cmd *cobra.Command, args []string) error { global.SetProgressQuiet(cmd) return runDockerRun(cmd, slices.Insert(args, 0, "--rm", "curlimages/curl")) } ================================================ FILE: pkg/client/cli/cmd/docker_run.go ================================================ package cmd import ( "context" "fmt" "slices" "strings" "sync/atomic" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/ann" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/connect" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" cliDocker "github.com/telepresenceio/telepresence/v2/pkg/client/cli/docker" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/flags" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/global" "github.com/telepresenceio/telepresence/v2/pkg/client/docker" "github.com/telepresenceio/telepresence/v2/pkg/errcat" "github.com/telepresenceio/telepresence/v2/pkg/proc" ) func dockerRunCmd() *cobra.Command { cmd := &cobra.Command{ Use: "docker-run", Short: "Docker run with daemon network", Args: cobra.ArbitraryArgs, Annotations: map[string]string{ ann.Session: ann.Optional, }, RunE: runDockerRunCLI, SilenceErrors: true, SilenceUsage: true, DisableFlagParsing: true, DisableFlagsInUseLine: true, ValidArgsFunction: cliDocker.AutocompleteRun, } return cmd } func findAndParseFlag(flags *pflag.FlagSet, flagName string, args []string) ([]string, error) { if i := slices.Index(args, "--"+flagName); i >= 0 && i+1 < len(args) { if err := flags.Parse(args[i : i+2]); err != nil { return nil, err } args = slices.Delete(args, i, i+2) } else if i = slices.IndexFunc(args, func(s string) bool { return strings.HasPrefix(s, "--"+flagName+"=") }); i >= 0 { if err := flags.Parse(args[i : i+1]); err != nil { return nil, err } args = slices.Delete(args, i, i+1) } return args, nil } func parseFlags(cmd *cobra.Command, args []string) ([]string, error) { // The command has all flag parsing disabled, but we must check for the global flags. Luckily, these flags do not conflict with // the docker run flags. opts := cmd.Flags() var err error for _, n := range global.FlagNames { args, err = findAndParseFlag(opts, n, args) if err != nil { return nil, err } } return args, nil } func runDockerRunCLI(cmd *cobra.Command, args []string) error { return errcat.NoDaemonLogs.New(runDockerRun(cmd, args)) } func runDockerRun(cmd *cobra.Command, args []string) error { args, err := parseFlags(cmd, args) if err != nil { return err } if slices.Contains(args, "--help") { return proc.StdCommand(cmd.Context(), docker.Exe, slices.Insert(args, 0, "run")...).Run() } err = connect.InitCommand(cmd) if err != nil { return err } ctx, cancel := context.WithCancel(cmd.Context()) defer cancel() ud := daemon.GetUserClient(ctx) if ud == nil { return fmt.Errorf("%s requires a connection", cmd.UseLine()) } if !ud.Containerized() { return fmt.Errorf("%s requires that --docker was used when the connection was established", cmd.UseLine()) } if name, ok, err := flags.GetUnparsedValue("name", 0, false, args); err == nil && ok { ip, err := ud.Lookup(ctx, name) if err == nil { // We're about to start a container with a name that is already present in the cluster. That's // probably a mistake. fmt.Fprintf(cmd.ErrOrStderr(), "Warning! The container name %q will override the current mapping to IP %s\n", name, ip) } } cni, cc, err := docker.Start(ctx, true, args...) if err != nil { return errcat.NoDaemonLogs.New(err) } if cc == nil { // Container already exited return nil } var exited, signalled atomic.Bool done := make(chan error, 1) if flags.HasOption("tty", 't', args) { close(done) } else { go cliDocker.EnsureStopContainer(ctx, cni.Name, cni.ID, nil, &exited, &signalled, done) } err = cc.Wait() exited.Store(true) cancel() if signalled.Load() { err = nil } waitErr := <-done if err == nil { err = waitErr } return errcat.NoDaemonLogs.New(err) } ================================================ FILE: pkg/client/cli/cmd/gather_logs.go ================================================ package cmd import ( "archive/zip" "bufio" "context" "errors" "fmt" "io" "os" "path/filepath" "regexp" "strings" "time" "github.com/spf13/cobra" "google.golang.org/grpc" "github.com/telepresenceio/telepresence/rpc/v2/connector" "github.com/telepresenceio/telepresence/v2/pkg/agentconfig" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/ann" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/connect" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/progress" "github.com/telepresenceio/telepresence/v2/pkg/dos" "github.com/telepresenceio/telepresence/v2/pkg/errcat" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" tpGrpc "github.com/telepresenceio/telepresence/v2/pkg/grpc" "github.com/telepresenceio/telepresence/v2/pkg/ioutil" ) type gatherLogsCommand struct { outputFile string daemons string trafficAgents string trafficManager bool anon bool podYaml bool } func gatherLogs() *cobra.Command { gl := &gatherLogsCommand{} cmd := &cobra.Command{ Use: "gather-logs", Args: cobra.NoArgs, Short: "Gather logs from traffic-manager, traffic-agent, user and root daemons, and export them into a zip file.", Long: `Gather logs from traffic-manager, traffic-agent, user and root daemons, and export them into a zip file. Useful if you are opening a Github issue or asking someone to help you debug Telepresence.`, Example: `Here are a few examples of how you can use this command: # Get all logs and export to a given file telepresence gather-logs -o /tmp/telepresence_logs.zip # Get all logs and pod yaml manifests for components in the kubernetes cluster telepresence gather-logs -o /tmp/telepresence_logs.zip --get-pod-yaml # Get all logs for the daemons only telepresence gather-logs --traffic-agents=None --traffic-manager=False # Get all logs for pods that have "echo-easy" in the name, useful if you have multiple replicas telepresence gather-logs --traffic-manager=False --traffic-agents=echo-easy # Get all logs for a specific pod telepresence gather-logs --traffic-manager=False --traffic-agents=echo-easy-6848967857-tw4jw # Get logs from everything except the daemons telepresence gather-logs --daemons=None `, RunE: gl.gatherLogs, Annotations: map[string]string{ ann.Session: ann.Optional, }, ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() flags.StringVarP(&gl.outputFile, "output-file", "o", "", "The file you want to output the logs to.") flags.StringVar(&gl.daemons, "daemons", "all", "Comma separated list of daemons you want logs from: all, root, user, kubeauth, None") flags.BoolVar(&gl.trafficManager, "traffic-manager", true, "If you want to collect logs from the traffic-manager") flags.StringVar(&gl.trafficAgents, "traffic-agents", "all", "Traffic-agents to collect logs from: all, name substring, None") flags.BoolVarP(&gl.anon, "anonymize", "a", false, "To anonymize pod names + namespaces from the logs") flags.BoolVarP(&gl.podYaml, "get-pod-yaml", "y", false, "Get the yaml of any pods you are getting logs for") return cmd } // anonymizer contains the mappings between things we want to anonymize // and their new, anonymized name. Using a map instead of simply redacting // makes it easier for us to maintain certain relationships in the logs (e.g. // namespaces things are in) which may be helpful in troubleshooting. type anonymizer struct { namespaces map[string]string podNames map[string]string } // gatherLogs gets the logs from the daemons (daemon + connector) and creates a zip. func (gl *gatherLogsCommand) gatherLogs(cmd *cobra.Command, _ []string) error { if err := connect.InitCommand(cmd); err != nil { return err } ctx := cmd.Context() defer progress.Stop(ctx) ctx = dos.WithStdio(ctx, cmd) // If the user did not provide an outputFile, we'll use their current working directory if gl.outputFile == "" { pwd, err := os.Getwd() if err != nil { return errcat.User.New(err) } gl.outputFile = filepath.Join(pwd, fmt.Sprintf("telepresence_logs_%s.zip", time.Now().UTC().Format("060102T150405"))) } else if !strings.HasSuffix(gl.outputFile, ".zip") { return errcat.User.New("output file must end in .zip") } // We will store the logs in a temp dir in the users log directory before we zip them for export, so that // containerized daemons will use the same place. exportDir, err := os.MkdirTemp(filelocation.AppUserCacheDir(ctx), "logs-*") if err != nil { return errcat.User.New(err) } defer func() { if err := os.RemoveAll(exportDir); err != nil { ioutil.Printf(dos.Stderr(ctx), "Failed to remove temp directory %s: %s", exportDir, err) } }() // Add the daemonLogs to the export directory var daemonLogs []string for _, daemon := range strings.Split(gl.daemons, ",") { daemon = strings.TrimSpace(daemon) switch daemon { case "all": daemonLogs = append(daemonLogs, "cli", "connector", "daemon", "kubeauth") case "cli": daemonLogs = append(daemonLogs, "cli") case "daemon", "root": daemonLogs = append(daemonLogs, "daemon") case "connector", "user": daemonLogs = append(daemonLogs, "connector") case "kubeauth": daemonLogs = append(daemonLogs, "kubeauth") case "", "None": default: return errcat.User.New("Options for --daemons are: all, root, user, or None") } } var az *anonymizer if gl.anon { az = &anonymizer{ namespaces: make(map[string]string), podNames: make(map[string]string), } } // Since getting the logs from k8s requires the connector, let's only do this // work if we know the user wants to get logs from k8s. // We gather those logs before we gather the connector.log so that problems that // may occur during that process will be included in the connector.log if gl.trafficManager || gl.trafficAgents != "None" { if err := gl.gatherClusterLogs(ctx, exportDir, az); err != nil { // We let the user know we were unable to get logs from the kubernetes components, // and why, but this shouldn't block the command returning successful with the logs // it was able to get. ioutil.Printf(dos.Stderr(ctx), "error getting logs from kubernetes components: %s\n", err) } } err = retrieveLocalLogs(ctx, daemonLogs, exportDir) if err != nil { return errcat.User.New(err) } // Zip up all the files we've created in the zip directory and return that to the user files, err := getLogFiles(exportDir) if err != nil { return errcat.User.New(err) } if az != nil { anonymizeLogs(ctx, files, az) } if err = zipFiles(files, gl.outputFile); err != nil { return err } ioutil.Printf(dos.Stdout(ctx), "Logs have been exported to %s\n", gl.outputFile) return nil } // retrieveLocalLogs retrieves all logs from the logDir that match the daemons the user cares about. func retrieveLocalLogs(ctx context.Context, daemonLogs []string, exportDir string) error { // Get all logs from the logDir that match the daemons the user cares about. logDir := filelocation.AppUserLogDir(ctx) logFiles, err := os.ReadDir(logDir) if err != nil { return err } for _, entry := range logFiles { if entry.IsDir() { continue } for _, logType := range daemonLogs { if !strings.Contains(entry.Name(), logType) { continue } srcFile := filepath.Join(logDir, entry.Name()) // The cli.log is often empty, so this check is relevant. empty, err := isEmpty(srcFile) if err != nil { ioutil.Printf(dos.Stderr(ctx), "failed stat on %s: %s\n", entry.Name(), err) continue } if empty { continue } dstFile := filepath.Join(exportDir, entry.Name()) if err := copyFiles(dstFile, srcFile); err != nil { // We don't want to fail / exit abruptly if we can't copy certain // files, but we do want the user to know we were unsuccessful ioutil.Printf(dos.Stderr(ctx), "failed exporting %s: %s\n", entry.Name(), err) } } } return nil } func getLogFiles(exportDir string) ([]string, error) { dirEntries, err := os.ReadDir(exportDir) if err != nil { return nil, err } files := make([]string, 0, len(dirEntries)) for _, entry := range dirEntries { if !entry.IsDir() { files = append(files, filepath.Join(exportDir, entry.Name())) } } return files, nil } func anonymizeLogs(ctx context.Context, files []string, az *anonymizer) { for _, fullFileName := range files { if err := az.anonymizeLog(fullFileName); err != nil { ioutil.Printf(dos.Stderr(ctx), "error anonymizing %s: %s\n", fullFileName, err) } files = append(files, fullFileName) } } func (gl *gatherLogsCommand) gatherClusterLogs(ctx context.Context, exportDir string, az *anonymizer) error { // To get logs from the components in the kubernetes cluster, we ask the // traffic-manager. rq := &connector.LogsRequest{ TrafficManager: gl.trafficManager, Agents: gl.trafficAgents, GetPodYaml: gl.podYaml, ExportDir: filepath.Base(exportDir), } userD := daemon.GetUserClient(ctx) if userD != nil { var opts []grpc.CallOption cfg := client.GetConfig(ctx) if mz := cfg.Grpc().MaxReceiveSize(); mz > 0 { opts = append(opts, grpc.MaxCallRecvMsgSize(int(mz))) } lr, err := userD.GatherLogs(ctx, rq, opts...) if err != nil { return tpGrpc.FromGRPC(err) } if az != nil { if err := az.anonymizeFileNames(lr, exportDir); err != nil { return err } } } return nil } func isEmpty(file string) (bool, error) { s, err := os.Stat(file) if err != nil { return false, err } return s.Size() == 0, err } // copyFiles copies files from one location into another. func copyFiles(dstFile, srcFile string) error { srcWriter, err := os.Open(srcFile) if err != nil { return err } defer srcWriter.Close() dstWriter, err := os.Create(dstFile) if err != nil { return err } defer dstWriter.Close() if _, err := io.Copy(dstWriter, srcWriter); err != nil { return err } return nil } // zipFiles creates a zip file with the contents of all the files passed in. // If some files do not exist, it will include that in the error message, // but it will still create a zip file with as many files as it can. func zipFiles(files []string, zipFileName string) error { zipFile, err := os.Create(zipFileName) if err != nil { return err } defer zipFile.Close() zipWriter := zip.NewWriter(zipFile) defer zipWriter.Close() addFileToZip := func(file string) error { fd, err := os.Open(file) if err != nil { return err } defer fd.Close() // Get the header information from the original file fileInfo, err := os.Stat(file) if err != nil { return err } fileHeader, err := zip.FileInfoHeader(fileInfo) if err != nil { return err } fileHeader.Method = zip.Deflate if err != nil { return err } // Get the basename of the file since that's all we want // to include in the zip baseName := filepath.Base(file) fileHeader.Name = baseName zfd, err := zipWriter.CreateHeader(fileHeader) if err != nil { return err } if _, err := io.Copy(zfd, fd); err != nil { return err } return nil } // Make a note of the files we fail to add to the zip so users know if the // zip is incomplete errMsg := "" for _, file := range files { // If the file doesn't have a name, then we obviously can't add it to // the zip. We have handling elsewhere to prevent files like this from // getting here but are extra cautious. if file == "" { continue } if err := addFileToZip(file); err != nil { errMsg += fmt.Sprintf("failed adding %s to zip file: %s ", file, err) } } if errMsg != "" { return errors.New(errMsg) } return nil } // anonymizeFileNames will anonymize the file names of all pods in the connector.LogResponse. func (a *anonymizer) anonymizeFileNames(lr *connector.LogsResponse, exportDir string) error { for n, v := range lr.PodInfo { qn := filepath.Join(exportDir, n) if v != "ok" { // Write the error to retrieve the log as the log content. It's better than nothing _ = os.WriteFile(qn, []byte(v), 0o666) } anonQn := filepath.Join(exportDir, a.getPodName(n)) if err := os.Rename(qn, anonQn); err != nil { return fmt.Errorf("failed to anonymize by renaming file name %s to %s", qn, anonQn) } } return nil } // getPodName returns an anonymized version of the podName. The anonymized value is cached so that // the same anonymized name will be returned on subsequent calls using the same podName. func (a *anonymizer) getPodName(podName string) string { // If this pod name has already been mapped, return that if anonName, ok := a.podNames[podName]; ok { return anonName } // the podName hasn't been anonymized yet so we split it up // so we can anonymize the namespace nameComponents := strings.SplitN(podName, ".", 2) if len(nameComponents) != 2 { // Note: the ordinal here is based on the total number of // pods, not the number of anonPods that are found. This // shouldn't be a problem because the main goal of this // is to make them distinct, but should we ever want the // ordinals to be strictly for anonPods, we'll need to // make a change here. unknownPodName := fmt.Sprintf("anonPod-%d.anonNamespace", len(a.podNames)+1) a.podNames[podName] = unknownPodName return unknownPodName } var anonPodName, anonNamespace string name, namespace := nameComponents[0], nameComponents[1] if val, ok := a.namespaces[namespace]; ok { anonNamespace = val } else { anonNamespace = fmt.Sprintf("namespace-%d", len(a.namespaces)+1) a.namespaces[namespace] = anonNamespace } // we want to special case the traffic-manager so we can easily distinguish // between that and the traffic-agents if strings.Contains(name, agentconfig.ManagerAppName) { anonPodName = fmt.Sprintf("%s.%s", agentconfig.ManagerAppName, anonNamespace) } else { anonPodName = fmt.Sprintf("pod-%d.%s", len(a.podNames)+1, anonNamespace) } // Store the anonPodName in the map a.podNames[podName] = anonPodName return anonPodName } // anonymizeLog is a helper function that replaces the namespace + podName // used in the log with its anonymized version, provided by the anonymizer. // It overwrites the file with the anonymized version. func (a *anonymizer) anonymizeLog(logFile string) error { // Read the contents we are going to overwrite from the file content, err := os.ReadFile(logFile) if err != nil { return err } // Open the file with write so we can overwrite it stringContent := string(content) f, err := os.OpenFile(logFile, os.O_RDWR, 0) if err != nil { return err } defer f.Close() // First we replace the actual namespace with the anonymized // version. for namespace, anonNamespace := range a.namespaces { stringContent = strings.ReplaceAll(stringContent, namespace, anonNamespace) } // Now we do pod name which is a little bit more complicated for fullPodName, fullAnonPodName := range a.podNames { // strip the namespace off of the anonymized name anonPodParts := strings.Split(fullAnonPodName, ".") anonPodName := anonPodParts[0] // Strip the namespace off of the podName podParts := strings.Split(fullPodName, ".") for _, name := range getSignificantPodNames(podParts[0]) { stringContent = strings.ReplaceAll(stringContent, name, anonPodName) } } // Overwrite the file with the anonymized log err = f.Truncate(0) if err != nil { return err } _, err = f.Seek(0, 0) if err != nil { return err } fdWriter := bufio.NewWriter(f) _, err = fdWriter.WriteString(stringContent) if err != nil { return err } fdWriter.Flush() return nil } // getSignificantPodNames is a helper function that takes in a // pod's name and returns the significant subnames that we want // to anonymize. It currently works for pods owned by StatefulSets, // ReplicaSets, and Deployments. func getSignificantPodNames(podName string) []string { // if the pods ends in an ordinal we can be pretty sure it's // coming from a StatefulSet. statefulSetRegex := regexp.MustCompile("(.*)-([0-9]+)$") // ReplicasSets, and therefore Deployments because they create // ReplicaSets, have a hash followed by a 5 character identity // string attached to the end. replicaSetRegex := regexp.MustCompile("(.*)-([0-9a-f]+)-([0-9a-z]{5})$") sigNames := []string{} switch { case statefulSetRegex.MatchString(podName): match := statefulSetRegex.FindStringSubmatch(podName) appName := match[1] // Add the pod name with and without the ordinal sigNames = append(sigNames, podName, appName) case replicaSetRegex.MatchString(podName): match := replicaSetRegex.FindStringSubmatch(podName) appName := match[1] rsName := fmt.Sprintf("%s-%s", appName, match[2]) // add the app name with and without generated ReplicaSet hash sigNames = append(sigNames, podName, rsName, appName) default: // For default we don't do anything and will leave sigNames // as an empty slice } return sigNames } ================================================ FILE: pkg/client/cli/cmd/gather_logs_test.go ================================================ package cmd import ( "archive/zip" "fmt" "io" "log/slog" "os" "regexp" "testing" "time" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/telepresenceio/clog" "github.com/telepresenceio/clog/testutil" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/global" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" ) func Test_gatherLogsZipFiles(t *testing.T) { type testcase struct { name string // We use these two slices so it's easier to write tests knowing which // files are expected to exist and which aren't. These slices are combined // prior to calling zipFiles in the tests. realFileNames []string fakeFileNames []string fileDir string } testCases := []testcase{ { name: "successfulZipAllFiles", realFileNames: []string{"file1.log", "file2.log", "diff_name.log"}, fakeFileNames: []string{}, fileDir: "testdata/zipDir", }, { name: "successfulZipSomeFiles", realFileNames: []string{"file1.log", "file2.log"}, fakeFileNames: []string{}, fileDir: "testdata/zipDir", }, { name: "successfulZipNoFiles", realFileNames: []string{}, fakeFileNames: []string{}, fileDir: "testdata/zipDir", }, { name: "zipOneIncorrectFile", realFileNames: []string{"file1.log", "file2.log", "diff_name.log"}, fakeFileNames: []string{"notreal.log"}, fileDir: "testdata/zipDir", }, { name: "zipIncorrectDir", realFileNames: []string{}, fakeFileNames: []string{"file1.log", "file2.log", "diff_name.log"}, fileDir: "testdata/fakeZipDir", }, } for _, tc := range testCases { tcName := tc.name t.Run(tcName, func(t *testing.T) { fileNames := make([]string, 0, len(tc.realFileNames)+len(tc.fakeFileNames)) fileNames = append(fileNames, tc.realFileNames...) fileNames = append(fileNames, tc.fakeFileNames...) if tc.fileDir != "" { for i := range fileNames { fileNames[i] = fmt.Sprintf("%s/%s", tc.fileDir, fileNames[i]) } } outputDir := t.TempDir() err := zipFiles(fileNames, fmt.Sprintf("%s/logs.zip", outputDir)) // If we put in fakeFileNames, then we verify we get the errors we expect if len(tc.fakeFileNames) > 0 { for _, name := range tc.fakeFileNames { assert.Contains(t, err.Error(), fmt.Sprintf("failed adding %s/%s to zip file", tc.fileDir, name)) } } else { require.NoError(t, err) } // Ensure the files in the zip match the files that wer zipped zipReader, err := zip.OpenReader(fmt.Sprintf("%s/logs.zip", outputDir)) require.NoError(t, err) defer zipReader.Close() for _, f := range zipReader.File { // Ensure the file was actually supposed to be in the zip assert.Contains(t, tc.realFileNames, f.Name) filesEqual, err := checkZipEqual(f, "testdata/zipDir") require.NoError(t, err) assert.True(t, filesEqual) } // Ensure that only the "real files" were added to the zip file assert.Equal(t, len(tc.realFileNames), len(zipReader.File)) }) } } func Test_gatherLogsCopyFiles(t *testing.T) { type testcase struct { name string srcFileName string fileDir string outputDir string errExpected bool } testCases := []testcase{ { name: "successfulCopyFile", srcFileName: "file1.log", fileDir: "testdata/zipDir", outputDir: "", errExpected: false, }, { name: "failSrcFile", srcFileName: "fake_file.log", fileDir: "testdata/zipDir", outputDir: "", errExpected: true, }, { name: "failDstFile", srcFileName: "file1.log", fileDir: "testdata/zipDir", outputDir: "notarealdir", errExpected: true, }, } for _, tc := range testCases { tcName := tc.name t.Run(tcName, func(t *testing.T) { if tc.outputDir == "" { tc.outputDir = t.TempDir() } dstFile := fmt.Sprintf("%s/copiedFile.log", tc.outputDir) srcFile := fmt.Sprintf("%s/%s", tc.fileDir, tc.srcFileName) err := copyFiles(dstFile, srcFile) if tc.errExpected { assert.Error(t, err) } else { assert.NoError(t, err) require.NoError(t, err) // when there's no error message, we validate that the file was // copied correctly dstContent, err := os.ReadFile(dstFile) require.NoError(t, err) srcContent, err := os.ReadFile(srcFile) require.NoError(t, err) assert.Equal(t, string(dstContent), string(srcContent)) } }) } } func Test_gatherLogsNoK8s(t *testing.T) { type testcase struct { name string outputFile string daemons string errMsg string } testCases := []testcase{ { name: "successfulZipAllDaemonLogs", outputFile: "", daemons: "all", errMsg: "", }, { name: "successfulZipOnlyRootLogs", outputFile: "", daemons: "root", errMsg: "", }, { name: "successfulZipOnlyConnectorLogs", outputFile: "", daemons: "user", errMsg: "", }, { name: "successfulZipConnectorAndDaemonLogs", outputFile: "", daemons: "user,root", errMsg: "", }, { name: "successfulZipNoDaemonLogs", outputFile: "", daemons: "None", errMsg: "", }, { name: "incorrectDaemonFlagValue", outputFile: "", daemons: "notARealFlagValue", errMsg: "Options for --daemons are: all, root, user, or None", }, } for _, tc := range testCases { tcName := tc.name t.Run(tcName, func(t *testing.T) { // Use this time to validate that the zip file says the // files inside were modified after the test started. startTime := time.Now() // Prepare the context + use our testdata log dir for these tests ctx := testutil.NewContext(t, false) testLogDir := "testdata/testLogDir" ctx = filelocation.WithAppUserLogDir(ctx, testLogDir) // this isn't actually used for our unit tests, but is needed for the function // when it is getting logs from k8s components cmd := &cobra.Command{} // override the outputFile outputDir := t.TempDir() if tc.outputFile == "" { tc.outputFile = fmt.Sprintf("%s/telepresence_logs.zip", outputDir) } stdout := clog.StdLogger(ctx, slog.LevelInfo).Writer() stderr := clog.StdLogger(ctx, slog.LevelError).Writer() cmd.SetOut(stdout) cmd.SetErr(stderr) cmd.PersistentFlags().AddFlagSet(global.Flags(ctx, false, false)) cmd.InitDefaultHelpFlag() // Ensures that persistent flags are merged. cmd.SetContext(ctx) gl := &gatherLogsCommand{ outputFile: tc.outputFile, daemons: tc.daemons, // We will test other values of this in our integration tests since // they require a kubernetes cluster trafficAgents: "None", trafficManager: false, } // Ensure we can create a zip of the logs cacheDir := filelocation.AppUserCacheDir(ctx) require.NoError(t, os.MkdirAll(cacheDir, 0o755)) err := gl.gatherLogs(cmd, nil) if tc.errMsg != "" { require.Error(t, err) assert.Contains(t, err.Error(), tc.errMsg) } else { require.NoError(t, err) // Validate that the zip file only contains the files we expect zipReader, err := zip.OpenReader(tc.outputFile) require.NoError(t, err) defer zipReader.Close() var regexStr string switch gl.daemons { case "all": regexStr = "cli|connector|daemon" case "root": regexStr = "daemon" case "user": regexStr = "connector" case "user,root": regexStr = "connector|daemon" case "None": regexStr = "a^" // impossible to match default: // We shouldn't hit this t.Fatal("Used an option for daemon that is impossible") } for _, f := range zipReader.File { // Ensure the file was actually supposed to be in the zip assert.Regexp(t, regexp.MustCompile(regexStr), f.Name) filesEqual, err := checkZipEqual(f, testLogDir) require.NoError(t, err) assert.True(t, filesEqual) // Ensure the zip file metadata is correct (e.g. not the // default which is 1979) that it was modified after the // test started. // This test is incredibly fast (within a second) so we // convert the times to unix timestamps (to get us to // nearest seconds) and ensure the unix timestamp for the // zip file is not less than the unix timestamp for the // start time. // If this ends up being flakey, we can move the start // time out of the test loop and add a sleep for a second // to ensure nothing weird could happen with rounding. assert.False(t, f.FileInfo().ModTime().Unix() < startTime.Unix(), fmt.Sprintf("Start time: %d, file time: %d", startTime.Unix(), f.FileInfo().ModTime().Unix())) } } }) } } func Test_gatherLogsGetPodName(t *testing.T) { podNames := []string{ "echo-auto-inject-64323-3454.default", "echo-easy-141245-23432.ambassador", "traffic-manager-123214-2332.ambassador", } podMapping := []string{ "pod-1.namespace-1", "pod-2.namespace-2", "traffic-manager.namespace-2", } // We need a fresh anonymizer for each test anonymizer := &anonymizer{ namespaces: make(map[string]string), podNames: make(map[string]string), } // Get the newPodName for each pod for _, podName := range podNames { newPodName := anonymizer.getPodName(podName) require.NotEqual(t, podName, newPodName) } // Ensure the anonymizer contains the total expected values require.Equal(t, 3, len(anonymizer.podNames)) require.Equal(t, 2, len(anonymizer.namespaces)) // Ensure the podNames were anonymized correctly for i := range podNames { require.Equal(t, podMapping[i], anonymizer.podNames[podNames[i]]) } // Ensure the namespaces were anonymized correctly require.Equal(t, "namespace-1", anonymizer.namespaces["default"]) require.Equal(t, "namespace-2", anonymizer.namespaces["ambassador"]) } func Test_gatherLogsAnonymizeLogs(t *testing.T) { anonymizer := &anonymizer{ namespaces: map[string]string{ "default": "namespace-1", "ambassador": "namespace-2", }, // these names are specific because they come from the test data podNames: map[string]string{ "echo-auto-inject-6496f77cbd-n86nc.default": "pod-1.namespace-1", "traffic-manager-5c69859f94-g4ntj.ambassador": "traffic-manager.namespace-2", }, } testLogDir := "testdata/testLogDir" outputDir := t.TempDir() files := []string{"echo-auto-inject-6496f77cbd-n86nc", "traffic-manager-5c69859f94-g4ntj"} for _, file := range files { // The anonymize function edits files in place // so copy the files before we do that srcFile := fmt.Sprintf("%s/%s", testLogDir, file) dstFile := fmt.Sprintf("%s/%s", outputDir, file) err := copyFiles(dstFile, srcFile) require.NoError(t, err) err = anonymizer.anonymizeLog(dstFile) require.NoError(t, err) // Now verify things have actually been anonymized anonFile, err := os.ReadFile(dstFile) require.NoError(t, err) require.NotContains(t, string(anonFile), "echo-auto-inject") require.NotContains(t, string(anonFile), "default") require.NotContains(t, string(anonFile), "ambassador") // Both logs make reference to "echo-auto-inject" so we // validate that "pod-1" appears in both logs require.Contains(t, string(anonFile), "pod-1") } } func Test_gatherLogsSignificantPodNames(t *testing.T) { type testcase struct { name string podName string results []string } testCases := []testcase{ { name: "deploymentPod", podName: "echo-easy-867b648b88-zjsp2", results: []string{ "echo-easy-867b648b88-zjsp2", "echo-easy-867b648b88", "echo-easy", }, }, { name: "statefulSetPod", podName: "echo-easy-0", results: []string{ "echo-easy-0", "echo-easy", }, }, { name: "unknownName", podName: "notarealname", results: []string{}, }, { name: "followPatternNotFullName", podName: "a123b", results: []string{}, }, { name: "emptyName", podName: "", results: []string{}, }, } for _, tc := range testCases { tcName := tc.name // We need a fresh anonymizer for each test t.Run(tcName, func(t *testing.T) { sigPodNames := getSignificantPodNames(tc.podName) require.Equal(t, tc.results, sigPodNames) }) } } // ReadZip reads a zip file and returns the []byte string. Used in tests for // checking that a zipped file's contents are correct. Exported since it is // also used in telepresence_test.go. func ReadZip(zippedFile *zip.File) ([]byte, error) { fileReader, err := zippedFile.Open() if err != nil { return nil, err } fileContent, err := io.ReadAll(fileReader) if err != nil { return nil, err } return fileContent, nil } // checkZipEqual is a helper function for validating that the zippedFile in the // zip directory matches the file that was used to create the zip. func checkZipEqual(zippedFile *zip.File, srcLogDir string) (bool, error) { dstContent, err := ReadZip(zippedFile) if err != nil { return false, err } srcContent, err := os.ReadFile(fmt.Sprintf("%s/%s", srcLogDir, zippedFile.Name)) if err != nil { return false, err } return string(dstContent) == string(srcContent), nil } ================================================ FILE: pkg/client/cli/cmd/genyaml.go ================================================ package cmd import ( "context" "io" "log/slog" "os" "strings" "time" "github.com/go-json-experiment/json" "github.com/spf13/cobra" "github.com/spf13/pflag" apps "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/kubernetes" "sigs.k8s.io/yaml" argorollouts "github.com/datawire/argo-rollouts-go-client/pkg/client/clientset/versioned" "github.com/telepresenceio/telepresence/v2/pkg/agentconfig" "github.com/telepresenceio/telepresence/v2/pkg/agentmap" "github.com/telepresenceio/telepresence/v2/pkg/annotation" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/flags" "github.com/telepresenceio/telepresence/v2/pkg/errcat" "github.com/telepresenceio/telepresence/v2/pkg/k8sapi" ) type genYAMLCommand struct { outputFile string inputFile string configFile string workloadName string namespace string } func genYAML() *cobra.Command { info := genYAMLCommand{} cmd := &cobra.Command{ Use: "genyaml", Args: cobra.NoArgs, Short: "Generate YAML for use in kubernetes manifests.", Long: `Generate traffic-agent yaml for use in kubernetes manifests. This allows the traffic agent to be injected by hand into existing kubernetes manifests. For your modified workload to be valid, you'll have to manually inject annotations, a container, and a volume into the workload; you can do this by running "genyaml config", "genyaml container", "genyaml initcontainer", "genyaml annotations", and "genyaml volume". NOTE: It is recommended that you not do this unless strictly necessary. Instead, we suggest letting telepresence's webhook injector configure the traffic agents on demand.`, ValidArgsFunction: cobra.NoFileCompletions, } fs := cmd.PersistentFlags() fs.StringVarP(&info.outputFile, "output", "o", "-", "Path to the file to place the output in. Defaults to '-' which means stdout.") cmd.AddCommand( genConfigMapSubCommand(&info), genContainerSubCommand(&info), genInitContainerSubCommand(&info), genVAnnotationsSubCommand(&info), genVolumeSubCommand(&info), ) return cmd } func getInput(inputFile string) ([]byte, error) { var f io.ReadCloser if inputFile == "-" { f = os.Stdin } else { var err error if f, err = os.Open(inputFile); err != nil { return nil, errcat.User.Errorf(err, "unable to open input file %q", inputFile) } defer f.Close() } b, err := io.ReadAll(f) if err != nil { return nil, errcat.User.Errorf(err, "error reading from %s", inputFile) } return b, nil } func (i *genYAMLCommand) getOutputWriter() (io.WriteCloser, error) { if i.outputFile == "-" { return os.Stdout, nil } f, err := os.Create(i.outputFile) if err != nil { return nil, errcat.User.Errorf(err, "unable to open output file %s", i.outputFile) } return f, nil } func (i *genYAMLCommand) loadConfigMapEntry() (*agentconfig.Sidecar, error) { if i.configFile == "" { return nil, errcat.User.New("--agent must be provided") } b, err := getInput(i.configFile) if err != nil { return nil, err } var cfg agentconfig.Sidecar b, err = yaml.YAMLToJSON(b) if err == nil { err = json.Unmarshal(b, &cfg) } if err != nil { return nil, errcat.User.Newf("unable to parse config %s: %w", i.configFile, err) } return &cfg, nil } func (i *genYAMLCommand) loadWorkload(ctx context.Context) (k8sapi.Workload, error) { if i.inputFile == "" { if i.workloadName == "" { return nil, errcat.User.New("either --input or --workload must be provided") } return k8sapi.GetWorkload(ctx, i.workloadName, i.namespace, "") } b, err := getInput(i.inputFile) if err != nil { return nil, err } scheme := runtime.NewScheme() scheme.AddKnownTypes(schema.GroupVersion{Group: apps.GroupName, Version: "v1"}, &apps.StatefulSet{}, &apps.Deployment{}, &apps.ReplicaSet{}) codecFactory := serializer.NewCodecFactory(scheme) deserializer := codecFactory.UniversalDeserializer() obj, kind, err := deserializer.Decode(b, nil, nil) if err != nil { return nil, errcat.User.Newf("unable to parse yaml in %s: %w", i.inputFile, err) } wl, err := k8sapi.WrapWorkload(obj) if err != nil { return nil, errcat.User.Newf("unexpected object of kind %s; please pass in a Deployment, ReplicaSet, or StatefulSet", kind) } if wl.GetNamespace() == "" { if d, ok := k8sapi.DeploymentImpl(wl); ok { d.Namespace = i.namespace } else if r, ok := k8sapi.ReplicaSetImpl(wl); ok { r.Namespace = i.namespace } else if s, ok := k8sapi.StatefulSetImpl(wl); ok { s.Namespace = i.namespace } } return wl, nil } func (i *genYAMLCommand) writeObjToOutput(obj any) error { doc, err := json.Marshal(obj) if err == nil { doc, err = yaml.JSONToYAML(doc) } if err != nil { return errcat.User.Errorf(err, "unable to marshal agent container") } w, err := i.getOutputWriter() if err != nil { return err } defer w.Close() _, err = w.Write(doc) if err != nil { return errcat.User.Errorf(err, "unable to write to output %s", i.outputFile) } return nil } func (i *genYAMLCommand) WithJoinedClientSetInterface(ctx context.Context, flagMap map[string]string) (context.Context, error) { configFlags := genericclioptions.NewConfigFlags(false) fs := pflag.NewFlagSet("", 0) configFlags.AddFlags(fs) for k, v := range flagMap { if err := fs.Set(k, v); err != nil { return nil, errcat.User.Errorf(err, "error processing kubectl flag --%s=%s", k, v) } } configLoader := configFlags.ToRawKubeConfigLoader() restConfig, err := configLoader.ClientConfig() if err != nil { return nil, errcat.Config.New(err) } config, err := configLoader.RawConfig() if err != nil { return nil, errcat.Config.New(err) } if len(config.Contexts) == 0 { return nil, errcat.Config.New("kubeconfig has no context definition") } ctxName := flagMap["context"] if ctxName == "" { ctxName = config.CurrentContext } c, ok := config.Contexts[ctxName] if !ok { return nil, errcat.Config.Newf("context %q does not exist in the kubeconfig", ctxName) } i.namespace = flagMap["namespace"] if i.namespace == "" { i.namespace = c.Namespace if i.namespace == "" { i.namespace = "default" } } cs, err := kubernetes.NewForConfig(restConfig) if err != nil { return ctx, err } if acs, err := argorollouts.NewForConfig(restConfig); err == nil { return k8sapi.WithJoinedClientSetInterface(ctx, cs, acs), nil } return ctx, err } type genConfigMap struct { agentmap.GeneratorConfig *genYAMLCommand } func allKubeFlags() *pflag.FlagSet { kubeFlags := pflag.NewFlagSet("Kubernetes flags", 0) kubeConfig := genericclioptions.NewConfigFlags(false) kubeConfig.AddFlags(kubeFlags) return kubeFlags } func genConfigMapSubCommand(yamlInfo *genYAMLCommand) *cobra.Command { kubeFlags := allKubeFlags() info := genConfigMap{genYAMLCommand: yamlInfo} cmd := &cobra.Command{ Use: "config", Args: cobra.NoArgs, Short: "Generate YAML for the agent's entry in the telepresence-agents configmap.", Long: "Generate YAML for the agent's entry in the telepresence-agents configmap. See genyaml for more info on what this means", RunE: func(cmd *cobra.Command, args []string) error { return info.run(cmd, flags.Map(kubeFlags)) }, } fs := cmd.Flags() fs.StringVarP(&info.inputFile, "input", "i", "", "Path to the yaml containing the workload definition (i.e. Deployment, StatefulSet, etc). Pass '-' for stdin.. Mutually exclusive to --workload") fs.StringVarP(&info.workloadName, "workload", "w", "", "Name of the workload. If given, the workload will be retrieved from the cluster, mutually exclusive to --input") fs.Uint16Var(&info.AgentPort, "agent-port", 9900, "The port number you wish the agent to listen on.") fs.StringVar(&info.QualifiedAgentImage, "agent-image", "ghcr.io/telepresenceio/tel2:", `The qualified name of the agent image`) fs.Uint16Var(&info.ManagerPort, "manager-port", 8081, `The traffic-manager API port`) fs.StringVar(&info.ManagerNamespace, "manager-namespace", "ambassador", `The traffic-manager namespace`) fs.TextVar(&info.LogLevel, "loglevel", slog.LevelInfo, `The loglevel for the generated traffic-agent sidecar`) fs.AddFlagSet(kubeFlags) return cmd } func (g *genConfigMap) generateConfigMap(ctx context.Context, wl k8sapi.Workload) (*agentconfig.Sidecar, error) { g.WatchRetryInterval = 10 * time.Second ac, err := g.Generate(ctx, wl, nil) if err != nil { return nil, errcat.NoDaemonLogs.New(err) } return ac, nil } func (g *genConfigMap) run(cmd *cobra.Command, kubeFlags map[string]string) error { // Resolve the placeholder in the default agent image with the actual version. g.QualifiedAgentImage = strings.ReplaceAll(g.QualifiedAgentImage, "", client.Semver().FinalizeVersion()) ctx, err := g.WithJoinedClientSetInterface(cmd.Context(), kubeFlags) if err != nil { return err } wl, err := g.loadWorkload(ctx) if err != nil { return err } cfg, err := g.generateConfigMap(ctx, wl) if err != nil { return err } cfg.Manual = true return g.writeObjToOutput(cfg) } type genContainerInfo struct { *genYAMLCommand } func genContainerSubCommand(yamlInfo *genYAMLCommand) *cobra.Command { kubeFlags := allKubeFlags() info := genContainerInfo{genYAMLCommand: yamlInfo} cmd := &cobra.Command{ Use: "container", Args: cobra.NoArgs, Short: "Generate YAML for the traffic-agent container.", Long: "Generate YAML for the traffic-agent container. See genyaml for more info on what this means", RunE: func(cmd *cobra.Command, args []string) error { return info.run(cmd, flags.Map(kubeFlags)) }, } fs := cmd.Flags() fs.StringVarP(&info.inputFile, "input", "i", "", "Optional path to the yaml containing the workload definition (i.e. Deployment, StatefulSet, etc). Pass '-' for stdin. Loaded from cluster by default") fs.StringVarP(&info.configFile, "agent", "a", "", "Path to the yaml containing the generated agent config") fs.AddFlagSet(kubeFlags) return cmd } func (g *genContainerInfo) run(cmd *cobra.Command, kubeFlags map[string]string) error { ctx, err := g.WithJoinedClientSetInterface(cmd.Context(), kubeFlags) if err != nil { return err } cm, err := g.loadConfigMapEntry() if err != nil { return err } if g.inputFile == "" { g.workloadName = cm.WorkloadName } wl, err := g.loadWorkload(ctx) if err != nil { return err } // Sanity check if wl.GetName() != cm.WorkloadName { return errcat.User.Newf("name %q of loaded workload is different from %q loaded configmap entry", wl.GetName(), cm.WorkloadName) } if wl.GetKind() != cm.WorkloadKind { return errcat.User.Newf("kind %q of loaded workload is different from %q loaded configmap entry", wl.GetKind(), cm.WorkloadKind) } ab := agentconfig.ContainerBuilder{ Pod: wl.GetPodTemplate(), Config: cm, } agentContainer, _, err := ab.AgentContainer(ctx) if err != nil { return errcat.User.New(err) } return g.writeObjToOutput(agentContainer) } type genInitContainerInfo struct { *genYAMLCommand } func genInitContainerSubCommand(yamlInfo *genYAMLCommand) *cobra.Command { kubeFlags := allKubeFlags() info := genInitContainerInfo{genYAMLCommand: yamlInfo} cmd := &cobra.Command{ Use: "initcontainer", Args: cobra.NoArgs, Short: "Generate YAML for the traffic-agent init container.", Long: "Generate YAML for the traffic-agent init container. See genyaml for more info on what this means", RunE: func(cmd *cobra.Command, args []string) error { return info.run(cmd, flags.Map(kubeFlags)) }, } fs := cmd.Flags() fs.StringVarP(&info.configFile, "agent", "a", "", "Path to the yaml containing the generated agent config") fs.AddFlagSet(kubeFlags) return cmd } func (g *genInitContainerInfo) run(*cobra.Command, map[string]string) error { cm, err := g.loadConfigMapEntry() if err != nil { return err } for _, cc := range cm.Containers { for _, ic := range cc.Intercepts { if ic.Headless || ic.TargetPortNumeric { return g.writeObjToOutput(agentconfig.InitContainer(cm)) } } } return errcat.User.New("deployment does not need an init container") } type genAnnotationsInfo struct { *genYAMLCommand } func genVAnnotationsSubCommand(yamlInfo *genYAMLCommand) *cobra.Command { info := genAnnotationsInfo{genYAMLCommand: yamlInfo} cmd := &cobra.Command{ Use: "annotations", Args: cobra.NoArgs, Short: "Generate YAML for the pod template metadata annotations.", Long: "Generate YAML for the pod template metadata annotations. See genyaml for more info on what this means", RunE: func(*cobra.Command, []string) error { return info.run() }, } fs := cmd.Flags() fs.StringVarP(&info.configFile, "agent", "a", "", "Path to the yaml containing the generated agent config") return cmd } func (g *genAnnotationsInfo) run() error { cm, err := g.loadConfigMapEntry() if err != nil { return err } cmJSON, err := agentconfig.MarshalTight(cm) if err != nil { return err } anns := map[string]string{ annotation.InjectTrafficAgent: "enabled", annotation.ManuallyInjected: "true", annotation.Config: cmJSON, } return g.writeObjToOutput(anns) } type genVolumeInfo struct { *genYAMLCommand } func genVolumeSubCommand(yamlInfo *genYAMLCommand) *cobra.Command { info := genVolumeInfo{genYAMLCommand: yamlInfo} kubeFlags := allKubeFlags() cmd := &cobra.Command{ Use: "volume", Args: cobra.NoArgs, Short: "Generate YAML for the traffic-agent volume.", Long: "Generate YAML for the traffic-agent volume. See genyaml for more info on what this means", RunE: func(cmd *cobra.Command, args []string) error { return info.run() }, } fs := cmd.Flags() fs.StringVarP(&info.inputFile, "input", "i", "", "Optional path to the yaml containing the workload definition (i.e. Deployment, StatefulSet, etc). Pass '-' for stdin. Loaded from cluster by default") fs.StringVarP(&info.configFile, "agent", "a", "", "Path to the yaml containing the generated agent config") fs.AddFlagSet(kubeFlags) return cmd } func (g *genVolumeInfo) run() error { volumes, err := agentconfig.AgentVolumes(g.workloadName, nil) if err != nil { return err } return g.writeObjToOutput(&volumes) } ================================================ FILE: pkg/client/cli/cmd/helm.go ================================================ package cmd import ( "context" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/helm" "github.com/telepresenceio/telepresence/v2/pkg/ioutil" "github.com/telepresenceio/telepresence/v2/pkg/version" ) func helmCmd() *cobra.Command { cmd := &cobra.Command{ Use: "helm command [flags]", Short: `Helm commands using the embedded Telepresence Helm chart.`, } cmd.AddCommand(helmInstall(), helmUpgrade(), helmUninstall(), helmLint(), helmVersion()) return cmd } type HelmCommand struct { helm.Request AllValues map[string]any rq *daemon.CobraRequest } var ( HelmInstallExtendFlagsFunc func(*pflag.FlagSet) //nolint:gochecknoglobals // extension point HelmInstallPrologFunc func(context.Context, *pflag.FlagSet, *HelmCommand) error //nolint:gochecknoglobals // extension point ) func helmInstall() *cobra.Command { var upgrade bool ha := &HelmCommand{ Request: helm.Request{ Type: helm.Install, }, } cmd := &cobra.Command{ Use: "install", Args: cobra.NoArgs, Short: "Install telepresence traffic manager", RunE: func(cmd *cobra.Command, args []string) error { if upgrade { ha.Request.Type = helm.Upgrade } return ha.run(cmd, args) }, ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() flags.BoolVarP(&ha.NoHooks, "no-hooks", "", false, "prevent hooks from running during install") flags.BoolVarP(&upgrade, "upgrade", "u", false, "replace the traffic manager if it already exists") flags.BoolVar(&ha.CreateNamespace, "create-namespace", true, "create a namespace for the traffic-manager if not present") flags.StringVar(&ha.Version, "version", "", "the telepresence version if different from the client's version. May be a range (e.g. ^2.21.0)") ha.addValueSettingFlags(flags) uf := flags.Lookup("upgrade") uf.Hidden = true uf.Deprecated = `Use "telepresence helm upgrade" instead of "telepresence helm install --upgrade"` ha.rq = daemon.InitRequest(cmd) return cmd } func helmUpgrade() *cobra.Command { ha := &HelmCommand{ Request: helm.Request{ Type: helm.Upgrade, }, } cmd := &cobra.Command{ Use: "upgrade", Args: cobra.NoArgs, Short: "Upgrade telepresence traffic manager", RunE: ha.run, } flags := cmd.Flags() ha.addValueSettingFlags(flags) flags.BoolVarP(&ha.NoHooks, "no-hooks", "", false, "disable pre/post upgrade hooks") flags.BoolVarP(&ha.ResetValues, "reset-values", "", false, "when upgrading, reset the values to the ones built into the chart") flags.BoolVarP(&ha.ReuseValues, "reuse-values", "", false, "when upgrading, reuse the last release's values and merge in any overrides from the command line via --set and -f") flags.BoolVarP(&ha.CreateNamespace, "create-namespace", "", true, "create the release namespace if not present") flags.StringVar(&ha.Version, "version", "", "the telepresence version if different from the client's version. May be a range (e.g. ^2.21.0)") ha.rq = daemon.InitRequest(cmd) return cmd } func (ha *HelmCommand) addValueSettingFlags(flags *pflag.FlagSet) { flags.StringArrayVarP(&ha.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL (can specify multiple)") flags.StringArrayVarP(&ha.Values, "set", "", []string{}, "specify a value as a.b=v (can specify multiple or separate values with commas: a.b=v1,a.c=v2)") flags.StringArrayVarP(&ha.FileValues, "set-file", "", []string{}, "set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)") flags.StringArrayVarP(&ha.JSONValues, "set-json", "", []string{}, "set JSON values on the command line (can specify multiple or separate values with commas: a.b=jsonval1,a.c=jsonval2)") flags.StringArrayVarP(&ha.StringValues, "set-string", "", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: a.b=val1,a.c=val2)") if HelmInstallExtendFlagsFunc != nil { HelmInstallExtendFlagsFunc(flags) } } func helmUninstall() *cobra.Command { ha := &HelmCommand{ Request: helm.Request{ Type: helm.Uninstall, }, } cmd := &cobra.Command{ Use: "uninstall", Args: cobra.NoArgs, Short: "Uninstall telepresence traffic manager", RunE: ha.run, } flags := cmd.Flags() flags.BoolVarP(&ha.NoHooks, "no-hooks", "", false, "prevent hooks from running during uninstallation") ha.rq = daemon.InitRequest(cmd) return cmd } func helmLint() *cobra.Command { ha := &HelmCommand{ Request: helm.Request{ Type: helm.Lint, }, } cmd := &cobra.Command{ Use: "lint", Args: cobra.NoArgs, Short: "Verify the embedded telepresence Helm chart", RunE: func(cmd *cobra.Command, args []string) error { return ha.run(cmd, args) }, ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() flags.StringVar(&ha.Version, "version", "", "the telepresence version if different from the client's version. May be a range (e.g. ^2.21.0)") ha.addValueSettingFlags(flags) ha.rq = daemon.InitRequest(cmd) return cmd } func helmVersion() *cobra.Command { return &cobra.Command{ Use: "version", Args: cobra.NoArgs, Short: "Print the version of the Helm client", RunE: func(cmd *cobra.Command, _ []string) (err error) { ioutil.Println(cmd.OutOrStdout(), version.HelmVersion) return nil }, ValidArgsFunction: cobra.NoFileCompletions, } } func (ha *HelmCommand) Type() helm.RequestType { return ha.Request.Type } func (ha *HelmCommand) run(cmd *cobra.Command, _ []string) (err error) { if err = ha.rq.CommitFlags(cmd); err != nil { return err } ctx := cmd.Context() if HelmInstallPrologFunc != nil { if err = HelmInstallPrologFunc(ctx, cmd.Flags(), ha); err != nil { return err } } return ha.Run(ctx, ha.rq.ConnectRequest) } ================================================ FILE: pkg/client/cli/cmd/ingest.go ================================================ package cmd import ( "github.com/spf13/cobra" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/ann" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/ingest" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/intercept" ) func ingestCmd() *cobra.Command { ic := &ingest.Command{} cmd := &cobra.Command{ Use: "ingest [flags] [-- [[docker run flags] ] OR []] args...]", Args: cobra.MinimumNArgs(1), Short: "Ingest a container", Annotations: map[string]string{ ann.Session: ann.Required, ann.UpdateCheckFormat: ann.Tel2, }, SilenceUsage: true, SilenceErrors: true, RunE: ic.Run, ValidArgsFunction: intercept.ValidArgs, // a list that this command shares with intercept } ic.AddFlags(cmd) return cmd } ================================================ FILE: pkg/client/cli/cmd/intercept.go ================================================ package cmd import ( "github.com/spf13/cobra" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/ann" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/intercept" ) func interceptCmd() *cobra.Command { ic := &intercept.Command{} cmd := &cobra.Command{ Use: "intercept [flags] [-- [[docker run flags] ] OR []] args...]", Args: cobra.MinimumNArgs(1), Short: "Intercept a service", Annotations: map[string]string{ ann.Session: ann.Required, ann.UpdateCheckFormat: ann.Tel2, }, SilenceUsage: true, SilenceErrors: true, RunE: ic.Run, ValidArgsFunction: intercept.ValidArgs, } ic.AddInterceptFlags(cmd) return cmd } ================================================ FILE: pkg/client/cli/cmd/kubeauth.go ================================================ package cmd import ( "fmt" "os" "github.com/spf13/cobra" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "github.com/telepresenceio/clog" rpc "github.com/telepresenceio/telepresence/rpc/v2/authenticator" ) func kubeauthCmd() *cobra.Command { cmd := &cobra.Command{ Use: "kubeauth", Args: cobra.ExactArgs(2), Short: "Resolve kubeconfig context using gRPC to the kubeauth daemon", RunE: authenticateContext, Hidden: true, ValidArgsFunction: cobra.NoFileCompletions, } return cmd } func authenticateContext(cmd *cobra.Command, args []string) (err error) { ctx := cmd.Context() contextName := args[0] serverAddr := args[1] defer func() { if err != nil { clog.Error(ctx, err) } }() var conn *grpc.ClientConn if conn, err = grpc.NewClient(serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials())); err != nil { return fmt.Errorf("failed to dial GRPC server %s: %w", serverAddr, err) } defer conn.Close() ac := rpc.NewAuthenticatorClient(conn) var res *rpc.GetContextExecCredentialsResponse if res, err = ac.GetContextExecCredentials(ctx, &rpc.GetContextExecCredentialsRequest{ContextName: contextName}); err != nil { return fmt.Errorf("failed to get exec credentials: %w", err) } if _, err = os.Stdout.Write(res.RawCredentials); err != nil { err = fmt.Errorf("failed to print raw credentials: %w", err) } return err } ================================================ FILE: pkg/client/cli/cmd/leave.go ================================================ package cmd import ( "context" "fmt" "strings" "github.com/spf13/cobra" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/rpc/v2/connector" "github.com/telepresenceio/telepresence/rpc/v2/manager" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/ann" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/connect" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/progress" "github.com/telepresenceio/telepresence/v2/pkg/client/docker" "github.com/telepresenceio/telepresence/v2/pkg/errcat" "github.com/telepresenceio/telepresence/v2/pkg/grpc" ) func leaveCmd() *cobra.Command { var containerName string cmd := &cobra.Command{ Use: "leave [flags] ", Args: cobra.ExactArgs(1), Short: "Remove existing intercept", Annotations: map[string]string{ ann.Session: ann.Required, }, RunE: func(cmd *cobra.Command, args []string) error { if err := connect.InitCommand(cmd); err != nil { return err } defer progress.Stop(cmd.Context()) return disengage(cmd.Context(), strings.TrimSpace(args[0]), containerName) }, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { shellCompDir := cobra.ShellCompDirectiveNoFileComp if len(args) != 0 { return nil, shellCompDir } if err := connect.InitCommand(cmd); err != nil { return nil, shellCompDir | cobra.ShellCompDirectiveError } ctx := cmd.Context() userD := daemon.MustGetUserClient(ctx) resp, err := userD.List(ctx, &connector.ListRequest{ Filter: connector.ListRequest_INTERCEPTS | connector.ListRequest_REPLACEMENTS | connector.ListRequest_INGESTS, }) if err != nil { return nil, shellCompDir | cobra.ShellCompDirectiveError } if len(resp.Workloads) == 0 { return nil, shellCompDir } var completions []string for _, wl := range resp.Workloads { for _, ii := range wl.InterceptInfo { name := ii.Spec.Name if strings.HasPrefix(name, toComplete) { completions = append(completions, name) } } for _, ig := range wl.IngestInfo { name := ig.Workload if strings.HasPrefix(name, toComplete) { completions = append(completions, name) } } } return completions, shellCompDir }, } cmd.Flags().StringVarP(&containerName, "container", "c", "", "Container name") return cmd } func disengage(ctx context.Context, name, container string) error { userD := daemon.MustGetUserClient(ctx) var ic *manager.InterceptInfo var ig *connector.IngestInfo var env map[string]string var err error icName := name if container != "" { icName += "/" + container } ic, err = userD.GetIntercept(ctx, &manager.GetInterceptRequest{Name: icName}) if err != nil && status.Code(err) != codes.NotFound { return grpc.FromGRPC(err) } if ic == nil { ig, err = userD.GetIngest(ctx, &connector.IngestIdentifier{ WorkloadName: name, ContainerName: container, }) if err != nil { if status.Code(err) != codes.NotFound { return grpc.FromGRPC(err) } // User probably misspelled the name of the replace/intercept/ingest msg := fmt.Sprintf("Found no replace, intercept, or ingest named %q", name) if container != "" { msg = fmt.Sprintf("%s with container %q", msg, container) } return errcat.User.New(msg) } env = ig.Environment } else { env = ic.Environment } if userD.DaemonID().Containerized { handlerContainer, stopContainer := env["TELEPRESENCE_HANDLER_CONTAINER_NAME"] if stopContainer { // Stop the handler's container. The daemon is most likely running in another // container, and won't be able to. err = docker.StopContainer(ctx, handlerContainer) if err != nil { clog.Error(ctx, err) } } } if ic != nil { _, err = userD.RemoveIntercept(ctx, &manager.RemoveInterceptRequest2{Name: ic.Spec.Name}) } else { _, err = userD.LeaveIngest(ctx, &connector.IngestIdentifier{ WorkloadName: ig.Workload, ContainerName: ig.Container, }) } return grpc.FromGRPC(err) } ================================================ FILE: pkg/client/cli/cmd/list.go ================================================ package cmd import ( "context" "io" "strings" "github.com/spf13/cobra" "google.golang.org/grpc" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/rpc/v2/connector" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/ann" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/connect" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/intercept" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/output" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/progress" "github.com/telepresenceio/telepresence/v2/pkg/errcat" tpGrpc "github.com/telepresenceio/telepresence/v2/pkg/grpc" "github.com/telepresenceio/telepresence/v2/pkg/ioutil" ) const ( includeIntercepts = iota includeIngests includeReplacements includeWiretaps ) type listCommand struct { inclusions [4]bool onlyAgents bool debug bool namespace string watch bool } func list() *cobra.Command { s := &listCommand{} cmd := &cobra.Command{ Use: "list", Args: cobra.NoArgs, Short: "List current intercepts", RunE: s.list, Annotations: map[string]string{ ann.Session: ann.Required, }, ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() flags.BoolVarP(&s.inclusions[includeIntercepts], "intercepts", "i", false, "intercepts") flags.BoolVarP(&s.inclusions[includeIngests], "ingests", "g", false, "ingests") flags.BoolVarP(&s.inclusions[includeReplacements], "replacements", "r", false, "replacements") flags.BoolVarP(&s.inclusions[includeWiretaps], "wiretaps", "t", false, "wiretaps") flags.BoolVarP(&s.onlyAgents, "agents", "a", false, "with installed agents only") flags.BoolVar(&s.debug, "debug", false, "include debugging information") flags.StringVarP(&s.namespace, "namespace", "n", "", "If present, the namespace scope for this CLI request") flags.BoolP("only-interceptable", "o", false, "") of := flags.Lookup("only-interceptable") of.Hidden = true of.Deprecated = "Redundant since all workloads are eligible for ingest, intercept, or replace" flags.BoolVarP(&s.watch, "watch", "w", false, "watch a namespace. --agents and --intercepts are disabled if this flag is set") wf := flags.Lookup("watch") wf.Hidden = true wf.Deprecated = `Use "--output json-stream" instead of "--watch"` _ = cmd.RegisterFlagCompletionFunc("namespace", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { shellCompDir := cobra.ShellCompDirectiveNoFileComp if err := connect.InitCommand(cmd); err != nil { shellCompDir |= cobra.ShellCompDirectiveError return nil, shellCompDir } ctx := cmd.Context() userD := daemon.MustGetUserClient(ctx) resp, err := userD.GetNamespaces(ctx, &connector.GetNamespacesRequest{ ForClientAccess: false, Prefix: toComplete, }) if err != nil { clog.Debugf(cmd.Context(), "error getting namespaces: %v", err) shellCompDir |= cobra.ShellCompDirectiveError return nil, shellCompDir } return resp.Namespaces, shellCompDir }) return cmd } type watchWorkloadStreamResponse struct { workloadInfoSnapshot *connector.WorkloadInfoSnapshot err error } // list requests a list current intercepts from the daemon. func (s *listCommand) list(cmd *cobra.Command, _ []string) error { if err := connect.InitCommand(cmd); err != nil { return err } defer progress.Stop(cmd.Context()) stdout := cmd.OutOrStdout() ctx := cmd.Context() userD := daemon.MustGetUserClient(ctx) filter := connector.ListRequest_UNSPECIFIED for i := range s.inclusions { if s.inclusions[i] { switch i { case includeIntercepts: filter |= connector.ListRequest_INTERCEPTS case includeReplacements: filter |= connector.ListRequest_REPLACEMENTS case includeIngests: filter |= connector.ListRequest_INGESTS case includeWiretaps: filter |= connector.ListRequest_WIRETAPS } } } if filter == connector.ListRequest_UNSPECIFIED && s.onlyAgents { filter = connector.ListRequest_INSTALLED_AGENTS } cfg := client.GetConfig(ctx) maxRecSize := int64(1024 * 1024 * 20) // Default to 20 Mb here. List can be quit long. if mz := cfg.Grpc().MaxReceiveSize(); mz > 0 { if mz > maxRecSize { maxRecSize = mz } } formattedOutput := output.WantsFormatted(cmd) if !output.WantsStream(cmd) { r, err := userD.List(ctx, &connector.ListRequest{Filter: filter, Namespace: s.namespace}, grpc.MaxCallRecvMsgSize(int(maxRecSize))) if err != nil { return tpGrpc.FromGRPC(err) } s.printList(ctx, r.Workloads, stdout, formattedOutput) return nil } stream, streamErr := userD.WatchWorkloads(ctx, &connector.WatchWorkloadsRequest{Namespaces: []string{s.namespace}}, grpc.MaxCallRecvMsgSize(int(maxRecSize))) if streamErr != nil { return tpGrpc.FromGRPC(streamErr) } ch := make(chan *watchWorkloadStreamResponse) go func() { for { snap, err := stream.Recv() ch <- &watchWorkloadStreamResponse{ workloadInfoSnapshot: snap, err: err, } if err != nil { close(ch) break } } }() for { select { case r, ok := <-ch: if !ok { return nil } if r.err != nil { return errcat.NoDaemonLogs.Newf("%v", r.err) } s.printList(ctx, r.workloadInfoSnapshot.Workloads, stdout, formattedOutput) case <-ctx.Done(): return nil } } } func (s *listCommand) printList(ctx context.Context, workloads []*connector.WorkloadInfo, stdout io.Writer, formattedOut bool) { if len(workloads) == 0 { if formattedOut { output.Object(ctx, []struct{}{}, false) } else { ioutil.Println(stdout, "No Workloads (Deployments, StatefulSets, ReplicaSets, or Rollouts)") } return } state := func(workload *connector.WorkloadInfo) string { if iis, igs := workload.InterceptInfo, workload.IngestInfo; len(iis)+len(igs) > 0 { return intercept.DescribeIntercepts(ctx, iis, igs, nil, s.debug) } if workload.NotInterceptableReason == "Progressing" { return "progressing..." } if workload.AgentVersion != "" { return "ready to engage (traffic-agent already installed)" } if workload.NotInterceptableReason != "" { return "unable to engage (traffic-agent not installed): " + workload.NotInterceptableReason } else { return "ready to engage (traffic-agent not yet installed)" } } if formattedOut { output.Object(ctx, workloads, false) } else { includeNs := false ns := s.namespace for _, dep := range workloads { depNs := dep.Namespace if ns != "" && depNs != ns { includeNs = true break } ns = depNs } typeLen := 0 nameLen := 0 for _, dep := range workloads { n := dep.WorkloadResourceType nl := len(n) if nl > typeLen { typeLen = nl } n = dep.Name nl = len(n) if includeNs { nl += len(dep.Namespace) + 1 } if nl > nameLen { nameLen = nl } } for _, workload := range workloads { t := workload.WorkloadResourceType n := workload.Name if includeNs { n += "." + workload.Namespace } ioutil.Printf(stdout, "%-*s %-*s: %s\n", typeLen, strings.ToLower(t), nameLen, n, state(workload)) } } } ================================================ FILE: pkg/client/cli/cmd/list_contexts.go ================================================ package cmd import ( "github.com/spf13/cobra" "k8s.io/client-go/tools/clientcmd/api" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/output" "github.com/telepresenceio/telepresence/v2/pkg/ioutil" ) type listContextsCommand struct { rq *daemon.CobraRequest } func listContexts() *cobra.Command { lcc := &listContextsCommand{} cmd := &cobra.Command{ Use: "list-contexts", Args: cobra.NoArgs, Short: "Show all contexts", RunE: lcc.run, ValidArgsFunction: cobra.NoFileCompletions, } lcc.rq = daemon.InitRequest(cmd) return cmd } type kubeCtx struct { *api.Context `yaml:",inline"` Current bool `json:"current,omitempty"` } func (lcc *listContextsCommand) run(cmd *cobra.Command, _ []string) error { config, err := lcc.rq.GetConfig(cmd) if err != nil { return err } ctx := cmd.Context() cm := make(map[string]kubeCtx, len(config.Contexts)) for n, c := range config.Contexts { cm[n] = kubeCtx{Context: c, Current: n == config.CurrentContext} } if output.WantsFormatted(cmd) { output.Object(ctx, cm, false) } else { for n, c := range cm { pfx := '-' if c.Current { pfx = '*' } ioutil.Printf(output.Out(ctx), "%c name: %s\n default namespace: %s\n", pfx, n, c.Namespace) } } return nil } ================================================ FILE: pkg/client/cli/cmd/list_namespaces.go ================================================ package cmd import ( "fmt" "github.com/spf13/cobra" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/output" ) type listNamespacesCommand struct { rq *daemon.CobraRequest } func listNamespaces() *cobra.Command { lnc := &listNamespacesCommand{} cmd := &cobra.Command{ Use: "list-namespaces", Args: cobra.NoArgs, Short: "Show all namespaces", RunE: lnc.run, ValidArgsFunction: cobra.NoFileCompletions, } lnc.rq = daemon.InitRequest(cmd) return cmd } func (lnc *listNamespacesCommand) run(cmd *cobra.Command, _ []string) error { nss, err := lnc.rq.GetAllNamespaces(cmd) if err != nil { return err } ctx := cmd.Context() if output.WantsFormatted(cmd) { output.Object(ctx, nss, false) } else { for _, ns := range nss { fmt.Fprintln(output.Out(ctx), ns) } } return nil } ================================================ FILE: pkg/client/cli/cmd/loglevel.go ================================================ package cmd import ( "errors" "fmt" "strings" "time" "github.com/spf13/cobra" "google.golang.org/protobuf/types/known/durationpb" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/rpc/v2/connector" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/ann" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/connect" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/progress" "github.com/telepresenceio/telepresence/v2/pkg/errcat" "github.com/telepresenceio/telepresence/v2/pkg/grpc" ) const defaultDuration = 30 * time.Minute type logLevelCommand struct { duration time.Duration localOnly bool remoteOnly bool } func logLevelArg(cmd *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("accepts exactly one argument (the log level)") } _, err := clog.ParseLevel(args[0]) if err != nil { return err } return nil } func loglevel() *cobra.Command { lls := logLevelCommand{} cmd := &cobra.Command{ Use: fmt.Sprintf("loglevel <%s>", strings.Join(clog.LevelStrings, ",")), Args: logLevelArg, Short: "Temporarily change the log-level of the traffic-manager, traffic-agent, and user and root daemons", RunE: lls.setTempLogLevel, ValidArgs: clog.LevelStrings, Annotations: map[string]string{ ann.Session: ann.Required, }, } flags := cmd.Flags() flags.DurationVarP(&lls.duration, "duration", "d", defaultDuration, "The time that the log-level will be in effect (0s means indefinitely)") flags.BoolVarP(&lls.localOnly, "local-only", "l", false, "Only affect the user and root daemons") flags.BoolVarP(&lls.remoteOnly, "remote-only", "r", false, "Only affect the traffic-manager and traffic-agents") return cmd } func (lls *logLevelCommand) setTempLogLevel(cmd *cobra.Command, args []string) error { rq := &connector.LogLevelRequest{LogLevel: args[0], Duration: durationpb.New(lls.duration)} switch { case lls.localOnly && lls.remoteOnly: return errcat.User.New("the local-only and remote-only options are mutually exclusive") case lls.localOnly: rq.Scope = connector.LogLevelRequest_LOCAL_ONLY case lls.remoteOnly: rq.Scope = connector.LogLevelRequest_REMOTE_ONLY } if err := connect.InitCommand(cmd); err != nil { return err } defer progress.Stop(cmd.Context()) ctx := cmd.Context() userD := daemon.MustGetUserClient(ctx) _, err := userD.SetLogLevel(ctx, rq) return grpc.FromGRPC(err) } ================================================ FILE: pkg/client/cli/cmd/man-pages.go ================================================ package cmd import ( "bytes" "fmt" "os" "strings" "github.com/spf13/cobra" ) func manPages() *cobra.Command { var dir string cmd := &cobra.Command{ Use: "man-pages", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return genMarkdown(cmd.Parent(), dir) }, Hidden: true, SilenceErrors: true, SilenceUsage: true, } flags := cmd.Flags() flags.StringVar(&dir, "dir", "/tmp", "Directory to write the manual page to") return cmd } func genMarkdown(cmd *cobra.Command, dir string) error { err := os.MkdirAll(dir, 0o755) if err != nil { return err } buf := &bytes.Buffer{} return genCommandMarkdown(cmd, dir, buf) } func entityEscape(s string, w *bytes.Buffer) { for _, c := range s { switch c { case '&': w.WriteString("&") case '<': w.WriteString("<") case '>': w.WriteString(">") case '"': w.WriteString(""") default: w.WriteRune(c) } } } // GenMarkdownCustom creates custom markdown output. func genCommandMarkdown(cmd *cobra.Command, dir string, buf *bytes.Buffer) error { cmd.InitDefaultHelpFlag() // Create a font-matter header. buf.WriteString("---\ntitle: ") buf.WriteString(cmd.CommandPath()) buf.WriteByte('\n') if cmd.Short != "" { buf.WriteString("description: ") entityEscape(cmd.Short, buf) buf.WriteByte('\n') } buf.WriteString("hide_table_of_contents: true\n---\n\n") if cmd.Short != "" { entityEscape(cmd.Short, buf) buf.WriteString("\n\n") } if cmd.Long != "" { buf.WriteString("## Synopsis:\n\n") entityEscape(cmd.Long, buf) buf.WriteString("\n\n") } entityEscape(cmd.UsageString(), buf) err := os.WriteFile(fmt.Sprintf("%s/%s.md", dir, strings.ReplaceAll(cmd.CommandPath(), " ", "_")), buf.Bytes(), 0o644) if err != nil { return err } for _, c := range cmd.Commands() { if c.Hidden || c.Name() == "help" { continue } buf.Reset() err = genCommandMarkdown(c, dir, buf) if err != nil { return err } } return nil } ================================================ FILE: pkg/client/cli/cmd/mcp.go ================================================ package cmd import ( "github.com/njayp/ophis" "github.com/spf13/cobra" ) func mcp() *cobra.Command { return ophis.Command(&ophis.Config{ Selectors: []ophis.Selector{ { CmdSelector: ophis.AllowCmds("telepresence connect"), // allow all local flags except kubeflags LocalFlagSelector: ophis.ExcludeFlags( "as", "as-group", "as-uid", "cache-dir", "certificate-authority", "client-certificate", "client-key", "cluster", "context", "disable-compression", "insecure-skip-tls-verify", "kubeconfig", "request-timeout", "server", "tls-server-name", "token", "user", ), InheritedFlagSelector: ophis.NoFlags, }, { CmdSelector: ophis.AllowCmds( "telepresence quit", "telepresence status", ), // no local or global flags LocalFlagSelector: ophis.NoFlags, InheritedFlagSelector: ophis.NoFlags, }, { CmdSelector: ophis.AllowCmds( "telepresence intercept", "telepresence ingest", "telepresence leave", "telepresence list", "telepresence wiretap", "telepresence replace", ), // allow local flags // allow global output flag for `--detailed-output` and `--output json` combo InheritedFlagSelector: ophis.AllowFlags("output"), }, }, }) } ================================================ FILE: pkg/client/cli/cmd/mcp_test.go ================================================ package cmd import ( "testing" "github.com/njayp/ophis/test" ) func TestMCP(t *testing.T) { ctx := WithSubCommands(t.Context()) cmd := Telepresence(ctx, nil) tools := test.GetTools(t, cmd) test.ToolNames(t, tools, "telepresence_quit", "telepresence_status", "telepresence_connect", "telepresence_intercept", "telepresence_ingest", "telepresence_leave", "telepresence_list", "telepresence_wiretap", "telepresence_replace", ) } ================================================ FILE: pkg/client/cli/cmd/quit.go ================================================ package cmd import ( "github.com/spf13/cobra" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/ann" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/connect" ) func quit() *cobra.Command { quitDaemons := false cmd := &cobra.Command{ Use: "quit", Args: cobra.NoArgs, Short: "Tell telepresence daemons to quit", RunE: func(cmd *cobra.Command, _ []string) error { if quitDaemons { connect.InitProgressWriter(cmd) connect.Quit(cmd.Context()) } else { cmd.Annotations = map[string]string{ann.UserDaemon: ann.Optional} if err := connect.InitCommand(cmd); err != nil { return err } connect.Disconnect(cmd.Context()) } return nil }, ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() flags.BoolVarP(&quitDaemons, "stop-daemons", "s", false, "stop all local telepresence daemons") return cmd } ================================================ FILE: pkg/client/cli/cmd/replace.go ================================================ package cmd import ( "github.com/spf13/cobra" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/ann" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/intercept" ) func replaceCmd() *cobra.Command { ic := &intercept.Command{} cmd := &cobra.Command{ Use: "replace [flags] [-- [[docker run flags] ] OR []] args...]", Args: cobra.MinimumNArgs(1), Short: "Replace a container", Annotations: map[string]string{ ann.Session: ann.Required, ann.UpdateCheckFormat: ann.Tel2, }, SilenceUsage: true, SilenceErrors: true, RunE: ic.RunReplace, ValidArgsFunction: intercept.ValidArgs, } ic.AddReplaceFlags(cmd) return cmd } ================================================ FILE: pkg/client/cli/cmd/revoke.go ================================================ package cmd import ( "context" "strings" "github.com/spf13/cobra" "github.com/telepresenceio/telepresence/rpc/v2/connector" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/ann" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/connect" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/progress" "github.com/telepresenceio/telepresence/v2/pkg/errcat" "github.com/telepresenceio/telepresence/v2/pkg/grpc" ) func revokeCmd() *cobra.Command { cmd := &cobra.Command{ Use: "revoke ", Args: cobra.ExactArgs(1), Short: "Revoke an intercept by intercept ID. The intercept ID must be in the format :", Long: `Revoke an intercept by intercept ID. This is an administrative operation that requires RBAC permissions to modify the "traffic-manager" configmap.`, Annotations: map[string]string{ ann.Session: ann.Required, }, RunE: func(cmd *cobra.Command, args []string) error { if err := connect.InitCommand(cmd); err != nil { return err } defer progress.Stop(cmd.Context()) return revokeIntercept(cmd.Context(), strings.TrimSpace(args[0])) }, } return cmd } func revokeIntercept(ctx context.Context, interceptID string) error { if interceptID == "" { return errcat.User.New("intercept_id cannot be empty") } userD := daemon.MustGetUserClient(ctx) _, err := userD.RevokeIntercept(ctx, &connector.RevokeInterceptRequest{ InterceptId: interceptID, }) return grpc.FromGRPC(err) } ================================================ FILE: pkg/client/cli/cmd/serve.go ================================================ package cmd import ( "context" "fmt" "net" "net/netip" "net/url" "os" "sync" "time" "github.com/pkg/browser" "github.com/spf13/cobra" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/ann" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/connect" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/progress" "github.com/telepresenceio/telepresence/v2/pkg/client/docker" "github.com/telepresenceio/telepresence/v2/pkg/errcat" "github.com/telepresenceio/telepresence/v2/pkg/ioutil" "github.com/telepresenceio/telepresence/v2/pkg/sigctx" ) type serveCommand struct { port uint16 } func serveCmd() *cobra.Command { sc := &serveCommand{} cmd := &cobra.Command{ Use: "serve ", Args: cobra.ExactArgs(1), Short: "Start the browser on a remote service", Annotations: map[string]string{ ann.Session: ann.Required, }, SilenceUsage: true, SilenceErrors: true, RunE: sc.run, } sc.addFlags(cmd) return cmd } func (sc *serveCommand) addFlags(cmd *cobra.Command) { fs := cmd.Flags() fs.Uint16VarP(&sc.port, "port", "p", 80, "service port") } func (sc *serveCommand) run(cmd *cobra.Command, args []string) error { svc := args[0] if len(svc) == 0 { return errcat.User.New("an empty string is never a valid service name") } err := connect.InitCommand(cmd) if err != nil { return err } return sigctx.DoWithSignalHandler(cmd.Context(), func(ctx context.Context) error { uc := daemon.MustGetUserClient(ctx) ip, err := uc.Lookup(ctx, svc) if err != nil { return err } if uc.Containerized() { err = sc.serveFromContainer(ctx, ip) } else { err = sc.serveFromHost(ctx, ip) } return err }) } const ( browserProgressID = "Web-browser" socatProgressID = "Port-forward" ) func (sc *serveCommand) serveFromContainer(ctx context.Context, addr netip.Addr) error { // We can't reliably just map a service port (typically port 80) to localhost, so instead of doing // that, we create a random port and use that. ps, err := ioutil.FreePortsTCP(1) if err != nil { return err } rndPort := ps[0].Port() ap := netip.AddrPortFrom(addr, sc.port) progress.Start(ctx, "Serving web browser from container") defer progress.Stop(ctx) ctx = progress.WithEventId(ctx, socatProgressID) progress.Workingf(ctx, "Starting port-forward %d:%s", rndPort, ap) cni, cc, err := docker.Start(ctx, true, "-p", fmt.Sprintf("%d:%d", rndPort, rndPort), "--rm", "alpine/socat", fmt.Sprintf("TCP-LISTEN:%d,fork", rndPort), fmt.Sprintf("TCP:%s", ap)) if err != nil { return errcat.User.New(err) } progress.Workingf(ctx, "Started port-forward %d:%s", rndPort, ap) go func() { <-ctx.Done() _ = docker.StopContainer(context.WithoutCancel(ctx), cni.ID) }() proto := "http" on, err := url.Parse(fmt.Sprintf("%s://%s", proto, net.JoinHostPort("localhost", fmt.Sprintf("%d", rndPort)))) if err != nil { return errcat.User.New(err) } wg := &sync.WaitGroup{} wg.Add(1) sc.openBrowser(progress.WithEventId(ctx, browserProgressID), on, wg) err = cc.Wait() if err != nil && ctx.Err() == nil { return errcat.NoDaemonLogs.New(cc.Wait()) } wg.Wait() progress.Donef(ctx, "Stopped port-forward %d:%s", rndPort, ap) return nil } func (sc *serveCommand) serveFromHost(ctx context.Context, addr netip.Addr) error { proto := "http" ap := netip.AddrPortFrom(addr, sc.port) on, err := url.Parse(fmt.Sprintf("%s://%s", proto, ap)) if err != nil { return errcat.User.New(err) } progress.Start(ctx, "Serving web browser from host") defer progress.Stop(ctx) wg := &sync.WaitGroup{} wg.Add(1) sc.openBrowser(progress.WithEventId(ctx, browserProgressID), on, wg) <-ctx.Done() wg.Wait() return nil } func (sc *serveCommand) openBrowser(ctx context.Context, on *url.URL, wg *sync.WaitGroup) { onStr := on.String() working := progress.Workingf(ctx, "Opening on %s", onStr) // The browser might not open an existing session, in which case the OpenURL call will wait for the browser to close. // We don't want to wait here regardless, so we use a separate go-routine to start it and produce initial output on stdout/stderr. // This context is canceled either due to a quickly returning OpenURL (existing session) or after a second when // the context times out. ctx, cancel := context.WithTimeout(ctx, time.Second) go func() { defer func() { wg.Done() cancel() browser.Stderr = os.Stderr browser.Stdout = os.Stdout }() browser.Stderr = working.Pump(ctx, progress.EventStatusWarning) browser.Stdout = working.Pump(ctx, progress.EventStatusInfo) err := browser.OpenURL(onStr) if err != nil { clog.Error(ctx, err) } }() <-ctx.Done() progress.Donef(ctx, "Browser opened on %s", onStr) } ================================================ FILE: pkg/client/cli/cmd/status.go ================================================ package cmd import ( "context" "errors" "fmt" "io" "net/netip" "strings" "github.com/go-json-experiment/json" "github.com/spf13/cobra" empty "google.golang.org/protobuf/types/known/emptypb" "github.com/telepresenceio/telepresence/rpc/v2/connector" daemonRpc "github.com/telepresenceio/telepresence/rpc/v2/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/ann" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/connect" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/output" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/progress" "github.com/telepresenceio/telepresence/v2/pkg/grpc" "github.com/telepresenceio/telepresence/v2/pkg/ioutil" ) type StatusInfo struct { RootDaemon RootDaemonStatus `json:"root_daemon"` UserDaemon UserDaemonStatus `json:"user_daemon"` TrafficManager TrafficManagerStatus `json:"traffic_manager"` } type MultiConnectStatusInfo struct { extendedInfo ioutil.WriterTos statusInfos []ioutil.WriterTos } type SingleConnectStatusInfo struct { extendedInfo ioutil.WriterTos statusInfo ioutil.WriterTos } type RootDaemonStatus struct { Managed bool `json:"managed,omitempty"` Running bool `json:"running,omitempty"` Name string `json:"name,omitempty"` Version string `json:"version,omitempty"` APIVersion int32 `json:"api_version,omitempty"` PortMappings []string `json:"port_mappings,omitempty"` DNS *client.DNSSnake `json:"dns,omitempty"` *client.RoutingSnake } type UserDaemonStatus struct { Running bool `json:"running,omitempty"` InDocker bool `json:"in_docker,omitempty"` Name string `json:"name,omitempty"` DaemonPort int `json:"daemon_port,omitempty"` ContainerNetwork string `json:"container_network,omitempty"` Hostname string `json:"hostname,omitempty"` ExposedPorts []string `json:"exposedPorts,omitempty"` Version string `json:"version,omitempty"` Executable string `json:"executable,omitempty"` InstallID string `json:"install_id,omitempty"` Status string `json:"status,omitempty"` Error string `json:"error,omitempty"` KubernetesServer string `json:"kubernetes_server,omitempty"` KubernetesContext string `json:"kubernetes_context,omitempty"` Namespace string `json:"namespace,omitempty"` ManagerNamespace string `json:"manager_namespace,omitempty"` MappedNamespaces []string `json:"mapped_namespaces,omitempty"` Ingests []ConnectStatusIngest `json:"ingests,omitempty"` Intercepts []ConnectStatusIntercept `json:"intercepts,omitempty"` Replacements []ConnectStatusIntercept `json:"replacements,omitempty"` Wiretaps []ConnectStatusIntercept `json:"wiretaps,omitempty"` versionName string } type ContainerizedDaemonStatus struct { *UserDaemonStatus PortMappings []string `json:"port_mappings,omitempty"` DNS *client.DNSSnake `json:"dns,omitempty"` *client.RoutingSnake } type TrafficManagerStatus struct { Name string `json:"name,omitempty"` Version string `json:"version,omitempty"` TrafficAgent string `json:"traffic_agent,omitempty"` extendedInfo ioutil.KeyValueProvider } type ConnectStatusIngest struct { Workload string `json:"workload,omitempty"` Container string `json:"container,omitempty"` Mount string `json:"mount,omitempty"` } type ConnectStatusIntercept struct { Name string `json:"name,omitempty"` Client string `json:"client,omitempty"` } const ( multiDaemonFlag = "multi-daemon" jsonFlag = "json" ) func statusCmd() *cobra.Command { cmd := &cobra.Command{ Use: "status", Args: cobra.NoArgs, Short: "Show connectivity status", RunE: run, Annotations: map[string]string{ ann.UserDaemon: ann.Optional, }, } flags := cmd.Flags() flags.Bool(multiDaemonFlag, false, "always use multi-daemon output format, even if there's only one daemon connected") return cmd } // status will retrieve connectivity status from the daemon and print it on stdout. func run(cmd *cobra.Command, _ []string) error { var mdErr daemon.MultipleDaemonsError err := connect.InitCommand(cmd) if err != nil { if !errors.As(err, &mdErr) { return err } } ctx := cmd.Context() defer progress.Stop(ctx) var sis []ioutil.WriterTos if len(mdErr) > 0 { sis = make([]ioutil.WriterTos, len(mdErr)) for i, info := range mdErr { udCtx, err := connect.ExistingDaemon(ctx, info) if err != nil { return err } sis[i], err = getStatusInfo(udCtx, info) _ = daemon.MustGetUserClient(udCtx).Close() if err != nil { return err } } } else { si, err := getStatusInfo(ctx, nil) if err != nil { return err } sis = []ioutil.WriterTos{si} } sx, err := GetStatusInfo(ctx) if err != nil { return err } multiFormat := len(sis) > 1 if !multiFormat { multiFormat, _ = cmd.Flags().GetBool(multiDaemonFlag) } var as ioutil.WriterTos if multiFormat { as = &MultiConnectStatusInfo{ extendedInfo: sx, statusInfos: sis, } } else { as = &SingleConnectStatusInfo{ extendedInfo: sx, statusInfo: sis[0], } } if output.WantsFormatted(cmd) { output.Object(ctx, &as, true) } else { _, _ = ioutil.WriteAllTo(cmd.OutOrStdout(), as.WriterTos()...) } return nil } // GetStatusInfo may return an extended struct // //nolint:gochecknoglobals // extension point var GetStatusInfo = func(ctx context.Context) (ioutil.WriterTos, error) { return nil, nil } // GetTrafficManagerStatusExtras may return an extended struct // //nolint:gochecknoglobals // extension point var GetTrafficManagerStatusExtras = func(context.Context, daemon.UserClient) ioutil.KeyValueProvider { return nil } func (s *StatusInfo) WriterTos() []io.WriterTo { if s.UserDaemon.InDocker { return []io.WriterTo{ &ContainerizedDaemonStatus{ UserDaemonStatus: &s.UserDaemon, PortMappings: s.RootDaemon.PortMappings, DNS: s.RootDaemon.DNS, RoutingSnake: s.RootDaemon.RoutingSnake, }, &s.TrafficManager, } } return []io.WriterTo{&s.UserDaemon, &s.RootDaemon, &s.TrafficManager} } func (s *StatusInfo) MarshalJSON() ([]byte, error) { return json.Marshal(s.toMap()) } func (s *StatusInfo) toMap() map[string]any { if s.UserDaemon.InDocker { return map[string]any{ "daemon": &ContainerizedDaemonStatus{ UserDaemonStatus: &s.UserDaemon, DNS: s.RootDaemon.DNS, RoutingSnake: s.RootDaemon.RoutingSnake, }, "traffic_manager": &s.TrafficManager, } } return map[string]any{ "user_daemon": &s.UserDaemon, "root_daemon": &s.RootDaemon, "traffic_manager": &s.TrafficManager, } } func setUserDaemonStatus(ctx context.Context, userD daemon.UserClient, di *daemon.Info, us *UserDaemonStatus) (*connector.ConnectInfo, error) { installID, err := client.InstallID(ctx) if err != nil { return nil, err } us.InstallID = installID us.Running = true us.Version = userD.Semver().String() us.versionName = userD.Name() us.Executable = userD.Executable() us.Name = userD.DaemonID().Name if userD.Containerized() { us.InDocker = true us.DaemonPort = userD.DaemonPort() if di != nil { us.Hostname = di.Hostname us.ExposedPorts = di.ExposedPorts } us.ContainerNetwork = userD.DaemonID().ContainerName() if us.versionName == "" { us.versionName = "Daemon" } } else if us.versionName == "" { us.versionName = "User daemon" } status, err := userD.Status(ctx, &empty.Empty{}) if err != nil { err = grpc.FromGRPC(err) us.Status = "Not connected" us.Error = err.Error() return nil, err } us.Status = "Connected" us.KubernetesServer = status.ClusterServer us.KubernetesContext = status.ClusterContext for _, ig := range status.GetIngests() { us.Ingests = append(us.Ingests, ConnectStatusIngest{ Workload: ig.Workload, Container: ig.Container, Mount: ig.ClientMountPoint, }) } for _, icept := range status.GetIntercepts().GetIntercepts() { cis := ConnectStatusIntercept{ Name: icept.Spec.Name, Client: icept.Spec.Client, } switch { case icept.Spec.NoDefaultPort: us.Replacements = append(us.Replacements, cis) case icept.Spec.Wiretap: us.Wiretaps = append(us.Wiretaps, cis) default: us.Intercepts = append(us.Intercepts, cis) } } us.Namespace = status.Namespace us.ManagerNamespace = status.ManagerNamespace us.MappedNamespaces = status.MappedNamespaces return status, nil } func getStatusInfo(ctx context.Context, di *daemon.Info) (*StatusInfo, error) { wt := &StatusInfo{} var rStatus *daemonRpc.DaemonStatus userD := daemon.GetUserClient(ctx) if userD != nil { status, err := setUserDaemonStatus(ctx, userD, di, &wt.UserDaemon) if err != nil { return nil, err } if mv := status.ManagerVersion; mv != nil { tm := &wt.TrafficManager tm.Name = mv.Name tm.Version = mv.Version if af, err := userD.AgentImageFQN(ctx, &empty.Empty{}); err == nil { tm.TrafficAgent = af.FQN } tm.extendedInfo = GetTrafficManagerStatusExtras(ctx, userD) } rStatus = status.DaemonStatus } else { conn, err := daemon.DialRootDaemon(ctx, false) if err != nil { return wt, nil } defer conn.Close() if rStatus, err = daemonRpc.NewDaemonClient(conn).Status(ctx, &empty.Empty{}); err != nil { return wt, err } } if rStatus == nil { return wt, nil } rs := &wt.RootDaemon rs.Running = true rs.Managed = rStatus.Managed rs.Name = rStatus.Version.Name if rs.Name == "" { rs.Name = "Root Daemon" } rs.Version = rStatus.Version.Version rs.APIVersion = rStatus.Version.ApiVersion if obc := rStatus.OutboundConfig; obc != nil { rs.PortMappings = obc.PortMappings } if rootCfg, err := daemon.GetRootClientConfig(rStatus); err == nil { us := &wt.UserDaemon rs.DNS = rootCfg.DNS().ToSnake() rs.RoutingSnake = rootCfg.Routing().ToSnake() if us.InDocker { if len(rs.Subnets) == 0 { // No teleroute network is started when there are no subnets to route. // DNS is exposed on port 53 on the containerized daemon, so the // IP that it exposes on the default bridge can be used for DNS. rs.DNS.LocalAddresses = []netip.AddrPort{netip.AddrPortFrom(userD.DaemonInfo().ContainerIP, 53)} us.ContainerNetwork = "default bridge" } } } return wt, nil } func (s *SingleConnectStatusInfo) WriterTos() []io.WriterTo { var wts []io.WriterTo if s.extendedInfo != nil { wts = s.extendedInfo.WriterTos() } wts = append(wts, s.statusInfo.WriterTos()...) return wts } func (s *SingleConnectStatusInfo) MarshalJSON() ([]byte, error) { m, err := s.toMap() if err != nil { return nil, err } return json.Marshal(m) } func (s *SingleConnectStatusInfo) toMap() (map[string]any, error) { m := make(map[string]any) if s.extendedInfo != nil { sx, err := json.Marshal(s.extendedInfo) if err != nil { return nil, err } if err = json.Unmarshal(sx, &m); err != nil { return nil, err } } sx, err := json.Marshal(s.statusInfo) if err != nil { return nil, err } if err = json.Unmarshal(sx, &m); err != nil { return nil, err } return m, nil } func (s *MultiConnectStatusInfo) MarshalJSON() ([]byte, error) { m, err := s.toMap() if err != nil { return nil, err } return json.Marshal(m) } func (s *MultiConnectStatusInfo) toMap() (map[string]any, error) { m := make(map[string]any) if s.extendedInfo != nil { sx, err := json.Marshal(s.extendedInfo) if err != nil { return nil, err } if err = json.Unmarshal(sx, &m); err != nil { return nil, err } } m["connections"] = s.statusInfos return m, nil } func (s *MultiConnectStatusInfo) WriterTos() []io.WriterTo { var wts []io.WriterTo if s.extendedInfo != nil { wts = s.extendedInfo.WriterTos() } for _, v := range s.statusInfos { wts = append(wts, v.WriterTos()...) } return wts } func (cs *ContainerizedDaemonStatus) WriteTo(out io.Writer) (int64, error) { n := 0 if cs.Running { n += ioutil.Printf(out, "%s %s: Running\n", cs.versionName, cs.Name) kvf := ioutil.DefaultKeyValueFormatter() kvf.Prefix = " " kvf.Indent = " " cs.print(kvf) if len(cs.PortMappings) > 0 { printPortMappings(kvf, cs.PortMappings) } if cs.DNS != nil { printDNS(kvf, cs.DNS) } if cs.RoutingSnake != nil { printRouting(kvf, cs.RoutingSnake) } n += kvf.Println(out) } else { n += ioutil.Println(out, "Daemon: Not running") } return int64(n), nil } func (ds *RootDaemonStatus) WriteTo(out io.Writer) (int64, error) { n := 0 if ds.Running { mgd := "" if ds.Managed { mgd = " (managed)" } n += ioutil.Printf(out, "%s%s: Running\n", ds.Name, mgd) kvf := ioutil.DefaultKeyValueFormatter() kvf.Prefix = " " kvf.Indent = " " kvf.Add("Version", ds.Version) if len(ds.PortMappings) > 0 { printPortMappings(kvf, ds.PortMappings) } if ds.DNS != nil { printDNS(kvf, ds.DNS) } if ds.RoutingSnake != nil { printRouting(kvf, ds.RoutingSnake) } n += kvf.Println(out) } else { n += ioutil.Println(out, "OSS Root Daemon: Not running") } return int64(n), nil } func printPortMappings(kvf *ioutil.KeyValueFormatter, pms []string) { pmKvf := ioutil.DefaultKeyValueFormatter() for _, pm := range pms { ix := strings.LastIndexByte(pm, ':') if ix < 0 { continue } pmKvf.Add(pm[:ix], pm[ix+1:]) } kvf.Add("Port Mappings", "\n"+pmKvf.String()) } func printDNS(kvf *ioutil.KeyValueFormatter, d *client.DNSSnake) { dnsKvf := ioutil.DefaultKeyValueFormatter() if d.Error != "" { dnsKvf.Add("Error", d.Error) } if len(d.LocalAddresses) > 0 { dnsKvf.Add("Local addresses", fmt.Sprintf("%s", d.LocalAddresses)) } if d.VIFAddress.IsValid() { dnsKvf.Add("VIF Address", d.VIFAddress.String()) } dnsKvf.Add("Exclude suffixes", fmt.Sprintf("%v", d.ExcludeSuffixes)) dnsKvf.Add("Include suffixes", fmt.Sprintf("%v", d.IncludeSuffixes)) if len(d.Excludes) > 0 { dnsKvf.Add("Excludes", fmt.Sprintf("%v", d.Excludes)) } if len(d.Mappings) > 0 { mappingsKvf := ioutil.DefaultKeyValueFormatter() for i := range d.Mappings { mappingsKvf.Add(d.Mappings[i].Name, d.Mappings[i].AliasFor) } dnsKvf.Add("Mappings", "\n"+mappingsKvf.String()) } dnsKvf.Add("Timeout", fmt.Sprintf("%v", d.LookupTimeout)) kvf.Add("DNS", "\n"+dnsKvf.String()) } func printRouting(kvf *ioutil.KeyValueFormatter, r *client.RoutingSnake) { printSubnets := func(title string, subnets []netip.Prefix) { if len(subnets) == 0 { return } out := &strings.Builder{} ioutil.Printf(out, "(%d subnets)", len(subnets)) for _, subnet := range subnets { ioutil.Printf(out, "\n- %s", subnet) } kvf.Add(title, out.String()) } printSubnets("Subnets", r.Subnets) printSubnets("Also Proxy", r.AlsoProxy) printSubnets("Never Proxy", r.NeverProxy) printSubnets("Allow conflicts for", r.AllowConflicting) } func (cs *UserDaemonStatus) WriteTo(out io.Writer) (int64, error) { n := 0 if cs.Running { n += ioutil.Printf(out, "%s: Running\n", cs.versionName) kvf := ioutil.DefaultKeyValueFormatter() kvf.Prefix = " " kvf.Indent = " " cs.print(kvf) n += kvf.Println(out) } else { n += ioutil.Println(out, "OSS User Daemon: Not running") } return int64(n), nil } func (cs *UserDaemonStatus) print(kvf *ioutil.KeyValueFormatter) { kvf.Add("Version", cs.Version) kvf.Add("Executable", cs.Executable) kvf.Add("Install ID", cs.InstallID) kvf.Add("Status", cs.Status) if cs.Error != "" { kvf.Add("Error", cs.Error) } kvf.Add("Kubernetes server", cs.KubernetesServer) kvf.Add("Kubernetes context", cs.KubernetesContext) if cs.ContainerNetwork != "" { kvf.Add("Container network", cs.ContainerNetwork) } kvf.Add("Namespace", cs.Namespace) kvf.Add("Manager namespace", cs.ManagerNamespace) if len(cs.MappedNamespaces) > 0 { kvf.Add("Mapped namespaces", fmt.Sprintf("%v", cs.MappedNamespaces)) } if cs.Hostname != "" { kvf.Add("Hostname", cs.Hostname) } if len(cs.ExposedPorts) > 0 { kvf.Add("Exposed ports", fmt.Sprintf("%v", cs.ExposedPorts)) } if il := len(cs.Ingests); il > 0 { out := &strings.Builder{} ioutil.Printf(out, "%d total\n", il) for _, ingest := range cs.Ingests { ioutil.Printf(out, " %s/%s\n", ingest.Workload, ingest.Container) } kvf.Add("Ingests", out.String()) } addInterceptGroup := func(name string, intercepts []ConnectStatusIntercept) { if il := len(intercepts); il > 0 { out := &strings.Builder{} ioutil.Printf(out, "%d total\n", il) subKvf := ioutil.DefaultKeyValueFormatter() subKvf.Indent = " " for _, intercept := range intercepts { subKvf.Add(intercept.Name, intercept.Client) } subKvf.Println(out) kvf.Add(name, out.String()) } } addInterceptGroup("Replacements", cs.Replacements) addInterceptGroup("Intercepts", cs.Intercepts) addInterceptGroup("Wiretaps", cs.Wiretaps) } func (ts *TrafficManagerStatus) MarshalJSON() ([]byte, error) { m, err := ts.toMap() if err != nil { return nil, err } return json.Marshal(m) } func (ts *TrafficManagerStatus) toMap() (map[string]any, error) { m := make(map[string]any) if ts.extendedInfo != nil { sx, err := json.Marshal(ts.extendedInfo) if err != nil { return nil, err } if err = json.Unmarshal(sx, &m); err != nil { return nil, err } } m["name"] = ts.Name m["traffic_agent"] = ts.TrafficAgent m["version"] = ts.Version return m, nil } func (ts *TrafficManagerStatus) WriteTo(out io.Writer) (int64, error) { n := 0 if ts.Name != "" { n += ioutil.Printf(out, "%s: Connected\n", ts.Name) kvf := ioutil.DefaultKeyValueFormatter() kvf.Prefix = " " kvf.Indent = " " kvf.Add("Version", ts.Version) if ts.TrafficAgent != "" { kvf.Add("Traffic Agent", ts.TrafficAgent) } if ts.extendedInfo != nil { ts.extendedInfo.AddTo(kvf) } n += kvf.Println(out) } else { n += ioutil.Println(out, "Traffic Manager: Not connected") } return int64(n), nil } ================================================ FILE: pkg/client/cli/cmd/telepresence.go ================================================ package cmd import ( "context" "errors" "fmt" "io/fs" "os" "path/filepath" "strings" "github.com/blang/semver/v4" "github.com/spf13/cobra" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/client/cache" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/connect" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/global" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/output" "github.com/telepresenceio/telepresence/v2/pkg/client/docker/kubeauth" "github.com/telepresenceio/telepresence/v2/pkg/client/rootd" userDaemon "github.com/telepresenceio/telepresence/v2/pkg/client/userd/daemon" "github.com/telepresenceio/telepresence/v2/pkg/errcat" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" "github.com/telepresenceio/telepresence/v2/pkg/maps" "github.com/telepresenceio/telepresence/v2/pkg/version" ) // Telepresence returns the top level "telepresence" CLI command. func Telepresence(ctx context.Context, args []string) *cobra.Command { useMarkdown := len(args) > 0 && args[0] == "man-pages" longHelp := helpPlain if useMarkdown { os.Setenv("KUBECACHEDIR", "$HOME/.kube/cache") longHelp = helpMarkdown } rootCmd := &cobra.Command{ Use: "telepresence", Args: OnlySubcommands, Short: "Connect your workstation to a Kubernetes cluster", Long: longHelp, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { if err := output.SetFormat(cmd, args); err != nil { return err } return ensureCacheVersion(cmd, args) }, RunE: RunSubcommands, SilenceErrors: true, // main() will handle it after .ExecuteContext() returns SilenceUsage: true, // our FlagErrorFunc will handle it TraverseChildren: true, ValidArgsFunction: cobra.NoFileCompletions, } rootCmd.SetArgs(args) rootCmd.SetContext(ctx) AddSubCommands(rootCmd, useMarkdown) rootCmd.SetFlagErrorFunc(func(_ *cobra.Command, err error) error { return errcat.User.New(err) }) return rootCmd } // TelepresenceDaemon returns the top level "telepresence" CLI limited to the subcommands kubeauthd, userd, and rootd. func TelepresenceDaemon(ctx context.Context, args []string) *cobra.Command { cmd := &cobra.Command{ Use: "telepresence", Args: OnlySubcommands, RunE: func(cmd *cobra.Command, args []string) error { cmd.SetOut(cmd.ErrOrStderr()) return nil }, SilenceErrors: true, // main() will handle it after .ExecuteContext() returns SilenceUsage: true, // our FlagErrorFunc will handle it } cmd.SetArgs(args) cmd.SetContext(ctx) AddSubCommands(cmd, false) return cmd } func setContext(cmd *cobra.Command, ctx context.Context) { cmd.SetContext(ctx) for _, c := range cmd.Commands() { setContext(c, ctx) } } // AddSubCommands adds subcommands to the given command, including the default help, the commands in the // CommandGroups found in the given command's context, and the completion command. It also replaces // the standard usage template with a custom template. func AddSubCommands(cmd *cobra.Command, markdown bool) { ctx := cmd.Context() commands := getSubCommands(cmd) for _, command := range commands { if ac := command.Args; ac != nil { // Ensure that args errors don't advice the user to look in log files command.Args = argsCheck(ac) } setContext(command, ctx) } cmd.AddCommand(commands...) if client.ProcessName() != client.RootDaemonName { cmd.PersistentFlags().AddFlagSet(global.Flags(ctx, false, markdown)) addCompletion(cmd, markdown) addUsageTemplate(cmd, markdown) _ = cmd.RegisterFlagCompletionFunc("context", autocompleteContext) } } // RunSubcommands is for use as a cobra.interceptCmd.RunE for commands that don't do anything themselves // but have subcommands. In such cases, it is important to set RunE even though there's nothing to // run, because otherwise cobra will treat that as "success", and it shouldn't be "success" if the // user typos a command and types something invalid. func RunSubcommands(cmd *cobra.Command, args []string) error { // determine if --help was explicitly asked for var usedHelpFlag bool for _, arg := range args { if arg == "--help" || arg == "-h" { usedHelpFlag = true } } // If there are no args or --help was used, then it's not a legacy // Telepresence command so we return the help text if len(args) == 0 || usedHelpFlag { cmd.HelpFunc()(cmd, args) return nil } return nil } // OnlySubcommands is a cobra.PositionalArgs that is similar to cobra.NoArgs, but prints a better // error message. func OnlySubcommands(cmd *cobra.Command, args []string) error { if len(args) == 0 { return nil } if args[0] == "-h" { return nil } err := fmt.Errorf("invalid subcommand %q", args[0]) if cmd.SuggestionsMinimumDistance <= 0 { cmd.SuggestionsMinimumDistance = 2 } if suggestions := cmd.SuggestionsFor(args[0]); len(suggestions) > 0 { err = fmt.Errorf("%w\nDid you mean one of these?\n\t%s", err, strings.Join(suggestions, "\n\t")) } return cmd.FlagErrorFunc()(cmd, err) } func WithSubCommands(ctx context.Context) context.Context { return MergeSubCommands(ctx, composeCmd(), configCmd(), connectCmd(), curlCmd(), dockerRunCmd(), gatherLogs(), genYAML(), helmCmd(), ingestCmd(), interceptCmd(), kubeauthCmd(), leaveCmd(), list(), listContexts(), revokeCmd(), listNamespaces(), loglevel(), manPages(), mcp(), quit(), replaceCmd(), serveCmd(), statusCmd(), uninstall(), versionCmd(), wiretapCmd(), ) } func WithDaemonSubCommands(ctx context.Context) context.Context { return MergeSubCommands(ctx, kubeauth.Command(ctx), userDaemon.Command(ctx), rootd.Command(ctx)) } type subCommandsKey struct{} func MergeSubCommands(ctx context.Context, commands ...*cobra.Command) context.Context { if ecs, ok := ctx.Value(subCommandsKey{}).(*[]*cobra.Command); ok { *ecs = mergeCommands(*ecs, commands) } else { ctx = context.WithValue(ctx, subCommandsKey{}, &commands) } return ctx } func getSubCommands(cmd *cobra.Command) []*cobra.Command { if gs, ok := cmd.Context().Value(subCommandsKey{}).(*[]*cobra.Command); ok { return *gs } return nil } // mergeCommands merges the command slice b into a, replacing commands using the same name // and returns the resulting slice. func mergeCommands(a, b []*cobra.Command) []*cobra.Command { ac := make(map[string]*cobra.Command, len(a)+len(b)) for _, c := range a { ac[c.Name()] = c } for _, c := range b { ac[c.Name()] = c } return maps.ToSortedSlice(ac) } // argsCheck wraps an PositionalArgs checker in a function that wraps a potential error // using errcat.User. func argsCheck(f cobra.PositionalArgs) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if err := f(cmd, args); err != nil { return errcat.User.New(err) } return nil } } func autocompleteContext(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() clog.Debugf(ctx, "context completion: %q", toComplete) cfg, err := daemon.GetKubeStartingConfig(cmd) if err != nil { clog.Errorf(ctx, "GetKubeStartingConfig: %v", err) return nil, cobra.ShellCompDirectiveError } cxl := cfg.Contexts nss := make([]string, len(cxl)) i := 0 for n := range cxl { nss[i] = n i++ } return nss, cobra.ShellCompDirectiveNoFileComp } type versionFile struct { Version semver.Version `json:"version"` } func ensureCacheVersion(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() var vf versionFile majorMinorMatch := false if err := cache.LoadFromUserCache(ctx, &vf, "version.json"); err == nil { majorMinorMatch = vf.Version.Major == version.Structured.Major && vf.Version.Minor == version.Structured.Minor if majorMinorMatch { return nil } } // Quit all daemons (best effort, ignore errors). connect.Quit(ctx) // Clear cache except logs. cacheDir := filelocation.AppUserCacheDir(ctx) entries, err := os.ReadDir(cacheDir) if err != nil && !errors.Is(err, fs.ErrNotExist) { return err } for _, entry := range entries { if entry.Name() == "logs" { continue } _ = os.RemoveAll(filepath.Join(cacheDir, entry.Name())) } return cache.SaveToUserCache(ctx, &versionFile{Version: version.Structured}, "version.json", cache.Public) } ================================================ FILE: pkg/client/cli/cmd/testdata/license ================================================ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ================================================ FILE: pkg/client/cli/cmd/testdata/testLogDir/cli.log ================================================ 2022-02-12 15:02:29.6927 info Logging at this level "debug" ================================================ FILE: pkg/client/cli/cmd/testdata/testLogDir/connector-20210916T130347.log ================================================ 2021-09-16 13:03:19.0501 info Logging at this level "info" 2021-09-16 13:03:19.0865 info --- 2021-09-16 13:03:19.0865 info Telepresence Connector v2.4.3-98-g0585bbd1-1631811285 (api v3) starting... 2021-09-16 13:03:19.0865 info PID is 42030 2021-09-16 13:03:19.0865 info 2021-09-16 13:03:19.0867 info connector/server-grpc : gRPC server started 2021-09-16 13:03:19.2546 info connector/background-init : Connecting to daemon... 2021-09-16 13:03:19.2556 info connector/background-init : Connecting to k8s cluster... 2021-09-16 13:03:19.4222 info connector/background-init : Server version v1.17.17+k3s1 2021-09-16 13:03:19.4224 info connector/background-init : Context: default 2021-09-16 13:03:19.4225 info connector/background-init : Server: https://35.224.222.254 2021-09-16 13:03:19.4226 info connector/background-init : Connected to context default (https://35.224.222.254) 2021/09/16 13:03:19 Patching synced Namespace 55b19f40-65d2-4a0a-b61e-3009a28881d4 2021-09-16 13:03:19.9566 info connector/background-init : Connecting to traffic manager... 2021-09-16 13:03:19.9567 info connector/background-init : Waiting for TrafficManager to connect 2021-09-16 13:03:20.1110 info connector/background-manager : Existing Traffic Manager found, upgrading to v2.4.3-98-g0585bbd1-1631811285... 2021-09-16 13:03:37.1864 info connector/server-grpc/conn=1/Uninstall-3 : In Deployment echo-easy, reveal hidden port "tm-http" in container echo-easy by restoring its origina name "http", and remove traffic-agent container with image docker.io/datawire/ambassador-telepresence-agent:1.11.1-rc.0. 2021-09-16 13:03:39.7361 info connector/server-grpc/conn=1/Uninstall-3 : Uninstalling Traffic Manager 2021-09-16 13:03:43.0277 info connector/background-manager:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:03:43.0277 info connector/background-systema:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:03:43.0279 info connector:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:03:43.0281 info connector/background-k8swatch:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:03:43.2349 info connector/server-grpc/conn=2:shutdown_logger : shutting down (gracefully)... ================================================ FILE: pkg/client/cli/cmd/testdata/testLogDir/connector-20210916T130356.log ================================================ 2021-09-16 13:03:47.7084 info Logging at this level "info" 2021-09-16 13:03:47.7451 info --- 2021-09-16 13:03:47.7451 info Telepresence Connector v2.4.3-98-g0585bbd1-1631811285 (api v3) starting... 2021-09-16 13:03:47.7452 info PID is 42128 2021-09-16 13:03:47.7452 info 2021-09-16 13:03:47.7453 info connector/server-grpc : gRPC server started 2021-09-16 13:03:54.6001 info connector/background-systema:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:03:54.6002 info connector:shutdown_logger : shutting down (gracefully)... ================================================ FILE: pkg/client/cli/cmd/testdata/testLogDir/connector-20210916T130624.log ================================================ 2021-09-16 13:03:56.0769 info Logging at this level "info" 2021-09-16 13:03:56.1107 info --- 2021-09-16 13:03:56.1108 info Telepresence Connector v2.4.3-98-g0585bbd1-1631811285 (api v3) starting... 2021-09-16 13:03:56.1108 info PID is 42196 2021-09-16 13:03:56.1108 info 2021-09-16 13:03:56.1109 info connector/server-grpc : gRPC server started 2021-09-16 13:04:02.6361 info connector/background-init : Connecting to daemon... 2021-09-16 13:04:02.6368 info connector/background-init : Connecting to k8s cluster... 2021-09-16 13:04:02.8130 info connector/background-init : Server version v1.17.17+k3s1 2021-09-16 13:04:02.8131 info connector/background-init : Context: default 2021-09-16 13:04:02.8131 info connector/background-init : Server: https://35.224.222.254 2021-09-16 13:04:02.8131 info connector/background-init : Connected to context default (https://35.224.222.254) 2021-09-16 13:04:02.8764 info connector/background-init : Connecting to traffic manager... 2021-09-16 13:04:02.8766 info connector/background-init : Waiting for TrafficManager to connect 2021/09/16 13:04:02 Patching synced Namespace 55b19f40-65d2-4a0a-b61e-3009a28881d4 2021-09-16 13:04:04.0645 info connector/background-manager : No existing Traffic Manager found, installing v2.4.3-98-g0585bbd1-1631811285... 2021-09-16 13:05:28.7671 info connector/background-k8swatch:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:05:28.7672 info connector/background-systema:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:05:28.7672 info connector:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:05:28.7673 info connector/background-manager:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:05:28.9704 info connector/server-grpc/conn=3:shutdown_logger : shutting down (gracefully)... ================================================ FILE: pkg/client/cli/cmd/testdata/testLogDir/connector-20210916T130643.log ================================================ 2021-09-16 13:06:24.4082 info Logging at this level "info" 2021-09-16 13:06:24.4460 info --- 2021-09-16 13:06:24.4461 info Telepresence Connector v2.4.3-98-g0585bbd1-1631811285 (api v3) starting... 2021-09-16 13:06:24.4461 info PID is 42655 2021-09-16 13:06:24.4461 info 2021-09-16 13:06:24.4462 info connector/server-grpc : gRPC server started 2021-09-16 13:06:24.6166 info connector/background-init : Connecting to daemon... 2021-09-16 13:06:24.6179 info connector/background-init : Connecting to k8s cluster... 2021-09-16 13:06:24.8007 info connector/background-init : Server version v1.17.17+k3s1 2021-09-16 13:06:24.8008 info connector/background-init : Context: default 2021-09-16 13:06:24.8008 info connector/background-init : Server: https://35.224.222.254 2021-09-16 13:06:24.8009 info connector/background-init : Connected to context default (https://35.224.222.254) 2021-09-16 13:06:24.8772 info connector/background-init : Connecting to traffic manager... 2021-09-16 13:06:24.8774 info connector/background-init : Waiting for TrafficManager to connect 2021/09/16 13:06:24 Patching synced Namespace 55b19f40-65d2-4a0a-b61e-3009a28881d4 2021-09-16 13:06:25.1442 info connector/background-manager : Existing Traffic Manager not owned by cli or does not need upgrade, will not modify 2021-09-16 13:06:26.1202 info connector/server-grpc/conn=1/Uninstall-3 : Uninstalling Traffic Manager 2021-09-16 13:06:29.1287 info connector/background-k8swatch:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:06:29.1289 info connector/background-manager:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:06:29.1289 info connector:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:06:29.1291 info connector/background-systema:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:06:29.3357 info connector/server-grpc/conn=2:shutdown_logger : shutting down (gracefully)... ================================================ FILE: pkg/client/cli/cmd/testdata/testLogDir/connector.log ================================================ 2021-09-16 13:06:43.4849 info Logging at this level "info" 2021-09-16 13:06:43.5211 info --- 2021-09-16 13:06:43.5211 info Telepresence Connector v2.4.3-98-g0585bbd1-1631811285 (api v3) starting... 2021-09-16 13:06:43.5211 info PID is 42749 2021-09-16 13:06:43.5212 info 2021-09-16 13:06:43.5213 info connector/server-grpc : gRPC server started 2021-09-16 13:06:43.6978 info connector/background-init : Connecting to daemon... 2021-09-16 13:06:43.6992 info connector/background-init : Connecting to k8s cluster... 2021-09-16 13:06:43.9374 info connector/background-init : Server version v1.17.17+k3s1 2021-09-16 13:06:43.9375 info connector/background-init : Context: default 2021-09-16 13:06:43.9375 info connector/background-init : Server: https://35.224.222.254 2021-09-16 13:06:43.9375 info connector/background-init : Connected to context default (https://35.224.222.254) 2021-09-16 13:06:44.0110 info connector/background-init : Connecting to traffic manager... 2021-09-16 13:06:44.0112 info connector/background-init : Waiting for TrafficManager to connect 2021/09/16 13:06:44 Patching synced Namespace 55b19f40-65d2-4a0a-b61e-3009a28881d4 2021-09-16 13:06:46.4289 info connector/background-manager : No existing Traffic Manager found, installing v2.4.3-98-g0585bbd1-1631811285... ================================================ FILE: pkg/client/cli/cmd/testdata/testLogDir/daemon-20210916T130318.log ================================================ 2021/09/15 16:54:57.4146 debug : Logging at this level "debug" 2021/09/15 16:54:57.4510 info : --- 2021/09/15 16:54:57.4510 info : Telepresence daemon v2.4.2 (api v3) starting... 2021/09/15 16:54:57.4510 info : PID is 21428 2021/09/15 16:54:57.4510 info : 2021/09/15 16:54:57.4512 debug daemon/server-grpc : gRPC server starting 2021/09/15 16:54:57.4512 debug daemon/server-router/TUN reader : Waiting until manager gRPC is configured 2021/09/15 16:54:57.4513 debug daemon/server-router/MGR stream : Waiting until manager gRPC is configured 2021/09/15 16:54:57.4516 info daemon/server-grpc : gRPC server started 2021/09/15 16:55:05.9771 info daemon/server-grpc/conn=2 : Adding also-proxy subnet 1.2.3.4/32 2021/09/15 16:55:06.0502 info daemon/watch-cluster-info : Adding service subnet 10.43.0.0/16 2021/09/15 16:55:06.0503 info daemon/watch-cluster-info : Adding pod subnet 10.42.0.0/24 2021/09/15 16:55:06.0514 debug daemon/server-router/TUN reader : TUN read loop starting 2021/09/15 16:55:06.0517 debug daemon/server-router/MGR stream : MGR read loop starting 2021/09/15 16:55:06.0524 info daemon/server-dns : Generated new /etc/resolver/telepresence.local 2021/09/15 16:55:06.0587 info daemon/server-dns : started command ["killall" "-HUP" "mDNSResponder"] : dexec.pid="21436" 2021/09/15 16:55:06.0591 info daemon/server-dns : : dexec.pid="21436" dexec.stream="stdin" dexec.err="EOF" 2021/09/15 16:55:06.0727 info daemon/server-dns : finished successfully: exit status 0 : dexec.pid="21436" 2021/09/15 16:55:06.0754 info daemon/server-dns : started command ["killall" "mDNSResponderHelper"] : dexec.pid="21437" 2021/09/15 16:55:06.0756 info daemon/server-dns : : dexec.pid="21437" dexec.stream="stdin" dexec.err="EOF" 2021/09/15 16:55:06.0852 info daemon/server-dns : : dexec.pid="21437" dexec.stream="stdout+stderr" dexec.data="No matching processes were found\n" 2021/09/15 16:55:06.0854 info daemon/server-dns : finished with error: exit status 1 : dexec.pid="21437" 2021/09/15 16:55:06.0877 info daemon/server-dns : started command ["dscacheutil" "-flushcache"] : dexec.pid="21438" 2021/09/15 16:55:06.0878 info daemon/server-dns : : dexec.pid="21438" dexec.stream="stdin" dexec.err="EOF" 2021/09/15 16:55:06.0911 info daemon/server-dns : finished successfully: exit status 0 : dexec.pid="21438" 2021/09/15 16:55:06.0912 info daemon/server-grpc/conn=2 : setting search paths ambassador default kube-node-lease kube-public kube-system 2021/09/15 16:55:06.0913 info daemon/server-grpc/conn=2 : Generated new /etc/resolver/telepresence.kube-system.local 2021/09/15 16:55:06.0915 info daemon/server-grpc/conn=2 : Generated new /etc/resolver/telepresence.tel2-search.local 2021/09/15 16:55:06.0917 info daemon/server-grpc/conn=2 : Generated new /etc/resolver/telepresence.ambassador.local 2021/09/15 16:55:06.0918 info daemon/server-grpc/conn=2 : Generated new /etc/resolver/telepresence.default.local 2021/09/15 16:55:06.0919 info daemon/server-grpc/conn=2 : Generated new /etc/resolver/telepresence.kube-node-lease.local 2021/09/15 16:55:06.0920 info daemon/server-grpc/conn=2 : Generated new /etc/resolver/telepresence.kube-public.local 2021/09/15 16:55:06.0941 info daemon/server-grpc/conn=2 : started command ["killall" "-HUP" "mDNSResponder"] : dexec.pid="21439" 2021/09/15 16:55:06.0942 info daemon/server-grpc/conn=2 : : dexec.pid="21439" dexec.stream="stdin" dexec.err="EOF" 2021/09/15 16:55:06.1017 info daemon/server-grpc/conn=2 : finished successfully: exit status 0 : dexec.pid="21439" 2021/09/15 16:55:06.1039 info daemon/server-grpc/conn=2 : started command ["killall" "mDNSResponderHelper"] : dexec.pid="21440" 2021/09/15 16:55:06.1040 info daemon/server-grpc/conn=2 : : dexec.pid="21440" dexec.stream="stdin" dexec.err="EOF" 2021/09/15 16:55:06.1133 info daemon/server-grpc/conn=2 : : dexec.pid="21440" dexec.stream="stdout+stderr" dexec.data="No matching processes were found\n" 2021/09/15 16:55:06.1135 info daemon/server-grpc/conn=2 : finished with error: exit status 1 : dexec.pid="21440" 2021/09/15 16:55:06.1160 info daemon/server-grpc/conn=2 : started command ["dscacheutil" "-flushcache"] : dexec.pid="21441" 2021/09/15 16:55:06.1162 info daemon/server-grpc/conn=2 : : dexec.pid="21441" dexec.stream="stdin" dexec.err="EOF" 2021/09/15 16:55:06.1194 info daemon/server-grpc/conn=2 : finished successfully: exit status 0 : dexec.pid="21441" 2021/09/15 16:55:06.1196 info daemon/server-grpc/conn=2 : setting search paths ambassador default kube-node-lease kube-public kube-system 2021/09/15 16:55:06.1220 info daemon/server-grpc/conn=2 : started command ["killall" "-HUP" "mDNSResponder"] : dexec.pid="21442" 2021/09/15 16:55:06.1222 info daemon/server-grpc/conn=2 : : dexec.pid="21442" dexec.stream="stdin" dexec.err="EOF" 2021/09/15 16:55:06.1301 info daemon/server-grpc/conn=2 : finished successfully: exit status 0 : dexec.pid="21442" 2021/09/15 16:55:06.1320 info daemon/server-grpc/conn=2 : started command ["killall" "mDNSResponderHelper"] : dexec.pid="21443" 2021/09/15 16:55:06.1322 info daemon/server-grpc/conn=2 : : dexec.pid="21443" dexec.stream="stdin" dexec.err="EOF" 2021/09/15 16:55:06.1418 info daemon/server-grpc/conn=2 : : dexec.pid="21443" dexec.stream="stdout+stderr" dexec.data="No matching processes were found\n" 2021/09/15 16:55:06.1420 info daemon/server-grpc/conn=2 : finished with error: exit status 1 : dexec.pid="21443" 2021/09/15 16:55:06.1440 info daemon/server-grpc/conn=2 : started command ["dscacheutil" "-flushcache"] : dexec.pid="21444" 2021/09/15 16:55:06.1441 info daemon/server-grpc/conn=2 : : dexec.pid="21444" dexec.stream="stdin" dexec.err="EOF" 2021/09/15 16:55:06.1468 info daemon/server-grpc/conn=2 : finished successfully: exit status 0 : dexec.pid="21444" 2021/09/15 16:55:07.6433 debug daemon/server-dns/Server : QTYPE[1] dkaxhzczcdgyrh.cluster.local. -> NOT FOUND 2021/09/15 16:55:07.6437 debug daemon/server-dns/Server : QTYPE[28] dkaxhzczcdgyrh.cluster.local. -> NOT FOUND 2021/09/15 16:55:07.6439 debug daemon/server-dns/Server : QTYPE[1] abplynvtqbyisno.cluster.local. -> NOT FOUND 2021/09/15 16:55:07.6440 debug daemon/server-dns/Server : QTYPE[28] abplynvtqbyisno.cluster.local. -> NOT FOUND 2021/09/15 16:55:07.6451 debug daemon/server-dns/Server : QTYPE[28] mhmgkrnjch.cluster.local. -> NOT FOUND 2021/09/15 16:55:07.6451 debug daemon/server-dns/Server : QTYPE[1] mhmgkrnjch.cluster.local. -> NOT FOUND 2021/09/15 17:16:30.0423 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 17:16:30.0424 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 17:16:30.0424 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 17:26:30.0387 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 17:26:30.0388 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 17:26:30.0391 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 17:36:30.0566 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 17:36:30.0568 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 17:36:30.0569 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:11:03.2070 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:11:03.2072 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:11:03.2072 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:20.5468 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:20.5472 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:20.5473 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:25.4311 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:25.4312 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:25.4314 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:25.4392 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:25.4392 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:25.4393 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:25.4396 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:25.4397 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:25.4398 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:25.4561 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:25.4563 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:25.4563 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:25.4629 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:25.4630 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:25.4631 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:31.0581 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:31.1962 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:31.1975 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:31.4009 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:31.4028 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:31.4506 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:31.4518 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:31.4525 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:31.4537 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:31.4563 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:31.4585 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:31.4615 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:31.4653 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:31.5002 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:31.5009 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:34.7635 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:34.8072 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:35.2077 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:35.6056 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:35.6062 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:35.6111 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:35.6136 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:35.6153 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:35.6516 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:35.6546 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:35.7552 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:36.1029 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:36.1172 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:36.5508 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:36.5528 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:36.5636 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:36.5642 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:36.5646 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:36.6021 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:36.6040 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:36.6045 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:39.2544 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:39.2556 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:39.8001 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:40.2010 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:40.2037 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:40.2267 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:40.3137 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:40.3143 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:40.4043 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:42.9169 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:42.9505 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:42.9531 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:41:44.9068 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:41:44.9131 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:41:44.9148 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:58:43.1337 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:58:43.1338 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:58:43.1338 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:58:44.5317 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:58:44.5318 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:58:44.5319 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 18:58:54.9011 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 18:58:54.9019 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 18:58:54.9024 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 19:59:32.0462 debug daemon/server-dns/Server : QTYPE[1] zhrtvzemyq.cluster.local. -> NOT FOUND 2021/09/15 19:59:32.0462 debug daemon/server-dns/Server : QTYPE[28] zhrtvzemyq.cluster.local. -> NOT FOUND 2021/09/15 19:59:32.0464 debug daemon/server-dns/Server : QTYPE[1] ybgebfqtgivhxf.cluster.local. -> NOT FOUND 2021/09/15 19:59:32.0466 debug daemon/server-dns/Server : QTYPE[28] ybgebfqtgivhxf.cluster.local. -> NOT FOUND 2021/09/15 19:59:32.0469 debug daemon/server-dns/Server : QTYPE[1] ozjolubccuuyuow.cluster.local. -> NOT FOUND 2021/09/15 19:59:32.0486 debug daemon/server-dns/Server : QTYPE[28] ozjolubccuuyuow.cluster.local. -> NOT FOUND 2021/09/15 19:59:33.3811 debug daemon/server-dns/Server : QTYPE[1] llvmprrmvyzgz.cluster.local. -> NOT FOUND 2021/09/15 19:59:33.3819 debug daemon/server-dns/Server : QTYPE[28] llvmprrmvyzgz.cluster.local. -> NOT FOUND 2021/09/15 19:59:33.3829 debug daemon/server-dns/Server : QTYPE[28] mtbdazgteqsqm.cluster.local. -> NOT FOUND 2021/09/15 19:59:33.3830 debug daemon/server-dns/Server : QTYPE[1] mtbdazgteqsqm.cluster.local. -> NOT FOUND 2021/09/15 19:59:33.3832 debug daemon/server-dns/Server : QTYPE[1] pcqfqmnpycan.cluster.local. -> NOT FOUND 2021/09/15 19:59:33.3834 debug daemon/server-dns/Server : QTYPE[28] pcqfqmnpycan.cluster.local. -> NOT FOUND 2021/09/15 19:59:34.7250 debug daemon/server-dns/Server : QTYPE[1] glqehuyh.cluster.local. -> NOT FOUND 2021/09/15 19:59:34.7254 debug daemon/server-dns/Server : QTYPE[28] glqehuyh.cluster.local. -> NOT FOUND 2021/09/15 19:59:34.7264 debug daemon/server-dns/Server : QTYPE[1] orrceyifpn.cluster.local. -> NOT FOUND 2021/09/15 19:59:34.7266 debug daemon/server-dns/Server : QTYPE[28] orrceyifpn.cluster.local. -> NOT FOUND 2021/09/15 19:59:34.7271 debug daemon/server-dns/Server : QTYPE[1] vzovgzvo.cluster.local. -> NOT FOUND 2021/09/15 19:59:34.7275 debug daemon/server-dns/Server : QTYPE[28] vzovgzvo.cluster.local. -> NOT FOUND 2021/09/15 19:59:38.7444 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 19:59:38.7446 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 19:59:38.7448 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 19:59:40.1866 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 19:59:40.1868 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 19:59:40.1872 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 19:59:41.7183 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 19:59:41.7234 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 19:59:41.7318 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 19:59:42.8927 debug daemon/server-dns/Server : QTYPE[1] fvltzbxxkxadr.cluster.local. -> NOT FOUND 2021/09/15 19:59:42.8944 debug daemon/server-dns/Server : QTYPE[28] fvltzbxxkxadr.cluster.local. -> NOT FOUND 2021/09/15 19:59:42.8947 debug daemon/server-dns/Server : QTYPE[1] uysebeufgxgohx.cluster.local. -> NOT FOUND 2021/09/15 19:59:42.8963 debug daemon/server-dns/Server : QTYPE[28] uysebeufgxgohx.cluster.local. -> NOT FOUND 2021/09/15 19:59:42.8980 debug daemon/server-dns/Server : QTYPE[1] ruhdngwdqkoydt.cluster.local. -> NOT FOUND 2021/09/15 19:59:42.8990 debug daemon/server-dns/Server : QTYPE[28] ruhdngwdqkoydt.cluster.local. -> NOT FOUND 2021/09/15 20:00:10.6147 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 20:00:10.6153 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 20:00:10.6162 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 20:00:12.1148 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 20:00:12.1157 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 20:00:12.1160 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 20:00:13.6150 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 20:00:13.6157 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 20:00:13.6170 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 21:00:18.4583 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 21:00:18.4584 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 21:00:18.4585 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 21:00:18.6692 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 21:00:18.6694 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 21:00:18.6695 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 21:00:28.7959 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 21:00:28.7969 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 21:00:28.7974 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 21:00:28.7978 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 21:00:28.7987 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 21:00:28.7991 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 21:01:03.7533 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 21:01:03.7924 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 21:01:03.7930 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/15 21:01:03.7970 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/15 21:01:03.7975 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/15 21:01:03.7979 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 02:05:24.5949 debug daemon/server-dns/Server : QTYPE[1] ysplhmngm.cluster.local. -> NOT FOUND 2021/09/16 02:05:24.5952 debug daemon/server-dns/Server : QTYPE[28] ysplhmngm.cluster.local. -> NOT FOUND 2021/09/16 02:05:24.5957 debug daemon/server-dns/Server : QTYPE[1] upbkrpiieh.cluster.local. -> NOT FOUND 2021/09/16 02:05:24.5962 debug daemon/server-dns/Server : QTYPE[28] upbkrpiieh.cluster.local. -> NOT FOUND 2021/09/16 02:05:24.5967 debug daemon/server-dns/Server : QTYPE[1] qdkbnjshqtwnc.cluster.local. -> NOT FOUND 2021/09/16 02:05:24.5972 debug daemon/server-dns/Server : QTYPE[28] qdkbnjshqtwnc.cluster.local. -> NOT FOUND 2021/09/16 02:05:27.7838 debug daemon/server-dns/Server : QTYPE[1] kexueunltwokcyn.cluster.local. -> NOT FOUND 2021/09/16 02:05:27.7839 debug daemon/server-dns/Server : QTYPE[28] kexueunltwokcyn.cluster.local. -> NOT FOUND 2021/09/16 02:05:27.7847 debug daemon/server-dns/Server : QTYPE[1] dzezptomrkwptz.cluster.local. -> NOT FOUND 2021/09/16 02:05:27.7848 debug daemon/server-dns/Server : QTYPE[28] dzezptomrkwptz.cluster.local. -> NOT FOUND 2021/09/16 02:05:27.7853 debug daemon/server-dns/Server : QTYPE[1] qywnrqtbzigoru.cluster.local. -> NOT FOUND 2021/09/16 02:05:27.7856 debug daemon/server-dns/Server : QTYPE[28] qywnrqtbzigoru.cluster.local. -> NOT FOUND 2021/09/16 02:05:28.8967 debug daemon/server-dns/Server : QTYPE[1] abrtfrjorszr.cluster.local. -> NOT FOUND 2021/09/16 02:05:28.8971 debug daemon/server-dns/Server : QTYPE[28] abrtfrjorszr.cluster.local. -> NOT FOUND 2021/09/16 02:05:28.8976 debug daemon/server-dns/Server : QTYPE[1] xkaxpuwcbjbsed.cluster.local. -> NOT FOUND 2021/09/16 02:05:28.8980 debug daemon/server-dns/Server : QTYPE[28] xkaxpuwcbjbsed.cluster.local. -> NOT FOUND 2021/09/16 02:05:28.8984 debug daemon/server-dns/Server : QTYPE[1] mowgwwxpkxf.cluster.local. -> NOT FOUND 2021/09/16 02:05:28.8986 debug daemon/server-dns/Server : QTYPE[28] mowgwwxpkxf.cluster.local. -> NOT FOUND 2021/09/16 02:05:31.0843 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 02:05:31.0844 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 02:05:31.0850 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 02:05:32.5769 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 02:05:32.5771 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 02:05:32.5772 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 02:05:32.5991 debug daemon/server-dns/Server : QTYPE[1] bagffywtesuvwbg.cluster.local. -> NOT FOUND 2021/09/16 02:05:32.6006 debug daemon/server-dns/Server : QTYPE[1] aluptequj.cluster.local. -> NOT FOUND 2021/09/16 02:05:32.6007 debug daemon/server-dns/Server : QTYPE[28] bagffywtesuvwbg.cluster.local. -> NOT FOUND 2021/09/16 02:05:32.6011 debug daemon/server-dns/Server : QTYPE[28] aluptequj.cluster.local. -> NOT FOUND 2021/09/16 02:05:32.6013 debug daemon/server-dns/Server : QTYPE[1] krtmfrzociyvajh.cluster.local. -> NOT FOUND 2021/09/16 02:05:32.6015 debug daemon/server-dns/Server : QTYPE[28] krtmfrzociyvajh.cluster.local. -> NOT FOUND 2021/09/16 02:05:34.0538 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 02:05:34.0545 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 02:05:34.0562 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 02:06:02.8013 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 02:06:02.8024 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 02:06:02.8030 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 02:06:04.3044 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 02:06:04.3051 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 02:06:04.3060 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 02:06:05.8017 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 02:06:05.8030 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 02:06:05.8043 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 03:06:09.8360 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 03:06:09.8362 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 03:06:09.8364 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 03:06:10.0387 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 03:06:10.0389 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 03:06:10.0390 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 03:06:20.2387 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 03:06:20.2735 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 03:06:20.2739 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 03:06:20.2758 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 03:06:20.2777 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 03:06:20.2792 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 03:06:54.8796 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 03:06:54.9323 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 03:06:54.9350 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 03:06:54.9799 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 03:06:54.9811 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 03:06:54.9824 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 08:52:43.1738 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 08:52:43.1755 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 08:52:43.1758 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 08:52:44.6671 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 08:52:44.6671 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 08:52:44.6672 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 08:52:44.6672 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 08:52:44.6672 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 08:52:44.6673 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 08:52:45.6695 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 08:52:45.6697 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 08:52:45.6698 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 08:52:45.6701 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 08:52:45.6704 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 08:52:45.6705 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 08:52:46.6726 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 08:52:46.6728 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 08:52:46.6728 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 08:52:48.0587 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 08:52:48.0588 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 08:52:48.0589 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 08:52:49.0604 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 08:52:49.0605 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 08:52:49.0605 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 08:52:49.0606 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 08:52:49.0607 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 08:52:49.0607 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 08:52:50.5576 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 08:52:50.5742 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 08:52:50.5751 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 08:52:50.5752 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 08:52:50.5753 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 08:52:50.5754 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 08:52:52.0359 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 08:52:52.0359 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 08:52:52.0360 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 08:56:50.3120 debug daemon/server-dns/Server : QTYPE[1] asfdsa.cluster.local. -> NOT FOUND 2021/09/16 08:56:50.3121 debug daemon/server-dns/Server : QTYPE[28] asfdsa.cluster.local. -> NOT FOUND 2021/09/16 09:14:44.2191 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 09:14:44.2191 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 09:14:44.2192 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 09:15:02.8252 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 09:15:02.8253 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 09:15:02.8255 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 09:15:14.8137 debug daemon/server-router/TUN writer : -> TUN icmp 10.43.0.0 -> 10.43.255.255, type destination unreachable, code 3 2021/09/16 09:15:14.8138 debug daemon/server-router/TUN writer : -> TUN icmp 10.42.0.0 -> 10.42.0.255, type destination unreachable, code 3 2021/09/16 09:15:14.8139 debug daemon/server-router/TUN writer : -> TUN icmp 1.2.3.4 -> 1.2.3.4, type destination unreachable, code 3 2021/09/16 10:11:43.8374 info daemon/server-grpc/conn=2 : setting search paths ambassador default kube-node-lease kube-public kube-system 2021/09/16 10:11:43.8466 info daemon/server-grpc/conn=2 : started command ["killall" "-HUP" "mDNSResponder"] : dexec.pid="30754" 2021/09/16 10:11:43.8467 info daemon/server-grpc/conn=2 : : dexec.pid="30754" dexec.stream="stdin" dexec.err="EOF" 2021/09/16 10:11:43.8539 info daemon/server-grpc/conn=2 : finished successfully: exit status 0 : dexec.pid="30754" 2021/09/16 10:11:43.8562 info daemon/server-grpc/conn=2 : started command ["killall" "mDNSResponderHelper"] : dexec.pid="30755" 2021/09/16 10:11:43.8563 info daemon/server-grpc/conn=2 : : dexec.pid="30755" dexec.stream="stdin" dexec.err="EOF" 2021/09/16 10:11:43.8647 info daemon/server-grpc/conn=2 : finished successfully: exit status 0 : dexec.pid="30755" 2021/09/16 10:11:43.8713 info daemon/server-grpc/conn=2 : started command ["dscacheutil" "-flushcache"] : dexec.pid="30756" 2021/09/16 10:11:43.8716 info daemon/server-grpc/conn=2 : : dexec.pid="30756" dexec.stream="stdin" dexec.err="EOF" 2021/09/16 10:11:43.8771 info daemon/server-grpc/conn=2 : finished successfully: exit status 0 : dexec.pid="30756" 2021/09/16 10:11:45.8314 debug daemon/server-dns/Server : QTYPE[1] edhxwytfvigm.cluster.local. -> NOT FOUND 2021/09/16 10:11:45.8317 debug daemon/server-dns/Server : QTYPE[28] edhxwytfvigm.cluster.local. -> NOT FOUND 2021/09/16 10:11:45.8325 debug daemon/server-dns/Server : QTYPE[1] qkeicqx.cluster.local. -> NOT FOUND 2021/09/16 10:11:45.8328 debug daemon/server-dns/Server : QTYPE[28] qkeicqx.cluster.local. -> NOT FOUND 2021/09/16 10:11:45.8331 debug daemon/server-dns/Server : QTYPE[1] ppzsutr.cluster.local. -> NOT FOUND 2021/09/16 10:11:45.8336 debug daemon/server-dns/Server : QTYPE[28] ppzsutr.cluster.local. -> NOT FOUND 2021/09/16 11:48:09.9530 info daemon/server-grpc/conn=2 : setting search paths ambassador default kube-node-lease kube-public kube-system 2021/09/16 11:48:09.9698 info daemon/server-grpc/conn=2 : started command ["killall" "-HUP" "mDNSResponder"] : dexec.pid="36755" 2021/09/16 11:48:09.9701 info daemon/server-grpc/conn=2 : : dexec.pid="36755" dexec.stream="stdin" dexec.err="EOF" 2021/09/16 11:48:09.9828 info daemon/server-grpc/conn=2 : finished successfully: exit status 0 : dexec.pid="36755" 2021/09/16 11:48:09.9855 info daemon/server-grpc/conn=2 : started command ["killall" "mDNSResponderHelper"] : dexec.pid="36756" 2021/09/16 11:48:09.9857 info daemon/server-grpc/conn=2 : : dexec.pid="36756" dexec.stream="stdin" dexec.err="EOF" 2021/09/16 11:48:09.9945 info daemon/server-grpc/conn=2 : : dexec.pid="36756" dexec.stream="stdout+stderr" dexec.data="No matching processes were found\n" 2021/09/16 11:48:09.9947 info daemon/server-grpc/conn=2 : finished with error: exit status 1 : dexec.pid="36756" 2021/09/16 11:48:10.0013 info daemon/server-grpc/conn=2 : started command ["dscacheutil" "-flushcache"] : dexec.pid="36757" 2021/09/16 11:48:10.0015 info daemon/server-grpc/conn=2 : : dexec.pid="36757" dexec.stream="stdin" dexec.err="EOF" 2021/09/16 11:48:10.0062 info daemon/server-grpc/conn=2 : finished successfully: exit status 0 : dexec.pid="36757" 2021/09/16 11:48:11.7403 debug daemon/server-dns/Server : QTYPE[1] nhxiznicnn.cluster.local. -> NOT FOUND 2021/09/16 11:48:11.7406 debug daemon/server-dns/Server : QTYPE[28] nhxiznicnn.cluster.local. -> NOT FOUND 2021/09/16 11:48:11.7409 debug daemon/server-dns/Server : QTYPE[1] ajzmlhkgconl.cluster.local. -> NOT FOUND 2021/09/16 11:48:11.7411 debug daemon/server-dns/Server : QTYPE[28] ajzmlhkgconl.cluster.local. -> NOT FOUND 2021/09/16 11:48:11.7417 debug daemon/server-dns/Server : QTYPE[1] bnasvzj.cluster.local. -> NOT FOUND 2021/09/16 11:48:11.7420 debug daemon/server-dns/Server : QTYPE[28] bnasvzj.cluster.local. -> NOT FOUND 2021/09/16 11:48:13.0571 info daemon/server-grpc/conn=2 : setting search paths ambassador default kube-node-lease kube-public kube-system 2021/09/16 11:48:13.0635 info daemon/server-grpc/conn=2 : started command ["killall" "-HUP" "mDNSResponder"] : dexec.pid="36760" 2021/09/16 11:48:13.0638 info daemon/server-grpc/conn=2 : : dexec.pid="36760" dexec.stream="stdin" dexec.err="EOF" 2021/09/16 11:48:13.0811 info daemon/server-grpc/conn=2 : finished successfully: exit status 0 : dexec.pid="36760" 2021/09/16 11:48:13.0845 info daemon/server-grpc/conn=2 : started command ["killall" "mDNSResponderHelper"] : dexec.pid="36761" 2021/09/16 11:48:13.0847 info daemon/server-grpc/conn=2 : : dexec.pid="36761" dexec.stream="stdin" dexec.err="EOF" 2021/09/16 11:48:13.0942 info daemon/server-grpc/conn=2 : : dexec.pid="36761" dexec.stream="stdout+stderr" dexec.data="No matching processes were found\n" 2021/09/16 11:48:13.0944 info daemon/server-grpc/conn=2 : finished with error: exit status 1 : dexec.pid="36761" 2021/09/16 11:48:13.0966 info daemon/server-grpc/conn=2 : started command ["dscacheutil" "-flushcache"] : dexec.pid="36762" 2021/09/16 11:48:13.0967 info daemon/server-grpc/conn=2 : : dexec.pid="36762" dexec.stream="stdin" dexec.err="EOF" 2021/09/16 11:48:13.0996 info daemon/server-grpc/conn=2 : finished successfully: exit status 0 : dexec.pid="36762" 2021/09/16 11:48:14.5843 debug daemon/server-dns/Server : QTYPE[1] nbwrpojuplzelrv.cluster.local. -> NOT FOUND 2021/09/16 11:48:14.5844 debug daemon/server-dns/Server : QTYPE[28] nbwrpojuplzelrv.cluster.local. -> NOT FOUND 2021/09/16 11:48:14.5847 debug daemon/server-dns/Server : QTYPE[1] bjgcoyxqkza.cluster.local. -> NOT FOUND 2021/09/16 11:48:14.5857 debug daemon/server-dns/Server : QTYPE[28] bjgcoyxqkza.cluster.local. -> NOT FOUND 2021/09/16 11:48:14.5864 debug daemon/server-dns/Server : QTYPE[1] kpvwofabpbmwypp.cluster.local. -> NOT FOUND 2021/09/16 11:48:14.5866 debug daemon/server-dns/Server : QTYPE[28] kpvwofabpbmwypp.cluster.local. -> NOT FOUND 2021/09/16 11:48:31.4011 info daemon/server-grpc/conn=2 : setting search paths ambassador default kube-node-lease kube-public kube-system 2021/09/16 11:48:31.4074 info daemon/server-grpc/conn=2 : started command ["killall" "-HUP" "mDNSResponder"] : dexec.pid="36771" 2021/09/16 11:48:31.4081 info daemon/server-grpc/conn=2 : : dexec.pid="36771" dexec.stream="stdin" dexec.err="EOF" 2021/09/16 11:48:31.4225 info daemon/server-grpc/conn=2 : finished successfully: exit status 0 : dexec.pid="36771" 2021/09/16 11:48:31.4253 info daemon/server-grpc/conn=2 : started command ["killall" "mDNSResponderHelper"] : dexec.pid="36772" 2021/09/16 11:48:31.4256 info daemon/server-grpc/conn=2 : : dexec.pid="36772" dexec.stream="stdin" dexec.err="EOF" 2021/09/16 11:48:31.4342 info daemon/server-grpc/conn=2 : : dexec.pid="36772" dexec.stream="stdout+stderr" dexec.data="No matching processes were found\n" 2021/09/16 11:48:31.4344 info daemon/server-grpc/conn=2 : finished with error: exit status 1 : dexec.pid="36772" 2021/09/16 11:48:31.4369 info daemon/server-grpc/conn=2 : started command ["dscacheutil" "-flushcache"] : dexec.pid="36773" 2021/09/16 11:48:31.4370 info daemon/server-grpc/conn=2 : : dexec.pid="36773" dexec.stream="stdin" dexec.err="EOF" 2021/09/16 11:48:31.4405 info daemon/server-grpc/conn=2 : finished successfully: exit status 0 : dexec.pid="36773" 2021/09/16 11:48:33.9617 debug daemon/server-dns/Server : QTYPE[1] gotaavy.cluster.local. -> NOT FOUND 2021/09/16 11:48:33.9632 debug daemon/server-dns/Server : QTYPE[28] gotaavy.cluster.local. -> NOT FOUND 2021/09/16 11:48:33.9631 debug daemon/server-dns/Server : QTYPE[28] xkpgangnzunnfos.cluster.local. -> NOT FOUND 2021/09/16 11:48:33.9635 debug daemon/server-dns/Server : QTYPE[1] xkpgangnzunnfos.cluster.local. -> NOT FOUND 2021/09/16 11:48:33.9647 debug daemon/server-dns/Server : QTYPE[28] uqvemntl.cluster.local. -> NOT FOUND 2021/09/16 11:48:33.9648 debug daemon/server-dns/Server : QTYPE[1] uqvemntl.cluster.local. -> NOT FOUND 2021/09/16 13:03:01.1865 debug daemon/server-grpc/conn=3 : Received gRPC Quit 2021/09/16 13:03:01.1870 debug daemon/server-router/TUN reader : goroutine "/daemon/server-router/TUN reader" exited without error 2021/09/16 13:03:01.1870 info daemon/daemon-quit : Shutting down connector 2021/09/16 13:03:01.1876 debug daemon/daemon-quit : Sending quit message to connector 2021/09/16 13:03:01.1880 debug daemon/daemon-quit : Connector shutdown complete 2021/09/16 13:03:01.3928 debug daemon/daemon-quit : goroutine "/daemon/daemon-quit" exited without error 2021/09/16 13:03:01.3931 info daemon/server-dns/Server:shutdown_logger : shutting down (gracefully)... 2021/09/16 13:03:01.3931 debug daemon/server-router/MGR stream : goroutine "/daemon/server-router/MGR stream" exited without error 2021/09/16 13:03:01.3932 info daemon/server-router:shutdown_logger : shutting down (gracefully)... 2021/09/16 13:03:01.3933 info daemon/server-dns:shutdown_logger : shutting down (gracefully)... 2021/09/16 13:03:01.3935 error daemon/watch-cluster-info : goroutine "/daemon/watch-cluster-info" exited with error: error when reading WatchClusterInfo: rpc error: code = Unavailable desc = error reading from server: EOF 2021/09/16 13:03:01.3936 info daemon:shutdown_logger : shutting down (gracefully)... 2021/09/16 13:03:01.3934 debug daemon/server-dns/Server/127.0.0.1:54973 : goroutine "/daemon/server-dns/Server/127.0.0.1:54973" exited without error 2021/09/16 13:03:01.3936 debug daemon/server-router/TUN writer : goroutine "/daemon/server-router/TUN writer" exited without error 2021/09/16 13:03:01.3940 debug daemon/server-dns/Server : goroutine "/daemon/server-dns/Server" exited without error 2021/09/16 13:03:01.3941 debug daemon/server-router : goroutine "/daemon/server-router" exited without error 2021/09/16 13:03:01.3948 debug daemon/server-grpc : gRPC server ended 2021/09/16 13:03:01.3950 debug daemon/server-grpc : goroutine "/daemon/server-grpc" exited without error 2021/09/16 13:03:01.4085 info daemon/server-dns : started command ["killall" "-HUP" "mDNSResponder"] : dexec.pid="41973" 2021/09/16 13:03:01.4089 info daemon/server-dns : : dexec.pid="41973" dexec.stream="stdin" dexec.err="EOF" 2021/09/16 13:03:01.4221 info daemon/server-dns : finished successfully: exit status 0 : dexec.pid="41973" 2021/09/16 13:03:01.4250 info daemon/server-dns : started command ["killall" "mDNSResponderHelper"] : dexec.pid="41974" 2021/09/16 13:03:01.4253 info daemon/server-dns : : dexec.pid="41974" dexec.stream="stdin" dexec.err="EOF" 2021/09/16 13:03:01.4335 info daemon/server-dns : : dexec.pid="41974" dexec.stream="stdout+stderr" dexec.data="No matching processes were found\n" 2021/09/16 13:03:01.4336 info daemon/server-dns : finished with error: exit status 1 : dexec.pid="41974" 2021/09/16 13:03:01.4393 info daemon/server-dns : started command ["dscacheutil" "-flushcache"] : dexec.pid="41975" 2021/09/16 13:03:01.4394 info daemon/server-dns : : dexec.pid="41975" dexec.stream="stdin" dexec.err="EOF" 2021/09/16 13:03:01.4431 info daemon/server-dns : finished successfully: exit status 0 : dexec.pid="41975" 2021/09/16 13:03:01.4432 debug daemon/server-dns : goroutine "/daemon/server-dns" exited without error 2021/09/16 13:03:01.4432 debug daemon/background-metriton : goroutine "/daemon/background-metriton" exited without error 2021/09/16 13:03:01.4433 info daemon:shutdown_status : final goroutine statuses: 2021/09/16 13:03:01.4434 info daemon:shutdown_status : /daemon/background-metriton: exited without error 2021/09/16 13:03:01.4434 info daemon:shutdown_status : /daemon/daemon-quit : exited without error 2021/09/16 13:03:01.4434 info daemon:shutdown_status : /daemon/server-dns : exited without error 2021/09/16 13:03:01.4434 info daemon:shutdown_status : /daemon/server-grpc : exited without error 2021/09/16 13:03:01.4434 info daemon:shutdown_status : /daemon/server-router : exited without error 2021/09/16 13:03:01.4435 info daemon:shutdown_status : /daemon/watch-cluster-info : exited with error 2021/09/16 13:03:01.4435 error : error when reading WatchClusterInfo: rpc error: code = Unavailable desc = error reading from server: EOF telepresence: error: error when reading WatchClusterInfo: rpc error: code = Unavailable desc = error reading from server: EOF ================================================ FILE: pkg/client/cli/cmd/testdata/testLogDir/daemon-20210916T130402.log ================================================ 2021-09-16 13:03:18.7998 debug Logging at this level "debug" 2021-09-16 13:03:18.8006 info --- 2021-09-16 13:03:18.8006 info Telepresence daemon v2.4.3-98-g0585bbd1-1631811285 (api v3) starting... 2021-09-16 13:03:18.8006 info PID is 42026 2021-09-16 13:03:18.8006 info 2021-09-16 13:03:18.8007 debug Listener opened 2021-09-16 13:03:18.8379 info daemon/server-grpc : gRPC server started 2021-09-16 13:03:18.8379 debug daemon/server-router/TUN reader : Waiting until manager gRPC is configured 2021-09-16 13:03:18.8379 debug daemon/server-router/MGR stream : Waiting until manager gRPC is configured 2021-09-16 13:03:36.8925 info daemon/server-grpc/conn=2 : Adding also-proxy subnet 1.2.3.4/32 2021-09-16 13:03:36.9395 info daemon/watch-cluster-info : Adding service subnet 10.43.0.0/16 2021-09-16 13:03:36.9397 info daemon/watch-cluster-info : Adding pod subnet 10.42.0.0/24 2021-09-16 13:03:36.9407 info daemon/watch-cluster-info : Setting cluster DNS to 10.43.0.10 2021-09-16 13:03:36.9408 info daemon/watch-cluster-info : Setting cluster domain to "cluster.local." 2021-09-16 13:03:36.9410 debug daemon/server-router/TUN reader : TUN read loop starting 2021-09-16 13:03:36.9413 debug daemon/server-router/MGR stream : MGR read loop starting 2021-09-16 13:03:36.9423 info daemon/server-dns : Generated new /etc/resolver/telepresence.local 2021-09-16 13:03:36.9431 debug daemon/server-dns/SearchPaths : [] -> [ambassador default kube-node-lease kube-public kube-system] 2021-09-16 13:03:36.9455 info daemon/server-dns/SearchPaths : setting search paths ambassador default kube-node-lease kube-public kube-system 2021-09-16 13:03:36.9462 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.default.local 2021-09-16 13:03:36.9477 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.kube-node-lease.local 2021-09-16 13:03:36.9484 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.kube-public.local 2021-09-16 13:03:36.9489 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.kube-system.local 2021-09-16 13:03:36.9495 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.tel2-search.local 2021-09-16 13:03:36.9499 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.ambassador.local 2021-09-16 13:03:36.9516 info daemon/server-dns : started command ["killall" "-HUP" "mDNSResponder"] : dexec.pid="42097" 2021-09-16 13:03:36.9519 info daemon/server-dns : : dexec.pid="42097" dexec.stream="stdin" dexec.err="EOF" 2021-09-16 13:03:36.9643 info daemon/server-dns : finished successfully: exit status 0 : dexec.pid="42097" 2021-09-16 13:03:36.9671 info daemon/server-dns : started command ["killall" "mDNSResponderHelper"] : dexec.pid="42098" 2021-09-16 13:03:36.9673 info daemon/server-dns : : dexec.pid="42098" dexec.stream="stdin" dexec.err="EOF" 2021-09-16 13:03:36.9786 info daemon/server-dns : : dexec.pid="42098" dexec.stream="stdout+stderr" dexec.data="No matching processes were found\n" 2021-09-16 13:03:36.9791 info daemon/server-dns : finished with error: exit status 1 : dexec.pid="42098" 2021-09-16 13:03:36.9815 info daemon/server-dns : started command ["dscacheutil" "-flushcache"] : dexec.pid="42099" 2021-09-16 13:03:36.9817 info daemon/server-dns : : dexec.pid="42099" dexec.stream="stdin" dexec.err="EOF" 2021-09-16 13:03:36.9844 info daemon/server-dns : finished successfully: exit status 0 : dexec.pid="42099" 2021-09-16 13:03:36.9880 debug daemon/server-router/MGR stream : setting tunnel's peer version to 1 2021-09-16 13:03:38.4784 debug daemon/server-dns/Server : QTYPE[1] ykbcmbcwnwgwvjt.cluster.local. -> NOT FOUND 2021-09-16 13:03:38.4786 debug daemon/server-dns/Server : QTYPE[28] ykbcmbcwnwgwvjt.cluster.local. -> NOT FOUND 2021-09-16 13:03:38.4791 debug daemon/server-dns/Server : QTYPE[1] znqoxvhdzp.cluster.local. -> NOT FOUND 2021-09-16 13:03:38.4795 debug daemon/server-dns/Server : QTYPE[28] znqoxvhdzp.cluster.local. -> NOT FOUND 2021-09-16 13:03:38.4802 debug daemon/server-dns/Server : QTYPE[1] lvwmxfvxyahtpa.cluster.local. -> NOT FOUND 2021-09-16 13:03:38.4806 debug daemon/server-dns/Server : QTYPE[28] lvwmxfvxyahtpa.cluster.local. -> NOT FOUND 2021-09-16 13:03:43.0249 debug daemon/server-grpc/conn=3 : Received gRPC Quit 2021-09-16 13:03:43.0256 debug daemon/server-router/TUN reader : goroutine "/daemon/server-router/TUN reader" exited without error 2021-09-16 13:03:43.0257 info daemon/daemon-quit : Shutting down connector 2021-09-16 13:03:43.0269 debug daemon/daemon-quit : Sending quit message to connector 2021-09-16 13:03:43.0279 debug daemon/daemon-quit : Connector shutdown complete 2021-09-16 13:03:43.2331 debug daemon/daemon-quit : goroutine "/daemon/daemon-quit" exited without error 2021-09-16 13:03:43.2334 info daemon/server-dns/Server:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:03:43.2335 debug daemon/server-router/MGR stream : goroutine "/daemon/server-router/MGR stream" exited without error 2021-09-16 13:03:43.2337 debug daemon/server-dns/SearchPaths : goroutine "/daemon/server-dns/SearchPaths" exited without error 2021-09-16 13:03:43.2334 info daemon:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:03:43.2337 info daemon/server-dns:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:03:43.2336 debug daemon/server-router/TUN writer : goroutine "/daemon/server-router/TUN writer" exited without error 2021-09-16 13:03:43.2339 debug daemon/server-dns/Server/127.0.0.1:52776 : goroutine "/daemon/server-dns/Server/127.0.0.1:52776" exited without error 2021-09-16 13:03:43.2337 info daemon/server-router:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:03:43.2343 debug daemon/server-dns/Server : goroutine "/daemon/server-dns/Server" exited without error 2021-09-16 13:03:43.2343 debug daemon/server-router : goroutine "/daemon/server-router" exited without error 2021-09-16 13:03:43.2341 debug daemon/watch-cluster-info : goroutine "/daemon/watch-cluster-info" exited without error 2021-09-16 13:03:43.2359 debug daemon/server-grpc : gRPC server ended 2021-09-16 13:03:43.2361 debug daemon/server-grpc : goroutine "/daemon/server-grpc" exited without error 2021-09-16 13:03:43.2425 info daemon/server-dns : started command ["killall" "-HUP" "mDNSResponder"] : dexec.pid="42103" 2021-09-16 13:03:43.2429 info daemon/server-dns : : dexec.pid="42103" dexec.stream="stdin" dexec.err="EOF" 2021-09-16 13:03:43.2600 info daemon/server-dns : finished successfully: exit status 0 : dexec.pid="42103" 2021-09-16 13:03:43.2630 info daemon/server-dns : started command ["killall" "mDNSResponderHelper"] : dexec.pid="42104" 2021-09-16 13:03:43.2632 info daemon/server-dns : : dexec.pid="42104" dexec.stream="stdin" dexec.err="EOF" 2021-09-16 13:03:43.2722 info daemon/server-dns : : dexec.pid="42104" dexec.stream="stdout+stderr" dexec.data="No matching processes were found\n" 2021-09-16 13:03:43.2724 info daemon/server-dns : finished with error: exit status 1 : dexec.pid="42104" 2021-09-16 13:03:43.2745 info daemon/server-dns : started command ["dscacheutil" "-flushcache"] : dexec.pid="42105" 2021-09-16 13:03:43.2746 info daemon/server-dns : : dexec.pid="42105" dexec.stream="stdin" dexec.err="EOF" 2021-09-16 13:03:43.2779 info daemon/server-dns : finished successfully: exit status 0 : dexec.pid="42105" 2021-09-16 13:03:43.2780 debug daemon/server-dns : goroutine "/daemon/server-dns" exited without error 2021-09-16 13:03:43.2781 debug daemon/background-metriton : goroutine "/daemon/background-metriton" exited without error ================================================ FILE: pkg/client/cli/cmd/testdata/testLogDir/daemon-20210916T130624.log ================================================ 2021-09-16 13:04:02.4495 debug Logging at this level "debug" 2021-09-16 13:04:02.4504 info --- 2021-09-16 13:04:02.4504 info Telepresence daemon v2.4.3-98-g0585bbd1-1631811285 (api v3) starting... 2021-09-16 13:04:02.4504 info PID is 42224 2021-09-16 13:04:02.4504 info 2021-09-16 13:04:02.4506 debug Listener opened 2021-09-16 13:04:02.4883 debug daemon/server-router/TUN reader : Waiting until manager gRPC is configured 2021-09-16 13:04:02.4883 info daemon/server-grpc : gRPC server started 2021-09-16 13:04:02.4883 debug daemon/server-router/MGR stream : Waiting until manager gRPC is configured 2021-09-16 13:04:12.9763 info daemon/server-grpc/conn=2 : Adding also-proxy subnet 1.2.3.4/32 2021-09-16 13:04:13.0247 info daemon/watch-cluster-info : Adding service subnet 10.43.0.0/16 2021-09-16 13:04:13.0249 info daemon/watch-cluster-info : Adding pod subnet 10.42.0.0/24 2021-09-16 13:04:13.0257 info daemon/watch-cluster-info : Setting cluster DNS to 10.43.0.10 2021-09-16 13:04:13.0257 info daemon/watch-cluster-info : Setting cluster domain to "cluster.local." 2021-09-16 13:04:13.0259 debug daemon/server-router/TUN reader : TUN read loop starting 2021-09-16 13:04:13.0263 debug daemon/server-router/MGR stream : MGR read loop starting 2021-09-16 13:04:13.0271 info daemon/server-dns : Generated new /etc/resolver/telepresence.local 2021-09-16 13:04:13.0276 debug daemon/server-dns/SearchPaths : [] -> [ambassador default kube-node-lease kube-public kube-system] 2021-09-16 13:04:13.0278 info daemon/server-dns/SearchPaths : setting search paths ambassador default kube-node-lease kube-public kube-system 2021-09-16 13:04:13.0303 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.tel2-search.local 2021-09-16 13:04:13.0314 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.ambassador.local 2021-09-16 13:04:13.0319 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.default.local 2021-09-16 13:04:13.0325 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.kube-node-lease.local 2021-09-16 13:04:13.0330 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.kube-public.local 2021-09-16 13:04:13.0335 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.kube-system.local 2021-09-16 13:04:13.0340 info daemon/server-dns : started command ["killall" "-HUP" "mDNSResponder"] : dexec.pid="42228" 2021-09-16 13:04:13.0343 info daemon/server-dns : : dexec.pid="42228" dexec.stream="stdin" dexec.err="EOF" 2021-09-16 13:04:13.0485 info daemon/server-dns : finished successfully: exit status 0 : dexec.pid="42228" 2021-09-16 13:04:13.0512 info daemon/server-dns : started command ["killall" "mDNSResponderHelper"] : dexec.pid="42229" 2021-09-16 13:04:13.0514 info daemon/server-dns : : dexec.pid="42229" dexec.stream="stdin" dexec.err="EOF" 2021-09-16 13:04:13.0603 info daemon/server-dns : : dexec.pid="42229" dexec.stream="stdout+stderr" dexec.data="No matching processes were found\n" 2021-09-16 13:04:13.0606 info daemon/server-dns : finished with error: exit status 1 : dexec.pid="42229" 2021-09-16 13:04:13.0627 info daemon/server-dns : started command ["dscacheutil" "-flushcache"] : dexec.pid="42230" 2021-09-16 13:04:13.0629 info daemon/server-dns : : dexec.pid="42230" dexec.stream="stdin" dexec.err="EOF" 2021-09-16 13:04:13.0655 info daemon/server-dns : finished successfully: exit status 0 : dexec.pid="42230" 2021-09-16 13:04:13.1154 debug daemon/server-router/MGR stream : setting tunnel's peer version to 1 2021-09-16 13:04:14.7134 debug daemon/server-dns/Server : QTYPE[1] ilyjgdjnzvuq.cluster.local. -> NOT FOUND 2021-09-16 13:04:14.7139 debug daemon/server-dns/Server : QTYPE[28] ilyjgdjnzvuq.cluster.local. -> NOT FOUND 2021-09-16 13:04:14.7144 debug daemon/server-dns/Server : QTYPE[1] ziwmaszezjymox.cluster.local. -> NOT FOUND 2021-09-16 13:04:14.7144 debug daemon/server-dns/Server : QTYPE[28] ziwmaszezjymox.cluster.local. -> NOT FOUND 2021-09-16 13:04:14.7149 debug daemon/server-dns/Server : QTYPE[1] hdyagrken.cluster.local. -> NOT FOUND 2021-09-16 13:04:14.7155 debug daemon/server-dns/Server : QTYPE[28] hdyagrken.cluster.local. -> NOT FOUND 2021-09-16 13:05:28.7657 debug daemon/server-grpc/conn=3 : Received gRPC Quit 2021-09-16 13:05:28.7661 debug daemon/server-router/TUN reader : goroutine "/daemon/server-router/TUN reader" exited without error 2021-09-16 13:05:28.7662 info daemon/daemon-quit : Shutting down connector 2021-09-16 13:05:28.7666 debug daemon/daemon-quit : Sending quit message to connector 2021-09-16 13:05:28.7672 debug daemon/daemon-quit : Connector shutdown complete 2021-09-16 13:05:28.9692 debug daemon/daemon-quit : goroutine "/daemon/daemon-quit" exited without error 2021-09-16 13:05:28.9695 info daemon/server-dns/Server:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:05:28.9697 debug daemon/server-router/MGR stream : goroutine "/daemon/server-router/MGR stream" exited without error 2021-09-16 13:05:28.9698 debug daemon/server-router/TUN writer : goroutine "/daemon/server-router/TUN writer" exited without error 2021-09-16 13:05:28.9699 debug daemon/server-dns/Server/127.0.0.1:60044 : goroutine "/daemon/server-dns/Server/127.0.0.1:60044" exited without error 2021-09-16 13:05:28.9697 info daemon/server-dns:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:05:28.9698 debug daemon/watch-cluster-info : goroutine "/daemon/watch-cluster-info" exited without error 2021-09-16 13:05:28.9697 debug daemon/server-dns/SearchPaths : goroutine "/daemon/server-dns/SearchPaths" exited without error 2021-09-16 13:05:28.9696 info daemon:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:05:28.9699 info daemon/server-router:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:05:28.9702 debug daemon/server-dns/Server : goroutine "/daemon/server-dns/Server" exited without error 2021-09-16 13:05:28.9710 debug daemon/server-router : goroutine "/daemon/server-router" exited without error 2021-09-16 13:05:28.9712 debug daemon/server-grpc : gRPC server ended 2021-09-16 13:05:28.9713 debug daemon/server-grpc : goroutine "/daemon/server-grpc" exited without error 2021-09-16 13:05:28.9790 info daemon/server-dns : started command ["killall" "-HUP" "mDNSResponder"] : dexec.pid="42537" 2021-09-16 13:05:28.9793 info daemon/server-dns : : dexec.pid="42537" dexec.stream="stdin" dexec.err="EOF" 2021-09-16 13:05:28.9931 info daemon/server-dns : finished successfully: exit status 0 : dexec.pid="42537" 2021-09-16 13:05:28.9956 info daemon/server-dns : started command ["killall" "mDNSResponderHelper"] : dexec.pid="42538" 2021-09-16 13:05:28.9958 info daemon/server-dns : : dexec.pid="42538" dexec.stream="stdin" dexec.err="EOF" 2021-09-16 13:05:29.0076 info daemon/server-dns : : dexec.pid="42538" dexec.stream="stdout+stderr" dexec.data="No matching processes were found\n" 2021-09-16 13:05:29.0079 info daemon/server-dns : finished with error: exit status 1 : dexec.pid="42538" 2021-09-16 13:05:29.0101 info daemon/server-dns : started command ["dscacheutil" "-flushcache"] : dexec.pid="42539" 2021-09-16 13:05:29.0103 info daemon/server-dns : : dexec.pid="42539" dexec.stream="stdin" dexec.err="EOF" 2021-09-16 13:05:29.0140 info daemon/server-dns : finished successfully: exit status 0 : dexec.pid="42539" 2021-09-16 13:05:29.0140 debug daemon/server-dns : goroutine "/daemon/server-dns" exited without error 2021-09-16 13:05:29.0141 debug daemon/background-metriton : goroutine "/daemon/background-metriton" exited without error ================================================ FILE: pkg/client/cli/cmd/testdata/testLogDir/daemon-20210916T130643.log ================================================ 2021-09-16 13:06:24.1581 debug Logging at this level "debug" 2021-09-16 13:06:24.1587 info --- 2021-09-16 13:06:24.1587 info Telepresence daemon v2.4.3-98-g0585bbd1-1631811285 (api v3) starting... 2021-09-16 13:06:24.1587 info PID is 42651 2021-09-16 13:06:24.1587 info 2021-09-16 13:06:24.1590 debug Listener opened 2021-09-16 13:06:24.1952 info daemon/server-grpc : gRPC server started 2021-09-16 13:06:24.1952 debug daemon/server-router/MGR stream : Waiting until manager gRPC is configured 2021-09-16 13:06:24.1952 debug daemon/server-router/TUN reader : Waiting until manager gRPC is configured 2021-09-16 13:06:25.6951 info daemon/server-grpc/conn=2 : Adding also-proxy subnet 1.2.3.4/32 2021-09-16 13:06:25.7534 info daemon/watch-cluster-info : Adding service subnet 10.43.0.0/16 2021-09-16 13:06:25.7535 info daemon/watch-cluster-info : Adding pod subnet 10.42.0.0/24 2021-09-16 13:06:25.7544 info daemon/watch-cluster-info : Setting cluster DNS to 10.43.0.10 2021-09-16 13:06:25.7545 info daemon/watch-cluster-info : Setting cluster domain to "cluster.local." 2021-09-16 13:06:25.7549 debug daemon/server-router/TUN reader : TUN read loop starting 2021-09-16 13:06:25.7552 debug daemon/server-router/MGR stream : MGR read loop starting 2021-09-16 13:06:25.7561 info daemon/server-dns : Generated new /etc/resolver/telepresence.local 2021-09-16 13:06:25.7568 debug daemon/server-dns/SearchPaths : [] -> [ambassador default kube-node-lease kube-public kube-system] 2021-09-16 13:06:25.7595 info daemon/server-dns/SearchPaths : setting search paths ambassador default kube-node-lease kube-public kube-system 2021-09-16 13:06:25.7605 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.default.local 2021-09-16 13:06:25.7614 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.kube-node-lease.local 2021-09-16 13:06:25.7625 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.kube-public.local 2021-09-16 13:06:25.7629 info daemon/server-dns : started command ["killall" "-HUP" "mDNSResponder"] : dexec.pid="42659" 2021-09-16 13:06:25.7630 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.kube-system.local 2021-09-16 13:06:25.7631 info daemon/server-dns : : dexec.pid="42659" dexec.stream="stdin" dexec.err="EOF" 2021-09-16 13:06:25.7635 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.tel2-search.local 2021-09-16 13:06:25.7639 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.ambassador.local 2021-09-16 13:06:25.7785 info daemon/server-dns : finished successfully: exit status 0 : dexec.pid="42659" 2021-09-16 13:06:25.7810 info daemon/server-dns : started command ["killall" "mDNSResponderHelper"] : dexec.pid="42660" 2021-09-16 13:06:25.7812 info daemon/server-dns : : dexec.pid="42660" dexec.stream="stdin" dexec.err="EOF" 2021-09-16 13:06:25.7902 info daemon/server-dns : : dexec.pid="42660" dexec.stream="stdout+stderr" dexec.data="No matching processes were found\n" 2021-09-16 13:06:25.7905 info daemon/server-dns : finished with error: exit status 1 : dexec.pid="42660" 2021-09-16 13:06:25.7929 info daemon/server-dns : started command ["dscacheutil" "-flushcache"] : dexec.pid="42661" 2021-09-16 13:06:25.7931 info daemon/server-dns : : dexec.pid="42661" dexec.stream="stdin" dexec.err="EOF" 2021-09-16 13:06:25.7968 info daemon/server-dns : finished successfully: exit status 0 : dexec.pid="42661" 2021-09-16 13:06:25.8220 debug daemon/server-router/MGR stream : setting tunnel's peer version to 1 2021-09-16 13:06:28.0021 debug daemon/server-dns/Server : QTYPE[1] larefwkqdqh.cluster.local. -> NOT FOUND 2021-09-16 13:06:28.0022 debug daemon/server-dns/Server : QTYPE[28] larefwkqdqh.cluster.local. -> NOT FOUND 2021-09-16 13:06:28.0031 debug daemon/server-dns/Server : QTYPE[1] rybugcmzgv.cluster.local. -> NOT FOUND 2021-09-16 13:06:28.0031 debug daemon/server-dns/Server : QTYPE[28] rybugcmzgv.cluster.local. -> NOT FOUND 2021-09-16 13:06:28.0043 debug daemon/server-dns/Server : QTYPE[1] palcebzx.cluster.local. -> NOT FOUND 2021-09-16 13:06:28.0043 debug daemon/server-dns/Server : QTYPE[28] palcebzx.cluster.local. -> NOT FOUND 2021-09-16 13:06:29.1253 debug daemon/server-grpc/conn=3 : Received gRPC Quit 2021-09-16 13:06:29.1261 debug daemon/server-router/TUN reader : goroutine "/daemon/server-router/TUN reader" exited without error 2021-09-16 13:06:29.1264 info daemon/daemon-quit : Shutting down connector 2021-09-16 13:06:29.1276 debug daemon/daemon-quit : Sending quit message to connector 2021-09-16 13:06:29.1299 debug daemon/daemon-quit : Connector shutdown complete 2021-09-16 13:06:29.3343 debug daemon/daemon-quit : goroutine "/daemon/daemon-quit" exited without error 2021-09-16 13:06:29.3346 info daemon/server-dns/Server:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:06:29.3347 debug daemon/server-router/MGR stream : goroutine "/daemon/server-router/MGR stream" exited without error 2021-09-16 13:06:29.3347 info daemon:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:06:29.3348 info daemon/server-dns:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:06:29.3348 debug daemon/server-dns/SearchPaths : goroutine "/daemon/server-dns/SearchPaths" exited without error 2021-09-16 13:06:29.3350 info daemon/server-router:shutdown_logger : shutting down (gracefully)... 2021-09-16 13:06:29.3350 debug daemon/server-router/TUN writer : goroutine "/daemon/server-router/TUN writer" exited without error 2021-09-16 13:06:29.3352 debug daemon/server-router : goroutine "/daemon/server-router" exited without error 2021-09-16 13:06:29.3350 debug daemon/server-dns/Server/127.0.0.1:60423 : goroutine "/daemon/server-dns/Server/127.0.0.1:60423" exited without error 2021-09-16 13:06:29.3351 debug daemon/watch-cluster-info : goroutine "/daemon/watch-cluster-info" exited without error 2021-09-16 13:06:29.3355 debug daemon/server-dns/Server : goroutine "/daemon/server-dns/Server" exited without error 2021-09-16 13:06:29.3367 debug daemon/server-grpc : gRPC server ended 2021-09-16 13:06:29.3367 debug daemon/server-grpc : goroutine "/daemon/server-grpc" exited without error 2021-09-16 13:06:29.3433 info daemon/server-dns : started command ["killall" "-HUP" "mDNSResponder"] : dexec.pid="42664" 2021-09-16 13:06:29.3435 info daemon/server-dns : : dexec.pid="42664" dexec.stream="stdin" dexec.err="EOF" 2021-09-16 13:06:29.3587 info daemon/server-dns : finished successfully: exit status 0 : dexec.pid="42664" 2021-09-16 13:06:29.3618 info daemon/server-dns : started command ["killall" "mDNSResponderHelper"] : dexec.pid="42665" 2021-09-16 13:06:29.3621 info daemon/server-dns : : dexec.pid="42665" dexec.stream="stdin" dexec.err="EOF" 2021-09-16 13:06:29.3717 info daemon/server-dns : : dexec.pid="42665" dexec.stream="stdout+stderr" dexec.data="No matching processes were found\n" 2021-09-16 13:06:29.3876 info daemon/server-dns : finished with error: exit status 1 : dexec.pid="42665" 2021-09-16 13:06:29.3896 info daemon/server-dns : started command ["dscacheutil" "-flushcache"] : dexec.pid="42666" 2021-09-16 13:06:29.3906 info daemon/server-dns : : dexec.pid="42666" dexec.stream="stdin" dexec.err="EOF" 2021-09-16 13:06:29.3931 info daemon/server-dns : finished successfully: exit status 0 : dexec.pid="42666" 2021-09-16 13:06:29.3931 debug daemon/server-dns : goroutine "/daemon/server-dns" exited without error 2021-09-16 13:06:29.3932 debug daemon/background-metriton : goroutine "/daemon/background-metriton" exited without error ================================================ FILE: pkg/client/cli/cmd/testdata/testLogDir/daemon.log ================================================ 2021-09-16 13:06:43.2401 debug Logging at this level "debug" 2021-09-16 13:06:43.2405 info --- 2021-09-16 13:06:43.2405 info Telepresence daemon v2.4.3-98-g0585bbd1-1631811285 (api v3) starting... 2021-09-16 13:06:43.2406 info PID is 42745 2021-09-16 13:06:43.2406 info 2021-09-16 13:06:43.2407 debug Listener opened 2021-09-16 13:06:43.2737 info daemon/server-grpc : gRPC server started 2021-09-16 13:06:43.2738 debug daemon/server-router/MGR stream : Waiting until manager gRPC is configured 2021-09-16 13:06:43.2738 debug daemon/server-router/TUN reader : Waiting until manager gRPC is configured 2021-09-16 13:06:55.0000 info daemon/server-grpc/conn=2 : Adding also-proxy subnet 1.2.3.4/32 2021-09-16 13:06:55.0459 info daemon/watch-cluster-info : Adding service subnet 10.43.0.0/16 2021-09-16 13:06:55.0460 info daemon/watch-cluster-info : Adding pod subnet 10.42.0.0/24 2021-09-16 13:06:55.0467 info daemon/watch-cluster-info : Setting cluster DNS to 10.43.0.10 2021-09-16 13:06:55.0468 info daemon/watch-cluster-info : Setting cluster domain to "cluster.local." 2021-09-16 13:06:55.0469 debug daemon/server-router/TUN reader : TUN read loop starting 2021-09-16 13:06:55.0473 debug daemon/server-router/MGR stream : MGR read loop starting 2021-09-16 13:06:55.0477 info daemon/server-dns : Generated new /etc/resolver/telepresence.local 2021-09-16 13:06:55.0482 debug daemon/server-dns/SearchPaths : [] -> [ambassador default kube-node-lease kube-public kube-system] 2021-09-16 13:06:55.0483 info daemon/server-dns/SearchPaths : setting search paths ambassador default kube-node-lease kube-public kube-system 2021-09-16 13:06:55.0505 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.kube-system.local 2021-09-16 13:06:55.0514 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.tel2-search.local 2021-09-16 13:06:55.0520 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.ambassador.local 2021-09-16 13:06:55.0528 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.default.local 2021-09-16 13:06:55.0534 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.kube-node-lease.local 2021-09-16 13:06:55.0539 info daemon/server-dns/SearchPaths : Generated new /etc/resolver/telepresence.kube-public.local 2021-09-16 13:06:55.0554 info daemon/server-dns : started command ["killall" "-HUP" "mDNSResponder"] : dexec.pid="42754" 2021-09-16 13:06:55.0557 info daemon/server-dns : : dexec.pid="42754" dexec.stream="stdin" dexec.err="EOF" 2021-09-16 13:06:55.0697 info daemon/server-dns : finished successfully: exit status 0 : dexec.pid="42754" 2021-09-16 13:06:55.0724 info daemon/server-dns : started command ["killall" "mDNSResponderHelper"] : dexec.pid="42755" 2021-09-16 13:06:55.0726 info daemon/server-dns : : dexec.pid="42755" dexec.stream="stdin" dexec.err="EOF" 2021-09-16 13:06:55.0814 info daemon/server-dns : : dexec.pid="42755" dexec.stream="stdout+stderr" dexec.data="No matching processes were found\n" 2021-09-16 13:06:55.0816 info daemon/server-dns : finished with error: exit status 1 : dexec.pid="42755" 2021-09-16 13:06:55.0836 info daemon/server-dns : started command ["dscacheutil" "-flushcache"] : dexec.pid="42756" 2021-09-16 13:06:55.0838 info daemon/server-dns : : dexec.pid="42756" dexec.stream="stdin" dexec.err="EOF" 2021-09-16 13:06:55.0870 info daemon/server-dns : finished successfully: exit status 0 : dexec.pid="42756" 2021-09-16 13:06:55.1605 debug daemon/server-router/MGR stream : setting tunnel's peer version to 1 2021-09-16 13:06:56.8504 debug daemon/server-dns/Server : QTYPE[1] kojjgoxv.cluster.local. -> NOT FOUND 2021-09-16 13:06:56.8506 debug daemon/server-dns/Server : QTYPE[28] kojjgoxv.cluster.local. -> NOT FOUND 2021-09-16 13:06:56.8507 debug daemon/server-dns/Server : QTYPE[1] vuujrnwogkmwdld.cluster.local. -> NOT FOUND 2021-09-16 13:06:56.8513 debug daemon/server-dns/Server : QTYPE[28] vuujrnwogkmwdld.cluster.local. -> NOT FOUND 2021-09-16 13:06:56.8515 debug daemon/server-dns/Server : QTYPE[1] gzmxavhqnmpqzl.cluster.local. -> NOT FOUND 2021-09-16 13:06:56.8517 debug daemon/server-dns/Server : QTYPE[28] gzmxavhqnmpqzl.cluster.local. -> NOT FOUND ================================================ FILE: pkg/client/cli/cmd/testdata/testLogDir/echo-auto-inject-6496f77cbd-n86nc ================================================ time="2021-09-16 17:07:04.8111" level=info msg="Logging at this level \"info\"" time="2021-09-16 17:07:04.8112" level=info msg="Traffic Agent v2.4.2 [pid:1]" time="2021-09-16 17:07:04.8112" level=info msg="{Name:echo-auto-inject Namespace:default PodIP:10.42.0.25 AgentPort:9900 AppMounts:/tel_app_mounts AppPort:8080 ManagerHost:traffic-manager.ambassador ManagerPort:8081}" time="2021-09-16 17:07:04.8257" level=info msg="new agent secrets mount path: /var/run/secrets/kubernetes.io" time="2021-09-16 17:07:04.8298" level=info msg="Connected to Manager v2.4.3-98-g0585bbd1-1631811285" THREAD=/client ================================================ FILE: pkg/client/cli/cmd/testdata/testLogDir/traffic-manager-5c69859f94-g4ntj ================================================ 2021-09-16 17:06:51.8710 info Logging at this level "info" 2021-09-16 17:06:51.8711 info Traffic Manager v2.4.3-98-g0585bbd1-1631811285 [pid:1] 2021-09-16 17:06:51.9889 info Using DNS IP from kube-dns.kube-system 2021-09-16 17:06:51.9959 info Using cluster domain "cluster.local." 2021-09-16 17:06:52.0282 info Extracting service subnet 10.43.0.0/16 from create service error message 2021-09-16 17:06:52.0572 info Deriving subnets from podCIRs of nodes 2021-09-16 17:06:52.0852 info agent-injector : Mutating webhook service is listening on :8443 2021/09/16 17:06:52 Patching synced Node fe5953af-7777-4787-accf-719601327fb6 2021-09-16 17:07:03.4843 info agent-injector : Injecting traffic-agent into pod echo-auto-inject-6496f77cbd-.default ================================================ FILE: pkg/client/cli/cmd/testdata/zipDir/diff_name.log ================================================ logs from diff_name.log ================================================ FILE: pkg/client/cli/cmd/testdata/zipDir/file1.log ================================================ logs from file1.log ================================================ FILE: pkg/client/cli/cmd/testdata/zipDir/file2.log ================================================ logs from file2.log ================================================ FILE: pkg/client/cli/cmd/uninstall.go ================================================ package cmd import ( "errors" "slices" "strings" "github.com/spf13/cobra" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/rpc/v2/connector" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/ann" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/connect" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/progress" "github.com/telepresenceio/telepresence/v2/pkg/grpc" "github.com/telepresenceio/telepresence/v2/pkg/ioutil" ) const allAgentsFlag = "all-agents" type uninstallCommand struct { agent bool allAgents bool } func uninstall() *cobra.Command { ui := &uninstallCommand{} cmd := &cobra.Command{ Use: "uninstall [flags] ", Args: ui.args, Short: "Uninstall telepresence agents", RunE: ui.run, Annotations: map[string]string{ ann.Session: ann.Required, }, ValidArgsFunction: validWorkloads, } flags := cmd.Flags() flags.BoolVarP(&ui.allAgents, allAgentsFlag, "a", false, "uninstall intercept agent on all workloads") // Hidden from help but will yield a deprecation warning if used flags.BoolVarP(&ui.agent, "agent", "d", false, "") flags.Lookup("agent").Hidden = true return cmd } func (u *uninstallCommand) args(_ *cobra.Command, args []string) error { if len(args) > 0 { if u.allAgents { return errors.New("--all-agents cannot be used with additional arguments") } } else if !u.allAgents { return errors.New("please specify at least one workload or use or --all-agents") } return nil } // uninstall. func (u *uninstallCommand) run(cmd *cobra.Command, args []string) error { if u.agent { ioutil.Println(cmd.OutOrStderr(), "--agent is deprecated (it's the default, so the flag has no effect)") } if err := connect.InitCommand(cmd); err != nil { return err } defer progress.Stop(cmd.Context()) ur := &connector.UninstallRequest{ UninstallType: 0, } if u.allAgents { ur.UninstallType = connector.UninstallRequest_ALL_AGENTS } else { ur.UninstallType = connector.UninstallRequest_NAMED_AGENTS ur.Agents = args } ctx := cmd.Context() _, err := daemon.MustGetUserClient(ctx).Uninstall(ctx, ur) return grpc.FromGRPC(err) } func validWorkloads(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // Trace level is used here, because we generally don't want to log expansion attempts // in the cli.log clog.Tracef(cmd.Context(), "toComplete = %s, args = %v", toComplete, args) all, _ := cmd.Flags().GetBool(allAgentsFlag) if all { return nil, cobra.ShellCompDirectiveNoFileComp } if err := connect.InitCommand(cmd); err != nil { clog.Debug(cmd.Context(), err) return nil, cobra.ShellCompDirectiveError } req := connector.ListRequest{ Filter: connector.ListRequest_INSTALLED_AGENTS, } ctx := cmd.Context() r, err := daemon.MustGetUserClient(ctx).List(ctx, &req) if err != nil { clog.Debugf(ctx, "unable to get list of workloads with agents: %v", err) return nil, cobra.ShellCompDirectiveError } list := make([]string, 0) for _, w := range r.Workloads { // only suggest strings that start with the string were autocompleting if strings.HasPrefix(w.Name, toComplete) && !slices.Contains(args, w.Name) { list = append(list, w.Name) } } return list, cobra.ShellCompDirectiveNoFileComp } ================================================ FILE: pkg/client/cli/cmd/usage.go ================================================ package cmd import ( "fmt" "os" "strconv" "strings" "github.com/moby/term" "github.com/spf13/cobra" "github.com/spf13/pflag" "k8s.io/cli-runtime/pkg/genericclioptions" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/flags" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/global" ) var CLIHelpDocumentationURL = "https://www.telepresence.io" //nolint:gochecknoglobals // extension point const ( helpPlain = `Telepresence can connect to a cluster and route all outbound traffic from your workstation to that cluster so that software running locally can communicate as if it executed remotely, inside the cluster. This is achieved using the command: telepresence connect Telepresence can also intercept traffic intended for a specific service in a cluster and redirect it to your local workstation: telepresence intercept Telepresence uses a background process, the user daemon, to manage the cluster session, and a system service, the root daemon, to modify the workstation's network and DNS so that the cluster's services become available locally. If Telepresence was installed as a standalone binary, the system service will not be present. A root daemon must then be started using sudo, which may result in a password prompt.` helpMarkdown = `Telepresence can connect to a cluster and route all outbound traffic from your workstation to that cluster so that software running locally can communicate as if it executed remotely, inside the cluster. This is achieved using the command: ` + "```bash" + ` telepresence connect ` + "```" + ` Telepresence can also intercept traffic intended for a specific service in a cluster and redirect it to your local workstation: ` + "```bash" + ` telepresence intercept ` + "```" + ` Telepresence uses a background process, the user daemon, to manage the cluster session, and a system service, the root daemon, to modify the workstation's network and DNS so that the cluster's services become available locally. If Telepresence was installed as a standalone binary, the system service will not be present. A root daemon must then be started using sudo, which may result in a password prompt.` usagePlain = `Usage: {{- if .HasAvailableSubCommands}} {{.CommandPath}} [command] [flags] {{- else}} {{- if .Runnable}} {{.UseLine}} {{- end}} {{- end}} {{- if gt (len .Aliases) 0}} Aliases: {{.NameAndAliases}} {{- end}} {{- if .HasExample}} Examples: {{.Example}} {{- end}} {{- if .HasAvailableSubCommands}} Available Commands: {{- range .Commands}} {{- if (or .IsAvailableCommand (eq .Name "help")) }} {{rpad .Name .NamePadding }} {{.Short}} {{- end}} {{- end}} {{- end}} {{- if .HasAvailableLocalFlags}} {{- range flags .}} {{.Name}}: {{wrappedFlagUsages . | trimTrailingWhitespaces}} {{- end}} {{- end}} {{- if hasKubeFlags .}} Kubernetes flags: {{kubeFlags | wrappedFlagUsages | trimTrailingWhitespaces}}{{end}} Global Flags: {{globalFlags . | wrappedFlagUsages | trimTrailingWhitespaces}} {{- if .HasHelpSubCommands}} Additional help topics: {{- range .Commands}} {{- if .IsAdditionalHelpTopicCommand}} {{rpad .CommandPath .CommandPathPadding}} {{.Short}} {{- end}} {{- end}} {{- end}} {{- if .HasAvailableSubCommands}} Use "{{.CommandPath}} [command] --help" for more information about a command. For complete documentation and quick-start guides, check out our website at {{ getDocumentationURL }} {{- end}} ` usageMarkdown = `### Usage: ` + "```" + ` {{- if .HasAvailableSubCommands}} {{.CommandPath}} [command] [flags] {{- else}} {{- if .Runnable}} {{.UseLine}} {{- end}} {{- end}} ` + "```" + ` {{- if gt (len .Aliases) 0}} ### Aliases: {{.NameAndAliases}} {{- end}} {{- if .HasExample}} ### Examples: ` + "```" + ` {{.Example}} ` + "```" + ` {{- end}} {{- if .HasAvailableSubCommands}} ### Available Commands: | Command | Description | |---------|-------------| {{- range .Commands}} {{- if .IsAvailableCommand }} | {{commandLink .}} | {{.Short}} | {{- end}} {{- end}} {{- end}} {{- if .HasAvailableLocalFlags}} {{- range flags .}} ### {{.Name}}: ` + "```" + ` {{wrappedFlagUsages . | trimTrailingWhitespaces}} ` + "```" + ` {{- end}} {{- end}} {{- if hasKubeFlags .}} ### Kubernetes flags: ` + "```" + ` {{kubeFlags | wrappedFlagUsages | trimTrailingWhitespaces}} ` + "```" + ` {{- end}} ### Global Flags: ` + "```" + ` {{globalFlags . | wrappedFlagUsages | trimTrailingWhitespaces}} ` + "```" + ` {{- if .HasHelpSubCommands}} ### Additional help topics: ` + "```" + ` {{- range .Commands}} {{- if .IsAdditionalHelpTopicCommand}} {{rpad .CommandPath .CommandPathPadding}} {{.Short}} {{- end}} {{- end}} ` + "```" + ` {{- end}} {{- if .HasAvailableSubCommands}} Use ` + "`{{.CommandPath}} [command] --help`" + ` for more information about a command. {{- end}} ` ) func flagEqual(a, b *pflag.Flag) bool { if a == b { return true } if a == nil || b == nil { return false } return a.Name == b.Name && a.Usage == b.Usage && a.Hidden == b.Hidden } func localFlags(cmd *cobra.Command, exclude ...*pflag.FlagSet) []*pflag.FlagSet { ngFlags := pflag.NewFlagSet("Flags", pflag.ContinueOnError) extra := flags.GetFlagSets(cmd.Context()) if len(extra) > 0 { exclude = append(exclude, extra...) } cmd.Flags().VisitAll(func(flag *pflag.Flag) { for _, ex := range exclude { if flagEqual(flag, ex.Lookup(flag.Name)) { return } } ngFlags.AddFlag(flag) }) return append([]*pflag.FlagSet{ngFlags}, extra...) } func kubeFlags() *pflag.FlagSet { pflag.NewFlagSet("Kubernetes flags", 0) kubeConfig := genericclioptions.NewConfigFlags(false) kubeConfig.Namespace = nil // "connect", don't take --namespace kfs := pflag.NewFlagSet("Kubernetes flags", 0) kubeConfig.AddFlags(kfs) return kfs } func hasKubeFlags(cmd *cobra.Command) bool { yep := true flags := cmd.Flags() kubeFlags().VisitAll(func(flag *pflag.Flag) { if yep && !flagEqual(flag, flags.Lookup(flag.Name)) { yep = false } }) return yep } func commandLink(cmd *cobra.Command) string { return fmt.Sprintf("[%s](%s)", cmd.Name(), strings.ReplaceAll(cmd.CommandPath(), " ", "_")) } func addUsageTemplate(cmd *cobra.Command, markdown bool) { cobra.AddTemplateFunc("globalFlags", func(cmd *cobra.Command) *pflag.FlagSet { return global.Flags(cmd.Context(), hasKubeFlags(cmd), markdown) }) cobra.AddTemplateFunc("flags", func(cmd *cobra.Command) []*pflag.FlagSet { return localFlags(cmd, kubeFlags(), global.Flags(cmd.Context(), hasKubeFlags(cmd), markdown)) }) cobra.AddTemplateFunc("hasKubeFlags", hasKubeFlags) cobra.AddTemplateFunc("kubeFlags", kubeFlags) cobra.AddTemplateFunc("commandLink", commandLink) cobra.AddTemplateFunc("wrappedFlagUsages", func(flags *pflag.FlagSet) string { cols := 0 if !markdown { // This is based off of what Docker does (github.com/docker/cli/cli/cobra.go), but is // adjusted // 1. to take a pflag.FlagSet instead of a cobra.interceptCmd, so that we can have flag groups, and // 2. to correct for the ways that Docker upsets me. // Obey COLUMNS if the shell or user sets it. (Docker doesn't do this.) var err error cols, err = strconv.Atoi(os.Getenv("COLUMNS")) if err != nil { // Try to detect the size of the stdout file descriptor. (Docker checks stdin, not stdout.) if ws, err := term.GetWinsize(1); err != nil { // If stdout is a terminal, but we were unable to get its size (I'm not sure how that can // happen), then fall back to assuming 80. If stdout isn't a terminal, then we leave cols // as 0, meaning "don't wrap it". (Docker wraps it even if stdout isn't a terminal.) if term.IsTerminal(1) { cols = 80 } } else { cols = int(ws.Width) } } } return flags.FlagUsagesWrapped(cols) }) cobra.AddTemplateFunc("getDocumentationURL", func() string { return CLIHelpDocumentationURL }) // Set a usage template that is derived from the default but replaces the "Available Commands" // section with the commandGroups() from the given command tpl := usagePlain if markdown { tpl = usageMarkdown } cmd.SetUsageTemplate(tpl) } ================================================ FILE: pkg/client/cli/cmd/version.go ================================================ package cmd import ( "context" "errors" "fmt" "github.com/spf13/cobra" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" empty "google.golang.org/protobuf/types/known/emptypb" "github.com/telepresenceio/telepresence/rpc/v2/common" daemonRpc "github.com/telepresenceio/telepresence/rpc/v2/daemon" "github.com/telepresenceio/telepresence/rpc/v2/manager" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/ann" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/connect" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/progress" tpGrpc "github.com/telepresenceio/telepresence/v2/pkg/grpc" "github.com/telepresenceio/telepresence/v2/pkg/ioutil" "github.com/telepresenceio/telepresence/v2/pkg/proc" ) func versionCmd() *cobra.Command { return &cobra.Command{ Use: "version", Args: cobra.NoArgs, Short: "Show version", RunE: printVersion, Annotations: map[string]string{ ann.UserDaemon: ann.Optional, ann.UpdateCheckFormat: ann.Tel2, }, ValidArgsFunction: cobra.NoFileCompletions, } } func addDaemonVersions(ctx context.Context, kvf *ioutil.KeyValueFormatter) { hasRootDaemon := true userD := daemon.GetUserClient(ctx) if userD != nil { hasRootDaemon = !(proc.IsAdmin() || userD.Containerized()) } if hasRootDaemon { version, err := daemonVersion(ctx) switch { case err == nil: kvf.Add(version.Name, version.Version) case errors.Is(err, daemon.ErrNoRootDaemon): kvf.Add("Root Daemon", "not running") default: kvf.Add("Root Daemon", fmt.Sprintf("error: %v", err)) } } if userD != nil { kvf.Add(userD.Name(), "v"+userD.Semver().String()) vi, err := managerVersion(ctx) switch { case err == nil: kvf.Add(vi.Name, vi.Version) af, err := trafficAgentFQN(ctx) switch status.Code(err) { case codes.OK: kvf.Add("Traffic Agent", af.FQN) case codes.Unimplemented: kvf.Add("Traffic Agent", "not reported by traffic-manager") case codes.Unavailable: kvf.Add("Traffic Agent", "not currently available") default: kvf.Add("Traffic Agent", fmt.Sprintf("error: %v", err)) } case status.Code(err) == codes.Unavailable: kvf.Add("Traffic Manager", "not connected") default: kvf.Add("Traffic Manager", fmt.Sprintf("error: %v", err)) } } else { kvf.Add("User Daemon", "not running") } } func printVersion(cmd *cobra.Command, _ []string) error { kvf := ioutil.DefaultKeyValueFormatter() kvf.Add(client.DisplayName, client.Version()) var mdErr daemon.MultipleDaemonsError err := connect.InitCommand(cmd) if err != nil { if !errors.As(err, &mdErr) { return err } } defer progress.Stop(cmd.Context()) ctx := cmd.Context() if len(mdErr) > 0 { for _, info := range mdErr { subKvf := &ioutil.KeyValueFormatter{ Indent: kvf.Indent, Separator: kvf.Separator, } udCtx, err := connect.ExistingDaemon(ctx, info) if err != nil { subKvf.Add("User Daemon", fmt.Sprintf("error: %v", err)) } addDaemonVersions(udCtx, subKvf) ud := daemon.MustGetUserClient(udCtx) kvf.Add("Connection "+ud.DaemonID().Name, "\n"+subKvf.String()) _ = ud.Close() } } else { addDaemonVersions(ctx, kvf) } kvf.Println(cmd.OutOrStdout()) return nil } func daemonVersion(ctx context.Context) (*common.VersionInfo, error) { if conn, err := daemon.DialRootDaemon(ctx, false); err == nil { defer conn.Close() return daemonRpc.NewDaemonClient(conn).Version(ctx, &empty.Empty{}) } return nil, daemon.ErrNoRootDaemon } func managerVersion(ctx context.Context) (*common.VersionInfo, error) { if s := daemon.GetSession(ctx); s != nil { mv := s.Info.ManagerVersion return &common.VersionInfo{ Version: mv.Version, Name: mv.Name, }, nil } if userD := daemon.GetUserClient(ctx); userD != nil { mv, err := userD.TrafficManagerVersion(ctx, &empty.Empty{}) return mv, tpGrpc.FromGRPC(err) } return nil, daemon.ErrNoUserDaemon } func trafficAgentFQN(ctx context.Context) (*manager.AgentImageFQN, error) { if userD := daemon.GetUserClient(ctx); userD != nil { ai, err := userD.AgentImageFQN(ctx, &empty.Empty{}) return ai, tpGrpc.FromGRPC(err) } return nil, daemon.ErrNoUserDaemon } ================================================ FILE: pkg/client/cli/cmd/wiretap.go ================================================ package cmd import ( "github.com/spf13/cobra" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/ann" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/intercept" ) func wiretapCmd() *cobra.Command { ic := &intercept.Command{ Wiretap: true, } cmd := &cobra.Command{ Use: "wiretap [flags] [-- ]", Args: cobra.MinimumNArgs(1), Short: "Wiretap a Service", Annotations: map[string]string{ ann.Session: ann.Required, ann.UpdateCheckFormat: ann.Tel2, }, SilenceUsage: true, SilenceErrors: true, RunE: ic.Run, ValidArgsFunction: intercept.ValidArgs, } ic.AddInterceptFlags(cmd) return cmd } ================================================ FILE: pkg/client/cli/connect/connector.go ================================================ package connect import ( "context" "errors" "fmt" "io/fs" "net/netip" "os" "path/filepath" "regexp" "slices" "strconv" "strings" "time" "github.com/blang/semver/v4" "github.com/cenkalti/backoff/v4" "github.com/spf13/cobra" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" "k8s.io/client-go/kubernetes" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/rpc/v2/common" "github.com/telepresenceio/telepresence/rpc/v2/connector" daemonRpc "github.com/telepresenceio/telepresence/rpc/v2/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/global" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/progress" "github.com/telepresenceio/telepresence/v2/pkg/client/docker" "github.com/telepresenceio/telepresence/v2/pkg/client/docker/teleroute" "github.com/telepresenceio/telepresence/v2/pkg/client/k8s" "github.com/telepresenceio/telepresence/v2/pkg/dos" "github.com/telepresenceio/telepresence/v2/pkg/errcat" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" tpGrpc "github.com/telepresenceio/telepresence/v2/pkg/grpc" "github.com/telepresenceio/telepresence/v2/pkg/ioutil" "github.com/telepresenceio/telepresence/v2/pkg/k8sapi" "github.com/telepresenceio/telepresence/v2/pkg/proc" "github.com/telepresenceio/telepresence/v2/pkg/types" ) //nolint:gochecknoglobals // extension point var QuitDaemonFuncs = []func(context.Context){ quitHostConnector, quitDockerDaemons, } // maybeComposeDown performs a `docker compose down -f ` and also deletes `` if it exists. // A full docker compose down is necessary because the created containers are dependent on the network represented by // the connection that is about to close. func maybeComposeDown(ctx context.Context, info *daemon.Info) { if info.ComposeFile == "" { return } defer func() { err := os.Remove(info.ComposeFile) if err != nil { clog.Error(ctx, err) } }() progress.Stop(ctx) err := proc.StdCommand(ctx, docker.Exe, "compose", "--file", info.ComposeFile, "down", "--remove-orphans", "--volumes").Run() if err != nil { clog.Error(ctx, err) } progress.Start(ctx, "Quitting") } func findHostConnectorInfo(ctx context.Context) (*daemon.Info, error) { il := daemon.NewUserInfoLoader(ctx) infos, err := il.LoadInfos() if err != nil { return nil, err } for _, info := range infos { if !info.InDocker() { return info, nil } } return nil, fmt.Errorf("unable to find host connector info: %w", fs.ErrNotExist) } func quitHostConnector(ctx context.Context) { info, err := findHostConnectorInfo(ctx) if err == nil { ctx, err = ExistingDaemon(ctx, info) } progress.Working(ctx, "Quitting") var errs error rootWillContinue := false if err != nil { if !(errors.Is(err, fs.ErrNotExist) || errors.Is(err, daemon.ErrNoUserDaemon)) { errs = errors.Join(errs, err) } } else { ud := daemon.MustGetUserClient(ctx) qr, err := ud.Quit(ctx, &emptypb.Empty{}) if err != nil { errs = errors.Join(errs, tpGrpc.FromGRPC(err)) } else { rootWillContinue = qr.RootDaemonWillContinue } _ = ud.Close() } if !rootWillContinue { // User daemon is responsible for killing the root daemon, but we kill it here too to cater for // the fact that the user daemon might have been killed ungracefully. if conn, err := daemon.DialRootDaemon(ctx, false); err == nil { if _, err = daemonRpc.NewDaemonClient(conn).Quit(ctx, &emptypb.Empty{}); err != nil { errs = errors.Join(errs, err) } _ = conn.Close() } } if errs != nil { _ = progress.MaybeWriteError(ctx, errs) } progress.PrintDone(ctx, "Quit") } func quitDockerDaemons(ctx context.Context) { il := daemon.NewUserInfoLoader(ctx) infos, err := il.LoadInfos() if err != nil { clog.Error(ctx, err) return } for _, info := range infos { maybeComposeDown(ctx, info) ctx := progress.WithEventId(ctx, info.DaemonID().Name) progress.Working(ctx, "Quitting") udCtx, err := ExistingDaemon(ctx, info) if err != nil { if !errors.Is(err, fs.ErrNotExist) { progress.Error(ctx, err.Error()) } continue } ud := daemon.MustGetUserClient(udCtx) _, _ = ud.Quit(ctx, &emptypb.Empty{}) _ = ud.Close() maybeDeleteNetwork(ctx, ud.DaemonInfo()) progress.PrintDone(ctx, "Quit") } } func EnsureUserDaemon(ctx context.Context, required bool) (rc context.Context, err error) { cr := daemon.MustGetRequest(ctx) daemonID, err := daemon.IdentifierFromFlags(ctx, cr.Name, cr.KubeFlags, cr.KubeconfigData, cr.Docker) if err != nil { return ctx, err } ctx = progress.WithEventId(ctx, daemonID.Name) launched := false defer func() { if err == nil && required && !(proc.IsAdmin() || daemon.MustGetUserClient(rc).Containerized()) { // The RootDaemon must be started if the UserDaemon was started err = EnsureRootDaemonRunning(ctx) } if err != nil && !(errors.Is(err, daemon.ErrNoUserDaemon) && !required) { err = progress.MaybeWriteError(ctx, err) } else if launched { progress.PrintDone(ctx, "Launched Daemon") } }() if daemon.GetUserClient(ctx) != nil { return ctx, nil } rc, launched, err = findOrLaunchConnectorDaemon(ctx, daemonID, client.GetExe(ctx), required) return rc, err } func EnsureSession(ctx context.Context, useLine string, required bool) (context.Context, error) { if daemon.GetSession(ctx) != nil { return ctx, nil } rq := daemon.MustGetRequest(ctx) s, err := connectSession(ctx, useLine, rq, required) if err != nil { return ctx, err } if s == nil { return ctx, nil } if s.Started && s.Containerized() { rootCfg, err := s.GetRootClientConfig() if err != nil { return ctx, err } if len(rootCfg.Routing().Subnets) > 0 { err = createTelerouteNetwork(ctx, s.DaemonInfo()) if err != nil { return ctx, err } } } for _, pm := range rq.LocalReroutes { err = ResolveLocalReroute(ctx, s, pm) if err != nil { return ctx, err } } for _, pm := range rq.RemoteReroutes { err = ResolveRemoteReroute(ctx, s, pm) if err != nil { return ctx, err } } return daemon.WithSession(ctx, s), nil } func ResolveLocalReroute(ctx context.Context, ds *daemon.Session, pm string) (err error) { ix := strings.IndexByte(pm, ':') if ix < 1 { return fmt.Errorf("invalid port mapping %s", pm) } localPort, err := types.ParsePortAndProto(pm[:ix]) if err != nil { return fmt.Errorf("invalid port mapping %s: local port %w", pm, err) } hostPort, err := resolveHostPort(ctx, ds, localPort.Proto, pm[ix+1:]) if err != nil { return err } hpb, err := hostPort.MarshalBinary() if err != nil { return err } _, err = ds.RerouteLocalPort(ctx, &daemonRpc.ReroutePortRequest{ DstHostPort: hpb, SrcPort: uint32(localPort.Port), }) return tpGrpc.FromGRPC(err) } func ResolveRemoteReroute(ctx context.Context, ds *daemon.Session, pm string) (err error) { ix := strings.LastIndexByte(pm, ':') if ix < 3 { return fmt.Errorf("invalid port mapping %s", pm) } newPort, err := types.ParsePortAndProto(pm[ix+1:]) if err != nil { return fmt.Errorf("invalid port mapping %s: new port %w", pm, err) } hostPort, err := resolveHostPort(ctx, ds, newPort.Proto, pm[:ix]) if err != nil { return err } hpb, err := hostPort.MarshalBinary() if err != nil { return err } _, err = ds.RerouteRemotePort(ctx, &daemonRpc.ReroutePortRequest{ DstHostPort: hpb, SrcPort: uint32(newPort.Port), }) return tpGrpc.FromGRPC(err) } func resolveHostPort(ctx context.Context, ds *daemon.Session, proto types.Proto, hostPortStr string) (hostPort types.AddrPortProto, err error) { hostPort.Proto = proto hostPort.AddrPort, err = netip.ParseAddrPort(hostPortStr) if err == nil { return hostPort, nil } // Resolve host and port name ix := strings.LastIndexByte(hostPortStr, ':') if ix < 1 { return hostPort, fmt.Errorf("invalid port mapping %s", hostPortStr) } portStr := hostPortStr[ix+1:] if hostPort.Proto != types.ProtoTCP { portStr = fmt.Sprintf("%s%c%s", portStr, types.ProtoSeparator, hostPort.Proto) } rsp, err := ds.ResolvePort(ctx, &daemonRpc.ResolvePortRequest{ Host: hostPortStr[:ix], Port: portStr, }) if err != nil { return hostPort, tpGrpc.FromGRPC(err) } err = hostPort.UnmarshalBinary(rsp.HostPort) return hostPort, err } func ExistingDaemon(ctx context.Context, info *daemon.Info) (context.Context, error) { var conn *grpc.ClientConn var err error if info.InDocker() { // The host relies on that the daemon has exposed a port to localhost conn, err = docker.ConnectDaemon(ctx, info) } else { conn, err = daemon.DialUserDaemon(ctx, false) } if err != nil { return ctx, err } return newUserDaemon(ctx, conn, info) } // Quit shuts down all daemons. func Quit(ctx context.Context) { progress.Start(ctx, "Quitting") defer progress.Stop(ctx) for _, quitFunc := range QuitDaemonFuncs { quitFunc(ctx) } } // Disconnect disconnects from a session in the user daemon. func Disconnect(ctx context.Context) { progress.Start(ctx, "Disconnecting") defer progress.Stop(ctx) if ud := daemon.GetUserClient(ctx); ud == nil { progress.PrintDone(progress.WithEventId(ctx, "daemon"), "Not connected") } else { id := ud.DaemonID() if id == nil { progress.PrintDone(progress.WithEventId(ctx, "daemon"), "Not connected") return } maybeComposeDown(ctx, ud.DaemonInfo()) ctx = progress.WithEventId(ctx, id.Name) progress.Working(ctx, "Disconnecting") _, err := ud.Disconnect(ctx, &emptypb.Empty{}) switch { case err == nil: progress.PrintDone(ctx, "Disconnected") maybeDeleteNetwork(ctx, ud.DaemonInfo()) case status.Code(err) == codes.Unavailable: progress.PrintDone(ctx, "Not connected") default: _ = progress.MaybeWriteError(ctx, fmt.Errorf("failed to disconnect: %w", tpGrpc.FromGRPC(err))) } } } func RunConnect(cmd *cobra.Command, args []string) error { if err := InitCommand(cmd); err != nil { return err } if len(args) == 0 { return nil } ctx := cmd.Context() if daemon.MustGetSession(ctx).Started { defer Disconnect(ctx) } return proc.Run(dos.WithStdio(ctx, cmd), nil, args[0], args[1:]...) } // DiscoverDaemon searches the daemon cache for an entry corresponding to the given name. A connection // to that daemon is returned if such an entry is found. func DiscoverDaemon(ctx context.Context, match *regexp.Regexp, daemonID *daemon.Identifier) (context.Context, error) { cr := daemon.MustGetRequest(ctx) if match == nil && !cr.Implicit { match = regexp.MustCompile(`\A` + regexp.QuoteMeta(daemonID.Name) + `\z`) } il := daemon.NewUserInfoLoader(ctx) info, err := il.LoadMatchingInfo(match) if err != nil { return ctx, err } if len(cr.ExposedPorts) > 0 && !slices.Equal(info.ExposedPorts, cr.ExposedPorts) { return ctx, errcat.User.New("exposed ports differ. Please quit and reconnect") } return ExistingDaemon(ctx, info) } func launchDockerDaemon(ctx context.Context, daemonID *daemon.Identifier, cr *daemon.Request) (context.Context, error) { // Ensure that the logfile is present before the daemon starts so that it isn't created with // permissions from the docker container. logDir := filelocation.AppUserLogDir(ctx) logFile := filepath.Join(logDir, "connector.log") if _, err := os.Stat(logFile); err != nil { if !errors.Is(err, fs.ErrNotExist) { return ctx, err } fh, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY, 0o666) if err != nil { return ctx, err } _ = fh.Close() } _, err := docker.EnsureNetworkPlugin(ctx) if err != nil { return ctx, err } // An initialized kubernetes interface is required by LaunchDaemon, because it is necessary // when checking if the containerized daemon is connecting to a k3s control plane node. kc, err := k8s.NewKubeconfig(ctx, false, cr.KubeFlags, cr.ManagerNamespace, cr.KubeconfigData) if err != nil { return ctx, err } var ki *kubernetes.Clientset ki, err = kubernetes.NewForConfig(kc.RestConfig) if err != nil { return ctx, err } ctx = kc.Context info, conn, err := docker.LaunchDaemon(k8sapi.WithK8sInterface(ctx, ki), daemonID) if err != nil { return ctx, err } return newUserDaemon(ctx, conn, info) } func launchHostDaemon(ctx context.Context, daemonID *daemon.Identifier, connectorDaemon string, cr *daemon.Request) (context.Context, error) { args := []string{connectorDaemon, client.UserDaemonName, "--" + global.FlagConfig, client.GetConfigFile(ctx)} if cr.UserDaemonProfilingPort > 0 { args = append(args, "--pprof", strconv.Itoa(int(cr.UserDaemonProfilingPort))) } if proc.IsAdmin() { // No use having multiple daemons when running as root. args = append(args, "--embed-network") } clog.Debugf(ctx, "Creating daemon info file %s (runs on host, or both CLI and daemon runs in container)", daemonID.Name) fp, err := ioutil.FreePortsTCP(1) if err != nil { return ctx, errcat.NoDaemonLogs.New(err) } info := &daemon.Info{ DaemonPort: fp[0].Port(), Name: daemonID.Name, KubeContext: daemonID.KubeContext, Namespace: daemonID.Namespace, ExposedPorts: cr.ExposedPorts, Hostname: cr.Hostname, } args = append(args, "--address", fmt.Sprintf(":%d", info.DaemonPort)) fn := daemonID.InfoFileName() il := daemon.NewUserInfoLoader(ctx) err = il.SaveInfo(info, fn) if err != nil { return ctx, errcat.NoDaemonLogs.New(err) } defer func() { if err != nil { file := daemonID.InfoFileName() clog.Debugf(ctx, "Deleting daemon info %s due to launch error: %v", file, err) _ = il.DeleteInfo(file) } }() if err = proc.StartInBackground(false, args...); err != nil { return ctx, errcat.NoDaemonLogs.Errorf(err, "failed to launch the connector service") } conn, err := il.DialDaemon(ctx, true) if err != nil { return ctx, errcat.NoDaemonLogs.New(err) } return newUserDaemon(ctx, conn, info) } func findOrLaunchConnectorDaemon(ctx context.Context, daemonID *daemon.Identifier, connectorDaemon string, required bool) (context.Context, bool, error) { cr := daemon.MustGetRequest(ctx) // Try dialing the host daemon using the well-known socket. ctx, err := DiscoverDaemon(ctx, cr.Use, daemonID) if err == nil { ud := daemon.MustGetUserClient(ctx) if ud.Containerized() { cr.Docker = true } if ud.Containerized() == cr.Docker { return ctx, false, nil } // A daemon running on the host does not fulfill a request for a containerized daemon. They can // coexist though. err = os.ErrNotExist } if !errors.Is(err, os.ErrNotExist) { return ctx, false, errcat.NoDaemonLogs.New(err) } if !required { return ctx, false, daemon.ErrNoUserDaemon } ctx = progress.WithEventId(ctx, daemonID.Name) progress.Working(ctx, "Launching Daemon") if err = ensureAppUserCacheDirs(ctx); err != nil { return ctx, false, err } if err = ensureAppUserConfigDir(ctx); err != nil { return ctx, false, err } if cr.Docker { if client.GetConfig(ctx).Intercept().UseFtp { err = errcat.Silent.New("FTP is not supported when using Docker. Please set intercept.useFtp=false in your config.yml and try again.") progress.Error(ctx, err) return ctx, false, err } ctx, err = launchDockerDaemon(ctx, daemonID, cr) } else { ctx, err = launchHostDaemon(ctx, daemonID, connectorDaemon, cr) } return ctx, err == nil, err } // getConnectorVersion is the first call to the user daemon, so a backoff is used here to trap errors // caused during the initial state change of the gRPC connection. func getConnectorVersion(ctx context.Context, cc connector.ConnectorClient) (*common.VersionInfo, error) { b := backoff.NewExponentialBackOff( backoff.WithMaxElapsedTime(3*time.Second), backoff.WithInitialInterval(50*time.Millisecond), backoff.WithMaxInterval(time.Second)) var vi *common.VersionInfo err := backoff.Retry(func() (err error) { quick, cancel := context.WithTimeout(ctx, 50*time.Millisecond) // This is a local call. Should be quick. defer cancel() vi, err = cc.Version(quick, &emptypb.Empty{}) return tpGrpc.FromGRPC(err) }, backoff.WithContext(b, ctx)) return vi, err } func newUserDaemon(ctx context.Context, conn *grpc.ClientConn, info *daemon.Info) (context.Context, error) { vi, err := getConnectorVersion(ctx, connector.NewConnectorClient(conn)) if err != nil { return ctx, err } v, err := semver.Parse(strings.TrimPrefix(vi.Version, "v")) if err != nil { return ctx, fmt.Errorf("unable to parse version obtained from connector daemon: %w", err) } ctx = daemon.WithUserClient(ctx, daemon.NewUserClientFunc(conn, info, v, vi.Name, vi.Executable)) ctx = progress.WithEventId(ctx, info.Name) return ctx, nil } func ensureDaemonVersion(ctx context.Context) error { // Ensure that the already running daemon has the correct version return versionCheck(ctx, client.GetExe(ctx)) } // warn if the version diff between cli and manager is > 3 or if there's an OSS/Enterprise mismatch. func warnMngrVersion(ctx context.Context, ci *connector.ConnectInfo) error { mv := ci.ManagerVersion // remove leading v from semver mSemver, err := semver.Parse(strings.TrimPrefix(mv.Version, "v")) if err != nil { return err } cliSemver := client.Semver() var diff uint64 if cliSemver.Minor > mSemver.Minor { diff = cliSemver.Minor - mSemver.Minor } else { diff = mSemver.Minor - cliSemver.Minor } const maxDiff = uint64(3) if diff > maxDiff { progress.Warningf(ctx, "The Traffic Manager version (%s) is more than %v minor versions diff from client version (%s), please consider upgrading.", mv.Version, maxDiff, client.Version()) } else if diff > 0 { clog.Debugf(ctx, "Diff between client and manager minor versions: %d", diff) } cv := ci.Version if strings.HasPrefix(cv.Name, "OSS ") && !strings.HasPrefix(mv.Name, "OSS ") { progress.Warningf(ctx, "You are using the OSS client %s to connect to an enterprise traffic manager %s. Please consider installing an\n"+ "enterprise client from getambassador.io, or use \"telepresence helm install\" to install an OSS traffic-manager.", cv.Version, mv.Version) } return nil } func connectResult(ctx context.Context, ci *connector.ConnectInfo, withProgress bool) *daemon.Session { err := warnMngrVersion(ctx, ci) if err != nil { clog.Error(ctx, err) } if withProgress { progress.PrintDonef(ctx, "Connected to context %s, namespace %s (%s)", ci.ClusterContext, ci.Namespace, ci.ClusterServer) } return &daemon.Session{Info: ci, Started: ci.Initial} } func connectSession(ctx context.Context, useLine string, request *daemon.Request, required bool) (session *daemon.Session, err error) { userD := daemon.MustGetUserClient(ctx) var ci *connector.ConnectInfo defer func() { if ci != nil { request.KubeFlags = ci.KubeFlags request.ManagerNamespace = ci.ManagerNamespace request.Name = ci.ConnectionName userD.SetConnectionInfo(ci.ConnectionName, ci.ClusterContext, ci.Namespace) } if session != nil { session.UserClient = userD } }() implicitConnect := false if request.Implicit { // implicit calls use the current Status instead of passing flags and mapped namespaces. ci, err = userD.Status(ctx, &emptypb.Empty{}) if err == nil && ci.ManagerVersion == nil { // If the manager version is nil, the user daemon is not connected. This is the same // as it being unavailable when the request is implicit. err = status.Errorf(codes.Unavailable, "user daemon is not connected") } if err == nil { return connectResult(ctx, ci, false), nil } if status.Code(err) != codes.Unavailable { return nil, tpGrpc.FromGRPC(err) } err = nil if required { implicitConnect = true } } if !required { return nil, nil } daemonID := userD.DaemonID() progress.Workingf(ctx, "Connecting to context %s, namespace %s", daemonID.KubeContext, daemonID.Namespace) if implicitConnect { progress.Warningf(ctx, `Warning: You are executing the %q command without a preceding "telepresence connect", causing an implicit `+ "connect to namespace %q. The implicit connect behavior is deprecated and will be removed in a future release.", useLine, daemonID.Namespace) } if ci, err = userD.Connect(ctx, request.ConnectRequest); err != nil { if !userD.Containerized() { file := userD.DaemonID().InfoFileName() clog.Debugf(ctx, "Deleting daemon info %s due to connect error: %v", file, err) _ = daemon.NewUserInfoLoader(ctx).DeleteInfo(file) } return nil, tpGrpc.FromGRPC(err) } return connectResult(ctx, ci, true), nil } func createTelerouteNetwork(ctx context.Context, info *daemon.Info) error { // Make an attempt to create the network with IPv6 enabled. This will fail unless the user has enabled // IPv6 in /etc/docker/daemon.json. cn := info.Name cli, err := docker.GetClient(ctx) if err != nil { return errcat.NoDaemonLogs.New(err) } teleroutePlugin := docker.NetworkPluginName(ctx) teleroutePort := client.GetConfig(ctx).Grpc().TeleroutePort err = teleroute.CreateNetwork(ctx, info, cli, teleroutePlugin, teleroutePort) if err != nil && strings.Contains(err.Error(), fmt.Sprintf("%s already exists", cn)) && teleroute.IsTelerouteNetwork(ctx, cli, cn) { var disconnected []string disconnected, err = teleroute.RemoveNetwork(ctx, cli, cn) if err == nil { err = teleroute.CreateNetwork(ctx, info, cli, teleroutePlugin, teleroutePort) if err == nil { teleroute.ReconnectNetwork(ctx, cli, cn, disconnected) } } } if err != nil { return errcat.NoDaemonLogs.Newf("Unable to create network %s: %v", cn, err) } err = teleroute.NetworkGC(ctx, cli) if err != nil { return errcat.NoDaemonLogs.Newf("Unable to garbage collect teleroute networks: %v", err) } return nil } func maybeDeleteNetwork(ctx context.Context, info *daemon.Info) { // Wait for container exit. dc, err := docker.GetClient(ctx) if err == nil { err = docker.WaitForExit(ctx, dc, info.ContainerID, 3*time.Second) if err == nil { err = teleroute.NetworkGC(ctx, dc) } } if err != nil { clog.Error(ctx, err) } } ================================================ FILE: pkg/client/cli/connect/daemon.go ================================================ package connect import ( "context" "errors" "fmt" "io/fs" "os" "path/filepath" "strconv" "google.golang.org/grpc" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/logging" "github.com/telepresenceio/telepresence/v2/pkg/errcat" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" "github.com/telepresenceio/telepresence/v2/pkg/ioutil" "github.com/telepresenceio/telepresence/v2/pkg/proc" ) func launchDaemon(ctx context.Context, cr *daemon.Request) (info *daemon.RootInfo, err error) { logFile := filepath.Join(filelocation.AppUserLogDir(ctx), "daemon.log") logFile, err = logging.ValidateLogFilePath(logFile) if err != nil { return nil, err } // Ensure that the logfile is present before the daemon starts so that it isn't created with // root permissions. if _, err = os.Stat(logFile); err != nil { if !errors.Is(err, fs.ErrNotExist) { return nil, errcat.NoDaemonLogs.New(err) } fh, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY, 0o600) if err != nil { return nil, errcat.NoDaemonLogs.New(err) } _ = fh.Close() } cacheDir := filelocation.AppUserCacheDir(ctx) daemonDir := filepath.Join(cacheDir, "rootd") _ = os.MkdirAll(daemonDir, 0o777) fp, err := ioutil.FreePortsTCP(1) if err != nil { return nil, errcat.NoDaemonLogs.New(err) } info = &daemon.RootInfo{DaemonPort: fp[0].Port()} err = daemon.NewRootInfoLoader(ctx, false).SaveInfo(info, daemon.InfoFileName) if err != nil { return nil, errcat.NoDaemonLogs.New(err) } addr := fmt.Sprintf(":%d", info.DaemonPort) args := []string{client.GetExe(ctx), client.RootDaemonName, "--cache", cacheDir, "--config", client.GetConfigFile(ctx), "--logfile", logFile, "--address", addr} if cr != nil && cr.RootDaemonProfilingPort > 0 { args = append(args, "--pprof", strconv.Itoa(int(cr.RootDaemonProfilingPort))) } return info, proc.StartInBackgroundAsRoot(ctx, args...) } // EnsureRootDaemonRunning ensures that the daemon is running. func EnsureRootDaemonRunning(ctx context.Context) error { cr := daemon.GetRequest(ctx) if cr != nil && cr.Docker { // Never start root daemon when connecting using a docker container. return nil } if addr := client.GetEnv(ctx).UserDaemonAddress; addr != "" { // Always assume that root daemon is running when a user daemon address is provided return nil } _, err := daemon.LoadRootServiceInfo(ctx) if err == nil { // Root daemon is running as a managed service. return nil } il := daemon.NewRootInfoLoader(ctx, false) _, err = il.LoadInfo(daemon.InfoFileName) if err != nil { _, err = launchDaemon(ctx, cr) if err != nil { return fmt.Errorf("failed to launch the daemon service: %w", err) } } // Wait for the root daemon to be ready var conn *grpc.ClientConn conn, err = il.DialDaemon(ctx, true) if err == nil { _ = conn.Close() } return err } func mkdir(dirType, path string) error { if err := os.MkdirAll(path, 0o700); err != nil { return errcat.NoDaemonLogs.Errorf(err, "unable to ensure that %s directory %q exists", dirType, path) } return nil } func ensureAppUserCacheDirs(ctx context.Context) error { cacheDir := filelocation.AppUserCacheDir(ctx) if err := mkdir("cache", filepath.Join(cacheDir, "daemons")); err != nil { return err } if err := mkdir("cache", filepath.Join(cacheDir, "sessions")); err != nil { return err } return nil } func ensureAppUserConfigDir(ctx context.Context) error { configDir := filelocation.AppUserConfigDir(ctx) err := mkdir("config", configDir) if err != nil { return err } _, err = client.InstallID(ctx) return err } ================================================ FILE: pkg/client/cli/connect/init_command.go ================================================ package connect import ( "context" "errors" "github.com/spf13/cobra" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/ann" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/flags" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/global" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/output" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/progress" "github.com/telepresenceio/telepresence/v2/pkg/dos" "github.com/telepresenceio/telepresence/v2/pkg/errcat" ) func InitProgressWriter(cmd *cobra.Command) { ctx := cmd.Context() mode := progress.ModeAuto if output.WantsFormatted(cmd) { mode = progress.ModeQuiet } else if progress.IsNoOp(ctx) { if pf := cmd.Flag(global.FlagProgress); pf != nil && pf.Changed { mode = progress.Mode(pf.Value.String()) } else if me, ok := dos.LookupEnv(ctx, "TELEPRESENCE_PROGRESS"); ok { mode = progress.Mode(me) } else if pa, ok := cmd.Annotations[ann.Progress]; ok { mode = progress.Mode(pa) } } w := progress.NewWriter(dos.Stdout(ctx), dos.Stderr(ctx), mode) cmd.SetContext(progress.WithContextWriter(ctx, w)) } func InitCommand(cmd *cobra.Command) (err error) { InitProgressWriter(cmd) ctx := cmd.Context() as := cmd.Annotations if v, ok := as[ann.Session]; ok { as[ann.UserDaemon] = v as[ann.VersionCheck] = ann.Required } progressStarted := false defer func() { if progressStarted { progress.Stop(ctx) } }() if v := as[ann.UserDaemon]; v == ann.Optional || v == ann.Required { if cr := daemon.GetRequest(ctx); cr == nil { if ctx, err = daemon.WithDefaultRequest(cmd); err != nil { return err } flags.DeprecationIfChanged(cmd, global.FlagDocker, "use telepresence connect to initiate the connection") flags.DeprecationIfChanged(cmd, global.FlagContext, "use telepresence connect to initiate the connection") } ctx, err = EnsureUserDaemon(ctx, v == ann.Required) if err != nil { if v == ann.Optional && (errors.Is(err, daemon.ErrNoUserDaemon) || errcat.GetCategory(err) == errcat.Config) { // This is OK, but further initialization is not possible err = nil } return err } cmd.SetContext(ctx) } else { // The rest requires a user daemon return nil } if as[ann.VersionCheck] == ann.Required { if err = ensureDaemonVersion(ctx); err != nil { return err } } if v := as[ann.Session]; v == ann.Optional || v == ann.Required { progress.Start(ctx, "Connecting") progressStarted = true ctx, err = EnsureSession(ctx, cmd.UseLine(), v == ann.Required) defer progress.Stop(ctx) if err != nil { return err } cmd.SetContext(ctx) } return nil } func GetOptionalSession(cmd *cobra.Command) (context.Context, *daemon.Session, error) { cmd.Annotations[ann.Session] = ann.Optional err := InitCommand(cmd) if err != nil { return nil, nil, err } ctx := cmd.Context() return ctx, daemon.GetSession(ctx), nil } ================================================ FILE: pkg/client/cli/connect/version_check.go ================================================ package connect import ( "context" "regexp" "strconv" empty "google.golang.org/protobuf/types/known/emptypb" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/dos" "github.com/telepresenceio/telepresence/v2/pkg/errcat" tpGrpc "github.com/telepresenceio/telepresence/v2/pkg/grpc" "github.com/telepresenceio/telepresence/v2/pkg/version" ) var validPrerelRx = regexp.MustCompile(`^[a-z]+\.\d+$`) func versionCheck(ctx context.Context, daemonBinary string) error { if debug, err := strconv.ParseBool(dos.Getenv(ctx, "TELEPRESENCE_DEBUG")); err == nil && debug { return nil } // Ensure that the already running daemons have the correct version userD := daemon.MustGetUserClient(ctx) uv := userD.Semver() if userD.Containerized() { // The user-daemon is remote (in a docker container, most likely). Compare the major, minor, and patch. Only // compare pre-release if it's rc.X or test.X, and don't check if the binaries match. cv := version.Structured if cv.Major == uv.Major && cv.Minor == uv.Minor && cv.Patch == uv.Patch { if len(cv.Pre) != 1 { // Prerelease does not consist of exactly one element, so it either doesn't exist or we don't care about it. return nil } if pv := cv.Pre[0].VersionStr; !validPrerelRx.MatchString(pv) || len(uv.Pre) == 1 && pv == uv.Pre[0].VersionStr { // Either not a prerelease that we care about comparing, or the prerelease was exactly equal. return nil } } return errcat.User.Newf("version mismatch. Client %s != remote user daemon %s", version.Version, uv) } if !version.Structured.EQ(uv) { // OSS Version mismatch. We never allow this return errcat.User.Newf("version mismatch. Client %s != user daemon %s, please run 'telepresence quit -s' and reconnect", version.Version, uv) } if daemonBinary != "" && userD.Executable() != daemonBinary { return errcat.User.Newf("executable mismatch. Connector using %s, configured to use %s, please run 'telepresence quit -s' and reconnect", userD.Executable(), daemonBinary) } vr, err := userD.RootDaemonVersion(ctx, &empty.Empty{}) if err != nil { return tpGrpc.FromGRPC(err) } if version.Version != vr.Version { return errcat.User.Newf("version mismatch. Client %s != Root Daemon %s, please run 'telepresence quit -s' and reconnect", version.Version, vr.Version) } return nil } ================================================ FILE: pkg/client/cli/daemon/dial.go ================================================ package daemon import ( "context" "errors" "google.golang.org/grpc" ) const InfoFileName = "daemon.json" var ErrNoRootDaemon = errors.New("telepresence root daemon is not running") var ErrNoUserDaemon = errors.New("telepresence user daemon is not running") func DialRootDaemon(ctx context.Context, waitForConnect bool) (conn *grpc.ClientConn, err error) { if ri, err := LoadRootServiceInfo(ctx); err == nil { return dialDaemon(ctx, "root", ri.DaemonPort) } return NewRootInfoLoader(ctx, false).DialDaemon(ctx, waitForConnect) } func DialUserDaemon(ctx context.Context, waitForConnect bool) (conn *grpc.ClientConn, err error) { return NewUserInfoLoader(ctx).DialDaemon(ctx, waitForConnect) } ================================================ FILE: pkg/client/cli/daemon/identifier.go ================================================ package daemon import ( "context" "github.com/telepresenceio/telepresence/v2/pkg/client/k8s" "github.com/telepresenceio/telepresence/v2/pkg/ioutil" ) type Identifier struct { Name string KubeContext string Namespace string Containerized bool } func NewIdentifier(name, contextName, namespace string, containerized bool) *Identifier { if namespace == "" { namespace = "default" } if name == "" { if contextName == "" { // Must be an in-cluster config name = "in-cluster-" + namespace } else { name = contextName + "-" + namespace } if containerized { name += "-cn" } } return &Identifier{ KubeContext: contextName, Namespace: namespace, Name: ioutil.SafeName(name), Containerized: containerized, } } func (id *Identifier) String() string { return id.Name } func (id *Identifier) InfoFileName() string { if id.Containerized && id.Name != "" { return id.Name + ".json" } return InfoFileName } func (id *Identifier) ContainerName() string { return id.String() } // IdentifierFromFlags returns a unique name created from the name of the current context // and the active namespace denoted by the given flagMap. func IdentifierFromFlags(ctx context.Context, name string, flagMap map[string]string, kubeConfigData []byte, containerized bool) (*Identifier, error) { cc := flagMap["context"] ns := flagMap["namespace"] if cc == "" || ns == "" { cld, err := k8s.ConfigLoader(ctx, flagMap, kubeConfigData) if err != nil { return nil, err } if ns == "" { ns, _, err = cld.Namespace() if err != nil { return nil, err } } if cc == "" { config, err := cld.RawConfig() if err != nil { return nil, err } cc = config.CurrentContext } } return NewIdentifier(name, cc, ns, containerized), nil } ================================================ FILE: pkg/client/cli/daemon/identifier_test.go ================================================ package daemon_test import ( "testing" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/ioutil" ) func TestDaemonInfoFileName(t *testing.T) { tests := []struct { name string namespace string result string inContainer bool }{ {name: "the-cure", namespace: "ns1", result: "the-cure-ns1-cn.json", inContainer: true}, {name: "arn:aws:eks:us-east-2:914373874199:cluster/test-auth", namespace: "ns1", result: "arn_aws_eks_us-east-2_914373874199_cluster_test-auth-ns1-cn.json", inContainer: true}, {name: "gke_datawireio_us-central1-b_kube-staging-apps-1", namespace: "ns1", result: "gke_datawireio_us-central1-b_kube-staging-apps-1-ns1-cn.json", inContainer: true}, {name: "the-cure", namespace: "ns1", result: "daemon.json", inContainer: false}, } for _, test := range tests { di := daemon.NewIdentifier("", test.name, test.namespace, test.inContainer) result := di.InfoFileName() if result != test.result { t.Fatalf("DaemonInfoFile gave bad output; expected %s got %s", test.result, result) } } } func TestSafeContainerName(t *testing.T) { tests := []struct { name string want string }{ { "@", "a", }, { "@x", "ax", }, { "x@", "x_", }, { "x@y", "x_y", }, { "x™y", // multibyte char "x_y", }, { "x™", // multibyte char "x_", }, { "_y", "ay", }, { "_y_", "ay_", }, // TODO: Add test cases. } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := ioutil.SafeName(tt.name); got != tt.want { t.Errorf("SafeName() = %v, want %v", got, tt.want) } }) } } ================================================ FILE: pkg/client/cli/daemon/info.go ================================================ package daemon import ( "context" "errors" "fmt" "io" "io/fs" "net/netip" "os" "path/filepath" "regexp" "slices" "strings" "time" "github.com/cenkalti/backoff/v4" "github.com/go-json-experiment/json" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/pkg/client/cache" "github.com/telepresenceio/telepresence/v2/pkg/dos" "github.com/telepresenceio/telepresence/v2/pkg/filelocation" grpcClient "github.com/telepresenceio/telepresence/v2/pkg/grpc/client" ) type RootInfo struct { DaemonPort uint16 `json:"daemon_port,omitempty"` } type Info struct { Name string `json:"name,omitempty"` KubeContext string `json:"kube_context,omitempty"` Namespace string `json:"namespace,omitempty"` DaemonPort uint16 `json:"daemon_port,omitempty"` ExposedPorts []string `json:"exposed_ports,omitempty"` Hostname string `json:"hostname,omitempty"` ContainerPID int `json:"container_pid,omitempty"` ContainerIP netip.Addr `json:"container_ip,omitempty"` ContainerID string `json:"container_id,omitempty"` ComposeFile string `json:"compose_file,omitempty"` } type TCPInfo interface { RootInfo | Info } func (info *Info) DaemonID() *Identifier { return NewIdentifier(info.Name, info.KubeContext, info.Namespace, info.InDocker()) } func (info *Info) InDocker() bool { return info.ContainerPID != 0 } func (info *Info) SetConnectionInfo(name string, clusterContext string, namespace string) { info.Name = name info.KubeContext = clusterContext info.Namespace = namespace } const ( daemonsDirName = "userd" rootDaemonsDirName = "rootd" keepAliveInterval = 2 * time.Second maxNoSignOfLife = 3 * keepAliveInterval ) type InfoLoader[T TCPInfo] struct { ctx context.Context dirName string } func NewUserInfoLoader(ctx context.Context) *InfoLoader[Info] { return &InfoLoader[Info]{ctx: ctx, dirName: daemonsDirName} } func NewRootInfoLoader(ctx context.Context, managed bool) *InfoLoader[RootInfo] { if managed { // The root daemon is running as a service, so use the root cache dir ctx = filelocation.WithAppUserCacheDir(ctx, filelocation.RootCacheDir) } return &InfoLoader[RootInfo]{ctx: ctx, dirName: rootDaemonsDirName} } func LoadRootServiceInfo(ctx context.Context) (*RootInfo, error) { return NewRootInfoLoader(ctx, true).LoadInfo(InfoFileName) } func (il *InfoLoader[T]) LoadInfo(file string) (*T, error) { path := filepath.Join(filelocation.AppUserCacheDir(il.ctx), il.dirName, file) f, err := dos.OpenFile(dos.WithLockedFs(il.ctx), path, os.O_RDONLY, 0) if err != nil { return nil, err } defer f.Close() fi, err := f.Stat() if err == nil { err = il.deleteIfStale(file, fi) } if err != nil { return nil, err } jsonContent, err := io.ReadAll(f) if err != nil { return nil, err } var di T if err := json.Unmarshal(jsonContent, &di); err != nil { return nil, fmt.Errorf("failed to parse JSON from file %s: %w", path, err) } return &di, nil } func (il *InfoLoader[T]) SaveInfo(object *T, file string) error { return cache.SaveToUserCache(il.ctx, object, filepath.Join(il.dirName, file), cache.Public) } func (il *InfoLoader[T]) DeleteInfo(file string) error { return cache.DeleteFromUserCache(il.ctx, filepath.Join(il.dirName, file)) } func (il *InfoLoader[T]) InfoExists(file string) (bool, error) { st, err := dos.Stat(dos.WithLockedFs(il.ctx), filepath.Join(filelocation.AppUserCacheDir(il.ctx), il.dirName, file)) if err == nil { err = il.deleteIfStale(file, st) } if err != nil { if errors.Is(err, os.ErrNotExist) { err = nil } return false, err } return true, nil } func (il *InfoLoader[T]) WatchInfos(onChange func(context.Context) error, files ...string) error { return cache.WatchUserCache(il.ctx, il.dirName, onChange, files...) } func (il *InfoLoader[T]) WaitUntilAllVanishes(ttw time.Duration) error { giveUp := time.Now().Add(ttw) for giveUp.After(time.Now()) { files, err := il.infoFiles() if err != nil || len(files) == 0 { return err } time.Sleep(250 * time.Millisecond) } return errors.New("timeout while waiting for daemon files to vanish") } func (il *InfoLoader[T]) DeleteAllInfos() error { files, err := il.infoFiles() if err != nil { return err } for _, file := range files { _ = cache.DeleteFromUserCache(il.ctx, filepath.Join(il.dirName, file.Name())) } return nil } func (il *InfoLoader[T]) LoadInfos() ([]*T, error) { files, err := il.infoFiles() if err != nil { return nil, err } DaemonInfos := make([]*T, len(files)) for i, file := range files { if err = cache.LoadFromUserCache(il.ctx, &DaemonInfos[i], filepath.Join(il.dirName, file.Name())); err != nil { return nil, err } } return DaemonInfos, nil } func (il *InfoLoader[T]) DialDaemon(ctx context.Context, waitForConnect bool) (conn *grpc.ClientConn, err error) { var info *T var cancel context.CancelFunc if waitForConnect { ctx, cancel = context.WithTimeout(ctx, 5*time.Second) defer cancel() err = backoff.Retry(func() error { info, err = il.LoadInfo(InfoFileName) return err }, backoff.WithContext(backoff.NewConstantBackOff(200*time.Millisecond), ctx)) } else { ctx, cancel = context.WithTimeout(ctx, 500*time.Millisecond) info, err = il.LoadInfo(InfoFileName) } defer cancel() if err != nil { return nil, err } var daemonName string var daemonPort uint16 if ii, ok := any(info).(*Info); ok { daemonName = "user" daemonPort = ii.DaemonPort } else { daemonName = "root" daemonPort = (any(info).(*RootInfo)).DaemonPort } conn, err = dialDaemon(ctx, daemonName, daemonPort) if errors.Is(err, context.DeadlineExceeded) && !waitForConnect { // A race may occur where the daemon is shutting down. We found the info file, but the daemon has since stopped responding. if daemonName == "user" { err = ErrNoUserDaemon } else { err = ErrNoRootDaemon } } return conn, err } func dialDaemon(ctx context.Context, name string, port uint16) (conn *grpc.ClientConn, err error) { conn, err = grpcClient.DialGRPC(ctx, fmt.Sprintf("127.0.0.1:%d", port), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithNoProxy()) if err != nil { err = fmt.Errorf("unable to dial %s daemon port %d: %w", name, port, err) } return conn, err } func (il *InfoLoader[T]) infoFiles() ([]fs.DirEntry, error) { files, err := os.ReadDir(filepath.Join(filelocation.AppUserCacheDir(il.ctx), il.dirName)) if err != nil { if errors.Is(err, fs.ErrNotExist) { err = nil } return nil, err } active := make([]fs.DirEntry, 0, len(files)) for _, file := range files { fi, err := file.Info() if err != nil { return nil, err } err = il.deleteIfStale(file.Name(), fi) if err != nil { if errors.Is(err, os.ErrNotExist) { continue } return nil, err } active = append(active, file) } return active, err } func (il *InfoLoader[T]) deleteIfStale(name string, fi fs.FileInfo) error { age := time.Since(fi.ModTime()) if age > maxNoSignOfLife { name = filepath.Join(il.dirName, name) clog.Debugf(il.ctx, "Deleting stale info %s with age = %s", name, age) if err := cache.DeleteFromUserCache(il.ctx, name); err != nil { return err } return fmt.Errorf("%s: %w (file stale and removed)", name, fs.ErrNotExist) } return nil } func infoName[T TCPInfo](info *T) string { if ii, ok := any(info).(*Info); ok { return ii.DaemonID().String() } return "rootd" } type InfoMatchError string func (i InfoMatchError) Error() string { return string(i) } type MultipleDaemonsError []*Info //nolint:errname // Don't want a plural name just because the type is a slice func (m MultipleDaemonsError) Error() string { sb := strings.Builder{} sb.WriteString("multiple daemons are running, please select ") l := len(m) i := 0 if l > 2 { sb.WriteString("one of ") for ; i+2 < l; i++ { sb.WriteString(m[i].DaemonID().Name) sb.WriteString(", ") } } else { sb.WriteString(m[i].DaemonID().Name) i++ } sb.WriteString(" or ") sb.WriteString(m[i].DaemonID().Name) sb.WriteString(" using the --use flag") return sb.String() } // LoadMatchingInfo loads the daemon info matching the given regexp or returns an error if there is none or more than one. func (il *InfoLoader[T]) LoadMatchingInfo(match *regexp.Regexp) (*T, error) { infos, err := il.LoadInfos() if err != nil { return nil, err } if match != nil { infos = slices.DeleteFunc(infos, func(i *T) bool { return !match.MatchString(infoName(i)) }) } switch len(infos) { case 0: if match == nil { err = fmt.Errorf("unable to find daemon info matching %s: %w", match, os.ErrNotExist) } else { err = fmt.Errorf("unable to find daemon info: %w", os.ErrNotExist) } return nil, err case 1: return infos[0], nil default: if iis, ok := any(infos).([]*Info); ok { return nil, MultipleDaemonsError(iis) } return nil, fmt.Errorf("unexpectedly found multiple %T infos", infos[0]) } } // CancelWhenRmFromCache watches for the file to be removed from the cache, then calls cancel. func (il *InfoLoader[T]) CancelWhenRmFromCache(cancel context.CancelFunc, filename string) error { return il.WatchInfos(func(ctx context.Context) error { exists, err := il.InfoExists(filename) if err != nil { return err } if !exists { // spec removed from cache, shut down gracefully clog.Infof(ctx, "daemon file %s removed from cache, shutting down gracefully", filename) cancel() } return nil }, filename) } // KeepInfoAlive updates the access and modification times of the given file // periodically so that it never gets older than keepAliveInterval. This means that // any file with a modification time older than the current time minus three keepAliveIntervals // can be considered stale and should be removed. // // The alive-poll ends, and the file is deleted when the context is canceled. func (il *InfoLoader[T]) KeepInfoAlive(file string) error { daemonFile := filepath.Join(filelocation.AppUserCacheDir(il.ctx), il.dirName, file) ticker := time.NewTicker(keepAliveInterval) defer ticker.Stop() now := time.Now() for { if err := os.Chtimes(daemonFile, now, now); err != nil { if errors.Is(err, fs.ErrNotExist) { // File is removed, so stop trying to update its timestamps clog.Debugf(il.ctx, "Daemon info %s does not exist", daemonFile) return nil } return fmt.Errorf("failed to update timestamp on %s: %w", daemonFile, err) } select { case <-il.ctx.Done(): clog.Debugf(il.ctx, "Deleting daemon info %s because context was cancelled", file) _ = il.DeleteInfo(file) return nil case now = <-ticker.C: } } } ================================================ FILE: pkg/client/cli/daemon/request.go ================================================ package daemon import ( "context" "errors" "fmt" "io" "net/netip" "os" "regexp" "slices" "strconv" "strings" "github.com/spf13/cobra" "github.com/spf13/pflag" "google.golang.org/protobuf/proto" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd/api" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/rpc/v2/connector" "github.com/telepresenceio/telepresence/rpc/v2/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/global" "github.com/telepresenceio/telepresence/v2/pkg/errcat" "github.com/telepresenceio/telepresence/v2/pkg/maps" "github.com/telepresenceio/telepresence/v2/pkg/slice" ) type Request struct { *connector.ConnectRequest // If set, then use a containerized daemon for the connection. Docker bool // Ports exposed by a containerized daemon. Only valid when Docker == true ExposedPorts []string // Hostname used by a containerized daemon. Only valid when Docker == true Hostname string // Match expression to use when finding an existing connection by name Use *regexp.Regexp // Request is created on-demand, not by InitRequest Implicit bool kubeConfig *genericclioptions.ConfigFlags UserDaemonProfilingPort uint16 RootDaemonProfilingPort uint16 // proxyVia holds the string version for the --proxy-via flag values. proxyVia []string // vnats holds the string version for the --vnat flag values. vnats []string // LocalReroutes maps ports on localhost to remote ports. LocalReroutes []string // RemoteReroutes uses the VIF to reroute remote host ports. RemoteReroutes []string // Aliases by which this daemon can be referenced (added to the telepresence network). NetworkAliases []string } type CobraRequest struct { Request kubeFlagSet *pflag.FlagSet } // InitRequest adds the networking flags and Kubernetes flags to the given command and // returns a Request and a FlagSet with the Kubernetes flags. The FlagSet is returned // here so that a map of flags that gets modified can be extracted using FlagMap once the flag // parsing has completed. func InitRequest(cmd *cobra.Command) *CobraRequest { cr := CobraRequest{ Request: Request{ ConnectRequest: &connector.ConnectRequest{}, }, } flags := cmd.Flags() nwFlags := pflag.NewFlagSet("Telepresence networking flags", 0) nwFlags.StringVar(&cr.Name, "name", "", "Optional name to use for the connection") nwFlags.StringSliceVar(&cr.MappedNamespaces, "mapped-namespaces", nil, ``+ `Comma separated list of namespaces considered by DNS resolver and NAT for outbound connections. `+ `Defaults to all namespaces`) nwFlags.StringVar(&cr.ManagerNamespace, "manager-namespace", "", `The namespace where the traffic manager is to be found. `+ `Overrides any other manager namespace set in config`) nwFlags.StringSliceVar(&cr.AlsoProxy, "also-proxy", nil, ``+ `Additional comma separated list of CIDR to proxy`) nwFlags.StringSliceVar(&cr.NeverProxy, "never-proxy", nil, ``+ `Comma separated list of CIDR to never proxy`) nwFlags.StringSliceVar(&cr.vnats, "vnat", nil, ``+ `Use Network Address Translation to create virtual IPs for the given CIDR. CIDR can be substituted for the `+ `symblic name "service", "pods", "also", or "all".`) nwFlags.StringSliceVar(&cr.LocalReroutes, "reroute-local", nil, ``+ `Reroute port on local host to remote host. Format is ::[/{tcp,udp}]. `+ ` can be symbolic when is a service name.`) nwFlags.StringSliceVar(&cr.RemoteReroutes, "reroute-remote", nil, ``+ `Reroute port on remote host. Format is ::[/{tcp,udp}]. `+ ` can be symbolic when is a service name.`) nwFlags.StringSliceVar(&cr.proxyVia, "proxy-via", nil, ``+ `Use Network Address Translation to create virtual IPs for the given CIDR, and route via WORKLOAD. Must be in the `+ `form CIDR=WORKLOAD. CIDR can be substituted for the symblic name "service", "pods", "also", or "all".`) nwFlags.StringSliceVar(&cr.AllowConflictingSubnets, "allow-conflicting-subnets", nil, ``+ `Comma separated list of CIDR that will be allowed to conflict with local subnets`) // Docker flags nwFlags.Bool(global.FlagDocker, false, "Start, or connect to, daemon in a docker container") nwFlags.StringArrayVar(&cr.ExposedPorts, "expose", nil, ``+ `Port that a containerized daemon will expose. See docker run -p for more info. Can be repeated`) nwFlags.StringVar(&cr.Hostname, "hostname", "", ``+ `Hostname used by a containerized daemon`) flags.AddFlagSet(nwFlags) dbgFlags := pflag.NewFlagSet("Debug and Profiling flags", 0) dbgFlags.Uint16Var(&cr.UserDaemonProfilingPort, "userd-profiling-port", 0, "Start a pprof server in the user daemon on this port") _ = dbgFlags.MarkHidden("userd-profiling-port") dbgFlags.Uint16Var(&cr.RootDaemonProfilingPort, "rootd-profiling-port", 0, "Start a pprof server in the root daemon on this port") _ = dbgFlags.MarkHidden("rootd-profiling-port") flags.AddFlagSet(dbgFlags) cr.kubeConfig = genericclioptions.NewConfigFlags(false) cr.KubeFlags = make(map[string]string) cr.kubeFlagSet = pflag.NewFlagSet("Kubernetes flags", 0) cr.kubeConfig.AddFlags(cr.kubeFlagSet) flags.AddFlagSet(cr.kubeFlagSet) _ = cmd.RegisterFlagCompletionFunc("mapped-namespaces", cr.autocompleteNamespaces) _ = cmd.RegisterFlagCompletionFunc("manager-namespace", cr.autocompleteNamespace) _ = cmd.RegisterFlagCompletionFunc("namespace", cr.autocompleteNamespace) _ = cmd.RegisterFlagCompletionFunc("cluster", cr.autocompleteCluster) return &cr } type requestKey struct{} func (cr *CobraRequest) CommitFlags(cmd *cobra.Command) error { var err error cr.kubeFlagSet.VisitAll(func(flag *pflag.Flag) { if flag.Changed { var v string if sv, ok := flag.Value.(pflag.SliceValue); ok { v = slice.AsCSV(sv.GetSlice()) } else { v = flag.Value.String() if flag.Name == "kubeconfig" && v == "-" { // Read kubeconfig from stdin cr.KubeconfigData, err = io.ReadAll(cmd.InOrStdin()) return // kubernetes will not understand "-" } } cr.KubeFlags[flag.Name] = v } }) if err != nil { return err } // A --vnat CIDR is the same as --proxy-via CIDR=local for _, vnat := range cr.vnats { cr.proxyVia = append(cr.proxyVia, vnat+"=local") } err = cr.setGlobalConnectFlags(cmd) if err != nil { return errcat.User.New(err) } ctx, err := cr.Commit(cmd.Context()) if err != nil { return err } cmd.SetContext(ctx) return nil } func (cr *Request) Commit(ctx context.Context) (context.Context, error) { cr.addKubeconfigEnv() var err error cr.SubnetViaWorkloads, err = parseProxyVias(cr.proxyVia) if err != nil { return ctx, errcat.User.New(err) } if len(cr.KubeconfigData) > 0 { kc, err := clientcmd.Load(cr.KubeconfigData) if err != nil { return ctx, fmt.Errorf("unable to parse kubeconfig: %w", err) } if cr.KubeFlags == nil { cr.KubeFlags = make(map[string]string) } if _, ok := cr.KubeFlags["context"]; !ok { cr.KubeFlags["context"] = kc.CurrentContext } if _, ok := cr.KubeFlags["namespace"]; !ok { if currCtx, ok := kc.Contexts[kc.CurrentContext]; ok { cr.KubeFlags["namespace"] = currCtx.Namespace } } // kubernetes will not understand "-" delete(cr.KubeFlags, "kubeconfig") } return context.WithValue(ctx, requestKey{}, cr), nil } type prefixViaWL struct { subnet netip.Prefix symbolic string workload string } func parseProxyVias(proxyVia []string) ([]*daemon.SubnetViaWorkload, error) { l := len(proxyVia) if l == 0 { return nil, nil } pvs := make([]prefixViaWL, 0, l) for _, dps := range proxyVia { dp, err := parseSubnetViaWorkload(dps) if err != nil { return nil, err } lastPvs := len(pvs) - 1 switch dp.symbolic { case "": for pi := lastPvs; pi >= 0; pi-- { pv := pvs[pi] if pv.symbolic == "" && pv.subnet.Overlaps(dp.subnet) { return nil, fmt.Errorf("CIDRs %s and %s are overlapping", pv.subnet, dp.subnet) } } pvs = append(pvs, dp) case "all": for pi := lastPvs; pi >= 0; pi-- { pv := pvs[pi] if pv.symbolic != "" { return nil, fmt.Errorf("CIDRs %s and %s are overlapping", pv.symbolic, dp.symbolic) } } // Normalize by replacing "all" with "also", "pods", and "service" for _, sym := range []string{"also", "pods", "service"} { pvs = append(pvs, prefixViaWL{ symbolic: sym, workload: dp.workload, }) } default: for pi := lastPvs; pi >= 0; pi-- { pv := pvs[pi] if pv.symbolic == dp.symbolic { return nil, fmt.Errorf("CIDRs %s and %s are overlapping", pv.symbolic, dp.symbolic) } } pvs = append(pvs, dp) } } svs := make([]*daemon.SubnetViaWorkload, len(pvs)) for i, pv := range pvs { n := pv.symbolic if n == "" { n = pv.subnet.String() } svs[i] = &daemon.SubnetViaWorkload{ Subnet: n, Workload: pv.workload, } } return svs, nil } func parseSubnetViaWorkload(dps string) (prefixViaWL, error) { var pv prefixViaWL eqIdx := strings.IndexByte(dps, '=') if eqIdx <= 0 { return pv, fmt.Errorf("--proxy-via %q is not in the format CIDR=WORKLOAD", dps) } lhs := dps[:eqIdx] rhs := dps[eqIdx+1:] if errs := validation.IsDNS1123Label(rhs); len(errs) > 0 { return pv, errors.New(errs[0]) } if sn, err := netip.ParsePrefix(lhs); err != nil { if !(lhs == "all" || lhs == "also" || lhs == "pods" || lhs == "service") { return pv, err } pv.symbolic = lhs } else { pv.subnet = sn } pv.workload = rhs return pv, nil } func (cr *Request) addKubeconfigEnv() { // Certain options' default are bound to the connector daemon process; this is notably true of the kubeconfig file(s) to use, // and since those files can be specified, both as a --kubeconfig flag and in the KUBECONFIG setting, and since the flag won't // accept multiple path entries, we need to pass the environment setting to the connector daemon so that it can set it every // time it receives a new config. cr.Environment = make(map[string]string, 2) addEnv := func(key string) { if v, ok := os.LookupEnv(key); ok { cr.Environment[key] = v } else { // A dash prefix in the key means "unset". cr.Environment["-"+key] = "" } } addEnv("KUBECONFIG") addEnv("GOOGLE_APPLICATION_CREDENTIALS") } // setContext deals with the global --context flag and assigns it to KubeFlags because it's // deliberately excluded from the original flags (to avoid conflict with the global flag). func (cr *Request) setGlobalConnectFlags(cmd *cobra.Command) error { if contextFlag := cmd.Flag(global.FlagContext); contextFlag != nil && contextFlag.Changed { cn := contextFlag.Value.String() cr.KubeFlags[global.FlagContext] = cn cr.kubeConfig.Context = &cn } if dockerFlag := cmd.Flag(global.FlagDocker); dockerFlag != nil && dockerFlag.Changed { cr.Docker, _ = strconv.ParseBool(dockerFlag.Value.String()) } if useFlag := cmd.Flag(global.FlagUse); useFlag != nil && useFlag.Changed { var err error if cr.Use, err = regexp.Compile(useFlag.Value.String()); err != nil { return errcat.User.Newf("argument to --use must be a valid regexp: %v", err) } } return nil } func (cr *Request) Clone() *Request { cl := *cr cl.ConnectRequest = proto.Clone(cr.ConnectRequest).(*connector.ConnectRequest) cl.AllowConflictingSubnets = slices.Clone(cr.AllowConflictingSubnets) cl.AlsoProxy = slices.Clone(cr.AlsoProxy) cl.Environment = maps.Copy(cl.Environment) cl.ExposedPorts = slices.Clone(cr.ExposedPorts) cl.KubeFlags = maps.Copy(cl.KubeFlags) cl.MappedNamespaces = slices.Clone(cr.MappedNamespaces) cl.NeverProxy = slices.Clone(cr.NeverProxy) cl.SubnetViaWorkloads = slices.Clone(cr.SubnetViaWorkloads) cl.proxyVia = slices.Clone(cr.proxyVia) return &cl } func GetRequest(ctx context.Context) *Request { if cr, ok := ctx.Value(requestKey{}).(*Request); ok { return cr } return nil } func MustGetRequest(ctx context.Context) *Request { rq := GetRequest(ctx) if rq != nil { return rq } panic("no request in context") } func WithDefaultRequest(cmd *cobra.Command) (context.Context, error) { cr := NewDefaultRequest() cr.Implicit = true cr.kubeConfig.Context = nil // --context is global // Handle deprecated namespace flag, but allow it in the list command. if cmd.Name() != "list" { if nsFlag := cmd.Flag("namespace"); nsFlag != nil && nsFlag.Changed { ns := nsFlag.Value.String() *cr.kubeConfig.Namespace = ns cr.KubeFlags["namespace"] = ns } } ctx := cmd.Context() if err := cr.setGlobalConnectFlags(cmd); err != nil { return ctx, err } return WithRequest(ctx, cr), nil } func WithRequest(ctx context.Context, cr *Request) context.Context { return context.WithValue(ctx, requestKey{}, cr) } func NewDefaultRequest() *Request { cr := Request{ ConnectRequest: &connector.ConnectRequest{ KubeFlags: make(map[string]string), }, kubeConfig: genericclioptions.NewConfigFlags(false), } cr.addKubeconfigEnv() return &cr } func GetKubeStartingConfig(cmd *cobra.Command) (*api.Config, error) { pathOpts := clientcmd.NewDefaultPathOptions() if kcFlag := cmd.Flag("kubeconfig"); kcFlag != nil && kcFlag.Changed { pathOpts.ExplicitFileFlag = kcFlag.Value.String() } return pathOpts.GetStartingConfig() } func (cr *CobraRequest) GetAllNamespaces(cmd *cobra.Command) ([]string, error) { if err := cr.CommitFlags(cmd); err != nil { return nil, err } rs, err := cr.kubeConfig.ToRESTConfig() if err != nil { return nil, errcat.NoDaemonLogs.Newf("ToRESTConfig: %v", err) } cs, err := kubernetes.NewForConfig(rs) if err != nil { return nil, errcat.NoDaemonLogs.Newf("NewForConfig: %v", err) } nsl, err := cs.CoreV1().Namespaces().List(cmd.Context(), v1.ListOptions{}) if err != nil { return nil, errcat.NoDaemonLogs.Newf("Namespaces.List: %v", err) } itms := nsl.Items nss := make([]string, len(itms)) for i, itm := range itms { nss[i] = itm.Name } return nss, nil } func (cr *CobraRequest) autocompleteNamespace(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { clog.Debugf(cmd.Context(), "autocompleteNamespace %q", toComplete) var stripFunc func(s string) bool if toComplete != "" { stripFunc = func(s string) bool { return !strings.HasPrefix(s, toComplete) } } return cr.autocompleteNamespaceFunc(cmd, stripFunc) } func (cr *CobraRequest) autocompleteNamespaces(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { var stripFunc func(s string) bool var pfx string if toComplete != "" { found := strings.Split(toComplete, ",") ll := len(found) - 1 last := found[ll] if ll > 0 { pfx = strings.Join(found[:ll], ",") + "," if last != "" { stripFunc = func(s string) bool { return slices.Contains(found, s) || !strings.HasPrefix(s, last) } } else { stripFunc = func(s string) bool { return slices.Contains(found, s) } } } else { stripFunc = func(s string) bool { return !strings.HasPrefix(s, last) } } } nss, d := cr.autocompleteNamespaceFunc(cmd, stripFunc) if pfx != "" { for i, ns := range nss { nss[i] = pfx + ns } } return nss, d } func (cr *CobraRequest) autocompleteNamespaceFunc(cmd *cobra.Command, stripFunc func(s string) bool) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() nss, err := cr.GetAllNamespaces(cmd) if err != nil { clog.Error(ctx, err) return nil, cobra.ShellCompDirectiveError } if stripFunc != nil { nss = slices.DeleteFunc(nss, stripFunc) } return nss, cobra.ShellCompDirectiveNoFileComp } func (cr *CobraRequest) autocompleteCluster(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() config, err := cr.GetConfig(cmd) if err != nil { clog.Error(ctx, err) return nil, cobra.ShellCompDirectiveError } cxl := config.Clusters cs := make([]string, len(cxl)) i := 0 for n := range cxl { cs[i] = n i++ } return cs, cobra.ShellCompDirectiveNoFileComp } func (cr *CobraRequest) GetConfig(cmd *cobra.Command) (*api.Config, error) { if err := cr.CommitFlags(cmd); err != nil { return nil, err } cfg, err := GetKubeStartingConfig(cmd) if err != nil { return nil, errcat.NoDaemonLogs.Newf("GetKubeStartingConfig: %v", err) } return cfg, nil } ================================================ FILE: pkg/client/cli/daemon/request_test.go ================================================ package daemon import ( "net/netip" "reflect" "testing" "github.com/telepresenceio/telepresence/rpc/v2/daemon" ) func Test_parseSubnetViaWorkload(t *testing.T) { tests := []struct { name string dps string want prefixViaWL wantErr bool }{ { "empty", "", prefixViaWL{}, true, }, { "workload with dot", "127.1.2.3/32=workload.namespace", prefixViaWL{}, true, }, { "invalid subnet", "bad=workload", prefixViaWL{}, true, }, { "ok", "127.1.2.3/32=workload", prefixViaWL{ subnet: netip.MustParsePrefix("127.1.2.3/32"), workload: "workload", }, false, }, { "all", "all=workload", prefixViaWL{ symbolic: "all", workload: "workload", }, false, }, { "also", "also=workload", prefixViaWL{ symbolic: "also", workload: "workload", }, false, }, { "pods", "pods=workload", prefixViaWL{ symbolic: "pods", workload: "workload", }, false, }, { "service", "service=workload", prefixViaWL{ symbolic: "service", workload: "workload", }, false, }, { "other", "other=workload", prefixViaWL{}, true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := parseSubnetViaWorkload(tt.dps) if (err != nil) != tt.wantErr { t.Errorf("parseDomainProxy(%q) error = %v, wantErr %v", tt.dps, err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("parseDomainProxy(%q) got = %v, want %v", tt.dps, got, tt.want) } }) } } func Test_parseProxyVias(t *testing.T) { tests := []struct { name string proxyVia []string want []*daemon.SubnetViaWorkload wantErr bool }{ { name: "single", proxyVia: []string{"127.1.2.0/24=workload"}, want: []*daemon.SubnetViaWorkload{{ Subnet: "127.1.2.0/24", Workload: "workload", }}, wantErr: false, }, { name: "multi", proxyVia: []string{"127.1.2.0/24=workload1", "127.1.3.0/24=workload2"}, want: []*daemon.SubnetViaWorkload{ { Subnet: "127.1.2.0/24", Workload: "workload1", }, { Subnet: "127.1.3.0/24", Workload: "workload2", }, }, wantErr: false, }, { name: "multi-overlap", proxyVia: []string{"127.1.2.0/16=workload1", "127.1.3.0/16=workload2"}, want: nil, wantErr: true, }, { name: "symbolic-overlap", proxyVia: []string{"also=workload1", "also=workload2"}, want: nil, wantErr: true, }, { name: "symbolic-overlap-all", proxyVia: []string{"also=workload1", "all=workload2"}, want: nil, wantErr: true, }, { name: "multi-mixed", proxyVia: []string{"127.1.2.0/16=workload1", "also=workload2"}, want: []*daemon.SubnetViaWorkload{ { Subnet: "127.1.2.0/16", Workload: "workload1", }, { Subnet: "also", Workload: "workload2", }, }, wantErr: false, }, { name: "multi-mixed-all", proxyVia: []string{"127.1.2.0/16=workload1", "all=workload2"}, want: []*daemon.SubnetViaWorkload{ { Subnet: "127.1.2.0/16", Workload: "workload1", }, { Subnet: "also", Workload: "workload2", }, { Subnet: "pods", Workload: "workload2", }, { Subnet: "service", Workload: "workload2", }, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := parseProxyVias(tt.proxyVia) if (err != nil) != tt.wantErr { t.Errorf("parseProxyVias() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("parseProxyVias() got = %v, want %v", got, tt.want) } }) } } ================================================ FILE: pkg/client/cli/daemon/userd.go ================================================ package daemon import ( "context" "errors" "io" "net/netip" "os/exec" "strconv" "strings" "github.com/blang/semver/v4" "github.com/spf13/cobra" "google.golang.org/grpc" grpcCodes "google.golang.org/grpc/codes" grpcStatus "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/rpc/v2/connector" "github.com/telepresenceio/telepresence/rpc/v2/daemon" "github.com/telepresenceio/telepresence/rpc/v2/manager" "github.com/telepresenceio/telepresence/v2/pkg/agentconfig" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/client/k8s" "github.com/telepresenceio/telepresence/v2/pkg/errcat" tpGrpc "github.com/telepresenceio/telepresence/v2/pkg/grpc" ) type UserClient interface { connector.ConnectorClient io.Closer Conn() *grpc.ClientConn Containerized() bool DaemonPort() int DaemonID() *Identifier Executable() string DaemonInfo() *Info Lookup(ctx context.Context, addr string) (netip.Addr, error) Name() string Semver() semver.Version AddHandler(ctx context.Context, id string, cmd *exec.Cmd, containerName string) error SetConnectionInfo(name string, clusterContext string, namespace string) } type userClient struct { connector.ConnectorClient conn *grpc.ClientConn info *Info version semver.Version executable string name string } var NewUserClientFunc = NewUserClient //nolint:gochecknoglobals // extension point func NewUserClient(conn *grpc.ClientConn, info *Info, version semver.Version, name string, executable string) UserClient { return &userClient{ConnectorClient: connector.NewConnectorClient(conn), conn: conn, info: info, version: version, name: name, executable: executable} } type Session struct { UserClient Info *connector.ConnectInfo Started bool } type userDaemonKey struct{} func GetUserClient(ctx context.Context) UserClient { if ud, ok := ctx.Value(userDaemonKey{}).(UserClient); ok { return ud } return nil } func MustGetUserClient(ctx context.Context) UserClient { ud := GetUserClient(ctx) if ud == nil { panic("no user client in context") } return ud } func WithUserClient(ctx context.Context, ud UserClient) context.Context { return context.WithValue(ctx, userDaemonKey{}, ud) } type sessionKey struct{} func GetSession(ctx context.Context) *Session { if s, ok := ctx.Value(sessionKey{}).(*Session); ok { return s } return nil } func MustGetSession(ctx context.Context) *Session { s := GetSession(ctx) if s == nil { panic("no session in context") } return s } func WithSession(ctx context.Context, s *Session) context.Context { return context.WithValue(ctx, sessionKey{}, s) } func (u *userClient) Close() error { return u.conn.Close() } func (u *userClient) Conn() *grpc.ClientConn { return u.conn } func (u *userClient) DaemonInfo() *Info { return u.info } func (u *userClient) Containerized() bool { return u.info != nil && u.info.InDocker() } func (u *userClient) DaemonID() *Identifier { return u.info.DaemonID() } func (u *userClient) Executable() string { return u.executable } func (u *userClient) Lookup(ctx context.Context, name string) (addr netip.Addr, err error) { ipb, err := u.LookupIP(ctx, &daemon.LookupIPRequest{Name: name}) if err != nil { return addr, errcat.User.Errorf(tpGrpc.FromGRPC(err), "unable to resolve name %q", name) } err = addr.UnmarshalBinary(ipb.Ip) if err != nil { return addr, errcat.NoDaemonLogs.New(err) } return addr, nil } func (u *userClient) Name() string { return u.name } func (u *userClient) Semver() semver.Version { return u.version } func (u *userClient) DaemonPort() int { if u.info.InDocker() { addr := u.conn.Target() if lc := strings.LastIndexByte(addr, ':'); lc >= 0 { if port, err := strconv.Atoi(addr[lc+1:]); err == nil { return port } } } return -1 } func (u *userClient) SetConnectionInfo(name string, clusterContext string, namespace string) { u.info.SetConnectionInfo(name, clusterContext, namespace) } func (u *userClient) AddHandler(ctx context.Context, id string, cmd *exec.Cmd, containerName string) error { // setup cleanup for the handler process ior := connector.Interceptor{ InterceptId: id, Pid: int32(cmd.Process.Pid), ContainerName: containerName, } // Send info about the pid and intercept id to the traffic-manager so that it kills // the process if it receives a leave of quit call. if _, err := u.AddInterceptor(ctx, &ior); err != nil { switch grpcStatus.Code(err) { case grpcCodes.NotFound, grpcCodes.Canceled: // The intercept was already deleted or deactivation was caused by a disconnect clog.Infof(ctx, "intercept no longer present when adding container %s as interceptor", containerName) err = nil default: err = tpGrpc.FromGRPC(err) clog.Errorf(ctx, "error adding process with pid %d as interceptor: %v", ior.Pid, err) } _ = cmd.Process.Kill() return err } return nil } func (s *Session) GetAgentConfig(ctx context.Context, workload string) (*agentconfig.Sidecar, error) { agc, err := s.UserClient.GetAgentConfig(ctx, &manager.AgentConfigRequest{Name: workload}) if err != nil { return nil, tpGrpc.FromGRPC(err) } return agentconfig.UnmarshalYAML(agc.Data) } func (s *Session) GetRootClientConfig() (client.Config, error) { return GetRootClientConfig(s.Info.GetDaemonStatus()) } func GetRootClientConfig(ds *daemon.DaemonStatus) (client.Config, error) { data := ds.GetOutboundConfig().GetClientConfig() if data == nil { return nil, errors.New("no outbound config") } return client.UnmarshalJSONConfig(data, false) } // GetCommandKubeConfig will return the fully resolved client.Kubeconfig for the given command. func GetCommandKubeConfig(cmd *cobra.Command) (*k8s.Kubeconfig, error) { ctx := cmd.Context() uc := GetUserClient(ctx) var kc *k8s.Kubeconfig var err error if uc != nil && !cmd.Flag("context").Changed { // Get the context that we're currently connected to. var ci *connector.ConnectInfo ci, err = uc.Status(ctx, &emptypb.Empty{}) if err == nil { kc, err = k8s.NewKubeconfig(ctx, false, map[string]string{"context": ci.ClusterContext}, "", nil) } else { err = tpGrpc.FromGRPC(err) } } else { if GetRequest(ctx) == nil { if ctx, err = WithDefaultRequest(cmd); err != nil { return nil, err } } rq := MustGetRequest(ctx) kc, err = k8s.NewKubeconfig(ctx, false, rq.KubeFlags, rq.ManagerNamespace, rq.KubeconfigData) } return kc, err } ================================================ FILE: pkg/client/cli/docker/auto_complete.go ================================================ package docker import ( "bytes" "os" "os/exec" "regexp" "slices" "strconv" "strings" "github.com/spf13/cobra" "github.com/telepresenceio/telepresence/v2/pkg/client/docker" ) var directiveCodeRx = regexp.MustCompile(`^:(\d)$`) //nolint:gochecknoglobals // constant func AutocompleteRun(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { args = slices.Insert(args, 0, "__completeNoDesc", "run") args = append(args, toComplete) cc := exec.CommandContext(cmd.Context(), docker.Exe, args...) cc.Env = os.Environ() ob := bytes.Buffer{} cc.Stdout = &ob if err := cc.Run(); err != nil { return nil, cobra.ShellCompDirectiveError } names := strings.Fields(ob.String()) if ln := len(names) - 1; ln >= 0 { // The last name is the directive in the form of a colon followed by a digit. if m := directiveCodeRx.FindStringSubmatch(names[ln]); m != nil { n, _ := strconv.Atoi(m[1]) return names[:ln], cobra.ShellCompDirective(n) } } return nil, cobra.ShellCompDirectiveError } ================================================ FILE: pkg/client/cli/docker/compose/config.go ================================================ package compose import ( "context" _ "embed" "errors" "fmt" "os" "slices" "strconv" "strings" "github.com/compose-spec/compose-go/v2/cli" "github.com/compose-spec/compose-go/v2/loader" compose "github.com/compose-spec/compose-go/v2/types" "github.com/go-json-experiment/json" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/cmd/cobraparser/v2/generate" "github.com/telepresenceio/telepresence/cmd/cobraparser/v2/types" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/flags" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/global" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/progress" "github.com/telepresenceio/telepresence/v2/pkg/client/docker" "github.com/telepresenceio/telepresence/v2/pkg/errcat" "github.com/telepresenceio/telepresence/v2/pkg/ioutil" "github.com/telepresenceio/telepresence/v2/pkg/log" "github.com/telepresenceio/telepresence/v2/pkg/proc" ) const ( extensionKey = "x-tele" ) type parentConfig struct { *topLevelExtension // Compose options projectDir string projectName string progress string configPaths []string envFiles []string profiles []string mustBeConnected bool services []string commandFlags *pflag.FlagSet existingProject *compose.Project } type config struct { topLevelExtension *parentConfig subCommandFlags *pflag.FlagSet } func GenerateSubCommands(cmd *cobra.Command) []*cobra.Command { pc := &parentConfig{} uf := cmd.UsageFunc() cmd.SetUsageFunc(func(*cobra.Command) error { cmd.SetContext(flags.WithFlagSets(cmd.Context(), pc.commandFlags)) return uf(cmd) }) pc.addComposeFlags(cmd) commands := make([]*cobra.Command, len(dockerComposeCLI.Subcommands)) for i, subCmd := range dockerComposeCLI.Subcommands { c := &config{parentConfig: pc} sc := c.subCommand(&subCmd) if dryRun := sc.Flags().Lookup("dry-run"); dryRun != nil { dryRun.Hidden = true } commands[i] = sc } return commands } // toProjectOptions was shamelessly copied from https://github.com/docker/compose/blob/main/cmd/compose/compose.go. // Kudos to the Docker Compose CLI authors. func (pc *parentConfig) toProjectOptions(po ...cli.ProjectOptionsFn) (*cli.ProjectOptions, error) { return cli.NewProjectOptions(pc.configPaths, append(po, cli.WithWorkingDirectory(pc.projectDir), // First, apply os.Environment, always win. cli.WithOsEnv, // Load PWD/.env if present and no explicit --env-file has been set. cli.WithEnvFiles(pc.envFiles...), // read the dot-env file to populate the project environment cli.WithDotEnv, // get the compose-file path set by COMPOSE_FILE cli.WithConfigFileEnv, // if none was selected, get the default compose.yaml file from the current dir or parent folder cli.WithDefaultConfigPath, // ... and then, a project directory != PWD maybe has been set, so let's load the .env file cli.WithEnvFiles(pc.envFiles...), cli.WithDotEnv, // eventually COMPOSE_PROFILES should have been set cli.WithDefaultProfiles(pc.profiles...), cli.WithName(pc.projectName))...) } var dockerComposeCLI types.CommandInfo //nolint:gochecknoglobals // this is a constant //go:embed dc-cli.json var dcCli []byte func init() { err := json.Unmarshal(dcCli, &dockerComposeCLI) if err != nil { panic(err) } } func (pc *parentConfig) addComposeFlags(cmd *cobra.Command) { cfs := cmd.Flags() cFlags := pflag.NewFlagSet("Compose flags", pflag.ContinueOnError) cFlags.StringArrayVarP(&pc.configPaths, "file", "f", []string{}, "Compose configuration files") cFlags.StringArrayVar(&pc.envFiles, "env-file", []string{}, "Optional environment files") cFlags.StringVar(&pc.projectDir, "project-directory", "", "Specify an alternate working directory (default: the path of the, first specified, Compose file)") cFlags.StringVar(&pc.projectName, "project-name", "", "Project name") cFlags.StringArrayVar(&pc.profiles, "profile", []string{}, "Profile to enable") cfs.AddFlagSet(cFlags) uf := cmd.UsageFunc() cmd.SetUsageFunc(func(*cobra.Command) error { cmd.SetContext(flags.WithFlagSets(cmd.Context(), cFlags)) return uf(cmd) }) pc.commandFlags = cFlags } func (c *config) subCommand(subCmd *types.CommandInfo) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("%s [flags] [services]", subCmd.Name), Args: cobra.ArbitraryArgs, Short: subCmd.Description, RunE: func(cmd *cobra.Command, args []string) error { c.services = args return c.run(cmd) }, ValidArgsFunction: func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { dir := cobra.ShellCompDirectiveNoFileComp if slices.Contains(os.Args, "--") { dir = cobra.ShellCompDirectiveDefault } return nil, dir }, } c.subCommandFlags = generate.FlagSet(fmt.Sprintf("Compose %s flags", subCmd.Name), subCmd) cmdFlags := cmd.Flags() err := flags.AddUnique(cmdFlags, c.subCommandFlags) if err != nil { ioutil.Printf(os.Stderr, "internal error: %v\n", err) os.Exit(1) } // Stop parsing flags at the first non-flag argument (the service name). This mirrors how // docker compose works, so that e.g. `telepresence compose exec svc wget -qO- ` // doesn't try to interpret `-qO-` as flags for `exec`. cmdFlags.SetInterspersed(false) uf := cmd.UsageFunc() cmd.SetUsageFunc(func(*cobra.Command) error { cmd.SetContext(flags.WithFlagSets(cmd.Context(), c.commandFlags, c.subCommandFlags)) return uf(cmd) }) return cmd } func (c *config) detached() bool { if f := c.subCommandFlags.Lookup("detach"); f != nil { return f.Changed && f.Value.String() == "true" } return false } func (c *config) appendFlags(flags *pflag.FlagSet, opts []string) []string { // Need VisitAll here because Visit doesn't use the Changed status of the actual flag, instead // it keeps track of flags set in the command's FlagSet. flags.VisitAll(func(f *pflag.Flag) { if !f.Changed { return } fv := f.Value switch fv.Type() { case "bool": v := "--" + f.Name if fv.String() == "false" { v += "=false" } opts = append(opts, v) case "stringArray": sv := fv.(pflag.SliceValue) opt := "--" + f.Name for _, v := range sv.GetSlice() { opts = append(opts, opt, v) } default: opts = append(opts, "--"+f.Name, fv.String()) } }) return opts } func (c *config) run(cmd *cobra.Command) (err error) { if dryFlag := cmd.Flag("dry-run"); dryFlag != nil && dryFlag.Changed { // A dry-run is impossible, because Telepresence will have to engage with a workload to get // the data needed to modify the docker compose project. The intercept, replace, ingest, and // wiretap will all install a traffic-agent and cannot be considered a dry-run. return errcat.User.New("--dry-run is not supported") } c.progress = cmd.Flag(global.FlagProgress).Value.String() name := cmd.Name() c.mustBeConnected = true switch name { case "build", "create", "start", "up": c.mustBeConnected = false case "ls", "version": return c.dispatchToCompose(cmd.Context(), name) } ctx, err := daemon.WithDefaultRequest(cmd) if err != nil { return err } // Any resources created here must be canceled when this function returns. ctx, cancel := context.WithCancel(ctx) defer cancel() w := progress.NewWriter(cmd.OutOrStdout(), cmd.ErrOrStderr(), progress.Mode(c.progress)) // Tell the underlying framework to keep quiet. ctx = progress.WithContextWriter(ctx, w) defer func() { progress.Stop(ctx) }() tr, err := c.loadProject(ctx) if err != nil { return errcat.User.New(err) } es := tr.serviceExtensions() if len(es) == 0 { return tr.runCommand(ctx, name) } connections := make(map[string]*connection) if name == "down" { defer func() { progress.Start(ctx, "Disconnecting") for _, cc := range connections { cc.disconnect() } }() } progress.Start(ctx, "Connecting") existingComposeFile, err := c.connect(ctx, es, connections) if err != nil { if c.mustBeConnected && errors.Is(err, daemon.ErrNoUserDaemon) { // The daemon is not running, although the command expects it to. This means that no services should be running either. // So let's just run the command without any extensions so that docker compose produces the expected error output. err = tr.runCommand(ctx, name) } return err } if existingComposeFile != "" { clog.Debugf(ctx, "Existing compose file: %s", existingComposeFile) p, err := loadExistingProject(ctx, c.projectDir, existingComposeFile) if err != nil { return err } c.existingProject = p } g := log.NewGroup(ctx) aesCh := make(chan *engagement, len(es)) progress.Start(ctx, "Engaging") for _, e := range es { g.Go(e.composeService().Name, func(ctx context.Context) error { return tr.engage(ctx, e, aesCh) }) } g.Go("compose", func(ctx context.Context) error { for i := len(es); i > 0; i-- { select { case <-ctx.Done(): return nil case ae := <-aesCh: tr.addEngagement(ae) } } progress.Stop(ctx) if name == "create" || name == "stop" || name == "up" && !c.detached() { defer tr.disengage(ctx) } return tr.runCommand(ctx, name) }) err = g.Wait() if err != nil && strings.Contains(err.Error(), "graceful shutdown") { err = nil } return err } func (c *config) connect(ctx context.Context, es map[string]serviceExtension, connections map[string]*connection) (existingComposeFile string, err error) { for _, e := range es { var cc *connectionConfig cc, err = c.getConnectionConfig(e.connectionName()) if err != nil { return "", err } cx, ok := connections[cc.Name] if !ok { cx, err = cc.Connect(ctx, es, c.mustBeConnected) if err != nil { return "", err } connections[cc.Name] = cx } clog.Debugf(ctx, "Service %q will be %s", e.composeService().Name, e.engagementType().WorkDone()) if existingComposeFile == "" { existingComposeFile = daemon.MustGetSession(cx).DaemonInfo().ComposeFile } e.setConnection(cx) } return existingComposeFile, nil } func (c *config) loadProject(ctx context.Context) (*transformer, error) { // Skip compose-go's built-in validation so that services without a local image (e.g. proxy // services) can be loaded. Those services are removed or transformed before the final // compose file is written to disk, at which point docker compose performs its own validation. options, err := c.toProjectOptions(cli.WithLoadOptions(loader.WithSkipValidation, func(o *loader.Options) { o.SkipConsistencyCheck = true })) if err != nil { return nil, err } p, err := options.LoadProject(ctx) if err != nil { return nil, err } ev, ok := p.Extensions[extensionKey] if ok { err = c.parse(ev) if err != nil { return nil, err } } tr, err := newTransformer(c, p) if err != nil { return nil, err } if len(c.profiles) > 0 { err = tr.withProfiles(c.profiles) if err != nil { return nil, err } } return tr, nil } func (c *config) dispatchToCompose(ctx context.Context, name string) error { return errcat.User.New(proc.StdCommand(ctx, docker.Exe, slices.Insert(c.services, 0, "compose", name)...).Run()) } //nolint:gochecknoglobals // constant var defaultConnectionConfig = &connectionConfig{ Namespace: "default", } func (c *config) getConnectionConfig(name string) (*connectionConfig, error) { if len(c.Connections) == 0 { c.Connections = []*connectionConfig{defaultConnectionConfig} } ccs := c.Connections if name == "" { if len(ccs) == 1 { return ccs[0], nil } return nil, errcat.User.New("multiple connections found, please specify a connection name") } for _, cc := range ccs { if cc.Name == name { return cc, nil } } return nil, errcat.User.Newf("connection %q not found", name) } func (c *config) getMountPort(e mountsExtension) (uint16, error) { if !e.needsVolumes() { return 0, nil } if ep := c.existingProject; ep != nil { cn := e.composeService().Name if s, ok := ep.Services[cn]; ok { if pa, ok := s.Annotations[mountPortAnnotation]; ok { if p, err := strconv.Atoi(pa); err == nil { clog.Debugf(e.connection(), "Found existing mount port %d for %q", p, cn) return uint16(p), nil } } } } lma, err := ioutil.FreePortsTCP(1) if err != nil { return 0, err } return lma[0].Port(), nil } func loadExistingProject(ctx context.Context, pwd, path string) (*compose.Project, error) { return loader.LoadWithContext(ctx, compose.ConfigDetails{ ConfigFiles: []compose.ConfigFile{{Filename: path}}, WorkingDir: pwd, }, func(options *loader.Options) { options.SkipConsistencyCheck = true options.SkipValidation = true options.SkipNormalization = true options.SkipInterpolation = true options.SkipResolveEnvironment = true options.SkipDefaultValues = true options.SkipExtends = true options.SkipInclude = true }) } ================================================ FILE: pkg/client/cli/docker/compose/connection.go ================================================ package compose import ( "context" "fmt" "net/netip" "strconv" "strings" "time" compose "github.com/compose-spec/compose-go/v2/types" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/connect" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/docker" "github.com/telepresenceio/telepresence/v2/pkg/errcat" "github.com/telepresenceio/telepresence/v2/pkg/types" ) type connectionConfig struct { // Namespace to connect to. Name string `json:"name,omitempty"` Namespace string `json:"namespace,omitempty"` AlsoProxy []netip.Prefix `json:"also-proxy,omitempty"` NeverProxy []netip.Prefix `json:"never-proxy,omitempty"` ManagerNamespace string `json:"manager-namespace,omitempty"` MappedNamespaces []string `json:"mapped-namespaces,omitempty"` } type connection struct { context.Context *connectionConfig dnsIP netip.Addr subnets []netip.Prefix proxies map[string]netip.Addr } func composePortString(p *compose.ServicePortConfig) string { b := strings.Builder{} if p.HostIP != "" { b.WriteString(p.HostIP) b.WriteByte(':') } b.WriteString(p.Published) b.WriteByte(':') b.WriteString(strconv.Itoa(int(p.Target))) if p.Protocol != "" { b.WriteByte('/') b.WriteString(p.Protocol) } return b.String() } func composePortTarget(p *compose.ServicePortConfig) types.PortAndProto { pp := types.PortAndProto{Port: uint16(p.Target)} pp.Proto, _ = types.ParseProto(p.Protocol) return pp } func (cc *connectionConfig) Connect(ctx context.Context, es map[string]serviceExtension, mustPreExist bool) (*connection, error) { cr := daemon.GetRequest(ctx) cr = cr.Clone() cr.Implicit = false cr.Docker = true if cc.Namespace != "default" { cr.KubeFlags["namespace"] = cc.Namespace } cr.Name = cc.Name if cr.Name == "" { cr.Name = "trn-" + cc.Namespace } cr.ManagerNamespace = cc.ManagerNamespace if l := len(cc.AlsoProxy); l > 0 { cr.AlsoProxy = make([]string, l) for i, p := range cc.AlsoProxy { cr.AlsoProxy[i] = p.String() } } if l := len(cc.NeverProxy); l > 0 { cr.NeverProxy = make([]string, l) for i, p := range cc.NeverProxy { cr.NeverProxy[i] = p.String() } } cr.MappedNamespaces = cc.MappedNamespaces for _, e := range es { if e.engagementType() == types.EngagementTypeProxy && (e.connectionName() == "" || e.connectionName() == cc.Name) { err := addProxyReroutes(e.(servicePortExtension), cr) if err != nil { return nil, err } } } ctx = daemon.WithRequest(ctx, cr) ctx, err := connect.EnsureUserDaemon(ctx, !mustPreExist) if err != nil { return nil, err } defer func() { if err != nil && !mustPreExist { connect.Disconnect(ctx) } }() ctx, err = connect.EnsureSession(ctx, "compose", true) if err != nil { return nil, err } ds := daemon.MustGetSession(ctx) rootCfg, err := daemon.GetRootClientConfig(ds.Info.DaemonStatus) if err != nil { return nil, fmt.Errorf("unable to obtain routing info for connection: %w", err) } dns, _, err := docker.GetDaemonContainerNetworkInfo(ctx) if err != nil { return nil, err } proxies, err := cc.resolveProxies(ctx, ds, es) if err != nil { return nil, err } return &connection{Context: ctx, connectionConfig: cc, dnsIP: dns, proxies: proxies, subnets: rootCfg.Routing().Subnets}, nil } // resolveProxies resolves the name of the proxy definition into its remote service IP. This IP will then be // made available to other compose services using `extra_hosts: ["="]`, so that any // reference to the original service instead goes to the remote IP. func (cc *connectionConfig) resolveProxies(ctx context.Context, ds *daemon.Session, es map[string]serviceExtension) (map[string]netip.Addr, error) { var proxies map[string]netip.Addr for _, e := range es { if e.engagementType() == types.EngagementTypeProxy && (e.connectionName() == "" || e.connectionName() == cc.Name) { ip, err := ds.Lookup(ctx, e.name()) if err != nil { return nil, err } cn := e.composeService().Name if proxies == nil { proxies = map[string]netip.Addr{cn: ip} } else { proxies[cn] = ip } } } return proxies, nil } func (c *connection) disconnect() { ctx, cancel := context.WithTimeout(context.WithoutCancel(c), 3*time.Second) defer cancel() connect.Disconnect(ctx) } func addProxyReroutes(e servicePortExtension, cr *daemon.Request) error { addProxyLocalReroutes(e, cr) return addProxyRemoteReroutes(e, cr) } // addProxyLocalReroutes ensures that the ports published in the docker compose file for a proxied service are published // by the Telepresence daemon container that facilitates the proxy. This is done using local reroutes. // // What's added here are essentially `--reroute-local ::` flags. func addProxyLocalReroutes(e servicePortExtension, cr *daemon.Request) { ports := e.composeService().Ports for pi := range ports { p := &ports[pi] cr.ExposedPorts = append(cr.ExposedPorts, composePortString(p)) cpt := composePortTarget(p) cr.LocalReroutes = append(cr.LocalReroutes, fmt.Sprintf("%d:%s:%d", cpt.Port, e.name(), cpt.Port)) } } // addProxyRemoteReroutes ensures that the Telepresence daemon container reroutes the container ports of a proxy to // their corresponding remote service ports. Other containers will reach this service using DNS, so a lookup for // `:` must be rerouted to `:`. // // What's added here are essentially `--reroute-remote ::` flags. func addProxyRemoteReroutes(e servicePortExtension, cr *daemon.Request) error { b := strings.Builder{} for _, p := range e.servicePorts() { _, s, _ := p.From().ProtoAndNameOrNumber() if s != "" { return errcat.User.Newf("port %s is not a number in proxy for %s", s, e.composeService().Name) } b.Reset() b.WriteString(e.name()) b.WriteByte(':') pis := p.ToAsIntOrStr() b.WriteString(pis.String()) b.WriteByte(':') b.WriteString(p.From().String()) cr.RemoteReroutes = append(cr.RemoteReroutes, b.String()) } return nil } ================================================ FILE: pkg/client/cli/docker/compose/dc-cli.json ================================================ { "name": "compose", "usage": "docker compose [OPTIONS] COMMAND", "description": "Define and run multi-container applications with Docker", "flags": [ { "name": "all-resources", "type": "bool", "description": "Include all resources, even those not used by services" }, { "name": "ansi", "type": "string", "description": "Control when to print ANSI control characters (\"never\"|\"always\"|\"auto\") (default \"auto\")" }, { "name": "compatibility", "type": "bool", "description": "Run compose in backward compatibility mode" }, { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "env-file", "type": "stringArray", "description": "Specify an alternate environment file" }, { "name": "file", "shorthand": "f", "type": "stringArray", "description": "Compose configuration files" }, { "name": "parallel", "type": "int", "description": "Control max parallelism, -1 for unlimited (default -1)" }, { "name": "profile", "type": "stringArray", "description": "Specify a profile to enable" }, { "name": "progress", "type": "string", "description": "Set type of progress output (auto, tty, plain, json, quiet)" }, { "name": "project-directory", "type": "string", "description": "Specify an alternate working directory (default: the path of the, first specified, Compose file)" }, { "name": "project-name", "shorthand": "p", "type": "string", "description": "Project name" } ], "subcommands": [ { "name": "bridge", "usage": "docker compose bridge [OPTIONS] COMMAND", "description": "Convert compose files into another model", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" } ], "subcommands": [ { "name": "transformations", "usage": "docker compose bridge transformations [OPTIONS] COMMAND", "description": "Manage transformation images", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" } ], "subcommands": [ { "name": "create", "usage": "docker compose bridge transformations create [OPTION] PATH", "description": "Create a new transformation", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "from", "shorthand": "f", "type": "string", "description": "Existing transformation to copy (default: docker/compose-bridge-kubernetes)" } ] }, { "name": "list", "usage": "docker compose bridge transformations list", "description": "List available transformations", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "format", "type": "string", "description": "Format the output. Values: [table | json] (default \"table\")" }, { "name": "quiet", "shorthand": "q", "type": "bool", "description": "Only display transformer names" } ] } ] }, { "name": "convert", "usage": "docker compose bridge convert", "description": "Convert compose files to Kubernetes manifests, Helm charts, or another model", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "output", "shorthand": "o", "type": "string", "description": "The output directory for the Kubernetes resources (default \"out\")" }, { "name": "templates", "type": "string", "description": "Directory containing transformation templates" }, { "name": "transformation", "shorthand": "t", "type": "stringArray", "description": "Transformation to apply to compose model (default: docker/compose-bridge-kubernetes)" } ] } ] }, { "name": "attach", "usage": "docker compose attach [OPTIONS] SERVICE", "description": "Attach local standard input, output, and error streams to a service's running container", "flags": [ { "name": "detach-keys", "type": "string", "description": "Override the key sequence for detaching from a container." }, { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "index", "type": "int", "description": "index of the container if service has multiple replicas." }, { "name": "no-stdin", "type": "bool", "description": "Do not attach STDIN" }, { "name": "sig-proxy", "type": "bool", "description": "Proxy all received signals to the process (default true)" } ] }, { "name": "build", "usage": "docker compose build [OPTIONS] [SERVICE...]", "description": "Build or rebuild services", "flags": [ { "name": "build-arg", "type": "stringArray", "description": "Set build-time variables for services" }, { "name": "builder", "type": "string", "description": "Set builder to use" }, { "name": "check", "type": "bool", "description": "Check build configuration" }, { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "memory", "shorthand": "m", "type": "bytes", "description": "Set memory limit for the build container. Not supported by BuildKit." }, { "name": "no-cache", "type": "bool", "description": "Do not use cache when building the image" }, { "name": "print", "type": "bool", "description": "Print equivalent bake file" }, { "name": "provenance", "type": "string", "description": "Add a provenance attestation" }, { "name": "pull", "type": "bool", "description": "Always attempt to pull a newer version of the image" }, { "name": "push", "type": "bool", "description": "Push service images" }, { "name": "quiet", "shorthand": "q", "type": "bool", "description": "Suppress the build output" }, { "name": "sbom", "type": "string", "description": "Add a SBOM attestation" }, { "name": "ssh", "type": "string", "description": "Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent)" }, { "name": "with-dependencies", "type": "bool", "description": "Also build dependencies (transitively)" } ] }, { "name": "commit", "usage": "docker compose commit [OPTIONS] SERVICE [REPOSITORY[:TAG]]", "description": "Create a new image from a service container's changes", "flags": [ { "name": "author", "shorthand": "a", "type": "string", "description": "Author (e.g., \"John Hannibal Smith \u003channibal@a-team.com\u003e\")" }, { "name": "change", "shorthand": "c", "type": "list", "description": "Apply Dockerfile instruction to the created image" }, { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "index", "type": "int", "description": "index of the container if service has multiple replicas." }, { "name": "message", "shorthand": "m", "type": "string", "description": "Commit message" }, { "name": "pause", "shorthand": "p", "type": "bool", "description": "Pause container during commit", "default": "true" } ] }, { "name": "config", "usage": "docker compose config [OPTIONS] [SERVICE...]", "description": "Parse, resolve and render compose file in canonical format", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "environment", "type": "bool", "description": "Print environment used for interpolation." }, { "name": "format", "type": "string", "description": "Format the output. Values: [yaml | json]" }, { "name": "hash", "type": "string", "description": "Print the service config hash, one per line." }, { "name": "images", "type": "bool", "description": "Print the image names, one per line." }, { "name": "lock-image-digests", "type": "bool", "description": "Produces an override file with image digests" }, { "name": "models", "type": "bool", "description": "Print the model names, one per line." }, { "name": "networks", "type": "bool", "description": "Print the network names, one per line." }, { "name": "no-consistency", "type": "bool", "description": "Don't check model consistency - warning: may produce invalid Compose output" }, { "name": "no-env-resolution", "type": "bool", "description": "Don't resolve service env files" }, { "name": "no-interpolate", "type": "bool", "description": "Don't interpolate environment variables" }, { "name": "no-normalize", "type": "bool", "description": "Don't normalize compose model" }, { "name": "no-path-resolution", "type": "bool", "description": "Don't resolve file paths" }, { "name": "output", "shorthand": "o", "type": "string", "description": "Save to file (default to stdout)" }, { "name": "profiles", "type": "bool", "description": "Print the profile names, one per line." }, { "name": "quiet", "shorthand": "q", "type": "bool", "description": "Only validate the configuration, don't print anything" }, { "name": "resolve-image-digests", "type": "bool", "description": "Pin image tags to digests" }, { "name": "services", "type": "bool", "description": "Print the service names, one per line." }, { "name": "variables", "type": "bool", "description": "Print model variables and default values." }, { "name": "volumes", "type": "bool", "description": "Print the volume names, one per line." } ] }, { "name": "cp", "usage": "docker compose cp [OPTIONS] SERVICE:SRC_PATH DEST_PATH|-", "description": "docker compose cp [OPTIONS] SRC_PATH|- SERVICE:DEST_PATH", "flags": [ { "name": "all", "type": "bool", "description": "Include containers created by the run command" }, { "name": "archive", "shorthand": "a", "type": "bool", "description": "Archive mode (copy all uid/gid information)" }, { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "follow-link", "shorthand": "L", "type": "bool", "description": "Always follow symbol link in SRC_PATH" }, { "name": "index", "type": "int", "description": "Index of the container if service has multiple replicas" } ] }, { "name": "create", "usage": "docker compose create [OPTIONS] [SERVICE...]", "description": "Creates containers for a service", "flags": [ { "name": "build", "type": "bool", "description": "Build images before starting containers" }, { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "force-recreate", "type": "bool", "description": "Recreate containers even if their configuration and image haven't changed" }, { "name": "no-build", "type": "bool", "description": "Don't build an image, even if it's policy" }, { "name": "no-recreate", "type": "bool", "description": "If containers already exist, don't recreate them. Incompatible with --force-recreate." }, { "name": "pull", "type": "string", "description": "Pull image before running (\"always\"|\"missing\"|\"never\"|\"build\") (default \"policy\")" }, { "name": "quiet-pull", "type": "bool", "description": "Pull without printing progress information" }, { "name": "remove-orphans", "type": "bool", "description": "Remove containers for services not defined in the Compose file" }, { "name": "scale", "type": "scale", "description": "Scale SERVICE to NUM instances. Overrides the scale setting in the Compose file if present." }, { "name": "yes", "shorthand": "y", "type": "bool", "description": "Assume \"yes\" as answer to all prompts and run non-interactively" } ] }, { "name": "down", "usage": "docker compose down [OPTIONS] [SERVICES]", "description": "Stop and remove containers, networks", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "remove-orphans", "type": "bool", "description": "Remove containers for services not defined in the Compose file" }, { "name": "rmi", "type": "string", "description": "Remove images used by services. \"local\" remove only images that don't have a custom tag (\"local\"|\"all\")" }, { "name": "timeout", "shorthand": "t", "type": "int", "description": "Specify a shutdown timeout in seconds" }, { "name": "volumes", "shorthand": "v", "type": "bool", "description": "Remove named volumes declared in the \"volumes\" section of the Compose file and anonymous volumes attached to containers" } ] }, { "name": "events", "usage": "docker compose events [OPTIONS] [SERVICE...]", "description": "Receive real time events from containers", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "json", "type": "bool", "description": "Output events as a stream of json objects" }, { "name": "since", "type": "string", "description": "Show all events created since timestamp" }, { "name": "until", "type": "string", "description": "Stream events until this timestamp" } ] }, { "name": "exec", "usage": "docker compose exec [OPTIONS] SERVICE COMMAND [ARGS...]", "description": "Execute a command in a running container", "flags": [ { "name": "detach", "shorthand": "d", "type": "bool", "description": "Detached mode: Run command in the background" }, { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "env", "shorthand": "e", "type": "stringArray", "description": "Set environment variables" }, { "name": "index", "type": "int", "description": "Index of the container if service has multiple replicas" }, { "name": "no-tty", "shorthand": "T", "type": "bool", "description": "Disable pseudo-TTY allocation. By default 'docker compose exec' allocates a TTY. (default true)" }, { "name": "privileged", "type": "bool", "description": "Give extended privileges to the process" }, { "name": "user", "shorthand": "u", "type": "string", "description": "Run the command as this user" }, { "name": "workdir", "shorthand": "w", "type": "string", "description": "Path to workdir directory for this command" } ] }, { "name": "export", "usage": "docker compose export [OPTIONS] SERVICE", "description": "Export a service container's filesystem as a tar archive", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "index", "type": "int", "description": "index of the container if service has multiple replicas." }, { "name": "output", "shorthand": "o", "type": "string", "description": "Write to a file, instead of STDOUT" } ] }, { "name": "images", "usage": "docker compose images [OPTIONS] [SERVICE...]", "description": "List images used by the created containers", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "format", "type": "string", "description": "Format the output. Values: [table | json] (default \"table\")" }, { "name": "quiet", "shorthand": "q", "type": "bool", "description": "Only display IDs" } ] }, { "name": "kill", "usage": "docker compose kill [OPTIONS] [SERVICE...]", "description": "Force stop service containers", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "remove-orphans", "type": "bool", "description": "Remove containers for services not defined in the Compose file" }, { "name": "signal", "shorthand": "s", "type": "string", "description": "SIGNAL to send to the container", "default": "\"SIGKILL\"" } ] }, { "name": "logs", "usage": "docker compose logs [OPTIONS] [SERVICE...]", "description": "View output from containers", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "follow", "shorthand": "f", "type": "bool", "description": "Follow log output" }, { "name": "index", "type": "int", "description": "index of the container if service has multiple replicas" }, { "name": "no-color", "type": "bool", "description": "Produce monochrome output" }, { "name": "no-log-prefix", "type": "bool", "description": "Don't print prefix in logs" }, { "name": "since", "type": "string", "description": "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)" }, { "name": "tail", "shorthand": "n", "type": "string", "description": "Number of lines to show from the end of the logs for each container (default \"all\")" }, { "name": "timestamps", "shorthand": "t", "type": "bool", "description": "Show timestamps" }, { "name": "until", "type": "string", "description": "Show logs before a timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)" } ] }, { "name": "ls", "usage": "docker compose ls [OPTIONS]", "description": "List running compose projects", "flags": [ { "name": "all", "shorthand": "a", "type": "bool", "description": "Show all stopped Compose projects" }, { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "filter", "type": "filter", "description": "Filter output based on conditions provided" }, { "name": "format", "type": "string", "description": "Format the output. Values: [table | json] (default \"table\")" }, { "name": "quiet", "shorthand": "q", "type": "bool", "description": "Only display project names" } ] }, { "name": "pause", "usage": "docker compose pause [SERVICE...]", "description": "Pause services", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" } ] }, { "name": "port", "usage": "docker compose port [OPTIONS] SERVICE PRIVATE_PORT", "description": "Print the public port for a port binding", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "index", "type": "int", "description": "Index of the container if service has multiple replicas" }, { "name": "protocol", "type": "string", "description": "tcp or udp", "default": "\"tcp\"" } ] }, { "name": "ps", "usage": "docker compose ps [OPTIONS] [SERVICE...]", "description": "List containers", "flags": [ { "name": "all", "shorthand": "a", "type": "bool", "description": "Show all stopped containers (including those created by the run command)" }, { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "filter", "type": "string", "description": "Filter services by a property (supported filters: status)" }, { "name": "format", "type": "string", "description": "Format output using a custom template: 'table': Print output in table format with column headers (default) 'table TEMPLATE': Print output in table format using the given Go template 'json': Print in JSON format 'TEMPLATE': Print output using the given Go template. Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates (default \"table\")" }, { "name": "no-trunc", "type": "bool", "description": "Don't truncate output" }, { "name": "orphans", "type": "bool", "description": "Include orphaned services (not declared by project) (default true)" }, { "name": "quiet", "shorthand": "q", "type": "bool", "description": "Only display IDs" }, { "name": "services", "type": "bool", "description": "Display services" }, { "name": "status", "type": "stringArray", "description": "Filter services by status. Values: [paused | restarting | removing | running | dead | created | exited]" } ] }, { "name": "publish", "usage": "docker compose publish [OPTIONS] REPOSITORY[:TAG]", "description": "Publish compose application", "flags": [ { "name": "app", "type": "bool", "description": "Published compose application (includes referenced images)" }, { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "oci-version", "type": "string", "description": "OCI image/artifact specification version (automatically determined by default)" }, { "name": "resolve-image-digests", "type": "bool", "description": "Pin image tags to digests" }, { "name": "with-env", "type": "bool", "description": "Include environment variables in the published OCI artifact" }, { "name": "yes", "shorthand": "y", "type": "bool", "description": "Assume \"yes\" as answer to all prompts" } ] }, { "name": "pull", "usage": "docker compose pull [OPTIONS] [SERVICE...]", "description": "Pull service images", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "ignore-buildable", "type": "bool", "description": "Ignore images that can be built" }, { "name": "ignore-pull-failures", "type": "bool", "description": "Pull what it can and ignores images with pull failures" }, { "name": "include-deps", "type": "bool", "description": "Also pull services declared as dependencies" }, { "name": "policy", "type": "string", "description": "Apply pull policy (\"missing\"|\"always\")" }, { "name": "quiet", "shorthand": "q", "type": "bool", "description": "Pull without printing progress information" } ] }, { "name": "push", "usage": "docker compose push [OPTIONS] [SERVICE...]", "description": "Push service images", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "ignore-push-failures", "type": "bool", "description": "Push what it can and ignores images with push failures" }, { "name": "include-deps", "type": "bool", "description": "Also push images of services declared as dependencies" }, { "name": "quiet", "shorthand": "q", "type": "bool", "description": "Push without printing progress information" } ] }, { "name": "restart", "usage": "docker compose restart [OPTIONS] [SERVICE...]", "description": "Restart service containers", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "no-deps", "type": "bool", "description": "Don't restart dependent services" }, { "name": "timeout", "shorthand": "t", "type": "int", "description": "Specify a shutdown timeout in seconds" } ] }, { "name": "rm", "usage": "docker compose rm [OPTIONS] [SERVICE...]", "description": "Removes stopped service containers", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "force", "shorthand": "f", "type": "bool", "description": "Don't ask to confirm removal" }, { "name": "stop", "shorthand": "s", "type": "bool", "description": "Stop the containers, if required, before removing" }, { "name": "volumes", "shorthand": "v", "type": "bool", "description": "Remove any anonymous volumes attached to containers" } ] }, { "name": "run", "usage": "docker compose run [OPTIONS] SERVICE [COMMAND] [ARGS...]", "description": "Run a one-off command on a service", "flags": [ { "name": "build", "type": "bool", "description": "Build image before starting container" }, { "name": "cap-add", "type": "list", "description": "Add Linux capabilities" }, { "name": "cap-drop", "type": "list", "description": "Drop Linux capabilities" }, { "name": "detach", "shorthand": "d", "type": "bool", "description": "Run container in background and print container ID" }, { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "entrypoint", "type": "string", "description": "Override the entrypoint of the image" }, { "name": "env", "shorthand": "e", "type": "stringArray", "description": "Set environment variables" }, { "name": "env-from-file", "type": "stringArray", "description": "Set environment variables from file" }, { "name": "interactive", "shorthand": "i", "type": "bool", "description": "Keep STDIN open even if not attached (default true)" }, { "name": "label", "shorthand": "l", "type": "stringArray", "description": "Add or override a label" }, { "name": "name", "type": "string", "description": "Assign a name to the container" }, { "name": "no-TTY", "shorthand": "T", "type": "bool", "description": "Disable pseudo-TTY allocation (default: auto-detected) (default true)" }, { "name": "no-deps", "type": "bool", "description": "Don't start linked services" }, { "name": "publish", "shorthand": "p", "type": "stringArray", "description": "Publish a container's port(s) to the host" }, { "name": "pull", "type": "string", "description": "Pull image before running (\"always\"|\"missing\"|\"never\") (default \"policy\")" }, { "name": "quiet", "shorthand": "q", "type": "bool", "description": "Don't print anything to STDOUT" }, { "name": "quiet-build", "type": "bool", "description": "Suppress progress output from the build process" }, { "name": "quiet-pull", "type": "bool", "description": "Pull without printing progress information" }, { "name": "remove-orphans", "type": "bool", "description": "Remove containers for services not defined in the Compose file" }, { "name": "rm", "type": "bool", "description": "Automatically remove the container when it exits" }, { "name": "service-ports", "shorthand": "P", "type": "bool", "description": "Run command with all service's ports enabled and mapped to the host" }, { "name": "use-aliases", "type": "bool", "description": "Use the service's network useAliases in the network(s) the container connects to" }, { "name": "user", "shorthand": "u", "type": "string", "description": "Run as specified username or uid" }, { "name": "volume", "shorthand": "v", "type": "stringArray", "description": "Bind mount a volume" }, { "name": "workdir", "shorthand": "w", "type": "string", "description": "Working directory inside the container" } ] }, { "name": "scale", "usage": "docker compose scale [SERVICE=REPLICAS...]", "description": "Scale services", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "no-deps", "type": "bool", "description": "Don't start linked services" } ] }, { "name": "start", "usage": "docker compose start [SERVICE...]", "description": "Start services", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "wait", "type": "bool", "description": "Wait for services to be running|healthy. Implies detached mode." }, { "name": "wait-timeout", "type": "int", "description": "Maximum duration in seconds to wait for the project to be running|healthy" } ] }, { "name": "stats", "usage": "docker compose stats [OPTIONS] [SERVICE]", "description": "Display a live stream of container(s) resource usage statistics", "flags": [ { "name": "all", "shorthand": "a", "type": "bool", "description": "Show all containers (default shows just running)" }, { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "format", "type": "string", "description": "Format output using a custom template: 'table': Print output in table format with column headers (default) 'table TEMPLATE': Print output in table format using the given Go template 'json': Print in JSON format 'TEMPLATE': Print output using the given Go template. Refer to https://docs.docker.com/engine/cli/formatting/ for more information about formatting output with templates" }, { "name": "no-stream", "type": "bool", "description": "Disable streaming stats and only pull the first result" }, { "name": "no-trunc", "type": "bool", "description": "Do not truncate output" } ] }, { "name": "stop", "usage": "docker compose stop [OPTIONS] [SERVICE...]", "description": "Stop services", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "timeout", "shorthand": "t", "type": "int", "description": "Specify a shutdown timeout in seconds" } ] }, { "name": "top", "usage": "docker compose top [SERVICES...]", "description": "Display the running processes", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" } ] }, { "name": "unpause", "usage": "docker compose unpause [SERVICE...]", "description": "Unpause services", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" } ] }, { "name": "up", "usage": "docker compose up [OPTIONS] [SERVICE...]", "description": "Create and start containers", "flags": [ { "name": "abort-on-container-exit", "type": "bool", "description": "Stops all containers if any container was stopped. Incompatible with -d" }, { "name": "abort-on-container-failure", "type": "bool", "description": "Stops all containers if any container exited with failure. Incompatible with -d" }, { "name": "always-recreate-deps", "type": "bool", "description": "Recreate dependent containers. Incompatible with --no-recreate." }, { "name": "attach", "type": "stringArray", "description": "Restrict attaching to the specified services. Incompatible with --attach-dependencies." }, { "name": "attach-dependencies", "type": "bool", "description": "Automatically attach to log output of dependent services" }, { "name": "build", "type": "bool", "description": "Build images before starting containers" }, { "name": "detach", "shorthand": "d", "type": "bool", "description": "Detached mode: Run containers in the background" }, { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "exit-code-from", "type": "string", "description": "Return the exit code of the selected service container. Implies --abort-on-container-exit" }, { "name": "force-recreate", "type": "bool", "description": "Recreate containers even if their configuration and image haven't changed" }, { "name": "menu", "type": "bool", "description": "Enable interactive shortcuts when running attached. Incompatible with --detach. Can also be enable/disable by setting COMPOSE_MENU environment var." }, { "name": "no-attach", "type": "stringArray", "description": "Do not attach (stream logs) to the specified services" }, { "name": "no-build", "type": "bool", "description": "Don't build an image, even if it's policy" }, { "name": "no-color", "type": "bool", "description": "Produce monochrome output" }, { "name": "no-deps", "type": "bool", "description": "Don't start linked services" }, { "name": "no-log-prefix", "type": "bool", "description": "Don't print prefix in logs" }, { "name": "no-recreate", "type": "bool", "description": "If containers already exist, don't recreate them. Incompatible with --force-recreate." }, { "name": "no-start", "type": "bool", "description": "Don't start the services after creating them" }, { "name": "pull", "type": "string", "description": "Pull image before running (\"always\"|\"missing\"|\"never\") (default \"policy\")" }, { "name": "quiet-build", "type": "bool", "description": "Suppress the build output" }, { "name": "quiet-pull", "type": "bool", "description": "Pull without printing progress information" }, { "name": "remove-orphans", "type": "bool", "description": "Remove containers for services not defined in the Compose file" }, { "name": "renew-anon-volumes", "shorthand": "V", "type": "bool", "description": "Recreate anonymous volumes instead of retrieving data from the previous containers" }, { "name": "scale", "type": "scale", "description": "Scale SERVICE to NUM instances. Overrides the scale setting in the Compose file if present." }, { "name": "timeout", "shorthand": "t", "type": "int", "description": "Use this timeout in seconds for container shutdown when attached or when containers are already running" }, { "name": "timestamps", "type": "bool", "description": "Show timestamps" }, { "name": "wait", "type": "bool", "description": "Wait for services to be running|healthy. Implies detached mode." }, { "name": "wait-timeout", "type": "int", "description": "Maximum duration in seconds to wait for the project to be running|healthy" }, { "name": "watch", "shorthand": "w", "type": "bool", "description": "Watch source code and rebuild/refresh containers when files are updated." }, { "name": "yes", "shorthand": "y", "type": "bool", "description": "Assume \"yes\" as answer to all prompts and run non-interactively" } ] }, { "name": "version", "usage": "docker compose version [OPTIONS]", "description": "Show the Docker Compose version information", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "format", "shorthand": "f", "type": "string", "description": "Format the output. Values: [pretty | json]. (Default: pretty)" }, { "name": "short", "type": "bool", "description": "Shows only Compose's version number" } ] }, { "name": "volumes", "usage": "docker compose volumes [OPTIONS] [SERVICE...]", "description": "List volumes", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "format", "type": "string", "description": "Format output using a custom template: 'table': Print output in table format with column headers (default) 'table TEMPLATE': Print output in table format using the given Go template 'json': Print in JSON format 'TEMPLATE': Print output using the given Go template. Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates (default \"table\")" }, { "name": "quiet", "shorthand": "q", "type": "bool", "description": "Only display volume names" } ] }, { "name": "wait", "usage": "docker compose wait SERVICE [SERVICE...] [OPTIONS]", "description": "Block until containers of all (or specified) services stop.", "flags": [ { "name": "down-project", "type": "bool", "description": "Drops project when the first container stops" }, { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" } ] }, { "name": "watch", "usage": "docker compose watch [SERVICE...]", "description": "Watch build context for service and rebuild/refresh containers when files are updated", "flags": [ { "name": "dry-run", "type": "bool", "description": "Execute command in dry run mode" }, { "name": "no-up", "type": "bool", "description": "Do not build \u0026 start services before watching" }, { "name": "prune", "type": "bool", "description": "Prune dangling images on rebuild", "default": "true" }, { "name": "quiet", "type": "bool", "description": "hide build output" } ] } ] } ================================================ FILE: pkg/client/cli/docker/compose/engagement.go ================================================ package compose import ( "context" "fmt" "net/netip" "slices" "strconv" "strings" compose "github.com/compose-spec/compose-go/v2/types" "github.com/puzpuzpuz/xsync/v4" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/pkg/client" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/output" "github.com/telepresenceio/telepresence/v2/pkg/client/docker" "github.com/telepresenceio/telepresence/v2/pkg/ioutil" "github.com/telepresenceio/telepresence/v2/pkg/json" "github.com/telepresenceio/telepresence/v2/pkg/maps" "github.com/telepresenceio/telepresence/v2/pkg/types" ) type engagement struct { serviceExtension environment map[string]string daemonID *daemon.Identifier sftpPort uint16 daemonIP netip.Addr } func createEngagement(ud daemon.UserClient, e serviceExtension, sftpPort uint16) (*engagement, error) { ae := &engagement{ serviceExtension: e, daemonID: ud.DaemonID(), daemonIP: ud.DaemonInfo().ContainerIP, sftpPort: sftpPort, } return ae, nil } func (a *engagement) assignEnvAndCreateMounts(remoteEnv map[string]string, remoteMounts map[string]int32, t *transformer) { env := make(map[string]string) maps.Merge(env, remoteEnv) a.environment = env we, ok := a.serviceExtension.(mountsExtension) if !ok { return } mounts, serviceVolumes := we.desiredRemoteMounts(types.MountPoliciesFromRPC(remoteMounts)) if len(mounts) == 0 { return } ro := a.engagementType() == types.EngagementTypeIngest || a.engagementType() == types.EngagementTypeWiretap ctx := a.connection().Context clog.Debugf(ctx, "mounts: %v, ro %t", mounts, ro) createVolumes(ctx, netip.AddrPortFrom(a.daemonIP, a.sftpPort), a.environment["TELEPRESENCE_CONTAINER"], mounts, serviceVolumes, ro, t) } const ( connectionAnnotationPrefix = "telepresence.io/connection-" mountPortAnnotation = "telepresence.io/mount-port" ) func (a *engagement) maybeAddConnection(s *compose.ServiceConfig) bool { conn := a.connection() cn := conn.Name if cn == "" { cn = "default" } myKey := connectionAnnotationPrefix + cn for k, sn := range s.Annotations { if strings.HasPrefix(k, connectionAnnotationPrefix) { if k == myKey { // Never add the same connection twice. return false } var subnets []netip.Prefix _ = json.Unmarshal([]byte(sn), &subnets, true) for _, osn := range subnets { for _, msn := range conn.subnets { if osn.Overlaps(msn) { clog.Warnf(conn, "Subnet %s in connection %s overlaps with subnet %s in connection %s. This prevents it from being added to service %s", msn, cn, osn, strings.TrimPrefix(k, connectionAnnotationPrefix), s.Name) return false } } } } } if s.Annotations == nil { s.Annotations = make(map[string]string) } myAnn, _ := json.Marshal(conn.subnets) s.Annotations[myKey] = string(myAnn) if s.Networks == nil { s.Networks = make(map[string]*compose.ServiceNetworkConfig) } s.Networks[a.daemonID.Name] = nil return true } func (a *engagement) engageService(s *compose.ServiceConfig) { a.maybeAddConnection(s) dnsIP := a.connection().dnsIP if ipS := dnsIP.String(); !slices.Contains(s.DNS, ipS) { s.DNS = append(s.DNS, ipS) } if !slices.Contains(s.DNSSearch, client.Tel2SubDomain) { s.DNSSearch = append(s.DNSSearch, client.Tel2SubDomain) } env := a.environment if len(env) > 0 { if s.Environment == nil { s.Environment = make(compose.MappingWithEquals, len(env)) } for k, v := range env { // Don't overwrite the value declared in the compose-spec. if _, ok := s.Environment[k]; !ok { s.Environment[k] = &v } } } if a.sftpPort > 0 { if s.Annotations == nil { s.Annotations = make(map[string]string) } s.Annotations[mountPortAnnotation] = strconv.Itoa(int(a.sftpPort)) } } func (a *engagement) engageProxyDependents(p *compose.Project, n string, dependents []string) { conn := a.connection() sm := p.Services dnsIP := a.connection().dnsIP.String() for _, d := range dependents { // Dependent services need a new DNS for the proxy, and also the network // that routes its IP. ds := sm[d] if !a.maybeAddConnection(&ds) { continue } if len(conn.proxies) > 0 { extraHosts := make(compose.HostsList, len(conn.proxies)) for name, ip := range conn.proxies { extraHosts[name] = []string{ip.String()} } if ds.ExtraHosts != nil { maps.Merge(ds.ExtraHosts, extraHosts) } else { ds.ExtraHosts = extraHosts } } if !slices.Contains(ds.DNS, dnsIP) { ds.DNS = append(ds.DNS, dnsIP) } if !slices.Contains(ds.DNSSearch, client.Tel2SubDomain) { ds.DNSSearch = append(ds.DNSSearch, client.Tel2SubDomain) } // A proxied service is simply removed from the compose-spec along with any dependents. delete(ds.DependsOn, n) sm[d] = ds } } func (a *engagement) engageProject(p *compose.Project) { if p.Networks == nil { p.Networks = make(compose.Networks) } nn := a.daemonID.Name if _, ok := p.Networks[nn]; !ok { p.Networks[nn] = compose.NetworkConfig{ Name: nn, External: true, } } } // createVolumes creates the VolumeConfigs necessary when mounting volumes required by when engaging a remote container. // The hostPort is the / where the access to the remote sftp-server is provided. // The mounts are provided as a map of mount policies keyed by paths. // Each volume is given the name of the remote container suffixed by a dash and a sequence number, starting at 1. // Returns a map of VolumeConfig keyed by volume target paths. func createVolumes( ctx context.Context, hostPort netip.AddrPort, remoteContainer string, mounts types.MountPolicies, serviceVolumes map[string]*compose.ServiceVolumeConfig, ro bool, t *transformer, ) { var plugin string i := 0 for dir, policy := range mounts { sv := serviceVolumes[dir] volRO := ro || sv.ReadOnly switch policy { case types.MountPolicyIgnore, types.MountPolicyLocal: continue case types.MountPolicyRemoteReadOnly: volRO = true default: } var err error if plugin == "" { plugin, err = docker.EnsureVolumePlugin(ctx) if err != nil { ioutil.Printf(output.Err(ctx), "Remote mount disabled: %s\n", err) return } } t.volumes().Compute(sv.Source, func(prev *compose.VolumeConfig, loaded bool) (vol *compose.VolumeConfig, op xsync.ComputeOp) { if loaded { if !volRO && prev.DriverOpts["ro"] == "true" { // A read-write volume is needed by this service, so we can't use a read-only one. delete(prev.DriverOpts, "ro") } return nil, xsync.CancelOp } i++ return createVolume(ctx, plugin, hostPort, fmt.Sprintf("%s-%d", remoteContainer, i), remoteContainer, dir, volRO), xsync.UpdateOp }) } } func createVolume(ctx context.Context, pluginName string, hostPort netip.AddrPort, volumeName, container, dir string, ro bool) *compose.VolumeConfig { return &compose.VolumeConfig{ Name: volumeName, Driver: pluginName, DriverOpts: docker.VolumeDriverOpts(ctx, pluginName, hostPort, volumeName, container, dir, ro), } } ================================================ FILE: pkg/client/cli/docker/compose/extension.go ================================================ package compose import ( "context" "fmt" "strings" compose "github.com/compose-spec/compose-go/v2/types" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "github.com/telepresenceio/telepresence/rpc/v2/connector" "github.com/telepresenceio/telepresence/rpc/v2/manager" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/intercept" "github.com/telepresenceio/telepresence/v2/pkg/errcat" "github.com/telepresenceio/telepresence/v2/pkg/grpc" "github.com/telepresenceio/telepresence/v2/pkg/json" "github.com/telepresenceio/telepresence/v2/pkg/types" ) type serviceExtension interface { // Activate the service extension. activate(*transformer) (*engagement, error) deactivate() error engaged() (*engagement, error) engagementType() types.EngagementType // name used for the proxy or the engagement. It defaults to the name of the compose-service. name() string // connection used when the extension is activated. connection() *connection // ConnectionName to use for this extension. It is optional unless more than one connection is present // in the top level x-tele extension. connectionName() string // ComposeService is the extended docker-compose service. composeService() *compose.ServiceConfig // NeedsVolumes returns true if the extended service has volumes unless this is a "connect" or "proxy" extension. needsVolumes() bool // SetConnection assigns the connection used when the extension is activated. setConnection(c *connection) init(*config, types.EngagementType, *compose.ServiceConfig) } type mountsExtension interface { serviceExtension desiredRemoteMounts(remoteMounts types.MountPolicies) (types.MountPolicies, map[string]*compose.ServiceVolumeConfig) } type workloadExtension interface { mountsExtension // workload is the name of the engaged workload. It defaults to name(). workload() string createInterceptRequest(localMountPort uint16) (*connector.CreateInterceptRequest, error) } type servicePortExtension interface { serviceExtension servicePorts() []types.PortMapping } func (c *config) parseServiceExtension(composeService *compose.ServiceConfig, v any) (se serviceExtension, err error) { m, ok := v.(map[string]any) if !ok { return nil, errcat.User.Newf("%s extension is not a map", extensionKey) } typ, ok := m["type"].(string) if !ok { return nil, errcat.User.Newf("%s extension must have a type", extensionKey) } et, err := types.ParseEngagementType(typ) if err != nil { return nil, errcat.User.Newf("%s extension has invalid type: %v", extensionKey, err) } data, err := json.Marshal(v) if err != nil { return nil, err } switch et { case types.EngagementTypeConnect: se = &extension{} case types.EngagementTypeIngest: se = &ingestExtension{} case types.EngagementTypeIntercept: se = &interceptExtension{} case types.EngagementTypeProxy: se = &proxyExtension{} case types.EngagementTypeReplace: se = &replaceExtension{} case types.EngagementTypeWiretap: se = &wiretapExtension{} default: return nil, errcat.User.Newf("%s has unsupported extension type %s", extensionKey, et) } err = json.Unmarshal(data, se, true) if err != nil { return nil, errcat.User.New(err) } se.init(c, et, composeService) return se, nil } type extension struct { Type types.EngagementType `json:"type"` Connection string `json:"connection,omitempty"` composeSvc *compose.ServiceConfig conn *connection } func (e *extension) init(_ *config, et types.EngagementType, composeService *compose.ServiceConfig) { e.Type = et e.composeSvc = composeService } // Name is the name of the service that this proxy connects to. It defaults to the name of the compose-service. func (e *extension) name() string { return e.composeSvc.Name } func (e *extension) engagementType() types.EngagementType { return e.Type } func (e *extension) connectionName() string { return e.Connection } // ComposeService is the name of the extended docker-compose service. func (e *extension) composeService() *compose.ServiceConfig { return e.composeSvc } func (e *extension) connection() *connection { return e.conn } func (e *extension) engaged() (*engagement, error) { return createEngagement(daemon.MustGetUserClient(e.conn), e, 0) } func (e *extension) needsVolumes() bool { return false } func (e *extension) setConnection(c *connection) { e.conn = c } func (e *extension) activate(*transformer) (*engagement, error) { return createEngagement(daemon.MustGetUserClient(e.conn), e, 0) } func (e *extension) deactivate() error { return nil } type proxyExtension struct { extension Name string `json:"name"` Ports []types.PortMapping `json:"ports,omitempty"` } func (e *proxyExtension) init(c *config, et types.EngagementType, composeService *compose.ServiceConfig) { e.extension.init(c, et, composeService) if e.Name == "" { e.Name = composeService.Name } } func (e *proxyExtension) activate(*transformer) (*engagement, error) { return createEngagement(daemon.MustGetUserClient(e.conn), e, 0) } // Name is the name of the service that this proxy connects to. It defaults to the name of the compose-service. func (e *proxyExtension) name() string { return e.Name } // ServicePorts mappings from local ports to service ports. func (e *proxyExtension) servicePorts() []types.PortMapping { return e.Ports } type engageExtension struct { extension Name string `json:"name"` mounts []volumeMountPolicy } func (e *engageExtension) init(c *config, et types.EngagementType, composeService *compose.ServiceConfig) { e.extension.init(c, et, composeService) if e.Name == "" { e.Name = composeService.Name } e.mounts = c.Mounts } // Name is the name of the engagement. It defaults to the name of the compose-service. func (e *engageExtension) name() string { return e.Name } // Workload is the name of the engaged workload. It defaults to Name. func (e *engageExtension) workload() string { return e.Name } func (e *engageExtension) needsVolumes() bool { return len(e.composeService().Volumes) > 0 } func (e *engageExtension) desiredRemoteMounts(remoteMounts types.MountPolicies) (types.MountPolicies, map[string]*compose.ServiceVolumeConfig) { desiredMounts := make(types.MountPolicies, len(remoteMounts)) volumes := make(map[string]*compose.ServiceVolumeConfig) composeVolumes := e.composeService().Volumes for p, m := range remoteMounts { if m == types.MountPolicyRemote || m == types.MountPolicyRemoteReadOnly { for vi := range composeVolumes { v := &composeVolumes[vi] if v.Type == compose.VolumeTypeVolume && v.Target == p { // This docker compose volume targets the remote mount point. for _, mp := range e.mounts { if mp.Matches(v.Source) { m = mp.Policy break } } if m == types.MountPolicyRemote || m == types.MountPolicyRemoteReadOnly { v.ReadOnly = v.ReadOnly || m == types.MountPolicyRemoteReadOnly volumes[p] = v desiredMounts[p] = m } break } } } } return desiredMounts, volumes } type httpFilterExtension struct { engageExtension HttpFilters map[string]string `json:"httpFilters,omitempty"` Metadata map[string]string `json:"metadata,omitempty"` Ports []types.PortMapping `json:"ports,omitempty"` Paths []string `json:"httpPaths,omitempty"` PathPrefixes []string `json:"httpPathPrefixes,omitempty"` PathRegexps []string `json:"httpPathRegexps,omitempty"` } func (e *httpFilterExtension) amendInterceptSpec(spec *manager.InterceptSpec) error { ports := e.servicePorts() if len(ports) == 0 { return fmt.Errorf("a %s requires at least one port", e.engagementType()) } err := addPortsSpec(spec, ports) if err != nil { return err } spec.HeaderFilters = e.HttpFilters spec.PathFilters = intercept.BuildPathFilters(e.Paths, e.PathPrefixes, e.PathRegexps) if len(spec.HeaderFilters) > 0 || len(spec.PathFilters) > 0 { spec.Mechanism = "http" } spec.Metadata = e.Metadata return nil } func (e *httpFilterExtension) servicePorts() []types.PortMapping { return e.Ports } type interceptExtension struct { httpFilterExtension Workload string `json:"workload,omitempty"` Service string `json:"service,omitempty"` ToPod []types.PortAndProto `json:"toPod,omitempty"` } func (e *interceptExtension) deactivate() error { return deactivateIntercept(e) } func (e *interceptExtension) init(c *config, et types.EngagementType, composeService *compose.ServiceConfig) { e.engageExtension.init(c, et, composeService) if e.Workload == "" { e.Workload = e.Name } } // Workload is the name of the engaged workload. It defaults to Name. func (e *interceptExtension) workload() string { return e.Workload } func (e *interceptExtension) activate(t *transformer) (*engagement, error) { return activateIntercept(e, t) } func (e *interceptExtension) engaged() (*engagement, error) { return createEngagement(daemon.MustGetUserClient(e.conn), e, 0) } func (e *interceptExtension) service() string { return e.Service } func (e *interceptExtension) toPod() []types.PortAndProto { return e.ToPod } func (e *interceptExtension) createInterceptRequest(localMountPort uint16) (*connector.CreateInterceptRequest, error) { ir := createInterceptRequest(e, localMountPort) spec := ir.Spec spec.ServiceName = e.service() for _, toPod := range e.toPod() { spec.LocalPorts = append(spec.LocalPorts, toPod.String()) } err := e.amendInterceptSpec(spec) if err != nil { return nil, err } return ir, nil } type ingestExtension struct { engageExtension Container string `json:"container,omitempty"` ToPod []types.PortAndProto `json:"toPod,omitempty"` } func (e *ingestExtension) activate(t *transformer) (*engagement, error) { ctx := e.conn ud := daemon.MustGetUserClient(ctx) sftpPort, err := t.config.getMountPort(e) if err != nil { return nil, err } // The ingest might be active already. ii, err := ud.GetIngest(ctx, &connector.IngestIdentifier{WorkloadName: e.workload()}) if err != nil { if status.Code(err) != codes.NotFound { return nil, grpc.FromGRPC(err) } } if ii == nil { ir := &connector.IngestRequest{ Identifier: &connector.IngestIdentifier{ WorkloadName: e.name(), ContainerName: e.container(), }, LocalMountPort: int32(sftpPort), } for _, toPod := range e.toPod() { ir.LocalPorts = append(ir.LocalPorts, toPod.String()) } ii, err = ud.Ingest(e.conn, ir) if err != nil { return nil, grpc.FromGRPC(err) } } ae, err := createEngagement(ud, e, sftpPort) if err != nil { return nil, err } ae.assignEnvAndCreateMounts(ii.Environment, ii.Mounts, t) return ae, nil } func (e *ingestExtension) container() string { return e.Container } func (e *ingestExtension) deactivate() error { ctx := context.WithoutCancel(e.connection().Context) ud := daemon.MustGetUserClient(ctx) ig, err := ud.GetIngest(ctx, &connector.IngestIdentifier{ WorkloadName: e.workload(), ContainerName: e.container(), }) if err != nil { if status.Code(err) == codes.NotFound { err = nil } return grpc.FromGRPC(err) } _, err = ud.LeaveIngest(ctx, &connector.IngestIdentifier{ WorkloadName: ig.Workload, ContainerName: ig.Container, }) return grpc.FromGRPC(err) } func (e *ingestExtension) engaged() (*engagement, error) { return createEngagement(daemon.MustGetUserClient(e.conn), e, 0) } // ToPod maps local ports to ports in an engaged pod. func (e *ingestExtension) toPod() []types.PortAndProto { return e.ToPod } type replaceExtension struct { engageExtension // Container is the name of the container that Telepresence will replace. Container string `json:"container,omitempty"` // Ports maps container ports to local ports. Ports []types.PortMapping `json:"ports,omitempty"` // ToPod maps local ports to ports in an engaged pod. ToPod []types.PortAndProto `json:"toPod,omitempty"` } func (e *replaceExtension) activate(t *transformer) (*engagement, error) { return activateIntercept(e, t) } func (e *replaceExtension) deactivate() error { return deactivateIntercept(e) } func (e *replaceExtension) engaged() (*engagement, error) { return createEngagement(daemon.MustGetUserClient(e.conn), e, 0) } func (e *replaceExtension) container() string { return e.Container } func (e *replaceExtension) containerPorts() []types.PortMapping { return e.Ports } func (e *replaceExtension) toPod() []types.PortAndProto { return e.ToPod } type wiretapExtension struct { httpFilterExtension // Service is the Kubernetes service that Telepresence will engage with. Service string `json:"service,omitempty"` } func (e *wiretapExtension) activate(t *transformer) (*engagement, error) { return activateIntercept(e, t) } func (e *wiretapExtension) deactivate() error { return deactivateIntercept(e) } func (e *wiretapExtension) engaged() (*engagement, error) { return createEngagement(daemon.MustGetUserClient(e.conn), e, 0) } func (e *wiretapExtension) service() string { return e.Service } func (e *wiretapExtension) createInterceptRequest(localMountPort uint16) (*connector.CreateInterceptRequest, error) { ir := createInterceptRequest(e, localMountPort) spec := ir.Spec spec.ServiceName = e.service() spec.Wiretap = true ir.MountReadOnly = true err := e.amendInterceptSpec(spec) if err != nil { return nil, err } return ir, nil } func (e *replaceExtension) createInterceptRequest(localMountPort uint16) (*connector.CreateInterceptRequest, error) { ir := createInterceptRequest(e, localMountPort) spec := ir.Spec spec.ContainerName = e.container() spec.Replace = true spec.NoDefaultPort = true for _, toPod := range e.toPod() { spec.LocalPorts = append(spec.LocalPorts, toPod.String()) } ports := e.containerPorts() if len(ports) == 0 { spec.PortIdentifier = "all" return ir, nil } err := addPortsSpec(spec, ports) if err != nil { return nil, err } return ir, nil } func addPortsSpec(spec *manager.InterceptSpec, ports []types.PortMapping) error { p0 := ports[0] spec.PortIdentifier = p0.To().String() spec.TargetPort = int32(p0.FromAsNumeric().Port) for i := 1; i < len(ports); i++ { pm := ports[i].String() if colIdx := strings.IndexByte(pm, ':'); colIdx > 0 { // An entry in the "ports" list puts the local port first, but it's the destination in the pod-port mapping. to := pm[:colIdx] from := pm[colIdx+1:] if slashIdx := strings.IndexByte(from, '/'); slashIdx > 0 { from = from[:slashIdx] to += from[slashIdx:] } pm = from + ":" + to } if err := types.PortMapping(pm).Validate(); err != nil { return err } spec.PodPorts = append(spec.PodPorts, pm) } return nil } func createInterceptRequest(e workloadExtension, localMountPort uint16) *connector.CreateInterceptRequest { spec := &manager.InterceptSpec{ Name: e.name(), Mechanism: "tcp", Agent: e.workload(), TargetHost: e.composeService().Name, } ir := &connector.CreateInterceptRequest{ Spec: spec, LocalMountPort: int32(localMountPort), } return ir } func activateIntercept(e workloadExtension, t *transformer) (*engagement, error) { ctx := e.connection() ud := daemon.MustGetUserClient(ctx) sftpPort, err := t.config.getMountPort(e) if err != nil { return nil, err } // The intercept might be active already. ii, err := ud.GetIntercept(ctx, &manager.GetInterceptRequest{Name: e.name()}) if err != nil { if status.Code(err) != codes.NotFound { return nil, grpc.FromGRPC(err) } } if ii == nil { ir, err := e.createInterceptRequest(sftpPort) if err != nil { return nil, err } ii, err = ud.CreateIntercept(e.connection(), ir) if err = grpc.FromGRPC(err); err != nil { return nil, fmt.Errorf("connector.CreateIntercept: %w", err) } } ae, err := createEngagement(ud, e, sftpPort) if err != nil { return nil, err } ae.assignEnvAndCreateMounts(ii.Environment, ii.Mounts, t) return ae, nil } func deactivateIntercept(e workloadExtension) error { ctx := context.WithoutCancel(e.connection().Context) ud := daemon.MustGetUserClient(ctx) ic, err := ud.GetIntercept(ctx, &manager.GetInterceptRequest{Name: e.name()}) if err != nil { if status.Code(err) == codes.NotFound { err = nil } return grpc.FromGRPC(err) } _, err = ud.RemoveIntercept(ctx, &manager.RemoveInterceptRequest2{Name: ic.Spec.Name}) return grpc.FromGRPC(err) } ================================================ FILE: pkg/client/cli/docker/compose/extension_test.go ================================================ package compose import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/telepresenceio/telepresence/rpc/v2/manager" "github.com/telepresenceio/telepresence/v2/pkg/types" ) func TestAmendInterceptSpec_MechanismSetToHTTP(t *testing.T) { tests := []struct { name string httpFilters map[string]string httpPaths []string httpPathPrefixes []string httpPathRegexps []string expectedMechanism string }{ { name: "no filters leaves mechanism as tcp", expectedMechanism: "tcp", }, { name: "header filter switches mechanism to http", httpFilters: map[string]string{"x-dev-id": "jdoe"}, expectedMechanism: "http", }, { name: "multiple header filters switch mechanism to http", httpFilters: map[string]string{"x-dev-id": "jdoe", "x-env": "staging"}, expectedMechanism: "http", }, { name: "exact path filter switches mechanism to http", httpPaths: []string{"/api/v1/users"}, expectedMechanism: "http", }, { name: "path prefix filter switches mechanism to http", httpPathPrefixes: []string{"/api/"}, expectedMechanism: "http", }, { name: "path regex filter switches mechanism to http", httpPathRegexps: []string{`/api/v[0-9]+/.*`}, expectedMechanism: "http", }, { name: "headers and paths together switch mechanism to http", httpFilters: map[string]string{"x-dev-id": "jdoe"}, httpPathPrefixes: []string{"/api/"}, expectedMechanism: "http", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ext := &httpFilterExtension{ HttpFilters: tt.httpFilters, Paths: tt.httpPaths, PathPrefixes: tt.httpPathPrefixes, PathRegexps: tt.httpPathRegexps, Ports: []types.PortMapping{"3005:3005"}, } spec := &manager.InterceptSpec{Mechanism: "tcp"} err := ext.amendInterceptSpec(spec) require.NoError(t, err) assert.Equal(t, tt.expectedMechanism, spec.Mechanism) }) } } func TestAmendInterceptSpec_HeaderFiltersPopulated(t *testing.T) { filters := map[string]string{"x-dev-id": "jdoe", "x-env": "staging"} ext := &httpFilterExtension{ HttpFilters: filters, Ports: []types.PortMapping{"3005:3005"}, } spec := &manager.InterceptSpec{Mechanism: "tcp"} err := ext.amendInterceptSpec(spec) require.NoError(t, err) assert.Equal(t, filters, spec.HeaderFilters) } func TestAmendInterceptSpec_NoPorts_ReturnsError(t *testing.T) { ext := &httpFilterExtension{ HttpFilters: map[string]string{"x-dev-id": "jdoe"}, } spec := &manager.InterceptSpec{Mechanism: "tcp"} err := ext.amendInterceptSpec(spec) require.Error(t, err) assert.Contains(t, err.Error(), "requires at least one port") } ================================================ FILE: pkg/client/cli/docker/compose/toplevelextension.go ================================================ package compose import ( "fmt" "regexp" "github.com/telepresenceio/telepresence/v2/pkg/json" "github.com/telepresenceio/telepresence/v2/pkg/types" ) type volumeMountPolicy struct { Volume string `json:"volume,omitempty"` VolumePattern *regexp.Regexp `json:"volumePattern,omitempty"` Policy types.MountPolicy `json:"policy"` } func (v *volumeMountPolicy) Matches(volumeName string) bool { return v.VolumePattern != nil && v.VolumePattern.MatchString(volumeName) || v.Volume == volumeName } type topLevelExtension struct { Connections []*connectionConfig `json:"connections,omitempty"` Mounts []volumeMountPolicy `json:"mounts,omitempty"` } func (tl *topLevelExtension) parse(v any) error { data, err := json.Marshal(v) if err != nil { return err } err = json.Unmarshal(data, tl, true) if err != nil { return err } if count := len(tl.Connections); count > 1 { // Assert that all connections have a name and that the names are unique. unique := make(map[string]struct{}, count) for _, cc := range tl.Connections { if cc.Name == "" { return fmt.Errorf("connection name is required when multiple connections are defined") } if _, ok := unique[cc.Name]; ok { return fmt.Errorf("duplicate connection name %q", cc.Name) } unique[cc.Name] = struct{}{} } } for _, m := range tl.Mounts { if m.VolumePattern != nil { if m.Volume != "" { return fmt.Errorf("volumePattern and volume are mutually exclusive") } } } return nil } ================================================ FILE: pkg/client/cli/docker/compose/transform.go ================================================ package compose import ( "context" "fmt" "os" "os/exec" "slices" "strings" compose "github.com/compose-spec/compose-go/v2/types" "github.com/puzpuzpuz/xsync/v4" "github.com/telepresenceio/clog" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/flags" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/progress" "github.com/telepresenceio/telepresence/v2/pkg/client/docker" "github.com/telepresenceio/telepresence/v2/pkg/dos" "github.com/telepresenceio/telepresence/v2/pkg/errcat" "github.com/telepresenceio/telepresence/v2/pkg/json" "github.com/telepresenceio/telepresence/v2/pkg/maps" "github.com/telepresenceio/telepresence/v2/pkg/proc" "github.com/telepresenceio/telepresence/v2/pkg/shellquote" "github.com/telepresenceio/telepresence/v2/pkg/types" ) type transformer struct { config *config project *compose.Project extensions map[string]serviceExtension engagements map[string]*engagement tpVolumes *xsync.Map[string, *compose.VolumeConfig] selectsAll bool } func newTransformer(config *config, p *compose.Project) (tr *transformer, err error) { t := &transformer{config: config, project: p, tpVolumes: xsync.NewMap[string, *compose.VolumeConfig](), selectsAll: true} t.extensions = make(map[string]serviceExtension) for n, sv := range t.project.Services { clog.Debugf(context.Background(), "Service %q has extension %q", n, sv.Extensions) ex, ok := sv.Extensions[extensionKey] if ok { if len(config.services) > 0 && !slices.Contains(config.services, n) { t.selectsAll = false continue } eg, err := t.config.parseServiceExtension(&sv, ex) if err != nil { return nil, err } t.extensions[n] = eg } } t.engagements = make(map[string]*engagement, len(t.extensions)) return t, nil } func (t *transformer) addEngagement(engagement *engagement) { t.engagements[engagement.composeService().Name] = engagement } func (t *transformer) createProject(cmdName, composeFile string) ([]string, error) { c := t.config opts := make([]string, 0, 10) opts = append(opts, "compose", "--file", composeFile) // Add "compose" specific options if c.projectName != "" { opts = append(opts, "--project-name", c.projectName) } if c.projectDir == "" { var err error c.projectDir, err = os.Getwd() if err != nil { return nil, errcat.NoDaemonLogs.New(err) } } opts = append(opts, "--project-directory", c.projectDir) if c.progress != "auto" { opts = append(opts, "--progress", c.progress) } for _, envFile := range c.envFiles { opts = append(opts, "--env-file", envFile) } opts = append(opts, cmdName) opts = c.appendFlags(c.subCommandFlags, opts) return opts, nil } func (t *transformer) marshalYAML() ([]byte, error) { return t.project.MarshalYAML() } func (t *transformer) engage(ctx context.Context, e serviceExtension, aesCh chan<- *engagement) (err error) { cn := e.composeService().Name ctx = progress.WithEventId(ctx, cn) progress.Workingf(ctx, fmt.Sprintf("%s %s", e.engagementType().Working(), cn)) var ae *engagement if t.config.mustBeConnected { ae, err = e.engaged() } else { ae, err = e.activate(t) } if err != nil { return progress.MaybeWriteError(ctx, err) } progress.Donef(ctx, fmt.Sprintf("%s %s", e.engagementType().WorkDone(), cn)) aesCh <- ae return nil } func (t *transformer) disengage(ctx context.Context) { progress.Start(ctx, "Disengaging") for _, n := range maps.SortedKeys(t.engagements) { e := t.engagements[n] eCtx := progress.WithEventId(ctx, n) progress.Workingf(eCtx, fmt.Sprintf("%s %s", e.engagementType().Leaving(), n)) err := e.deactivate() if err != nil { clog.Error(eCtx, err) } progress.Donef(eCtx, fmt.Sprintf("%s %s", e.engagementType().Left(), n)) } progress.Stop(ctx) } func (t *transformer) runCommand(ctx context.Context, name string) error { forceRecreate := false canCreate := t.selectsAll if canCreate { if f := t.config.subCommandFlags.Lookup("force-recreate"); f != nil && f.Changed { forceRecreate = f.Value.String() == "true" } if f := t.config.subCommandFlags.Lookup("no-recreate"); f != nil && f.Changed { canCreate = f.Value.String() != "true" } } composeFile, err := t.createConfigFile(ctx, canCreate, forceRecreate) if err != nil { return err } if ep := t.config.existingProject; ep != nil && !t.selectsAll { // Verify that all extended services that provide volumes are included. teleVols := make(map[string]struct{}) for n, v := range ep.Volumes { if strings.Contains(v.Driver, "/telemount:") { teleVols[n] = struct{}{} } } clog.Debugf(ctx, "teleVols: %v", teleVols) for n, sv := range ep.Services { ex, ok := sv.Extensions[extensionKey] if !ok { continue } if slices.Contains(t.config.services, n) { continue } eg, err := t.config.parseServiceExtension(&sv, ex) if err != nil { return err } switch eg.engagementType() { case types.EngagementTypeConnect, types.EngagementTypeProxy: continue default: } clog.Debugf(ctx, "Checking if service %q is a volume provider", n) if sv.Volumes != nil { for _, v := range sv.Volumes { if v.Type == compose.VolumeTypeVolume { if _, ok := teleVols[v.Source]; ok { return errcat.User.Newf("volume %q is provided extended service %q, but that service is not included", v.Source, n) } } } } } } opts, err := t.createProject(name, composeFile) if err != nil { return err } err = t.runCompose(ctx, name, composeFile, opts) if err != nil { // Prefer error output from the command to the exit code error. err = errcat.Silent.New(err) } return err } func (t *transformer) runCompose(ctx context.Context, name, composeFile string, opts []string) (err error) { if name == "up" && !flags.HasOption("detach", 'd', opts) { return t.runAttachedUp(ctx, composeFile, opts) } cmd := proc.StdCommand(ctx, docker.Exe, append(opts, t.config.services...)...) cmd.Stdin = dos.Stdin(ctx) cmd.Env = os.Environ() return cmd.Run() } func (t *transformer) runAttachedUp(parentCtx context.Context, composeFile string, opts []string) (err error) { // We need to ensure that containers are stopped when the parentCtx is canceled, but we don't want to do that // by killing the "docker compose up" process. There are multiple reasons for this: // // 1. If the "docker compose up" process is interrupted, it will detach, and the containers will continue to run // for a while longer. We don't want that because some of them might depend on engagements that will end once // this function returns. // 2. On windows, the "docker compose up" will detach, but it won't stop the containers at all.s ctx := context.WithoutCancel(parentCtx) cmd := proc.StdCommand(ctx, docker.Exe, append(opts, t.config.services...)...) cmd.Stdin = dos.Stdin(ctx) cmd.Env = os.Environ() err = cmd.Start() if err != nil { return err } parentCtx, parentCancel := context.WithCancel(parentCtx) stopDone := make(chan struct{}) go func() { <-parentCtx.Done() args := append([]string{"compose", "--file", composeFile, "stop"}, t.config.services...) stopCmd := exec.CommandContext(ctx, docker.Exe, args...) // Don't assign stdout/stderr. Avoid duplicated output from "compose up" and "compose stop". stopCmd.Env = os.Environ() clog.Debug(ctx, shellquote.ShellString(docker.Exe, args)) _ = stopCmd.Run() close(stopDone) }() err = cmd.Wait() parentCancel() <-stopDone return err } func (t *transformer) serviceExtensions() (ses map[string]serviceExtension) { return t.extensions } func (t *transformer) volumes() *xsync.Map[string, *compose.VolumeConfig] { return t.tpVolumes } func (t *transformer) withProfiles(profiles []string) error { p, err := t.project.WithProfiles(profiles) if err == nil { t.project = p } return err } // ApplyEngagements ensures that the compose-spec is modified in accordance with the engagements. func (t *transformer) applyEngagements() error { if len(t.engagements) == 0 { return nil } // WithServiceDisabled performs a deepCopy of the project when called without arguments. We // want the original Project intact. p := t.project.WithServicesDisabled() sm := p.Services deps := make(map[string][]string) for n, e := range t.engagements { if s, ok := sm[n]; ok { if e.engagementType() == types.EngagementTypeProxy { deps[n] = s.GetDependents(p) delete(sm, n) } else { e.engageService(&s) sm[n] = s } } } for n, e := range t.engagements { if proxyDeps, ok := deps[n]; ok { e.engageProxyDependents(p, n, proxyDeps) } } for _, e := range t.engagements { e.engageProject(p) } if t.tpVolumes != nil { t.tpVolumes.Range(func(k string, v *compose.VolumeConfig) bool { p.Volumes[k] = *v return true }) } err := t.ensureTopLevelExtension(p) if err != nil { return err } err = p.CheckContainerNameUnicity() if err == nil { t.project = p } return err } func (t *transformer) ensureTopLevelExtension(p *compose.Project) error { if _, ok := p.Extensions[extensionKey]; ok { return nil } // Marshal the extension to JSON and then unmarshal it back to a map. This is necessary because the // extension is a map[string]any and the marshaler used by Docker Compose doesn't support our json-tags. js, err := json.Marshal(t.config.topLevelExtension) if err != nil { return err } var ms map[string]any err = json.Unmarshal(js, &ms, true) if err != nil { return err } if p.Extensions == nil { p.Extensions = make(map[string]any) } p.Extensions[extensionKey] = ms return nil } func (t *transformer) connections() []*connection { ccs := t.config.Connections cs := make([]*connection, 0, len(ccs)) nextCfg: for _, cc := range ccs { for _, e := range t.engagements { if e.connection().connectionConfig == cc { cs = append(cs, e.connection()) continue nextCfg } } } return cs } func (t *transformer) createConfigFile(ctx context.Context, canCreate, forceRecreate bool) (composeFile string, err error) { cs := t.connections() for _, c := range cs { ud := daemon.MustGetUserClient(c) composeFile = ud.DaemonInfo().ComposeFile if composeFile != "" { if !forceRecreate { return composeFile, nil } break } } if !canCreate { return "", errcat.User.New(`the initial invocation of "compose up" or "compose create" must include all extended services`) } if composeFile != "" { clog.Debugf(ctx, "Recreating existing compose file %q", composeFile) } err = t.applyEngagements() if err != nil { return "", err } yml, err := t.marshalYAML() if err != nil { return "", err } clog.Debug(ctx, string(yml)) if composeFile == "" { var mcf *os.File mcf, err = os.CreateTemp("", "tpc-*.yaml") if err != nil { return "", err } composeFile = mcf.Name() defer func() { if err != nil { _ = os.Remove(composeFile) } }() _, err = mcf.Write(yml) mcf.Close() if err != nil { return "", err } // Save the name of the docker compose file in the daemon info. This ensures that a `docker compose down` is // issued if one of the connections is removed. This is necessary because the network represented by the // daemon will no longer be available. for _, c := range cs { ud := daemon.MustGetUserClient(c) info := ud.DaemonInfo() info.ComposeFile = composeFile err = daemon.NewUserInfoLoader(ctx).SaveInfo(info, ud.DaemonID().InfoFileName()) } } else { err = os.WriteFile(composeFile, yml, 0o644) if err != nil { return "", err } } return composeFile, nil } ================================================ FILE: pkg/client/cli/docker/flags.go ================================================ package docker import ( "context" "fmt" "strings" "github.com/spf13/pflag" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/flags" "github.com/telepresenceio/telepresence/v2/pkg/client/cli/progress" "github.com/telepresenceio/telepresence/v2/pkg/client/docker" "github.com/telepresenceio/telepresence/v2/pkg/errcat" ) type Flags struct { Run bool // --docker-run Debug bool // set if --docker-debug was used BuildOptions []string // --docker-build-opt key=value, // Optional flag to docker build can be repeated (but not comma separated) Context string // Set to build or debug by Validate function Image string Mount string // --docker-mount // where to mount in a docker container. Defaults to mount unless mount is "true" or "false". build string // --docker-build DIR | URL debug string // --docker-debug DIR | URL args []string imageIndex int } func (f *Flags) AddFlags(flagSet *pflag.FlagSet, what string) { flagSet.BoolVar(&f.Run, "docker-run", false, fmt.Sprintf( `Run a Docker container with %s environment, volume mount, by passing arguments after -- to 'docker run', `+ `e.g. '--docker-run -- -it --rm ubuntu:20.04 /bin/bash'`, what)) flagSet.StringVar(&f.build, "docker-build", "", fmt.Sprintf( `Build a Docker container from the given docker-context (path or URL), and run it with %s environment and volume mounts, `+ `by passing arguments after -- to 'docker run', e.g. '--docker-build /path/to/docker/context -- -it IMAGE /bin/bash'`, what)) flagSet.StringVar(&f.debug, "docker-debug", "", ``+ `Like --docker-build, but allows a debugger to run inside the container with relaxed security`) flagSet.StringArrayVar(&f.BuildOptions, "docker-build-opt", nil, `Options to docker-build in the form key=value, e.g. --docker-build-opt tag=mytag.`) flagSet.StringVar(&f.Mount, "docker-mount", "", ``+ `The volume mount point in docker. Defaults to same as "--mount"`) } func (f *Flags) Validate(args []string) error { drCount := 0 if f.Run { drCount++ } if f.build != "" { drCount++ f.Context = f.build } if f.debug != "" { drCount++ f.Context = f.debug f.Debug = true } alts := "--docker-run, --docker-build, or --docker-debug" if drCount > 1 { return errcat.User.Newf("only one of %s can be used", alts) } f.Run = drCount == 1 if !f.Run { if f.Mount != "" { return errcat.User.Newf("--docker-mount must be used together with %s", alts) } return nil } if flags.HasOption("detach", 'd', args) { return errcat.User.New("running docker container in background using -d or --detach is not supported") } f.Image, f.imageIndex = firstArg(args) f.args = args // Ensure that the image is ready to run before we create the intercept. if f.Context == "" { if f.imageIndex < 0 { return errcat.User.New(`unable to find the image name. When using --docker-run, the syntax after "--" must be [OPTIONS] IMAGE [COMMAND] [ARG...]`) } if f.Image != "IMAGE" { return nil } } f.Image = "" if f.imageIndex < 0 && len(args) > 0 { return errcat.User.New(`` + `the string "IMAGE", acting as a placeholder for image ID, must be included after "--" when using "--docker-build", so ` + `that flags intended for docker run can be distinguished from the command and arguments intended for the container.`) } return nil } // PullOrBuildImage will pull or build the image and return the args list suitable // when starting it. func (f *Flags) PullOrBuildImage(ctx context.Context) error { if f.Image != "" { return docker.PullImage(ctx, f.Image) } opts := make([]string, len(f.BuildOptions)) for i, opt := range f.BuildOptions { opts[i] = "--" + opt } progress.Working(ctx, "Building") imageID, err := docker.BuildImage(ctx, f.Context, opts) if err != nil { return progress.MaybeWriteError(ctx, err) } progress.Done(ctx, "Built") if f.imageIndex < 0 { f.args = []string{imageID} f.imageIndex = 0 } else { f.args[f.imageIndex] = imageID } return nil } func (f *Flags) GetContainerNameAndArgs(defaultContainerName string) (string, []string, error) { name, found, err := flags.GetUnparsedValue("name", 0, false, f.args) if err != nil { return "", nil, err } if !found { name = defaultContainerName f.args = append([]string{"--name", name}, f.args...) f.imageIndex += 2 } return name, f.args, nil } func (f *Flags) AdjustImageIndex(adjust int) { if f.imageIndex >= 0 { f.imageIndex += adjust } } var boolFlags = map[string]bool{ //nolint:gochecknoglobals // this is a constant "--detach": true, "--init": true, "--interactive": true, "--no-healthcheck": true, "--oom-kill-disable": true, "--privileged": true, "--publish-all": true, "--quiet": true, "--read-only": true, "--rm": true, "--sig-proxy": true, "--tty": true, } // firstArg returns the first argument that isn't an option. This requires knowledge // about boolean docker flags, and if new such flags arrive and are used, this // function might return an incorrect image. func firstArg(args []string) (string, int) { t := len(args) for i := 0; i < t; i++ { arg := args[i] if !strings.HasPrefix(arg, "-") { return arg, i } if strings.IndexByte(arg, '=') > 0 { continue } if strings.HasPrefix(arg, "--") { if !boolFlags[arg] { i++ } } else if strings.ContainsAny(arg, "ehlmpuvw") { // Shorthand flag that require an argument. Might be prefixed by shorthand booleans, e.g. -itl