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

schema: Introduce CoreModuleSchemaForConstraint #129

Merged
merged 5 commits into from
Aug 9, 2022
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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.14

require (
github.com/google/go-cmp v0.5.8
github.com/hashicorp/go-cleanhttp v0.5.1
github.com/hashicorp/go-version v1.6.0
github.com/hashicorp/hcl-lang v0.0.0-20220801150536-118ac453e267
github.com/hashicorp/hcl/v2 v2.13.0
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
Expand Down
28 changes: 13 additions & 15 deletions internal/schema/0.14/provisioners.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,19 @@ func ConnectionDependentBodies(v *version.Version) map[schema.SchemaKey]*schema.
return v013_mod.ConnectionDependentBodies(v)
}

func ProvisionerDependentBodies(v *version.Version) map[schema.SchemaKey]*schema.BodySchema {
return map[schema.SchemaKey]*schema.BodySchema{
labelKey("file"): FileProvisioner,
labelKey("local-exec"): LocalExecProvisioner,
labelKey("remote-exec"): RemoteExecProvisioner,

// Vendor provisioners are deprecated in 0.13.4+
// See https://discuss.hashicorp.com/t/notice-terraform-to-begin-deprecation-of-vendor-tool-specific-provisioners-starting-in-terraform-0-13-4/13997
// Some of these provisioners have complex schemas
// but we can at least helpfully list their names
labelKey("chef"): {IsDeprecated: true},
labelKey("salt-masterless"): {IsDeprecated: true},
labelKey("habitat"): {IsDeprecated: true},
labelKey("puppet"): {IsDeprecated: true},
}
var ProvisionerDependentBodies = map[schema.SchemaKey]*schema.BodySchema{
labelKey("file"): FileProvisioner,
labelKey("local-exec"): LocalExecProvisioner,
labelKey("remote-exec"): RemoteExecProvisioner,

// Vendor provisioners are deprecated in 0.13.4+
// See https://discuss.hashicorp.com/t/notice-terraform-to-begin-deprecation-of-vendor-tool-specific-provisioners-starting-in-terraform-0-13-4/13997
// Some of these provisioners have complex schemas
// but we can at least helpfully list their names
labelKey("chef"): {IsDeprecated: true},
labelKey("salt-masterless"): {IsDeprecated: true},
labelKey("habitat"): {IsDeprecated: true},
labelKey("puppet"): {IsDeprecated: true},
}

func labelKey(value string) schema.SchemaKey {
Expand Down
2 changes: 1 addition & 1 deletion internal/schema/0.14/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func ModuleSchema(v *version.Version) *schema.BodySchema {

bs.Blocks["variable"] = variableBlockSchema
bs.Blocks["terraform"] = terraformBlockSchema(v)
bs.Blocks["resource"].Body.Blocks["provisioner"].DependentBody = ProvisionerDependentBodies(v)
bs.Blocks["resource"].Body.Blocks["provisioner"].DependentBody = ProvisionerDependentBodies

return bs
}
2 changes: 1 addition & 1 deletion internal/schema/0.15/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

func ModuleSchema(v *version.Version) *schema.BodySchema {
bs := v014_mod.ModuleSchema(v)
bs.Blocks["terraform"] = patchTerraformBlockSchema(bs.Blocks["terraform"], v)
bs.Blocks["terraform"] = patchTerraformBlockSchema(bs.Blocks["terraform"])
bs.Blocks["resource"].Body.Blocks["provisioner"].DependentBody = ProvisionerDependentBodies(v)
bs.Blocks["resource"].Body.Blocks["connection"].DependentBody = ConnectionDependentBodies(v)

Expand Down
3 changes: 1 addition & 2 deletions internal/schema/0.15/terraform.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package schema

import (
"github.com/hashicorp/go-version"
"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/hcl-lang/schema"
"github.com/hashicorp/terraform-schema/internal/schema/refscope"
"github.com/zclconf/go-cty/cty"
)

func patchTerraformBlockSchema(bs *schema.BlockSchema, v *version.Version) *schema.BlockSchema {
func patchTerraformBlockSchema(bs *schema.BlockSchema) *schema.BlockSchema {
bs.Body.Blocks["required_providers"].Body = &schema.BodySchema{
AnyAttribute: &schema.AttributeSchema{
Expr: schema.ExprConstraints{
Expand Down
2 changes: 1 addition & 1 deletion internal/schema/1.1/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ import (
func ModuleSchema(v *version.Version) *schema.BodySchema {
bs := v015_mod.ModuleSchema(v)
bs.Blocks["moved"] = movedBlockSchema
bs.Blocks["terraform"] = patchTerraformBlockSchema(bs.Blocks["terraform"], v)
bs.Blocks["terraform"] = patchTerraformBlockSchema(bs.Blocks["terraform"])
return bs
}
3 changes: 1 addition & 2 deletions internal/schema/1.1/terraform.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package schema

import (
"github.com/hashicorp/go-version"
"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/hcl-lang/schema"
"github.com/zclconf/go-cty/cty"
)

func patchTerraformBlockSchema(bs *schema.BlockSchema, v *version.Version) *schema.BlockSchema {
func patchTerraformBlockSchema(bs *schema.BlockSchema) *schema.BlockSchema {
bs.Body.Blocks["cloud"] = &schema.BlockSchema{
Description: lang.PlainText("Terraform Cloud configuration"),
MaxItems: 1,
Expand Down
2 changes: 0 additions & 2 deletions internal/schema/1.2/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import (
v1_1_mod "github.com/hashicorp/terraform-schema/internal/schema/1.1"
)

var v1_2 = version.Must(version.NewVersion("1.2.0"))

func ModuleSchema(v *version.Version) *schema.BodySchema {
bs := v1_1_mod.ModuleSchema(v)
bs.Blocks["data"].Body.Blocks = map[string]*schema.BlockSchema{
Expand Down
148 changes: 148 additions & 0 deletions internal/versiongen/gen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package main

import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"net/url"
"os"
"sort"
"text/template"
"time"

"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-version"
)

var baseURL = "https://api.releases.hashicorp.com/v1"

type release struct {
Version *version.Version `json:"version"`
Created *time.Time `json:"timestamp_created"`
}

func main() {
var writePath string
flag.StringVar(&writePath, "w", "", "Path to write to")
flag.Parse()

output := os.Stdout
if writePath != "" {
f, err := os.OpenFile(writePath, os.O_RDWR|os.O_CREATE, 0o755)
if err != nil {
log.Fatal(err)
}
output = f
}

releases, err := GetTerraformReleases()
if err != nil {
log.Fatal(err)
}

sort.SliceStable(releases, func(i, j int) bool {
return releases[i].Version.GreaterThan(releases[j].Version)
})

outputTpl := `// Code generated by "versiongen"; DO NOT EDIT.
package schema

import (
"github.com/hashicorp/go-version"
)

var (
OldestAvailableVersion = version.Must(version.NewVersion("{{ .OldestVersion }}"))
LatestAvailableVersion = version.Must(version.NewVersion("{{ .LatestVersion }}"))

terraformVersions = version.Collection{
{{- range .Releases }}
version.Must(version.NewVersion("{{ .Version }}")),
{{- end }}
}
)
`
tpl, err := template.New("output").Parse(outputTpl)
if err != nil {
log.Fatal(err)
}

type data struct {
Releases []release
OldestVersion *version.Version
LatestVersion *version.Version
}

// we keep this hard-coded to 0.12 since
// we don't have schema for older versions
oldestVersion := version.Must(version.NewVersion("0.12.0"))

err = tpl.Execute(output, data{
Releases: releases,
LatestVersion: releases[0].Version,
OldestVersion: oldestVersion,
})
if err != nil {
log.Fatal(err)
}
}

func GetTerraformReleases() ([]release, error) {
releases := make([]release, 0)

var after *time.Time
for {
r, err := getTerraformReleasesAfter(after)
if err != nil {
return releases, err
}
if len(r) == 0 {
break
}

releases = append(releases, r...)
after = r[len(r)-1].Created
}

return releases, nil
}

func getTerraformReleasesAfter(after *time.Time) ([]release, error) {
u, err := url.Parse(fmt.Sprintf("%s/releases/%s", baseURL, "terraform"))
if err != nil {
return nil, err
}

params := u.Query()
params.Set("limit", "20")
if after != nil {
params.Set("after", after.Format(time.RFC3339))
}
u.RawQuery = params.Encode()

client := cleanhttp.DefaultClient()
log.Printf("calling %q", u.String())
resp, err := client.Get(u.String())
if err != nil {
return nil, err
}

if resp.StatusCode != 200 {
return nil, fmt.Errorf("server returned %q", resp.Status)
}

b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

var releases []release
err = json.Unmarshal(b, &releases)
if err != nil {
return nil, err
}

return releases, nil
}
34 changes: 34 additions & 0 deletions internal/versiongen/gen_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package main

import (
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/go-version"
)

func TestGetTerraformReleases(t *testing.T) {
releases, err := GetTerraformReleases()
if err != nil {
t.Fatal(err)
}

minExpectedLength := 234
if minExpectedLength < len(releases) {
t.Fatalf("expected >= %d releases, %d given", minExpectedLength, len(releases))
}

// The oldest release should really be 0.1.0. We're however getting
// releases sorted by dates and those dates were backfilled as part
// of some older data migrations where the original dates were lost.
expectedDate := time.Date(2017, 3, 1, 17, 36, 49, 0, time.UTC)
expectedOldestRelease := release{
Version: version.Must(version.NewVersion("0.6.4")),
Created: &expectedDate,
}
oldestRelease := releases[len(releases)-1]
if diff := cmp.Diff(expectedOldestRelease, oldestRelease); diff != "" {
t.Fatalf("unexpected oldest release: %s", diff)
}
}
13 changes: 12 additions & 1 deletion schema/core_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,18 @@ func CoreModuleSchemaForVersion(v *version.Version) (*schema.BodySchema, error)
return mod_v0_12.ModuleSchema(ver), nil
}

return nil, fmt.Errorf("no compatible schema found for %s", v.String())
return nil, NoCompatibleSchemaErr{Version: ver}
}

//go:generate go run ../internal/versiongen -w ./versions_gen.go
func CoreModuleSchemaForConstraint(vc version.Constraints) (*schema.BodySchema, error) {
for _, v := range terraformVersions {
if vc.Check(v) {
return CoreModuleSchemaForVersion(v)
}
}

return nil, NoCompatibleSchemaErr{Constraints: vc}
}

func semVer(ver *version.Version) (*version.Version, error) {
Expand Down
49 changes: 49 additions & 0 deletions schema/core_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,53 @@ func TestCoreModuleSchemaForVersion_matching(t *testing.T) {
}
}

func TestCoreModuleSchemaForConstraint(t *testing.T) {
testCases := []struct {
constraint version.Constraints
matchedSchema *schema.BodySchema
expectedErr error
}{
{
version.MustConstraints(version.NewConstraint(">= 0.12, < 0.13")),
mod_v0_12.ModuleSchema(version.Must(version.NewVersion("0.12.31"))),
nil,
},
{
version.Constraints{},
mod_v1_2.ModuleSchema(version.Must(version.NewVersion("1.3.0"))),
nil,
},
{
version.MustConstraints(version.NewConstraint("< 0.12")),
nil,
fmt.Errorf("no compatible schema found for 0.11.15"),
},
{
version.MustConstraints(version.NewConstraint("> 999.999.999")),
nil,
fmt.Errorf("no compatible schema found for > 999.999.999"),
},
}

for i, tc := range testCases {
t.Run(fmt.Sprintf("%d-%s", i, tc.constraint.String()), func(t *testing.T) {
bodySchema, err := CoreModuleSchemaForConstraint(tc.constraint)
if err != nil && tc.expectedErr == nil {
t.Fatal(err)
}
if err != nil && err.Error() != tc.expectedErr.Error() {
t.Fatalf("expected error: %q, given: %q", err.Error(), tc.expectedErr.Error())
}
if err == nil && tc.expectedErr != nil {
t.Fatalf("expected error: %q", tc.expectedErr.Error())
}

expectedSchema := tc.matchedSchema
if diff := cmp.Diff(expectedSchema, bodySchema, ctydebug.CmpOptions); diff != "" {
t.Fatalf("schema mismatch: %s", diff)
}
})
}
}

type versionedBodySchema func(*version.Version) *schema.BodySchema
Loading