diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 288b846..474b800 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 @@ -77,7 +77,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Set up Docker Buildx id: buildx @@ -109,10 +109,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Download docs artifacts - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v7 with: name: docs path: .pipeline/output/doc diff --git a/lib/pix/command/help.ex b/lib/pix/command/help.ex index dadf5b0..679e025 100644 --- a/lib/pix/command/help.ex +++ b/lib/pix/command/help.ex @@ -75,7 +75,7 @@ defmodule Pix.Command.Help do defp help_cmd("run") do """ - #{_cmd("pix run")} [#{_opt("--output")}] [#{_opt("--ssh")}] [#{_opt("--arg ARG")} ...] [#{_opt("--progress PROGRESS")}] [#{_opt("--target TARGET")} [#{_opt("--tag TAG")}]] [#{_opt("--no-cache")}] [#{_opt("--no-cache-filter TARGET")} ...] PIPELINE + #{_cmd("pix run")} [#{_opt("--output")}] [#{_opt("--ssh [SPEC]")} ...] [#{_opt("--arg ARG")} ...] [#{_opt("--progress PROGRESS")}] [#{_opt("--target TARGET")} [#{_opt("--tag TAG")}]] [#{_opt("--no-cache")}] [#{_opt("--no-cache-filter TARGET")} ...] PIPELINE Run PIPELINE. @@ -91,7 +91,8 @@ defmodule Pix.Command.Help do #{_opt("--no-cache-filter")}* Do not cache specified targets #{_opt("--progress")} Set type of progress output - "auto", "plain", "tty", "rawjson" (default "auto") #{_opt("--secret")}* Forward one or more secrets to `buildx build` - #{_opt("--ssh")} Forward SSH agent to `buildx build` + #{_opt("--ssh")}* Forward SSH agent/keys to `buildx build` (format: default[=] | =). + Can be specified multiple times. #{_opt("--tag")} Tag the TARGET's docker image (default: no tag) #{_opt("--target")} Run PIPELINE for a specific TARGET (default: all the PIPELINE targets) """ @@ -99,7 +100,7 @@ defmodule Pix.Command.Help do defp help_cmd("shell") do """ - #{_cmd("pix shell")} [#{_opt("--ssh")}] [#{_opt("--arg ARG")} ...] [#{_opt("--target TARGET")}] [#{_opt("--host")}] PIPELINE [COMMAND] + #{_cmd("pix shell")} [#{_opt("--ssh [SPEC]")} ...] [#{_opt("--arg ARG")} ...] [#{_opt("--target TARGET")}] [#{_opt("--host")}] PIPELINE [COMMAND] Shell into the specified target of the PIPELINE. @@ -113,7 +114,9 @@ defmodule Pix.Command.Help do OPTIONS: #{_opt("--arg")}* Set one or more pipeline ARG (format KEY=value) #{_opt("--secret")}* Forward one or more secrets to `buildx build` - #{_opt("--ssh")} Forward SSH agent to shell container + #{_opt("--ssh")}* Forward SSH agent/keys to the build and shell container (format: default[=] | =). + Custom key files are mounted read-only into the shell container. + Can be specified multiple times. #{_opt("--target")} The shell target """ end diff --git a/lib/pix/command/run.ex b/lib/pix/command/run.ex index 74c1189..b6e8271 100644 --- a/lib/pix/command/run.ex +++ b/lib/pix/command/run.ex @@ -8,7 +8,7 @@ defmodule Pix.Command.Run do output: :boolean, progress: :string, secret: [:string, :keep], - ssh: :boolean, + ssh: [:string, :keep], tag: :string, target: :string ] diff --git a/lib/pix/command/shell.ex b/lib/pix/command/shell.ex index 3a1451f..f923667 100644 --- a/lib/pix/command/shell.ex +++ b/lib/pix/command/shell.ex @@ -5,7 +5,7 @@ defmodule Pix.Command.Shell do arg: [:string, :keep], host: :boolean, secret: [:string, :keep], - ssh: :boolean, + ssh: [:string, :keep], target: :string ] diff --git a/lib/pix/docker.ex b/lib/pix/docker.ex index f02098a..9afe8b8 100644 --- a/lib/pix/docker.ex +++ b/lib/pix/docker.ex @@ -40,9 +40,10 @@ defmodule Pix.Docker do end end - @spec run(image :: String.t(), ssh_fwd? :: boolean(), opts(), cmd_args :: [String.t()]) :: status :: non_neg_integer() - def run(image, ssh_fwd?, opts, cmd_args) do - ssh_opts = if ssh_fwd?, do: run_opts_ssh_forward(), else: [] + @spec run(image :: String.t(), ssh_specs :: [String.t()], opts(), cmd_args :: [String.t()]) :: + status :: non_neg_integer() + def run(image, ssh_specs, opts, cmd_args) do + ssh_opts = if ssh_specs != [], do: run_opts_ssh_forward(ssh_specs), else: [] opts = opts ++ ssh_opts ++ run_opts_docker_outside_of_docker() args = ["run"] ++ opts_encode(opts) ++ Pix.Env.pix_docker_run_opts() ++ [image] ++ cmd_args @@ -73,8 +74,16 @@ defmodule Pix.Docker do :ok end - @spec run_opts_ssh_forward :: opts() - defp run_opts_ssh_forward do + @spec run_opts_ssh_forward([String.t()]) :: opts() + defp run_opts_ssh_forward(ssh_specs) do + agent_opts = if "default" in ssh_specs, do: run_opts_ssh_agent_forward(), else: [] + {volume_opts, key_paths} = run_opts_ssh_key_mounts(ssh_specs) + git_ssh_opts = run_opts_git_ssh_command(key_paths) + agent_opts ++ volume_opts ++ git_ssh_opts + end + + @spec run_opts_ssh_agent_forward :: opts() + defp run_opts_ssh_agent_forward do ssh_sock = cond do :os.type() == {:unix, :darwin} -> @@ -101,6 +110,30 @@ defmodule Pix.Docker do end end + @spec run_opts_ssh_key_mounts([String.t()]) :: {opts(), key_paths :: [String.t()]} + defp run_opts_ssh_key_mounts(ssh_specs) do + Enum.reduce(ssh_specs, {[], []}, fn spec, {vol_acc, key_path_acc} -> + case String.split(spec, "=", parts: 2) do + [_id, path] when path != "" -> + path = Path.expand(path) + container_path = "/root/.ssh/#{Path.basename(path)}" + Pix.Report.internal(">>> mounting SSH key #{inspect(path)} as #{container_path} into shell container\n") + {[{:volume, "#{path}:#{container_path}:ro"} | vol_acc], [container_path | key_path_acc]} + + _ -> + {vol_acc, key_path_acc} + end + end) + end + + @spec run_opts_git_ssh_command([String.t()]) :: opts() + defp run_opts_git_ssh_command([]), do: [] + + defp run_opts_git_ssh_command(key_paths) do + identity_flags = Enum.map_join(key_paths, " ", &"-i #{&1}") + [env: "GIT_SSH_COMMAND=ssh #{identity_flags} -o StrictHostKeyChecking=no"] + end + @spec run_opts_docker_outside_of_docker :: opts() defp run_opts_docker_outside_of_docker do docker_socket = "/var/run/docker.sock" @@ -108,9 +141,9 @@ defmodule Pix.Docker do [volume: "#{docker_socket}:#{docker_socket}"] end - @spec build(ssh_fwd? :: boolean(), opts(), String.t()) :: exit_status :: non_neg_integer() - def build(ssh_fwd?, opts, ctx) do - ssh_opts = if ssh_fwd?, do: [ssh: "default"], else: [] + @spec build(ssh_specs :: [String.t()], opts(), String.t()) :: exit_status :: non_neg_integer() + def build(ssh_specs, opts, ctx) do + ssh_opts = Enum.map(ssh_specs, fn spec -> {:ssh, spec} end) builder_opts = case buildx_builder() do diff --git a/lib/pix/pipeline.ex b/lib/pix/pipeline.ex index e1a680a..19b1d9f 100644 --- a/lib/pix/pipeline.ex +++ b/lib/pix/pipeline.ex @@ -8,7 +8,7 @@ defmodule Pix.Pipeline do | {:output, boolean()} | {:progress, String.t()} | {:secret, String.t()} - | {:ssh, boolean()} + | {:ssh, String.t()} | {:tag, String.t()} | {:target, String.t()} ] @@ -16,7 +16,8 @@ defmodule Pix.Pipeline do @type shell_cli_opts :: [ {:arg, String.t()} | {:host, boolean()} - | {:ssh, boolean()} + | {:secret, String.t()} + | {:ssh, String.t()} | {:target, String.t()} ] @@ -198,7 +199,9 @@ defmodule Pix.Pipeline do Pix.Report.info("\nRunning pipeline (targets: #{inspect(targets)})\n\n") build_opts = build_opts ++ [file: dockerfile_path] - Pix.Docker.build(cli_opts[:ssh], build_opts, ".") |> halt_on_error() + ssh_opts = Keyword.get_values(cli_opts, :ssh) + + Pix.Docker.build(ssh_opts, build_opts, ".") |> halt_on_error() if cli_opts[:output] do Pix.Report.info("\nExported pipeline outputs to #{output_dir()}:\n") @@ -216,7 +219,9 @@ defmodule Pix.Pipeline do build_opts = build_opts ++ [target: cli_opts[:target], file: dockerfile_path] build_opts = add_run_tag_option(build_opts, cli_opts) - Pix.Docker.build(cli_opts[:ssh], build_opts, ".") |> halt_on_error() + ssh_opts = Keyword.get_values(cli_opts, :ssh) + + Pix.Docker.build(ssh_opts, build_opts, ".") |> halt_on_error() :ok end @@ -281,7 +286,9 @@ defmodule Pix.Pipeline do defp execute_shell_build(build_opts, shell_target, cli_opts) do Pix.Report.info("\nBuilding pipeline (target=#{shell_target})\n\n") - Pix.Docker.build(cli_opts[:ssh], build_opts, ".") + ssh_opts = Keyword.get_values(cli_opts, :ssh) + + Pix.Docker.build(ssh_opts, build_opts, ".") |> halt_on_error() :ok @@ -292,9 +299,10 @@ defmodule Pix.Pipeline do Pix.Report.info("\nEntering shell\n") opts = shell_run_options(shell_target, from, cli_opts) + ssh_opts = Keyword.get_values(cli_opts, :ssh) shell_docker_image - |> Pix.Docker.run(cli_opts[:ssh], opts, cmd_args) + |> Pix.Docker.run(ssh_opts, opts, cmd_args) |> halt_on_error() :ok diff --git a/lib/pix/user_settings.ex b/lib/pix/user_settings.ex index 5dac148..1afaaae 100644 --- a/lib/pix/user_settings.ex +++ b/lib/pix/user_settings.ex @@ -16,12 +16,12 @@ defmodule Pix.UserSettings do command: %{ run: %{ cli_opts: [ - ssh: true + ssh: "default" ] }, shell: %{ cli_opts: [ - ssh: true + ssh: "default" ] } } diff --git a/shell_completions/pix.fish b/shell_completions/pix.fish index bd9c4eb..3c9f539 100644 --- a/shell_completions/pix.fish +++ b/shell_completions/pix.fish @@ -70,7 +70,7 @@ complete -c pix -n "__fish_seen_subcommand_from graph" -l "format" -d "Output fo # run command options complete -c pix -n "__fish_seen_subcommand_from run" -a "(__pix_get_pipelines)" -d "Pipeline" complete -c pix -n "__fish_seen_subcommand_from run" -l "output" -d "Output the target artifacts under .pipeline/output directory" -complete -c pix -n "__fish_seen_subcommand_from run" -l "ssh" -d "Forward SSH agent to buildx build" +complete -c pix -n "__fish_seen_subcommand_from run" -l "ssh" -d "Forward SSH agent/keys to buildx build (default, or id=path)" -rf complete -c pix -n "__fish_seen_subcommand_from run" -l "arg" -d "Set one or more pipeline ARG (format KEY=value)" -a "(__pix_get_run_target_args)" complete -c pix -n "__fish_seen_subcommand_from run" -l "progress" -d "Set type of progress output" -a "auto plain tty rawjson" complete -c pix -n "__fish_seen_subcommand_from run" -l "secret" -d "Forward one or more secrets to `buildx build`" @@ -81,7 +81,7 @@ complete -c pix -n "__fish_seen_subcommand_from run" -l "no-cache-filter" -d "Do # shell command options complete -c pix -n "__fish_seen_subcommand_from shell" -a "(__pix_get_pipelines)" -d "Pipeline" -complete -c pix -n "__fish_seen_subcommand_from shell" -l "ssh" -d "Forward SSH agent to shell container" +complete -c pix -n "__fish_seen_subcommand_from shell" -l "ssh" -d "Forward SSH agent/keys to shell container (default, or id=path)" -rf complete -c pix -n "__fish_seen_subcommand_from shell" -l "arg" -d "Set one or more pipeline ARG (format KEY=value)" complete -c pix -n "__fish_seen_subcommand_from shell" -l "secret" -d "Forward one or more secrets to `buildx build`" complete -c pix -n "__fish_seen_subcommand_from shell" -l "target" -d "The shell target"