Пример #1
0
def create_new_deployment(
    runner: Runner, deployment_arg: str, image_name: str, expose: PortMapping,
    add_custom_nameserver: bool
) -> Tuple[str, str]:
    """
    Create a new Deployment, return its name and Kubernetes label.
    """
    span = runner.span()
    run_id = runner.session_id
    runner.show(
        "Starting network proxy to cluster using "
        "new Deployment {}".format(deployment_arg)
    )

    def remove_existing_deployment(quiet=False):
        if not quiet:
            runner.show("Cleaning up Deployment {}".format(deployment_arg))
        runner.check_call(
            runner.kubectl(
                "delete",
                "--ignore-not-found",
                "svc,deploy",
                "--selector=telepresence=" + run_id,
            )
        )

    runner.add_cleanup("Delete new deployment", remove_existing_deployment)
    remove_existing_deployment(quiet=True)
    command = [
        "run",  # This will result in using Deployment:
        "--restart=Always",
        "--limits=cpu=100m,memory=256Mi",
        "--requests=cpu=25m,memory=64Mi",
        deployment_arg,
        "--image=" + image_name,
        "--labels=telepresence=" + run_id,
    ]
    # Provide a stable argument ordering.  Reverse it because that happens to
    # make some current tests happy but in the long run that's totally
    # arbitrary and doesn't need to be maintained.  See issue 494.
    for port in sorted(expose.remote(), reverse=True):
        command.append("--port={}".format(port))
    if expose.remote():
        command.append("--expose")
    # If we're on local VM we need to use different nameserver to prevent
    # infinite loops caused by sshuttle:
    if add_custom_nameserver:
        command.append(
            "--env=TELEPRESENCE_NAMESERVER=" + get_alternate_nameserver()
        )
    try:
        runner.check_call(runner.kubectl(command))
    except CalledProcessError as exc:
        raise runner.fail(
            "Failed to create deployment {}:\n{}".format(
                deployment_arg, exc.stderr
            )
        )
    span.end()
    return deployment_arg, run_id
Пример #2
0
def main():
    """
    Top-level function for Telepresence
    """

    ########################################
    # Preliminaries: No changes to the machine or the cluster, no cleanup
    # Capture environment info and the user's intent

    # Check for a subcommand
    with crash_reporting():
        args = command_parse_args(None, only_for_commands=True)
    if args is not None:
        command_main(args)

    with crash_reporting():
        args = parse_args()  # tab-completion stuff goes here

        runner = Runner(Output(args.logfile), None, args.verbose)
        span = runner.span()
        runner.add_cleanup("Stop time tracking", span.end)
        runner.kubectl = KubeInfo(runner, args)

        start_proxy = proxy.setup(runner, args)
        do_connect = connect.setup(runner, args)
        get_remote_env, write_env_files = remote_env.setup(runner, args)
        launch = outbound.setup(runner, args)
        mount_remote = mount.setup(runner, args)

        final_checks(runner, args)

        # Usage tracking
        call_scout(runner, args)

    ########################################
    # Now it's okay to change things

    with runner.cleanup_handling(), crash_reporting(runner):
        # Set up the proxy pod (operation -> pod name)
        remote_info = start_proxy(runner)

        # Connect to the proxy (pod name -> ssh object)
        socks_port, ssh = do_connect(runner, remote_info)

        # Capture remote environment information (ssh object -> env info)
        env = get_remote_env(runner, remote_info)

        # Handle filesystem stuff
        mount_dir = mount_remote(runner, env, ssh)

        # Maybe write environment files
        write_env_files(runner, env)

        # Set up outbound networking (pod name, ssh object)
        # Launch user command with the correct environment (...)
        user_process = launch(
            runner, remote_info, env, socks_port, ssh, mount_dir
        )

        wait_for_exit(runner, user_process)
Пример #3
0
def create_with_cleanup(runner: Runner, manifests: Iterable[Manifest]) -> None:
    """Create resources and set up their removal at cleanup.

    Uses "kubectl create" with the supplied manifests to create resources.
    Assumes that all the created resources include the telepresence label so it
    can use a label selector to delete those resources.
    """
    kinds = set(str(manifest["kind"]).capitalize() for manifest in manifests)
    kinds_display = ", ".join(kinds)
    manifest_list = make_k8s_list(manifests)
    manifest_json = json.dumps(manifest_list)
    try:
        runner.check_call(runner.kubectl("create", "-f", "-"),
                          input=manifest_json.encode("utf-8"))
    except CalledProcessError as exc:
        raise runner.fail("Failed to create {}:\n{}".format(
            kinds_display, exc.stderr))

    def clean_up() -> None:
        runner.show("Cleaning up {}".format(kinds_display))
        runner.check_call(
            runner.kubectl(
                "delete",
                "--ignore-not-found",
                "--wait=false",
                "--selector=telepresence=" + runner.session_id,
                ",".join(kinds),
            ))

    runner.add_cleanup("Delete proxy {}".format(kinds_display), clean_up)
Пример #4
0
def sip_workaround(runner: Runner, existing_paths: str,
                   unsupported_tools_path: str) -> str:
    """
    Workaround System Integrity Protection.

    Newer OS X don't allow injecting libraries into binaries in /bin, /sbin and
    /usr. We therefore make a copy of them and modify $PATH to point at their
    new location. It's only ~100MB so this should be pretty fast!

    :param existing_paths: Current $PATH.
    :param unsupported_tools_path: Path where we have custom versions of ping
        etc. Needs to be first in list so that we override system versions.
    """
    protected = {"/bin", "/sbin", "/usr/sbin", "/usr/bin"}
    # Remove protected paths from $PATH:
    paths = [p for p in existing_paths.split(":") if p not in protected]
    # Add temp dir
    bin_dir = mkdtemp(dir="/tmp")
    paths.insert(0, bin_dir)
    runner.add_cleanup("Remove SIP workaround binaries", rmtree, bin_dir)
    for directory in protected:
        for file in os.listdir(directory):
            try:
                copy(os.path.join(directory, file), bin_dir)
            except IOError:
                continue
            os.chmod(os.path.join(bin_dir, file), 0o775)
    paths = [unsupported_tools_path] + paths
    # Return new $PATH
    return ":".join(paths)
