-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
tooling - Introduce PR Schema breaking change detection (#21205)
- Loading branch information
1 parent
d1d0fc2
commit 986ef22
Showing
25 changed files
with
1,392 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
--- | ||
name: Breaking Schema Changes | ||
|
||
permissions: | ||
contents: read | ||
pull-requests: read | ||
|
||
|
||
on: | ||
pull_request: | ||
types: ['opened', 'synchronize'] | ||
paths: | ||
- '.github/workflows/breaking-change-detection.yaml' | ||
- 'vendor/**' | ||
- 'internal/**.go' | ||
|
||
concurrency: | ||
group: 'breakingChange-${{ github.head_ref }}' | ||
cancel-in-progress: true | ||
|
||
jobs: | ||
detect: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v3 | ||
- uses: actions/setup-go@v3 | ||
with: | ||
go-version-file: ./.go-version | ||
- run: bash ./scripts/run-breaking-change-detection.sh |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
package differ | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/hashicorp/terraform-provider-azurerm/internal/tools/schema-api/providerjson" | ||
schema_rules "github.com/hashicorp/terraform-provider-azurerm/internal/tools/schema-api/schema-rules" | ||
) | ||
|
||
type Differ struct { | ||
base *providerjson.ProviderWrapper | ||
current *providerjson.ProviderWrapper | ||
} | ||
|
||
func (d *Differ) Diff(fileName string, providerName string) []string { | ||
if err := d.loadFromProvider(providerjson.LoadData(), providerName); err != nil { | ||
return []string{err.Error()} | ||
} | ||
|
||
if err := d.loadFromFile(fileName); err != nil { | ||
return []string{err.Error()} | ||
} | ||
|
||
if d.base.ProviderName != d.current.ProviderName { | ||
return []string{fmt.Sprintf("provider name mismatch, expected %q, got %q", d.base.ProviderName, d.current.ProviderName)} | ||
} | ||
|
||
violations := make([]string, 0) | ||
|
||
for resource, rs := range d.current.ProviderSchema.ResourcesMap { | ||
_, ok := d.base.ProviderSchema.ResourcesMap[resource] | ||
if !ok { | ||
// New resource, no breaking changes to worry about | ||
continue | ||
} | ||
for propertyName, propertySchema := range rs.Schema { | ||
// Get the same from the base (released) json | ||
baseItem, ok := d.base.ProviderSchema.ResourcesMap[resource].Schema[propertyName] | ||
if !ok { | ||
// New property, could be breaking - Required etc | ||
baseItem = providerjson.SchemaJSON{} | ||
} | ||
if errs := compareNode(baseItem, propertySchema, propertyName); errs != nil { | ||
violations = append(violations, errs...) | ||
} | ||
} | ||
} | ||
|
||
for dataSource, ds := range d.current.ProviderSchema.DataSourcesMap { | ||
_, ok := d.base.ProviderSchema.DataSourcesMap[dataSource] | ||
if !ok { | ||
// New resource, no breaking changes to worry about | ||
continue | ||
} | ||
for propertyName, propertySchema := range ds.Schema { | ||
// Get the same from the base (released) json | ||
baseItem, ok := d.base.ProviderSchema.DataSourcesMap[dataSource].Schema[propertyName] | ||
if !ok { | ||
// New property, could be breaking - Required etc | ||
baseItem = providerjson.SchemaJSON{} | ||
} | ||
if errs := compareNode(baseItem, propertySchema, propertyName); errs != nil { | ||
violations = append(violations, errs...) | ||
} | ||
} | ||
} | ||
|
||
return violations | ||
} | ||
|
||
func compareNode(base providerjson.SchemaJSON, current providerjson.SchemaJSON, nodeName string) (errs []string) { | ||
if nodeIsBlock(base) { | ||
newBaseRaw := base.Elem.(providerjson.ResourceJSON).Schema | ||
newCurrent := current.Elem.(*providerjson.ResourceJSON).Schema | ||
for k, newBase := range newBaseRaw { | ||
errs = append(errs, compareNode(newBase, newCurrent[k], k)...) | ||
} | ||
} | ||
|
||
for _, v := range schema_rules.BreakingChangeRules { | ||
if err := v.Check(base, current, nodeName); err != nil { | ||
errs = append(errs, *err) | ||
} | ||
} | ||
|
||
return | ||
} | ||
|
||
func nodeIsBlock(input providerjson.SchemaJSON) bool { | ||
if input.Type == "TypeList" || input.Type == "TypeSet" { | ||
if _, ok := input.Elem.(providerjson.ResourceJSON); ok { | ||
return true | ||
} | ||
} | ||
return false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package differ | ||
|
||
import ( | ||
"encoding/json" | ||
"os" | ||
|
||
"github.com/hashicorp/terraform-provider-azurerm/internal/tools/schema-api/providerjson" | ||
) | ||
|
||
func (d *Differ) loadFromFile(fileName string) error { | ||
f, err := os.Open(fileName) | ||
if err != nil { | ||
return err | ||
} | ||
defer f.Close() | ||
buf := &providerjson.ProviderWrapper{} | ||
// TODO - Custom marshalling to fix the type assertions later? meh, works for now... | ||
if err := json.NewDecoder(f).Decode(buf); err != nil { | ||
return err | ||
} | ||
d.base = buf | ||
|
||
return nil | ||
} | ||
|
||
func (d *Differ) loadFromProvider(data *providerjson.ProviderJSON, providerName string) error { | ||
if s, err := providerjson.ProviderFromRaw(data); err != nil { | ||
return err | ||
} else { | ||
d.current = &providerjson.ProviderWrapper{ | ||
ProviderName: providerName, | ||
ProviderSchema: s, | ||
} | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package main | ||
|
||
import ( | ||
"flag" | ||
"fmt" | ||
"log" | ||
"net/http" | ||
"os" | ||
"os/signal" | ||
"syscall" | ||
|
||
"github.com/hashicorp/go-azure-helpers/lang/pointer" | ||
"github.com/hashicorp/terraform-provider-azurerm/internal/tools/schema-api/differ" | ||
"github.com/hashicorp/terraform-provider-azurerm/internal/tools/schema-api/providerjson" | ||
) | ||
|
||
func main() { | ||
f := flag.NewFlagSet("providerJson", flag.ExitOnError) | ||
|
||
apiPort := f.Int("api-port", 8080, "the port on which to run the Provider JSON server") | ||
dumpSchema := f.Bool("dump", false, "used to simply dump the entire provider schema") | ||
providerName := f.String("provider-name", "azurerm", "set the provider name, defaults to `azurerm`") | ||
exportSchema := f.String("export", "", "export the schema to the given path/filename. Intended for use in the release process") | ||
detectBreakingChanges := f.String("detect", "", "compare current schema to named dump.") | ||
errorOnBreakingChange := f.Bool("error-on-violation", false, "should the detect mode exit with a non-zero error code. Defaults to `false`") | ||
|
||
if err := f.Parse(os.Args[1:]); err != nil { | ||
fmt.Printf("error parsing args: %+v", err) | ||
os.Exit(1) | ||
} | ||
|
||
data := providerjson.LoadData() | ||
|
||
switch { | ||
case pointer.From(dumpSchema): | ||
{ | ||
// Call the method to stdout | ||
log.Printf("dumping schema for '%s'", *providerName) | ||
wrappedProvider := &providerjson.ProviderWrapper{ | ||
ProviderName: *providerName, | ||
SchemaVersion: "1", | ||
} | ||
if err := providerjson.DumpWithWrapper(wrappedProvider, data); err != nil { | ||
log.Fatalf("error dumping provider: %+v", err) | ||
} | ||
|
||
os.Exit(0) | ||
} | ||
|
||
case pointer.From(detectBreakingChanges) != "": | ||
{ | ||
d := differ.Differ{} | ||
if violations := d.Diff(*detectBreakingChanges, *providerName); violations != nil { | ||
for _, v := range violations { | ||
log.Println(v) | ||
} | ||
if pointer.From(errorOnBreakingChange) { | ||
os.Exit(1) | ||
} | ||
} | ||
|
||
os.Exit(0) | ||
} | ||
|
||
case pointer.From(exportSchema) != "": | ||
{ | ||
log.Printf("dumping schema for '%s'", *providerName) | ||
wrappedProvider := &providerjson.ProviderWrapper{ | ||
ProviderName: *providerName, | ||
SchemaVersion: "1", | ||
} | ||
if err := providerjson.WriteWithWrapper(wrappedProvider, data, *exportSchema); err != nil { | ||
log.Fatalf("error writing provider schema for %q to %q: %+v", *providerName, *exportSchema, err) | ||
} | ||
|
||
os.Exit(0) | ||
} | ||
} | ||
|
||
if *apiPort < 1024 || *apiPort > 65534 { | ||
log.Fatal(fmt.Printf("invalid value for apiport, must be between 1024 and 65534, got %+v", apiPort)) | ||
} | ||
|
||
sig := make(chan os.Signal, 1) | ||
|
||
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) | ||
|
||
go func() { | ||
sig := <-sig | ||
log.Printf("%s signal received, closing provider API server on port %d", sig, apiPort) | ||
os.Exit(0) | ||
}() | ||
|
||
mux := http.NewServeMux() | ||
// paths | ||
mux.HandleFunc(providerjson.DataSourcesList, data.ListDataSources) | ||
mux.HandleFunc(providerjson.ResourcesList, data.ListResources) | ||
|
||
mux.HandleFunc(providerjson.DataSourcesPath, data.DataSourcesHandler) | ||
mux.HandleFunc(providerjson.ResourcesPath, data.ResourcesHandler) | ||
|
||
mux.HandleFunc(providerjson.DumpSchema, data.DumpAllSchema) | ||
|
||
log.Printf("starting api service on localhost:%d", *apiPort) | ||
log.Println(http.ListenAndServe(fmt.Sprintf(":%d", *apiPort), mux)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
package providerjson | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"log" | ||
"net/http" | ||
"strings" | ||
) | ||
|
||
const ( | ||
DataSourcesList = "/ProviderSchema-data/v1/data-sources" // Lists all data sources in the Provider | ||
ResourcesList = "/ProviderSchema-data/v1/resources" // Lists all Resources in the Provider | ||
DataSourcesPath = "/ProviderSchema-data/v1/data-sources/" // Gets all ProviderSchema data for a data source | ||
ResourcesPath = "/ProviderSchema-data/v1/resources/" // Gets all ProviderSchema data for a Resource | ||
DumpSchema = "/ProviderSchema-data/v1/dump/" // Gets all ProviderSchema | ||
) | ||
|
||
func (p *ProviderJSON) DataSourcesHandler(w http.ResponseWriter, req *http.Request) { | ||
w.Header().Set("Content-Type", "application/json; charset=UTF-8") | ||
|
||
dsRaw := strings.Split(req.URL.RequestURI(), DataSourcesPath) | ||
ds := strings.Split(dsRaw[1], "/")[0] | ||
data, err := resourceFromRaw(p.DataSourcesMap[ds]) | ||
if err != nil { | ||
w.WriteHeader(http.StatusNotFound) | ||
log.Println(w.Write([]byte(fmt.Sprintf("[{\"error\": \"Could not process ProviderSchema for %q from provider: %+v\"}]", ds, err)))) | ||
} else if err := json.NewEncoder(w).Encode(data); err != nil { | ||
w.WriteHeader(http.StatusInternalServerError) | ||
log.Println(w.Write([]byte(fmt.Sprintf("Marshall error: %+v", err)))) | ||
} | ||
} | ||
|
||
func (p *ProviderJSON) ResourcesHandler(w http.ResponseWriter, req *http.Request) { | ||
w.Header().Set("Content-Type", "application/json; charset=UTF-8") | ||
|
||
dsRaw := strings.Split(req.URL.RequestURI(), ResourcesPath) | ||
ds := strings.Split(dsRaw[1], "/")[0] | ||
data, err := resourceFromRaw(p.ResourcesMap[ds]) | ||
if err != nil { | ||
w.WriteHeader(http.StatusNotFound) | ||
log.Println(w.Write([]byte(fmt.Sprintf("[{\"error\": \"Could not process ProviderSchema for %q from provider: %+v\"}]", ds, err)))) | ||
} else if err := json.NewEncoder(w).Encode(data); err != nil { | ||
w.WriteHeader(http.StatusInternalServerError) | ||
log.Println(w.Write([]byte(fmt.Sprintf("Marshall error: %+v", err)))) | ||
} | ||
} | ||
|
||
func (p *ProviderJSON) ListResources(w http.ResponseWriter, _ *http.Request) { | ||
w.Header().Set("Content-Type", "application/json; charset=UTF-8") | ||
if err := json.NewEncoder(w).Encode(p.Resources()); err != nil { | ||
log.Println(w.Write([]byte(fmt.Sprintf("Marshall error: %+v", err)))) | ||
} | ||
} | ||
|
||
func (p *ProviderJSON) ListDataSources(w http.ResponseWriter, _ *http.Request) { | ||
w.Header().Set("Content-Type", "application/json; charset=UTF-8") | ||
if err := json.NewEncoder(w).Encode(p.DataSources()); err != nil { | ||
log.Println(w.Write([]byte(fmt.Sprintf("Marshall error: %+v", err)))) | ||
} | ||
} | ||
|
||
func (p *ProviderJSON) DumpAllSchema(w http.ResponseWriter, _ *http.Request) { | ||
w.Header().Set("Content-Type", "application/json; charset=UTF-8") | ||
provider, err := ProviderFromRaw(p) | ||
if err != nil { | ||
w.WriteHeader(http.StatusInternalServerError) | ||
log.Println(w.Write([]byte(fmt.Sprintf("[{\"error\": \"Could not process provider: %+v\"}]", err)))) | ||
} | ||
if err := json.NewEncoder(w).Encode(provider); err != nil { | ||
log.Println(w.Write([]byte(fmt.Sprintf("Marshall error: %+v", err)))) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package providerjson | ||
|
||
import ( | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform" | ||
) | ||
|
||
func (p *ProviderJSON) DataSources() []terraform.DataSource { | ||
s := schema.Provider(*p) | ||
return s.DataSources() | ||
} | ||
|
||
func (p *ProviderJSON) Resources() []terraform.ResourceType { | ||
s := schema.Provider(*p) | ||
return s.Resources() | ||
} |
Oops, something went wrong.