diff --git a/internal/features/modules/hooks/module_version_test.go b/internal/features/modules/hooks/module_version_test.go index f2265430e..5aaa5ef1c 100644 --- a/internal/features/modules/hooks/module_version_test.go +++ b/internal/features/modules/hooks/module_version_test.go @@ -23,23 +23,29 @@ import ( ) var moduleVersionsMockResponse = `{ - "modules": [ - { - "source": "terraform-aws-modules/vpc/aws", - "versions": [ - { - "version": "0.0.1" - }, - { - "version": "2.0.24" - }, - { - "version": "1.33.7" - } - ] - } - ] - }` + "addr": { + "display": "azure/aks/azurerm", + "namespace": "azure", + "name": "aks", + "target": "azurerm" + }, + "description": "Terraform Module for deploying an AKS cluster", + "versions": [ + { + "id": "v9.1.0", + "published": "2024-07-04T07:12:29+01:00" + }, + { + "id": "v9.0.0", + "published": "2024-06-07T02:31:28+01:00" + }, + { + "id": "v8.0.0", + "published": "2024-03-05T07:33:07Z" + } + ], + "is_blocked": false +}` func TestHooks_RegistryModuleVersions(t *testing.T) { ctx := context.Background() @@ -67,7 +73,7 @@ func TestHooks_RegistryModuleVersions(t *testing.T) { 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" { + if r.RequestURI == "/modules/azure/aks/azurerm/index.json" { w.Write([]byte(moduleVersionsMockResponse)) return } @@ -90,7 +96,7 @@ func TestHooks_RegistryModuleVersions(t *testing.T) { ModuleCalls: map[string]tfmod.DeclaredModuleCall{ "vpc": { LocalName: "vpc", - SourceAddr: tfaddr.MustParseModuleSource("registry.terraform.io/terraform-aws-modules/vpc/aws"), + SourceAddr: tfaddr.MustParseModuleSource("registry.opentofu.org/azure/aks/azurerm"), RangePtr: &hcl.Range{ Filename: "main.tf", Start: hcl.Pos{Line: 1, Column: 1, Byte: 1}, @@ -106,26 +112,27 @@ func TestHooks_RegistryModuleVersions(t *testing.T) { expectedCandidates := []decoder.Candidate{ { - Label: `"2.0.24"`, + Label: `"9.1.0"`, Kind: lang.StringCandidateKind, - RawInsertText: `"2.0.24"`, + RawInsertText: `"9.1.0"`, SortText: " 0", }, { - Label: `"1.33.7"`, + Label: `"9.0.0"`, Kind: lang.StringCandidateKind, - RawInsertText: `"1.33.7"`, + RawInsertText: `"9.0.0"`, SortText: " 1", }, { - Label: `"0.0.1"`, + Label: `"8.0.0"`, Kind: lang.StringCandidateKind, - RawInsertText: `"0.0.1"`, + RawInsertText: `"8.0.0"`, SortText: " 2", }, } candidates, _ := h.RegistryModuleVersions(ctx, cty.StringVal("")) + fmt.Print(candidates) if diff := cmp.Diff(expectedCandidates, candidates); diff != "" { t.Fatalf("mismatched candidates: %s", diff) } diff --git a/internal/features/modules/jobs/schema.go b/internal/features/modules/jobs/schema.go index 09149593c..6eae908c0 100644 --- a/internal/features/modules/jobs/schema.go +++ b/internal/features/modules/jobs/schema.go @@ -250,15 +250,9 @@ func GetModuleDataFromRegistry(ctx context.Context, regClient registry.Client, m continue } - inputs := make([]tfregistry.Input, len(metaData.Root.Inputs)) - for i, input := range metaData.Root.Inputs { - isRequired := isRegistryModuleInputRequired(metaData.PublishedAt, input) - inputs[i] = tfregistry.Input{ - Name: input.Name, - Description: lang.Markdown(input.Description), - Required: isRequired, - } - + inputs := make([]tfregistry.Input, 0, len(metaData.Inputs)) + for name, input := range metaData.Inputs { + isRequired := input.Required inputType := cty.DynamicPseudoType if input.Type != "" { // Registry API unfortunately doesn't marshal types using @@ -270,27 +264,34 @@ func GetModuleDataFromRegistry(ctx context.Context, regClient registry.Client, m inputType = typ } } - inputs[i].Type = inputType + newInput := tfregistry.Input{ + Name: name, + Description: lang.Markdown(input.Description), + Required: isRequired, + Type: inputType, + } - if input.Default != "" { + if input.Default != nil { // Registry API unfortunately doesn't marshal values using // cty marshalers, making it lossy, so we just try to decode // on best-effort basis. - val, err := ctyjson.Unmarshal([]byte(input.Default), inputType) + val, err := ctyjson.Unmarshal([]byte(fmt.Sprintf("%v", input.Default)), inputType) if err == nil { - inputs[i].Default = val + newInput.Default = val } } + inputs = append(inputs, newInput) } - outputs := make([]tfregistry.Output, len(metaData.Root.Outputs)) - for i, output := range metaData.Root.Outputs { - outputs[i] = tfregistry.Output{ - Name: output.Name, + + outputs := make([]tfregistry.Output, 0, len(metaData.Outputs)) + for name, output := range metaData.Outputs { + outputs = append(outputs, tfregistry.Output{ + Name: name, Description: lang.Markdown(output.Description), - } + }) } - modVersion, err := version.NewVersion(metaData.Version) + modVersion, err := version.NewVersion(metaData.ID) if err != nil { errs = multierror.Append(errs, err) continue @@ -313,21 +314,3 @@ func GetModuleDataFromRegistry(ctx context.Context, regClient registry.Client, m return errs.ErrorOrNil() } - -// isRegistryModuleInputRequired checks whether the module input is required. -// It reflects the fact that modules ingested into the Registry -// may have used `default = null` (implying optional variable) which -// the Registry wasn't able to recognise until ~ 19th August 2022. -func isRegistryModuleInputRequired(publishTime time.Time, input registry.Input) bool { - fixTime := time.Date(2022, time.August, 20, 0, 0, 0, 0, time.UTC) - // Modules published after the date have "nullable" inputs - // (default = null) ingested as Required=false and Default="null". - // - // The same inputs ingested prior to the date make it impossible - // to distinguish variable with `default = null` and missing default. - if input.Required && input.Default == "" && publishTime.Before(fixTime) { - // To avoid false diagnostics, we safely assume the input is optional - return false - } - return input.Required -} diff --git a/internal/registry/module.go b/internal/registry/module.go index a33c1556f..3fb37eef0 100644 --- a/internal/registry/module.go +++ b/internal/registry/module.go @@ -45,68 +45,74 @@ type ModuleVersionDescriptor struct { Published time.Time `json:"published"` } -type ModuleVersion struct { - ID string `json:"id"` - Published time.Time `json:"published"` - Description string `json:"description"` - Downloads int `json:"downloads"` - Version string `json:"version"` -} - type ModuleDetails struct { - BaseDetails - Dependencies []ModuleDependency `json:"dependencies"` + ID string `json:"id"` + Published time.Time `json:"published"` + Readme bool `json:"readme"` + Inputs map[string]Variable `json:"variables"` + Outputs map[string]Output `json:"outputs"` Providers []ProviderDependency `json:"providers"` + Dependencies []ModuleDependency `json:"dependencies"` + Submodules map[string]Submodule `json:"submodules"` Resources []Resource `json:"resources"` } -type BaseDetails struct { - Readme bool `json:"readme"` - Variables map[string]Variable `json:"variables"` - Outputs map[string]Output `json:"outputs"` - SchemaError string `json:"schema_error"` - EditLink string `json:"edit_link"` +type Variable struct { + Type string `json:"type"` + Default interface{} `json:"default"` + Description string `json:"description"` + Required bool `json:"required"` + Sensitive bool `json:"sensitive"` } -type ModuleDependency struct { - Name string `json:"name"` - VersionConstraint string `json:"version_constraint"` - Source string `json:"source"` +type Output struct { + Description string `json:"description"` + Sensitive bool `json:"sensitive"` } type ProviderDependency struct { - Alias string `json:"alias"` Name string `json:"name"` FullName string `json:"full_name"` VersionConstraint string `json:"version_constraint"` } +type ModuleDependency struct { + Name string `json:"name"` + VersionConstraint string `json:"version_constraint"` + Source string `json:"source"` +} + type Resource struct { Address string `json:"address"` Type string `json:"type"` Name string `json:"name"` } -type Variable struct { - Type string `json:"type"` - Default interface{} `json:"default"` - Description string `json:"description"` - Required bool `json:"required"` - Sensitive bool `json:"sensitive"` +type Submodule struct { + ModuleDetails } -type Output struct { - Description string `json:"description"` - Sensitive bool `json:"sensitive"` +type Example struct { + ModuleDetails +} + +type License struct { + SPDX string `json:"spdx"` + Confidence float64 `json:"confidence"` + IsCompatible bool `json:"is_compatible"` + File string `json:"file"` + Link string `json:"link"` } +type LicenseList []License + func (c Client) GetModuleData(ctx context.Context, addr tfaddr.Module, cons version.Constraints) (*ModuleDetails, error) { v, err := c.GetMatchingModuleVersion(ctx, addr, cons) if err != nil { return nil, err } - url := fmt.Sprintf("%s/modules/%s/%s/%s/%s/index.json", + url := fmt.Sprintf("%s/modules/%s/%s/%s/v%s/index.json", c.BaseURL, addr.Package.Namespace, addr.Package.Name, diff --git a/internal/registry/module_mock_responses_test.go b/internal/registry/module_mock_responses_test.go index ddd8986a7..85ca93e8a 100644 --- a/internal/registry/module_mock_responses_test.go +++ b/internal/registry/module_mock_responses_test.go @@ -1,272 +1,207 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright (c) Gamunu Balagalla. // SPDX-License-Identifier: MPL-2.0 package registry -// moduleVersionsMockResponse represents response from https://registry.terraform.io/v1/modules/puppetlabs/deployment/ec/versions var moduleVersionsMockResponse = `{ - "modules": [ + "id": "v0.8.1", + "published": "2024-08-05T17:16:42+01:00", + "readme": true, + "edit_link": "https://github.com/azure/terraform-azurerm-alz/blob/v0.8.1/README.md", + "variables": { + "architecture_name": { + "type": "string", + "default": null, + "description": "The name of the architecture to create. This needs to be*.alz_architecture_definition.[json|yaml|yml] files.\n", + "sensitive": false, + "required": true + }, + "location": { + "type": "string", + "default": null, + "description": "The default location for resources in this management group. Used for policy managed identities.\n", + "sensitive": false, + "required": true + } + }, + "outputs": { + "management_group_resource_ids": { + "sensitive": false, + "description": "A map of management group names to their resource ids." + }, + "policy_assignment_resource_ids": { + "sensitive": false, + "description": "A map of policy assignment names to their resource ids." + } + }, + "schema_error": "", + "providers": [], + "dependencies": [ { - "source": "puppetlabs/deployment/ec", - "versions": [ - { - "version": "0.0.5", - "root": { - "providers": [ - { - "name": "ec", - "namespace": "", - "source": "elastic/ec", - "version": "0.2.1" - } - ], - "dependencies": [] - }, - "submodules": [] - }, - { - "version": "0.0.6", - "root": { - "providers": [ - { - "name": "ec", - "namespace": "", - "source": "elastic/ec", - "version": "0.2.1" - } - ], - "dependencies": [] - }, - "submodules": [] - }, - { - "version": "0.0.8", - "root": { - "providers": [ - { - "name": "ec", - "namespace": "", - "source": "elastic/ec", - "version": "0.2.1" - } - ], - "dependencies": [] - }, - "submodules": [] - }, - { - "version": "0.0.2", - "root": { - "providers": [ - { - "name": "ec", - "namespace": "", - "source": "elastic/ec", - "version": "0.2.1" - } - ], - "dependencies": [] - }, - "submodules": [] - }, - { - "version": "0.0.1", - "root": { - "providers": [], - "dependencies": [] - }, - "submodules": [ - { - "path": "modules/ec-deployment", - "providers": [ - { - "name": "ec", - "namespace": "", - "source": "elastic/ec", - "version": "0.2.1" - } - ], - "dependencies": [] - } - ] + "name": "policy_assignment", + "version_constraint": "", + "source": "./modules/azapi_helper" + }, + { + "name": "policy_definitions", + "version_constraint": "", + "source": "./modules/azapi_helper" + } + ], + "resources": [ + { + "address": "modtm_telemetry.telemetry", + "type": "modtm_telemetry", + "name": "telemetry" + }, + { + "address": "random_uuid.telemetry", + "type": "random_uuid", + "name": "telemetry" + } + ], + "link": "https://github.com/azure/terraform-azurerm-alz/tree/v0.8.1", + "vcs_repository": "", + "licenses": [ + { + "spdx": "MIT", + "confidence": 1, + "is_compatible": true, + "file": "LICENSE", + "link": "https://github.com/azure/terraform-azurerm-alz/blob/v0.8.1/LICENSE" + } + ], + "incompatible_license": false, + "examples": { + "default": { + "readme": true, + "edit_link": "https://github.com/azure/terraform-azurerm-alz/blob/v0.8.1/examples/default/README.md", + "variables": {}, + "outputs": {}, + "schema_error": "" + }, + "policy-assignment-modification-with-custom-lib": { + "readme": true, + "edit_link": "https://github.com/azure/terraform-azurerm-alz/blob/v0.8.1/examples/policy-assignment-modification-with-custom-lib/README.md", + "variables": {}, + "outputs": {}, + "schema_error": "" + } + }, + "submodules": { + "azapi_helper": { + "readme": true, + "edit_link": "https://github.com/azure/terraform-azurerm-alz/blob/v0.8.1/modules/azapi_helper/README.md", + "variables": { + "body": { + "type": "dynamic", + "default": null, + "description": "The body object of the resource.", + "sensitive": false, + "required": true }, - { - "version": "0.0.4", - "root": { - "providers": [ - { - "name": "ec", - "namespace": "", - "source": "elastic/ec", - "version": "0.2.1" - } - ], - "dependencies": [] - }, - "submodules": [] + "name": { + "type": "string", + "default": null, + "description": "The name of resource.", + "sensitive": false, + "required": true + } + }, + "outputs": { + "identity": { + "sensitive": false, + "description": "The identity configuration of the resource." }, + "name": { + "sensitive": false, + "description": "The name of the resource." + } + }, + "schema_error": "", + "providers": [], + "dependencies": [], + "resources": [ { - "version": "0.0.3", - "root": { - "providers": [ - { - "name": "ec", - "namespace": "", - "source": "elastic/ec", - "version": "0.2.1" - } - ], - "dependencies": [] - }, - "submodules": [] + "address": "azapi_resource.this", + "type": "azapi_resource", + "name": "this" }, { - "version": "0.0.7", - "root": { - "providers": [ - { - "name": "ec", - "namespace": "", - "source": "elastic/ec", - "version": "0.2.1" - } - ], - "dependencies": [] - }, - "submodules": [] + "address": "terraform_data.replace_trigger", + "type": "terraform_data", + "name": "replace_trigger" } ] } - ] + } }` -// moduleDataMockResponse represents response from https://registry.terraform.io/v1/modules/puppetlabs/deployment/ec/0.0.8 var moduleDataMockResponse = `{ - "id": "puppetlabs/deployment/ec/0.0.8", - "owner": "mattkirby", - "namespace": "puppetlabs", - "name": "deployment", - "version": "0.0.8", - "provider": "ec", - "provider_logo_url": "/images/providers/generic.svg?2", - "description": "", - "source": "https://github.com/puppetlabs/terraform-ec-deployment", - "tag": "v0.0.8", - "published_at": "2021-08-05T00:26:33.501756Z", - "downloads": 3059237, - "verified": false, - "root": { - "path": "", - "name": "deployment", - "readme": "# EC project Terraform module\n\nTerraform module which creates a Elastic Cloud project.\n\n## Usage\n\nDetails coming soon\n", - "empty": false, - "inputs": [ - { - "name": "autoscale", - "type": "string", - "description": "Enable autoscaling of elasticsearch", - "default": "\"true\"", - "required": false - }, - { - "name": "ec_stack_version", - "type": "string", - "description": "Version of Elastic Cloud stack to deploy", - "default": "\"\"", - "required": false - }, - { - "name": "name", - "type": "string", - "description": "Name of resources", - "default": "\"ecproject\"", - "required": false - }, - { - "name": "traffic_filter_sourceip", - "type": "string", - "description": "traffic filter source IP", - "default": "\"\"", - "required": false - }, - { - "name": "ec_region", - "type": "string", - "description": "cloud provider region", - "default": "\"gcp-us-west1\"", - "required": false - }, - { - "name": "deployment_templateid", - "type": "string", - "description": "ID of Elastic Cloud deployment type", - "default": "\"gcp-io-optimized\"", - "required": false - } - ], - "outputs": [ - { - "name": "elasticsearch_password", - "description": "elasticsearch password" - }, - { - "name": "deployment_id", - "description": "Elastic Cloud deployment ID" - }, - { - "name": "elasticsearch_version", - "description": "Stack version deployed" - }, - { - "name": "elasticsearch_cloud_id", - "description": "Elastic Cloud project deployment ID" - }, - { - "name": "elasticsearch_https_endpoint", - "description": "elasticsearch https endpoint" - }, - { - "name": "elasticsearch_username", - "description": "elasticsearch username" - } - ], - "dependencies": [], - "provider_dependencies": [ - { - "name": "ec", - "namespace": "elastic", - "source": "elastic/ec", - "version": "0.2.1" - } - ], - "resources": [ - { - "name": "ecproject", - "type": "ec_deployment" - }, - { - "name": "gcp_vpc_nat", - "type": "ec_deployment_traffic_filter" - }, - { - "name": "ec_tf_association", - "type": "ec_deployment_traffic_filter_association" - } - ] + "addr": { + "display": "azure/alz/azurerm", + "namespace": "azure", + "name": "alz", + "target": "azurerm" }, - "submodules": [], - "examples": [], - "providers": [ - "ec" - ], + "description": "Terraform module to deploy Azure Landing Zones", "versions": [ - "0.0.1", - "0.0.2", - "0.0.3", - "0.0.4", - "0.0.5", - "0.0.6", - "0.0.7", - "0.0.8" - ] + { + "id": "v0.8.1", + "published": "2024-08-05T17:16:42+01:00" + }, + { + "id": "v0.8.0", + "published": "2024-07-10T16:41:49+01:00" + }, + { + "id": "v0.7.0", + "published": "2024-07-08T17:55:56+01:00" + }, + { + "id": "v0.6.0", + "published": "2024-03-13T12:31:59Z" + }, + { + "id": "v0.5.0", + "published": "2024-03-08T10:29:31Z" + }, + { + "id": "v0.4.1", + "published": "2023-11-06T21:35:38Z" + }, + { + "id": "v0.4.0", + "published": "2023-11-06T21:26:06Z" + }, + { + "id": "v0.3.3", + "published": "2023-11-02T17:22:57Z" + }, + { + "id": "v0.3.2", + "published": "2023-11-02T10:44:07Z" + }, + { + "id": "v0.3.1", + "published": "2023-10-26T14:08:19+01:00" + }, + { + "id": "v0.3.0", + "published": "2023-10-09T20:19:28+01:00" + }, + { + "id": "v0.2.0", + "published": "2023-10-06T16:56:28+01:00" + }, + { + "id": "v0.1.1", + "published": "2023-08-08T17:26:02+01:00" + }, + { + "id": "v0.1.0", + "published": "2023-08-08T15:43:59+01:00" + } + ], + "is_blocked": false }` diff --git a/internal/registry/module_test.go b/internal/registry/module_test.go index 690c1189d..e951139c4 100644 --- a/internal/registry/module_test.go +++ b/internal/registry/module_test.go @@ -1,3 +1,5 @@ +// Copyright (c) Gamunu Balagalla. +// SPDX-License-Identifier: MPL-2.0 // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 @@ -19,25 +21,24 @@ import ( func TestGetModuleData(t *testing.T) { ctx := context.Background() - addr, err := tfaddr.ParseModuleSource("puppetlabs/deployment/ec") + addr, err := tfaddr.ParseModuleSource("azure/alz/azurerm") if err != nil { t.Fatal(err) } - cons := version.MustConstraints(version.NewConstraint("0.0.8")) + cons := version.MustConstraints(version.NewConstraint("0.8.1")) client := NewClient() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.RequestURI == "/v1/modules/puppetlabs/deployment/ec/versions" { - w.Write([]byte(moduleVersionsMockResponse)) - return - } - if r.RequestURI == "/v1/modules/puppetlabs/deployment/ec/0.0.8" { + switch r.RequestURI { + case "/modules/azure/alz/azurerm/index.json": w.Write([]byte(moduleDataMockResponse)) - return + case "/modules/azure/alz/azurerm/v0.8.1/index.json": + w.Write([]byte(moduleVersionsMockResponse)) + default: + http.Error(w, fmt.Sprintf("unexpected request: %q", r.RequestURI), 400) } - http.Error(w, fmt.Sprintf("unexpected request: %q", r.RequestURI), 400) })) client.BaseURL = srv.URL t.Cleanup(srv.Close) @@ -46,82 +47,103 @@ func TestGetModuleData(t *testing.T) { if err != nil { t.Fatal(err) } - expectedData := &ModuleResponse{ - Version: "0.0.8", - PublishedAt: time.Date(2021, time.August, 5, 0, 26, 33, 501756000, time.UTC), - Root: ModuleRoot{ - Inputs: []Input{ - { - Name: "autoscale", - Type: "string", - Description: "Enable autoscaling of elasticsearch", - Default: "\"true\"", - Required: false, - }, - { - Name: "ec_stack_version", - Type: "string", - Description: "Version of Elastic Cloud stack to deploy", - Default: "\"\"", - Required: false, - }, - { - Name: "name", - Type: "string", - Description: "Name of resources", - Default: "\"ecproject\"", - Required: false, - }, - { - Name: "traffic_filter_sourceip", - Type: "string", - Description: "traffic filter source IP", - Default: "\"\"", - Required: false, - }, - { - Name: "ec_region", - Type: "string", - Description: "cloud provider region", - Default: "\"gcp-us-west1\"", - Required: false, - }, - { - Name: "deployment_templateid", - Type: "string", - Description: "ID of Elastic Cloud deployment type", - Default: "\"gcp-io-optimized\"", - Required: false, - }, + + expectedData := &ModuleDetails{ + ID: "v0.8.1", + Published: time.Date(2024, time.August, 5, 17, 16, 42, 0, time.FixedZone("", 3600)), + Readme: true, + Inputs: map[string]Variable{ + "architecture_name": { + Type: "string", + Description: "The name of the architecture to create. This needs to be*.alz_architecture_definition.[json|yaml|yml] files.\n", + Required: true, }, - Outputs: []Output{ - { - Name: "elasticsearch_password", - Description: "elasticsearch password", - }, - { - Name: "deployment_id", - Description: "Elastic Cloud deployment ID", - }, - { - Name: "elasticsearch_version", - Description: "Stack version deployed", - }, - { - Name: "elasticsearch_cloud_id", - Description: "Elastic Cloud project deployment ID", - }, - { - Name: "elasticsearch_https_endpoint", - Description: "elasticsearch https endpoint", - }, - { - Name: "elasticsearch_username", - Description: "elasticsearch username", + "location": { + Type: "string", + Description: "The default location for resources in this management group. Used for policy managed identities.\n", + Required: true, + }, + }, + Outputs: map[string]Output{ + "management_group_resource_ids": { + Description: "A map of management group names to their resource ids.", + Sensitive: false, + }, + "policy_assignment_resource_ids": { + Description: "A map of policy assignment names to their resource ids.", + Sensitive: false, + }, + }, + Providers: []ProviderDependency{}, + Dependencies: []ModuleDependency{ + { + Name: "policy_assignment", + VersionConstraint: "", + Source: "./modules/azapi_helper", + }, + { + Name: "policy_definitions", + VersionConstraint: "", + Source: "./modules/azapi_helper", + }, + }, + Resources: []Resource{ + { + Address: "modtm_telemetry.telemetry", + Type: "modtm_telemetry", + Name: "telemetry", + }, + { + Address: "random_uuid.telemetry", + Type: "random_uuid", + Name: "telemetry", + }, + }, + Submodules: map[string]Submodule{ + "azapi_helper": { + ModuleDetails: ModuleDetails{ + Readme: true, + Inputs: map[string]Variable{ + "body": { + Type: "dynamic", + Description: "The body object of the resource.", + Required: true, + }, + "name": { + Type: "string", + Description: "The name of resource.", + Required: true, + }, + }, + Outputs: map[string]Output{ + "identity": { + Description: "The identity configuration of the resource.", + Sensitive: false, + }, + "name": { + Description: "The name of the resource.", + Sensitive: false, + }, + }, + Providers: []ProviderDependency{}, + Dependencies: []ModuleDependency{}, + Resources: []Resource{ + { + Address: "azapi_resource.this", + Type: "azapi_resource", + Name: "this", + }, + { + Address: "terraform_data.replace_trigger", + Type: "terraform_data", + Name: "replace_trigger", + }, + }, }, }, }, } + if diff := cmp.Diff(expectedData, data); diff != "" { t.Fatalf("mismatched data: %s", diff) } @@ -129,16 +151,16 @@ func TestGetModuleData(t *testing.T) { func TestGetMatchingModuleVersion(t *testing.T) { ctx := context.Background() - addr, err := tfaddr.ParseModuleSource("puppetlabs/deployment/ec") + addr, err := tfaddr.ParseModuleSource("azure/alz/azurerm") if err != nil { t.Fatal(err) } - cons := version.MustConstraints(version.NewConstraint(">=0.0.7")) + cons := version.MustConstraints(version.NewConstraint(">=0.7.0")) client := NewClient() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.RequestURI == "/v1/modules/puppetlabs/deployment/ec/versions" { - w.Write([]byte(moduleVersionsMockResponse)) + if r.RequestURI == "/modules/azure/alz/azurerm/index.json" { + w.Write([]byte(moduleDataMockResponse)) return } http.Error(w, fmt.Sprintf("unexpected request: %q", r.RequestURI), 400) @@ -151,28 +173,73 @@ func TestGetMatchingModuleVersion(t *testing.T) { t.Fatal(err) } - expectedVersion := version.Must(version.NewVersion("0.0.8")) + expectedVersion := version.Must(version.NewVersion("v0.8.1")) if !expectedVersion.Equal(v) { t.Fatalf("expected version: %s, given: %s", expectedVersion, v) } } +func TestGetModuleVersions(t *testing.T) { + ctx := context.Background() + addr, err := tfaddr.ParseModuleSource("azure/alz/azurerm") + if err != nil { + t.Fatal(err) + } + client := NewClient() + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.RequestURI == "/modules/azure/alz/azurerm/index.json" { + w.Write([]byte(moduleDataMockResponse)) + return + } + http.Error(w, fmt.Sprintf("unexpected request: %q", r.RequestURI), 400) + })) + client.BaseURL = srv.URL + t.Cleanup(srv.Close) + + versions, err := client.GetModuleVersions(ctx, addr) + if err != nil { + t.Fatal(err) + } + + expectedVersions := version.Collection{ + version.Must(version.NewVersion("0.8.1")), + version.Must(version.NewVersion("0.8.0")), + version.Must(version.NewVersion("0.7.0")), + version.Must(version.NewVersion("0.6.0")), + version.Must(version.NewVersion("0.5.0")), + version.Must(version.NewVersion("0.4.1")), + version.Must(version.NewVersion("0.4.0")), + version.Must(version.NewVersion("0.3.3")), + version.Must(version.NewVersion("0.3.2")), + version.Must(version.NewVersion("0.3.1")), + version.Must(version.NewVersion("0.3.0")), + version.Must(version.NewVersion("0.2.0")), + version.Must(version.NewVersion("0.1.1")), + version.Must(version.NewVersion("0.1.0")), + } + + if diff := cmp.Diff(expectedVersions, versions); diff != "" { + t.Fatalf("mismatched versions: %s", diff) + } +} + func TestCancellationThroughContext(t *testing.T) { ctx := context.Background() ctx, cancelFunc := context.WithTimeout(ctx, 50*time.Millisecond) t.Cleanup(cancelFunc) - addr, err := tfaddr.ParseModuleSource("puppetlabs/deployment/ec") + addr, err := tfaddr.ParseModuleSource("azure/alz/azurerm") if err != nil { t.Fatal(err) } - cons := version.MustConstraints(version.NewConstraint(">=0.0.7")) + cons := version.MustConstraints(version.NewConstraint(">=0.7.0")) client := NewClient() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - time.Sleep(500 * time.Millisecond) - if r.RequestURI == "/v1/modules/puppetlabs/deployment/ec/versions" { - w.Write([]byte(moduleVersionsMockResponse)) + time.Sleep(500 * time.Millisecond) // Delay longer than the context timeout + if r.RequestURI == "/modules/azure/alz/azurerm/index.json" { + w.Write([]byte(moduleDataMockResponse)) return } http.Error(w, fmt.Sprintf("unexpected request: %q", r.RequestURI), 400) @@ -181,12 +248,28 @@ func TestCancellationThroughContext(t *testing.T) { t.Cleanup(srv.Close) _, err = client.GetMatchingModuleVersion(ctx, addr, cons) - e, ok := err.(*url.Error) + if err == nil { + t.Fatal("expected error due to context cancellation, got nil") + } + + urlErr, ok := err.(*url.Error) if !ok { - t.Fatalf("expected error, got %#v", err) + t.Fatalf("expected *url.Error, got: %T", err) + } + + if urlErr.Err != context.DeadlineExceeded { + t.Fatalf("expected context.DeadlineExceeded error, got: %v", urlErr.Err) + } +} + +func TestClientError(t *testing.T) { + err := ClientError{ + StatusCode: 404, + Body: "Not Found", } - if e.Err != context.DeadlineExceeded { - t.Fatalf("expected error: %#v, given: %#v", context.DeadlineExceeded, e.Err) + expected := "404: Not Found" + if err.Error() != expected { + t.Fatalf("expected error message: %s, got: %s", expected, err.Error()) } } diff --git a/internal/registry/provider_test.go b/internal/registry/provider_test.go index b83d25fab..9d3c7dae1 100644 --- a/internal/registry/provider_test.go +++ b/internal/registry/provider_test.go @@ -1,4 +1,4 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright (c) Gamunu Balagalla. // SPDX-License-Identifier: MPL-2.0 package registry @@ -8,6 +8,7 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/google/go-cmp/cmp" ) @@ -16,252 +17,94 @@ func TestListProviders(t *testing.T) { client := NewClient() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.RequestURI == "/v2/providers?page[size]=2&filter[tier]=official&page[number]=1" { + if r.RequestURI == "/providers/index.json" { w.Write([]byte(`{ - "data": [ + "providers": [ { - "type": "providers", - "id": "315", - "attributes": { - "alias": "azuread", - "description": "Manage users, groups, service principals, and applications in Azure Active Directory using the Microsoft Graph API. This provider is maintained by the Azure providers team at HashiCorp.", - "downloads": 46684900, - "featured": false, - "full-name": "hashicorp/azuread", - "logo-url": "/images/providers/azure.svg?3", - "name": "azuread", + "addr": { + "display": "hashicorp/azuread", "namespace": "hashicorp", - "owner-name": "", - "robots-noindex": false, - "source": "https://github.com/hashicorp/terraform-provider-azuread", - "tier": "official", - "unlisted": false, - "warning": "" + "name": "azuread" }, - "links": { - "self": "/v2/providers/315" - } + "description": "Manage Azure Active Directory resources", + "popularity": 1000, + "fork_count": 50, + "versions": [ + { + "id": "0.36.1", + "published": "2022-08-24T19:09:29Z" + } + ], + "is_blocked": false }, { - "type": "providers", - "id": "378", - "attributes": { - "alias": "http", - "description": "Utility provider for interacting with generic HTTP servers as part of a Terraform configuration.", - "downloads": 23888754, - "featured": false, - "full-name": "hashicorp/http", - "logo-url": "/images/providers/hashicorp.svg", - "name": "http", + "addr": { + "display": "hashicorp/http", "namespace": "hashicorp", - "owner-name": "", - "robots-noindex": false, - "source": "https://github.com/hashicorp/terraform-provider-http", - "tier": "official", - "unlisted": false, - "warning": "" + "name": "http" }, - "links": { - "self": "/v2/providers/378" - } + "description": "Interact with HTTP servers", + "popularity": 500, + "fork_count": 20, + "versions": [ + { + "id": "3.2.1", + "published": "2022-06-21T18:56:56Z" + } + ], + "is_blocked": false } - ], - "links": { - "first": "/v2/providers?filter%5Btier%5D=official&page%5Bnumber%5D=1&page%5Bsize%5D=2", - "last": "/v2/providers?filter%5Btier%5D=official&page%5Bnumber%5D=2&page%5Bsize%5D=2", - "next": "/v2/providers?filter%5Btier%5D=official&page%5Bnumber%5D=2&page%5Bsize%5D=2", - "prev": null - }, - "meta": { - "pagination": { - "page-size": 2, - "current-page": 1, - "next-page": 2, - "prev-page": null, - "total-pages": 2, - "total-count": 3 - } - } -} -`)) - return - } - if r.RequestURI == "/v2/providers?page[size]=2&filter[tier]=official&page[number]=2" { - w.Write([]byte(`{ - "data": [ - { - "type": "providers", - "id": "370", - "attributes": { - "alias": "tfe", - "description": "Provision Terraform Cloud or Terraform Enterprise - with Terraform! Management of organizations, workspaces, teams, variables, run triggers, policy sets, and more. Maintained by the Terraform Cloud team at HashiCorp.", - "downloads": 8555685, - "featured": false, - "full-name": "hashicorp/tfe", - "logo-url": "/images/providers/terraform.svg?3", - "name": "tfe", - "namespace": "hashicorp", - "owner-name": "", - "robots-noindex": false, - "source": "https://github.com/hashicorp/terraform-provider-tfe", - "tier": "official", - "unlisted": false, - "warning": "" - }, - "links": { - "self": "/v2/providers/370" - } - } - ], - "links": { - "first": "/v2/providers?filter%5Btier%5D=official&page%5Bnumber%5D=1&page%5Bsize%5D=2", - "last": "/v2/providers?filter%5Btier%5D=official&page%5Bnumber%5D=2&page%5Bsize%5D=2", - "next": null, - "prev": "/v2/providers?filter%5Btier%5D=official&page%5Bnumber%5D=1&page%5Bsize%5D=2" - }, - "meta": { - "pagination": { - "page-size": 2, - "current-page": 1, - "next-page": null, - "prev-page": 1, - "total-pages": 2, - "total-count": 3 - } - } -} -`)) + ] +}`)) return } http.Error(w, fmt.Sprintf("unexpected request: %q", r.RequestURI), 400) })) client.BaseURL = srv.URL - client.ProviderPageSize = 2 t.Cleanup(srv.Close) - providers, err := client.ListProviders("official") + providers, err := client.ListProviders() if err != nil { t.Fatal(err) } expectedProviders := []Provider{ { - ID: "315", - Attributes: ProviderAttributes{ + Addr: ProviderAddr{ + Display: "hashicorp/azuread", Name: "azuread", Namespace: "hashicorp", }, + Description: "Manage Azure Active Directory resources", + Popularity: 1000, + ForkCount: 50, + Versions: []ProviderVersion{ + { + ID: "0.36.1", + Published: time.Date(2022, 8, 24, 19, 9, 29, 0, time.UTC), + }, + }, + IsBlocked: false, }, { - ID: "378", - Attributes: ProviderAttributes{ + Addr: ProviderAddr{ + Display: "hashicorp/http", Name: "http", Namespace: "hashicorp", }, - }, - { - ID: "370", - Attributes: ProviderAttributes{ - Name: "tfe", - Namespace: "hashicorp", + Description: "Interact with HTTP servers", + Popularity: 500, + ForkCount: 20, + Versions: []ProviderVersion{ + { + ID: "3.2.1", + Published: time.Date(2022, 6, 21, 18, 56, 56, 0, time.UTC), + }, }, + IsBlocked: false, }, } if diff := cmp.Diff(expectedProviders, providers); diff != "" { t.Fatalf("unexpected providers: %s", diff) } } - -func TestGetLatestProviderVersion(t *testing.T) { - client := NewClient() - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.RequestURI == "/v2/providers/370/provider-versions/latest?include=provider-platforms" { - w.Write([]byte(`{ - "data": { - "type": "provider-versions", - "id": "27307", - "attributes": { - "description": "terraform-provider-tfe", - "downloads": 243313, - "published-at": "2022-08-24T19:09:29Z", - "tag": "v0.36.1", - "version": "0.36.1" - }, - "relationships": { - "platforms": { - "data": [ - { - "type": "provider-platforms", - "id": "287323" - }, - { - "type": "provider-platforms", - "id": "287326" - } - ] - } - }, - "links": { - "self": "/v2/provider-versions/27307" - } - }, - "included": [ - { - "type": "provider-platforms", - "id": "287327", - "attributes": { - "arch": "arm", - "downloads": 237, - "os": "freebsd" - } - }, - { - "type": "provider-platforms", - "id": "287324", - "attributes": { - "arch": "arm64", - "downloads": 2481, - "os": "darwin" - } - } - ] -}`)) - return - } - })) - client.BaseURL = srv.URL - t.Cleanup(srv.Close) - - resp, err := client.GetLatestProviderVersion("370") - if err != nil { - t.Fatal(err) - } - - expectedResponse := &ProviderVersionResponse{ - Data: ProviderVersionData{ - Attributes: ProviderVersionAttributes{ - Version: "0.36.1", - }, - }, - Included: []Included{ - { - Type: "provider-platforms", - Attributes: IncludedAttributes{ - Arch: "arm", - Os: "freebsd", - }, - }, - { - Type: "provider-platforms", - Attributes: IncludedAttributes{ - Arch: "arm64", - Os: "darwin", - }, - }, - }, - } - if diff := cmp.Diff(expectedResponse, resp); diff != "" { - t.Fatalf("unexpected response: %s", diff) - } -}