Пример #5
0
def main():
    """
    Top-level function for Telepresence
    """

    with crash_reporting():
        ########################################
        # Preliminaries: No changes to the machine or the cluster, no cleanup
        # Capture environment info

        args = parse_args()  # tab-completion stuff goes here

        runner = Runner(args.logfile, args.verbose)
        span = runner.span()
        runner.add_cleanup("Stop time tracking", span.end)
        set_kube_command(runner, args)

    with runner.cleanup_handling(), crash_reporting(runner):
        ########################################
        # Intent: Fast, user prompts here, cleanup available
        # Capture the user's intent

        start_proxy = proxy.setup(runner, args)
        do_connect = connect.setup(runner, args)
        get_remote_env, write_env_files = remote_env.setup(runner, args)
        launch = outbound.setup(runner, args)
        mount_remote = mount.setup(runner, args)

        final_checks(runner, args)

        # Usage tracking
        call_scout(runner, args)

        ########################################
        # Action: Perform the user's intended operation(s)
        # Now it's okay to change things

        # Set up the proxy pod (operation -> pod name)
        remote_info = start_proxy(runner)

        # Connect to the proxy (pod name -> ssh object)
        socks_port, ssh = do_connect(runner, remote_info)

        # Capture remote environment information (ssh object -> env info)
        env, pod_info = get_remote_env(runner, ssh, remote_info)

        # Handle filesystem stuff
        mount_dir = mount_remote(runner, env, ssh)

        # Maybe write environment files
        write_env_files(runner, env)

        # Set up outbound networking (pod name, ssh object)
        # Launch user command with the correct environment (...)
        user_process = launch(
            runner, remote_info, env, socks_port, ssh, mount_dir, pod_info
        )

        runner.wait_for_exit(user_process)
Пример #6
0
def swap_deployment_openshift(runner: Runner, deployment_arg: str,
                              expose: PortMapping,
                              add_custom_nameserver: bool) -> Tuple[str, str]:
    """
    Swap out an existing DeploymentConfig and also clears any triggers
    which were registered, otherwise replaced telepresence pod would
    be immediately swapped back to the original one because of
    image change trigger.

    Returns (Deployment name, unique K8s label, JSON of original container that
    was swapped out.)

    """

    run_id = runner.session_id
    deployment, container = _split_deployment_container(deployment_arg)

    dc_json_with_triggers = json.loads(
        runner.get_output(
            runner.kubectl("get", "dc/{}".format(deployment), "-o", "json",
                           "--export")))

    runner.check_call(
        runner.kubectl("set", "triggers", "dc/{}".format(deployment),
                       "--remove-all"))

    dc_json = json.loads(
        runner.get_output(
            runner.kubectl("get", "dc/{}".format(deployment), "-o", "json",
                           "--export")))

    def apply_json(json_config):
        runner.check_call(runner.kubectl("replace", "-f", "-"),
                          input=json.dumps(json_config).encode("utf-8"))
        # Now that we've updated the deployment config,
        # let's rollout latest version to apply the changes
        runner.check_call(
            runner.kubectl("rollout", "latest", "dc/{}".format(deployment)))

        runner.check_call(
            runner.kubectl("rollout", "status", "-w",
                           "dc/{}".format(deployment)))

    runner.add_cleanup("Restore original deployment config", apply_json,
                       dc_json_with_triggers)

    container = _get_container_name(container, dc_json)

    new_dc_json = new_swapped_deployment(
        dc_json,
        container,
        run_id,
        expose,
        add_custom_nameserver,
    )

    apply_json(new_dc_json)

    return deployment, run_id
Пример #7
0
def swap_deployment_openshift(
        runner: Runner, args: argparse.Namespace) -> Tuple[str, str, Dict]:
    """
    Swap out an existing DeploymentConfig.

    Returns (Deployment name, unique K8s label, JSON of original container that
    was swapped out.)

    In practice OpenShift doesn't seem to do the right thing when a
    DeploymentConfig is updated. In particular, we need to disable the image
    trigger so that we can use the new image, but the replicationcontroller
    then continues to deploy the existing image.

    So instead we use a different approach than for Kubernetes, replacing the
    current ReplicationController with one that uses the Telepresence image,
    then restores it. We delete the pods to force the RC to do its thing.
    """
    run_id = runner.session_id
    deployment_name, *container_name = args.swap_deployment.split(":", 1)
    if container_name:
        container_name = container_name[0]
    rcs = runner.get_output(
        runner.kubectl(
            "get", "rc", "-o", "name", "--selector",
            "openshift.io/deployment-config.name=" + deployment_name))
    rc_name = sorted(rcs.split(),
                     key=lambda n: int(n.split("-")[-1]))[0].split("/", 1)[1]
    rc_json = json.loads(
        runner.get_output(runner.kubectl("get", "rc", "-o", "json", "--export",
                                         rc_name),
                          stderr=STDOUT))

    def apply_json(json_config):
        runner.check_call(runner.kubectl("apply", "-f", "-"),
                          input=json.dumps(json_config).encode("utf-8"))
        # Now that we've updated the replication controller, delete pods to
        # make sure changes get applied:
        runner.check_call(
            runner.kubectl("delete", "pod", "--selector",
                           "deployment=" + rc_name))

    runner.add_cleanup("Restore original replication controller", apply_json,
                       rc_json)

    # If no container name was given, just use the first one:
    if not container_name:
        container_name = rc_json["spec"]["template"]["spec"]["containers"][0][
            "name"]

    new_rc_json, orig_container_json = new_swapped_deployment(
        rc_json,
        container_name,
        run_id,
        TELEPRESENCE_REMOTE_IMAGE,
        args.method == "vpn-tcp" and args.in_local_vm,
    )
    apply_json(new_rc_json)
    return deployment_name, run_id, orig_container_json
