Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support single files and parent (non-Terraform) folders #32

Closed
radeksimko opened this issue Mar 22, 2020 · 14 comments · Fixed by #843
Closed

Support single files and parent (non-Terraform) folders #32

radeksimko opened this issue Mar 22, 2020 · 14 comments · Fixed by #843
Assignees
Labels
enhancement New feature or request modules Functionality related to the module block and modules generally
Milestone

Comments

@radeksimko
Copy link
Member

radeksimko commented Mar 22, 2020

Single files

A restriction is currently in place that prevents the Language Server from communicating with an IDE (client) that opened a single file. As a result it enforces the user to open whole folders.

This is done by failing initialization with the following error:

Editing a single file is not yet supported. Please open a directory.

Why

The restriction prevents the LS from having to deal with the potentially increased complexity in any logic that concerns the relationship between a file and its plugins (providers).

The LS has responsibility for:

  • finding compatible Terraform binary (happens during initialize via $PATH)
  • finding and storing Terraform version (happens during initialize)
  • retrieving and caching schema for inited providers (happens during initialize)
  • invalidating the cache and retrieving it again when providers change (happens any time via watching the lock file in a plugin folder)

The initialize method is called only once for every opened folder, which is mainly where the lower complexity comes from.

Parent (non-Terraform) folders

This also affects users who wish to open a folder higher up the filesystem hierarchy, e.g. a folder with all Terraform workspaces, as opposed to opening workspaces individually. We do not support such case today either, but don't actually raise any error in this case (yet?).

The same reasoning and complexity scope applies here too though and it's likely that both use cases have the same (or very similar) solution.

Local Modules

Users with locally stored modules (where module's source is path in a local filesystem) may fall into this category as such modules tend to be not inited within their own folder (i.e. such modules often don't have their own .terraform folder). Supporting them will rely heavily on how their provider inheritance is set up - i.e. we need to understand where can we get the provider schemas from for every module.

Future

If we support single files, we need to consider the above happening at different times (probably during didOpen) and significantly more often. Hence we need to ensure this scales well in terms of resource consumption - e.g. by ensuring that we never watch/refresh the same plugin directory twice within the context of a running server process. We may also need to decouple the Terraform binary discovery and version handling, to ensure we only do it once per workspace.

Related

This limitation also helps us avoid bugs similar to juliosueiras/terraform-lsp#58

Proposal

  • walker: introduce more limited walking mode, such that it doesn't descend into lower directories (to prevent unexpected outcomes if user opens ~ home dir)
  • initialize handler: avoid indexing if rootUri is empty (single file was open) and store a flag to say "LS is in single file mode"
  • didOpen handler: index on-demand if "LS is in single file mode" - basically just call
    modPath, err := uri.PathFromURI(added.URI)
    if err != nil {
    jrpc2.ServerFromContext(ctx).Notify(ctx, "window/showMessage", &lsp.ShowMessageParams{
    Type: lsp.Warning,
    Message: fmt.Sprintf("Ignoring new workspace folder %s: %s."+
    " This is most likely bug, please report it.", added.URI, err),
    })
    continue
    }
    err = watcher.AddModule(modPath)
    if err != nil {
    svc.logger.Printf("failed to add module to watcher: %s", err)
    continue
    }
    walker.EnqueuePath(modPath)
@amasover
Copy link
Contributor

