diff --git a/README.md b/README.md index 855595e..b160176 100644 --- a/README.md +++ b/README.md @@ -91,22 +91,26 @@ require("devcontainer").setup { -- This can be useful to mount local configuration -- And any other mounts when attaching to containers with this plugin attach_mounts = { + -- Can be set to true to always mount items defined below + -- And not only when directly attaching + -- This can be useful if executing attach command separately + always = false, neovim_config = { -- enables mounting local config to /root/.config/nvim in container - enabled = true, + enabled = false, -- makes mount readonly in container options = { "readonly" } }, neovim_data = { -- enables mounting local data to /root/.local/share/nvim in container - enabled = true, + enabled = false, -- no options by default options = {} }, -- Only useful if using neovim 0.8.0+ neovim_state = { -- enables mounting local state to /root/.local/state/nvim in container - enabled = true, + enabled = false, -- no options by default options = {} }, @@ -136,6 +140,8 @@ If not disabled by using `generate_commands = false` in setup, this plugin provi - `DevcontainerComposeDown` - run docker-compose down based on devcontainer.json - `DevcontainerComposeRm` - run docker-compose rm based on devcontainer.json - `DevcontainerStartAuto` - start whatever is defined in devcontainer.json +- `DevcontainerStartAutoAndAttach` - start and attach to whatever is defined in devcontainer.json +- `DevcontainerAttachAuto` - attach to whatever is defined in devcontainer.json - `DevcontainerStopAuto` - stop whatever was started based on devcontainer.json - `DevcontainerStopAll` - stop everything started with this plugin (in current session) - `DevcontainerRemoveAll` - remove everything started with this plugin (in current session) diff --git a/lua/devcontainer/commands.lua b/lua/devcontainer/commands.lua index 8fb2e8d..dde5940 100644 --- a/lua/devcontainer/commands.lua +++ b/lua/devcontainer/commands.lua @@ -54,7 +54,6 @@ end local function generate_common_run_command_args(data) local run_args = nil - -- TODO: Add support for remoteEnv? if data.forwardPorts then run_args = run_args or {} for _, v in ipairs(data.forwardPorts) do @@ -67,19 +66,20 @@ end local function generate_run_command_args(data, attaching) local run_args = generate_common_run_command_args(data) + -- TODO: Add support for containerEnv! if data.containerUser then run_args = run_args or {} table.insert(run_args, "--user") table.insert(run_args, data.containerUser) end if data.workspaceFolder or data.workspaceMount then - if data.workspaceMount == nil or data.workspaceFolder == nil then - vim.notify("workspaceFolder and workspaceMount have to both be defined to be used!", vim.log.levels.WARN) - else - run_args = run_args or {} - table.insert(run_args, "--mount") - table.insert(run_args, data.workspaceMount) - end + -- if data.workspaceMount == nil or data.workspaceFolder == nil then + -- vim.notify("workspaceFolder and workspaceMount have to both be defined to be used!", vim.log.levels.WARN) + -- else + run_args = run_args or {} + table.insert(run_args, "--mount") + table.insert(run_args, data.workspaceMount) + -- end end if data.mounts then run_args = run_args or {} @@ -95,7 +95,7 @@ local function generate_run_command_args(data, attaching) table.insert(run_args, v) end end - if attaching and plugin_config.attach_mounts then + if plugin_config.attach_mounts and (attaching or plugin_config.attach_mounts.always) then run_args = run_args or {} local am = plugin_config.attach_mounts @@ -152,6 +152,21 @@ local function generate_run_command_args(data, attaching) return run_args end +local function generate_exec_command_args(data) + local exec_args = nil + -- remoteEnv currently unsupported + if data.workspaceFolder or data.workspaceMount then + -- if data.workspaceMount == nil or data.workspaceFolder == nil then + -- vim.notify("workspaceFolder and workspaceMount have to both be defined to be used!", vim.log.levels.WARN) + -- else + exec_args = exec_args or {} + table.insert(exec_args, "--workdir") + table.insert(exec_args, data.workspaceFolder) + -- end + end + return exec_args +end + local function generate_compose_up_command_args(data) local run_args = nil if data.runServices then @@ -339,23 +354,55 @@ function M.docker_image_run(callback) end) end -local function spawn_docker_build_and_run(data, on_success, add_neovim) +local function attach_to_container(data, container_id, on_success) + docker.exec(container_id, { + tty = true, + command = "nvim", + args = generate_exec_command_args(data), + on_success = on_success, + on_fail = function() + vim.notify("Attaching to container (" .. container_id .. ") failed!", vim.log.levels.ERROR) + end, + }) +end + +local function attach_to_compose_service(data, on_success) + if not data.service then + vim.notify( + "service must be defined in " .. data.metadata.file_path .. " to attach to docker compose", + vim.log.levels.ERROR + ) + return + end + vim.notify("Found docker compose file definition. Attaching to service: " .. data.service) + docker_compose.get_container_id(data.dockerComposeFile, data.service, { + on_success = function(container_id) + attach_to_container(data, container_id, function() + on_success(data) + end) + end, + }) +end + +local function spawn_docker_build_and_run(data, on_success, add_neovim, attach) docker.build(data.build.dockerfile, data.build.context, { args = generate_build_command_args(data), add_neovim = add_neovim, on_success = function(image_id) docker.run(image_id, { args = generate_run_command_args(data, add_neovim), - tty = add_neovim, - -- TODO: Potentially add in the future for better compatibility - -- or (data.overrideCommand and { - -- "/bin/sh", - -- "-c", - -- "'while sleep 1000; do :; done'", - -- }) - command = (add_neovim and "nvim") or nil, + -- -- TODO: Potentially add in the future for better compatibility + -- -- or (data.overrideCommand and { + -- -- "/bin/sh", + -- -- "-c", + -- -- "'while sleep 1000; do :; done'", + -- -- }) on_success = function(container_id) - on_success(data, image_id, container_id) + if attach then + attach_to_container(data, container_id, function() + on_success(data, image_id, container_id) + end) + end end, on_fail = function() vim.notify("Running built image (" .. image_id .. ") failed!", vim.log.levels.ERROR) @@ -389,7 +436,7 @@ local function execute_docker_build_and_run(callback, add_neovim) ) return end - spawn_docker_build_and_run(data, on_success, add_neovim) + spawn_docker_build_and_run(data, on_success, add_neovim, add_neovim) end) end @@ -421,8 +468,9 @@ end ---Then it looks for dockerfile ---And last it looks for image ---@param callback function|nil called on success - devcontainer config is passed to the callback +---@param attach boolean|nil if true, automatically attach after starting ---@usage `require("devcontainer.commands").start_auto()` -function M.start_auto(callback) +function M.start_auto(callback, attach) vim.validate({ callback = { callback, { "function", "nil" } }, }) @@ -438,7 +486,11 @@ function M.start_auto(callback) docker_compose.up(data.dockerComposeFile, { args = generate_compose_up_command_args(data), on_success = function() - on_success(data) + if attach then + attach_to_compose_service(data, on_success) + else + on_success(data) + end end, on_fail = function() vim.notify("Docker compose up failed!", vim.log.levels.ERROR) @@ -449,14 +501,14 @@ function M.start_auto(callback) if data.build.dockerfile then vim.notify("Found dockerfile definition. Running docker build and run...") - spawn_docker_build_and_run(data, on_success, false) + spawn_docker_build_and_run(data, on_success, attach, attach) return end if data.image then vim.notify("Found image definition. Running docker run...") docker.run(data.image, { - args = generate_run_command_args(data, false), + args = generate_run_command_args(data, attach), on_success = function(_) on_success(data) end, @@ -469,6 +521,48 @@ function M.start_auto(callback) end) end +---Parses devcontainer.json and attaches to whatever is defined there +---Looks for dockerComposeFile first +---Then it looks for dockerfile +---And last it looks for image +---@param callback function|nil called on success - devcontainer config is passed to the callback +---@usage `require("devcontainer.commands").attach_auto()` +function M.attach_auto(callback) + vim.validate({ + callback = { callback, { "function", "nil" } }, + }) + + local on_success = callback + or function(config) + vim.notify("Successfully attached to container from " .. config.metadata.file_path) + end + + get_nearest_devcontainer_config(function(data) + if data.dockerComposeFile then + attach_to_compose_service(data, on_success) + return + end + + if data.build.dockerfile then + vim.notify("Found dockerfile definition. Attaching to the container...") + local container = status.find_container({ source_dockerfile = data.build.dockerfile }) + attach_to_container(data, container.container_id, function() + on_success(data) + end) + return + end + + if data.image then + vim.notify("Found image definition. Attaaching to the container...") + local container = status.find_container({ source_dockerfile = data.build.dockerfile }) + attach_to_container(data, container.container_id, function() + on_success(data) + end) + return + end + end) +end + ---Parses devcontainer.json and stops whatever is defined there ---Looks for dockerComposeFile first ---Then it looks for dockerfile diff --git a/lua/devcontainer/config.lua b/lua/devcontainer/config.lua index 54d9338..9baf454 100644 --- a/lua/devcontainer/config.lua +++ b/lua/devcontainer/config.lua @@ -149,26 +149,30 @@ M.devcontainer_json_template = default_devcontainer_json_template ---@field options List[string]|nil additional bind options, useful to define { "readonly" } ---@class AttachMountsOpts +---@field always boolean|nil if true these mounts are used on every run, to be available when attaching later ---@field neovim_config MountOpts|nil if true attaches neovim local config to /root/.config/nvim in container ---@field neovim_data MountOpts|nil if true attaches neovim data to /root/.local/share/nvim in container ---@field neovim_state MountOpts|nil if true attaches neovim state to /root/.local/state/nvim in container ---@field custom_mounts List[string] list of custom mounts to add when attaching ---Configuration for mounts when using attach command +---NOTE: when attaching in a separate command, it is useful to set +---always to true, since these have to be attached when starting ---Useful to mount neovim configuration into container ---Applicable only to `devcontainer.commands` functions! ---@type AttachMountsOpts M.attach_mounts = { + always = false, neovim_config = { - enabled = true, + enabled = false, options = { "readonly" }, }, neovim_data = { - enabled = true, + enabled = false, options = {}, }, neovim_state = { - enabled = true, + enabled = false, options = {}, }, custom_mounts = {}, diff --git a/lua/devcontainer/docker-compose.lua b/lua/devcontainer/docker-compose.lua index 8bec567..687c0aa 100644 --- a/lua/devcontainer/docker-compose.lua +++ b/lua/devcontainer/docker-compose.lua @@ -117,33 +117,45 @@ function M.down(compose_file, opts) end) end ----@class DockerComposeRmOpts ----@field on_success function() success callback +---@class DockerComposeGetContainerIdOpts +---@field on_success function(container_id) success callback ---@field on_fail function() failure callback ----Run docker-compose rm with passed file +---Run docker-compose ps with passed file and service to get its container_id ---@param compose_file string|table path to docker-compose.yml file or files ----@param opts DockerComposeRmOpts Additional options including callbacks ----@usage `require("devcontainer.docker-compose").rm("docker-compose.yml")` -function M.rm(compose_file, opts) +---@param service string service name +---@param opts DockerComposeGetContainerIdOpts Additional options including callbacks +---@usage `docker_compose.get_container_id("docker-compose.yml", { on_success = function(container_id) end })` +function M.get_container_id(compose_file, service, opts) vim.validate({ compose_file = { compose_file, { "string", "table" } }, + service = { service, "string" }, }) opts = opts or {} v.validate_callbacks(opts) local on_success = opts.on_success - or function() - vim.notify("Successfully removed containers from " .. compose_file) + or function(container_id) + vim.notify("Container id of service " .. service .. " from " .. compose_file .. " is " .. container_id) end local on_fail = opts.on_fail or function() - vim.notify("Removing containers from " .. compose_file .. " failed!", vim.log.levels.ERROR) + vim.notify( + "Fetching container id for " .. service .. " from " .. compose_file .. " failed!", + vim.log.levels.ERROR + ) end local command = get_compose_files_command(compose_file) - vim.list_extend(command, { "rm", "-fsv" }) - run_docker_compose(command, nil, function(code, _) + vim.list_extend(command, { "ps", "-q", service }) + local container_id = nil + run_docker_compose(command, { + stdout = function(_, data) + if data then + container_id = vim.split(data, "\n")[1] + end + end, + }, function(code, _) if code == 0 then - on_success() + on_success(container_id) else on_fail() end diff --git a/lua/devcontainer/docker.lua b/lua/devcontainer/docker.lua index e2a845b..210048f 100644 --- a/lua/devcontainer/docker.lua +++ b/lua/devcontainer/docker.lua @@ -238,10 +238,8 @@ end ---@class DockerRunOpts ---@field autoremove boolean automatically remove container after stopping - true by default ----@field tty boolean attach to container TTY and display it in terminal buffer, using configured terminal handler ---@field command string|table|nil command to run in container ---@field args table|nil list of additional arguments to run command ----@field terminal_handler function(command) override to open terminal in a different way, :tabnew + termopen by default ---@field on_success function(container_id) success callback taking the id of the started container - not invoked if tty ---@field on_fail function() failure callback @@ -259,8 +257,6 @@ function M.run(image, opts) v.validate_opts_with_callbacks(opts, { command = { "string", "table" }, autoremove = "boolean", - tty = "boolean", - terminal_handler = "function", args = function(x) return vim.tbl_islist(x) end, @@ -274,12 +270,7 @@ function M.run(image, opts) vim.notify("Starting image " .. image .. " failed!", vim.log.levels.ERROR) end - local command = { "run", "-i" } - if opts.tty then - table.insert(command, "-t") - else - table.insert(command, "-d") - end + local command = { "run", "-i", "-d" } if opts.autoremove ~= false then table.insert(command, "--rm") end @@ -295,24 +286,90 @@ function M.run(image, opts) end end + local container_id = nil + run_docker(command, { + stdout = function(_, data) + if data then + container_id = vim.split(data, "\n")[1] + end + end, + }, function(code, _) + if code == 0 then + status.add_container({ + image_id = image, + container_id = container_id, + autoremove = opts.autoremove, + }) + on_success(container_id) + else + on_fail() + end + end) +end + +---@class DockerExecOpts +---@field tty boolean attach to container TTY and display it in terminal buffer, using configured terminal handler +---@field terminal_handler function(command) override to open terminal in a different way, :tabnew + termopen by default +---@field command string|table|nil command to run in container +---@field args table|nil list of additional arguments to exec command +---@field on_success function() success callback - not called if tty +---@field on_fail function() failure callback - not called if tty + +---Run command on a container using docker exec +---Useful for attaching to neovim +---NOTE: If terminal_handler is passed, then it needs to start the process too - default termopen does just that +---@param container_id string Docker container to exec on +---@param opts DockerRunOpts Additional options including callbacks +---@usage `docker.exec("some_id", { command = "nvim", on_success = function() end, on_fail = function() end })` +function M.exec(container_id, opts) + vim.validate({ + container_id = { container_id, "string" }, + opts = { opts, { "table", "nil" } }, + }) + opts = opts or {} + v.validate_opts_with_callbacks(opts, { + command = { "string", "table" }, + tty = "boolean", + terminal_handler = "function", + args = function(x) + return vim.tbl_islist(x) + end, + }) + + local on_success = opts.on_success + or function() + vim.notify("Successfully executed command " .. opts.command .. "on container " .. container_id) + end + local on_fail = opts.on_fail + or function() + vim.notify( + "Executing command " .. opts.command .. " on container " .. container_id .. " failed!", + vim.log.levels.ERROR + ) + end + + local command = { "exec", "-i" } + if opts.tty then + table.insert(command, "-t") + end + + vim.list_extend(command, opts.args or {}) + + table.insert(command, container_id) + if opts.command then + if type(opts.command) == "string" then + table.insert(command, opts.command) + elseif type(opts.command) == "table" then + vim.list_extend(command, opts.command) + end + end + if opts.tty then (opts.terminal_handler or config.terminal_handler)(vim.list_extend({ "docker" }, command)) else - local container_id = nil - run_docker(command, { - stdout = function(_, data) - if data then - container_id = vim.split(data, "\n")[1] - end - end, - }, function(code, _) + run_docker(command, nil, function(code, _) if code == 0 then - status.add_container({ - image_id = image, - container_id = container_id, - autoremove = opts.autoremove, - }) - on_success(container_id) + on_success() else on_fail() end diff --git a/lua/devcontainer/init.lua b/lua/devcontainer/init.lua index 0d6d78d..b6594d5 100644 --- a/lua/devcontainer/init.lua +++ b/lua/devcontainer/init.lua @@ -67,6 +67,7 @@ function M.setup(opts) if opts.attach_mounts then local am = opts.attach_mounts v.validate_deep(am, "opts.attach_mounts", { + always = "boolean", neovim_config = "table", neovim_data = "table", neovim_state = "table", @@ -166,6 +167,18 @@ function M.setup(opts) nargs = 0, desc = "Start either compose, dockerfile or image from .devcontainer.json", }) + vim.api.nvim_create_user_command("DevcontainerStartAutoAndAttach", function(_) + commands.start_auto(nil, true) + end, { + nargs = 0, + desc = "Start and attach to either compose, dockerfile or image from .devcontainer.json", + }) + vim.api.nvim_create_user_command("DevcontainerAttachAuto", function(_) + commands.attach_auto() + end, { + nargs = 0, + desc = "Attach to either compose, dockerfile or image from .devcontainer.json", + }) vim.api.nvim_create_user_command("DevcontainerStopAuto", function(_) commands.stop_auto() end, { diff --git a/scripts/docs-template.txt b/scripts/docs-template.txt index a132fb9..70bad15 100644 --- a/scripts/docs-template.txt +++ b/scripts/docs-template.txt @@ -107,22 +107,26 @@ It is possible to override some of the functionality of the plugin with options -- This can be useful to mount local configuration -- And any other mounts when attaching to containers with this plugin attach_mounts = { + -- Can be set to true to always mount items defined below + -- And not only when directly attaching + -- This can be useful if executing attach command separately + always = false, neovim_config = { -- enables mounting local config to /root/.config/nvim in container - enabled = true, + enabled = false, -- makes mount readonly in container options = { "readonly" } }, neovim_data = { -- enables mounting local data to /root/.local/share/nvim in container - enabled = true, + enabled = false, -- no options by default options = {} }, -- Only useful if using neovim 0.8.0+ neovim_state = { -- enables mounting local state to /root/.local/state/nvim in container - enabled = true, + enabled = false, -- no options by default options = {} }, @@ -152,6 +156,8 @@ If not disabled by using {generate_commands = false} in setup, this plugin provi *:DevcontainerComposeDown* - run docker-compose down based on devcontainer.json *:DevcontainerComposeRm* - run docker-compose rm based on devcontainer.json *:DevcontainerStartAuto* - start whatever is defined in devcontainer.json +*:DevcontainerStartAutoAndAttach* - start and attach to whatever is defined in devcontainer.json +*:DevcontainerAttachAuto* - attach to whatever is defined in devcontainer.json *:DevcontainerStopAuto* - stop whatever was started based on devcontainer.json *:DevcontainerStopAll* - stop everything started with this plugin (in current session) *:DevcontainerRemoveAll* - remove everything started with this plugin (in current session)