Пример #8
0
def swap_deployment_openshift(runner: Runner, deployment_arg: str,
                              image_name: str, expose: PortMapping,
                              add_custom_nameserver: bool) -> Tuple[str, str]:
    """
    Swap out an existing DeploymentConfig.

    Returns (Deployment name, unique K8s label, JSON of original container that
    was swapped out.)

    In practice OpenShift doesn't seem to do the right thing when a
    DeploymentConfig is updated. In particular, we need to disable the image
    trigger so that we can use the new image, but the replicationcontroller
    then continues to deploy the existing image.

    So instead we use a different approach than for Kubernetes, replacing the
    current ReplicationController with one that uses the Telepresence image,
    then restores it. We delete the pods to force the RC to do its thing.
    """
    run_id = runner.session_id
    deployment, container = _split_deployment_container(deployment_arg)
    rcs = runner.get_output(
        runner.kubectl("get", "rc", "-o", "name", "--selector",
                       "openshift.io/deployment-config.name=" + deployment))
    rc_name = sorted(rcs.split(),
                     key=lambda n: int(n.split("-")[-1]))[0].split("/", 1)[1]
    rc_json = json.loads(
        runner.get_output(runner.kubectl("get", "rc", "-o", "json", "--export",
                                         rc_name),
                          stderr=STDOUT))

    def apply_json(json_config):
        runner.check_call(runner.kubectl("apply", "-f", "-"),
                          input=json.dumps(json_config).encode("utf-8"))
        # Now that we've updated the replication controller, delete pods to
        # make sure changes get applied:
        runner.check_call(
            runner.kubectl("delete", "pod", "--selector",
                           "deployment=" + rc_name))

    runner.add_cleanup("Restore original replication controller", apply_json,
                       rc_json)

    container = _get_container_name(container, rc_json)

    new_rc_json, orig_container_json = new_swapped_deployment(
        rc_json,
        container,
        run_id,
        image_name,
        add_custom_nameserver,
    )
    apply_json(new_rc_json)

    _merge_expose_ports(expose, orig_container_json)

    return deployment, run_id
Пример #9
0
def connect_sshuttle(runner: Runner, remote_info: RemoteInfo,
                     hosts_or_ips: List[str], ssh: SSH) -> None:
    """Connect to Kubernetes using sshuttle."""
    span = runner.span()
    sshuttle_method = "auto"
    if runner.platform == "linux":
        # sshuttle tproxy mode seems to have issues:
        sshuttle_method = "nat"
    runner.launch(
        "sshuttle",
        get_sshuttle_command(ssh, sshuttle_method) + [
            # DNS proxy running on remote pod:
            "--to-ns",
            "127.0.0.1:9053",
        ] + get_proxy_cidrs(runner, remote_info, hosts_or_ips),
        keep_session=True,  # Avoid trouble with interactive sudo
    )

    # sshuttle will take a while to startup. We can detect it being up when
    # DNS resolution of services starts working. We use a specific single
    # segment so any search/domain statements in resolv.conf are applied,
    # which then allows the DNS proxy to detect the suffix domain and
    # filter it out.
    # On Macs, and perhaps elsewhere, there is OS-level caching of
    # NXDOMAIN, so bypass caching by sending new domain each time. Another,
    # less robust alternative, is to `killall -HUP mDNSResponder`.
    subspan = runner.span("sshuttle-wait")
    countdown = 3
    for idx in runner.loop_until(35, 0.1):
        # Construct a different name each time to avoid NXDOMAIN caching.
        name = "hellotelepresence-{}".format(idx)
        runner.write("Wait for vpn-tcp connection: {}".format(name))
        if dns_lookup(runner, name, 5):
            countdown -= 1
            runner.write("Resolved {}. {} more...".format(name, countdown))
            if countdown == 0:
                break
        # The loop uses a single segment to try to capture suffix or search
        # path in the proxy. However, in some network setups, single-segment
        # names don't get resolved the normal way. To see whether we're running
        # into this, also try to resolve a name with many dots. This won't
        # resolve successfully but will show up in the logs. See also:
        # https://github.com/telepresenceio/telepresence/issues/242. We use a
        # short timeout here because (1) this takes a long time for some users
        # and (2) we're only looking for a log entry; we don't expect this to
        # succeed and don't benefit from waiting for the NXDOMAIN.
        many_dotted_name = "{}.a.sanity.check.telepresence.io".format(name)
        dns_lookup(runner, many_dotted_name, 1)

    if countdown != 0:
        runner.add_cleanup("Diagnose vpn-tcp", log_info_vpn_crash, runner)
        raise RuntimeError("vpn-tcp tunnel did not connect")

    subspan.end()
    span.end()
Пример #10
0
def launch_inject(
    runner: Runner,
    command: List[str],
    socks_port: int,
    env_overrides: Dict[str, str],
) -> Popen:
    """
    Launch the user's command under torsocks
    """
    torsocks_env = set_up_torsocks(runner, socks_port)
    env_overrides.update(torsocks_env)
    env = get_local_env(runner, env_overrides, True)
    process = Popen(command, env=env)
    runner.add_cleanup("Terminate local process", terminate_local_process,
                       runner, process)
    return process