Users with locally stored modules (where module's source is path in a local filesystem) may fall into this category as such modules tend to be not inited within their own folder

Potentially of note: terragrunt users usually have the .terraform folder stored outside the module folder, and don't directly init on modules. But I can't think of a clean way to not require a direct init on the module in that case.

I have probably a minority use case, but I actually use pre init hooks in terragrunt to set up my backend configs. The way I have things set up, terraform init fails when run directly against a module unless I set the backend to "local" temporarily. I wonder if #56 would help me. Or perhaps I should consider changing my modules in a way that init-ing against them directly runs successfully (without breaking my pre init hooks).

@radeksimko
Copy link
Member Author

Thanks for sharing that use case @amasover - that's a useful insight!

I am not that familiar with Terragrunt and this kind of workflow where .terraform folder is outside of the directory. Would the 1-to-1 mapping between .terraform and a directory still apply in this case, or would Terragrunt share a single .terraform with multiple directories?

If it's 1-to-N, would this functionally be the same as setting a custom plugin cache?

In theory we could traverse up a few levels to lookup parent .terraform, but in practice I worry that such design would be flawed in that we cannot be sure whether the directory genuinely belongs to that .terraform, i.e. there's a lot of room for error, so I'd rather avoid doing that.

That said we could initially allow setting a static plugin cache path per #24 and potentially make it project-specific settings via some kind of standardised configs?

FWIW the only reason the LS needs access to .terraform is for schema for now, but I can imagine us also needing access to downloaded modules eventually, so we can complete references to outputs from modules for example in module.servers.<HERE>, but that's one for #93

@amasover
Copy link
Contributor

amasover commented May 14, 2020

The workflow is something more like this:

terraform-live repository:

staging
├── terragrunt.hcl
├── frontend-app
│   ├── .terragrunt-cache/..../module-for-frontend/.terraform
│   ├── main.tf
│   └── terragrunt.hcl
└── mysql
    ├── .terragrunt-cache/..../module-for-mysql/.terraform
    ├── main.tf
    └── terragrunt.hcl
production
├── terragrunt.hcl
├── frontend-app
│   ├── .terragrunt-cache/..../module-for-frontend/.terraform
│   ├── main.tf
│   └── terragrunt.hcl
└── mysql
    ├── .terragrunt-cache/..../module-for-mysql/.terraform
    ├── main.tf
    └── terragrunt.hcl

terraform-modules repository:

module-for-frontend
└── main.tf
module-for-mysql
└── main.tf

(No .terraform dirs in the terraform-modules repository)

The terragrunt.hcl files contain references to modules like this:

terraform {
  source = "git::git@mygithost/terraform-modules//module-for-frontend"
}

Terragrunt then pulls down the module and runs the terraform command that you want against it.

The best example of how this works is Terragrunt's example repos:
https://github.com/gruntwork-io/terragrunt-infrastructure-live-example
https://github.com/gruntwork-io/terragrunt-infrastructure-modules-example

In local development usually you reference a local source. So a typical loop would be, make a change in a module, and try it out from a terraform-live environment via terragrunt apply --terragrunt-source /home/$USER/code/terraform-modules//module-for-frontend.

I think the problem here is that it's more like an N-to-1 mapping where multiple copies of modules can exist, for different environments in terraform-live. For example, you might have a staging environment and a production environment, both referencing module-for-frontend, and each environment-specific copy of the module-for-frontend has its own .terraform dir. That's kind of why I'm saying I don't see a clean way for terraform-ls to solve this problem.

Ultimately though, when you edit a module, you're doing it in the terraform-modules directory, not in the terraform-live directory. So your editor should see the project root being somewhere in terraform-modules, and look there for the .terraform dir. So I'd argue it probably isn't too much of a burden to ask that people run terraform init directly against modules. My case is a little special due to my custom pre-init hooks, but (and I could be wrong), I think usually people who use terragrunt still retain the ability to directly init modules.

I think static plugin cache with standardized configs would also be great in this situation -- then you could check the configs into source control, and it should work for everyone who needs to edit modules. My understanding is, if the LS supports that, then we wouldn't need to directly init in the terraform-modules directory, because the LS can get schema info directly from the plugin cache? I tried to set up juliosueiras's terraform-lsp in this way, but I was only ever able to get language server features to work after running terraform-init directly from modules.

@paultyng
Copy link
Contributor

paultyng commented Jun 5, 2020

Different scenarios (will start to populate these with examples):

Repo / Directory Scenarios

Single root module / repo

No subdirectory modules, all modules referenced from external sources (registry, etc). Repo root is Terraform root.

Nested submodule

Repo root is terraform root, subdirectories are modules sourced from the root.

Example:

Our internal terraform-repositories repo for GitHub management. I'm trying to find some good public examples.

Nested root module

Repo root is module, sub directories can contain multiple root Terraform modules.

Example:

Open at repo root: https://github.com/terraform-aws-modules/terraform-aws-security-group
The root of the repo is actually a module, not a terraform root module.
The terraform init and schema cache would be invoked from a nested example:
https://github.com/terraform-aws-modules/terraform-aws-security-group/blob/master/examples/computed/main.tf#L20-L35

Multiple Root Terraform

Repo root is not a Terraform module, subfolders are root Terraform modules.

This is probably easier solved on the editor side, but opening each subfolder as an additional directory in a VS Code "workspace" (or Sublime "project"), etc. and running multiple language servers for each one.

Submodule only / No Terraform root

If someone is working on a submodule in a repository without any test root Terraform modules, there may be no way to acquire schema information / initialize. Mostly listing this for completeness, we could potentially try to guess at the providers/schema. Its possible you could walk up the tree and see if there was an initialized TF directory somewhere above the repo?

Single File Scenarios

I think for the most part we should skip all but the single file in a repo root scenario in the near term.

Single file in Terraform root

This is relatively straightforward, probably follows the same path as repo root being Terraform root.

Single file not in Terraform root

Similar to the Submodule only scenario above, you could potentially walk up the tree, but otherwise not sure what else you could do.

@magne
Copy link

magne commented Jun 11, 2020

I'm using the Main Module Approach with CI, QA, and Prod environments. The CI environments includes the main module as a local module (`source = "../../main"), while the QA and Prod environments use git module source. This allows me to pin QA and Prod to a specific commit or tag.

It would be helpful to be able to select the directory containing the terraform root.

@paultyng
Copy link
Contributor

Just an update for anyone following this issue, this is our primary focus at the moment, #167 introduced some refactoring to make this simpler. The intended approach here has two primary things we are looking to add

@danieladams456
Copy link

Just throwing my setup out there since @radeksimko mentioned this was the correct issue...

Under @paultyng's categories, I'm using "Multiple Root Terraform." If possible [eventually], it would be nice to keep opening the root folder in VScode since then I can use workspace history to switch between them. Sub-projects might have very similar names like api-gateway which could get confusing.

Readme.md
-variables
  --custom-variables-derivation-files-here.sh
-subproject-one
  --.terraform.d
  --manage.sh
  --main.tf
  --other.tf
-subproject-two
  --.terraform.d
  --manage.sh
  --main.tf
  --other.tf

In this case subproject one and two might have different providers. It would be nice for the language server to work up the folder tree for the currently active file and find where its parent project providers are initialized for suggesting the appropriate autocompletes, etc.

I have different git branches for different environments, but they are really more like pointers along a linear git history. The custom variables derivation sourced by manage.sh is used to default/override/interpolate variables at a per environment level. Because of that, I will expect/want to run terraform init myself. It will fail if the IDE tries since it requires several -backend-config flags for info not hardcoded in the terraform files.

Thanks for all your work making the extension/language server officially maintained!

@billyshambrook
Copy link

We use Atlantis to apply our multi project repo. There is a subdirectory per project and we reference each project in the atlantis.yaml file which sits at the root of the repo. Maybe the plugin could read the atlantis.yaml file to understand the projects within the workspace?

Here's an example of our atlantis.yaml file:

version: 3

automerge: true

projects:
- name: projecta
  dir: projecta
  workflow: default
  autoplan:
    enabled: true
  apply_requirements: [approved, mergeable]
- name: projectb
  dir: terraform/projectb
  workflow: default
  autoplan:
    enabled: true
  apply_requirements: [approved, mergeable]
- name: projectc
  dir: terraform/projectc
  workflow: default
  autoplan:
    enabled: true
  apply_requirements: [approved, mergeable]

workflows:
  default:
    plan:
      steps:
      - init
      - plan:
          extra_args: ["-var-file", "atlantis.tfvars"]

The repo layout is something like:

docs/
  index.md
terraform/
  projecta/
    main.tf
  projectb/
    main.tf
  projectc/
    main.tf
atlantis.yaml

@radeksimko
Copy link
Member Author

radeksimko commented Jun 24, 2020

Thank you for the examples you have all posted so far! These are useful.

#176 (once released) will introduce support for a number of different hierarchies.

Generally all scenarios where there is only a single matching root (even if nested) should be supported fully, i.e. case where you are editing a local submodule which is only references by a single initd root module.

There's some more work to be done around picking the right candidates when there's more than 1, some ideas on how to do that are outlined in #179 and #180 - any feedback to these, or alternative ideas are welcomed!

With that in mind:

@danieladams456 I believe this hierarchy is covered by https://github.com/hashicorp/terraform-ls/tree/master/internal/terraform/rootmodule/testdata/multi-root-no-modules

@billyshambrook your hierarchy looks similar/same as @danieladams456 's, but you would need to init projecta, projectb and projectc locally in order to make them discoverable by the language server. I think that initialization will be a requirement for the foreseeable future.

@magne @amasover both "main module approach" and Terragrunt should work, but it's highly likely that a wrong root module will be matched, if all of your environments are initd (and hence discovered) at the same time. This inaccuracy is something we can address separately as part of #179 #180 and/or hashicorp/vscode-terraform#396

@danieladams456
Copy link

@radeksimko Actually multi-root-local-modules-down comes into play for me as well. I had just omitted it for brevity assuming it would be the same since it was self contained. You all are doing great work!!!

@radeksimko
Copy link
Member Author

v0.4.0 was published earlier today. This update should pop up automatically for those in VSCode (after VSCode restart). Otherwise download it manually from https://releases.hashicorp.com/terraform-ls/0.4.0/

This should support the following hierarchies https://github.com/hashicorp/terraform-ls/tree/master/internal/terraform/rootmodule/testdata with the caveats that wrong root module may be picked up when there is multiple candidates (see #179 and #180 ) and these root modules have to be initd. There is no way of setting custom root modules yet - see hashicorp/vscode-terraform#396

Keep in mind there is also #128 which you may still run into - workaround is mentioned in the thread.

@radeksimko
Copy link
Member Author

radeksimko commented Apr 7, 2022

To add some context around closing:

@github-actions
Copy link

This functionality has been released in v0.27.0 of the language server.

For further feature requests or bug reports with this functionality, please create a new GitHub issue following the template. Thank you!

@github-actions
Copy link

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues.
If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 15, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request modules Functionality related to the module block and modules generally
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants