Skip to content

Commit

Permalink
feat: add support for separate attach command (#63)
Browse files Browse the repository at this point in the history
This closes #60
This closes #59

BREAKING CHANGE: neovim configuration is no longer automatically mounted
It has to be enabled in setup
It is also only mounted if attaching directly
To better support separate attach command, set attach_mounts.always flag
to true in setup, to always mount configured neovim points
  • Loading branch information
esensar authored May 20, 2022
1 parent fda41b4 commit de5ca3b
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 69 deletions.
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
},
Expand Down Expand Up @@ -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)
Expand Down
142 changes: 118 additions & 24 deletions lua/devcontainer/commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {}
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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" } },
})
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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
Expand Down
10 changes: 7 additions & 3 deletions lua/devcontainer/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {},
Expand Down
36 changes: 24 additions & 12 deletions lua/devcontainer/docker-compose.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit de5ca3b

Please sign in to comment.