Пример #11
0
def launch_vpn(
    runner: Runner,
    remote_info: RemoteInfo,
    command: List[str],
    also_proxy: List[str],
    env_overrides: Dict[str, str],
    ssh: SSH,
) -> Popen:
    """
    Launch sshuttle and the user's command
    """
    connect_sshuttle(runner, remote_info, also_proxy, ssh)
    env = get_local_env(runner, env_overrides, False)
    process = Popen(command, env=env)
    runner.add_cleanup("Terminate local process", terminate_local_process,
                       runner, process)
    return process
Пример #12
0
def run_local_command(
    runner: Runner,
    remote_info: RemoteInfo,
    args: argparse.Namespace,
    env_overrides: Dict[str, str],
    socks_port: int,
    ssh: SSH,
) -> Popen:
    """--run-shell/--run support, run command locally."""
    env = os.environ.copy()
    env.update(env_overrides)

    # Don't use runner.popen() since we want to give program access to current
    # stdout and stderr if it wants it.
    env["PROMPT_COMMAND"] = ('PS1="@{}|$PS1";unset PROMPT_COMMAND'.format(
        args.context))

    # Inject replacements for unsupported tools like ping:
    unsupported_tools_path = get_unsupported_tools(runner,
                                                   args.method != "inject-tcp")
    env["PATH"] = unsupported_tools_path + ":" + env["PATH"]

    # Make sure we use "bash", no "/bin/bash", so we get the copied version on
    # OS X:
    if args.run is None:
        # We skip .bashrc since it might e.g. have kubectl running to get bash
        # autocomplete, and Go programs don't like DYLD on macOS at least (see
        # https://github.com/datawire/telepresence/issues/125).
        command = ["bash", "--norc"]
    else:
        command = args.run
    if args.method == "inject-tcp":
        setup_torsocks(runner, env, socks_port, unsupported_tools_path)
        p = Popen(["torsocks"] + command, env=env)
    elif args.method == "vpn-tcp":
        connect_sshuttle(runner, remote_info, args, env, ssh)
        p = Popen(command, env=env)

    def terminate_if_alive():
        runner.write("Shutting down local process...\n")
        if p.poll() is None:
            runner.write("Killing local process...\n")
            kill_process(p)

    runner.add_cleanup("Terminate local process", terminate_if_alive)
    return p
Пример #13
0
def create_new_deployment(runner: Runner,
                          args: argparse.Namespace) -> Tuple[str, str]:
    """Create a new Deployment, return its name and Kubernetes label."""
    span = runner.span()
    run_id = runner.session_id

    def remove_existing_deployment():
        runner.get_output(
            runner.kubectl(
                "delete",
                "--ignore-not-found",
                "svc,deploy",
                "--selector=telepresence=" + run_id,
            ))

    runner.add_cleanup("Delete new deployment", remove_existing_deployment)
    remove_existing_deployment()
    if args.needs_root:
        image_name = TELEPRESENCE_REMOTE_IMAGE_PRIV
    else:
        image_name = TELEPRESENCE_REMOTE_IMAGE
    command = [
        "run",
        # This will result in using Deployment:
        "--restart=Always",
        "--limits=cpu=100m,memory=256Mi",
        "--requests=cpu=25m,memory=64Mi",
        args.new_deployment,
        "--image=" + image_name,
        "--labels=telepresence=" + run_id,
    ]
    # Provide a stable argument ordering.  Reverse it because that happens to
    # make some current tests happy but in the long run that's totally
    # arbitrary and doesn't need to be maintained.  See issue 494.
    for port in sorted(args.expose.remote(), reverse=True):
        command.append("--port={}".format(port))
    if args.expose.remote():
        command.append("--expose")
    # If we're on local VM we need to use different nameserver to prevent
    # infinite loops caused by sshuttle:
    if args.method == "vpn-tcp" and args.in_local_vm:
        command.append("--env=TELEPRESENCE_NAMESERVER=" +
                       get_alternate_nameserver())
    runner.get_output(runner.kubectl(command))
    span.end()
    return args.new_deployment, run_id
Пример #14
0
def command_main(args):
    """
    Top-level function for Telepresence when executing subcommands
    """

    with crash_reporting():
        runner = Runner(Output(args.logfile), None, args.verbose)
        span = runner.span()
        runner.add_cleanup("Stop time tracking", span.end)
        runner.kubectl = KubeInfo(runner, args)

        args.operation = args.command
        args.method = "teleproxy"
        call_scout(runner, args)

    if args.command == "outbound":
        return outbound.command(runner)

    raise runner.fail("Not implemented!")
Пример #15
0
def launch_vpn(
    runner: Runner,
    remote_info: RemoteInfo,
    command: List[str],
    also_proxy: List[str],
    env_overrides: Dict[str, str],
    ssh: SSH,
) -> Popen:
    """
    Launch sshuttle and the user's command
    """
    connect_sshuttle(runner, remote_info, also_proxy, ssh)
    env = get_local_env(runner, env_overrides, False)
    try:
        process = Popen(command, env=env)
    except OSError as exc:
        raise runner.fail("Failed to launch your command: {}".format(exc))
    runner.add_cleanup("Terminate local process", terminate_local_process,
                       runner, process)
    return process
Пример #16
0
    def act(self, runner: Runner) -> RemoteInfo:
        runner.show("Starting network proxy to cluster by swapping out " +
                    "{} {} ".format(self.deployment_type, self.intent.name) +
                    "with a proxy Pod")

        def resize_original(replicas: str) -> None:
            """Resize the original deployment (kubectl scale)."""
            runner.check_call(
                runner.kubectl("scale", self.deployment_type, self.intent.name,
                               "--replicas={}".format(replicas)))

        create_with_cleanup(runner, self.manifests)

        # Scale down the original deployment
        runner.add_cleanup("Re-scale original deployment", resize_original,
                           self.original_replicas)
        resize_original("0")

        wait_for_pod(runner, self.remote_info)

        return self.remote_info
Пример #17
0
def call_scout(runner: Runner, args):
    config_root = Path(Path.home() / ".config" / "telepresence")
    config_root.mkdir(parents=True, exist_ok=True)
    id_file = Path(config_root / "id")

    scout_kwargs = dict(kubectl_version=runner.kubectl.command_version,
                        kubernetes_version=runner.kubectl.cluster_version,
                        operation=args.operation,
                        method=args.method)

    try:
        with id_file.open('x') as f:
            install_id = str(uuid4())
            f.write(install_id)
            scout_kwargs["new_install"] = True
    except FileExistsError:
        with id_file.open('r') as f:
            install_id = f.read()
            scout_kwargs["new_install"] = False

    scout = Scout("telepresence", __version__, install_id)
    scouted = scout.report(**scout_kwargs)

    runner.write("Scout info: {}".format(scouted))

    my_version = get_numeric_version(__version__)
    try:
        latest = get_numeric_version(scouted["latest_version"])
    except (KeyError, ValueError):
        latest = my_version

    if latest > my_version:
        message = ("\nTelepresence {} is available (you're running {}). "
                   "https://www.telepresence.io/reference/changelog").format(
                       scouted["latest_version"], __version__)

        def ver_notice():
            runner.show(message)

        runner.add_cleanup("Show version notice", ver_notice)
Пример #18
0
def launch_local(
    runner: Runner,
    command: List[str],
    env_overrides: Dict[str, str],
    replace_dns_tools: bool,
) -> Popen:
    # Compute user process environment
    env = os.environ.copy()
    env.update(env_overrides)
    env["PROMPT_COMMAND"] = ('PS1="@{}|$PS1";unset PROMPT_COMMAND'.format(
        runner.kubectl.context))
    env["PATH"] = apply_workarounds(runner, env["PATH"], replace_dns_tools)

    # Launch the user process
    runner.show("Setup complete. Launching your command.")
    try:
        process = Popen(command, env=env)
    except OSError as exc:
        raise runner.fail("Failed to launch your command: {}".format(exc))
    runner.add_cleanup("Terminate local process", terminate_local_process,
                       runner, process)
    return process
Пример #19
0
def supplant_deployment(runner: Runner,
                        args: argparse.Namespace) -> Tuple[str, str, Dict]:
    """
    Swap out an existing Deployment, supplant method.

    Native Kubernetes version.

    Returns (Deployment name, unique K8s label, JSON of original container that
    was swapped out.)
    """
    span = runner.span()
    run_id = runner.session_id

    deployment_name, *container_name = args.swap_deployment.split(":", 1)
    if container_name:
        container_name = container_name[0]
    deployment_json = get_deployment_json(
        runner,
        deployment_name,
        args.context,
        args.namespace,
        "deployment",
    )

    # If no container name was given, just use the first one:
    if not container_name:
        container_name = deployment_json["spec"]["template"]["spec"][
            "containers"][0]["name"]

    # If we're on local VM we need to use different nameserver to
    # prevent infinite loops caused by sshuttle.
    add_custom_nameserver = args.method == "vpn-tcp" and args.in_local_vm

    if args.needs_root:
        image_name = TELEPRESENCE_REMOTE_IMAGE_PRIV
    else:
        image_name = TELEPRESENCE_REMOTE_IMAGE

    new_deployment_json, orig_container_json = new_swapped_deployment(
        deployment_json,
        container_name,
        run_id,
        image_name,
        add_custom_nameserver,
    )

    # Compute a new name that isn't too long, i.e. up to 63 characters.
    # Trim the original name until "tel-{run_id}-{pod_id}" fits.
    # https://github.com/kubernetes/community/blob/master/contributors/design-proposals/architecture/identifiers.md
    new_deployment_name = "{name:.{max_width}s}-{id}".format(
        name=deployment_json["metadata"]["name"],
        id=run_id,
        max_width=(50 - (len(run_id) + 1)))
    new_deployment_json["metadata"]["name"] = new_deployment_name

    def resize_original(replicas):
        """Resize the original deployment (kubectl scale)"""
        runner.check_call(
            runner.kubectl("scale", "deployment", deployment_name,
                           "--replicas={}".format(replicas)))

    def delete_new_deployment(check):
        """Delete the new (copied) deployment"""
        ignore = []
        if not check:
            ignore = ["--ignore-not-found"]
        runner.check_call(
            runner.kubectl("delete", "deployment", new_deployment_name,
                           *ignore))

    # Launch the new deployment
    runner.add_cleanup("Delete new deployment", delete_new_deployment, True)
    delete_new_deployment(False)  # Just in case
    runner.check_call(runner.kubectl("apply", "-f", "-"),
                      input=json.dumps(new_deployment_json).encode("utf-8"))

    # Scale down the original deployment
    runner.add_cleanup("Re-scale original deployment", resize_original,
                       deployment_json["spec"]["replicas"])
    resize_original(0)

    span.end()
    return new_deployment_name, run_id, orig_container_json
