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

Cache registry module errors #1258

Merged
merged 7 commits into from
Apr 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions internal/registry/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ type ModuleVersion struct {
Version string `json:"version"`
}

type ClientError struct {
StatusCode int
Body string
}

func (rce ClientError) Error() string {
return fmt.Sprintf("%d: %s", rce.StatusCode, rce.Body)
}

func (c Client) GetModuleData(ctx context.Context, addr tfaddr.Module, cons version.Constraints) (*ModuleResponse, error) {
var response ModuleResponse

Expand All @@ -77,14 +86,15 @@ func (c Client) GetModuleData(ctx context.Context, addr tfaddr.Module, cons vers
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != 200 {
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
defer resp.Body.Close()

return nil, fmt.Errorf("unexpected response %s: %s", resp.Status, string(bodyBytes))
return nil, ClientError{StatusCode: resp.StatusCode, Body: string(bodyBytes)}
}

err = json.NewDecoder(resp.Body).Decode(&response)
Expand Down Expand Up @@ -128,14 +138,15 @@ func (c Client) GetModuleVersions(ctx context.Context, addr tfaddr.Module) (vers
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != 200 {
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
defer resp.Body.Close()

return nil, fmt.Errorf("unexpected response %s: %s", resp.Status, string(bodyBytes))
return nil, ClientError{StatusCode: resp.StatusCode, Body: string(bodyBytes)}
}

var response ModuleVersionsResponse
Expand Down
4 changes: 4 additions & 0 deletions internal/state/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,10 @@ func (s *ModuleStore) RegistryModuleMeta(addr tfaddr.Module, cons version.Constr
for item := it.Next(); item != nil; item = it.Next() {
mod := item.(*RegistryModuleData)

if mod.Error {
continue
}

if cons.Check(mod.Version) {
return &registry.ModuleData{
Version: mod.Version,
Expand Down
36 changes: 36 additions & 0 deletions internal/state/registry_modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
type RegistryModuleData struct {
Source tfaddr.Module
Version *version.Version
Error bool
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 only cache 404 errors, is it worth calling this e.g. NotFound or something similar instead of just Error?

Copy link
Member Author

Choose a reason for hiding this comment

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

Since we now cache most 4XX errors, I'll keep it as is?

Inputs []registry.Input
Outputs []registry.Output
}
Expand All @@ -28,6 +29,13 @@ func (s *RegistryModuleStore) Exists(sourceAddr tfaddr.Module, constraint versio

for obj := iter.Next(); obj != nil; obj = iter.Next() {
p := obj.(*RegistryModuleData)
// There was an error fetching the module, so we can't compare
// any versions
if p.Error {
return true, nil
}

// Check if there an entry with a matching version
if constraint.Check(p.Version) {
return true, nil
}
Expand Down Expand Up @@ -67,3 +75,31 @@ func (s *RegistryModuleStore) Cache(sourceAddr tfaddr.Module, modVer *version.Ve
txn.Commit()
return nil
}

func (s *RegistryModuleStore) CacheError(sourceAddr tfaddr.Module) error {
Copy link
Member

Choose a reason for hiding this comment

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

Similar question to above - should we call this e.g. CacheNotFound()?

txn := s.db.Txn(true)
defer txn.Abort()

obj, err := txn.First(s.tableName, "id", sourceAddr, nil)
if err != nil {
return err
}
if obj != nil {
return &AlreadyExistsError{
Idx: sourceAddr.String(),
}
}

modData := &RegistryModuleData{
Source: sourceAddr,
Error: true,
}

err = txn.Insert(s.tableName, modData)
if err != nil {
return err
}

txn.Commit()
return nil
}
37 changes: 37 additions & 0 deletions internal/state/registry_modules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,40 @@ func TestModule_DeclaredModuleMeta(t *testing.T) {
t.Fatalf("mismatch chached metadata: %s", diff)
}
}

func TestStateStore_cache_error(t *testing.T) {
s, err := NewStateStore()
if err != nil {
t.Fatal(err)
}

source, err := tfaddr.ParseModuleSource("terraform-aws-modules/eks/aws")
if err != nil {
t.Fatal(err)
}
c := version.MustConstraints(version.NewConstraint(">= 3.0"))

// should be false
exists, err := s.RegistryModules.Exists(source, c)
if err != nil {
t.Fatal(err)
}
if exists == true {
t.Fatal("should not exist")
}

// store an error for a moudle
err = s.RegistryModules.CacheError(source)
if err != nil {
t.Fatal(err)
}

// should be true
exists, err = s.RegistryModules.Exists(source, c)
if err != nil {
t.Fatal(err)
}
if exists != true {
t.Fatal("should exist")
}
}
1 change: 1 addition & 0 deletions internal/state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ var dbSchema = &memdb.DBSchema{
&StringerFieldIndexer{Field: "Source"},
&VersionFieldIndexer{Field: "Version"},
},
AllowMissing: true,
},
},
"source_addr": {
Expand Down
12 changes: 12 additions & 0 deletions internal/terraform/module/module_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,18 @@ func GetModuleDataFromRegistry(ctx context.Context, regClient registry.Client, m
metaData, err := regClient.GetModuleData(ctx, sourceAddr, declaredModule.Version)
if err != nil {
errs = multierror.Append(errs, err)

clientError := registry.ClientError{}
if errors.As(err, &clientError) &&
((clientError.StatusCode >= 400 && clientError.StatusCode < 408) ||
(clientError.StatusCode > 408 && clientError.StatusCode < 429)) {
// Still cache the module
err = modRegStore.CacheError(sourceAddr)
if err != nil {
errs = multierror.Append(errs, err)
}
}

continue
}

Expand Down
23 changes: 22 additions & 1 deletion internal/terraform/module/module_ops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,6 @@ func TestGetModuleDataFromRegistry_moduleNotFound(t *testing.T) {

// Verify that 2nd module is still cached even if
// obtaining data for the other one errored out

addr, err := tfaddr.ParseModuleSource("puppetlabs/deployment/ec")
if err != nil {
t.Fatal(err)
Expand All @@ -186,6 +185,28 @@ func TestGetModuleDataFromRegistry_moduleNotFound(t *testing.T) {
if diff := cmp.Diff(expectedModuleData, meta, ctydebug.CmpOptions); diff != "" {
t.Fatalf("metadata mismatch: %s", diff)
}

// Verify that the third module is still cached even if
// it returns a not found error
addr, err = tfaddr.ParseModuleSource("terraform-aws-modules/eks/aws")
if err != nil {
t.Fatal(err)
}
cons = version.MustConstraints(version.NewConstraint("0.0.8"))

exists, err = ss.RegistryModules.Exists(addr, cons)
if err != nil {
t.Fatal(err)
}
if !exists {
t.Fatalf("expected cached metadata to exist for %q %q", addr, cons)
}

// But it shouldn't return any module data
_, err = ss.Modules.RegistryModuleMeta(addr, cons)
if err == nil {
t.Fatal("expected module to be not found")
}
}

func TestGetModuleDataFromRegistry_apiTimeout(t *testing.T) {
Expand Down