Skip to content

Editor Setup for Kubernetes

Kubernetes manifests are YAML. Helm templates are YAML with Go template directives injected. A well-configured editor validates YAML against schemas, completes Kubernetes resource fields, and highlights Go template syntax. This guide sets up Neovim to handle all three.

Install the language servers before configuring Neovim:

Terminal window
# yaml-language-server: LSP for YAML with schema validation
npm install -g yaml-language-server
# helm-ls: LSP for Helm chart templates
go install github.com/mrjosh/helm-ls@latest

Confirm both are on your $PATH:

Terminal window
which yaml-language-server
which helm-ls

yamlls handles plain YAML files — values.yaml, Chart.yaml, raw Kubernetes manifests. It validates against Kubernetes schemas and completes resource fields.

helm_ls handles Helm template files (anything inside templates/). It parses Go template syntax ({{ .Values.foo }}), renders templates internally, and delegates YAML validation to yamlls as an embedded language server.

Neovim must route each file to the right server. Plain .yaml files get the yaml filetype and attach yamlls. Files inside templates/ get the helm filetype and attach helm_ls. Filetype detection handles the routing.

Add both servers to your LSP server list:

-- lua/config/lsp_server_list.lua
M.servers = {
-- ... your other servers ...
'yamlls',
'helm_ls',
}

If you use Mason, both names are recognized by mason-lspconfig and install automatically.

schemastore.nvim provides a catalog of JSON/YAML schemas: every Kubernetes resource type, Helm’s Chart.yaml, GitHub Actions workflows, docker-compose files, and hundreds more. Without it, yamlls cannot validate manifests against the Kubernetes spec.

Add it as a dependency of your LSP plugin:

-- lua/plugins/lsp.lua
local main_lsp = {
"neovim/nvim-lspconfig",
dependencies = { "hrsh7th/cmp-nvim-lsp", "b0o/schemastore.nvim" },
config = function()
require("config/lsp").run_setup()
end,
}

yamlls ships with its own schemastore integration, but it is limited. Disable the built-in store and feed it schemas from schemastore.nvim instead. The filetypes field excludes helm so yamlls does not compete with helm_ls on template files.

-- lua/config/lsp.lua
vim.lsp.config('yamlls', {
filetypes = { 'yaml', 'yaml.docker-compose', 'yaml.gitlab' },
settings = {
yaml = {
schemaStore = {
enable = false,
url = '',
},
schemas = require('schemastore').yaml.schemas(),
},
},
})

With this in place, opening values.yaml or any Kubernetes manifest gives you field completion, type validation, and enum checking driven by the resource’s apiVersion and kind.

helm_ls needs the path to yaml-language-server so it can delegate YAML validation for the non-template portions of Helm files:

-- lua/config/lsp.lua
vim.lsp.config('helm_ls', {
settings = {
['helm-ls'] = {
yamlls = {
path = 'yaml-language-server',
},
},
},
})

helm_ls provides go-to-definition for .Values.x references (jumping to values.yaml), completion for built-in objects like .Release and .Chart, diagnostics from helm template rendering, and hover docs for template functions like include, toYaml, and default.

Install the yaml and helm tree-sitter parsers. The helm parser highlights Go template directives ({{ }}) inside YAML — something the standard YAML parser cannot do.

-- lua/config/treesitter_list.lua
M.servers = {
'markdown', 'sql', 'python', 'rust', 'typst', 'typescript', 'yaml', 'helm',
}

After adding them, restart Neovim. Parsers install on first load, or run :TSInstall yaml helm manually.

Neovim does not detect helm as a filetype out of the box. An autocommand checks whether a YAML file lives inside a Helm chart’s templates/ directory by walking up the path until it finds a directory named templates, then confirming the parent contains a Chart.yaml.

-- lua/autocommands.lua
local helm_group = vim.api.nvim_create_augroup("Helm", { clear = true })
vim.api.nvim_create_autocmd({ "BufNewFile", "BufRead" }, {
pattern = "*/templates/*.yaml,*/templates/*.yml,*/templates/*.tpl",
group = helm_group,
callback = function(args)
local path = args.file
local templates_dir = vim.fn.fnamemodify(path, ':h')
while vim.fn.fnamemodify(templates_dir, ':t') ~= 'templates' do
local parent = vim.fn.fnamemodify(templates_dir, ':h')
if parent == templates_dir then return end
templates_dir = parent
end
local chart_dir = vim.fn.fnamemodify(templates_dir, ':h')
if vim.fn.filereadable(chart_dir .. '/Chart.yaml') == 1 then
vim.bo[args.buf].filetype = 'helm'
end
end,
desc = "Detect Helm chart templates and set filetype to helm",
})

Plain YAML files outside Helm charts keep the yaml filetype, so yamlls handles them directly.

Open files from a Helm chart and confirm the expected filetype and LSP client attach:

FileExpected filetypeExpected LSPWhat to check
templates/deployment.yamlhelmhelm_lsGo template highlighting, .Values completion
values.yamlyamlyamllsField completion, schema validation
Chart.yamlyamlyamllsSchema validation for chart metadata

Check with:

" Check filetype
:set filetype?
" Check attached LSP clients
:checkhealth lsp

helm_ls does not attach to template files — Run :set filetype?. If it shows yaml instead of helm, the autocommand did not fire. Confirm the file path matches */templates/*.yaml and that a Chart.yaml exists in the parent of templates/.

yamlls attaches to Helm template files — The yamlls filetypes config must exclude helm. If yamlls reports errors on {{ }} syntax, it is parsing Go templates as YAML. Verify the filetypes list.

No schema validation on Kubernetes manifests — Check that schemastore.nvim is installed. Run :lua print(vim.inspect(require('schemastore').yaml.schemas())) to confirm it returns a schema table.

Tree-sitter highlighting missing for Go templates — Run :TSInstall helm and restart. The helm parser must be installed separately from yaml.