Пример #20
0
def create_new_deployment(
    runner: Runner,
    deployment_arg: str,
    expose: PortMapping,
    custom_nameserver: Optional[str],
    service_account: str,
) -> Tuple[str, str]:
    """
    Create a new Deployment, return its name and Kubernetes label.
    """
    span = runner.span()
    run_id = runner.session_id
    runner.show(
        "Starting network proxy to cluster using "
        "new Deployment {}".format(deployment_arg)
    )

    def remove_existing_deployment(quiet=False):
        if not quiet:
            runner.show("Cleaning up Deployment {}".format(deployment_arg))
        runner.check_call(
            runner.kubectl(
                "delete",
                "--ignore-not-found",
                "svc,deploy",
                "--selector=telepresence=" + run_id,
            )
        )

    runner.add_cleanup("Delete new deployment", remove_existing_deployment)
    remove_existing_deployment(quiet=True)
    # Define the deployment as yaml
    env = {}
    if custom_nameserver:
        # If we're on local VM we need to use different nameserver to prevent
        # infinite loops caused by sshuttle:
        env["TELEPRESENCE_NAMESERVER"] = custom_nameserver
    # Create the deployment via yaml
    deployment_yaml = _get_deployment_yaml(
        deployment_arg,
        run_id,
        get_image_name(runner, expose),
        service_account,
        env,
    )
    try:
        runner.check_call(
            runner.kubectl("create", "-f", "-"),
            input=deployment_yaml.encode("utf-8")
        )
    except CalledProcessError as exc:
        raise runner.fail(
            "Failed to create deployment {}:\n{}".format(
                deployment_arg, exc.stderr
            )
        )
    # Expose the deployment with a service
    if expose.remote():
        command = [
            "expose",
            "deployment",
            deployment_arg,
        ]
        # Provide a stable argument ordering.  Reverse it because that
        # happens to make some current tests happy but in the long run
        # that's totally arbitrary and doesn't need to be maintained.
        # See issue 494.
        for port in sorted(expose.remote(), reverse=True):
            command.append("--port={}".format(port))
        try:
            runner.check_call(runner.kubectl(*command))
        except CalledProcessError as exc:
            raise runner.fail(
                "Failed to expose deployment {}:\n{}".format(
                    deployment_arg, exc.stderr
                )
            )
    span.end()
    return deployment_arg, run_id
Пример #21
0
def connect(runner: Runner, remote_info: RemoteInfo, is_container_mode: bool,
            expose: PortMapping) -> Tuple[int, SSH]:
    """
    Start all the processes that handle remote proxying.

    Return (local port of SOCKS proxying tunnel, SSH instance).
    """
    span = runner.span()
    # Keep local copy of pod logs, for debugging purposes:
    runner.launch(
        "kubectl logs",
        runner.kubectl("logs", "-f", remote_info.pod_name, "--container",
                       remote_info.container_name),
        bufsize=0,
    )

    ssh = SSH(runner, find_free_port())

    # forward remote port to here, by tunneling via remote SSH server:
    runner.launch(
        "kubectl port-forward",
        runner.kubectl("port-forward", remote_info.pod_name,
                       "{}:8022".format(ssh.port)))
    if is_container_mode:
        # kubectl port-forward currently only listens on loopback. So we
        # portforward from the docker0 interface on Linux, and the lo0 alias we
        # added on OS X, to loopback (until we can use kubectl port-forward
        # option to listen on docker0 -
        # https://github.com/kubernetes/kubernetes/pull/46517, or all our users
        # have latest version of Docker for Mac, which has nicer solution -
        # https://github.com/datawire/telepresence/issues/224).
        if runner.platform == "linux":

            # If ip addr is available use it if not fall back to ifconfig.
            missing = runner.depend(["ip", "ifconfig"])
            if "ip" not in missing:
                docker_interfaces = re.findall(
                    r"(\d+\.\d+\.\d+\.\d+)",
                    runner.get_output(["ip", "addr", "show", "dev",
                                       "docker0"]))
            elif "ifconfig" not in missing:
                docker_interfaces = re.findall(
                    r"(\d+\.\d+\.\d+\.\d+)",
                    runner.get_output(["ifconfig", "docker0"]))
            else:
                raise runner.fail(
                    """At least one of "ip addr" or "ifconfig" must be """ +
                    "available to retrieve Docker interface info.")

            if len(docker_interfaces) == 0:
                raise runner.fail("No interface for docker found")

            docker_interface = docker_interfaces[0]

        else:
            # The way to get routing from container to host is via an alias on
            # lo0 (https://docs.docker.com/docker-for-mac/networking/). We use
            # an IP range that is assigned for testing network devices and
            # therefore shouldn't conflict with real IPs or local private
            # networks (https://tools.ietf.org/html/rfc6890).
            runner.check_call(
                ["sudo", "ifconfig", "lo0", "alias", MAC_LOOPBACK_IP])
            runner.add_cleanup(
                "Mac Loopback", runner.check_call,
                ["sudo", "ifconfig", "lo0", "-alias", MAC_LOOPBACK_IP])
            docker_interface = MAC_LOOPBACK_IP

        runner.launch("socat for docker", [
            "socat", "TCP4-LISTEN:{},bind={},reuseaddr,fork".format(
                ssh.port,
                docker_interface,
            ), "TCP4:127.0.0.1:{}".format(ssh.port)
        ])

    ssh.wait()

    # In Docker mode this happens inside the local Docker container:
    if not is_container_mode:
        expose_local_services(
            runner,
            ssh,
            list(expose.local_to_remote()),
        )

    # Start tunnels for the SOCKS proxy (local -> remote)
    # and the local server for the proxy to poll (remote -> local).
    socks_port = find_free_port()
    local_server_port = find_free_port()
    runner.track_background(
        launch_local_server(local_server_port, runner.output))
    forward_args = [
        "-L127.0.0.1:{}:127.0.0.1:9050".format(socks_port),
        "-R9055:127.0.0.1:{}".format(local_server_port)
    ]
    runner.launch("SSH port forward (socks and proxy poll)",
                  ssh.bg_command(forward_args))

    span.end()
    return socks_port, ssh
