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

Complete module source and version #1024

Merged
merged 11 commits into from
Aug 11, 2022
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 = ""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mind updating the GoReleaser config as well to pass these?

I have just added the secrets to the repo as ALGOLIA_API_KEY and ALGOLIA_APP_ID.

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.0
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that we ignore these errors from hooks upstream https://github.com/hashicorp/hcl-lang/blob/b66451713db4e6e3bcf6cde7d70bd8cb51cc2729/decoder/expression_candidates.go#L278

is it worth logging it here? e.g. by plumbing logger to Hooks and using it here.

}

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