Skip to content

Commit

Permalink
Complete module source and version (#1024)
Browse files Browse the repository at this point in the history
* Update hcl-lang to b66451

* Expose registry client to completion hooks

* Use Range when creating module calls

* Implement completion for local module sources

* Extend TF registry for getting module versions

* Implement completion for module versions

* Implement completion for registry module sources

* Expose algolia credentials on main

* Review feedback

* Update goreleaser config with Algolia credentials

* Add logging to registry hook
  • Loading branch information
dbanck authored Aug 11, 2022
1 parent 7c476e2 commit 607a812
Show file tree
Hide file tree
Showing 20 changed files with 748 additions and 22 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ jobs:
version: latest
args: release
env:
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
HC_RELEASES_HOST: ${{ secrets.HC_RELEASES_HOST_STAGING }}
HC_RELEASES_KEY: ${{ secrets.HC_RELEASES_KEY_STAGING }}
CODESIGN_IMAGE: ${{ steps.codesign.outputs.image }}
Expand Down
4 changes: 2 additions & 2 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ builds:
- -trimpath
- -tags=preloadschema
ldflags:
- '-s -w -X "main.version={{ .RawVersion }}" -X "main.prerelease={{ if .IsSnapshot }}snapshot.{{ .ShortCommit }}{{ else }}{{ .Prerelease }}{{ end }}"'
- '-s -w -X "main.version={{ .RawVersion }}" -X "main.prerelease={{ if .IsSnapshot }}snapshot.{{ .ShortCommit }}{{ else }}{{ .Prerelease }}{{ end }}" -X "main.algoliaAppID={{ .Env.ALGOLIA_APP_ID }}" -X "main.algoliaAPIKey={{ .Env.ALGOLIA_API_KEY }}"'
goarch:
- '386'
- amd64
Expand All @@ -40,7 +40,7 @@ builds:
- -trimpath
- -tags=preloadschema
ldflags:
- '-s -w -X "main.version={{ .RawVersion }}" -X "main.prerelease={{ if .IsSnapshot }}snapshot.{{ .ShortCommit }}{{ else }}{{ .Prerelease }}{{ end }}"'
- '-s -w -X "main.version={{ .RawVersion }}" -X "main.prerelease={{ if .IsSnapshot }}snapshot.{{ .ShortCommit }}{{ else }}{{ .Prerelease }}{{ end }}" -X "main.algoliaAppID={{ .Env.ALGOLIA_APP_ID }}" -X "main.algoliaAPIKey={{ .Env.ALGOLIA_API_KEY }}"'
goarch:
- '386'
- amd64
Expand Down
7 changes: 7 additions & 0 deletions algolia.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package main

// Algolia application ID which should be used for searching Terraform registry modules
var algoliaAppID = ""

// Algolia API key which should be used for searching Terraform registry modules
var algoliaAPIKey = ""
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.16

require (
github.com/agext/levenshtein v1.2.2 // indirect
github.com/algolia/algoliasearch-client-go/v3 v3.26.0
github.com/apparentlymart/go-textseg v1.0.0
github.com/creachadair/jrpc2 v0.41.1
github.com/google/go-cmp v0.5.8
Expand All @@ -14,7 +15,7 @@ require (
github.com/hashicorp/go-uuid v1.0.3
github.com/hashicorp/go-version v1.6.0
github.com/hashicorp/hc-install v0.4.0
github.com/hashicorp/hcl-lang v0.0.0-20220801150536-118ac453e267
github.com/hashicorp/hcl-lang v0.0.0-20220808135720-b66451713db4
github.com/hashicorp/hcl/v2 v2.13.0
github.com/hashicorp/terraform-exec v0.17.2
github.com/hashicorp/terraform-json v0.14.0
Expand Down
5 changes: 4 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/algolia/algoliasearch-client-go/v3 v3.26.0 h1:shlL6HS5p2Nx/+rj5mzbXRj6bUzUSy1jv+CEfyOtlH4=
github.com/algolia/algoliasearch-client-go/v3 v3.26.0/go.mod h1:i7tLoP7TYDmHX3Q7vkIOL4syVse/k5VJ+k0i8WqFiJk=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/antonmedv/expr v1.9.0 h1:j4HI3NHEdgDnN9p6oI6Ndr0G5QryMY0FNxT4ONrFDGU=
Expand Down Expand Up @@ -323,8 +325,9 @@ github.com/hashicorp/hc-install v0.4.0 h1:cZkRFr1WVa0Ty6x5fTvL1TuO1flul231rWkGH9
github.com/hashicorp/hc-install v0.4.0/go.mod h1:5d155H8EC5ewegao9A4PUTMNPZaq+TbOzkJJZ4vrXeI=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/hcl-lang v0.0.0-20220801150536-118ac453e267 h1:8Vn+GbhtkSH0rMv4SYF/WJO1j5z1z0vvpvwHPZB3Fxs=
github.com/hashicorp/hcl-lang v0.0.0-20220801150536-118ac453e267/go.mod h1:zGFgYSTTY5D5F+diri+HFKo2JGLqJb5icJl4CSEGPHk=
github.com/hashicorp/hcl-lang v0.0.0-20220808135720-b66451713db4 h1:NltaEn+1rMj2T0kmmTm8u+6abPntW6PPoOlq4h2Kyvk=
github.com/hashicorp/hcl-lang v0.0.0-20220808135720-b66451713db4/go.mod h1:zGFgYSTTY5D5F+diri+HFKo2JGLqJb5icJl4CSEGPHk=
github.com/hashicorp/hcl/v2 v2.13.0 h1:0Apadu1w6M11dyGFxWnmhhcMjkbAiKCv7G1r/2QgCNc=
github.com/hashicorp/hcl/v2 v2.13.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
Expand Down
22 changes: 22 additions & 0 deletions internal/algolia/algolia.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package algolia

import "context"

var credentialsKey struct{}

type Credentials struct {
AppID string
APIKey string
}

func WithCredentials(ctx context.Context, appID, apiKey string) context.Context {
return context.WithValue(ctx, credentialsKey, Credentials{
AppID: appID,
APIKey: apiKey,
})
}

func CredentialsFromContext(ctx context.Context) (Credentials, bool) {
credentials, ok := ctx.Value(credentialsKey).(Credentials)
return credentials, ok
}
7 changes: 7 additions & 0 deletions internal/cmd/serve_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"
"syscall"

"github.com/hashicorp/terraform-ls/internal/algolia"
lsctx "github.com/hashicorp/terraform-ls/internal/context"
"github.com/hashicorp/terraform-ls/internal/langserver"
"github.com/hashicorp/terraform-ls/internal/langserver/handlers"
Expand All @@ -23,6 +24,9 @@ type ServeCommand struct {
Ui cli.Ui
Version string

AlgoliaAppID string
AlgoliaAPIKey string

// flags
port int
logFilePath string
Expand Down Expand Up @@ -97,6 +101,9 @@ func (c *ServeCommand) Run(args []string) int {
logger.Printf("Starting terraform-ls %s", c.Version)

ctx = lsctx.WithLanguageServerVersion(ctx, c.Version)
if c.AlgoliaAppID != "" && c.AlgoliaAPIKey != "" {
ctx = algolia.WithCredentials(ctx, c.AlgoliaAppID, c.AlgoliaAPIKey)
}

srv := langserver.NewLangServer(ctx, handlers.NewSession)
srv.SetLogger(logger)
Expand Down
13 changes: 11 additions & 2 deletions internal/hooks/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@
// registered via AppendCompletionHooks in completion_hooks.go.
package hooks

import "github.com/hashicorp/terraform-ls/internal/state"
import (
"log"

"github.com/algolia/algoliasearch-client-go/v3/algolia/search"
"github.com/hashicorp/terraform-ls/internal/registry"
"github.com/hashicorp/terraform-ls/internal/state"
)

type Hooks struct {
ModStore *state.ModuleStore
ModStore *state.ModuleStore
RegistryClient registry.Client
AlgoliaClient *search.Client
Logger *log.Logger
}
42 changes: 39 additions & 3 deletions internal/hooks/module_source_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,53 @@ package hooks

import (
"context"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/hashicorp/hcl-lang/decoder"
"github.com/hashicorp/terraform-ls/internal/terraform/datadir"
"github.com/zclconf/go-cty/cty"
)

func (h *Hooks) LocalModuleSources(ctx context.Context, value cty.Value) ([]decoder.Candidate, error) {
candidates := make([]decoder.Candidate, 0)

// Obtain indexed modules via h.modStore.List()
// TODO filter modules inside .terraform
// TODO build candidates
modules, err := h.ModStore.List()
path, ok := decoder.PathFromContext(ctx)
if err != nil || !ok {
return candidates, err
}

for _, mod := range modules {
dirName := fmt.Sprintf("%c%s%c", os.PathSeparator, datadir.DataDirName, os.PathSeparator)
if strings.Contains(mod.Path, dirName) {
// Skip installed module copies in cache directories
continue
}
if mod.Path == path.Path {
// Exclude the module we're providing completion in
// to avoid cyclic references
continue
}

relPath, err := filepath.Rel(path.Path, mod.Path)
if err != nil {
continue
}
if !strings.HasPrefix(relPath, "..") {
// filepath.Rel will return the cleaned relative path, but Terraform
// expects local module sources to start with ./
relPath = "./" + relPath
}
relPath = filepath.ToSlash(relPath)
c := decoder.ExpressionCompletionCandidate(decoder.ExpressionCandidate{
Value: cty.StringVal(relPath),
Detail: "local",
})
candidates = append(candidates, c)
}

return candidates, nil
}
93 changes: 93 additions & 0 deletions internal/hooks/module_source_local_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package hooks

import (
"context"
"path/filepath"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl-lang/decoder"
"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/terraform-ls/internal/state"
"github.com/zclconf/go-cty/cty"
)

func TestHooks_LocalModuleSources(t *testing.T) {
ctx := context.Background()
tmpDir := t.TempDir()

ctx = decoder.WithPath(ctx, lang.Path{
Path: tmpDir,
LanguageID: "terraform",
})
s, err := state.NewStateStore()
if err != nil {
t.Fatal(err)
}

h := &Hooks{
ModStore: s.Modules,
}

modules := []string{
tmpDir,
filepath.Join(tmpDir, "alpha"),
filepath.Join(tmpDir, "beta"),
filepath.Join(tmpDir, "..", "gamma"),
filepath.Join(".terraform", "modules", "web_server_sg"),
filepath.Join(tmpDir, "any.terraformany"),
filepath.Join(tmpDir, "any.terraform"),
filepath.Join(tmpDir, ".terraformany"),
}

for _, mod := range modules {
err := s.Modules.Add(mod)
if err != nil {
t.Fatal(err)
}
}

expectedCandidates := []decoder.Candidate{
{
Label: "\"./.terraformany\"",
Detail: "local",
Kind: lang.StringCandidateKind,
RawInsertText: "\"./.terraformany\"",
},
{
Label: "\"./alpha\"",
Detail: "local",
Kind: lang.StringCandidateKind,
RawInsertText: "\"./alpha\"",
},
{
Label: "\"./any.terraform\"",
Detail: "local",
Kind: lang.StringCandidateKind,
RawInsertText: "\"./any.terraform\"",
},
{
Label: "\"./any.terraformany\"",
Detail: "local",
Kind: lang.StringCandidateKind,
RawInsertText: "\"./any.terraformany\"",
},
{
Label: "\"./beta\"",
Detail: "local",
Kind: lang.StringCandidateKind,
RawInsertText: "\"./beta\"",
},
{
Label: "\"../gamma\"",
Detail: "local",
Kind: lang.StringCandidateKind,
RawInsertText: "\"../gamma\"",
},
}

candidates, _ := h.LocalModuleSources(ctx, cty.StringVal(""))
if diff := cmp.Diff(expectedCandidates, candidates); diff != "" {
t.Fatalf("mismatched candidates: %s", diff)
}
}
89 changes: 89 additions & 0 deletions internal/hooks/module_source_registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package hooks

import (
"context"
"strings"

"github.com/algolia/algoliasearch-client-go/v3/algolia/opt"
"github.com/hashicorp/hcl-lang/decoder"
"github.com/hashicorp/hcl-lang/lang"
"github.com/zclconf/go-cty/cty"
)

type RegistryModule struct {
FullName string `json:"full-name"`
Description string `json:"description"`
}

const algoliaModuleIndex = "tf-registry:prod:modules"

func (h *Hooks) fetchModulesFromAlgolia(ctx context.Context, term string) ([]RegistryModule, error) {
modules := make([]RegistryModule, 0)

index := h.AlgoliaClient.InitIndex(algoliaModuleIndex)
params := []interface{}{
ctx, // transport.Request will magically extract the context from here
opt.AttributesToRetrieve("full-name", "description"),
opt.HitsPerPage(10),
}

res, err := index.Search(term, params...)
if err != nil {
return modules, err
}

err = res.UnmarshalHits(&modules)
if err != nil {
return modules, err

}

return modules, nil
}

func (h *Hooks) RegistryModuleSources(ctx context.Context, value cty.Value) ([]decoder.Candidate, error) {
candidates := make([]decoder.Candidate, 0)
prefix := value.AsString()

if isModuleSourceLocal(prefix) {
// We're dealing with a local module source here, no need to search the registry
return candidates, nil
}

if h.AlgoliaClient == nil {
return candidates, nil
}

modules, err := h.fetchModulesFromAlgolia(ctx, prefix)
if err != nil {
h.Logger.Printf("Error fetching modules from Algolia: %#v", err)
return candidates, err
}

for _, mod := range modules {
c := decoder.ExpressionCompletionCandidate(decoder.ExpressionCandidate{
Value: cty.StringVal(mod.FullName),
Detail: "registry",
Description: lang.PlainText(mod.Description),
})
candidates = append(candidates, c)
}

return candidates, nil
}

var moduleSourceLocalPrefixes = []string{
"./",
"../",
".\\",
"..\\",
}

func isModuleSourceLocal(raw string) bool {
for _, prefix := range moduleSourceLocalPrefixes {
if strings.HasPrefix(raw, prefix) {
return true
}
}
return false
}
Loading

0 comments on commit 607a812

Please sign in to comment.