Пример #22
0
def run_docker_command(
    runner: Runner,
    remote_info: RemoteInfo,
    args: argparse.Namespace,
    remote_env: Dict[str, str],
    ssh: SSH,
    mount_dir: Optional[str],
) -> Popen:
    """
    --docker-run support.

    Connect using sshuttle running in a Docker container, and then run user
    container.

    :param args: Command-line args to telepresence binary.
    :param remote_env: Dictionary with environment on remote pod.
    :param mount_dir: Path to local directory where remote pod's filesystem is
        mounted.
    """
    if SUDO_FOR_DOCKER:
        runner.require_sudo()

    # Update environment:
    remote_env["TELEPRESENCE_METHOD"] = "container"  # mostly just for tests :(

    # Extract --publish flags and add them to the sshuttle container, which is
    # responsible for defining the network entirely.
    docker_args, publish_args = parse_docker_args(args.docker_run)

    # Start the sshuttle container:
    name = random_name()
    config = {
        "port":
        ssh.port,
        "cidrs":
        get_proxy_cidrs(
            runner, args, remote_info, remote_env["KUBERNETES_SERVICE_HOST"]
        ),
        "expose_ports":
        list(args.expose.local_to_remote()),
    }
    if runner.platform == "darwin":
        config["ip"] = MAC_LOOPBACK_IP
    # Image already has tini init so doesn't need --init option:
    span = runner.span()
    runner.launch(
        "Network container",
        docker_runify(
            publish_args + [
                "--rm", "--privileged", "--name=" +
                name, TELEPRESENCE_LOCAL_IMAGE, "proxy",
                json.dumps(config)
            ]
        ),
        killer=make_docker_kill(runner, name)
    )

    # Wait for sshuttle to be running:
    while True:
        try:
            runner.check_call(
                docker_runify([
                    "--network=container:" + name, "--rm",
                    TELEPRESENCE_LOCAL_IMAGE, "wait"
                ])
            )
        except CalledProcessError as e:
            if e.returncode == 100:
                # We're good!
                break
            elif e.returncode == 125:
                # Docker failure, probably due to original container not
                # starting yet... so sleep and try again:
                sleep(1)
                continue
            else:
                raise
        else:
            raise RuntimeError(
                "Waiting container exited prematurely. File a bug, please!"
            )

    # Start the container specified by the user:
    container_name = random_name()
    docker_command = docker_runify([
        "--name=" + container_name,
        "--network=container:" + name,
    ],
                                   env=True)

    # Prepare container environment
    for key in remote_env:
        docker_command.append("-e={}".format(key))
    docker_env = os.environ.copy()
    docker_env.update(remote_env)

    if mount_dir:
        docker_command.append("--volume={}:{}".format(mount_dir, mount_dir))

    # Don't add --init if the user is doing something with it
    init_args = [
        arg for arg in docker_args
        if arg == "--init" or arg.startswith("--init=")
    ]
    # Older versions of Docker don't have --init:
    if not init_args and "--init" in runner.get_output([
        "docker", "run", "--help"
    ]):
        docker_command += ["--init"]
    docker_command += docker_args
    span.end()

    p = Popen(docker_command, env=docker_env)

    def terminate_if_alive():
        runner.write("Shutting down containers...\n")
        if p.poll() is None:
            runner.write("Killing local container...\n")
            make_docker_kill(runner, container_name)()

    runner.add_cleanup("Terminate local container", terminate_if_alive)
    return p
Пример #23
0
def supplant_deployment(runner: Runner, deployment_arg: str, image_name: str,
                        expose: PortMapping,
                        add_custom_nameserver: bool) -> Tuple[str, str]:
    """
    Swap out an existing Deployment, supplant method.

    Native Kubernetes version.

    Returns (Deployment name, unique K8s label, JSON of original container that
    was swapped out.)
    """
    span = runner.span()
    run_id = runner.session_id

    deployment, container = _split_deployment_container(deployment_arg)
    deployment_json = get_deployment_json(
        runner,
        deployment,
        "deployment",
    )
    container = _get_container_name(container, deployment_json)

    new_deployment_json, orig_container_json = new_swapped_deployment(
        deployment_json,
        container,
        run_id,
        image_name,
        add_custom_nameserver,
    )

    # Compute a new name that isn't too long, i.e. up to 63 characters.
    # Trim the original name until "tel-{run_id}-{pod_id}" fits.
    # https://github.com/kubernetes/community/blob/master/contributors/design-proposals/architecture/identifiers.md
    new_deployment_name = "{name:.{max_width}s}-{id}".format(
        name=deployment_json["metadata"]["name"],
        id=run_id,
        max_width=(50 - (len(run_id) + 1)))
    new_deployment_json["metadata"]["name"] = new_deployment_name

    def resize_original(replicas):
        """Resize the original deployment (kubectl scale)"""
        runner.check_call(
            runner.kubectl("scale", "deployment", deployment,
                           "--replicas={}".format(replicas)))

    def delete_new_deployment(check):
        """Delete the new (copied) deployment"""
        ignore = []
        if not check:
            ignore = ["--ignore-not-found"]
        runner.check_call(
            runner.kubectl("delete", "deployment", new_deployment_name,
                           *ignore))

    # Launch the new deployment
    runner.add_cleanup("Delete new deployment", delete_new_deployment, True)
    delete_new_deployment(False)  # Just in case
    runner.check_call(runner.kubectl("apply", "-f", "-"),
                      input=json.dumps(new_deployment_json).encode("utf-8"))

    # Scale down the original deployment
    runner.add_cleanup("Re-scale original deployment", resize_original,
                       deployment_json["spec"]["replicas"])
    resize_original(0)

    _merge_expose_ports(expose, orig_container_json)

    span.end()
    return new_deployment_name, run_id
