terraform-ls
terraform-ls copied to clipboard
Investigate optimisation opportunities for workspaces w/ symlinks
Originally posted by @briceburg in https://github.com/hashicorp/terraform-ls/issues/1375#issuecomment-1714496313
Ever since switching to a monolithic structure (where all terraform configurations under a single repo) the vs code extension has become unresponsive/unusable for me. even when using v0.31.5 of the language server.
Repo structure looks like;
.
├── CODEOWNERS
├── README.md
├── acme
│ ├── bar
│ │ ├── README.md
│ │ ├── __global.tf
│ │ ├── docker-compose.yml
│ │ ├── environments
│ │ │ ├── _common.tf
│ │ │ ├── _variables-legacy.tf
│ │ │ ├── dev
│ │ │ │ ├── __global.tf -> ../../__global.tf
│ │ │ │ ├── _common.tf -> ../_common.tf
│ │ │ │ ├── _variables-legacy.tf -> ../_variables-legacy.tf
│ │ │ │ ├── main.tf
│ │ │ │ ├── service_account.yaml -> ../../service_account.yaml
│ │ │ │ ├── vgs.auto.tfvars -> ../../secrets/acme-secrets/vgs.auto.tfvars
│ │ │ │ └── vgs.dev.sh -> ../../secrets/acme-secrets/vgs.dev.sh
│ │ │ ├── prod
│ │ │ │ ├── __global.tf -> ../../__global.tf
│ │ │ │ ├── _common.tf -> ../_common.tf
│ │ │ │ ├── _variables-legacy.tf -> ../_variables-legacy.tf
│ │ │ │ ├── main.tf
│ │ │ │ ├── migration-20230731.tf
│ │ │ │ ├── service_account.yaml -> ../../service_account.yaml
│ │ │ │ ├── vgs.auto.tfvars -> ../../secrets/acme-secrets/vgs.auto.tfvars
│ │ │ │ └── vgs.prod.sh -> ../../secrets/acme-secrets/vgs.prod.sh
│ │ │ ├── sandbox
│ │ │ │ ├── __global.tf -> ../../__global.tf
│ │ │ │ ├── _common.tf -> ../_common.tf
│ │ │ │ ├── _variables-legacy.tf -> ../_variables-legacy.tf
│ │ │ │ ├── main.tf
│ │ │ │ ├── migration-20230730.tf
│ │ │ │ ├── service_account.yaml -> ../../service_account.yaml
│ │ │ │ ├── vgs.auto.tfvars -> ../../secrets/acme-secrets/vgs.auto.tfvars
│ │ │ │ └── vgs.sandbox.sh -> ../../secrets/acme-secrets/vgs.sandbox.sh
│ │ │ └── stg
│ │ │ ├── __global.tf -> ../../__global.tf
│ │ │ ├── _common.tf -> ../_common.tf
│ │ │ ├── _variables-legacy.tf -> ../_variables-legacy.tf
│ │ │ ├── main.tf
│ │ │ ├── service_account.yaml -> ../../service_account.yaml
│ │ │ ├── vgs.auto.tfvars -> ../../secrets/acme-secrets/vgs.auto.tfvars
│ │ │ └── vgs.stg.sh -> ../../secrets/acme-secrets/vgs.stg.sh
│ │ ├── modules
│ │ │ └── main
│ │ │ ├── locals.tf
│ │ │ ├── main.tf
│ │ │ ├── outputs.tf
│ │ │ └── variables.tf
│ │ ├── secrets
│ │ └── service_account.yaml
│ └── foo
│ ├── README.md
│ ├── __global.tf
│ ├── environments
│ │ ├── _common.tf
│ │ ├── _variables-legacy.tf
│ │ ├── review
│ │ │ ├── __global.tf -> ../../__global.tf
│ │ │ ├── _common.tf -> ../_common.tf
│ │ │ ├── _variables-legacy.tf -> ../_variables-legacy.tf
│ │ │ ├── cloudflare.auto.tfvars -> ../../secrets/acme-secrets/cloudflare.auto.tfvars
│ │ │ ├── datadog_secrets.auto.tfvars -> ../../secrets/acme-secrets/datadog_secrets.auto.tfvars
│ │ │ ├── main.tf
│ │ │ └── service_secrets.auto.tfvars -> ../../secrets/acme-secrets/service_secrets.auto.tfvars
│ │ └── sandbox
│ │ ├── __global.tf -> ../../__global.tf
│ │ ├── _common.tf -> ../_common.tf
│ │ ├── _variables-legacy.tf -> ../_variables-legacy.tf
│ │ ├── cloudflare.auto.tfvars -> ../../secrets/acme-secrets/cloudflare.auto.tfvars
│ │ ├── datadog_secrets.auto.tfvars -> ../../secrets/acme-secrets/datadog_secrets.auto.tfvars
│ │ ├── main.tf
│ │ ├── migration-20230721.tf
│ │ └── service_secrets.auto.tfvars -> ../../secrets/acme-secrets/service_secrets.auto.tfvars
│ ├── modules
│ │ └── main
│ │ ├── data.tf
│ │ ├── locals.tf
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ ├── services-mailcatch.tf
│ │ ├── services.tf
│ │ └── variables.tf
│ ├── namespaces
│ │ ├── _common.tf
│ │ ├── review
│ │ │ ├── __global.tf -> ../../__global.tf
│ │ │ ├── _common.tf -> ../_common.tf
│ │ │ ├── main.tf
│ │ │ └── security.tf
│ │ └── sandbox
│ │ ├── __global.tf -> ../../__global.tf
│ │ ├── _common.tf -> ../_common.tf
│ │ ├── main.tf
│ │ └── security.tf
│ └── secrets
├── platform
│ ├── backstage
│ │ └── TBD
│ └── sample-app
│ ├── README.md
│ ├── __global.tf
│ ├── environments
│ │ ├── _common.tf
│ │ ├── dev
│ │ │ ├── __global.tf -> ../../__global.tf
│ │ │ ├── _common.tf -> ../_common.tf
│ │ │ └── main.tf
│ │ └── review
│ │ ├── __global.tf -> ../../__global.tf
│ │ ├── _common.tf -> ../_common.tf
│ │ └── main.tf
│ ├── modules
│ │ └── main
│ │ ├── data.tf
│ │ ├── locals.tf
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ ├── providers.tf
│ │ └── variables.tf
│ └── namespaces
│ ├── _common.tf
│ ├── dev
│ │ ├── __global.tf -> ../../__global.tf
│ │ ├── _common.tf -> ../_common.tf
│ │ └── main.tf
│ └── review
│ ├── __global.tf -> ../../__global.tf
│ ├── _common.tf -> ../_common.tf
│ └── main.tf
└── sso
├── README.md
├── namespaces
│ ├── _common.tf
│ ├── dev
│ │ ├── _common.tf -> ../_common.tf
│ │ ├── main.tf
│ │ └── security.tf
│ ├── prod
│ │ ├── _common.tf -> ../_common.tf
│ │ ├── main.tf
│ │ └── security.tf
│ ├── sandbox
│ │ ├── _common.tf -> ../_common.tf
│ │ ├── main.tf
│ │ └── security.tf
│ └── stg
│ ├── _common.tf -> ../_common.tf
│ ├── main.tf
│ └── security.tf
└── token-handler
├── README.md
├── __global.tf
├── environments
│ ├── _common.tf
│ ├── _variables-legacy.tf
│ ├── dev
│ │ ├── __global.tf -> ../../__global.tf
│ │ ├── _common.tf -> ../_common.tf
│ │ ├── _variables-legacy.tf -> ../_variables-legacy.tf
│ │ ├── cloudflare.auto.tfvars -> ../../secrets/acme-secrets/cloudflare.auto.tfvars
│ │ ├── datadog.auto.tfvars -> ../../secrets/acme-secrets/datadog.auto.tfvars
│ │ └── main.tf
│ ├── prod
│ │ ├── __global.tf -> ../../__global.tf
│ │ ├── _common.tf -> ../_common.tf
│ │ ├── _variables-legacy.tf -> ../_variables-legacy.tf
│ │ ├── cloudflare.auto.tfvars -> ../../secrets/acme-secrets/cloudflare.auto.tfvars
│ │ ├── datadog.auto.tfvars -> ../../secrets/acme-secrets/datadog.auto.tfvars
│ │ └── main.tf
│ ├── sandbox
│ │ ├── __global.tf -> ../../__global.tf
│ │ ├── _common.tf -> ../_common.tf
│ │ ├── _variables-legacy.tf -> ../_variables-legacy.tf
│ │ ├── cloudflare.auto.tfvars -> ../../secrets/acme-secrets/cloudflare.auto.tfvars
│ │ ├── datadog.auto.tfvars -> ../../secrets/acme-secrets/datadog.auto.tfvars
│ │ └── main.tf
│ └── stg
│ ├── __global.tf -> ../../__global.tf
│ ├── _common.tf -> ../_common.tf
│ ├── _variables-legacy.tf -> ../_variables-legacy.tf
│ ├── cloudflare.auto.tfvars -> ../../secrets/acme-secrets/cloudflare.auto.tfvars
│ ├── datadog.auto.tfvars -> ../../secrets/acme-secrets/datadog.auto.tfvars
│ └── main.tf
├── modules
│ ├── api-gateway
│ │ ├── data.tf
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ └── main
│ ├── data.tf
│ ├── discovery.tf
│ ├── locals.tf
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
└── secrets
@briceburg Thank you for describing the tree with symlinks. We can try to reproduce this but based on some other issues I have filed before I can imagine symlinks generally causing issues, performance-related or not.
I have transferred your comment here as the issue you commented on primarily focuses on different problem, which is processing that happens after file is opened.
The optimisation there could help in your case but it doesn't target symlinks in any way, so it would be incidental if you see improvements, rather than intentional.
Related:
- https://github.com/hashicorp/terraform-ls/issues/1175
- https://github.com/hashicorp/terraform-ls/issues/241
We have done some investigating in https://github.com/hashicorp/terraform-ls/issues/1409 and have some ideas on how we can improve the performance when links are involved. It is however non-trivial, so while we still want to address this problem, we will have to weigh the complexity/effort against other work.
For these reasons (to aid prioritisation): @briceburg would you mind describing your motivations in more detail? As far as I understand, you're dealing with repetition between different customers, apps and different environments. What I'm specifically curious about is what problems are symlinks solving for you, which modules cannot solve, or solve in a suboptimal way?
On a separate note, I'm vaguely aware that symlinks are generally Unix concept and they may not translate well to Windows. How do you deal with this problem? Do you just assume/ensure that all users, CI systems etc. always run on Unix?
Marking this issue as stale due to inactivity over the last 30 days. This helps our maintainers find and focus on the active issues. If this issue receives no comments in the next 30 days it will automatically be closed. Maintainers can also remove the stale label.
Thank you for understanding.
@radeksimko I am terribly sorry for my delayed response.
would you mind describing your motivations in more detail?
primarily our reliance on symlinks is to maintain DRYness when defining providers, common variables/locals, and other core bits of the root configuration. while we previously used workspaces to handle this, in practice it became cumbersome and less flexible than having separate root configurations per environment per google's advice. we do use modules in this root configuration as would be expected, e.g.;
# foo-project/environments/production/main.tf
module "main" {
source = "../../modules/main"
...
}
... but chose to use symlinks as a happy medium to stay DRY when switching from workspaces to using environment directories.
I'm vaguely aware that symlinks are generally Unix concept and they may not translate well to Windows. How do you deal with this problem?
thankfully git properly handles symlinks and I believe Windows 10+ support is better (os x is fine) -- although you are correct in that we ensure our CI systems are containerized and or under a linux runner.
Maybe I am wrong, but why not just let the language server open and parse symlinks when they match the *\.tf
regex?
It looks like the program is intentionally checking whether an inode object IS NOT a symlink before opening it.
Terraform itself works well with symlynks, which are a useful way to centralize stuff without copying it over and over.
Workspaces are an obvious footgun. Symlinks are a more robust solution. It would be appreciated if terraform-ls
parsed them correctly to eliminate false-positive not defined
errors.
Indeed. Also because terraform-ls
should conform to terraform
itself.
This deviating behavior by the language server is simply breaking the rules set by the same developer/company.
I'm in a similar situation, where I use a directory per state, but this causes some issues with the LSP.
In env/dev-eu-north-1
, for example:
_main.tf -> ../../../shared/bases/main.tf
_providers.tf -> ../../../shared/bases/providers.tf
_variables.tf -> ../../../shared/bases/variables.tf
_versions.tf -> ../../../shared/bases/versions.tf
main.tf -> ../main.tf
outputs.tf -> ../outputs.tf
terraform.tfvars
variables.tf -> ../variables.tf
versions.tf
We get several shared base configurations that are linked into many states. Then, for a given domain's environment, we link to the directory just above, so that all states have the base configurations and the domain configurations. In that way, the only actual non-link files in the environment directory are the backend configuration in versions.tf and the variables in terraform.tfvars.
This works extremely well, except it make the LSP complain a bit and jumping to definitions doesn't work.
I'm not sure what the actual solution could be though, because in the files actual location, the source paths are just not correct, and I don't see a way for the LSP to intuit where it should be looking. It doesn't know where the file could be potentially linked.
However, it would be done with a comment directive like:
module "core" {
# terraform-ls: source = "./module/domain-core"
source = "../modules/domain-core"
# ...
}