From a512e9aa4f255400f9f00a0506e55d8cd46771d9 Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Wed, 3 Aug 2022 17:26:37 +0200 Subject: [PATCH 01/11] Update hcl-lang to b66451 --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 2e531d863..e2499d206 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,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 diff --git a/go.sum b/go.sum index ca7ca6004..e10b2dd8f 100644 --- a/go.sum +++ b/go.sum @@ -323,8 +323,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= From 3a8dbada9d0d15c027df0ed56c8dae8bf8e5379d Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Tue, 2 Aug 2022 12:29:57 +0200 Subject: [PATCH 02/11] Expose registry client to completion hooks --- internal/hooks/hooks.go | 8 ++++++-- internal/langserver/handlers/completion_hooks.go | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/internal/hooks/hooks.go b/internal/hooks/hooks.go index 64ddf7b49..996ea31e1 100644 --- a/internal/hooks/hooks.go +++ b/internal/hooks/hooks.go @@ -3,8 +3,12 @@ // registered via AppendCompletionHooks in completion_hooks.go. package hooks -import "github.com/hashicorp/terraform-ls/internal/state" +import ( + "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 } diff --git a/internal/langserver/handlers/completion_hooks.go b/internal/langserver/handlers/completion_hooks.go index 4cff05107..83e15ec47 100644 --- a/internal/langserver/handlers/completion_hooks.go +++ b/internal/langserver/handlers/completion_hooks.go @@ -7,7 +7,8 @@ import ( func (s *service) AppendCompletionHooks(ctx decoder.DecoderContext) { h := hooks.Hooks{ - ModStore: s.modStore, + ModStore: s.modStore, + RegistryClient: s.registryClient, } ctx.CompletionHooks["CompleteLocalModuleSources"] = h.LocalModuleSources From f820c762d63141e20be2573bc0aaed12803496a4 Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Tue, 2 Aug 2022 12:30:53 +0200 Subject: [PATCH 03/11] Use Range when creating module calls --- internal/state/module.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/state/module.go b/internal/state/module.go index fb48d1d50..ffba34f41 100644 --- a/internal/state/module.go +++ b/internal/state/module.go @@ -385,6 +385,7 @@ func (s *ModuleStore) ModuleCalls(modPath string) (tfmod.ModuleCalls, error) { SourceAddr: mc.SourceAddr, Version: mc.Version, InputNames: mc.InputNames, + RangePtr: mc.RangePtr, } } From 7caf0d46c6775b4c725d0c1f23918d758642ba51 Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Tue, 2 Aug 2022 12:31:48 +0200 Subject: [PATCH 04/11] Implement completion for local module sources --- internal/hooks/module_source_local.go | 37 +++++++++- internal/hooks/module_source_local_test.go | 84 ++++++++++++++++++++++ 2 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 internal/hooks/module_source_local_test.go diff --git a/internal/hooks/module_source_local.go b/internal/hooks/module_source_local.go index 89c108231..c915261c7 100644 --- a/internal/hooks/module_source_local.go +++ b/internal/hooks/module_source_local.go @@ -2,17 +2,48 @@ package hooks import ( "context" + "fmt" + "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 { + if strings.Contains(mod.Path, datadir.DataDirName) { + // Skip anything from the data directory + continue + } + if mod.Path == path.Path { + // Exclude the current module + continue + } + + relPath, err := filepath.Rel(path.Path, mod.Path) + if err != nil { + continue + } + if !strings.HasPrefix(relPath, ".") { + relPath = fmt.Sprintf("./%s", relPath) + } + relPath = strings.ReplaceAll(relPath, "\\", "/") + c := decoder.ExpressionCompletionCandidate(decoder.ExpressionCandidate{ + Value: cty.StringVal(relPath), + Detail: "local", + }) + candidates = append(candidates, c) + } return candidates, nil } diff --git a/internal/hooks/module_source_local_test.go b/internal/hooks/module_source_local_test.go new file mode 100644 index 000000000..8fa7a876c --- /dev/null +++ b/internal/hooks/module_source_local_test.go @@ -0,0 +1,84 @@ +package hooks + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "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/registry" + "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) + } + + regClient := registry.NewClient() + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, fmt.Sprintf("unexpected request: %q", r.RequestURI), 400) + })) + regClient.BaseURL = srv.URL + t.Cleanup(srv.Close) + + h := &Hooks{ + ModStore: s.Modules, + RegistryClient: regClient, + } + + modules := []string{ + tmpDir, + filepath.Join(tmpDir, "alpha"), + filepath.Join(tmpDir, "beta"), + filepath.Join(tmpDir, "..", "gamma"), + filepath.Join(".terraform", "modules", "web_server_sg"), + } + + for _, mod := range modules { + err := s.Modules.Add(mod) + if err != nil { + t.Fatal(err) + } + } + + expectedCandidates := []decoder.Candidate{ + { + Label: "\"./alpha\"", + Detail: "local", + Kind: lang.StringCandidateKind, + RawInsertText: "\"./alpha\"", + }, + { + 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) + } +} From 5d770a5e6d7b9ff71649385739b46f4b665af795 Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Tue, 2 Aug 2022 17:42:03 +0200 Subject: [PATCH 05/11] Extend TF registry for getting module versions --- internal/registry/registry.go | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/internal/registry/registry.go b/internal/registry/registry.go index 3ffe47b62..94682d3a7 100644 --- a/internal/registry/registry.go +++ b/internal/registry/registry.go @@ -76,6 +76,21 @@ func (c Client) GetModuleData(ctx context.Context, addr tfaddr.Module, cons vers } func (c Client) GetMatchingModuleVersion(ctx context.Context, addr tfaddr.Module, con version.Constraints) (*version.Version, error) { + foundVersions, err := c.GetModuleVersions(ctx, addr) + if err != nil { + return nil, err + } + + for _, fv := range foundVersions { + if con.Check(fv) { + return fv, nil + } + } + + return nil, fmt.Errorf("no suitable version found for %q %q", addr, con) +} + +func (c Client) GetModuleVersions(ctx context.Context, addr tfaddr.Module) (version.Collection, error) { url := fmt.Sprintf("%s/v1/modules/%s/%s/%s/versions", c.BaseURL, addr.Package.Namespace, addr.Package.Name, @@ -121,11 +136,5 @@ func (c Client) GetMatchingModuleVersion(ctx context.Context, addr tfaddr.Module sort.Sort(sort.Reverse(foundVersions)) - for _, fv := range foundVersions { - if con.Check(fv) { - return fv, nil - } - } - - return nil, fmt.Errorf("no suitable version found for %q %q", addr, con) + return foundVersions, nil } From 11ba385e7b0c3ebe7e11b3409564c5515aa97f65 Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Tue, 2 Aug 2022 17:42:57 +0200 Subject: [PATCH 06/11] Implement completion for module versions --- internal/hooks/module_version.go | 84 ++++++++++++ internal/hooks/module_version_test.go | 124 ++++++++++++++++++ .../langserver/handlers/completion_hooks.go | 1 + internal/lsp/completion.go | 1 + 4 files changed, 210 insertions(+) create mode 100644 internal/hooks/module_version.go create mode 100644 internal/hooks/module_version_test.go diff --git a/internal/hooks/module_version.go b/internal/hooks/module_version.go new file mode 100644 index 000000000..2226afe36 --- /dev/null +++ b/internal/hooks/module_version.go @@ -0,0 +1,84 @@ +package hooks + +import ( + "context" + "errors" + "fmt" + + "github.com/hashicorp/hcl-lang/decoder" + "github.com/hashicorp/hcl/v2" + tfaddr "github.com/hashicorp/terraform-registry-address" + tfmod "github.com/hashicorp/terraform-schema/module" + "github.com/zclconf/go-cty/cty" +) + +func getModuleSourceAddr(moduleCalls map[string]tfmod.DeclaredModuleCall, pos hcl.Pos, filename string) (tfmod.ModuleSourceAddr, bool) { + for _, mc := range moduleCalls { + if mc.RangePtr == nil { + // This can only happen if the file is JSON + // In this case we're not providing completion anyway + continue + } + if mc.RangePtr.ContainsPos(pos) && mc.RangePtr.Filename == filename { + return mc.SourceAddr, true + } + } + + return nil, false +} + +func (h *Hooks) RegistryModuleVersions(ctx context.Context, value cty.Value) ([]decoder.Candidate, error) { + candidates := make([]decoder.Candidate, 0) + + path, ok := decoder.PathFromContext(ctx) + if !ok { + return candidates, errors.New("missing context: path") + } + pos, ok := decoder.PosFromContext(ctx) + if !ok { + return candidates, errors.New("missing context: pos") + } + filename, ok := decoder.FilenameFromContext(ctx) + if !ok { + return candidates, errors.New("missing context: filename") + } + maxCandidates, ok := decoder.MaxCandidatesFromContext(ctx) + if !ok { + return candidates, errors.New("missing context: maxCandidates") + } + + module, err := h.ModStore.ModuleByPath(path.Path) + if err != nil { + return candidates, err + } + + sourceAddr, ok := getModuleSourceAddr(module.Meta.ModuleCalls, pos, filename) + if !ok { + return candidates, nil + } + registryAddr, ok := sourceAddr.(tfaddr.Module) + if !ok { + // Trying to complete version on local or external module + return candidates, nil + } + + versions, err := h.RegistryClient.GetModuleVersions(ctx, registryAddr) + if err != nil { + return candidates, err + } + + for i, v := range versions { + if uint(i) >= maxCandidates { + return candidates, nil + } + + c := decoder.ExpressionCompletionCandidate(decoder.ExpressionCandidate{ + Value: cty.StringVal(v.String()), + }) + c.SortText = fmt.Sprintf("%3d", i) + + candidates = append(candidates, c) + } + + return candidates, nil +} diff --git a/internal/hooks/module_version_test.go b/internal/hooks/module_version_test.go new file mode 100644 index 000000000..93ab204be --- /dev/null +++ b/internal/hooks/module_version_test.go @@ -0,0 +1,124 @@ +package hooks + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl-lang/decoder" + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform-ls/internal/registry" + "github.com/hashicorp/terraform-ls/internal/state" + tfaddr "github.com/hashicorp/terraform-registry-address" + tfmod "github.com/hashicorp/terraform-schema/module" + "github.com/zclconf/go-cty/cty" +) + +var moduleVersionsMockResponse = `{ + "modules": [ + { + "source": "terraform-aws-modules/vpc/aws", + "versions": [ + { + "version": "0.0.1" + }, + { + "version": "2.0.24" + }, + { + "version": "1.33.7" + } + ] + } + ] + }` + +func TestHooks_RegistryModuleVersions(t *testing.T) { + ctx := context.Background() + tmpDir := t.TempDir() + + ctx = decoder.WithPath(ctx, lang.Path{ + Path: tmpDir, + LanguageID: "terraform", + }) + ctx = decoder.WithPos(ctx, hcl.Pos{ + Line: 2, + Column: 5, + Byte: 5, + }) + ctx = decoder.WithFilename(ctx, "main.tf") + ctx = decoder.WithMaxCandidates(ctx, 3) + s, err := state.NewStateStore() + if err != nil { + t.Fatal(err) + } + + regClient := registry.NewClient() + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.RequestURI == "/v1/modules/terraform-aws-modules/vpc/aws/versions" { + w.Write([]byte(moduleVersionsMockResponse)) + return + } + http.Error(w, fmt.Sprintf("unexpected request: %q", r.RequestURI), 400) + })) + regClient.BaseURL = srv.URL + t.Cleanup(srv.Close) + + h := &Hooks{ + ModStore: s.Modules, + RegistryClient: regClient, + } + + err = s.Modules.Add(tmpDir) + if err != nil { + t.Fatal(err) + } + metadata := &tfmod.Meta{ + Path: tmpDir, + ModuleCalls: map[string]tfmod.DeclaredModuleCall{ + "vpc": { + LocalName: "vpc", + SourceAddr: tfaddr.MustParseModuleSource("registry.terraform.io/terraform-aws-modules/vpc/aws"), + RangePtr: &hcl.Range{ + Filename: "main.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 20}, + }, + }, + }, + } + err = s.Modules.UpdateMetadata(tmpDir, metadata, nil) + if err != nil { + t.Fatal(err) + } + + expectedCandidates := []decoder.Candidate{ + { + Label: `"2.0.24"`, + Kind: lang.StringCandidateKind, + RawInsertText: `"2.0.24"`, + SortText: " 0", + }, + { + Label: `"1.33.7"`, + Kind: lang.StringCandidateKind, + RawInsertText: `"1.33.7"`, + SortText: " 1", + }, + { + Label: `"0.0.1"`, + Kind: lang.StringCandidateKind, + RawInsertText: `"0.0.1"`, + SortText: " 2", + }, + } + + candidates, _ := h.RegistryModuleVersions(ctx, cty.StringVal("")) + if diff := cmp.Diff(expectedCandidates, candidates); diff != "" { + t.Fatalf("mismatched candidates: %s", diff) + } +} diff --git a/internal/langserver/handlers/completion_hooks.go b/internal/langserver/handlers/completion_hooks.go index 83e15ec47..7b6e80316 100644 --- a/internal/langserver/handlers/completion_hooks.go +++ b/internal/langserver/handlers/completion_hooks.go @@ -12,5 +12,6 @@ func (s *service) AppendCompletionHooks(ctx decoder.DecoderContext) { } ctx.CompletionHooks["CompleteLocalModuleSources"] = h.LocalModuleSources + ctx.CompletionHooks["CompleteRegistryModuleVersions"] = h.RegistryModuleVersions } diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go index c01214516..8eee68340 100644 --- a/internal/lsp/completion.go +++ b/internal/lsp/completion.go @@ -71,6 +71,7 @@ func toCompletionItem(candidate lang.Candidate, caps lsp.CompletionClientCapabil TextEdit: textEdit(candidate.TextEdit, snippetSupport), Command: cmd, AdditionalTextEdits: TextEdits(candidate.AdditionalTextEdits, snippetSupport), + SortText: candidate.SortText, } if candidate.ResolveHook != nil { From ff56bd0d56c5f9fc48f609e49c64c14132abfcfc Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Tue, 2 Aug 2022 21:06:07 +0200 Subject: [PATCH 07/11] Implement completion for registry module sources --- go.mod | 1 + go.sum | 2 + internal/hooks/hooks.go | 2 + internal/hooks/module_source_registry.go | 86 ++++++++ internal/hooks/module_source_registry_test.go | 188 ++++++++++++++++++ .../langserver/handlers/completion_hooks.go | 14 +- 6 files changed, 290 insertions(+), 3 deletions(-) create mode 100644 internal/hooks/module_source_registry.go create mode 100644 internal/hooks/module_source_registry_test.go diff --git a/go.mod b/go.mod index e2499d206..f81846714 100644 --- a/go.mod +++ b/go.mod @@ -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.0 github.com/google/go-cmp v0.5.8 diff --git a/go.sum b/go.sum index e10b2dd8f..f80f5d2f7 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/hooks/hooks.go b/internal/hooks/hooks.go index 996ea31e1..ab7853f54 100644 --- a/internal/hooks/hooks.go +++ b/internal/hooks/hooks.go @@ -4,6 +4,7 @@ package hooks import ( + "github.com/algolia/algoliasearch-client-go/v3/algolia/search" "github.com/hashicorp/terraform-ls/internal/registry" "github.com/hashicorp/terraform-ls/internal/state" ) @@ -11,4 +12,5 @@ import ( type Hooks struct { ModStore *state.ModuleStore RegistryClient registry.Client + AlgoliaClient *search.Client } diff --git a/internal/hooks/module_source_registry.go b/internal/hooks/module_source_registry.go new file mode 100644 index 000000000..3a35f47f8 --- /dev/null +++ b/internal/hooks/module_source_registry.go @@ -0,0 +1,86 @@ +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"` +} + +func (h *Hooks) fetchModulesFromAlgolia(ctx context.Context, term string) ([]RegistryModule, error) { + modules := make([]RegistryModule, 0) + + index := h.AlgoliaClient.InitIndex("tf-registry:prod:modules") + 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 { + 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 +} diff --git a/internal/hooks/module_source_registry_test.go b/internal/hooks/module_source_registry_test.go new file mode 100644 index 000000000..1df2b9aae --- /dev/null +++ b/internal/hooks/module_source_registry_test.go @@ -0,0 +1,188 @@ +package hooks + +import ( + "context" + "crypto/tls" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl-lang/decoder" + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/terraform-ls/internal/registry" + "github.com/hashicorp/terraform-ls/internal/state" + "github.com/zclconf/go-cty/cty" +) + +const responseAWS = `{ + "hits": [ + { + "full-name": "terraform-aws-modules/vpc/aws", + "description": "Terraform module which creates VPC resources on AWS", + "objectID": "modules:23" + }, + { + "full-name": "terraform-aws-modules/eks/aws", + "description": "Terraform module to create an Elastic Kubernetes (EKS) cluster and associated resources", + "objectID": "modules:1143" + } + ], + "nbHits": 10200, + "page": 0, + "nbPages": 100, + "hitsPerPage": 2, + "exhaustiveNbHits": true, + "exhaustiveTypo": true, + "query": "aws", + "params": "attributesToRetrieve=%5B%22full-name%22%2C%22description%22%5D&hitsPerPage=2&query=aws", + "renderingContent": {}, + "processingTimeMS": 1, + "processingTimingsMS": {} +}` + +const responseEmpty = `{ + "hits": [], + "nbHits": 0, + "page": 0, + "nbPages": 0, + "hitsPerPage": 2, + "exhaustiveNbHits": true, + "exhaustiveTypo": true, + "query": "foo", + "params": "attributesToRetrieve=%5B%22full-name%22%2C%22description%22%5D&hitsPerPage=2&query=foo", + "renderingContent": {}, + "processingTimeMS": 1 +}` + +const responseErr = `{ + "message": "Invalid Application-ID or API key", + "status": 403 +}` + +type testRequester struct { + client *http.Client +} + +func (r *testRequester) Request(req *http.Request) (*http.Response, error) { + return r.client.Do(req) +} + +func TestHooks_RegistryModuleSources(t *testing.T) { + ctx := context.Background() + + s, err := state.NewStateStore() + if err != nil { + t.Fatal(err) + } + + regServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, fmt.Sprintf("unexpected request: %q", r.RequestURI), 400) + })) + t.Cleanup(regServer.Close) + regClient := registry.NewClient() + regClient.BaseURL = regServer.URL + + searchServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.RequestURI == "/1/indexes/tf-registry%3Aprod%3Amodules/query" { + b, _ := io.ReadAll(r.Body) + + if strings.Contains(string(b), "query=aws") { + w.Write([]byte(responseAWS)) + return + } else if strings.Contains(string(b), "query=err") { + http.Error(w, responseErr, http.StatusForbidden) + return + } + + w.Write([]byte(responseEmpty)) + return + } + http.Error(w, fmt.Sprintf("unexpected request: %q", r.RequestURI), 400) + })) + searchServer.StartTLS() + t.Cleanup(searchServer.Close) + + // Algolia requires hosts to be without a protocol and always assumes https + u, err := url.Parse(searchServer.URL) + if err != nil { + t.Fatal(err) + } + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + r := &testRequester{ + client: &http.Client{Transport: tr}, + } + searchClient := search.NewClientWithConfig(search.Configuration{ + Hosts: []string{u.Host}, + Requester: r, + }) + + h := &Hooks{ + ModStore: s.Modules, + RegistryClient: regClient, + AlgoliaClient: searchClient, + } + + tests := []struct { + name string + value cty.Value + want []decoder.Candidate + wantErr bool + }{ + { + "simple search", + cty.StringVal("aws"), + []decoder.Candidate{ + { + Label: `"terraform-aws-modules/vpc/aws"`, + Detail: "registry", + Kind: lang.StringCandidateKind, + Description: lang.PlainText("Terraform module which creates VPC resources on AWS"), + RawInsertText: `"terraform-aws-modules/vpc/aws"`, + }, + { + Label: `"terraform-aws-modules/eks/aws"`, + Detail: "registry", + Kind: lang.StringCandidateKind, + Description: lang.PlainText("Terraform module to create an Elastic Kubernetes (EKS) cluster and associated resources"), + RawInsertText: `"terraform-aws-modules/eks/aws"`, + }, + }, + false, + }, + { + "empty result", + cty.StringVal("foo"), + []decoder.Candidate{}, + false, + }, + { + "auth error", + cty.StringVal("err"), + []decoder.Candidate{}, + true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + candidates, err := h.RegistryModuleSources(ctx, tt.value) + + if (err != nil) != tt.wantErr { + t.Errorf("Hooks.RegistryModuleSources() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if diff := cmp.Diff(tt.want, candidates); diff != "" { + t.Fatalf("mismatched candidates: %s", diff) + } + }) + } +} diff --git a/internal/langserver/handlers/completion_hooks.go b/internal/langserver/handlers/completion_hooks.go index 7b6e80316..b3eb5b7bd 100644 --- a/internal/langserver/handlers/completion_hooks.go +++ b/internal/langserver/handlers/completion_hooks.go @@ -1,17 +1,25 @@ package handlers import ( + "github.com/algolia/algoliasearch-client-go/v3/algolia/search" "github.com/hashicorp/hcl-lang/decoder" "github.com/hashicorp/terraform-ls/internal/hooks" ) -func (s *service) AppendCompletionHooks(ctx decoder.DecoderContext) { +var AlgoliaAppID = "" +var AlgoliaAPIKey = "" + +func (s *service) AppendCompletionHooks(decoderContext decoder.DecoderContext) { h := hooks.Hooks{ ModStore: s.modStore, RegistryClient: s.registryClient, } - ctx.CompletionHooks["CompleteLocalModuleSources"] = h.LocalModuleSources - ctx.CompletionHooks["CompleteRegistryModuleVersions"] = h.RegistryModuleVersions + if AlgoliaAppID != "" && AlgoliaAPIKey != "" { + h.AlgoliaClient = search.NewClient(AlgoliaAppID, AlgoliaAPIKey) + } + decoderContext.CompletionHooks["CompleteLocalModuleSources"] = h.LocalModuleSources + decoderContext.CompletionHooks["CompleteRegistryModuleSources"] = h.RegistryModuleSources + decoderContext.CompletionHooks["CompleteRegistryModuleVersions"] = h.RegistryModuleVersions } From 72ae2360b77bc9e8fc9f2ecd970023588d056a57 Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Tue, 9 Aug 2022 17:44:50 +0200 Subject: [PATCH 08/11] Expose algolia credentials on main --- algolia.go | 7 +++++++ internal/algolia/algolia.go | 6 ++++++ internal/cmd/serve_command.go | 4 ++++ internal/context/context.go | 14 ++++++++++++++ internal/langserver/handlers/completion_hooks.go | 9 ++++----- main.go | 6 ++++-- 6 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 algolia.go create mode 100644 internal/algolia/algolia.go diff --git a/algolia.go b/algolia.go new file mode 100644 index 000000000..25fd860af --- /dev/null +++ b/algolia.go @@ -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 = "" diff --git a/internal/algolia/algolia.go b/internal/algolia/algolia.go new file mode 100644 index 000000000..6e77983d2 --- /dev/null +++ b/internal/algolia/algolia.go @@ -0,0 +1,6 @@ +package algolia + +type AlgoliaCredentials struct { + AlgoliaAppID string + AlgoliaAPIKey string +} diff --git a/internal/cmd/serve_command.go b/internal/cmd/serve_command.go index 8bf87ec6a..9043e33d5 100644 --- a/internal/cmd/serve_command.go +++ b/internal/cmd/serve_command.go @@ -23,6 +23,9 @@ type ServeCommand struct { Ui cli.Ui Version string + AlgoliaAppID string + AlgoliaAPIKey string + // flags port int logFilePath string @@ -97,6 +100,7 @@ func (c *ServeCommand) Run(args []string) int { logger.Printf("Starting terraform-ls %s", c.Version) ctx = lsctx.WithLanguageServerVersion(ctx, c.Version) + ctx = lsctx.WithAlgoliaCredentials(ctx, c.AlgoliaAppID, c.AlgoliaAPIKey) srv := langserver.NewLangServer(ctx, handlers.NewSession) srv.SetLogger(logger) diff --git a/internal/context/context.go b/internal/context/context.go index b3e7e31be..025af55f3 100644 --- a/internal/context/context.go +++ b/internal/context/context.go @@ -4,6 +4,7 @@ import ( "context" "time" + "github.com/hashicorp/terraform-ls/internal/algolia" "github.com/hashicorp/terraform-ls/internal/langserver/diagnostics" lsp "github.com/hashicorp/terraform-ls/internal/protocol" "github.com/hashicorp/terraform-ls/internal/settings" @@ -27,6 +28,7 @@ var ( ctxLsVersion = &contextKey{"language server version"} ctxProgressToken = &contextKey{"progress token"} ctxExperimentalFeatures = &contextKey{"experimental features"} + ctxAlgoliaCredentials = &contextKey{"algolia credentials"} ) func missingContextErr(ctxKey *contextKey) *MissingContextErr { @@ -162,3 +164,15 @@ func ExperimentalFeatures(ctx context.Context) (settings.ExperimentalFeatures, e } return *expFeatures, nil } + +func WithAlgoliaCredentials(ctx context.Context, algoliaAppID, algoliaAPIKey string) context.Context { + return context.WithValue(ctx, ctxAlgoliaCredentials, algolia.AlgoliaCredentials{ + AlgoliaAppID: algoliaAppID, + AlgoliaAPIKey: algoliaAPIKey, + }) +} + +func AlgoliaCredentials(ctx context.Context) (algolia.AlgoliaCredentials, bool) { + credentials, ok := ctx.Value(ctxAlgoliaCredentials).(algolia.AlgoliaCredentials) + return credentials, ok +} diff --git a/internal/langserver/handlers/completion_hooks.go b/internal/langserver/handlers/completion_hooks.go index b3eb5b7bd..304a5ced2 100644 --- a/internal/langserver/handlers/completion_hooks.go +++ b/internal/langserver/handlers/completion_hooks.go @@ -3,20 +3,19 @@ package handlers import ( "github.com/algolia/algoliasearch-client-go/v3/algolia/search" "github.com/hashicorp/hcl-lang/decoder" + lsctx "github.com/hashicorp/terraform-ls/internal/context" "github.com/hashicorp/terraform-ls/internal/hooks" ) -var AlgoliaAppID = "" -var AlgoliaAPIKey = "" - func (s *service) AppendCompletionHooks(decoderContext decoder.DecoderContext) { h := hooks.Hooks{ ModStore: s.modStore, RegistryClient: s.registryClient, } - if AlgoliaAppID != "" && AlgoliaAPIKey != "" { - h.AlgoliaClient = search.NewClient(AlgoliaAppID, AlgoliaAPIKey) + credentials, ok := lsctx.AlgoliaCredentials(s.srvCtx) + if ok { + h.AlgoliaClient = search.NewClient(credentials.AlgoliaAppID, credentials.AlgoliaAPIKey) } decoderContext.CompletionHooks["CompleteLocalModuleSources"] = h.LocalModuleSources diff --git a/main.go b/main.go index b911400d8..3a99ebdb7 100644 --- a/main.go +++ b/main.go @@ -29,8 +29,10 @@ func main() { c.Commands = map[string]cli.CommandFactory{ "serve": func() (cli.Command, error) { return &cmd.ServeCommand{ - Ui: ui, - Version: VersionString(), + Ui: ui, + Version: VersionString(), + AlgoliaAppID: algoliaAppID, + AlgoliaAPIKey: algoliaAPIKey, }, nil }, "inspect-module": func() (cli.Command, error) { From d288819c3cef32523ac955a2b2831a8f1690a94c Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Thu, 11 Aug 2022 13:29:41 +0200 Subject: [PATCH 09/11] Review feedback --- internal/algolia/algolia.go | 22 ++++- internal/cmd/serve_command.go | 5 +- internal/context/context.go | 14 --- internal/hooks/module_source_local.go | 17 ++-- internal/hooks/module_source_local_test.go | 35 +++++--- internal/hooks/module_source_registry.go | 4 +- internal/hooks/module_source_registry_test.go | 90 ++++++++++++------- internal/hooks/module_version.go | 4 + .../langserver/handlers/completion_hooks.go | 6 +- internal/registry/registry_test.go | 2 +- 10 files changed, 127 insertions(+), 72 deletions(-) diff --git a/internal/algolia/algolia.go b/internal/algolia/algolia.go index 6e77983d2..fff4b913a 100644 --- a/internal/algolia/algolia.go +++ b/internal/algolia/algolia.go @@ -1,6 +1,22 @@ package algolia -type AlgoliaCredentials struct { - AlgoliaAppID string - AlgoliaAPIKey string +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 } diff --git a/internal/cmd/serve_command.go b/internal/cmd/serve_command.go index 9043e33d5..7563778b3 100644 --- a/internal/cmd/serve_command.go +++ b/internal/cmd/serve_command.go @@ -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" @@ -100,7 +101,9 @@ func (c *ServeCommand) Run(args []string) int { logger.Printf("Starting terraform-ls %s", c.Version) ctx = lsctx.WithLanguageServerVersion(ctx, c.Version) - ctx = lsctx.WithAlgoliaCredentials(ctx, c.AlgoliaAppID, c.AlgoliaAPIKey) + if c.AlgoliaAppID != "" && c.AlgoliaAPIKey != "" { + ctx = algolia.WithCredentials(ctx, c.AlgoliaAppID, c.AlgoliaAPIKey) + } srv := langserver.NewLangServer(ctx, handlers.NewSession) srv.SetLogger(logger) diff --git a/internal/context/context.go b/internal/context/context.go index 025af55f3..b3e7e31be 100644 --- a/internal/context/context.go +++ b/internal/context/context.go @@ -4,7 +4,6 @@ import ( "context" "time" - "github.com/hashicorp/terraform-ls/internal/algolia" "github.com/hashicorp/terraform-ls/internal/langserver/diagnostics" lsp "github.com/hashicorp/terraform-ls/internal/protocol" "github.com/hashicorp/terraform-ls/internal/settings" @@ -28,7 +27,6 @@ var ( ctxLsVersion = &contextKey{"language server version"} ctxProgressToken = &contextKey{"progress token"} ctxExperimentalFeatures = &contextKey{"experimental features"} - ctxAlgoliaCredentials = &contextKey{"algolia credentials"} ) func missingContextErr(ctxKey *contextKey) *MissingContextErr { @@ -164,15 +162,3 @@ func ExperimentalFeatures(ctx context.Context) (settings.ExperimentalFeatures, e } return *expFeatures, nil } - -func WithAlgoliaCredentials(ctx context.Context, algoliaAppID, algoliaAPIKey string) context.Context { - return context.WithValue(ctx, ctxAlgoliaCredentials, algolia.AlgoliaCredentials{ - AlgoliaAppID: algoliaAppID, - AlgoliaAPIKey: algoliaAPIKey, - }) -} - -func AlgoliaCredentials(ctx context.Context) (algolia.AlgoliaCredentials, bool) { - credentials, ok := ctx.Value(ctxAlgoliaCredentials).(algolia.AlgoliaCredentials) - return credentials, ok -} diff --git a/internal/hooks/module_source_local.go b/internal/hooks/module_source_local.go index c915261c7..172106d64 100644 --- a/internal/hooks/module_source_local.go +++ b/internal/hooks/module_source_local.go @@ -3,6 +3,7 @@ package hooks import ( "context" "fmt" + "os" "path/filepath" "strings" @@ -21,12 +22,14 @@ func (h *Hooks) LocalModuleSources(ctx context.Context, value cty.Value) ([]deco } for _, mod := range modules { - if strings.Contains(mod.Path, datadir.DataDirName) { - // Skip anything from the data directory + 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 current module + // Exclude the module we're providing completion in + // to avoid cyclic references continue } @@ -34,10 +37,12 @@ func (h *Hooks) LocalModuleSources(ctx context.Context, value cty.Value) ([]deco if err != nil { continue } - if !strings.HasPrefix(relPath, ".") { - relPath = fmt.Sprintf("./%s", relPath) + if !strings.HasPrefix(relPath, "..") { + // filepath.Rel will return the cleaned relative path, but Terraform + // expects local module sources to start with ./ + relPath = "./" + relPath } - relPath = strings.ReplaceAll(relPath, "\\", "/") + relPath = filepath.ToSlash(relPath) c := decoder.ExpressionCompletionCandidate(decoder.ExpressionCandidate{ Value: cty.StringVal(relPath), Detail: "local", diff --git a/internal/hooks/module_source_local_test.go b/internal/hooks/module_source_local_test.go index 8fa7a876c..6453f28aa 100644 --- a/internal/hooks/module_source_local_test.go +++ b/internal/hooks/module_source_local_test.go @@ -2,16 +2,12 @@ package hooks import ( "context" - "fmt" - "net/http" - "net/http/httptest" "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/registry" "github.com/hashicorp/terraform-ls/internal/state" "github.com/zclconf/go-cty/cty" ) @@ -29,16 +25,8 @@ func TestHooks_LocalModuleSources(t *testing.T) { t.Fatal(err) } - regClient := registry.NewClient() - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Error(w, fmt.Sprintf("unexpected request: %q", r.RequestURI), 400) - })) - regClient.BaseURL = srv.URL - t.Cleanup(srv.Close) - h := &Hooks{ - ModStore: s.Modules, - RegistryClient: regClient, + ModStore: s.Modules, } modules := []string{ @@ -47,6 +35,9 @@ func TestHooks_LocalModuleSources(t *testing.T) { 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 { @@ -57,12 +48,30 @@ func TestHooks_LocalModuleSources(t *testing.T) { } 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", diff --git a/internal/hooks/module_source_registry.go b/internal/hooks/module_source_registry.go index 3a35f47f8..215eca39e 100644 --- a/internal/hooks/module_source_registry.go +++ b/internal/hooks/module_source_registry.go @@ -15,10 +15,12 @@ type RegistryModule struct { 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("tf-registry:prod:modules") + index := h.AlgoliaClient.InitIndex(algoliaModuleIndex) params := []interface{}{ ctx, // transport.Request will magically extract the context from here opt.AttributesToRetrieve("full-name", "description"), diff --git a/internal/hooks/module_source_registry_test.go b/internal/hooks/module_source_registry_test.go index 1df2b9aae..a5c6f62a8 100644 --- a/internal/hooks/module_source_registry_test.go +++ b/internal/hooks/module_source_registry_test.go @@ -5,17 +5,18 @@ import ( "crypto/tls" "fmt" "io" + "net" "net/http" "net/http/httptest" "net/url" "strings" "testing" + "time" "github.com/algolia/algoliasearch-client-go/v3/algolia/search" "github.com/google/go-cmp/cmp" "github.com/hashicorp/hcl-lang/decoder" "github.com/hashicorp/hcl-lang/lang" - "github.com/hashicorp/terraform-ls/internal/registry" "github.com/hashicorp/terraform-ls/internal/state" "github.com/zclconf/go-cty/cty" ) @@ -81,14 +82,7 @@ func TestHooks_RegistryModuleSources(t *testing.T) { t.Fatal(err) } - regServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Error(w, fmt.Sprintf("unexpected request: %q", r.RequestURI), 400) - })) - t.Cleanup(regServer.Close) - regClient := registry.NewClient() - regClient.BaseURL = regServer.URL - - searchServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + searchClient := buildSearchClientMock(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.RequestURI == "/1/indexes/tf-registry%3Aprod%3Amodules/query" { b, _ := io.ReadAll(r.Body) @@ -105,29 +99,10 @@ func TestHooks_RegistryModuleSources(t *testing.T) { } http.Error(w, fmt.Sprintf("unexpected request: %q", r.RequestURI), 400) })) - searchServer.StartTLS() - t.Cleanup(searchServer.Close) - - // Algolia requires hosts to be without a protocol and always assumes https - u, err := url.Parse(searchServer.URL) - if err != nil { - t.Fatal(err) - } - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - r := &testRequester{ - client: &http.Client{Transport: tr}, - } - searchClient := search.NewClientWithConfig(search.Configuration{ - Hosts: []string{u.Host}, - Requester: r, - }) h := &Hooks{ - ModStore: s.Modules, - RegistryClient: regClient, - AlgoliaClient: searchClient, + ModStore: s.Modules, + AlgoliaClient: searchClient, } tests := []struct { @@ -186,3 +161,58 @@ func TestHooks_RegistryModuleSources(t *testing.T) { }) } } + +func TestHooks_RegistryModuleSourcesCtxCancel(t *testing.T) { + ctx := context.Background() + ctx, cancelFunc := context.WithTimeout(ctx, 50*time.Millisecond) + t.Cleanup(cancelFunc) + + s, err := state.NewStateStore() + if err != nil { + t.Fatal(err) + } + + searchClient := buildSearchClientMock(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(500 * time.Millisecond) + http.Error(w, fmt.Sprintf("unexpected request: %q", r.RequestURI), 400) + })) + + h := &Hooks{ + ModStore: s.Modules, + AlgoliaClient: searchClient, + } + + _, err = h.RegistryModuleSources(ctx, cty.StringVal("aws")) + e, ok := err.(net.Error) + if !ok { + t.Fatalf("expected error, got %#v", err) + } + + if !strings.Contains(e.Error(), "context deadline exceeded") { + t.Fatalf("expected error with: %q, given: %q", "context deadline exceeded", e.Error()) + } +} + +func buildSearchClientMock(t *testing.T, handler http.HandlerFunc) *search.Client { + searchServer := httptest.NewTLSServer(handler) + t.Cleanup(searchServer.Close) + + // Algolia requires hosts to be without a protocol and always assumes https + u, err := url.Parse(searchServer.URL) + if err != nil { + t.Fatal(err) + } + searchClient := search.NewClientWithConfig(search.Configuration{ + Hosts: []string{u.Host}, + // We need to disable certificate checking here, because of the self signed cert + Requester: &testRequester{ + client: &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + }, + }, + }) + + return searchClient +} diff --git a/internal/hooks/module_version.go b/internal/hooks/module_version.go index 2226afe36..91962012c 100644 --- a/internal/hooks/module_version.go +++ b/internal/hooks/module_version.go @@ -75,6 +75,10 @@ func (h *Hooks) RegistryModuleVersions(ctx context.Context, value cty.Value) ([] c := decoder.ExpressionCompletionCandidate(decoder.ExpressionCandidate{ Value: cty.StringVal(v.String()), }) + // We rely on the fact that hcl-lang limits number of candidates + // to 100, so padding with <=3 zeros provides naive but good enough + // way to reliably "lexicographically" sort the versions as there's + // no better way to do it in LSP. c.SortText = fmt.Sprintf("%3d", i) candidates = append(candidates, c) diff --git a/internal/langserver/handlers/completion_hooks.go b/internal/langserver/handlers/completion_hooks.go index 304a5ced2..40d022e64 100644 --- a/internal/langserver/handlers/completion_hooks.go +++ b/internal/langserver/handlers/completion_hooks.go @@ -3,7 +3,7 @@ package handlers import ( "github.com/algolia/algoliasearch-client-go/v3/algolia/search" "github.com/hashicorp/hcl-lang/decoder" - lsctx "github.com/hashicorp/terraform-ls/internal/context" + "github.com/hashicorp/terraform-ls/internal/algolia" "github.com/hashicorp/terraform-ls/internal/hooks" ) @@ -13,9 +13,9 @@ func (s *service) AppendCompletionHooks(decoderContext decoder.DecoderContext) { RegistryClient: s.registryClient, } - credentials, ok := lsctx.AlgoliaCredentials(s.srvCtx) + credentials, ok := algolia.CredentialsFromContext(s.srvCtx) if ok { - h.AlgoliaClient = search.NewClient(credentials.AlgoliaAppID, credentials.AlgoliaAPIKey) + h.AlgoliaClient = search.NewClient(credentials.AppID, credentials.APIKey) } decoderContext.CompletionHooks["CompleteLocalModuleSources"] = h.LocalModuleSources diff --git a/internal/registry/registry_test.go b/internal/registry/registry_test.go index 620d6801b..76c6bb6e1 100644 --- a/internal/registry/registry_test.go +++ b/internal/registry/registry_test.go @@ -166,7 +166,7 @@ func TestCancellationThroughContext(t *testing.T) { client := NewClient() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - time.Sleep(100 * time.Millisecond) + time.Sleep(500 * time.Millisecond) if r.RequestURI == "/v1/modules/puppetlabs/deployment/ec/versions" { w.Write([]byte(moduleVersionsMockResponse)) return From d54ffa6d9c0c5cb01e34a238f5db83bcd0a9da0f Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Thu, 11 Aug 2022 13:35:20 +0200 Subject: [PATCH 10/11] Update goreleaser config with Algolia credentials --- .github/workflows/release.yml | 2 ++ .goreleaser.yml | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 67fdf2ed1..c5ffed2da 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 }} diff --git a/.goreleaser.yml b/.goreleaser.yml index 7de187b2c..fea74795a 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -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 @@ -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 From bc53c47b9344413a6c1550a82f5b5c0676f5ae73 Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Thu, 11 Aug 2022 13:54:42 +0200 Subject: [PATCH 11/11] Add logging to registry hook --- internal/hooks/hooks.go | 3 +++ internal/hooks/module_source_registry.go | 1 + internal/hooks/module_source_registry_test.go | 4 ++++ internal/langserver/handlers/completion_hooks.go | 1 + 4 files changed, 9 insertions(+) diff --git a/internal/hooks/hooks.go b/internal/hooks/hooks.go index ab7853f54..0bc30d7ab 100644 --- a/internal/hooks/hooks.go +++ b/internal/hooks/hooks.go @@ -4,6 +4,8 @@ package hooks 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" @@ -13,4 +15,5 @@ type Hooks struct { ModStore *state.ModuleStore RegistryClient registry.Client AlgoliaClient *search.Client + Logger *log.Logger } diff --git a/internal/hooks/module_source_registry.go b/internal/hooks/module_source_registry.go index 215eca39e..c89291425 100644 --- a/internal/hooks/module_source_registry.go +++ b/internal/hooks/module_source_registry.go @@ -56,6 +56,7 @@ func (h *Hooks) RegistryModuleSources(ctx context.Context, value cty.Value) ([]d modules, err := h.fetchModulesFromAlgolia(ctx, prefix) if err != nil { + h.Logger.Printf("Error fetching modules from Algolia: %#v", err) return candidates, err } diff --git a/internal/hooks/module_source_registry_test.go b/internal/hooks/module_source_registry_test.go index a5c6f62a8..438898edc 100644 --- a/internal/hooks/module_source_registry_test.go +++ b/internal/hooks/module_source_registry_test.go @@ -5,6 +5,8 @@ import ( "crypto/tls" "fmt" "io" + "io/ioutil" + "log" "net" "net/http" "net/http/httptest" @@ -103,6 +105,7 @@ func TestHooks_RegistryModuleSources(t *testing.T) { h := &Hooks{ ModStore: s.Modules, AlgoliaClient: searchClient, + Logger: log.New(ioutil.Discard, "", 0), } tests := []struct { @@ -180,6 +183,7 @@ func TestHooks_RegistryModuleSourcesCtxCancel(t *testing.T) { h := &Hooks{ ModStore: s.Modules, AlgoliaClient: searchClient, + Logger: log.New(ioutil.Discard, "", 0), } _, err = h.RegistryModuleSources(ctx, cty.StringVal("aws")) diff --git a/internal/langserver/handlers/completion_hooks.go b/internal/langserver/handlers/completion_hooks.go index 40d022e64..96f2110a4 100644 --- a/internal/langserver/handlers/completion_hooks.go +++ b/internal/langserver/handlers/completion_hooks.go @@ -11,6 +11,7 @@ func (s *service) AppendCompletionHooks(decoderContext decoder.DecoderContext) { h := hooks.Hooks{ ModStore: s.modStore, RegistryClient: s.registryClient, + Logger: s.logger, } credentials, ok := algolia.CredentialsFromContext(s.srvCtx)