Пример #24
0
def run_docker_command(
    runner: Runner,
    remote_info: RemoteInfo,
    docker_run: List[str],
    expose: PortMapping,
    to_pod: List[int],
    from_pod: List[int],
    container_to_host: PortMapping,
    remote_env: Dict[str, str],
    ssh: SSH,
    mount_dir: Optional[str],
    use_docker_mount: Optional[bool],
    pod_info: Dict[str, str],
) -> "subprocess.Popen[bytes]":
    """
    --docker-run support.

    Connect using sshuttle running in a Docker container, and then run user
    container.

    :param remote_env: Dictionary with environment on remote pod.
    :param mount_dir: Path to local directory where remote pod's filesystem is
        mounted.
    """
    # Update environment:
    remote_env["TELEPRESENCE_METHOD"] = "container"  # mostly just for tests :(

    # Extract --publish flags and add them to the sshuttle container, which is
    # responsible for defining the network entirely.
    docker_args, publish_args = parse_docker_args(docker_run)

    # Point a host port to the network container's sshd
    container_sshd_port = find_free_port()
    publish_args.append(
        "--publish=127.0.0.1:{}:38022/tcp".format(container_sshd_port)
    )
    local_ssh = SSH(runner, container_sshd_port, "[email protected]")

    # Start the network (sshuttle) container:
    name = random_name()
    config = {
        "cidrs": ["0/0"],
        "expose_ports": list(expose.local_to_remote()),
        "to_pod": to_pod,
        "from_pod": from_pod,
    }
    dns_args = []
    if "hostname" in pod_info:
        dns_args.append("--hostname={}".format(pod_info["hostname"].strip()))
    if "hosts" in pod_info:
        dns_args.extend(parse_hosts_aliases(pod_info["hosts"]))
    if "resolv" in pod_info:
        dns_args.extend(parse_resolv_conf(pod_info["resolv"]))

    # Image already has tini init so doesn't need --init option:
    span = runner.span()
    runner.launch(
        "Network container",
        runner.docker(
            "run", *publish_args, *dns_args, "--rm", "--privileged",
            "--name=" + name, TELEPRESENCE_LOCAL_IMAGE, "proxy",
            json.dumps(config)
        ),
        killer=make_docker_kill(runner, name),
        keep_session=runner.sudo_for_docker,
    )

    # Set up ssh tunnel to allow the container to reach the cluster
    if not local_ssh.wait():
        raise RuntimeError("SSH to the network container failed to start.")

    container_forward_args = ["-R", "38023:127.0.0.1:{}".format(ssh.port)]
    for container_port, host_port in container_to_host.local_to_remote():
        if runner.chatty:
            runner.show(
                "Forwarding container port {} to host port {}.".format(
                    container_port, host_port
                )
            )
        container_forward_args.extend([
            "-R", "{}:127.0.0.1:{}".format(container_port, host_port)
        ])
    runner.launch(
        "Local SSH port forward", local_ssh.bg_command(container_forward_args)
    )

    # Wait for sshuttle to be running:
    sshuttle_ok = False
    for _ in runner.loop_until(120, 1):
        try:
            runner.check_call(
                runner.docker(
                    "run", "--network=container:" + name, "--rm",
                    TELEPRESENCE_LOCAL_IMAGE, "wait"
                )
            )
        except subprocess.CalledProcessError as e:
            if e.returncode == 100:
                # We're good!
                sshuttle_ok = True
                break
            elif e.returncode == 125:
                # Docker failure, probably due to original container not
                # starting yet... so try again:
                continue
            else:
                raise
        else:
            raise RuntimeError(
                "Waiting container exited prematurely. File a bug, please!"
            )
    if not sshuttle_ok:
        # This used to loop forever. Now we time out after two minutes.
        raise RuntimeError(
            "Waiting for network container timed out. File a bug, please!"
        )

    # Start the container specified by the user:
    container_name = random_name()
    docker_command = runner.docker(
        "run",
        "--name=" + container_name,
        "--network=container:" + name,
        env=True,
    )

    # Prepare container environment
    for key in remote_env:
        docker_command.append("-e={}".format(key))
    docker_env = os.environ.copy()
    docker_env.update(remote_env)

    if mount_dir:
        if use_docker_mount:
            mount_volume = "telepresence-" + runner.session_id
        else:
            mount_volume = mount_dir

        docker_command.append("--volume={}:{}".format(mount_volume, mount_dir))

    # Don't add --init if the user is doing something with it
    init_args = [
        arg for arg in docker_args
        if arg == "--init" or arg.startswith("--init=")
    ]
    # Older versions of Docker don't have --init:
    docker_run_help = runner.get_output(["docker", "run", "--help"])
    if not init_args and "--init" in docker_run_help:
        docker_command += ["--init"]
    docker_command += docker_args
    span.end()

    runner.show("Setup complete. Launching your container.")
    process = subprocess.Popen(docker_command, env=docker_env)

    def terminate_if_alive() -> None:
        runner.write("Shutting down containers...\n")
        if process.poll() is None:
            runner.write("Killing local container...\n")
            make_docker_kill(runner, container_name)()

    runner.add_cleanup("Terminate local container", terminate_if_alive)
    return process