From f3b3ff463c0f5bbdd1cdc015cc1e2b1897658046 Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Fri, 1 Dec 2023 13:03:48 +0100 Subject: [PATCH 01/20] tools/data-api-differ: scaffolding this out --- .../workflows/unit-test-data-api-differ.yaml | 27 +++++++++++++++++++ tools/data-api-differ/.gitignore | 2 ++ tools/data-api-differ/GNUmakefile | 12 +++++++++ tools/data-api-differ/go.mod | 3 +++ tools/data-api-differ/main.go | 7 +++++ 5 files changed, 51 insertions(+) create mode 100644 .github/workflows/unit-test-data-api-differ.yaml create mode 100644 tools/data-api-differ/.gitignore create mode 100644 tools/data-api-differ/GNUmakefile create mode 100644 tools/data-api-differ/go.mod create mode 100644 tools/data-api-differ/main.go diff --git a/.github/workflows/unit-test-data-api-differ.yaml b/.github/workflows/unit-test-data-api-differ.yaml new file mode 100644 index 00000000000..985a55ba5be --- /dev/null +++ b/.github/workflows/unit-test-data-api-differ.yaml @@ -0,0 +1,27 @@ +--- +name: Data API Differ (Unit Tests) +on: + pull_request: + types: ['opened', 'synchronize'] + paths: + - '.github/workflows/**' + - 'tools/data-api-differ/**' + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: true + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + submodules: recursive + + - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version: '1.21.3' + + - name: run unit tests + run: | + cd ./tools/data-api-differ + make test diff --git a/tools/data-api-differ/.gitignore b/tools/data-api-differ/.gitignore new file mode 100644 index 00000000000..938da3b60f4 --- /dev/null +++ b/tools/data-api-differ/.gitignore @@ -0,0 +1,2 @@ +data-api-differ +vendor/ \ No newline at end of file diff --git a/tools/data-api-differ/GNUmakefile b/tools/data-api-differ/GNUmakefile new file mode 100644 index 00000000000..4807a301a63 --- /dev/null +++ b/tools/data-api-differ/GNUmakefile @@ -0,0 +1,12 @@ +default: build + +build: + go build . + +fmt: + find . -name '*.go' | grep -v vendor | xargs gofmt -s -w + +test: + go test -v ./... + +.PHONY: build fmt test diff --git a/tools/data-api-differ/go.mod b/tools/data-api-differ/go.mod new file mode 100644 index 00000000000..c70cd778227 --- /dev/null +++ b/tools/data-api-differ/go.mod @@ -0,0 +1,3 @@ +module github.com/hashicorp/pandora/tools/data-api-differ + +go 1.21 diff --git a/tools/data-api-differ/main.go b/tools/data-api-differ/main.go new file mode 100644 index 00000000000..6d20d360a43 --- /dev/null +++ b/tools/data-api-differ/main.go @@ -0,0 +1,7 @@ +package main + +import "log" + +func main() { + log.Printf("Data API Differ - launched") +} From 92379711e6a95b2a86c8b40f19e43f9be0fede64 Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Wed, 6 Dec 2023 09:41:40 +0100 Subject: [PATCH 02/20] tools/data-api-differ: building out the different types of Changes This commit introduces one struct per Type of Change - allowing us to pick out later on which Changes are relevant for which Command. --- .../internal/changes/api_resource_added.go | 20 ++++++++ .../internal/changes/api_resource_removed.go | 20 ++++++++ .../internal/changes/api_version_added.go | 17 +++++++ .../internal/changes/api_version_removed.go | 18 +++++++ .../internal/changes/change.go | 9 ++++ .../internal/changes/constant_added.go | 29 +++++++++++ .../changes/constant_key_value_added.go | 29 +++++++++++ .../changes/constant_key_value_changed.go | 32 ++++++++++++ .../changes/constant_key_value_removed.go | 29 +++++++++++ .../internal/changes/constant_removed.go | 29 +++++++++++ .../internal/changes/constant_type_changed.go | 31 ++++++++++++ .../internal/changes/field_added.go | 26 ++++++++++ .../internal/changes/field_is_now_optional.go | 29 +++++++++++ .../internal/changes/field_is_now_required.go | 30 +++++++++++ .../changes/field_json_name_changed.go | 35 +++++++++++++ .../field_object_definition_changed.go | 33 ++++++++++++ .../internal/changes/field_removed.go | 26 ++++++++++ .../internal/changes/model_added.go | 23 +++++++++ .../model_discriminated_parent_type_added.go | 31 ++++++++++++ ...model_discriminated_parent_type_changed.go | 32 ++++++++++++ ...model_discriminated_parent_type_removed.go | 31 ++++++++++++ ...odel_discriminated_type_hint_in_changed.go | 34 +++++++++++++ .../model_discriminated_value_changed.go | 35 +++++++++++++ .../internal/changes/model_removed.go | 23 +++++++++ .../internal/changes/operation_added.go | 26 ++++++++++ .../changes/operation_content_type_changed.go | 36 +++++++++++++ ...operation_expected_status_codes_changed.go | 50 +++++++++++++++++++ .../changes/operation_long_running_added.go | 23 +++++++++ .../changes/operation_long_running_removed.go | 23 +++++++++ .../changes/operation_method_changed.go | 31 ++++++++++++ .../changes/operation_options_added.go | 27 ++++++++++ .../changes/operation_options_changed.go | 43 ++++++++++++++++ .../changes/operation_options_removed.go | 27 ++++++++++ .../operation_pagination_field_changed.go | 32 ++++++++++++ .../internal/changes/operation_removed.go | 26 ++++++++++ .../changes/operation_request_object_added.go | 27 ++++++++++ .../operation_request_object_changed.go | 29 +++++++++++ .../operation_request_object_removed.go | 27 ++++++++++ .../changes/operation_resource_id_added.go | 26 ++++++++++ .../changes/operation_resource_id_changed.go | 39 +++++++++++++++ .../changes/operation_resource_id_removed.go | 27 ++++++++++ .../changes/operation_resource_id_renamed.go | 35 +++++++++++++ .../operation_response_object_added.go | 27 ++++++++++ .../operation_response_object_changed.go | 29 +++++++++++ .../operation_response_object_removed.go | 27 ++++++++++ .../changes/operation_uri_suffix_added.go | 27 ++++++++++ .../changes/operation_uri_suffix_changed.go | 30 +++++++++++ .../changes/operation_uri_suffix_removed.go | 27 ++++++++++ .../internal/changes/resource_id_added.go | 29 +++++++++++ .../changes/resource_id_common_id_added.go | 34 +++++++++++++ .../changes/resource_id_common_id_changed.go | 40 +++++++++++++++ .../changes/resource_id_common_id_removed.go | 34 +++++++++++++ .../internal/changes/resource_id_removed.go | 29 +++++++++++ .../resource_id_segment_changed_value.go | 34 +++++++++++++ .../resource_id_segments_changed_length.go | 33 ++++++++++++ .../internal/changes/service_added.go | 14 ++++++ .../internal/changes/service_removed.go | 14 ++++++ 57 files changed, 1633 insertions(+) create mode 100644 tools/data-api-differ/internal/changes/api_resource_added.go create mode 100644 tools/data-api-differ/internal/changes/api_resource_removed.go create mode 100644 tools/data-api-differ/internal/changes/api_version_added.go create mode 100644 tools/data-api-differ/internal/changes/api_version_removed.go create mode 100644 tools/data-api-differ/internal/changes/change.go create mode 100644 tools/data-api-differ/internal/changes/constant_added.go create mode 100644 tools/data-api-differ/internal/changes/constant_key_value_added.go create mode 100644 tools/data-api-differ/internal/changes/constant_key_value_changed.go create mode 100644 tools/data-api-differ/internal/changes/constant_key_value_removed.go create mode 100644 tools/data-api-differ/internal/changes/constant_removed.go create mode 100644 tools/data-api-differ/internal/changes/constant_type_changed.go create mode 100644 tools/data-api-differ/internal/changes/field_added.go create mode 100644 tools/data-api-differ/internal/changes/field_is_now_optional.go create mode 100644 tools/data-api-differ/internal/changes/field_is_now_required.go create mode 100644 tools/data-api-differ/internal/changes/field_json_name_changed.go create mode 100644 tools/data-api-differ/internal/changes/field_object_definition_changed.go create mode 100644 tools/data-api-differ/internal/changes/field_removed.go create mode 100644 tools/data-api-differ/internal/changes/model_added.go create mode 100644 tools/data-api-differ/internal/changes/model_discriminated_parent_type_added.go create mode 100644 tools/data-api-differ/internal/changes/model_discriminated_parent_type_changed.go create mode 100644 tools/data-api-differ/internal/changes/model_discriminated_parent_type_removed.go create mode 100644 tools/data-api-differ/internal/changes/model_discriminated_type_hint_in_changed.go create mode 100644 tools/data-api-differ/internal/changes/model_discriminated_value_changed.go create mode 100644 tools/data-api-differ/internal/changes/model_removed.go create mode 100644 tools/data-api-differ/internal/changes/operation_added.go create mode 100644 tools/data-api-differ/internal/changes/operation_content_type_changed.go create mode 100644 tools/data-api-differ/internal/changes/operation_expected_status_codes_changed.go create mode 100644 tools/data-api-differ/internal/changes/operation_long_running_added.go create mode 100644 tools/data-api-differ/internal/changes/operation_long_running_removed.go create mode 100644 tools/data-api-differ/internal/changes/operation_method_changed.go create mode 100644 tools/data-api-differ/internal/changes/operation_options_added.go create mode 100644 tools/data-api-differ/internal/changes/operation_options_changed.go create mode 100644 tools/data-api-differ/internal/changes/operation_options_removed.go create mode 100644 tools/data-api-differ/internal/changes/operation_pagination_field_changed.go create mode 100644 tools/data-api-differ/internal/changes/operation_removed.go create mode 100644 tools/data-api-differ/internal/changes/operation_request_object_added.go create mode 100644 tools/data-api-differ/internal/changes/operation_request_object_changed.go create mode 100644 tools/data-api-differ/internal/changes/operation_request_object_removed.go create mode 100644 tools/data-api-differ/internal/changes/operation_resource_id_added.go create mode 100644 tools/data-api-differ/internal/changes/operation_resource_id_changed.go create mode 100644 tools/data-api-differ/internal/changes/operation_resource_id_removed.go create mode 100644 tools/data-api-differ/internal/changes/operation_resource_id_renamed.go create mode 100644 tools/data-api-differ/internal/changes/operation_response_object_added.go create mode 100644 tools/data-api-differ/internal/changes/operation_response_object_changed.go create mode 100644 tools/data-api-differ/internal/changes/operation_response_object_removed.go create mode 100644 tools/data-api-differ/internal/changes/operation_uri_suffix_added.go create mode 100644 tools/data-api-differ/internal/changes/operation_uri_suffix_changed.go create mode 100644 tools/data-api-differ/internal/changes/operation_uri_suffix_removed.go create mode 100644 tools/data-api-differ/internal/changes/resource_id_added.go create mode 100644 tools/data-api-differ/internal/changes/resource_id_common_id_added.go create mode 100644 tools/data-api-differ/internal/changes/resource_id_common_id_changed.go create mode 100644 tools/data-api-differ/internal/changes/resource_id_common_id_removed.go create mode 100644 tools/data-api-differ/internal/changes/resource_id_removed.go create mode 100644 tools/data-api-differ/internal/changes/resource_id_segment_changed_value.go create mode 100644 tools/data-api-differ/internal/changes/resource_id_segments_changed_length.go create mode 100644 tools/data-api-differ/internal/changes/service_added.go create mode 100644 tools/data-api-differ/internal/changes/service_removed.go diff --git a/tools/data-api-differ/internal/changes/api_resource_added.go b/tools/data-api-differ/internal/changes/api_resource_added.go new file mode 100644 index 00000000000..c2b30b87f5b --- /dev/null +++ b/tools/data-api-differ/internal/changes/api_resource_added.go @@ -0,0 +1,20 @@ +package changes + +var _ Change = ApiResourceAdded{} + +// ApiResourceAdded defines information about an API Resource that has been added. +type ApiResourceAdded struct { + // ServiceName specifies the name of the Service which contains this API Resource. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this API Resource. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Resource. + ResourceName string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ApiResourceAdded) IsBreaking() bool { + return false +} diff --git a/tools/data-api-differ/internal/changes/api_resource_removed.go b/tools/data-api-differ/internal/changes/api_resource_removed.go new file mode 100644 index 00000000000..8bc745f18fd --- /dev/null +++ b/tools/data-api-differ/internal/changes/api_resource_removed.go @@ -0,0 +1,20 @@ +package changes + +var _ Change = ApiResourceRemoved{} + +// ApiResourceRemoved defines information about an API Resource which has been removed. +type ApiResourceRemoved struct { + // ServiceName specifies the name of the Service which contained this API Resource. + ServiceName string + + // ApiVersion specifies the name of the API Version which contained this API Resource. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contained this Resource. + ResourceName string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ApiResourceRemoved) IsBreaking() bool { + return true +} diff --git a/tools/data-api-differ/internal/changes/api_version_added.go b/tools/data-api-differ/internal/changes/api_version_added.go new file mode 100644 index 00000000000..96b714542c3 --- /dev/null +++ b/tools/data-api-differ/internal/changes/api_version_added.go @@ -0,0 +1,17 @@ +package changes + +var _ Change = ApiVersionAdded{} + +// ApiVersionAdded defines information about a new API Version for an existing Service. +type ApiVersionAdded struct { + // ServiceName specifies the name of this Service (e.g. `Compute`). + ServiceName string + + // ApiVersion specifies the API Version (e.g. `2023-01-01-preview`). + ApiVersion string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ApiVersionAdded) IsBreaking() bool { + return false +} diff --git a/tools/data-api-differ/internal/changes/api_version_removed.go b/tools/data-api-differ/internal/changes/api_version_removed.go new file mode 100644 index 00000000000..d3c4318cbf8 --- /dev/null +++ b/tools/data-api-differ/internal/changes/api_version_removed.go @@ -0,0 +1,18 @@ +package changes + +var _ Change = ApiVersionRemoved{} + +// ApiVersionRemoved defines information about an API Version which was previously +// supported for an existing Service but is no longer present. +type ApiVersionRemoved struct { + // ServiceName specifies the name of this Service (e.g. `Compute`). + ServiceName string + + // ApiVersion specifies the API Version (e.g. `2023-01-01-preview`). + ApiVersion string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ApiVersionRemoved) IsBreaking() bool { + return true +} diff --git a/tools/data-api-differ/internal/changes/change.go b/tools/data-api-differ/internal/changes/change.go new file mode 100644 index 00000000000..6e93273cef9 --- /dev/null +++ b/tools/data-api-differ/internal/changes/change.go @@ -0,0 +1,9 @@ +package changes + +// Change defines the type of Change between one set of API Definitions and another. +// Each implementation of Change contains additional information about the change +// for example, ConstantKeyValueAdded / ResourceIdRemoved which is useful elsewhere. +type Change interface { + // IsBreaking returns whether this Change is considered a Breaking Change. + IsBreaking() bool +} diff --git a/tools/data-api-differ/internal/changes/constant_added.go b/tools/data-api-differ/internal/changes/constant_added.go new file mode 100644 index 00000000000..f68c680a44b --- /dev/null +++ b/tools/data-api-differ/internal/changes/constant_added.go @@ -0,0 +1,29 @@ +package changes + +var _ Change = ConstantAdded{} + +// ConstantAdded defines information about a new Constant. +type ConstantAdded struct { + // ServiceName specifies the name of the Service which contains this Constant. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Constant. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Constant. + ResourceName string + + // ConstantName specifies the name of the Constant which has been added. + ConstantName string + + // ConstantType specifies the type of Constant (e.g. Int/String) that this is. + ConstantType string + + // KeysAndValues specifies the Keys and Values for the Constant which has been added. + KeysAndValues map[string]string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ConstantAdded) IsBreaking() bool { + return false +} diff --git a/tools/data-api-differ/internal/changes/constant_key_value_added.go b/tools/data-api-differ/internal/changes/constant_key_value_added.go new file mode 100644 index 00000000000..f2ab104f1df --- /dev/null +++ b/tools/data-api-differ/internal/changes/constant_key_value_added.go @@ -0,0 +1,29 @@ +package changes + +var _ Change = ConstantKeyValueAdded{} + +// ConstantKeyValueAdded specifies when a new Key/Value combination is added to an existing Constant. +type ConstantKeyValueAdded struct { + // ServiceName specifies the name of the Service which contains this Constant. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Constant. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Constant. + ResourceName string + + // ConstantName specifies the name of the Constant which has been updated. + ConstantName string + + // ConstantKey specifies the key for this new Constant Key/Value. + ConstantKey string + + // ConstantValue specifies the value for this new Constant Key/Value. + ConstantValue string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ConstantKeyValueAdded) IsBreaking() bool { + return false +} diff --git a/tools/data-api-differ/internal/changes/constant_key_value_changed.go b/tools/data-api-differ/internal/changes/constant_key_value_changed.go new file mode 100644 index 00000000000..8c1bd2d6c61 --- /dev/null +++ b/tools/data-api-differ/internal/changes/constant_key_value_changed.go @@ -0,0 +1,32 @@ +package changes + +var _ Change = ConstantKeyValueChanged{} + +// ConstantKeyValueChanged specifies when Constant Key has a new Value +type ConstantKeyValueChanged struct { + // ServiceName specifies the name of the Service which contains this Constant. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Constant. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Constant. + ResourceName string + + // ConstantName specifies the name of the Constant which has an updated value. + ConstantName string + + // ConstantKey specifies the key within the Constant which has changed. + ConstantKey string + + // OldConstantValue specifies the old Value for this Constant Key. + OldConstantValue string + + // NewConstantValue specifies the new/updated Value for this Constant Key. + NewConstantValue string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ConstantKeyValueChanged) IsBreaking() bool { + return true +} diff --git a/tools/data-api-differ/internal/changes/constant_key_value_removed.go b/tools/data-api-differ/internal/changes/constant_key_value_removed.go new file mode 100644 index 00000000000..5ca4e182c13 --- /dev/null +++ b/tools/data-api-differ/internal/changes/constant_key_value_removed.go @@ -0,0 +1,29 @@ +package changes + +var _ Change = ConstantKeyValueRemoved{} + +// ConstantKeyValueRemoved specifies when a Key/Value combination is removed to an existing Constant. +type ConstantKeyValueRemoved struct { + // ServiceName specifies the name of the Service which contains this Constant. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Constant. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Constant. + ResourceName string + + // ConstantName specifies the name of the Constant which has been updated. + ConstantName string + + // ConstantKey specifies the key for the Constant Key/Value which has been removed. + ConstantKey string + + // ConstantValue specifies the value for the Constant Key/Value which has been removed + ConstantValue string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ConstantKeyValueRemoved) IsBreaking() bool { + return true +} diff --git a/tools/data-api-differ/internal/changes/constant_removed.go b/tools/data-api-differ/internal/changes/constant_removed.go new file mode 100644 index 00000000000..8fe81730cfb --- /dev/null +++ b/tools/data-api-differ/internal/changes/constant_removed.go @@ -0,0 +1,29 @@ +package changes + +var _ Change = ConstantRemoved{} + +// ConstantRemoved defines information about a Constant which has been removed. +type ConstantRemoved struct { + // ServiceName specifies the name of the Service which contained this Constant. + ServiceName string + + // ApiVersion specifies the name of the API Version which contained this Constant. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contained this Constant. + ResourceName string + + // ConstantName specifies the name of the Constant which has been removed. + ConstantName string + + // ConstantType specifies the type of Constant (e.g. Int/String) that this is. + ConstantType string + + // KeysAndValues specifies the Keys and Values for the Constant which has been removed. + KeysAndValues map[string]string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ConstantRemoved) IsBreaking() bool { + return true +} diff --git a/tools/data-api-differ/internal/changes/constant_type_changed.go b/tools/data-api-differ/internal/changes/constant_type_changed.go new file mode 100644 index 00000000000..5eaf642547f --- /dev/null +++ b/tools/data-api-differ/internal/changes/constant_type_changed.go @@ -0,0 +1,31 @@ +package changes + +var _ Change = ConstantTypeChanged{} + +// ConstantTypeChanged specifies when a Constant has changed Type (e.g. `int` -> `string`) +type ConstantTypeChanged struct { + // ServiceName specifies the name of the Service which contains this Constant. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Constant. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Constant. + ResourceName string + + // ConstantName specifies the name of the Constant which has changed. + ConstantName string + + // OldType specifies the old type value for this Constant + OldType string + + // NewType specifies the new/updated type value for this Constant + NewType string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. + +func (c ConstantTypeChanged) IsBreaking() bool { + // If a constant changes type, this is going to require code changes to account for this + return true +} diff --git a/tools/data-api-differ/internal/changes/field_added.go b/tools/data-api-differ/internal/changes/field_added.go new file mode 100644 index 00000000000..c9ee4e93986 --- /dev/null +++ b/tools/data-api-differ/internal/changes/field_added.go @@ -0,0 +1,26 @@ +package changes + +var _ Change = FieldAdded{} + +// FieldAdded defines information about a new Field. +type FieldAdded struct { + // ServiceName specifies the name of the Service which contains this Field. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Field. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Field. + ResourceName string + + // ModelName specifies the name of the Model which contains this Field. + ModelName string + + // FieldName specifies the name of the Field which has been added. + FieldName string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (FieldAdded) IsBreaking() bool { + return false +} diff --git a/tools/data-api-differ/internal/changes/field_is_now_optional.go b/tools/data-api-differ/internal/changes/field_is_now_optional.go new file mode 100644 index 00000000000..f04a7a32cfc --- /dev/null +++ b/tools/data-api-differ/internal/changes/field_is_now_optional.go @@ -0,0 +1,29 @@ +package changes + +var _ Change = FieldIsNowOptional{} + +// FieldIsNowOptional defines a change where an existing Field in an existing Model +// has become Optional. +type FieldIsNowOptional struct { + // ServiceName specifies the name of the Service which contains this Field. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Field. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Field. + ResourceName string + + // ModelName specifies the name of the Model which contains this Field. + ModelName string + + // FieldName specifies the name of the Field which is now Optional. + FieldName string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (FieldIsNowOptional) IsBreaking() bool { + // If the field has gone from Required -> Optional this will change the semantics + // making this field a pointer in the Go SDK - meaning this will require code changes. + return true +} diff --git a/tools/data-api-differ/internal/changes/field_is_now_required.go b/tools/data-api-differ/internal/changes/field_is_now_required.go new file mode 100644 index 00000000000..0db564ae825 --- /dev/null +++ b/tools/data-api-differ/internal/changes/field_is_now_required.go @@ -0,0 +1,30 @@ +package changes + +var _ Change = FieldIsNowRequired{} + +// FieldIsNowRequired defines a change where an existing Field in an existing Model +// has become Required. +type FieldIsNowRequired struct { + // ServiceName specifies the name of the Service which contains this Field. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Field. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Field. + ResourceName string + + // ModelName specifies the name of the Model which contains this Field. + ModelName string + + // FieldName specifies the name of the Field which is now Required. + FieldName string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (FieldIsNowRequired) IsBreaking() bool { + // If the field has gone from Optional -> Required this will change the semantics + // making this field no longer a pointer in the Go SDK - meaning this will require + // code changes. + return true +} diff --git a/tools/data-api-differ/internal/changes/field_json_name_changed.go b/tools/data-api-differ/internal/changes/field_json_name_changed.go new file mode 100644 index 00000000000..92ce740f3b1 --- /dev/null +++ b/tools/data-api-differ/internal/changes/field_json_name_changed.go @@ -0,0 +1,35 @@ +package changes + +var _ Change = FieldJsonNameChanged{} + +// FieldJsonNameChanged defines when the JsonName for an existing Field within an existing Model +// changes - indicating this field represents a different field in the API Request/Response. +type FieldJsonNameChanged struct { + // ServiceName specifies the name of the Service which contains this Field. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Field. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Field. + ResourceName string + + // ModelName specifies the name of the Model which contains this Field. + ModelName string + + // FieldName specifies the name of the Field which has an updated JsonName. + FieldName string + + // OldValue specifies the old/existing JsonName for this Field. + OldValue string + + // NewValue specifies the new/updated JsonName for this Field. + NewValue string +} + +func (FieldJsonNameChanged) IsBreaking() bool { + // If the JSON Name for this field has changed in the API then this is a breaking change + // since existing (or perhaps new) callers will be unable to marshal the existing result. + // As such this requires additional investigation. + return true +} diff --git a/tools/data-api-differ/internal/changes/field_object_definition_changed.go b/tools/data-api-differ/internal/changes/field_object_definition_changed.go new file mode 100644 index 00000000000..61a9893c2a5 --- /dev/null +++ b/tools/data-api-differ/internal/changes/field_object_definition_changed.go @@ -0,0 +1,33 @@ +package changes + +var _ Change = FieldObjectDefinitionChanged{} + +// FieldObjectDefinitionChanged defines when an existing Field within an existing Model gets an +// updated ObjectDefinition (e.g. a String becomes a Constant). +type FieldObjectDefinitionChanged struct { + // ServiceName specifies the name of the Service which contains this Field. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Field. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Field. + ResourceName string + + // ModelName specifies the name of the Model which contains this Field. + ModelName string + + // FieldName specifies the name of the Field which has an updated Object Definition. + FieldName string + + // OldValue specifies the old/existing ObjectDefinition for this Field. + OldValue string + + // NewValue specifies the new/updated ObjectDefinition for this Field. + NewValue string +} + +func (FieldObjectDefinitionChanged) IsBreaking() bool { + // If the ObjectDefinition has changed this is going to require code changes + return true +} diff --git a/tools/data-api-differ/internal/changes/field_removed.go b/tools/data-api-differ/internal/changes/field_removed.go new file mode 100644 index 00000000000..a4c65a34c23 --- /dev/null +++ b/tools/data-api-differ/internal/changes/field_removed.go @@ -0,0 +1,26 @@ +package changes + +var _ Change = FieldRemoved{} + +// FieldRemoved defines information about a Field which has been removed. +type FieldRemoved struct { + // ServiceName specifies the name of the Service which contained this Field. + ServiceName string + + // ApiVersion specifies the name of the API Version which contained this Field. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contained this Field. + ResourceName string + + // ModelName specifies the name of the Model which contained this Field. + ModelName string + + // FieldName specifies the name of the Field which has been removed. + FieldName string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (FieldRemoved) IsBreaking() bool { + return true +} diff --git a/tools/data-api-differ/internal/changes/model_added.go b/tools/data-api-differ/internal/changes/model_added.go new file mode 100644 index 00000000000..14fb65a6aa0 --- /dev/null +++ b/tools/data-api-differ/internal/changes/model_added.go @@ -0,0 +1,23 @@ +package changes + +var _ Change = ModelAdded{} + +// ModelAdded defines information about a new Model. +type ModelAdded struct { + // ServiceName specifies the name of the Service which contains this Model. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Model. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Model. + ResourceName string + + // ModelName specifies the name of the Model which has been added. + ModelName string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ModelAdded) IsBreaking() bool { + return false +} diff --git a/tools/data-api-differ/internal/changes/model_discriminated_parent_type_added.go b/tools/data-api-differ/internal/changes/model_discriminated_parent_type_added.go new file mode 100644 index 00000000000..6c29c8f8d01 --- /dev/null +++ b/tools/data-api-differ/internal/changes/model_discriminated_parent_type_added.go @@ -0,0 +1,31 @@ +package changes + +var _ Change = ModelDiscriminatedParentTypeAdded{} + +// ModelDiscriminatedParentTypeAdded defines that an existing Model is now +// a Discriminated Implementation of another Parent Type. +type ModelDiscriminatedParentTypeAdded struct { + // ServiceName specifies the name of the Service which contains this Model. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Model. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Model. + ResourceName string + + // ModelName specifies the name of the Model which has become a Discriminated + // Implementation. + ModelName string + + // NewParentModelName specifies the name of the Parent Model that this Model is an + // Implementation of. + NewParentModelName string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ModelDiscriminatedParentTypeAdded) IsBreaking() bool { + // If an existing Model becomes a Discriminated Implementation then this is a breaking + // change since we'll need to update the codebase to account for it. + return true +} diff --git a/tools/data-api-differ/internal/changes/model_discriminated_parent_type_changed.go b/tools/data-api-differ/internal/changes/model_discriminated_parent_type_changed.go new file mode 100644 index 00000000000..2f656111e14 --- /dev/null +++ b/tools/data-api-differ/internal/changes/model_discriminated_parent_type_changed.go @@ -0,0 +1,32 @@ +package changes + +var _ Change = ModelDiscriminatedParentTypeChanged{} + +// ModelDiscriminatedParentTypeChanged defines that the Parent Model Name for this +// Discriminated Type has changed. +type ModelDiscriminatedParentTypeChanged struct { + // ServiceName specifies the name of the Service which contains this Model. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Model. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Model. + ResourceName string + + // ModelName specifies the name of the Model which has become a Discriminated + // Implementation. + ModelName string + + // OldParentModelName specifies the name of the old Parent Model for this Model. + OldParentModelName string + + // NewParentModelName specifies the name of the new Parent Model for this Model. + NewParentModelName string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ModelDiscriminatedParentTypeChanged) IsBreaking() bool { + // If a Model changes Parent Type then this is a breaking change + return true +} diff --git a/tools/data-api-differ/internal/changes/model_discriminated_parent_type_removed.go b/tools/data-api-differ/internal/changes/model_discriminated_parent_type_removed.go new file mode 100644 index 00000000000..57727f65dbc --- /dev/null +++ b/tools/data-api-differ/internal/changes/model_discriminated_parent_type_removed.go @@ -0,0 +1,31 @@ +package changes + +var _ Change = ModelDiscriminatedParentTypeRemoved{} + +// ModelDiscriminatedParentTypeRemoved defines that an existing Model was a Discriminated Type +// (i.e. had a Parent Type) but no longer does. +type ModelDiscriminatedParentTypeRemoved struct { + // ServiceName specifies the name of the Service which contains this Model. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Model. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Model. + ResourceName string + + // ModelName specifies the name of the Model which has become a Discriminated + // Implementation. + ModelName string + + // OldParentModelName specifies the name of the Parent Model that this Model was an + // Implementation of. + OldParentModelName string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ModelDiscriminatedParentTypeRemoved) IsBreaking() bool { + // If an existing Model is no longer a Discriminated Implementation then this is a + // breaking change since we'll need to update the codebase to account for it. + return true +} diff --git a/tools/data-api-differ/internal/changes/model_discriminated_type_hint_in_changed.go b/tools/data-api-differ/internal/changes/model_discriminated_type_hint_in_changed.go new file mode 100644 index 00000000000..62acee93107 --- /dev/null +++ b/tools/data-api-differ/internal/changes/model_discriminated_type_hint_in_changed.go @@ -0,0 +1,34 @@ +package changes + +var _ Change = ModelDiscriminatedTypeHintInChanged{} + +// ModelDiscriminatedTypeHintInChanged defines that the TypeHintIn field has changed for the +// Model in question. +type ModelDiscriminatedTypeHintInChanged struct { + // ServiceName specifies the name of the Service which contains this Model. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Model. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Model. + ResourceName string + + // ModelName specifies the name of the Model where the Discriminated TypeHintIn has changed. + ModelName string + + // OldValue specifies the old name of the Field that was used to uniquely identify this + // Discriminated Implementation. + OldValue string + + // OldValue specifies the new/updated name of the Field that was used to uniquely identify this + // Discriminated Implementation. + NewValue string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ModelDiscriminatedTypeHintInChanged) IsBreaking() bool { + // If the field containing the Type Hint used to uniquely identify this Discriminated + // Implementation has changed this will likely break all existing implementations. + return true +} diff --git a/tools/data-api-differ/internal/changes/model_discriminated_value_changed.go b/tools/data-api-differ/internal/changes/model_discriminated_value_changed.go new file mode 100644 index 00000000000..804d822a609 --- /dev/null +++ b/tools/data-api-differ/internal/changes/model_discriminated_value_changed.go @@ -0,0 +1,35 @@ +package changes + +var _ Change = ModelDiscriminatedTypeValueChanged{} + +// ModelDiscriminatedTypeValueChanged defines that the Discriminated Value used to uniquely +// identify this Discriminated Type has changed. +type ModelDiscriminatedTypeValueChanged struct { + // ServiceName specifies the name of the Service which contains this Model. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Model. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Model. + ResourceName string + + // ModelName specifies the name of the Model where the Discriminated Type Value has changed. + ModelName string + + // OldValue specifies the old Value that was used to uniquely identify this Discriminated + // Implementation. + OldValue string + + // NewValue specifies the new/updated Value used to uniquely identify this Discriminated + // Implementation. + NewValue string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ModelDiscriminatedTypeValueChanged) IsBreaking() bool { + // If the Discriminated Type Value has changed this is a BIG breaking change + // since any existing Implementations will likely fail - such this requires + // additional investigation. + return true +} diff --git a/tools/data-api-differ/internal/changes/model_removed.go b/tools/data-api-differ/internal/changes/model_removed.go new file mode 100644 index 00000000000..c67d6730466 --- /dev/null +++ b/tools/data-api-differ/internal/changes/model_removed.go @@ -0,0 +1,23 @@ +package changes + +var _ Change = ModelRemoved{} + +// ModelRemoved defines information about a Model which has been Removed. +type ModelRemoved struct { + // ServiceName specifies the name of the Service which contained this Model. + ServiceName string + + // ApiVersion specifies the name of the API Version which contained this Model. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contained this Model. + ResourceName string + + // ModelName specifies the name of the Model which has been removed. + ModelName string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ModelRemoved) IsBreaking() bool { + return true +} diff --git a/tools/data-api-differ/internal/changes/operation_added.go b/tools/data-api-differ/internal/changes/operation_added.go new file mode 100644 index 00000000000..81c0618d6eb --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_added.go @@ -0,0 +1,26 @@ +package changes + +var _ Change = OperationAdded{} + +// OperationAdded defines an Operation which has been added to an existing API Resource. +type OperationAdded struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation which has been added. + OperationName string + + // Uri specifies the URI of the Operation which has been added. + Uri string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (OperationAdded) IsBreaking() bool { + return false +} diff --git a/tools/data-api-differ/internal/changes/operation_content_type_changed.go b/tools/data-api-differ/internal/changes/operation_content_type_changed.go new file mode 100644 index 00000000000..74d35fcea05 --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_content_type_changed.go @@ -0,0 +1,36 @@ +package changes + +var _ Change = OperationContentTypeChanged{} + +// OperationContentTypeChanged defines that the ContentType for an existing Operation has changed. +type OperationContentTypeChanged struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation which has been an updated ContentType. + OperationName string + + // OldContentType specifies the old/existing value for the Content-Type field for this Operation. + OldContentType string + + // NewContentType specifies the new/updated value for the Content-Type field for this Operation. + NewContentType string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (OperationContentTypeChanged) IsBreaking() bool { + // If the `Content-Type` for an operation has changed this will affect serialization/deserialization + // and in the case of the API supporting multiple `Content-Type`s on a single endpoint - can lead + // to semantically different objects. + // + // As such whilst this MAY not be a breaking change (for example changing `application/json` -> + // `application/json; encoding=utf8`) this will require a manual review, so should be flagged as + // a breaking change. + return true +} diff --git a/tools/data-api-differ/internal/changes/operation_expected_status_codes_changed.go b/tools/data-api-differ/internal/changes/operation_expected_status_codes_changed.go new file mode 100644 index 00000000000..6c25395d35b --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_expected_status_codes_changed.go @@ -0,0 +1,50 @@ +package changes + +var _ Change = OperationExpectedStatusCodesChanged{} + +// OperationExpectedStatusCodesChanged defines when the Expected Status Codes for an existing Operation +// have changed. +type OperationExpectedStatusCodesChanged struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation which has an updated set of Expected Status Codes. + OperationName string + + // OldExpectedStatusCodes specifies the old/existing Expected Status Codes for this Operation. + OldExpectedStatusCodes []int + + // NewExpectedStatusCodes specifies the new/updated Expected Status Codes for this Operation. + NewExpectedStatusCodes []int +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (c OperationExpectedStatusCodesChanged) IsBreaking() bool { + // Whether this is a breaking change depends on whether any existing Expected Status Codes have + // been removed. Whilst adding a new Expected Status Code COULD well be a breaking change, in + // practice this depends on the downstream usage (for example if we've added a Custom Poller to + // work around this in the interim). + removed := make([]int, 0) + for _, v := range c.OldExpectedStatusCodes { + existsInNew := false + for _, other := range c.NewExpectedStatusCodes { + if other == v { + existsInNew = true + break + } + } + + if !existsInNew { + removed = append(removed, v) + } + } + + isBreakingChange := len(removed) > 0 + return isBreakingChange +} diff --git a/tools/data-api-differ/internal/changes/operation_long_running_added.go b/tools/data-api-differ/internal/changes/operation_long_running_added.go new file mode 100644 index 00000000000..01c6cef33ec --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_long_running_added.go @@ -0,0 +1,23 @@ +package changes + +var _ Change = OperationLongRunningAdded{} + +// OperationLongRunningAdded defines when an existing Operation is now Long Running. +type OperationLongRunningAdded struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation which is now a Long Running Operation. + OperationName string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (OperationLongRunningAdded) IsBreaking() bool { + return true +} diff --git a/tools/data-api-differ/internal/changes/operation_long_running_removed.go b/tools/data-api-differ/internal/changes/operation_long_running_removed.go new file mode 100644 index 00000000000..92aca5311e8 --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_long_running_removed.go @@ -0,0 +1,23 @@ +package changes + +var _ Change = OperationLongRunningRemoved{} + +// OperationLongRunningRemoved defines when an existing Operation is no longer Long Running. +type OperationLongRunningRemoved struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation which is no longer a Long Running Operation. + OperationName string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (OperationLongRunningRemoved) IsBreaking() bool { + return true +} diff --git a/tools/data-api-differ/internal/changes/operation_method_changed.go b/tools/data-api-differ/internal/changes/operation_method_changed.go new file mode 100644 index 00000000000..3fb883ce386 --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_method_changed.go @@ -0,0 +1,31 @@ +package changes + +var _ Change = OperationMethodChanged{} + +// OperationMethodChanged defines when the HTTP Method used for an existing Operation changes. +type OperationMethodChanged struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation which has an updated HTTP Method. + OperationName string + + // OldValue specifies the old/existing HTTP Method for this Operation. + OldValue string + + // NewValue specifies the new/updated HTTP Method for this Operation. + NewValue string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (OperationMethodChanged) IsBreaking() bool { + // If the HTTP Method for the Operation has changed this is an entirely different Operation + // and this is a breaking change. + return true +} diff --git a/tools/data-api-differ/internal/changes/operation_options_added.go b/tools/data-api-differ/internal/changes/operation_options_added.go new file mode 100644 index 00000000000..ba29ff60d52 --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_options_added.go @@ -0,0 +1,27 @@ +package changes + +var _ Change = OperationOptionsAdded{} + +// OperationOptionsAdded defines where an existing Operation now supports Options. +type OperationOptionsAdded struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation which has had Options added. + OperationName string + + // NewValue specifies a slice of the new/updated Options for this Operation. + NewValue map[string]string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (OperationOptionsAdded) IsBreaking() bool { + // This will require code changes so is a breaking change + return true +} diff --git a/tools/data-api-differ/internal/changes/operation_options_changed.go b/tools/data-api-differ/internal/changes/operation_options_changed.go new file mode 100644 index 00000000000..3c9f549f82d --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_options_changed.go @@ -0,0 +1,43 @@ +package changes + +var _ Change = OperationOptionsChanged{} + +// OperationOptionsChanged defines an existing Operation which has had its Options changed. +type OperationOptionsChanged struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation which has had its Options changed. + OperationName string + + // OldValue specifies a slice of the old/existing Options for this Operation. + OldValue map[string]string + + // NewValue specifies a slice of the new/updated Options for this Operation. + NewValue map[string]string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (o OperationOptionsChanged) IsBreaking() bool { + // Whether this is a breaking change depends on if the existing Options remain to be supported + for key, value := range o.OldValue { + otherValue, exists := o.NewValue[key] + if !exists { + // if the Option no longer exists it's a breaking change + return true + } + + // if the Option itself has changed this is /potentially/ a breaking change, so flag it + if value != otherValue { + return true + } + } + + return false +} diff --git a/tools/data-api-differ/internal/changes/operation_options_removed.go b/tools/data-api-differ/internal/changes/operation_options_removed.go new file mode 100644 index 00000000000..df4137e939e --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_options_removed.go @@ -0,0 +1,27 @@ +package changes + +var _ Change = OperationOptionsRemoved{} + +// OperationOptionsRemoved defines where an existing Operation no longer supports Options. +type OperationOptionsRemoved struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation which no longer supports Options. + OperationName string + + // OldValue specifies a slice of the old/existing Options for this Operation. + OldValue map[string]string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (OperationOptionsRemoved) IsBreaking() bool { + // This will require code changes so is a breaking change + return true +} diff --git a/tools/data-api-differ/internal/changes/operation_pagination_field_changed.go b/tools/data-api-differ/internal/changes/operation_pagination_field_changed.go new file mode 100644 index 00000000000..302dd761588 --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_pagination_field_changed.go @@ -0,0 +1,32 @@ +package changes + +var _ Change = OperationPaginationFieldChanged{} + +// OperationPaginationFieldChanged defines where an existing Operation has an updated value for the +// Pagination Field. +type OperationPaginationFieldChanged struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation where the Pagination Field has changed. + OperationName string + + // OldValue specifies the old/existing value for the Pagination Field for this operation. + OldValue string + + // NewValue specifies the new/updated value for the Pagination Field for this operation. + NewValue string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (OperationPaginationFieldChanged) IsBreaking() bool { + // This is going to cause a breakage - either to the existing code (as used today, which would be + // a regression) - or in the updated code - and will require manual investigation. + return true +} diff --git a/tools/data-api-differ/internal/changes/operation_removed.go b/tools/data-api-differ/internal/changes/operation_removed.go new file mode 100644 index 00000000000..c50f690255c --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_removed.go @@ -0,0 +1,26 @@ +package changes + +var _ Change = OperationRemoved{} + +// OperationRemoved defines an Operation which has been removed from an existing API Resource. +type OperationRemoved struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation which has been removed. + OperationName string + + // Uri specifies the URI of the Operation which has been removed. + Uri string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (OperationRemoved) IsBreaking() bool { + return true +} diff --git a/tools/data-api-differ/internal/changes/operation_request_object_added.go b/tools/data-api-differ/internal/changes/operation_request_object_added.go new file mode 100644 index 00000000000..0028bbb5c8f --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_request_object_added.go @@ -0,0 +1,27 @@ +package changes + +var _ Change = OperationRequestObjectAdded{} + +// OperationRequestObjectAdded defines that a Request Object has been added to an existing Operation. +type OperationRequestObjectAdded struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation which now has a Request Object. + OperationName string + + // NewRequestObject specifies the new/updated value for the Request Object. + NewRequestObject string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (OperationRequestObjectAdded) IsBreaking() bool { + // This will require code changes + return true +} diff --git a/tools/data-api-differ/internal/changes/operation_request_object_changed.go b/tools/data-api-differ/internal/changes/operation_request_object_changed.go new file mode 100644 index 00000000000..eb7c41a3773 --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_request_object_changed.go @@ -0,0 +1,29 @@ +package changes + +var _ Change = OperationRequestObjectChanged{} + +// OperationRequestObjectChanged defines an existing Operation where the Request Object has changed. +type OperationRequestObjectChanged struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation which now has a Request Object. + OperationName string + + // NewRequestObject specifies the new/updated value for the Request Object. + NewRequestObject string + + // OldRequestObject specifies the old/existing value for the Request Object. + OldRequestObject string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (OperationRequestObjectChanged) IsBreaking() bool { + return true +} diff --git a/tools/data-api-differ/internal/changes/operation_request_object_removed.go b/tools/data-api-differ/internal/changes/operation_request_object_removed.go new file mode 100644 index 00000000000..0887121a5de --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_request_object_removed.go @@ -0,0 +1,27 @@ +package changes + +var _ Change = OperationRequestObjectRemoved{} + +// OperationRequestObjectRemoved defines that a Request Object has been removed from an existing Operation. +type OperationRequestObjectRemoved struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation which no longer has a Request Object. + OperationName string + + // OldRequestObject specifies the old/existing value for the Request Object. + OldRequestObject string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (OperationRequestObjectRemoved) IsBreaking() bool { + // This will require code changes + return true +} diff --git a/tools/data-api-differ/internal/changes/operation_resource_id_added.go b/tools/data-api-differ/internal/changes/operation_resource_id_added.go new file mode 100644 index 00000000000..810c12b1f61 --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_resource_id_added.go @@ -0,0 +1,26 @@ +package changes + +var _ Change = OperationResourceIdAdded{} + +// OperationResourceIdAdded defines when a Resource Id is added to an existing Operation. +type OperationResourceIdAdded struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation which now has a Resource Id Name. + OperationName string + + // NewResourceIdName specifies the new/updated value for the Resource Id Name. + NewResourceIdName string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (OperationResourceIdAdded) IsBreaking() bool { + return true +} diff --git a/tools/data-api-differ/internal/changes/operation_resource_id_changed.go b/tools/data-api-differ/internal/changes/operation_resource_id_changed.go new file mode 100644 index 00000000000..e6dea796a92 --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_resource_id_changed.go @@ -0,0 +1,39 @@ +package changes + +var _ Change = OperationResourceIdChanged{} + +// OperationResourceIdChanged defines when the Resource Id for an Operation has changed value. +// +// This is different to OperationResourceIdRenamed because in this instance the underlying +// Resource ID Value (i.e. URI) has changed (i.e. a new Resource) - rather than being renamed. +type OperationResourceIdChanged struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation which has a new/updated Resource Id Name. + OperationName string + + // OldResourceIdName specifies the old/existing value for the Resource ID Name. + OldResourceIdName string + + // OldValue specifies the old/existing value for this Resource ID. + OldValue string + + // NewResourceIdName specifies the new/updated value for the Resource ID Name. + NewResourceIdName string + + // NewValue specifies the new/updated value for this Resource ID. + NewValue string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (OperationResourceIdChanged) IsBreaking() bool { + // If the ResourceId used by this Operation has changed this would require code changes. + return true +} diff --git a/tools/data-api-differ/internal/changes/operation_resource_id_removed.go b/tools/data-api-differ/internal/changes/operation_resource_id_removed.go new file mode 100644 index 00000000000..89a3f79376c --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_resource_id_removed.go @@ -0,0 +1,27 @@ +package changes + +var _ Change = OperationResourceIdRemoved{} + +// OperationResourceIdRemoved defines that an existing Operation no longer requires a Resource ID. +type OperationResourceIdRemoved struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation which now has a Resource Id Name. + OperationName string + + // OldResourceIdName specifies the old/existing value for the Resource Id Name. + OldResourceIdName string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (OperationResourceIdRemoved) IsBreaking() bool { + // If a Resource ID is removed from an Operation then this will be a breaking change. + return true +} diff --git a/tools/data-api-differ/internal/changes/operation_resource_id_renamed.go b/tools/data-api-differ/internal/changes/operation_resource_id_renamed.go new file mode 100644 index 00000000000..64e780e50df --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_resource_id_renamed.go @@ -0,0 +1,35 @@ +package changes + +var _ Change = OperationResourceIdRenamed{} + +// OperationResourceIdRenamed defines when the Resource Id for an Operation has been renamed. +// +// This is different to OperationResourceIdChanged because the Resource ID is semantically +// the same - therefore we're targeting the same Resource - but this is an internal-only change +// thus whilst this IS a breaking change (to the code) it's not a breaking change in the API. +type OperationResourceIdRenamed struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation which has a new/updated Resource Id Name. + OperationName string + + // NewResourceIdName specifies the new/updated value for the Resource Id Name. + NewResourceIdName string + + // OldResourceIdName specifies the old/existing value for the Resource Id Name. + OldResourceIdName string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (OperationResourceIdRenamed) IsBreaking() bool { + // If the Resource Id Name has been renamed (but is the same value under the hood) then this + // will require code changes to fix. + return true +} diff --git a/tools/data-api-differ/internal/changes/operation_response_object_added.go b/tools/data-api-differ/internal/changes/operation_response_object_added.go new file mode 100644 index 00000000000..6adad4af564 --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_response_object_added.go @@ -0,0 +1,27 @@ +package changes + +var _ Change = OperationResponseObjectAdded{} + +// OperationResponseObjectAdded defines that a Response Object has been added to an existing Operation. +type OperationResponseObjectAdded struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation which now has a Response Object. + OperationName string + + // NewResponseObject specifies the new/updated value for the Response Object. + NewResponseObject string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (OperationResponseObjectAdded) IsBreaking() bool { + // This will require code changes + return true +} diff --git a/tools/data-api-differ/internal/changes/operation_response_object_changed.go b/tools/data-api-differ/internal/changes/operation_response_object_changed.go new file mode 100644 index 00000000000..e3e698a8ab4 --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_response_object_changed.go @@ -0,0 +1,29 @@ +package changes + +var _ Change = OperationResponseObjectChanged{} + +// OperationResponseObjectChanged defines an existing Operation where the Response Object has changed. +type OperationResponseObjectChanged struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation which now has a Response Object. + OperationName string + + // NewResponseObject specifies the new/updated value for the Response Object. + NewResponseObject string + + // OldResponseObject specifies the old/existing value for the Response Object. + OldResponseObject string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (OperationResponseObjectChanged) IsBreaking() bool { + return true +} diff --git a/tools/data-api-differ/internal/changes/operation_response_object_removed.go b/tools/data-api-differ/internal/changes/operation_response_object_removed.go new file mode 100644 index 00000000000..733ab9caa07 --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_response_object_removed.go @@ -0,0 +1,27 @@ +package changes + +var _ Change = OperationResponseObjectRemoved{} + +// OperationResponseObjectRemoved defines that a Response Object has been removed from an existing Operation. +type OperationResponseObjectRemoved struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation which no longer has a Response Object. + OperationName string + + // OldResponseObject specifies the old/existing value for the Response Object. + OldResponseObject string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (OperationResponseObjectRemoved) IsBreaking() bool { + // This will require code changes + return true +} diff --git a/tools/data-api-differ/internal/changes/operation_uri_suffix_added.go b/tools/data-api-differ/internal/changes/operation_uri_suffix_added.go new file mode 100644 index 00000000000..7e21a443ca0 --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_uri_suffix_added.go @@ -0,0 +1,27 @@ +package changes + +var _ Change = OperationUriSuffixAdded{} + +// OperationUriSuffixAdded defines when an existing Operation now has a Uri Suffix. +type OperationUriSuffixAdded struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation where the Uri Suffix has been added. + OperationName string + + // NewValue specifies the new/updated Uri Suffix for this Operation. + NewValue string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (OperationUriSuffixAdded) IsBreaking() bool { + // This would be operating on a different Resource, so is a breaking change. + return true +} diff --git a/tools/data-api-differ/internal/changes/operation_uri_suffix_changed.go b/tools/data-api-differ/internal/changes/operation_uri_suffix_changed.go new file mode 100644 index 00000000000..5e3976cb858 --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_uri_suffix_changed.go @@ -0,0 +1,30 @@ +package changes + +var _ Change = OperationUriSuffixChanged{} + +// OperationUriSuffixChanged defines when an existing Operation has an updated Uri Suffix. +type OperationUriSuffixChanged struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation where the Uri Suffix has changed. + OperationName string + + // OldValue specifies the old/existing Uri Suffix for this Operation. + OldValue string + + // NewValue specifies the new/updated Uri Suffix for this Operation. + NewValue string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (OperationUriSuffixChanged) IsBreaking() bool { + // This would be operating on a different Resource, so is a breaking change. + return true +} diff --git a/tools/data-api-differ/internal/changes/operation_uri_suffix_removed.go b/tools/data-api-differ/internal/changes/operation_uri_suffix_removed.go new file mode 100644 index 00000000000..f9f62bf4300 --- /dev/null +++ b/tools/data-api-differ/internal/changes/operation_uri_suffix_removed.go @@ -0,0 +1,27 @@ +package changes + +var _ Change = OperationUriSuffixRemoved{} + +// OperationUriSuffixRemoved defines when an existing Operation no longer has a Uri Suffix. +type OperationUriSuffixRemoved struct { + // ServiceName specifies the name of the Service which contains this Operation. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Operation. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Operation. + ResourceName string + + // OperationName specifies the name of the Operation where the Uri Suffix has changed. + OperationName string + + // OldValue specifies the old/existing Uri Suffix for this Operation which has been removed. + OldValue string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (OperationUriSuffixRemoved) IsBreaking() bool { + // This would be operating on a different Resource, so is a breaking change. + return true +} diff --git a/tools/data-api-differ/internal/changes/resource_id_added.go b/tools/data-api-differ/internal/changes/resource_id_added.go new file mode 100644 index 00000000000..da56dd85431 --- /dev/null +++ b/tools/data-api-differ/internal/changes/resource_id_added.go @@ -0,0 +1,29 @@ +package changes + +var _ Change = ResourceIdAdded{} + +// ResourceIdAdded defines information about a new Resource ID. +type ResourceIdAdded struct { + // ServiceName specifies the name of the Service which contains this + // Resource ID. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this + // Resource ID. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this + // Resource ID. + ResourceName string + + // ResourceIdName specifies the name of the Resource ID which has been added. + ResourceIdName string + + // ResourceIdValue specifies the value used for this Resource ID e.g. `/foo/{bar}` + ResourceIdValue string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ResourceIdAdded) IsBreaking() bool { + return false +} diff --git a/tools/data-api-differ/internal/changes/resource_id_common_id_added.go b/tools/data-api-differ/internal/changes/resource_id_common_id_added.go new file mode 100644 index 00000000000..ec76fbaadb4 --- /dev/null +++ b/tools/data-api-differ/internal/changes/resource_id_common_id_added.go @@ -0,0 +1,34 @@ +package changes + +var _ Change = ResourceIdCommonIdAdded{} + +// ResourceIdCommonIdAdded defines that a Resource ID which existed previously has +// been updated to be a Common ID. +type ResourceIdCommonIdAdded struct { + // ServiceName specifies the name of the Service which contained this + // Resource ID. + ServiceName string + + // ApiVersion specifies the name of the API Version which contained this + // Resource ID. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contained this + // Resource ID. + ResourceName string + + // ResourceIdName specifies the name of the Resource ID which is now a Common ID. + ResourceIdName string + + // CommonAliasName specifies the name of the Common Alias for this Resource ID. + CommonAliasName string + + // ResourceIdValue specifies the value used for this Resource ID e.g. `/foo/{bar}` + ResourceIdValue string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ResourceIdCommonIdAdded) IsBreaking() bool { + // If a Resource ID is now a Common ID this is going to require code changes + return true +} diff --git a/tools/data-api-differ/internal/changes/resource_id_common_id_changed.go b/tools/data-api-differ/internal/changes/resource_id_common_id_changed.go new file mode 100644 index 00000000000..d802366d32f --- /dev/null +++ b/tools/data-api-differ/internal/changes/resource_id_common_id_changed.go @@ -0,0 +1,40 @@ +package changes + +var _ Change = ResourceIdCommonIdChanged{} + +// ResourceIdCommonIdChanged defines that an existing Resource ID that previously used a Common ID now +// references a different Common ID - this would happen when a Common ID gets renamed. +type ResourceIdCommonIdChanged struct { + // ServiceName specifies the name of the Service which contained this + // Resource ID. + ServiceName string + + // ApiVersion specifies the name of the API Version which contained this + // Resource ID. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contained this + // Resource ID. + ResourceName string + + // ResourceIdName specifies the name of the Resource ID which is now a Common ID. + ResourceIdName string + + // NewCommonAliasName specifies the new/updated value for the Common Alias associated with this Resource ID. + NewCommonAliasName string + + // OldCommonAliasName specifies the old/existing value for the Common Alias associated with this Resource ID. + OldCommonAliasName string + + // OldValue specifies the old/existing value for this Resource ID. + OldValue string + + // NewValue specifies the new/updated value for this Resource ID. + NewValue string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ResourceIdCommonIdChanged) IsBreaking() bool { + // This is going to require code changes + return true +} diff --git a/tools/data-api-differ/internal/changes/resource_id_common_id_removed.go b/tools/data-api-differ/internal/changes/resource_id_common_id_removed.go new file mode 100644 index 00000000000..78b814817b9 --- /dev/null +++ b/tools/data-api-differ/internal/changes/resource_id_common_id_removed.go @@ -0,0 +1,34 @@ +package changes + +var _ Change = ResourceIdCommonIdRemoved{} + +// ResourceIdCommonIdRemoved defines that a Resource ID which existed previously is no +// longer a Common ID. +type ResourceIdCommonIdRemoved struct { + // ServiceName specifies the name of the Service which contained this + // Resource ID. + ServiceName string + + // ApiVersion specifies the name of the API Version which contained this + // Resource ID. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contained this + // Resource ID. + ResourceName string + + // ResourceIdName specifies the name of the Resource ID which is no longer a Common ID. + ResourceIdName string + + // CommonAliasName specifies the name of the Common Alias for this Resource ID. + CommonAliasName string + + // ResourceIdValue specifies the value used for this Resource ID e.g. `/foo/{bar}` + ResourceIdValue string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ResourceIdCommonIdRemoved) IsBreaking() bool { + // If a Resource ID is no longer a Common ID this is going to require code changes + return true +} diff --git a/tools/data-api-differ/internal/changes/resource_id_removed.go b/tools/data-api-differ/internal/changes/resource_id_removed.go new file mode 100644 index 00000000000..a5e01912b19 --- /dev/null +++ b/tools/data-api-differ/internal/changes/resource_id_removed.go @@ -0,0 +1,29 @@ +package changes + +var _ Change = ResourceIdRemoved{} + +// ResourceIdRemoved defines information about a Resource ID that has been removed. +type ResourceIdRemoved struct { + // ServiceName specifies the name of the Service which contained this + // Resource ID. + ServiceName string + + // ApiVersion specifies the name of the API Version which contained this + // Resource ID. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contained this + // Resource ID. + ResourceName string + + // ResourceIdName specifies the name of the Resource ID which has been removed. + ResourceIdName string + + // ResourceIdValue specifies the value used for this Resource ID e.g. `/foo/{bar}` + ResourceIdValue string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ResourceIdRemoved) IsBreaking() bool { + return true +} diff --git a/tools/data-api-differ/internal/changes/resource_id_segment_changed_value.go b/tools/data-api-differ/internal/changes/resource_id_segment_changed_value.go new file mode 100644 index 00000000000..73eb1edd03a --- /dev/null +++ b/tools/data-api-differ/internal/changes/resource_id_segment_changed_value.go @@ -0,0 +1,34 @@ +package changes + +var _ Change = ResourceIdSegmentChangedValue{} + +// ResourceIdSegmentChangedValue defines where an existing Resource ID Segment changes its value. +// For example there's an updated Name for the Segment, or where the Segment Type changes from a +// String -> Constant. +type ResourceIdSegmentChangedValue struct { + // ServiceName specifies the name of the Service which contains this Resource ID. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Resource ID. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Resource ID. + ResourceName string + + // ResourceIdName specifies the name of the Resource ID which contains the Segment that has changed. + ResourceIdName string + + // SegmentIndex specifies the index of this Resource ID Segment which has changed. + SegmentIndex int + + // OldValue specifies the old/existing value for this Resource ID Segment. + OldValue string + + // NewValue specifies the new/updated value for this Resource ID Segment. + NewValue string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (r ResourceIdSegmentChangedValue) IsBreaking() bool { + return true +} diff --git a/tools/data-api-differ/internal/changes/resource_id_segments_changed_length.go b/tools/data-api-differ/internal/changes/resource_id_segments_changed_length.go new file mode 100644 index 00000000000..2d75b2d3bc0 --- /dev/null +++ b/tools/data-api-differ/internal/changes/resource_id_segments_changed_length.go @@ -0,0 +1,33 @@ +package changes + +var _ Change = ResourceIdSegmentsChangedLength{} + +// ResourceIdSegmentsChangedLength defines when an existing Resource ID has an entirely different set +// of Resource ID Segments - because the Length of the Resource ID Segments differs between the older +// and updated Resource ID. +type ResourceIdSegmentsChangedLength struct { + // ServiceName specifies the name of the Service which contains this Resource ID. + ServiceName string + + // ApiVersion specifies the name of the API Version which contains this Resource ID. + ApiVersion string + + // ResourceName specifies the name of the API Resource which contains this Resource ID. + ResourceName string + + // ResourceIdName specifies the name of the Resource ID which contains the Segment that has changed. + ResourceIdName string + + // OldValue specifies the old/existing value for this Resource ID Segment. + OldValue []string + + // NewValue specifies the new/updated value for this Resource ID Segment. + NewValue []string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (r ResourceIdSegmentsChangedLength) IsBreaking() bool { + // If an existing Resource ID has added/removed a Segment this is a breaking change + // which requires additional investigation/understanding. + return true +} diff --git a/tools/data-api-differ/internal/changes/service_added.go b/tools/data-api-differ/internal/changes/service_added.go new file mode 100644 index 00000000000..3df00e61c1d --- /dev/null +++ b/tools/data-api-differ/internal/changes/service_added.go @@ -0,0 +1,14 @@ +package changes + +var _ Change = ServiceAdded{} + +// ServiceAdded defines information about a new Service. +type ServiceAdded struct { + // ServiceName is the name of the Service (e.g. `Compute`). + ServiceName string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ServiceAdded) IsBreaking() bool { + return false +} diff --git a/tools/data-api-differ/internal/changes/service_removed.go b/tools/data-api-differ/internal/changes/service_removed.go new file mode 100644 index 00000000000..5e4395de082 --- /dev/null +++ b/tools/data-api-differ/internal/changes/service_removed.go @@ -0,0 +1,14 @@ +package changes + +var _ Change = ServiceRemoved{} + +// ServiceRemoved defines information about a Service which has been removed. +type ServiceRemoved struct { + // ServiceName is the name of the Service (e.g. `Compute`). + ServiceName string +} + +// IsBreaking returns whether this Change is considered a Breaking Change. +func (ServiceRemoved) IsBreaking() bool { + return true +} From 8e15d3846292fb15fdfadf53dc133419cea6bad0 Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Wed, 6 Dec 2023 09:42:30 +0100 Subject: [PATCH 03/20] tools/data-api-differ: adding a thin wrapper around the Data API Models for now This is necessary until the Data API SDK is updated - but can be reconciled later --- .../data-api-differ/internal/dataapi/data.go | 132 ++++++++++++++++++ .../internal/dataapi/data_api_cmd.go | 80 +++++++++++ .../internal/dataapi/models.go | 38 +++++ .../data-api-differ/internal/dataapi/todo.go | 5 + 4 files changed, 255 insertions(+) create mode 100644 tools/data-api-differ/internal/dataapi/data.go create mode 100644 tools/data-api-differ/internal/dataapi/data_api_cmd.go create mode 100644 tools/data-api-differ/internal/dataapi/models.go create mode 100644 tools/data-api-differ/internal/dataapi/todo.go diff --git a/tools/data-api-differ/internal/dataapi/data.go b/tools/data-api-differ/internal/dataapi/data.go new file mode 100644 index 00000000000..17f793e63d4 --- /dev/null +++ b/tools/data-api-differ/internal/dataapi/data.go @@ -0,0 +1,132 @@ +package dataapi + +import ( + "fmt" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" + "github.com/hashicorp/pandora/tools/sdk/resourcemanager" + "math/rand" +) + +// ParseDataFromPath launches the Data API using inputPath as the API Definitions directory. +func ParseDataFromPath(dataApiBinary, inputPath string) (*Data, error) { + port := randomPortNumber() + log.Logger.Info("Launching Data API..") + dataApi := newDataApiCmd(dataApiBinary, port, inputPath) + if err := dataApi.launchAndWait(); err != nil { + return nil, fmt.Errorf("launching the Data API: %+v", err) + } + defer dataApi.shutdown() + + // TODO: once the client is updated to take a `SourceData` argument - update this to target _ALL_ endpoints not just Resource Manager + client := resourcemanager.NewResourceManagerClient(dataApi.endpoint) + resourceManagerServices, err := populateResourceManagerServices(client) + if err != nil { + return nil, fmt.Errorf("populating Resource Manager Services: %+v", err) + } + + return &Data{ + ResourceManagerServices: *resourceManagerServices, + }, nil +} + +// populateResourceManagerServices populates the list of Resource Manager Services from the Data API. +func populateResourceManagerServices(client resourcemanager.Client) (*map[string]ServiceData, error) { + log.Logger.Info("Retrieving Services..") + servicesResp, err := client.Services().Get() + if err != nil { + return nil, fmt.Errorf("retrieving Services: %+v", err) + } + + // We need to obtain and then flatten this data - arguably this could/should live in the SDK - but that's a question + // for another day. + log.Logger.Trace("Processing Services..") + services := make(map[string]ServiceData) + for serviceName, serviceSummary := range *servicesResp { + log.Logger.Trace(fmt.Sprintf("Populating Details for Service %q..", serviceName)) + service, err := populateServiceDetails(client, serviceSummary) + if err != nil { + return nil, fmt.Errorf("populating the Service Details for %q: %+v", serviceName, err) + } + + services[serviceName] = *service + } + + return &services, nil +} + +// populateServiceDetails populates ServiceData for the specified Service. +func populateServiceDetails(client resourcemanager.Client, summary resourcemanager.ServiceSummary) (*ServiceData, error) { + serviceDetails, err := client.ServiceDetails().Get(summary) + if err != nil { + return nil, fmt.Errorf("retrieving details: %+v", err) + } + + apiVersions := make(map[string]ApiVersionData) + for apiVersion, versionSummary := range serviceDetails.Versions { + log.Logger.Trace(fmt.Sprintf("Populating Details for API Version %q..", apiVersion)) + apiVersionDetails, err := populateApiVersionDetails(client, versionSummary) + if err != nil { + return nil, fmt.Errorf("populating details for API Version %q: %+v", apiVersion, err) + } + apiVersions[apiVersion] = *apiVersionDetails + } + + return &ServiceData{ + ApiVersions: apiVersions, + Generate: summary.Generate, + ResourceProvider: serviceDetails.ResourceProvider, + TerraformPackageName: serviceDetails.TerraformPackageName, + }, nil +} + +// populateApiVersionDetails populates ApiVersionData for this API Version. +func populateApiVersionDetails(client resourcemanager.Client, summary resourcemanager.ServiceVersion) (*ApiVersionData, error) { + resp, err := client.ServiceVersion().Get(summary) + if err != nil { + return nil, fmt.Errorf("retrieving details: %+v", err) + } + + resources := make(map[string]ApiResourceData) + for resourceName, resourceDetails := range resp.Resources { + log.Logger.Trace(fmt.Sprintf("Populating details for API Resource %q..", resourceName)) + resource, err := populateApiResourceDetails(client, resourceDetails) + if err != nil { + return nil, fmt.Errorf("populating details for API Resource %q: %+v", resourceName, err) + } + resources[resourceName] = *resource + } + + return &ApiVersionData{ + Generate: summary.Generate, + Preview: summary.Preview, + Resources: resources, + Source: resp.Source, + }, nil +} + +// populateApiResourceDetails populates ApiResourceData (containing the Operations and Schema) for the specified API Resource. +func populateApiResourceDetails(client resourcemanager.Client, details resourcemanager.ResourceSummary) (*ApiResourceData, error) { + log.Logger.Trace("Retrieving API Operation Details..") + operationsResp, err := client.ApiOperations().Get(details) + if err != nil { + return nil, fmt.Errorf("retrieving API Operation details: %+v", err) + } + + log.Logger.Trace("Retrieving API Schema Details..") + schemaResp, err := client.ApiSchema().Get(details) + if err != nil { + return nil, fmt.Errorf("retrieving API Schema details: %+v", err) + } + + return &ApiResourceData{ + Constants: schemaResp.Constants, + Models: schemaResp.Models, + ResourceIds: schemaResp.ResourceIds, + Operations: operationsResp.Operations, + }, nil +} + +// randomPortNumber returns a random port number - this allows launching a unique instance each time +func randomPortNumber() int { + return rand.Intn(50000-10000) + 10000 +} diff --git a/tools/data-api-differ/internal/dataapi/data_api_cmd.go b/tools/data-api-differ/internal/dataapi/data_api_cmd.go new file mode 100644 index 00000000000..99b85470cc0 --- /dev/null +++ b/tools/data-api-differ/internal/dataapi/data_api_cmd.go @@ -0,0 +1,80 @@ +package dataapi + +import ( + "fmt" + "io" + "net/http" + "os" + "os/exec" + "strings" + "time" + + "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" +) + +// dataApiCmd is a wrapper for managing Data API V2 which picks a unique port and serves the API. +type dataApiCmd struct { + cmd *exec.Cmd + endpoint string + port int +} + +// newDataApiCmd prepares the Data API (V2) to be launched. +func newDataApiCmd(binary string, port int, workingDirectory string) *dataApiCmd { + args := []string{ + "serve", + fmt.Sprintf("--data-directory=%s", workingDirectory), + } + log.Logger.Debug(fmt.Sprintf("Launching %q with args %q..", binary, strings.Join(args, " "))) + cmd := exec.Command(binary, args...) + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, fmt.Sprintf("PANDORA_API_PORT=%d", port)) + + // we could pipe this elsewhere, but this is probably sufficient to ignore for now + cmd.Stderr = io.Discard + cmd.Stdout = io.Discard + + return &dataApiCmd{ + cmd: cmd, + endpoint: fmt.Sprintf("http://localhost:%d", port), + port: port, + } +} + +// launchAndWait launches the Data API and then polls until it's fully available/online. +func (p *dataApiCmd) launchAndWait() error { + log.Logger.Trace(fmt.Sprintf("Launching Data API on Port %d", p.port)) + if err := p.cmd.Start(); err != nil { + return fmt.Errorf("launching Data API: %+v", err) + } + log.Logger.Trace(fmt.Sprintf("Data API is launched at %q.", p.endpoint)) + + // then ensure it's accepting requests prior to hitting it (e.g. firewalls) + for attempts := 0; attempts < 30; attempts++ { + log.Logger.Trace(fmt.Sprintf("Checking the health of the Data API - attempt %d/30", attempts+1)) + resp, err := http.Get(fmt.Sprintf("%s/v1/health", p.endpoint)) + if err != nil { + log.Logger.Trace(fmt.Sprintf("API not ready - waiting 1s to try again (%+v)", err)) + time.Sleep(1 * time.Second) + continue + } + + if resp.StatusCode == http.StatusOK { + log.Logger.Trace("API available") + return nil + } + + return fmt.Errorf("unexpected status code %d", resp.StatusCode) + } + + return fmt.Errorf("the Data API didn't return a 200 OK within 30 seconds") +} + +// shutdown will terminate the Data API process if launched. +func (p *dataApiCmd) shutdown() error { + if p.cmd.Process != nil { + p.cmd.Process.Kill() + } + + return nil +} diff --git a/tools/data-api-differ/internal/dataapi/models.go b/tools/data-api-differ/internal/dataapi/models.go new file mode 100644 index 00000000000..3e779a0bde7 --- /dev/null +++ b/tools/data-api-differ/internal/dataapi/models.go @@ -0,0 +1,38 @@ +package dataapi + +import "github.com/hashicorp/pandora/tools/sdk/resourcemanager" + +type Data struct { + ResourceManagerServices map[string]ServiceData +} + +type ServiceData struct { + // ApiVersions is a map of the ApiVersion (key) to apiVersionData (value) + ApiVersions map[string]ApiVersionData + + // Generate specifies whether this should be generated + Generate bool + + // ResourceProvider is the Resource Provider this service represents + ResourceProvider string + + // TerraformPackageName is the name of the Service Package within + // the Terraform Provider associated with this service. + TerraformPackageName *string + + // TODO: support for Terraform Resources in the future +} + +type ApiVersionData struct { + Generate bool + Preview bool + Resources map[string]ApiResourceData + Source resourcemanager.ApiDefinitionsSource +} + +type ApiResourceData struct { + Constants map[string]resourcemanager.ConstantDetails + Models map[string]resourcemanager.ModelDetails + ResourceIds map[string]resourcemanager.ResourceIdDefinition + Operations map[string]resourcemanager.ApiOperation +} diff --git a/tools/data-api-differ/internal/dataapi/todo.go b/tools/data-api-differ/internal/dataapi/todo.go new file mode 100644 index 00000000000..7339b26b746 --- /dev/null +++ b/tools/data-api-differ/internal/dataapi/todo.go @@ -0,0 +1,5 @@ +package dataapi + +// TODO: this package can disappear/be merged into the updated SDK once that's available +// TODO: update the import path to use the updated API Models once split out +// TODO: update this to obtain ALL the Data Sources (Resource Manager/Microsoft Graph) once available From a3bdb8b5a376b0070a26f0b7021331695c159ba1 Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Wed, 6 Dec 2023 09:43:07 +0100 Subject: [PATCH 04/20] tools/data-api-differ: adding a Logger for logging purposes This'll need to be configured both in any Tests and the Main function, but allows this to be switched out as needed --- tools/data-api-differ/internal/log/logger.go | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 tools/data-api-differ/internal/log/logger.go diff --git a/tools/data-api-differ/internal/log/logger.go b/tools/data-api-differ/internal/log/logger.go new file mode 100644 index 00000000000..48456578e7f --- /dev/null +++ b/tools/data-api-differ/internal/log/logger.go @@ -0,0 +1,5 @@ +package log + +import "github.com/hashicorp/go-hclog" + +var Logger hclog.Logger From 836764b93a8bf0ee7830b2b20ef196a4a1184760 Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Wed, 6 Dec 2023 09:43:53 +0100 Subject: [PATCH 05/20] tools/data-api-differ: building out the initial version of the `differ` This allows detecting the changes between two sets of Resource Manager Services, but in the future can be extended to also support Microsoft Graph when the SDK exposes it. This allows detecting of changes in: * Services * API Versions * API Resources * Constants * Models * Operations (and Operation Options) * Resource IDs (and Resource ID Segments) This currently _doesn't_ support tracking changes to Terraform Resources, but that seems like a good logical extension point for the future. --- .../resource_id_segments_changed_length.go | 2 +- .../data-api-differ/internal/differ/differ.go | 28 + .../internal/differ/differ_api_resource.go | 107 ++ .../differ/differ_api_resource_test.go | 54 + .../internal/differ/differ_api_versions.go | 83 ++ .../differ/differ_api_versions_test.go | 52 + .../internal/differ/differ_constants.go | 137 +++ .../internal/differ/differ_constants_test.go | 246 ++++ .../internal/differ/differ_fields.go | 132 ++ .../internal/differ/differ_fields_test.go | 222 ++++ .../internal/differ/differ_helpers.go | 32 + .../internal/differ/differ_helpers_test.go | 77 ++ .../internal/differ/differ_models.go | 162 +++ .../internal/differ/differ_models_test.go | 200 +++ .../internal/differ/differ_operations.go | 584 +++++++++ .../internal/differ/differ_operations_test.go | 1072 +++++++++++++++++ .../internal/differ/differ_resource_ids.go | 185 +++ .../differ/differ_resource_ids_test.go | 370 ++++++ .../internal/differ/differ_services.go | 86 ++ .../internal/differ/differ_services_test.go | 48 + .../internal/differ/differ_test.go | 751 ++++++++++++ .../internal/differ/interface.go | 26 + .../data-api-differ/internal/differ/models.go | 18 + 23 files changed, 4673 insertions(+), 1 deletion(-) create mode 100644 tools/data-api-differ/internal/differ/differ.go create mode 100644 tools/data-api-differ/internal/differ/differ_api_resource.go create mode 100644 tools/data-api-differ/internal/differ/differ_api_resource_test.go create mode 100644 tools/data-api-differ/internal/differ/differ_api_versions.go create mode 100644 tools/data-api-differ/internal/differ/differ_api_versions_test.go create mode 100644 tools/data-api-differ/internal/differ/differ_constants.go create mode 100644 tools/data-api-differ/internal/differ/differ_constants_test.go create mode 100644 tools/data-api-differ/internal/differ/differ_fields.go create mode 100644 tools/data-api-differ/internal/differ/differ_fields_test.go create mode 100644 tools/data-api-differ/internal/differ/differ_helpers.go create mode 100644 tools/data-api-differ/internal/differ/differ_helpers_test.go create mode 100644 tools/data-api-differ/internal/differ/differ_models.go create mode 100644 tools/data-api-differ/internal/differ/differ_models_test.go create mode 100644 tools/data-api-differ/internal/differ/differ_operations.go create mode 100644 tools/data-api-differ/internal/differ/differ_operations_test.go create mode 100644 tools/data-api-differ/internal/differ/differ_resource_ids.go create mode 100644 tools/data-api-differ/internal/differ/differ_resource_ids_test.go create mode 100644 tools/data-api-differ/internal/differ/differ_services.go create mode 100644 tools/data-api-differ/internal/differ/differ_services_test.go create mode 100644 tools/data-api-differ/internal/differ/differ_test.go create mode 100644 tools/data-api-differ/internal/differ/interface.go create mode 100644 tools/data-api-differ/internal/differ/models.go diff --git a/tools/data-api-differ/internal/changes/resource_id_segments_changed_length.go b/tools/data-api-differ/internal/changes/resource_id_segments_changed_length.go index 2d75b2d3bc0..5dd6f28ff51 100644 --- a/tools/data-api-differ/internal/changes/resource_id_segments_changed_length.go +++ b/tools/data-api-differ/internal/changes/resource_id_segments_changed_length.go @@ -26,7 +26,7 @@ type ResourceIdSegmentsChangedLength struct { } // IsBreaking returns whether this Change is considered a Breaking Change. -func (r ResourceIdSegmentsChangedLength) IsBreaking() bool { +func (ResourceIdSegmentsChangedLength) IsBreaking() bool { // If an existing Resource ID has added/removed a Segment this is a breaking change // which requires additional investigation/understanding. return true diff --git a/tools/data-api-differ/internal/differ/differ.go b/tools/data-api-differ/internal/differ/differ.go new file mode 100644 index 00000000000..3f2c6defba6 --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ.go @@ -0,0 +1,28 @@ +package differ + +import ( + "fmt" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/dataapi" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" +) + +type differ struct { +} + +func performDiff(initial dataapi.Data, updated dataapi.Data) (*Result, error) { + diff := differ{} + output := make([]changes.Change, 0) + + // TODO: support additional Data Sources when we're using the updated SDK + log.Logger.Trace("Detecting changes to the Resource Manager Services..") + resourceManagerChanges, err := diff.changesForServices(initial.ResourceManagerServices, updated.ResourceManagerServices) + if err != nil { + return nil, fmt.Errorf("determining changes for the Resource Manager Services: %+v", err) + } + output = append(output, *resourceManagerChanges...) + + return &Result{ + Changes: output, + }, nil +} diff --git a/tools/data-api-differ/internal/differ/differ_api_resource.go b/tools/data-api-differ/internal/differ/differ_api_resource.go new file mode 100644 index 00000000000..791555f8bc0 --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_api_resource.go @@ -0,0 +1,107 @@ +package differ + +import ( + "fmt" + "sort" + + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/dataapi" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" + "github.com/hashicorp/pandora/tools/sdk/resourcemanager" +) + +// changesForApiResources determines the changes between the API Resources within the specified API Version. +func (d differ) changesForApiResources(serviceName, apiVersion string, initial, updated map[string]dataapi.ApiResourceData) (*[]changes.Change, error) { + output := make([]changes.Change, 0) + apiResources := d.uniqueApiResources(initial, updated) + for _, apiResource := range apiResources { + log.Logger.Trace(fmt.Sprintf("Detecting changes in API Resource %q..", apiResource)) + changesForApiResource, err := d.changesForApiResource(serviceName, apiVersion, apiResource, initial, updated) + if err != nil { + return nil, fmt.Errorf("detecting changes to the API Resource %q: %+v", apiResource, err) + } + output = append(output, *changesForApiResource...) + } + return &output, nil +} + +// changesForApiResource determines the changes between two versions of the specified API Resource. +func (d differ) changesForApiResource(serviceName, apiVersion, apiResource string, initial, updated map[string]dataapi.ApiResourceData) (*[]changes.Change, error) { + output := make([]changes.Change, 0) + + oldData, inOldData := initial[apiResource] + updatedData, inUpdatedData := updated[apiResource] + if inOldData && !inUpdatedData { + log.Logger.Trace(fmt.Sprintf("API Resource %q in API Version %q in Service %q was removed", apiResource, apiVersion, serviceName)) + output = append(output, changes.ApiResourceRemoved{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + }) + // no point continuing to diff if it's gone + return &output, nil + } + if !inOldData && inUpdatedData { + log.Logger.Trace(fmt.Sprintf("API Resource %q in API Version %q in Service %q is a new API Version", apiResource, apiVersion, serviceName)) + output = append(output, changes.ApiResourceAdded{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + }) + // intentionally not returning here + } + + // we then need to diff each of the individual components - however note that the old version may not exist + // TODO: switch to using the updated types once in the SDK + initialConstants := make(map[string]resourcemanager.ConstantDetails) + initialModels := make(map[string]resourcemanager.ModelDetails) + initialOperations := make(map[string]resourcemanager.ApiOperation) + initialResourceIds := make(map[string]resourcemanager.ResourceIdDefinition) + if inOldData { + initialConstants = oldData.Constants + initialModels = oldData.Models + initialOperations = oldData.Operations + initialResourceIds = oldData.ResourceIds + } + log.Logger.Trace("Detecting changes to Constants..") + changesInConstants := d.changesForConstants(serviceName, apiVersion, apiResource, initialConstants, updatedData.Constants) + output = append(output, changesInConstants...) + + log.Logger.Trace("Detecting changes to Models..") + changesInModels, err := d.changesForModels(serviceName, apiVersion, apiResource, initialModels, updatedData.Models) + if err != nil { + return nil, fmt.Errorf("determining the changes to Models: %+v", err) + } + output = append(output, *changesInModels...) + + log.Logger.Trace("Detecting changes to Operations..") + changesInOperations, err := d.changesForOperations(serviceName, apiVersion, apiResource, initialOperations, updatedData.Operations, initialResourceIds, updatedData.ResourceIds) + if err != nil { + return nil, fmt.Errorf("determining the changes to Operations: %+v", err) + } + output = append(output, *changesInOperations...) + + log.Logger.Trace("Detecting changes to Resource Ids..") + changesInResourceIds := d.changesForResourceIds(serviceName, apiVersion, apiResource, initialResourceIds, updatedData.ResourceIds) + output = append(output, changesInResourceIds...) + + return &output, nil +} + +// uniqueApiResources returns a unique, sorted list of API Resources from the keys of initial and updated. +func (d differ) uniqueApiResources(initial map[string]dataapi.ApiResourceData, updated map[string]dataapi.ApiResourceData) []string { + uniqueNames := make(map[string]struct{}) + for name := range initial { + uniqueNames[name] = struct{}{} + } + for name := range updated { + uniqueNames[name] = struct{}{} + } + + output := make([]string, 0) + for k := range uniqueNames { + output = append(output, k) + } + sort.Strings(output) + return output +} diff --git a/tools/data-api-differ/internal/differ/differ_api_resource_test.go b/tools/data-api-differ/internal/differ/differ_api_resource_test.go new file mode 100644 index 00000000000..f71ad5ef048 --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_api_resource_test.go @@ -0,0 +1,54 @@ +package differ + +import ( + "testing" + + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/dataapi" +) + +func TestDiff_APIResourceAdded(t *testing.T) { + initial := map[string]dataapi.ApiResourceData{ + "First": {}, + } + updated := map[string]dataapi.ApiResourceData{ + "First": {}, + "Second": {}, + } + actual, err := differ{}.changesForApiResources("Computer", "2020-01-01", initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.ApiResourceAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Second", + }, + } + assertChanges(t, expected, *actual) + assertContainsNoBreakingChanges(t, *actual) +} + +func TestDiff_APIResourceRemoved(t *testing.T) { + initial := map[string]dataapi.ApiResourceData{ + "First": {}, + "Second": {}, + } + updated := map[string]dataapi.ApiResourceData{ + "First": {}, + } + actual, err := differ{}.changesForApiResources("Computer", "2020-01-01", initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.ApiResourceRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Second", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} diff --git a/tools/data-api-differ/internal/differ/differ_api_versions.go b/tools/data-api-differ/internal/differ/differ_api_versions.go new file mode 100644 index 00000000000..f880a898362 --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_api_versions.go @@ -0,0 +1,83 @@ +package differ + +import ( + "fmt" + "sort" + + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/dataapi" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" +) + +// changesForApiVersions determines the changes between the different API versions for the provided Service. +func (d differ) changesForApiVersions(serviceName string, initial map[string]dataapi.ApiVersionData, updated map[string]dataapi.ApiVersionData) (*[]changes.Change, error) { + output := make([]changes.Change, 0) + apiVersions := d.uniqueApiVersions(initial, updated) + for _, apiVersion := range apiVersions { + log.Logger.Trace(fmt.Sprintf("Detecting changes in API Version %q..", apiVersion)) + changesForApiVersion, err := d.changesForApiVersion(serviceName, apiVersion, initial, updated) + if err != nil { + return nil, fmt.Errorf("detecting changes to API Version %q: %+v", apiVersion, err) + } + output = append(output, *changesForApiVersion...) + } + return &output, nil +} + +// changesForApiVersion determines the changes between two different API Versions within a given Service. +func (d differ) changesForApiVersion(serviceName, apiVersion string, initial, updated map[string]dataapi.ApiVersionData) (*[]changes.Change, error) { + output := make([]changes.Change, 0) + + oldData, inOldData := initial[apiVersion] + updatedData, inUpdatedData := updated[apiVersion] + if inOldData && !inUpdatedData { + log.Logger.Trace(fmt.Sprintf("API Version %q in Service %q is not present in the updated data", apiVersion, serviceName)) + output = append(output, changes.ApiVersionRemoved{ + ServiceName: serviceName, + ApiVersion: apiVersion, + }) + // no point continuing to diff if it's gone + return &output, nil + } + if !inOldData && inUpdatedData { + log.Logger.Trace(fmt.Sprintf("API Version %q in Service %q is a new API Version", apiVersion, serviceName)) + output = append(output, changes.ApiVersionAdded{ + ServiceName: serviceName, + ApiVersion: apiVersion, + }) + // intentionally not returning here + } + + // TODO: support diffing `initial.Generate` / `initial.Preview` and `initial.Source` if needed in the future + + // diff the API Resources within this API version - however note that the old version may not exist + initialResources := make(map[string]dataapi.ApiResourceData) + if inOldData { + initialResources = oldData.Resources + } + changesInApiResources, err := d.changesForApiResources(serviceName, apiVersion, initialResources, updatedData.Resources) + if err != nil { + return nil, fmt.Errorf("detecting changes to the API Resources: %+v", err) + } + output = append(output, *changesInApiResources...) + + return &output, nil +} + +// uniqueApiVersions returns a unique, sorted list of API Versions from the keys of initial and updated. +func (d differ) uniqueApiVersions(initial map[string]dataapi.ApiVersionData, updated map[string]dataapi.ApiVersionData) []string { + uniqueNames := make(map[string]struct{}) + for name := range initial { + uniqueNames[name] = struct{}{} + } + for name := range updated { + uniqueNames[name] = struct{}{} + } + + output := make([]string, 0) + for k := range uniqueNames { + output = append(output, k) + } + sort.Strings(output) + return output +} diff --git a/tools/data-api-differ/internal/differ/differ_api_versions_test.go b/tools/data-api-differ/internal/differ/differ_api_versions_test.go new file mode 100644 index 00000000000..32b5da6e72d --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_api_versions_test.go @@ -0,0 +1,52 @@ +package differ + +import ( + "testing" + + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/dataapi" +) + +func TestDiff_APIVersionAdded(t *testing.T) { + initial := map[string]dataapi.ApiVersionData{ + "2020-01-01": {}, + } + updated := map[string]dataapi.ApiVersionData{ + "2020-01-01": {}, + "2023-01-01": {}, + } + actual, err := differ{}.changesForApiVersions("Computer", initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.ApiVersionAdded{ + ServiceName: "Computer", + ApiVersion: "2023-01-01", + }, + } + assertChanges(t, expected, *actual) + assertContainsNoBreakingChanges(t, *actual) +} + +func TestDiff_APIVersionRemoved(t *testing.T) { + initial := map[string]dataapi.ApiVersionData{ + "2020-01-01": {}, + "2023-01-01": {}, + } + updated := map[string]dataapi.ApiVersionData{ + "2020-01-01": {}, + } + actual, err := differ{}.changesForApiVersions("Computer", initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.ApiVersionRemoved{ + ServiceName: "Computer", + ApiVersion: "2023-01-01", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} diff --git a/tools/data-api-differ/internal/differ/differ_constants.go b/tools/data-api-differ/internal/differ/differ_constants.go new file mode 100644 index 00000000000..6bcb33037a9 --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_constants.go @@ -0,0 +1,137 @@ +package differ + +import ( + "fmt" + "sort" + + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" + "github.com/hashicorp/pandora/tools/sdk/resourcemanager" +) + +// changesForConstants determines the changes between the different Constants within the provided API Resource. +func (d differ) changesForConstants(serviceName, apiVersion, apiResource string, initial, updated map[string]resourcemanager.ConstantDetails) []changes.Change { + output := make([]changes.Change, 0) + constantNames := d.uniqueConstantNames(initial, updated) + for _, constantName := range constantNames { + log.Logger.Trace(fmt.Sprintf("Detecting changes in Constant %q..", constantName)) + changesForConstant := d.changesForConstant(serviceName, apiVersion, apiResource, constantName, initial, updated) + output = append(output, changesForConstant...) + } + return output +} + +// changesForConstant determines the changes between two different versions of the same Constant. +func (d differ) changesForConstant(serviceName, apiVersion, apiResource, constantName string, initial, updated map[string]resourcemanager.ConstantDetails) []changes.Change { + output := make([]changes.Change, 0) + + oldData, inOldData := initial[constantName] + updatedData, inUpdatedData := updated[constantName] + if inOldData && !inUpdatedData { + log.Logger.Trace(fmt.Sprintf("Constant %q not present in the updated data", constantName)) + output = append(output, changes.ConstantRemoved{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ConstantName: constantName, + ConstantType: string(oldData.Type), + KeysAndValues: oldData.Values, + }) + // no point continuing to diff if it's gone + return output + } + if !inOldData && inUpdatedData { + log.Logger.Trace(fmt.Sprintf("Constant %q is new", constantName)) + output = append(output, changes.ConstantAdded{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ConstantName: constantName, + ConstantType: string(updatedData.Type), + KeysAndValues: updatedData.Values, + }) + // no point returning since `ConstantAdded` contains the details + return output + } + + if inOldData && inUpdatedData { + // the Type changing would be problematic - so we should surface that + if oldData.Type != updatedData.Type { + output = append(output, changes.ConstantTypeChanged{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ConstantName: constantName, + OldType: string(oldData.Type), + NewType: string(updatedData.Type), + }) + } + } + + keys := d.uniqueKeys(oldData.Values, updatedData.Values) + for _, key := range keys { + log.Logger.Trace(fmt.Sprintf("Detecting changes in Constant Key %q..", key)) + oldValue, oldContainsKey := oldData.Values[key] + updatedValue, updatedContainsKey := updatedData.Values[key] + + if oldContainsKey && !updatedContainsKey { + log.Logger.Trace("Key %q / Value %q has been removed", key, oldValue) + output = append(output, changes.ConstantKeyValueRemoved{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ConstantName: constantName, + ConstantKey: key, + ConstantValue: oldValue, + }) + continue + } + if !oldContainsKey && updatedContainsKey { + log.Logger.Trace("Key %q / Value %q has been added", key, updatedValue) + output = append(output, changes.ConstantKeyValueAdded{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ConstantName: constantName, + ConstantKey: key, + ConstantValue: updatedValue, + }) + continue + } + + // NOTE: if the casing of the Value changes this would be a breaking change too + if oldValue != updatedValue { + log.Logger.Trace("Key %q has changed value from %q to %q", key, oldValue, updatedValue) + output = append(output, changes.ConstantKeyValueChanged{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ConstantName: constantName, + ConstantKey: key, + OldConstantValue: oldValue, + NewConstantValue: updatedValue, + }) + continue + } + } + + return output +} + +// uniqueConstantNames returns a unique, sorted list of Constant Names from the keys of initial and updated. +func (d differ) uniqueConstantNames(initial, updated map[string]resourcemanager.ConstantDetails) []string { + uniqueNames := make(map[string]struct{}) + for name := range initial { + uniqueNames[name] = struct{}{} + } + for name := range updated { + uniqueNames[name] = struct{}{} + } + + output := make([]string, 0) + for k := range uniqueNames { + output = append(output, k) + } + sort.Strings(output) + return output +} diff --git a/tools/data-api-differ/internal/differ/differ_constants_test.go b/tools/data-api-differ/internal/differ/differ_constants_test.go new file mode 100644 index 00000000000..b7d195d9caf --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_constants_test.go @@ -0,0 +1,246 @@ +package differ + +import ( + "testing" + + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/sdk/resourcemanager" +) + +func TestDiff_ConstantAdded(t *testing.T) { + initial := map[string]resourcemanager.ConstantDetails{ + "First": {}, + } + updated := map[string]resourcemanager.ConstantDetails{ + "First": {}, + "Second": { + Type: resourcemanager.IntegerConstant, + Values: map[string]string{ + "One": "1", + "Two": "2", + "Three": "3", + }, + }, + } + actual := differ{}.changesForConstants("Computer", "2020-01-01", "Example", initial, updated) + expected := []changes.Change{ + changes.ConstantAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ConstantName: "Second", + ConstantType: string(resourcemanager.IntegerConstant), + KeysAndValues: map[string]string{ + "One": "1", + "Two": "2", + "Three": "3", + }, + }, + } + assertChanges(t, expected, actual) + assertContainsNoBreakingChanges(t, actual) +} + +func TestDiff_ConstantRemoved(t *testing.T) { + initial := map[string]resourcemanager.ConstantDetails{ + "First": {}, + "Second": { + Type: resourcemanager.IntegerConstant, + Values: map[string]string{ + "One": "1", + "Two": "2", + "Three": "3", + }, + }, + } + updated := map[string]resourcemanager.ConstantDetails{ + "First": {}, + } + actual := differ{}.changesForConstants("Computer", "2020-01-01", "Example", initial, updated) + expected := []changes.Change{ + changes.ConstantRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ConstantName: "Second", + ConstantType: string(resourcemanager.IntegerConstant), + KeysAndValues: map[string]string{ + "One": "1", + "Two": "2", + "Three": "3", + }, + }, + } + assertChanges(t, expected, actual) + assertContainsBreakingChanges(t, actual) +} + +func TestDiff_ConstantKeyValueAdded(t *testing.T) { + initial := map[string]resourcemanager.ConstantDetails{ + "First": { + Type: resourcemanager.IntegerConstant, + Values: map[string]string{ + "One": "1", + "Two": "2", + "Three": "3", + }, + }, + } + updated := map[string]resourcemanager.ConstantDetails{ + "First": { + Type: resourcemanager.IntegerConstant, + Values: map[string]string{ + "One": "1", + "Two": "2", + "Three": "3", + "Four": "4", + }, + }, + } + actual := differ{}.changesForConstants("Computer", "2020-01-01", "Example", initial, updated) + expected := []changes.Change{ + changes.ConstantKeyValueAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ConstantName: "First", + ConstantKey: "Four", + ConstantValue: "4", + }, + } + assertChanges(t, expected, actual) + assertContainsNoBreakingChanges(t, actual) +} + +func TestDiff_ConstantKeyValueChanged(t *testing.T) { + initial := map[string]resourcemanager.ConstantDetails{ + "SkuName": { + Type: resourcemanager.StringConstant, + Values: map[string]string{ + "Basic": "basic", + "Premium": "premium", + "Standard": "standard", + }, + }, + } + updated := map[string]resourcemanager.ConstantDetails{ + "SkuName": { + Type: resourcemanager.StringConstant, + Values: map[string]string{ + "Basic": "Basic", + "Premium": "Premium", + "Standard": "Standard", + }, + }, + } + actual := differ{}.changesForConstants("Computer", "2020-01-01", "Example", initial, updated) + expected := []changes.Change{ + changes.ConstantKeyValueChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ConstantName: "SkuName", + ConstantKey: "Basic", + OldConstantValue: "basic", + NewConstantValue: "Basic", + }, + changes.ConstantKeyValueChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ConstantName: "SkuName", + ConstantKey: "Premium", + OldConstantValue: "premium", + NewConstantValue: "Premium", + }, + changes.ConstantKeyValueChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ConstantName: "SkuName", + ConstantKey: "Standard", + OldConstantValue: "standard", + NewConstantValue: "Standard", + }, + } + assertChanges(t, expected, actual) + assertContainsBreakingChanges(t, actual) +} + +func TestDiff_ConstantKeyValueRemoved(t *testing.T) { + initial := map[string]resourcemanager.ConstantDetails{ + "First": { + Type: resourcemanager.IntegerConstant, + Values: map[string]string{ + "One": "1", + "Two": "2", + "Three": "3", + "Four": "4", + }, + }, + } + updated := map[string]resourcemanager.ConstantDetails{ + "First": { + Type: resourcemanager.IntegerConstant, + Values: map[string]string{ + "One": "1", + "Two": "2", + "Three": "3", + }, + }, + } + actual := differ{}.changesForConstants("Computer", "2020-01-01", "Example", initial, updated) + expected := []changes.Change{ + changes.ConstantKeyValueRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ConstantName: "First", + ConstantKey: "Four", + ConstantValue: "4", + }, + } + assertChanges(t, expected, actual) + assertContainsBreakingChanges(t, actual) +} + +func TestDiff_ConstantTypeChanged(t *testing.T) { + initial := map[string]resourcemanager.ConstantDetails{ + "First": { + Type: resourcemanager.IntegerConstant, + Values: map[string]string{ + "One": "1", + }, + }, + } + updated := map[string]resourcemanager.ConstantDetails{ + "First": { + Type: resourcemanager.StringConstant, + Values: map[string]string{ + "One": "one", + }, + }, + } + actual := differ{}.changesForConstants("Computer", "2020-01-01", "Example", initial, updated) + expected := []changes.Change{ + changes.ConstantTypeChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ConstantName: "First", + OldType: string(resourcemanager.IntegerConstant), + NewType: string(resourcemanager.StringConstant), + }, + changes.ConstantKeyValueChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ConstantName: "First", + ConstantKey: "One", + OldConstantValue: "1", + NewConstantValue: "one", + }, + } + assertChanges(t, expected, actual) + assertContainsBreakingChanges(t, actual) +} diff --git a/tools/data-api-differ/internal/differ/differ_fields.go b/tools/data-api-differ/internal/differ/differ_fields.go new file mode 100644 index 00000000000..ff4e0a0849e --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_fields.go @@ -0,0 +1,132 @@ +package differ + +import ( + "fmt" + "sort" + + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" + "github.com/hashicorp/pandora/tools/sdk/resourcemanager" +) + +// changesForFields determines the changes between the initial and updated Fields within the specified Model. +func (d differ) changesForFields(serviceName, apiVersion, apiResource, modelName string, initial, updated map[string]resourcemanager.FieldDetails) (*[]changes.Change, error) { + output := make([]changes.Change, 0) + fieldNames := d.uniqueFieldNames(initial, updated) + for _, fieldName := range fieldNames { + log.Logger.Trace(fmt.Sprintf("Detecting changes in Field %q..", fieldName)) + changesForField, err := d.changesForField(serviceName, apiVersion, apiResource, modelName, fieldName, initial, updated) + if err != nil { + return nil, fmt.Errorf("detecting changes in the Field %q: %+v", fieldName, err) + } + output = append(output, *changesForField...) + } + return &output, nil +} + +// changesForField determines the changes between the initial and updated Fields within the specified Model. +func (d differ) changesForField(serviceName, apiVersion, apiResource, modelName, fieldName string, initial, updated map[string]resourcemanager.FieldDetails) (*[]changes.Change, error) { + output := make([]changes.Change, 0) + + oldData, isInOld := initial[fieldName] + updatedData, isInUpdated := updated[fieldName] + if isInOld && !isInUpdated { + log.Logger.Trace(fmt.Sprintf("Field %q has been removed", fieldName)) + output = append(output, changes.FieldRemoved{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ModelName: modelName, + FieldName: fieldName, + }) + return &output, nil + } + if !isInOld && isInUpdated { + log.Logger.Trace(fmt.Sprintf("Field %q is new", fieldName)) + output = append(output, changes.FieldAdded{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ModelName: modelName, + FieldName: fieldName, + }) + // in the event of a new field, we can skip the other details + return &output, nil + } + + // diff the individual properties of note + if !oldData.Optional && updatedData.Optional { + output = append(output, changes.FieldIsNowOptional{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ModelName: modelName, + FieldName: fieldName, + }) + } + if !oldData.Required && updatedData.Required { + output = append(output, changes.FieldIsNowRequired{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ModelName: modelName, + FieldName: fieldName, + }) + } + if oldData.JsonName != updatedData.JsonName { + output = append(output, changes.FieldJsonNameChanged{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ModelName: modelName, + FieldName: fieldName, + OldValue: oldData.JsonName, + NewValue: updatedData.JsonName, + }) + } + + // for the sake of simplicity when reviewing let's normalise this object to a string + oldObjectDefinition, err := d.stringifyObjectDefinition(oldData.ObjectDefinition) + if err != nil { + return nil, fmt.Errorf("stringifying the Old Object Definition: %+v", err) + } + newObjectDefinition, err := d.stringifyObjectDefinition(updatedData.ObjectDefinition) + if err != nil { + return nil, fmt.Errorf("stringifying the Updated Object Definition: %+v", err) + } + if *oldObjectDefinition != *newObjectDefinition { + // To catch unintentional type changes e.g. + // https://github.com/hashicorp/terraform-provider-azurerm/pull/23129 + output = append(output, changes.FieldObjectDefinitionChanged{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ModelName: modelName, + FieldName: fieldName, + OldValue: *oldObjectDefinition, + NewValue: *newObjectDefinition, + }) + } + + // TODO: DateFormat / Validation in the future? + + return &output, nil +} + +// uniqueFieldNames returns a unique, sorted list of Field Names from the keys of initial and updated. +func (d differ) uniqueFieldNames(initial, updated map[string]resourcemanager.FieldDetails) []string { + uniqueNames := make(map[string]struct{}) + for name := range initial { + uniqueNames[name] = struct{}{} + } + for name := range updated { + uniqueNames[name] = struct{}{} + } + + output := make([]string, 0) + for k := range uniqueNames { + output = append(output, k) + } + sort.Strings(output) + return output +} diff --git a/tools/data-api-differ/internal/differ/differ_fields_test.go b/tools/data-api-differ/internal/differ/differ_fields_test.go new file mode 100644 index 00000000000..ecfae168e43 --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_fields_test.go @@ -0,0 +1,222 @@ +package differ + +import ( + "testing" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/sdk/resourcemanager" +) + +func TestDiff_FieldAdded(t *testing.T) { + initial := map[string]resourcemanager.FieldDetails{ + "First": { + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + } + updated := map[string]resourcemanager.FieldDetails{ + "First": { + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + "Second": { + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + } + actual, err := differ{}.changesForFields("Computer", "2020-01-01", "Example", "SomeModel", initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.FieldAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ModelName: "SomeModel", + FieldName: "Second", + }, + } + assertChanges(t, expected, *actual) + assertContainsNoBreakingChanges(t, *actual) +} + +func TestDiff_FieldRemoved(t *testing.T) { + initial := map[string]resourcemanager.FieldDetails{ + "First": { + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + "Second": { + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + } + updated := map[string]resourcemanager.FieldDetails{ + "First": { + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + } + actual, err := differ{}.changesForFields("Computer", "2020-01-01", "Example", "SomeModel", initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.FieldRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ModelName: "SomeModel", + FieldName: "Second", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_FieldJsonNameChanged(t *testing.T) { + initial := map[string]resourcemanager.FieldDetails{ + "First": { + JsonName: "someField", + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + } + updated := map[string]resourcemanager.FieldDetails{ + "First": { + JsonName: "SomeField", // casing differs == a breaking change + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + } + actual, err := differ{}.changesForFields("Computer", "2020-01-01", "Example", "SomeModel", initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.FieldJsonNameChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ModelName: "SomeModel", + FieldName: "First", + OldValue: "someField", + NewValue: "SomeField", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_FieldIsNowOptional(t *testing.T) { + initial := map[string]resourcemanager.FieldDetails{ + "First": { + Optional: false, + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + } + updated := map[string]resourcemanager.FieldDetails{ + "First": { + Optional: true, + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + } + actual, err := differ{}.changesForFields("Computer", "2020-01-01", "Example", "SomeModel", initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.FieldIsNowOptional{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ModelName: "SomeModel", + FieldName: "First", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_FieldIsNowRequired(t *testing.T) { + initial := map[string]resourcemanager.FieldDetails{ + "First": { + Required: false, + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + } + updated := map[string]resourcemanager.FieldDetails{ + "First": { + Required: true, + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + } + actual, err := differ{}.changesForFields("Computer", "2020-01-01", "Example", "SomeModel", initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.FieldIsNowRequired{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ModelName: "SomeModel", + FieldName: "First", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_FieldObjectDefinitionChanged(t *testing.T) { + initial := map[string]resourcemanager.FieldDetails{ + "First": { + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + } + updated := map[string]resourcemanager.FieldDetails{ + "First": { + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.ReferenceApiObjectDefinitionType, + ReferenceName: pointer.To("SomeConstant"), + }, + }, + } + actual, err := differ{}.changesForFields("Computer", "2020-01-01", "Example", "SomeModel", initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.FieldObjectDefinitionChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ModelName: "SomeModel", + FieldName: "First", + OldValue: "string", + NewValue: "SomeConstant", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} diff --git a/tools/data-api-differ/internal/differ/differ_helpers.go b/tools/data-api-differ/internal/differ/differ_helpers.go new file mode 100644 index 00000000000..f93f50d4716 --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_helpers.go @@ -0,0 +1,32 @@ +package differ + +import ( + "sort" + + "github.com/hashicorp/pandora/tools/sdk/resourcemanager" +) + +// stringifyObjectDefinition returns a human readable, string version of this Object Definition. +func (d differ) stringifyObjectDefinition(input resourcemanager.ApiObjectDefinition) (*string, error) { + // NOTE: whilst the `.GolangTypeName()` method exists in the `resourcemanager` SDK, this won't + // in the future so this method exists to have a single place to change in the future. + return input.GolangTypeName(nil) +} + +// uniqueConstantNames returns a unique, sorted list of Keys from initial and updated. +func (d differ) uniqueKeys(initial, updated map[string]string) []string { + uniqueNames := make(map[string]struct{}) + for name := range initial { + uniqueNames[name] = struct{}{} + } + for name := range updated { + uniqueNames[name] = struct{}{} + } + + output := make([]string, 0) + for k := range uniqueNames { + output = append(output, k) + } + sort.Strings(output) + return output +} diff --git a/tools/data-api-differ/internal/differ/differ_helpers_test.go b/tools/data-api-differ/internal/differ/differ_helpers_test.go new file mode 100644 index 00000000000..626e61d2c73 --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_helpers_test.go @@ -0,0 +1,77 @@ +package differ + +import ( + "reflect" + "testing" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/dataapi" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" +) + +func init() { + log.Logger = hclog.Default() +} + +// determineAndValidateDiff runs a full diff of the two sets of data. +// This is intended to be used by tests covering the entire `diff` code path, simulating a real-world usage. +func determineAndValidateDiff(t *testing.T, initial, updated dataapi.Data, expected []changes.Change, breakingChanges bool) { + actual, err := performDiff(initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + if actual == nil { + t.Fatalf("`actual` was unexpectedly nil") + } + if len(actual.Changes) != len(expected) { + t.Fatalf("expected %d changes but got %d. Expected: %+v\n\nActual: %+v", len(expected), len(actual.Changes), expected, *actual) + } + if actual.ContainsBreakingChanges() != breakingChanges { + t.Fatalf("expected `ContainsBreakingChanges` to return %t but got %t", breakingChanges, actual.ContainsBreakingChanges()) + } + + assertChanges(t, expected, actual.Changes) +} + +// assertChanges is used to validate that the Changes returned match what's expected. +// This is intended to be used by tests to validate the individual diff stages (services, api versions) +// return the expected set of changes. +func assertChanges(t *testing.T, expected, actual []changes.Change) { + if len(expected) != len(actual) { + t.Fatalf("expected %d changes but got %d changes", len(expected), len(actual)) + } + + for i, expectedVal := range expected { + actualVal := actual[i] + if reflect.TypeOf(expectedVal).Name() != reflect.TypeOf(actualVal).Name() { + t.Fatalf("Change at index %d differed by name - expected %q but got %q", i, reflect.TypeOf(expectedVal).Name(), reflect.TypeOf(actualVal).Name()) + } + if !reflect.DeepEqual(expectedVal, actualVal) { + t.Fatalf("Change at index %d didn't match - expected `%+v` got `%+v`", i, expectedVal, actualVal) + } + } +} + +// assertContainsBreakingChanges is used to validate that the changes contain breaking changes +func assertContainsBreakingChanges(t *testing.T, actual []changes.Change) { + breaking := false + for _, item := range actual { + if item.IsBreaking() { + breaking = true + } + } + + if !breaking { + t.Fatalf("expected `actual` to contain breaking changes but it didn't") + } +} + +// assertContainsNoBreakingChanges is used to validate that the changes contain no breaking changes +func assertContainsNoBreakingChanges(t *testing.T, actual []changes.Change) { + for _, item := range actual { + if item.IsBreaking() { + t.Fatalf("expected `actual` to contain no breaking changes but got: %+v", item) + } + } +} diff --git a/tools/data-api-differ/internal/differ/differ_models.go b/tools/data-api-differ/internal/differ/differ_models.go new file mode 100644 index 00000000000..296c9be2dc1 --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_models.go @@ -0,0 +1,162 @@ +package differ + +import ( + "fmt" + "sort" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" + "github.com/hashicorp/pandora/tools/sdk/resourcemanager" +) + +// changesForModels determines the changes between the initial and updated Models within the specified API Resource. +func (d differ) changesForModels(serviceName, apiVersion, apiResource string, initial, updated map[string]resourcemanager.ModelDetails) (*[]changes.Change, error) { + output := make([]changes.Change, 0) + modelNames := d.uniqueModelNames(initial, updated) + for _, modelName := range modelNames { + log.Logger.Trace(fmt.Sprintf("Detecting changes in Model %q..", modelName)) + changesForModel, err := d.changesForModel(serviceName, apiVersion, apiResource, modelName, initial, updated) + if err != nil { + return nil, fmt.Errorf("detecting changes to the Model %q: %+v", modelName, err) + } + output = append(output, *changesForModel...) + } + return &output, nil +} + +// changesForModel determines the changes between the initial and updated Models within the specified API Resource. +func (d differ) changesForModel(serviceName, apiVersion, apiResource, modelName string, initial, updated map[string]resourcemanager.ModelDetails) (*[]changes.Change, error) { + output := make([]changes.Change, 0) + + oldData, isInOld := initial[modelName] + updatedData, isInUpdated := updated[modelName] + if isInOld && !isInUpdated { + log.Logger.Trace(fmt.Sprintf("Model %q has been removed", modelName)) + output = append(output, changes.ModelRemoved{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ModelName: modelName, + }) + return &output, nil + } + if !isInOld && isInUpdated { + log.Logger.Trace(fmt.Sprintf("Model %q is new", modelName)) + output = append(output, changes.ModelAdded{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ModelName: modelName, + }) + // in the event of a new model, we can skip the other details + return &output, nil + } + + // check for any changes to the discriminated information + log.Logger.Trace("Checking for changes to the Discriminated Type Information..") + discriminatorChangesForModel := d.discriminatorChangesForModel(serviceName, apiVersion, apiResource, modelName, oldData, updatedData) + output = append(output, discriminatorChangesForModel...) + + // then detect any changes to the fields + log.Logger.Trace("Checking for changes to the Fields..") + changesToFields, err := d.changesForFields(serviceName, apiVersion, apiResource, modelName, oldData.Fields, updatedData.Fields) + if err != nil { + return nil, fmt.Errorf("detecting changes to Fields: %+v", err) + } + output = append(output, *changesToFields...) + + return &output, nil +} + +func (d differ) discriminatorChangesForModel(serviceName, apiVersion, apiResource, modelName string, initial, updated resourcemanager.ModelDetails) []changes.Change { + output := make([]changes.Change, 0) + + // Handle this being a Discriminated Implementation + if initial.ParentTypeName == nil && updated.ParentTypeName != nil { + // the Model has gone from not having a Parent Type -> having a Parent Type + // meaning this is now a Discriminated Implementation. + log.Logger.Trace(fmt.Sprintf("Model %q is now a Discriminated Implementation of %q", modelName, *updated.ParentTypeName)) + output = append(output, changes.ModelDiscriminatedParentTypeAdded{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ModelName: modelName, + NewParentModelName: *updated.ParentTypeName, + }) + } + if initial.ParentTypeName != nil && updated.ParentTypeName == nil { + // The model has gone from having a Parent Type -> not having a Parent Type + // meaning this is no longer a Discriminated Implementation. + log.Logger.Trace(fmt.Sprintf("Model %q is no longer a Discriminated Implementation of %q", modelName, *initial.ParentTypeName)) + output = append(output, changes.ModelDiscriminatedParentTypeRemoved{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ModelName: modelName, + OldParentModelName: *initial.ParentTypeName, + }) + } + if initial.ParentTypeName != nil && updated.ParentTypeName != nil && *initial.ParentTypeName != *updated.ParentTypeName { + // The model has changed its Parent Type - which is going to require additional investigation. + log.Logger.Trace(fmt.Sprintf("Model %q is is a Discriminated Implementation but changed it's Parent from %q to %q", modelName, *initial.ParentTypeName, *updated.ParentTypeName)) + output = append(output, changes.ModelDiscriminatedParentTypeChanged{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ModelName: modelName, + OldParentModelName: *initial.ParentTypeName, + NewParentModelName: *updated.ParentTypeName, + }) + } + + // Handle the Field containing the Discriminated Value changing + if pointer.From(initial.TypeHintIn) != pointer.From(updated.TypeHintIn) { + // Since TypeHintIn changing is unlikely (but has been seen) but is required along with at least + // one Discriminated Implementation - we don't need to handle Added/Removed here. + log.Logger.Trace(fmt.Sprintf("Model %q has a new value for `TypeHintIn` - old %q / new %q", modelName, pointer.From(initial.TypeHintIn), pointer.From(updated.TypeHintIn))) + output = append(output, changes.ModelDiscriminatedTypeHintInChanged{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ModelName: modelName, + OldValue: pointer.From(initial.TypeHintIn), + NewValue: pointer.From(updated.TypeHintIn), + }) + } + + // Handle the Discriminated Value changing + if pointer.From(initial.TypeHintValue) != pointer.From(updated.TypeHintValue) { + // We're (intentionally) only handling Changed and not Added/Removed here since this is a Required + // field when `ParentTypeName` is set. + log.Logger.Trace(fmt.Sprintf("Model %q has a new value for `TypeHintValue` - old %q / new %q", modelName, pointer.From(initial.TypeHintValue), pointer.From(updated.TypeHintValue))) + output = append(output, changes.ModelDiscriminatedTypeValueChanged{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ModelName: modelName, + OldValue: pointer.From(initial.TypeHintValue), + NewValue: pointer.From(updated.TypeHintValue), + }) + } + + return output +} + +// uniqueModelNames returns a unique, sorted list of Model Names from the keys of initial and updated. +func (d differ) uniqueModelNames(initial, updated map[string]resourcemanager.ModelDetails) []string { + uniqueNames := make(map[string]struct{}) + for name := range initial { + uniqueNames[name] = struct{}{} + } + for name := range updated { + uniqueNames[name] = struct{}{} + } + + output := make([]string, 0) + for k := range uniqueNames { + output = append(output, k) + } + sort.Strings(output) + return output +} diff --git a/tools/data-api-differ/internal/differ/differ_models_test.go b/tools/data-api-differ/internal/differ/differ_models_test.go new file mode 100644 index 00000000000..89b1b6c15f5 --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_models_test.go @@ -0,0 +1,200 @@ +package differ + +import ( + "testing" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/sdk/resourcemanager" +) + +func TestDiff_ModelAdded(t *testing.T) { + initial := map[string]resourcemanager.ModelDetails{ + "First": {}, + } + updated := map[string]resourcemanager.ModelDetails{ + "First": {}, + "Second": {}, + } + actual, err := differ{}.changesForModels("Computer", "2020-01-01", "Example", initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.ModelAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ModelName: "Second", + }, + } + assertChanges(t, expected, *actual) + assertContainsNoBreakingChanges(t, *actual) +} + +func TestDiff_ModelRemoved(t *testing.T) { + initial := map[string]resourcemanager.ModelDetails{ + "First": {}, + "Second": {}, + } + updated := map[string]resourcemanager.ModelDetails{ + "First": {}, + } + actual, err := differ{}.changesForModels("Computer", "2020-01-01", "Example", initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.ModelRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ModelName: "Second", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_ModelDiscriminatedParentTypeAdded(t *testing.T) { + initial := map[string]resourcemanager.ModelDetails{ + "First": { + TypeHintIn: nil, + }, + } + updated := map[string]resourcemanager.ModelDetails{ + "First": { + ParentTypeName: pointer.To("NewValue"), + }, + } + actual, err := differ{}.changesForModels("Computer", "2020-01-01", "Example", initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.ModelDiscriminatedParentTypeAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ModelName: "First", + NewParentModelName: "NewValue", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_ModelDiscriminatedParentTypeChanged(t *testing.T) { + initial := map[string]resourcemanager.ModelDetails{ + "First": { + ParentTypeName: pointer.To("OldValue"), + }, + } + updated := map[string]resourcemanager.ModelDetails{ + "First": { + ParentTypeName: pointer.To("NewValue"), + }, + } + actual, err := differ{}.changesForModels("Computer", "2020-01-01", "Example", initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.ModelDiscriminatedParentTypeChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ModelName: "First", + OldParentModelName: "OldValue", + NewParentModelName: "NewValue", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_ModelDiscriminatedParentTypeRemoved(t *testing.T) { + initial := map[string]resourcemanager.ModelDetails{ + "First": { + ParentTypeName: pointer.To("OldValue"), + }, + } + updated := map[string]resourcemanager.ModelDetails{ + "First": { + TypeHintIn: nil, + }, + } + actual, err := differ{}.changesForModels("Computer", "2020-01-01", "Example", initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.ModelDiscriminatedParentTypeRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ModelName: "First", + OldParentModelName: "OldValue", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_ModelDiscriminatedTypeHintInChanged(t *testing.T) { + initial := map[string]resourcemanager.ModelDetails{ + "First": { + TypeHintIn: pointer.To("OldValue"), + }, + } + updated := map[string]resourcemanager.ModelDetails{ + "First": { + TypeHintIn: pointer.To("NewValue"), + }, + } + actual, err := differ{}.changesForModels("Computer", "2020-01-01", "Example", initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.ModelDiscriminatedTypeHintInChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ModelName: "First", + OldValue: "OldValue", + NewValue: "NewValue", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_ModelDiscriminatedTypeHintValueChanged(t *testing.T) { + initial := map[string]resourcemanager.ModelDetails{ + "First": { + TypeHintValue: pointer.To("OldValue"), + }, + } + updated := map[string]resourcemanager.ModelDetails{ + "First": { + TypeHintValue: pointer.To("NewValue"), + }, + } + actual, err := differ{}.changesForModels("Computer", "2020-01-01", "Example", initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.ModelDiscriminatedTypeValueChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ModelName: "First", + OldValue: "OldValue", + NewValue: "NewValue", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} diff --git a/tools/data-api-differ/internal/differ/differ_operations.go b/tools/data-api-differ/internal/differ/differ_operations.go new file mode 100644 index 00000000000..0743ff581ef --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_operations.go @@ -0,0 +1,584 @@ +package differ + +import ( + "fmt" + "reflect" + "sort" + "strings" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" + "github.com/hashicorp/pandora/tools/sdk/resourcemanager" +) + +// changesForOperations determines the changes between the initial and updated Operations within the specified API Resource. +func (d differ) changesForOperations(serviceName, apiVersion, apiResource string, initial, updated map[string]resourcemanager.ApiOperation, initialResourceIds, updatedResourceIds map[string]resourcemanager.ResourceIdDefinition) (*[]changes.Change, error) { + output := make([]changes.Change, 0) + operationNames := d.uniqueOperationNames(initial, updated) + for _, operationName := range operationNames { + log.Logger.Trace(fmt.Sprintf("Detecting changes in Operation %q..", operationName)) + changesForOperation, err := d.changesForOperation(serviceName, apiVersion, apiResource, operationName, initial, updated, initialResourceIds, updatedResourceIds) + if err != nil { + return nil, fmt.Errorf("detecting changes for the Operation %q: %+v", operationName, err) + } + output = append(output, *changesForOperation...) + } + return &output, nil +} + +// changesForOperation determines the changes between the initial and updated Operations within the specified API Resource. +func (d differ) changesForOperation(serviceName, apiVersion, apiResource, operationName string, initial, updated map[string]resourcemanager.ApiOperation, initialResourceIds, updatedResourceIds map[string]resourcemanager.ResourceIdDefinition) (*[]changes.Change, error) { + output := make([]changes.Change, 0) + + oldData, isInOld := initial[operationName] + updatedData, isInUpdated := updated[operationName] + oldUri := "" + if isInOld { + oldUri = d.uriForOperation(oldData, initialResourceIds) + } + updatedUri := "" + if isInUpdated { + updatedUri = d.uriForOperation(updatedData, updatedResourceIds) + } + + if isInOld && !isInUpdated { + log.Logger.Trace(fmt.Sprintf("Operation %q has been removed", operationName)) + output = append(output, changes.OperationRemoved{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + Uri: oldUri, + }) + return &output, nil + } + if !isInOld && isInUpdated { + log.Logger.Trace(fmt.Sprintf("Operation %q is new", operationName)) + output = append(output, changes.OperationAdded{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + Uri: updatedUri, + }) + // in the event of a new operation, we can skip the other details + return &output, nil + } + + log.Logger.Trace("Detecting changes to the Resource ID for this Operation..") + resourceIdChanges, err := d.changesForOperationResourceId(serviceName, apiVersion, apiResource, operationName, oldData, updatedData, initialResourceIds, updatedResourceIds) + if err != nil { + return nil, fmt.Errorf("determining changes to the Resource ID: %+v", err) + } + output = append(output, *resourceIdChanges...) + + // When the Content-Type changes we're sending a semantically different object + if pointer.From(oldData.ContentType) != pointer.From(updatedData.ContentType) { + log.Logger.Trace("Content Type didn't match") + output = append(output, changes.OperationContentTypeChanged{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + OldContentType: pointer.From(oldData.ContentType), + NewContentType: pointer.From(updatedData.ContentType), + }) + } + + // When the Field containing Pagination Details changes we're breaking either the existing or + // updated code paths. + if pointer.From(oldData.FieldContainingPaginationDetails) != pointer.From(updatedData.FieldContainingPaginationDetails) { + log.Logger.Trace("Field Containing Pagination Details didn't match") + output = append(output, changes.OperationPaginationFieldChanged{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + OldValue: pointer.From(oldData.FieldContainingPaginationDetails), + NewValue: pointer.From(updatedData.FieldContainingPaginationDetails), + }) + } + + // When the HTTP Method changes we're performing a totally different Operation + if oldData.Method != updatedData.Method { + log.Logger.Trace("Method didn't match") + output = append(output, changes.OperationMethodChanged{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + OldValue: oldData.Method, + NewValue: updatedData.Method, + }) + } + + if !d.expectedStatusCodesMatch(oldData.ExpectedStatusCodes, updatedData.ExpectedStatusCodes) { + log.Logger.Trace("Expected Status Codes didn't match") + output = append(output, changes.OperationExpectedStatusCodesChanged{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + OldExpectedStatusCodes: oldData.ExpectedStatusCodes, + NewExpectedStatusCodes: updatedData.ExpectedStatusCodes, + }) + } + + // Long Running Operations + if oldData.LongRunning && !updatedData.LongRunning { + log.Logger.Trace("Operation is no longer a Long Running Operation") + output = append(output, changes.OperationLongRunningRemoved{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + }) + } + if !oldData.LongRunning && updatedData.LongRunning { + log.Logger.Trace("Operation is now a Long Running Operation") + output = append(output, changes.OperationLongRunningAdded{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + }) + } + + // Options + log.Logger.Trace("Detecting changes to the Options Object..") + optionsChanges, err := d.changesForOperationOptionsObject(serviceName, apiVersion, apiResource, operationName, oldData, updatedData) + if err != nil { + return nil, fmt.Errorf("detecting changes to to the Options Object: %+v", err) + } + output = append(output, *optionsChanges...) + + // Request Object + log.Logger.Trace("Detecting changes to the Request Object..") + requestObjectChanges, err := d.changesForOperationRequestObject(serviceName, apiVersion, apiResource, operationName, oldData, updatedData) + if err != nil { + return nil, fmt.Errorf("detecting changes to the Request Object: %+v", err) + } + output = append(output, *requestObjectChanges...) + + // Response Object + log.Logger.Trace("Detecting changes to the Response Object..") + responseObjectChanges, err := d.changesForOperationResponseObject(serviceName, apiVersion, apiResource, operationName, oldData, updatedData) + if err != nil { + return nil, fmt.Errorf("detecting changes to the Response Object: %+v", err) + } + output = append(output, *responseObjectChanges...) + + // Uri Suffix + log.Logger.Trace("Detecting changes to the Uri Suffix..") + uriSuffixChanges := d.changesForOperationUriSuffix(serviceName, apiVersion, apiResource, operationName, oldData, updatedData) + output = append(output, uriSuffixChanges...) + + return &output, nil +} + +// changesForOperationOptionsObject determines any changes to the Options Object between the initial and updated version of this Operation. +func (d differ) changesForOperationOptionsObject(serviceName, apiVersion, apiResource, operationName string, initial, updated resourcemanager.ApiOperation) (*[]changes.Change, error) { + output := make([]changes.Change, 0) + + if len(initial.Options) == 0 && len(updated.Options) > 0 { + log.Logger.Trace("The Operation now has options") + updatedStringified, err := d.stringifyOperationOptions(updated.Options) + if err != nil { + return nil, fmt.Errorf("stringifying the Updated Operation Options: %+v", err) + } + output = append(output, changes.OperationOptionsAdded{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + NewValue: *updatedStringified, + }) + } + if len(initial.Options) > 0 && len(updated.Options) == 0 { + log.Logger.Trace("The Operation no longer has options") + initialStringified, err := d.stringifyOperationOptions(initial.Options) + if err != nil { + return nil, fmt.Errorf("stringifying the Initial Operation Options: %+v", err) + } + output = append(output, changes.OperationOptionsRemoved{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + OldValue: *initialStringified, + }) + } + if len(initial.Options) > 0 && len(updated.Options) > 0 { + matches, err := d.optionsMatch(initial.Options, updated.Options) + if err != nil { + return nil, fmt.Errorf("determining whether the initial and updated Options Objects match: %+v", err) + } + if !*matches { + log.Logger.Trace("The Options for this Operation have changed") + initialStringified, err := d.stringifyOperationOptions(initial.Options) + if err != nil { + return nil, fmt.Errorf("stringifying the Initial Operation Options: %+v", err) + } + updatedStringified, err := d.stringifyOperationOptions(updated.Options) + if err != nil { + return nil, fmt.Errorf("stringifying the Updated Operation Options: %+v", err) + } + output = append(output, changes.OperationOptionsChanged{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + OldValue: *initialStringified, + NewValue: *updatedStringified, + }) + } + } + + return &output, nil +} + +// changesForOperationRequestObject determines any changes to the Request Object in both the initial and updated versions of this Operation. +func (d differ) changesForOperationRequestObject(serviceName, apiVersion, apiResource, operationName string, initial, updated resourcemanager.ApiOperation) (*[]changes.Change, error) { + output := make([]changes.Change, 0) + + if initial.RequestObject != nil && updated.RequestObject == nil { + log.Logger.Trace("The updated Operation no longer has a Request Object") + oldStringified, err := d.stringifyObjectDefinition(*initial.RequestObject) + if err != nil { + return nil, fmt.Errorf("stringifying the Old Object Definition: %+v", err) + } + output = append(output, changes.OperationRequestObjectRemoved{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + OldRequestObject: *oldStringified, + }) + } + if initial.RequestObject == nil && updated.RequestObject != nil { + log.Logger.Trace("The updated Operation now has a Request Object") + updatedStringified, err := d.stringifyObjectDefinition(*updated.RequestObject) + if err != nil { + return nil, fmt.Errorf("stringifying the Updated Object Definition: %+v", err) + } + output = append(output, changes.OperationRequestObjectAdded{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + NewRequestObject: *updatedStringified, + }) + } + if initial.RequestObject != nil && updated.RequestObject != nil { + oldStringified, err := d.stringifyObjectDefinition(*initial.RequestObject) + if err != nil { + return nil, fmt.Errorf("stringifying the Old Object Definition: %+v", err) + } + updatedStringified, err := d.stringifyObjectDefinition(*updated.RequestObject) + if err != nil { + return nil, fmt.Errorf("stringifying the Updated Object Definition: %+v", err) + } + if oldStringified != updatedStringified { + log.Logger.Trace("The updated Operation has a different Request Object") + output = append(output, changes.OperationRequestObjectChanged{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + OldRequestObject: *oldStringified, + NewRequestObject: *updatedStringified, + }) + } + } + + return &output, nil +} + +// changesForOperationResourceId determines any changes to the Resource Id used in both the initial and updated versions of this Operation. +func (d differ) changesForOperationResourceId(serviceName, apiVersion, apiResource, operationName string, initial, updated resourcemanager.ApiOperation, initialResourceIds, updatedResourceIds map[string]resourcemanager.ResourceIdDefinition) (*[]changes.Change, error) { + output := make([]changes.Change, 0) + + if initial.ResourceIdName != nil && updated.ResourceIdName == nil { + log.Logger.Trace(fmt.Sprintf("The Operation no longer requires a Resource ID %q", *initial.ResourceIdName)) + output = append(output, changes.OperationResourceIdRemoved{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + OldResourceIdName: *initial.ResourceIdName, + }) + } + if initial.ResourceIdName == nil && updated.ResourceIdName != nil { + log.Logger.Trace(fmt.Sprintf("The Operation now requires a Resource ID %q", *updated.ResourceIdName)) + output = append(output, changes.OperationResourceIdAdded{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + NewResourceIdName: *updated.ResourceIdName, + }) + } + if initial.ResourceIdName != nil && updated.ResourceIdName != nil { + oldId, ok := initialResourceIds[*initial.ResourceIdName] + if !ok { + return nil, fmt.Errorf("Unable to find the original Resource ID %q", *initial.ResourceIdName) + } + updatedId, ok := updatedResourceIds[*updated.ResourceIdName] + if !ok { + return nil, fmt.Errorf("Unable to find the updated Resource ID %q", *updated.ResourceIdName) + } + + // Determine if the Resource ID itself has changed - or whether it's been renamed + if *initial.ResourceIdName != *updated.ResourceIdName { + log.Logger.Trace("The Operation uses a different Resource ID Name") + output = append(output, changes.OperationResourceIdRenamed{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + NewResourceIdName: *updated.ResourceIdName, + OldResourceIdName: *initial.ResourceIdName, + }) + } + + if oldId.Id != updatedId.Id { + log.Logger.Trace("The Operation now targets a different Resource ID") + output = append(output, changes.OperationResourceIdChanged{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + OldResourceIdName: *initial.ResourceIdName, + OldValue: oldId.Id, + NewResourceIdName: *updated.ResourceIdName, + NewValue: updatedId.Id, + }) + } + } + + return &output, nil +} + +// changesForOperationResponseObject determines any changes to the Response Object in both the initial and updated versions of this Operation. +func (d differ) changesForOperationResponseObject(serviceName, apiVersion, apiResource, operationName string, initial, updated resourcemanager.ApiOperation) (*[]changes.Change, error) { + output := make([]changes.Change, 0) + + if initial.ResponseObject != nil && updated.ResponseObject == nil { + log.Logger.Trace("The updated Operation no longer has a Response Object") + oldStringified, err := d.stringifyObjectDefinition(*initial.ResponseObject) + if err != nil { + return nil, fmt.Errorf("stringifying the Old Object Definition: %+v", err) + } + output = append(output, changes.OperationResponseObjectRemoved{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + OldResponseObject: *oldStringified, + }) + } + if initial.ResponseObject == nil && updated.ResponseObject != nil { + log.Logger.Trace("The updated Operation now has a Response Object") + updatedStringified, err := d.stringifyObjectDefinition(*updated.ResponseObject) + if err != nil { + return nil, fmt.Errorf("stringifying the Updated Object Definition: %+v", err) + } + output = append(output, changes.OperationResponseObjectAdded{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + NewResponseObject: *updatedStringified, + }) + } + if initial.ResponseObject != nil && updated.ResponseObject != nil { + oldStringified, err := d.stringifyObjectDefinition(*initial.ResponseObject) + if err != nil { + return nil, fmt.Errorf("stringifying the Old Object Definition: %+v", err) + } + updatedStringified, err := d.stringifyObjectDefinition(*updated.ResponseObject) + if err != nil { + return nil, fmt.Errorf("stringifying the Updated Object Definition: %+v", err) + } + if oldStringified != updatedStringified { + log.Logger.Trace("The updated Operation has a different Response Object") + output = append(output, changes.OperationResponseObjectChanged{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + OldResponseObject: *oldStringified, + NewResponseObject: *updatedStringified, + }) + } + } + + return &output, nil +} + +// changesForOperationUriSuffix determines any changes to the Uri Suffix between the initial and updated versions of this Operation. +func (d differ) changesForOperationUriSuffix(serviceName, apiVersion, apiResource, operationName string, initial, updated resourcemanager.ApiOperation) []changes.Change { + output := make([]changes.Change, 0) + + if initial.UriSuffix != nil && updated.UriSuffix == nil { + log.Logger.Trace("The existing Uri Suffix has been removed") + output = append(output, changes.OperationUriSuffixRemoved{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + OldValue: *initial.UriSuffix, + }) + return output + } + + if initial.UriSuffix == nil && updated.UriSuffix != nil { + log.Logger.Trace("A new Uri Suffix has been added") + output = append(output, changes.OperationUriSuffixAdded{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + NewValue: *updated.UriSuffix, + }) + return output + } + + if initial.UriSuffix != nil && updated.UriSuffix != nil && *initial.UriSuffix != *updated.UriSuffix { + log.Logger.Trace("The Uri Suffix has changed") + output = append(output, changes.OperationUriSuffixChanged{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + OperationName: operationName, + OldValue: *initial.UriSuffix, + NewValue: *updated.UriSuffix, + }) + } + + return output +} + +// expectedStatusCodesMatch determines whether the two sets of Expected Status Codes are the same. +func (d differ) expectedStatusCodesMatch(initial, updated []int) bool { + if len(initial) != len(updated) { + return false + } + + sort.Ints(initial) + sort.Ints(updated) + return reflect.DeepEqual(initial, updated) +} + +// optionsMatch determines whether the two sets of Options are the same. +func (d differ) optionsMatch(initial, updated map[string]resourcemanager.ApiOperationOption) (*bool, error) { + // since we're stringifying the options for output, we can reuse this here + initialStringified, err := d.stringifyOperationOptions(initial) + if err != nil { + return nil, fmt.Errorf("stringifying the Initial Operation Options: %+v", err) + } + updatedStringified, err := d.stringifyOperationOptions(updated) + if err != nil { + return nil, fmt.Errorf("stringifying the Updated Operation Options: %+v", err) + } + uniqueKeys := d.uniqueKeys(*initialStringified, *updatedStringified) + for _, key := range uniqueKeys { + initialVal, isInInitial := (*initialStringified)[key] + if !isInInitial { + return pointer.To(false), nil + } + + updatedVal, isInUpdated := (*updatedStringified)[key] + if !isInUpdated { + return pointer.To(false), nil + } + + if initialVal != updatedVal { + return pointer.To(false), nil + } + } + + return pointer.To(true), nil +} + +// stringifyOperationOptions returns a stringified version of the Options object +// which is used to provide a human-readable output. +func (d differ) stringifyOperationOptions(input map[string]resourcemanager.ApiOperationOption) (*map[string]string, error) { + output := make(map[string]string) + + for key, value := range input { + log.Logger.Trace(fmt.Sprintf("Processing Option %q", key)) + stringifiedObjectDefinition, err := d.stringifyObjectDefinition(value.ObjectDefinition) + if err != nil { + return nil, fmt.Errorf("stringifying the Object Definition for Option %q: %+v", key, err) + } + + components := make([]string, 0) + + // Why not `json.Encode` this? because it's worth normalizing the ObjectDefinition so its + // consistent with the Object Definition against Field + components = append(components, fmt.Sprintf("ObjectDefinition %q", *stringifiedObjectDefinition)) + components = append(components, fmt.Sprintf("Required %t", value.Required)) + if value.HeaderName != nil { + components = append(components, fmt.Sprintf("HeaderName %q", *value.HeaderName)) + } + if value.QueryStringName != nil { + components = append(components, fmt.Sprintf("QueryStringName %q", *value.QueryStringName)) + } + + output[key] = strings.Join(components, " / ") + } + + return &output, nil +} + +// uriForOperation returns the formatted URI for this operation, comprising either the Resource ID, the Resource ID and the Uri Suffix +// or just the Uri Suffix. +func (d differ) uriForOperation(input resourcemanager.ApiOperation, resourceIds map[string]resourcemanager.ResourceIdDefinition) string { + components := make([]string, 0) + if input.ResourceIdName != nil { + id, ok := resourceIds[*input.ResourceIdName] + if !ok { + // TODO: thread through errors/raise this, but for now: + components = append(components, "{MISSING RESOURCE ID}") + } else { + components = append(components, id.Id) + } + } + + if input.UriSuffix != nil { + components = append(components, *input.UriSuffix) + } + + // We could use a strings.Join(components, "/") here but since we need to check if both the Resource ID and UriSuffix + // value have start with a "/" we might as well do this here? + out := "" + for _, val := range components { + component := val + if strings.HasPrefix(component, "/") { + component = strings.TrimPrefix(component, "/") + } + out += fmt.Sprintf("/%s", component) + } + + return out +} + +// uniqueOperationNames returns a unique, sorted list of Operation Names from the keys of initial and updated. +func (d differ) uniqueOperationNames(initial, updated map[string]resourcemanager.ApiOperation) []string { + uniqueNames := make(map[string]struct{}) + for name := range initial { + uniqueNames[name] = struct{}{} + } + for name := range updated { + uniqueNames[name] = struct{}{} + } + + output := make([]string, 0) + for k := range uniqueNames { + output = append(output, k) + } + sort.Strings(output) + return output +} diff --git a/tools/data-api-differ/internal/differ/differ_operations_test.go b/tools/data-api-differ/internal/differ/differ_operations_test.go new file mode 100644 index 00000000000..5c3476da8c8 --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_operations_test.go @@ -0,0 +1,1072 @@ +package differ + +import ( + "net/http" + "testing" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/sdk/resourcemanager" +) + +func TestDiff_OperationAddedWithResourceId(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": {}, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": {}, + "Second": { + ResourceIdName: pointer.To("SomeId"), + }, + } + ids := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/some/resource/id", + }, + } + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "Second", + Uri: "/some/resource/id", + }, + } + assertChanges(t, expected, *actual) + assertContainsNoBreakingChanges(t, *actual) +} + +func TestDiff_OperationAddedWithResourceIdAndUriSuffix(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": {}, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": {}, + "Second": { + ResourceIdName: pointer.To("SomeId"), + UriSuffix: pointer.To("/example"), + }, + } + ids := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/some/resource/id", + }, + } + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "Second", + Uri: "/some/resource/id/example", + }, + } + assertChanges(t, expected, *actual) + assertContainsNoBreakingChanges(t, *actual) +} + +func TestDiff_OperationAddedWithUriSuffix(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": {}, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": {}, + "Second": { + UriSuffix: pointer.To("/example"), + }, + } + ids := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "Second", + Uri: "/example", + }, + } + assertChanges(t, expected, *actual) + assertContainsNoBreakingChanges(t, *actual) +} + +func TestDiff_OperationContentTypeChanged(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": { + ContentType: pointer.To("application/json"), + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": { + ContentType: pointer.To("text/xml"), + }, + } + ids := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationContentTypeChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "First", + OldContentType: "application/json", + NewContentType: "text/xml", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationExpectedStatusCodeChanged(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": { + ExpectedStatusCodes: []int{200}, + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": { + ExpectedStatusCodes: []int{200, 202}, + }, + } + ids := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationExpectedStatusCodesChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "First", + OldExpectedStatusCodes: []int{200}, + NewExpectedStatusCodes: []int{200, 202}, + }, + } + assertChanges(t, expected, *actual) + assertContainsNoBreakingChanges(t, *actual) +} + +func TestDiff_OperationExpectedStatusCodeChangedBreakingChange(t *testing.T) { + // ExpectedStatusCodes is /conditionally/ a breaking change, if a Status Code is removed then it's a breaking change + // else it's not + initial := map[string]resourcemanager.ApiOperation{ + "First": { + ExpectedStatusCodes: []int{200, 202}, + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": { + ExpectedStatusCodes: []int{200}, + }, + } + ids := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationExpectedStatusCodesChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "First", + OldExpectedStatusCodes: []int{200, 202}, + NewExpectedStatusCodes: []int{200}, + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationLongRunningAdded(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": { + LongRunning: false, + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": { + LongRunning: true, + }, + } + ids := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationLongRunningAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "First", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationLongRunningRemoved(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": { + LongRunning: true, + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": { + LongRunning: false, + }, + } + ids := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationLongRunningRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "First", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationMethodChanged(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": { + Method: http.MethodHead, + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": { + Method: http.MethodGet, + }, + } + ids := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationMethodChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "First", + OldValue: http.MethodHead, + NewValue: http.MethodGet, + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationOptionsAdded(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": { + Options: nil, + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": { + Options: map[string]resourcemanager.ApiOperationOption{ + "Expand": { + QueryStringName: pointer.To("expand"), + Required: false, + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + }, + }, + } + ids := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationOptionsAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "First", + NewValue: map[string]string{ + "Expand": `ObjectDefinition "string" / Required false / QueryStringName "expand"`, + }, + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationOptionsChanged(t *testing.T) { + // Adding a new Option to an existing set of Options is not a breaking change + initial := map[string]resourcemanager.ApiOperation{ + "First": { + Options: map[string]resourcemanager.ApiOperationOption{ + "Expand": { + QueryStringName: pointer.To("expand"), + Required: false, + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + }, + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": { + Options: map[string]resourcemanager.ApiOperationOption{ + "Expand": { + QueryStringName: pointer.To("expand"), + Required: false, + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + "Other": { + QueryStringName: pointer.To("other"), + Required: false, + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + }, + }, + } + ids := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationOptionsChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "First", + OldValue: map[string]string{ + "Expand": `ObjectDefinition "string" / Required false / QueryStringName "expand"`, + }, + NewValue: map[string]string{ + "Expand": `ObjectDefinition "string" / Required false / QueryStringName "expand"`, + "Other": `ObjectDefinition "string" / Required false / QueryStringName "other"`, + }, + }, + } + assertChanges(t, expected, *actual) + assertContainsNoBreakingChanges(t, *actual) +} + +func TestDiff_OperationOptionsChangedBreakingChange(t *testing.T) { + // Changing the ObjectDefinition for an existing Object is a breaking change + initial := map[string]resourcemanager.ApiOperation{ + "First": { + Options: map[string]resourcemanager.ApiOperationOption{ + "Expand": { + QueryStringName: pointer.To("expand"), + Required: false, + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + }, + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": { + Options: map[string]resourcemanager.ApiOperationOption{ + "Expand": { + QueryStringName: pointer.To("expand"), + Required: false, + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.ReferenceApiObjectDefinitionType, + ReferenceName: pointer.To("SomeConstant"), + }, + }, + }, + }, + } + ids := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationOptionsChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "First", + OldValue: map[string]string{ + "Expand": `ObjectDefinition "string" / Required false / QueryStringName "expand"`, + }, + NewValue: map[string]string{ + "Expand": `ObjectDefinition "SomeConstant" / Required false / QueryStringName "expand"`, + }, + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationOptionsRemoved(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": { + Options: map[string]resourcemanager.ApiOperationOption{ + "Expand": { + QueryStringName: pointer.To("expand"), + Required: false, + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + }, + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": { + Options: nil, + }, + } + ids := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationOptionsRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "First", + OldValue: map[string]string{ + "Expand": `ObjectDefinition "string" / Required false / QueryStringName "expand"`, + }, + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationPaginationFieldChanged(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": { + FieldContainingPaginationDetails: pointer.To("OldField"), + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": { + FieldContainingPaginationDetails: pointer.To("NewField"), + }, + } + ids := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationPaginationFieldChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "First", + OldValue: "OldField", + NewValue: "NewField", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationRemovedWithResourceId(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": {}, + "Second": { + ResourceIdName: pointer.To("SomeId"), + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": {}, + } + ids := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/some/example/id", + }, + } + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "Second", + Uri: "/some/example/id", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationRemovedWithResourceIdAndUriSuffix(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": {}, + "Second": { + ResourceIdName: pointer.To("SomeId"), + UriSuffix: pointer.To("/example"), + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": {}, + } + ids := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/some/example/id", + }, + } + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "Second", + Uri: "/some/example/id/example", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationRemovedWithUriSuffix(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": {}, + "Second": { + UriSuffix: pointer.To("/example"), + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": {}, + } + ids := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "Second", + Uri: "/example", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationRequestObjectAdded(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": { + RequestObject: nil, + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": { + RequestObject: &resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + } + ids := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationRequestObjectAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "First", + NewRequestObject: "string", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationRequestObjectChanged(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": { + RequestObject: &resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": { + RequestObject: &resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.ReferenceApiObjectDefinitionType, + ReferenceName: pointer.To("SomeConstant"), + }, + }, + } + ids := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationRequestObjectChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "First", + OldRequestObject: "string", + NewRequestObject: "SomeConstant", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationRequestObjectRemoved(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": { + RequestObject: &resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": { + RequestObject: nil, + }, + } + ids := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationRequestObjectRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "First", + OldRequestObject: "string", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationResourceIDAdded(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": { + ResourceIdName: nil, + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": { + ResourceIdName: pointer.To("Example"), + }, + } + oldIds := make(map[string]resourcemanager.ResourceIdDefinition) + newIds := map[string]resourcemanager.ResourceIdDefinition{ + "Example": { + Id: "/some/example/id", + }, + } + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, oldIds, newIds) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationResourceIdAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "First", + NewResourceIdName: "Example", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationResourceIDChangedUri(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "SomeOperation": { + ResourceIdName: pointer.To("First"), + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "SomeOperation": { + ResourceIdName: pointer.To("First"), + }, + } + oldIds := map[string]resourcemanager.ResourceIdDefinition{ + "First": { + Id: "/some/example/id", + }, + } + newIds := map[string]resourcemanager.ResourceIdDefinition{ + "First": { + Id: "/some/other/id", + }, + } + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, oldIds, newIds) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationResourceIdChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "SomeOperation", + OldResourceIdName: "First", + NewResourceIdName: "First", + OldValue: "/some/example/id", + NewValue: "/some/other/id", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationResourceIDChangedNameAndUri(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "SomeOperation": { + ResourceIdName: pointer.To("First"), + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "SomeOperation": { + ResourceIdName: pointer.To("Second"), + }, + } + oldIds := map[string]resourcemanager.ResourceIdDefinition{ + "First": { + Id: "/some/example/id", + }, + } + newIds := map[string]resourcemanager.ResourceIdDefinition{ + "Second": { + Id: "/some/other/id", + }, + } + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, oldIds, newIds) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationResourceIdRenamed{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "SomeOperation", + OldResourceIdName: "First", + NewResourceIdName: "Second", + }, + changes.OperationResourceIdChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "SomeOperation", + OldResourceIdName: "First", + NewResourceIdName: "Second", + OldValue: "/some/example/id", + NewValue: "/some/other/id", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationResourceIDRemoved(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": { + ResourceIdName: pointer.To("Example"), + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": { + ResourceIdName: nil, + }, + } + oldIds := map[string]resourcemanager.ResourceIdDefinition{ + "Example": { + Id: "/some/example/id", + }, + } + newIds := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, oldIds, newIds) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationResourceIdRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "First", + OldResourceIdName: "Example", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationResourceIDRenamed(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "SomeOperation": { + ResourceIdName: pointer.To("First"), + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "SomeOperation": { + ResourceIdName: pointer.To("Second"), + }, + } + oldIds := map[string]resourcemanager.ResourceIdDefinition{ + "First": { + Id: "/some/example/id", + }, + } + newIds := map[string]resourcemanager.ResourceIdDefinition{ + "Second": { + Id: "/some/example/id", + }, + } + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, oldIds, newIds) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationResourceIdRenamed{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "SomeOperation", + OldResourceIdName: "First", + NewResourceIdName: "Second", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationResponseObjectAdded(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": { + ResponseObject: nil, + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": { + ResponseObject: &resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + } + ids := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationResponseObjectAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "First", + NewResponseObject: "string", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationResponseObjectChanged(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": { + ResponseObject: &resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": { + ResponseObject: &resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.ReferenceApiObjectDefinitionType, + ReferenceName: pointer.To("SomeConstant"), + }, + }, + } + ids := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationResponseObjectChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "First", + OldResponseObject: "string", + NewResponseObject: "SomeConstant", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationResponseObjectRemoved(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": { + ResponseObject: &resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": { + ResponseObject: nil, + }, + } + ids := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationResponseObjectRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "First", + OldResponseObject: "string", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationUriSuffixAdded(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": { + UriSuffix: nil, + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": { + UriSuffix: pointer.To("/doSomething"), + }, + } + ids := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationUriSuffixAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "First", + NewValue: "/doSomething", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationUriSuffixChanged(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": { + UriSuffix: pointer.To("/doSomething"), + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": { + UriSuffix: pointer.To("/doSomethingElse"), + }, + } + ids := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationUriSuffixChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "First", + OldValue: "/doSomething", + NewValue: "/doSomethingElse", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} + +func TestDiff_OperationUriSuffixRemoved(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": { + UriSuffix: pointer.To("/doSomething"), + }, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": { + UriSuffix: nil, + }, + } + ids := make(map[string]resourcemanager.ResourceIdDefinition) + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.OperationUriSuffixRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + OperationName: "First", + OldValue: "/doSomething", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} diff --git a/tools/data-api-differ/internal/differ/differ_resource_ids.go b/tools/data-api-differ/internal/differ/differ_resource_ids.go new file mode 100644 index 00000000000..65a28724466 --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_resource_ids.go @@ -0,0 +1,185 @@ +package differ + +import ( + "fmt" + "sort" + "strings" + + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" + "github.com/hashicorp/pandora/tools/sdk/resourcemanager" +) + +func (d differ) changesForResourceIds(serviceName, apiVersion, apiResource string, initial, updated map[string]resourcemanager.ResourceIdDefinition) []changes.Change { + output := make([]changes.Change, 0) + resourceIdNames := d.uniqueResourceIdNames(initial, updated) + for _, resourceIdName := range resourceIdNames { + log.Logger.Trace(fmt.Sprintf("Detecting changes in Resource ID %q..", resourceIdName)) + changesForResourceId := d.changesForResourceId(serviceName, apiVersion, apiResource, resourceIdName, initial, updated) + output = append(output, changesForResourceId...) + } + return output +} + +func (d differ) changesForResourceId(serviceName, apiVersion, apiResource, resourceIdName string, initial, updated map[string]resourcemanager.ResourceIdDefinition) []changes.Change { + output := make([]changes.Change, 0) + + oldData, isInOld := initial[resourceIdName] + updatedData, isInUpdated := updated[resourceIdName] + + if isInOld && !isInUpdated { + log.Logger.Trace(fmt.Sprintf("Resource ID %q was removed", resourceIdName)) + output = append(output, changes.ResourceIdRemoved{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ResourceIdName: resourceIdName, + ResourceIdValue: oldData.Id, + }) + return output + } + if !isInOld && isInUpdated { + log.Logger.Trace(fmt.Sprintf("Resource ID %q was added", resourceIdName)) + output = append(output, changes.ResourceIdAdded{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ResourceIdName: resourceIdName, + ResourceIdValue: updatedData.Id, + }) + return output + } + + log.Logger.Trace("Determining any changes to the Common Alias..") + commonAliasChanges := d.changesForResourceIdCommonAlias(serviceName, apiVersion, apiResource, resourceIdName, oldData, updatedData) + output = append(output, commonAliasChanges...) + + log.Logger.Trace("Determining any changes to the Resource ID Segments..") + segmentChanges := d.changesForResourceIdSegments(serviceName, apiVersion, apiResource, resourceIdName, oldData, updatedData) + output = append(output, segmentChanges...) + + return output +} + +// changesForResourceIdCommonAlias determines any changes related to the Common Alias for the initial and updated version of this Resource ID. +func (d differ) changesForResourceIdCommonAlias(serviceName, apiVersion, apiResource, resourceIdName string, initial, updated resourcemanager.ResourceIdDefinition) []changes.Change { + output := make([]changes.Change, 0) + + if initial.CommonAlias != nil && updated.CommonAlias == nil { + log.Logger.Trace(fmt.Sprintf("The Resource ID %q is no longer a Common ID", resourceIdName)) + output = append(output, changes.ResourceIdCommonIdRemoved{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ResourceIdName: resourceIdName, + CommonAliasName: *initial.CommonAlias, + ResourceIdValue: initial.Id, + }) + } + if initial.CommonAlias == nil && updated.CommonAlias != nil { + log.Logger.Trace(fmt.Sprintf("The Resource ID %q is now a Common ID", resourceIdName)) + output = append(output, changes.ResourceIdCommonIdAdded{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ResourceIdName: resourceIdName, + CommonAliasName: *updated.CommonAlias, + ResourceIdValue: updated.Id, + }) + } + if initial.CommonAlias != nil && updated.CommonAlias != nil && *initial.CommonAlias != *updated.CommonAlias { + log.Logger.Trace(fmt.Sprintf("The Resource ID %q has changed it's Common Alias from %q to %q", resourceIdName, *initial.CommonAlias, *updated.CommonAlias)) + output = append(output, changes.ResourceIdCommonIdChanged{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ResourceIdName: resourceIdName, + OldCommonAliasName: *initial.CommonAlias, + OldValue: initial.Id, + NewCommonAliasName: *updated.CommonAlias, + NewValue: updated.Id, + }) + } + + return output +} + +// changesForResourceIdSegments determines any changes related to the Segments for the initial and updated version of this Resource ID. +func (d differ) changesForResourceIdSegments(serviceName, apiVersion, apiResource, resourceIdName string, initial, updated resourcemanager.ResourceIdDefinition) []changes.Change { + output := make([]changes.Change, 0) + + // Stringify the Resource ID Segments so these are consistent for diffing and output + oldStringified := d.stringifyResourceIdSegments(initial.Segments) + updatedStringified := d.stringifyResourceIdSegments(updated.Segments) + + if len(initial.Segments) != len(updated.Segments) { + log.Logger.Trace(fmt.Sprintf("The Resource ID has a different set of segments (old %d segments / new %d segments)", len(initial.Segments), len(updated.Segments))) + output = append(output, changes.ResourceIdSegmentsChangedLength{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ResourceIdName: resourceIdName, + OldValue: oldStringified, + NewValue: updatedStringified, + }) + return output + } + + // at this point we should be able to assume these are the same size, so: + for i, oldValue := range oldStringified { + updatedValue := updatedStringified[i] + if oldValue != updatedValue { + log.Logger.Trace(fmt.Sprintf("Resource ID Segment Index %d differs", i)) + output = append(output, changes.ResourceIdSegmentChangedValue{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ResourceIdName: resourceIdName, + SegmentIndex: i, + OldValue: oldValue, + NewValue: updatedValue, + }) + } + } + return output +} + +func (d differ) stringifyResourceIdSegments(input []resourcemanager.ResourceIdSegment) []string { + output := make([]string, 0) + + for _, item := range input { + // We could JSON marshal these, but this is _probably_ clearer having this in text rather than JSON? + components := []string{ + fmt.Sprintf("Name %q", item.Name), + fmt.Sprintf("Type %q", string(item.Type)), + } + if item.ConstantReference != nil { + components = append(components, fmt.Sprintf("ConstantReference %q", *item.ConstantReference)) + } + if item.FixedValue != nil { + components = append(components, fmt.Sprintf("FixedValue %q", *item.FixedValue)) + } + components = append(components, fmt.Sprintf("ExampleValue %q", item.ExampleValue)) + output = append(output, strings.Join(components, " / ")) + } + + return output +} + +// uniqueResourceIdNames returns a unique, sorted list of Resource ID Names from the keys of initial and updated. +func (d differ) uniqueResourceIdNames(initial, updated map[string]resourcemanager.ResourceIdDefinition) []string { + uniqueNames := make(map[string]struct{}) + for name := range initial { + uniqueNames[name] = struct{}{} + } + for name := range updated { + uniqueNames[name] = struct{}{} + } + + output := make([]string, 0) + for k := range uniqueNames { + output = append(output, k) + } + sort.Strings(output) + return output +} diff --git a/tools/data-api-differ/internal/differ/differ_resource_ids_test.go b/tools/data-api-differ/internal/differ/differ_resource_ids_test.go new file mode 100644 index 00000000000..5dbbdc79326 --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_resource_ids_test.go @@ -0,0 +1,370 @@ +package differ + +import ( + "testing" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/sdk/resourcemanager" +) + +func TestDiff_ResourceIdAdded(t *testing.T) { + oldIds := make(map[string]resourcemanager.ResourceIdDefinition) + newIds := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/some/resource/id", + }, + } + actual := differ{}.changesForResourceIds("Computer", "2020-01-01", "Example", oldIds, newIds) + expected := []changes.Change{ + changes.ResourceIdAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ResourceIdName: "SomeId", + ResourceIdValue: "/some/resource/id", + }, + } + assertChanges(t, expected, actual) + assertContainsNoBreakingChanges(t, actual) +} + +func TestDiff_ResourceIdCommonIdAdded(t *testing.T) { + oldIds := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/some/resource/id", + }, + } + newIds := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/some/resource/id", + CommonAlias: pointer.To("SomeCommonAlias"), + }, + } + actual := differ{}.changesForResourceIds("Computer", "2020-01-01", "Example", oldIds, newIds) + expected := []changes.Change{ + changes.ResourceIdCommonIdAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ResourceIdName: "SomeId", + CommonAliasName: "SomeCommonAlias", + ResourceIdValue: "/some/resource/id", + }, + } + assertChanges(t, expected, actual) + assertContainsBreakingChanges(t, actual) +} + +func TestDiff_ResourceIdCommonIdChanged(t *testing.T) { + oldIds := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/some/resource/id", + CommonAlias: pointer.To("SomeCommonAlias"), + }, + } + newIds := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/some/resource/id", + CommonAlias: pointer.To("SomeOtherAlias"), + }, + } + actual := differ{}.changesForResourceIds("Computer", "2020-01-01", "Example", oldIds, newIds) + expected := []changes.Change{ + changes.ResourceIdCommonIdChanged{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ResourceIdName: "SomeId", + OldCommonAliasName: "SomeCommonAlias", + OldValue: "/some/resource/id", + NewCommonAliasName: "SomeOtherAlias", + NewValue: "/some/resource/id", + }, + } + assertChanges(t, expected, actual) + assertContainsBreakingChanges(t, actual) +} + +func TestDiff_ResourceIdCommonIdRemoved(t *testing.T) { + oldIds := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/some/resource/id", + CommonAlias: pointer.To("SomeCommonAlias"), + }, + } + newIds := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/some/resource/id", + }, + } + actual := differ{}.changesForResourceIds("Computer", "2020-01-01", "Example", oldIds, newIds) + expected := []changes.Change{ + changes.ResourceIdCommonIdRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ResourceIdName: "SomeId", + CommonAliasName: "SomeCommonAlias", + ResourceIdValue: "/some/resource/id", + }, + } + assertChanges(t, expected, actual) + assertContainsBreakingChanges(t, actual) +} + +func TestDiff_ResourceIdSegmentsAdded(t *testing.T) { + // NOTE: when Segments are added this should still get surfaced as `ResourceIdSegmentsChangedLength` + oldIds := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/example/{name}", + Segments: []resourcemanager.ResourceIdSegment{ + { + Name: "name", + Type: resourcemanager.UserSpecifiedSegment, + ExampleValue: "someName", + }, + }, + }, + } + newIds := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/{name}", + Segments: []resourcemanager.ResourceIdSegment{ + { + Name: "staticExample", + Type: resourcemanager.StaticSegment, + FixedValue: pointer.To("example"), + ExampleValue: "example", + }, + { + Name: "name", + Type: resourcemanager.UserSpecifiedSegment, + ExampleValue: "someName", + }, + }, + }, + } + actual := differ{}.changesForResourceIds("Computer", "2020-01-01", "Example", oldIds, newIds) + expected := []changes.Change{ + changes.ResourceIdSegmentsChangedLength{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ResourceIdName: "SomeId", + OldValue: []string{ + `Name "name" / Type "UserSpecified" / ExampleValue "someName"`, + }, + NewValue: []string{ + `Name "staticExample" / Type "Static" / FixedValue "example" / ExampleValue "example"`, + `Name "name" / Type "UserSpecified" / ExampleValue "someName"`, + }, + }, + } + assertChanges(t, expected, actual) + assertContainsBreakingChanges(t, actual) +} + +func TestDiff_ResourceIdSegmentsChangedFixedValue(t *testing.T) { + oldIds := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/first", + Segments: []resourcemanager.ResourceIdSegment{ + { + Name: "staticExample", + Type: resourcemanager.StaticSegment, + FixedValue: pointer.To("first"), + ExampleValue: "first", + }, + }, + }, + } + newIds := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/second", + Segments: []resourcemanager.ResourceIdSegment{ + { + Name: "staticExample", + Type: resourcemanager.StaticSegment, + FixedValue: pointer.To("second"), + ExampleValue: "second", + }, + }, + }, + } + actual := differ{}.changesForResourceIds("Computer", "2020-01-01", "Example", oldIds, newIds) + expected := []changes.Change{ + changes.ResourceIdSegmentChangedValue{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ResourceIdName: "SomeId", + SegmentIndex: 0, + OldValue: `Name "staticExample" / Type "Static" / FixedValue "first" / ExampleValue "first"`, + NewValue: `Name "staticExample" / Type "Static" / FixedValue "second" / ExampleValue "second"`, + }, + } + assertChanges(t, expected, actual) + assertContainsBreakingChanges(t, actual) +} + +func TestDiff_ResourceIdSegmentsChangedName(t *testing.T) { + oldIds := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/first", + Segments: []resourcemanager.ResourceIdSegment{ + { + Name: "staticExample", + Type: resourcemanager.StaticSegment, + FixedValue: pointer.To("first"), + ExampleValue: "first", + }, + }, + }, + } + newIds := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/first", + Segments: []resourcemanager.ResourceIdSegment{ + { + Name: "updatedName", + Type: resourcemanager.StaticSegment, + FixedValue: pointer.To("first"), + ExampleValue: "first", + }, + }, + }, + } + actual := differ{}.changesForResourceIds("Computer", "2020-01-01", "Example", oldIds, newIds) + expected := []changes.Change{ + changes.ResourceIdSegmentChangedValue{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ResourceIdName: "SomeId", + SegmentIndex: 0, + OldValue: `Name "staticExample" / Type "Static" / FixedValue "first" / ExampleValue "first"`, + NewValue: `Name "updatedName" / Type "Static" / FixedValue "first" / ExampleValue "first"`, + }, + } + assertChanges(t, expected, actual) + assertContainsBreakingChanges(t, actual) +} + +func TestDiff_ResourceIdSegmentsChangedType(t *testing.T) { + // This test covers when the Resource ID segment has changed type (e.g. a String to a Constant) + + oldIds := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/{skuName}", + Segments: []resourcemanager.ResourceIdSegment{ + { + Name: "skuName", + Type: resourcemanager.UserSpecifiedSegment, + ExampleValue: "first", + }, + }, + }, + } + newIds := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/{skuName}", + Segments: []resourcemanager.ResourceIdSegment{ + { + Name: "skuName", + Type: resourcemanager.ConstantSegment, + ConstantReference: pointer.To("SomeConstant"), + ExampleValue: "Basic", + }, + }, + ConstantNames: []string{"SomeConstant"}, + }, + } + actual := differ{}.changesForResourceIds("Computer", "2020-01-01", "Example", oldIds, newIds) + expected := []changes.Change{ + changes.ResourceIdSegmentChangedValue{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ResourceIdName: "SomeId", + SegmentIndex: 0, + OldValue: `Name "skuName" / Type "UserSpecified" / ExampleValue "first"`, + NewValue: `Name "skuName" / Type "Constant" / ConstantReference "SomeConstant" / ExampleValue "Basic"`, + }, + } + assertChanges(t, expected, actual) + assertContainsBreakingChanges(t, actual) +} + +func TestDiff_ResourceIdSegmentsRemoved(t *testing.T) { + // NOTE: when Segments are removed this should still get surfaced as `ResourceIdSegmentsChangedLength` + oldIds := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/example/{name}", + Segments: []resourcemanager.ResourceIdSegment{ + { + Name: "staticExample", + Type: resourcemanager.StaticSegment, + FixedValue: pointer.To("example"), + ExampleValue: "example", + }, + { + Name: "name", + Type: resourcemanager.UserSpecifiedSegment, + ExampleValue: "someName", + }, + }, + }, + } + newIds := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/{name}", + Segments: []resourcemanager.ResourceIdSegment{ + { + Name: "name", + Type: resourcemanager.UserSpecifiedSegment, + ExampleValue: "someName", + }, + }, + }, + } + actual := differ{}.changesForResourceIds("Computer", "2020-01-01", "Example", oldIds, newIds) + expected := []changes.Change{ + changes.ResourceIdSegmentsChangedLength{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ResourceIdName: "SomeId", + OldValue: []string{ + `Name "staticExample" / Type "Static" / FixedValue "example" / ExampleValue "example"`, + `Name "name" / Type "UserSpecified" / ExampleValue "someName"`, + }, + NewValue: []string{ + `Name "name" / Type "UserSpecified" / ExampleValue "someName"`, + }, + }, + } + assertChanges(t, expected, actual) + assertContainsBreakingChanges(t, actual) +} + +func TestDiff_ResourceIdRemoved(t *testing.T) { + oldIds := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/some/resource/id", + }, + } + newIds := make(map[string]resourcemanager.ResourceIdDefinition) + actual := differ{}.changesForResourceIds("Computer", "2020-01-01", "Example", oldIds, newIds) + expected := []changes.Change{ + changes.ResourceIdRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ResourceIdName: "SomeId", + ResourceIdValue: "/some/resource/id", + }, + } + assertChanges(t, expected, actual) + assertContainsBreakingChanges(t, actual) +} diff --git a/tools/data-api-differ/internal/differ/differ_services.go b/tools/data-api-differ/internal/differ/differ_services.go new file mode 100644 index 00000000000..67e745bb4d2 --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_services.go @@ -0,0 +1,86 @@ +package differ + +import ( + "fmt" + "sort" + + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/dataapi" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" +) + +// changesForServices determines the changes between the initial and updated set of Services. +func (d differ) changesForServices(initial map[string]dataapi.ServiceData, updated map[string]dataapi.ServiceData) (*[]changes.Change, error) { + output := make([]changes.Change, 0) + // first pull out a unique list of Service names + log.Logger.Info("Identifying a unique list of Service Names..") + serviceNames := d.uniqueServiceNames(initial, updated) + for _, serviceName := range serviceNames { + log.Logger.Info(fmt.Sprintf("Detecting changes in Service %q..", serviceName)) + changesForService, err := d.changesForService(serviceName, initial, updated) + if err != nil { + return nil, fmt.Errorf("detecting changes to the Service %q: %+v", serviceName, err) + } + output = append(output, *changesForService...) + } + return &output, nil +} + +// changesForService determines the changes between two different Services. +func (d differ) changesForService(serviceName string, initial map[string]dataapi.ServiceData, updated map[string]dataapi.ServiceData) (*[]changes.Change, error) { + output := make([]changes.Change, 0) + oldData, inOldData := initial[serviceName] + updatedData, inUpdatedData := updated[serviceName] + + // if it's been removed + if inOldData && !inUpdatedData { + log.Logger.Trace(fmt.Sprintf("Service %q was removed", serviceName)) + output = append(output, changes.ServiceRemoved{ + ServiceName: serviceName, + }) + // no point continuing to diff if it's gone + return &output, nil + } + + // is it a new Service? + if !inOldData && inUpdatedData { + log.Logger.Trace(fmt.Sprintf("Service %q was added", serviceName)) + output = append(output, changes.ServiceAdded{ + ServiceName: serviceName, + }) + // intentionally not returning here + } + + // TODO: support raising if `Generate`, `ResourceProvider` or `TerraformPackageName` changes if required + + // the old set may not necessarily exist + var oldApiVersions map[string]dataapi.ApiVersionData + if inOldData { + oldApiVersions = oldData.ApiVersions + } + changesForApiVersions, err := d.changesForApiVersions(serviceName, oldApiVersions, updatedData.ApiVersions) + if err != nil { + return nil, fmt.Errorf("detecting changes to the API Versions: %+v", err) + } + output = append(output, *changesForApiVersions...) + + return &output, nil +} + +// uniqueServiceNames returns a unique, ordered list of Service Names from the initial and updated set of Services. +func (d differ) uniqueServiceNames(initial map[string]dataapi.ServiceData, updated map[string]dataapi.ServiceData) []string { + uniqueNames := make(map[string]struct{}) + for name := range initial { + uniqueNames[name] = struct{}{} + } + for name := range updated { + uniqueNames[name] = struct{}{} + } + + output := make([]string, 0) + for k := range uniqueNames { + output = append(output, k) + } + sort.Strings(output) + return output +} diff --git a/tools/data-api-differ/internal/differ/differ_services_test.go b/tools/data-api-differ/internal/differ/differ_services_test.go new file mode 100644 index 00000000000..d9653023be5 --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_services_test.go @@ -0,0 +1,48 @@ +package differ + +import ( + "testing" + + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/dataapi" +) + +func TestDiff_ServiceAdded(t *testing.T) { + initial := map[string]dataapi.ServiceData{ + // intentionally empty + } + updated := map[string]dataapi.ServiceData{ + "Computer": {}, + } + actual, err := differ{}.changesForService("Computer", initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.ServiceAdded{ + ServiceName: "Computer", + }, + } + assertChanges(t, expected, *actual) + assertContainsNoBreakingChanges(t, *actual) +} + +func TestDiff_ServiceRemoved(t *testing.T) { + initial := map[string]dataapi.ServiceData{ + "Computer": {}, + } + updated := map[string]dataapi.ServiceData{ + // intentionally empty + } + actual, err := differ{}.changesForService("Computer", initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []changes.Change{ + changes.ServiceRemoved{ + ServiceName: "Computer", + }, + } + assertChanges(t, expected, *actual) + assertContainsBreakingChanges(t, *actual) +} diff --git a/tools/data-api-differ/internal/differ/differ_test.go b/tools/data-api-differ/internal/differ/differ_test.go new file mode 100644 index 00000000000..c86b79d1d4d --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_test.go @@ -0,0 +1,751 @@ +package differ + +import ( + "testing" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/dataapi" + "github.com/hashicorp/pandora/tools/sdk/resourcemanager" +) + +// These tests check the Differ's main code path for each Data Source (e.g. Resource Manager) +// the specific Changes are tested in the subcomponents (e.g. `differ_services.go`) so these +// are high-level tests to ensure we're touching the main code paths, rather than necessarily +// testing every bit of functionality. + +func TestDiff_ResourceManager_ServiceAdded(t *testing.T) { + initial := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + // intentionally empty + }, + } + updated := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": {}, + }, + } + expected := []changes.Change{ + changes.ServiceAdded{ + ServiceName: "Computer", + }, + } + containsBreakingChanges := false + determineAndValidateDiff(t, initial, updated, expected, containsBreakingChanges) +} + +func TestDiff_ResourceManager_ServiceRemoved(t *testing.T) { + initial := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": {}, + }, + } + updated := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + // intentionally empty + }, + } + expected := []changes.Change{ + changes.ServiceRemoved{ + ServiceName: "Computer", + }, + } + containsBreakingChanges := true + determineAndValidateDiff(t, initial, updated, expected, containsBreakingChanges) +} + +func TestDiff_ResourceManager_ApiVersionAdded(t *testing.T) { + initial := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": {}, + }, + }, + }, + } + updated := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": {}, + "2022-01-01": {}, + }, + }, + }, + } + expected := []changes.Change{ + changes.ApiVersionAdded{ + ServiceName: "Computer", + ApiVersion: "2022-01-01", + }, + } + containsBreakingChanges := false + determineAndValidateDiff(t, initial, updated, expected, containsBreakingChanges) +} + +func TestDiff_ResourceManager_ApiVersionRemoved(t *testing.T) { + initial := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": {}, + "2022-01-01": {}, + }, + }, + }, + } + updated := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": {}, + }, + }, + }, + } + expected := []changes.Change{ + changes.ApiVersionRemoved{ + ServiceName: "Computer", + ApiVersion: "2022-01-01", + }, + } + containsBreakingChanges := true + determineAndValidateDiff(t, initial, updated, expected, containsBreakingChanges) +} + +func TestDiff_ResourceManager_ApiResourceAdded(t *testing.T) { + initial := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": {}, + }, + }, + }, + }, + }, + } + updated := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": {}, + "OnPremise": {}, + }, + }, + }, + }, + }, + } + expected := []changes.Change{ + changes.ApiResourceAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "OnPremise", + }, + } + containsBreakingChanges := false + determineAndValidateDiff(t, initial, updated, expected, containsBreakingChanges) +} + +func TestDiff_ResourceManager_ApiResourceRemoved(t *testing.T) { + initial := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": {}, + "OnPremise": {}, + }, + }, + }, + }, + }, + } + updated := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": {}, + }, + }, + }, + }, + }, + } + expected := []changes.Change{ + changes.ApiResourceRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "OnPremise", + }, + } + containsBreakingChanges := true + determineAndValidateDiff(t, initial, updated, expected, containsBreakingChanges) +} + +func TestDiff_ResourceManager_ConstantAdded(t *testing.T) { + initial := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": { + Constants: map[string]resourcemanager.ConstantDetails{ + "Skus": {}, + }, + }, + }, + }, + }, + }, + }, + } + updated := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": { + Constants: map[string]resourcemanager.ConstantDetails{ + "NetworkPerformance": { + Type: resourcemanager.StringConstant, + Values: map[string]string{ + "First": "Value", + "Second": "OtherValue", + }, + }, + "Skus": {}, + }, + }, + }, + }, + }, + }, + }, + } + expected := []changes.Change{ + changes.ConstantAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Instances", + ConstantName: "NetworkPerformance", + ConstantType: string(resourcemanager.StringConstant), + KeysAndValues: map[string]string{ + "First": "Value", + "Second": "OtherValue", + }, + }, + } + containsBreakingChanges := false + determineAndValidateDiff(t, initial, updated, expected, containsBreakingChanges) +} + +func TestDiff_ResourceManager_ConstantRemoved(t *testing.T) { + initial := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": { + Constants: map[string]resourcemanager.ConstantDetails{ + "NetworkPerformance": { + Type: resourcemanager.StringConstant, + Values: map[string]string{ + "First": "Value", + "Second": "OtherValue", + }, + }, + "Skus": {}, + }, + }, + }, + }, + }, + }, + }, + } + updated := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": { + Constants: map[string]resourcemanager.ConstantDetails{ + "Skus": {}, + }, + }, + }, + }, + }, + }, + }, + } + expected := []changes.Change{ + changes.ConstantRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Instances", + ConstantName: "NetworkPerformance", + ConstantType: string(resourcemanager.StringConstant), + KeysAndValues: map[string]string{ + "First": "Value", + "Second": "OtherValue", + }, + }, + } + containsBreakingChanges := true + determineAndValidateDiff(t, initial, updated, expected, containsBreakingChanges) +} + +func TestDiff_ResourceManager_ModelAdded(t *testing.T) { + initial := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": { + Models: map[string]resourcemanager.ModelDetails{ + "VirtualMachine": {}, + }, + }, + }, + }, + }, + }, + }, + } + updated := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": { + Models: map[string]resourcemanager.ModelDetails{ + "PhysicalMachine": {}, + "VirtualMachine": {}, + }, + }, + }, + }, + }, + }, + }, + } + expected := []changes.Change{ + changes.ModelAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Instances", + ModelName: "PhysicalMachine", + }, + } + containsBreakingChanges := false + determineAndValidateDiff(t, initial, updated, expected, containsBreakingChanges) +} + +func TestDiff_ResourceManager_ModelRemoved(t *testing.T) { + initial := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": { + Models: map[string]resourcemanager.ModelDetails{ + "PhysicalMachine": {}, + "VirtualMachine": {}, + }, + }, + }, + }, + }, + }, + }, + } + updated := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": { + Models: map[string]resourcemanager.ModelDetails{ + "VirtualMachine": {}, + }, + }, + }, + }, + }, + }, + }, + } + expected := []changes.Change{ + changes.ModelRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Instances", + ModelName: "PhysicalMachine", + }, + } + containsBreakingChanges := true + determineAndValidateDiff(t, initial, updated, expected, containsBreakingChanges) +} + +func TestDiff_ResourceManager_FieldAdded(t *testing.T) { + initial := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": { + Models: map[string]resourcemanager.ModelDetails{ + "PhysicalMachine": { + Fields: map[string]resourcemanager.FieldDetails{ + "First": { + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + updated := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": { + Models: map[string]resourcemanager.ModelDetails{ + "PhysicalMachine": { + Fields: map[string]resourcemanager.FieldDetails{ + "First": { + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + "Second": { + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + expected := []changes.Change{ + changes.FieldAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Instances", + ModelName: "PhysicalMachine", + FieldName: "Second", + }, + } + containsBreakingChanges := false + determineAndValidateDiff(t, initial, updated, expected, containsBreakingChanges) +} + +func TestDiff_ResourceManager_FieldRemoved(t *testing.T) { + initial := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": { + Models: map[string]resourcemanager.ModelDetails{ + "PhysicalMachine": { + Fields: map[string]resourcemanager.FieldDetails{ + "First": { + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + "Second": { + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + updated := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": { + Models: map[string]resourcemanager.ModelDetails{ + "PhysicalMachine": { + Fields: map[string]resourcemanager.FieldDetails{ + "First": { + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + expected := []changes.Change{ + changes.FieldRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Instances", + ModelName: "PhysicalMachine", + FieldName: "Second", + }, + } + containsBreakingChanges := true + determineAndValidateDiff(t, initial, updated, expected, containsBreakingChanges) +} + +func TestDiff_ResourceManager_OperationAdded(t *testing.T) { + initial := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": { + Operations: map[string]resourcemanager.ApiOperation{ + "First": { + UriSuffix: pointer.To("/hello"), + }, + }, + }, + }, + }, + }, + }, + }, + } + updated := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": { + Operations: map[string]resourcemanager.ApiOperation{ + "First": { + UriSuffix: pointer.To("/hello"), + }, + "Second": { + UriSuffix: pointer.To("/world"), + }, + }, + }, + }, + }, + }, + }, + }, + } + expected := []changes.Change{ + changes.OperationAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Instances", + OperationName: "Second", + Uri: "/world", + }, + } + containsBreakingChanges := false + determineAndValidateDiff(t, initial, updated, expected, containsBreakingChanges) +} + +func TestDiff_ResourceManager_OperationRemoved(t *testing.T) { + initial := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": { + Operations: map[string]resourcemanager.ApiOperation{ + "First": { + UriSuffix: pointer.To("/hello"), + }, + "Second": { + UriSuffix: pointer.To("/world"), + }, + }, + }, + }, + }, + }, + }, + }, + } + updated := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": { + Operations: map[string]resourcemanager.ApiOperation{ + "First": { + UriSuffix: pointer.To("/hello"), + }, + }, + }, + }, + }, + }, + }, + }, + } + expected := []changes.Change{ + changes.OperationRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Instances", + OperationName: "Second", + Uri: "/world", + }, + } + containsBreakingChanges := true + determineAndValidateDiff(t, initial, updated, expected, containsBreakingChanges) +} + +func TestDiff_ResourceManager_ResourceIdAdded(t *testing.T) { + initial := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": { + ResourceIds: make(map[string]resourcemanager.ResourceIdDefinition), + }, + }, + }, + }, + }, + }, + } + updated := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": { + ResourceIds: map[string]resourcemanager.ResourceIdDefinition{ + "First": { + Id: "/example", + }, + }, + }, + }, + }, + }, + }, + }, + } + expected := []changes.Change{ + changes.ResourceIdAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Instances", + ResourceIdName: "First", + ResourceIdValue: "/example", + }, + } + containsBreakingChanges := false + determineAndValidateDiff(t, initial, updated, expected, containsBreakingChanges) +} + +func TestDiff_ResourceManager_ResourceIdRemoved(t *testing.T) { + initial := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": { + ResourceIds: map[string]resourcemanager.ResourceIdDefinition{ + "First": { + Id: "/example", + }, + }, + }, + }, + }, + }, + }, + }, + } + updated := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Resources: map[string]dataapi.ApiResourceData{ + "Instances": { + ResourceIds: make(map[string]resourcemanager.ResourceIdDefinition), + }, + }, + }, + }, + }, + }, + } + expected := []changes.Change{ + changes.ResourceIdRemoved{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Instances", + ResourceIdName: "First", + ResourceIdValue: "/example", + }, + } + containsBreakingChanges := true + determineAndValidateDiff(t, initial, updated, expected, containsBreakingChanges) +} diff --git a/tools/data-api-differ/internal/differ/interface.go b/tools/data-api-differ/internal/differ/interface.go new file mode 100644 index 00000000000..025c4601f63 --- /dev/null +++ b/tools/data-api-differ/internal/differ/interface.go @@ -0,0 +1,26 @@ +package differ + +import ( + "fmt" + + "github.com/hashicorp/pandora/tools/data-api-differ/internal/dataapi" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" +) + +// Diff returns information about the changes between `initialPath` and `updatedPath`. +func Diff(dataApiBinaryPath, initialPath, updatedPath string) (*Result, error) { + log.Logger.Trace(fmt.Sprintf("Parsing the Initial Data Set from %q..", initialPath)) + initialData, err := dataapi.ParseDataFromPath(dataApiBinaryPath, initialPath) + if err != nil { + return nil, fmt.Errorf("parsing data from %q: %+v", initialPath, err) + } + + log.Logger.Trace(fmt.Sprintf("Parsing the Updated Data Set from %q..", updatedPath)) + updatedData, err := dataapi.ParseDataFromPath(dataApiBinaryPath, updatedPath) + if err != nil { + return nil, fmt.Errorf("parsing data from %q: %+v", updatedPath, err) + } + + log.Logger.Trace("Performing the diff..") + return performDiff(*initialData, *updatedData) +} diff --git a/tools/data-api-differ/internal/differ/models.go b/tools/data-api-differ/internal/differ/models.go new file mode 100644 index 00000000000..73ba836781e --- /dev/null +++ b/tools/data-api-differ/internal/differ/models.go @@ -0,0 +1,18 @@ +package differ + +import "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + +type Result struct { + // Changes is a slice of the Changes present between the two paths + Changes []changes.Change +} + +func (r Result) ContainsBreakingChanges() bool { + for _, change := range r.Changes { + if change.IsBreaking() { + return true + } + } + + return false +} From a7f55948d849fe146f1726cb5878250aa2fc0676 Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Wed, 6 Dec 2023 10:39:55 +0100 Subject: [PATCH 06/20] tools/data-api-differ: adding a happy path/no changes test too --- .../data-api-differ/internal/dataapi/data.go | 6 +- .../internal/dataapi/models.go | 2 +- .../internal/differ/differ_constants_test.go | 27 +++++++++ .../internal/differ/differ_fields_test.go | 24 ++++++++ .../internal/differ/differ_models_test.go | 34 +++++++++++ .../internal/differ/differ_operations_test.go | 21 +++++++ .../differ/differ_resource_ids_test.go | 17 ++++++ .../internal/differ/differ_services_test.go | 16 +++++ .../internal/differ/differ_test.go | 60 +++++++++++++++++++ 9 files changed, 204 insertions(+), 3 deletions(-) diff --git a/tools/data-api-differ/internal/dataapi/data.go b/tools/data-api-differ/internal/dataapi/data.go index 17f793e63d4..67fd1a2a54c 100644 --- a/tools/data-api-differ/internal/dataapi/data.go +++ b/tools/data-api-differ/internal/dataapi/data.go @@ -2,9 +2,11 @@ package dataapi import ( "fmt" + "math/rand" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" "github.com/hashicorp/pandora/tools/sdk/resourcemanager" - "math/rand" ) // ParseDataFromPath launches the Data API using inputPath as the API Definitions directory. @@ -74,7 +76,7 @@ func populateServiceDetails(client resourcemanager.Client, summary resourcemanag return &ServiceData{ ApiVersions: apiVersions, Generate: summary.Generate, - ResourceProvider: serviceDetails.ResourceProvider, + ResourceProvider: pointer.To(serviceDetails.ResourceProvider), TerraformPackageName: serviceDetails.TerraformPackageName, }, nil } diff --git a/tools/data-api-differ/internal/dataapi/models.go b/tools/data-api-differ/internal/dataapi/models.go index 3e779a0bde7..5482272ac47 100644 --- a/tools/data-api-differ/internal/dataapi/models.go +++ b/tools/data-api-differ/internal/dataapi/models.go @@ -14,7 +14,7 @@ type ServiceData struct { Generate bool // ResourceProvider is the Resource Provider this service represents - ResourceProvider string + ResourceProvider *string // TerraformPackageName is the name of the Service Package within // the Terraform Provider associated with this service. diff --git a/tools/data-api-differ/internal/differ/differ_constants_test.go b/tools/data-api-differ/internal/differ/differ_constants_test.go index b7d195d9caf..be9c406b520 100644 --- a/tools/data-api-differ/internal/differ/differ_constants_test.go +++ b/tools/data-api-differ/internal/differ/differ_constants_test.go @@ -7,6 +7,33 @@ import ( "github.com/hashicorp/pandora/tools/sdk/resourcemanager" ) +func TestDiff_ConstantNoChanges(t *testing.T) { + initial := map[string]resourcemanager.ConstantDetails{ + "First": { + Type: resourcemanager.IntegerConstant, + Values: map[string]string{ + "One": "1", + "Two": "2", + "Three": "3", + }, + }, + } + updated := map[string]resourcemanager.ConstantDetails{ + "First": { + Type: resourcemanager.IntegerConstant, + Values: map[string]string{ + "One": "1", + "Two": "2", + "Three": "3", + }, + }, + } + actual := differ{}.changesForConstants("Computer", "2020-01-01", "Example", initial, updated) + expected := make([]changes.Change, 0) + assertChanges(t, expected, actual) + assertContainsNoBreakingChanges(t, actual) +} + func TestDiff_ConstantAdded(t *testing.T) { initial := map[string]resourcemanager.ConstantDetails{ "First": {}, diff --git a/tools/data-api-differ/internal/differ/differ_fields_test.go b/tools/data-api-differ/internal/differ/differ_fields_test.go index ecfae168e43..0ac22152197 100644 --- a/tools/data-api-differ/internal/differ/differ_fields_test.go +++ b/tools/data-api-differ/internal/differ/differ_fields_test.go @@ -8,6 +8,30 @@ import ( "github.com/hashicorp/pandora/tools/sdk/resourcemanager" ) +func TestDiff_FieldNoChanges(t *testing.T) { + initial := map[string]resourcemanager.FieldDetails{ + "First": { + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + } + updated := map[string]resourcemanager.FieldDetails{ + "First": { + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + }, + } + actual, err := differ{}.changesForFields("Computer", "2020-01-01", "Example", "SomeModel", initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + expected := make([]changes.Change, 0) + assertChanges(t, expected, *actual) + assertContainsNoBreakingChanges(t, *actual) +} + func TestDiff_FieldAdded(t *testing.T) { initial := map[string]resourcemanager.FieldDetails{ "First": { diff --git a/tools/data-api-differ/internal/differ/differ_models_test.go b/tools/data-api-differ/internal/differ/differ_models_test.go index 89b1b6c15f5..06c229439ec 100644 --- a/tools/data-api-differ/internal/differ/differ_models_test.go +++ b/tools/data-api-differ/internal/differ/differ_models_test.go @@ -8,6 +8,40 @@ import ( "github.com/hashicorp/pandora/tools/sdk/resourcemanager" ) +func TestDiff_ModelNoChanges(t *testing.T) { + initial := map[string]resourcemanager.ModelDetails{ + "First": { + Fields: map[string]resourcemanager.FieldDetails{ + "Example": { + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + Required: true, + }, + }, + }, + } + updated := map[string]resourcemanager.ModelDetails{ + "First": { + Fields: map[string]resourcemanager.FieldDetails{ + "Example": { + ObjectDefinition: resourcemanager.ApiObjectDefinition{ + Type: resourcemanager.StringApiObjectDefinitionType, + }, + Required: true, + }, + }, + }, + } + actual, err := differ{}.changesForModels("Computer", "2020-01-01", "Example", initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + expected := make([]changes.Change, 0) + assertChanges(t, expected, *actual) + assertContainsNoBreakingChanges(t, *actual) +} + func TestDiff_ModelAdded(t *testing.T) { initial := map[string]resourcemanager.ModelDetails{ "First": {}, diff --git a/tools/data-api-differ/internal/differ/differ_operations_test.go b/tools/data-api-differ/internal/differ/differ_operations_test.go index 5c3476da8c8..3cf7f3ccc2c 100644 --- a/tools/data-api-differ/internal/differ/differ_operations_test.go +++ b/tools/data-api-differ/internal/differ/differ_operations_test.go @@ -9,6 +9,27 @@ import ( "github.com/hashicorp/pandora/tools/sdk/resourcemanager" ) +func TestDiff_OperationNoChanges(t *testing.T) { + initial := map[string]resourcemanager.ApiOperation{ + "First": {}, + } + updated := map[string]resourcemanager.ApiOperation{ + "First": {}, + } + ids := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/some/resource/id", + }, + } + actual, err := differ{}.changesForOperations("Computer", "2020-01-01", "Example", initial, updated, ids, ids) + if err != nil { + t.Fatalf(err.Error()) + } + expected := make([]changes.Change, 0) + assertChanges(t, expected, *actual) + assertContainsNoBreakingChanges(t, *actual) +} + func TestDiff_OperationAddedWithResourceId(t *testing.T) { initial := map[string]resourcemanager.ApiOperation{ "First": {}, diff --git a/tools/data-api-differ/internal/differ/differ_resource_ids_test.go b/tools/data-api-differ/internal/differ/differ_resource_ids_test.go index 5dbbdc79326..47a5e453689 100644 --- a/tools/data-api-differ/internal/differ/differ_resource_ids_test.go +++ b/tools/data-api-differ/internal/differ/differ_resource_ids_test.go @@ -8,6 +8,23 @@ import ( "github.com/hashicorp/pandora/tools/sdk/resourcemanager" ) +func TestDiff_ResourceIdNoChanges(t *testing.T) { + oldIds := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/some/resource/id", + }, + } + newIds := map[string]resourcemanager.ResourceIdDefinition{ + "SomeId": { + Id: "/some/resource/id", + }, + } + actual := differ{}.changesForResourceIds("Computer", "2020-01-01", "Example", oldIds, newIds) + expected := make([]changes.Change, 0) + assertChanges(t, expected, actual) + assertContainsNoBreakingChanges(t, actual) +} + func TestDiff_ResourceIdAdded(t *testing.T) { oldIds := make(map[string]resourcemanager.ResourceIdDefinition) newIds := map[string]resourcemanager.ResourceIdDefinition{ diff --git a/tools/data-api-differ/internal/differ/differ_services_test.go b/tools/data-api-differ/internal/differ/differ_services_test.go index d9653023be5..6d00876107c 100644 --- a/tools/data-api-differ/internal/differ/differ_services_test.go +++ b/tools/data-api-differ/internal/differ/differ_services_test.go @@ -7,6 +7,22 @@ import ( "github.com/hashicorp/pandora/tools/data-api-differ/internal/dataapi" ) +func TestDiff_ServiceNoChanges(t *testing.T) { + initial := map[string]dataapi.ServiceData{ + "Computer": {}, + } + updated := map[string]dataapi.ServiceData{ + "Computer": {}, + } + actual, err := differ{}.changesForService("Computer", initial, updated) + if err != nil { + t.Fatalf(err.Error()) + } + expected := make([]changes.Change, 0) + assertChanges(t, expected, *actual) + assertContainsNoBreakingChanges(t, *actual) +} + func TestDiff_ServiceAdded(t *testing.T) { initial := map[string]dataapi.ServiceData{ // intentionally empty diff --git a/tools/data-api-differ/internal/differ/differ_test.go b/tools/data-api-differ/internal/differ/differ_test.go index c86b79d1d4d..33aa32f4f98 100644 --- a/tools/data-api-differ/internal/differ/differ_test.go +++ b/tools/data-api-differ/internal/differ/differ_test.go @@ -14,6 +14,66 @@ import ( // are high-level tests to ensure we're touching the main code paths, rather than necessarily // testing every bit of functionality. +func TestDiff_ResourceManager_NoChanges(t *testing.T) { + initial := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + Generate: true, + ResourceProvider: pointer.To("Microsoft.Computer"), + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Generate: true, + Preview: false, + Resources: map[string]dataapi.ApiResourceData{ + "VirtualMachines": { + Constants: make(map[string]resourcemanager.ConstantDetails), + Models: make(map[string]resourcemanager.ModelDetails), + ResourceIds: make(map[string]resourcemanager.ResourceIdDefinition), + Operations: map[string]resourcemanager.ApiOperation{ + "Example": { + UriSuffix: pointer.To("/doSomething"), + }, + }, + }, + }, + Source: resourcemanager.ApiDefinitionsSourceHandWritten, + }, + }, + }, + }, + } + updated := dataapi.Data{ + ResourceManagerServices: map[string]dataapi.ServiceData{ + "Computer": { + Generate: true, + ResourceProvider: pointer.To("Microsoft.Computer"), + ApiVersions: map[string]dataapi.ApiVersionData{ + "2020-01-01": { + Generate: true, + Preview: false, + Resources: map[string]dataapi.ApiResourceData{ + "VirtualMachines": { + Constants: make(map[string]resourcemanager.ConstantDetails), + Models: make(map[string]resourcemanager.ModelDetails), + ResourceIds: make(map[string]resourcemanager.ResourceIdDefinition), + Operations: map[string]resourcemanager.ApiOperation{ + "Example": { + UriSuffix: pointer.To("/doSomething"), + }, + }, + }, + }, + Source: resourcemanager.ApiDefinitionsSourceHandWritten, + }, + }, + }, + }, + } + expected := make([]changes.Change, 0) + containsBreakingChanges := false + determineAndValidateDiff(t, initial, updated, expected, containsBreakingChanges) +} + func TestDiff_ResourceManager_ServiceAdded(t *testing.T) { initial := dataapi.Data{ ResourceManagerServices: map[string]dataapi.ServiceData{ From bb543e5c627e0e2bfa45b484267869f35dd86bf7 Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Wed, 6 Dec 2023 10:47:14 +0100 Subject: [PATCH 07/20] tools/data-api-definitions: scaffolding out the cli --- tools/data-api-differ/go.mod | 32 +++++++ tools/data-api-differ/go.sum | 87 +++++++++++++++++++ .../commands/detect_breaking_changes.go | 40 +++++++++ .../internal/commands/detect_changes.go | 44 ++++++++++ .../data-api-differ/internal/commands/dev.go | 77 ++++++++++++++++ .../output_new_resource_id_segments.go | 43 +++++++++ tools/data-api-differ/main.go | 45 +++++++++- 7 files changed, 366 insertions(+), 2 deletions(-) create mode 100644 tools/data-api-differ/go.sum create mode 100644 tools/data-api-differ/internal/commands/detect_breaking_changes.go create mode 100644 tools/data-api-differ/internal/commands/detect_changes.go create mode 100644 tools/data-api-differ/internal/commands/dev.go create mode 100644 tools/data-api-differ/internal/commands/output_new_resource_id_segments.go diff --git a/tools/data-api-differ/go.mod b/tools/data-api-differ/go.mod index c70cd778227..d26cd612dd6 100644 --- a/tools/data-api-differ/go.mod +++ b/tools/data-api-differ/go.mod @@ -1,3 +1,35 @@ module github.com/hashicorp/pandora/tools/data-api-differ go 1.21 + +require ( + github.com/hashicorp/go-azure-helpers v0.64.0 + github.com/hashicorp/go-hclog v1.5.0 + github.com/hashicorp/pandora/tools/sdk v0.0.0-00010101000000-000000000000 + github.com/mitchellh/cli v1.1.5 +) + +require ( + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.1.1 // indirect + github.com/Masterminds/sprig/v3 v3.2.1 // indirect + github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 // indirect + github.com/bgentry/speakeasy v0.1.0 // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/google/uuid v1.1.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/huandu/xstrings v1.3.2 // indirect + github.com/imdario/mergo v0.3.11 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/posener/complete v1.1.1 // indirect + github.com/shopspring/decimal v1.2.0 // indirect + github.com/spf13/cast v1.3.1 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/sys v0.13.0 // indirect +) + +replace github.com/hashicorp/pandora/tools/sdk => ../sdk diff --git a/tools/data-api-differ/go.sum b/tools/data-api-differ/go.sum new file mode 100644 index 00000000000..422e3b6d9e1 --- /dev/null +++ b/tools/data-api-differ/go.sum @@ -0,0 +1,87 @@ +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/sprig/v3 v3.2.1 h1:n6EPaDyLSvCEa3frruQvAiHuNp2dhBlMSmkEr+HuzGc= +github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-azure-helpers v0.64.0 h1:JNTt0F7tbApmpmF9hzNq2TN1lduOiXJFkMFoAPVctK8= +github.com/hashicorp/go-azure-helpers v0.64.0/go.mod h1:ELmZ65vzHJNTk6ml4jsPD+xq2gZb7t78D35s+XN02Kk= +github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +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= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= +github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tools/data-api-differ/internal/commands/detect_breaking_changes.go b/tools/data-api-differ/internal/commands/detect_breaking_changes.go new file mode 100644 index 00000000000..2d9a91b0f5f --- /dev/null +++ b/tools/data-api-differ/internal/commands/detect_breaking_changes.go @@ -0,0 +1,40 @@ +package commands + +import ( + "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" + "github.com/mitchellh/cli" +) + +var _ cli.Command = &DetectBreakingChangesCommand{} + +type DetectBreakingChangesCommand struct { + dataApiBinaryPath string + pathToInitialApiDefinitions string + pathToUpdatedApiDefinitions string +} + +func NewDetectBreakingChangesCommand(dataApiBinaryPath, initialPath, updatedPath string) func() (cli.Command, error) { + return func() (cli.Command, error) { + return &DetectBreakingChangesCommand{ + dataApiBinaryPath: dataApiBinaryPath, + pathToInitialApiDefinitions: initialPath, + pathToUpdatedApiDefinitions: updatedPath, + }, nil + } +} + +func (DetectBreakingChangesCommand) Help() string { + return `data-api-differ detect-breaking-changes + +This command detects any breaking changes that exist between the existing and an updated set of API Definitions - output as a report. +` +} + +func (DetectBreakingChangesCommand) Run(args []string) int { + log.Logger.Info("Running `detect-breaking-changes` command..") + return 0 +} + +func (DetectBreakingChangesCommand) Synopsis() string { + return "Retrieves two sets of API Definitions from the Data API and determines if there are any breaking changes" +} diff --git a/tools/data-api-differ/internal/commands/detect_changes.go b/tools/data-api-differ/internal/commands/detect_changes.go new file mode 100644 index 00000000000..c86524b9054 --- /dev/null +++ b/tools/data-api-differ/internal/commands/detect_changes.go @@ -0,0 +1,44 @@ +package commands + +import ( + "log" + + "github.com/mitchellh/cli" +) + +var _ cli.Command = &DetectChangesCommand{} + +type DetectChangesCommand struct { + dataApiBinaryPath string + pathToInitialApiDefinitions string + pathToUpdatedApiDefinitions string +} + +func NewDetectChangesCommand(dataApiBinaryPath, initialPath, updatedPath string) func() (cli.Command, error) { + return func() (cli.Command, error) { + return &DetectChangesCommand{ + dataApiBinaryPath: dataApiBinaryPath, + pathToInitialApiDefinitions: initialPath, + pathToUpdatedApiDefinitions: updatedPath, + }, nil + } +} + +func (DetectChangesCommand) Help() string { + return `data-api-differ detect-changes + +This command detects any changes that exist between the existing and an updated set of API Definitions - output as a report. + +This includes both breaking and non-breaking changes. +` +} + +func (DetectChangesCommand) Run(args []string) int { + //TODO: implement me + log.Printf("TODO: Implement me") + return 0 +} + +func (DetectChangesCommand) Synopsis() string { + return "Detects any changes between the existing and updated set of API Definitions" +} diff --git a/tools/data-api-differ/internal/commands/dev.go b/tools/data-api-differ/internal/commands/dev.go new file mode 100644 index 00000000000..c5b2baa92ed --- /dev/null +++ b/tools/data-api-differ/internal/commands/dev.go @@ -0,0 +1,77 @@ +package commands + +import ( + "fmt" + "reflect" + + "github.com/hashicorp/pandora/tools/data-api-differ/internal/differ" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" + "github.com/mitchellh/cli" +) + +var _ cli.Command = DevCommand{} + +type DevCommand struct { + dataApiBinaryPath string + pathToInitialApiDefinitions string + pathToUpdatedApiDefinitions string +} + +func NewDevCommand(dataApiBinaryPath, initialPath, updatedPath string) func() (cli.Command, error) { + return func() (cli.Command, error) { + return &DevCommand{ + dataApiBinaryPath: dataApiBinaryPath, + pathToInitialApiDefinitions: initialPath, + pathToUpdatedApiDefinitions: updatedPath, + }, nil + } +} + +func (d DevCommand) Help() string { + return "Dev Mode" +} + +func (d DevCommand) Run(args []string) int { + result, err := differ.Diff(d.dataApiBinaryPath, d.pathToInitialApiDefinitions, d.pathToUpdatedApiDefinitions) + if err != nil { + log.Logger.Error("performing diff: %+v", err) + return 1 + } + log.Logger.Info(fmt.Sprintf("Has Breaking Changes: %t", result.ContainsBreakingChanges())) + for _, change := range result.Changes { + log.Logger.Info(fmt.Sprintf("Change Type %q: %+v", reflect.TypeOf(change).Name(), change)) + } + + /* + We want to output three different reports here: + 1. Breaking Changes + 2. Resource ID Segments + 3. Summary of changes + + We also need to know: if a package is being removed - is it currently vendored into `AzureRM`? + ^ this should be determinable by whether the package is vendored into the provider at which + point we should be able to determine from the imports whether this is required or not + + Worth outputting this in GitHub's expandable format? + + ``` + ## Breaking Changes + + * Service `` API version `` + + -- + + ## Service `MyService` + * New API Version: `2020-01-01` + ``` + + In the future it'd also be nice to show the output of the updated Go SDK and Terraform Generator too, by showing + the diff result as a GitHub comment? + + */ + return 0 +} + +func (d DevCommand) Synopsis() string { + return "Dev Mode" +} diff --git a/tools/data-api-differ/internal/commands/output_new_resource_id_segments.go b/tools/data-api-differ/internal/commands/output_new_resource_id_segments.go new file mode 100644 index 00000000000..652e653507b --- /dev/null +++ b/tools/data-api-differ/internal/commands/output_new_resource_id_segments.go @@ -0,0 +1,43 @@ +package commands + +import ( + "log" + + "github.com/mitchellh/cli" +) + +var _ cli.Command = &OutputNewResourceIdSegmentsCommand{} + +type OutputNewResourceIdSegmentsCommand struct { + dataApiBinaryPath string + pathToInitialApiDefinitions string + pathToUpdatedApiDefinitions string +} + +func NewOutputNewResourceIdSegmentsCommand(dataApiBinaryPath, initialPath, updatedPath string) func() (cli.Command, error) { + return func() (cli.Command, error) { + return &OutputNewResourceIdSegmentsCommand{ + dataApiBinaryPath: dataApiBinaryPath, + pathToInitialApiDefinitions: initialPath, + pathToUpdatedApiDefinitions: updatedPath, + }, nil + } +} + +func (OutputNewResourceIdSegmentsCommand) Help() string { + return `data-api-differ output-new-resource-id-segments + +This command detects any new Resource IDs have been added between the existing and updated set of API Definitions and +then outputs a unique, sorted list of Resource ID Segments for review. +` +} + +func (OutputNewResourceIdSegmentsCommand) Run(args []string) int { + //TODO: implement me + log.Printf("TODO: Implement me") + return 0 +} + +func (OutputNewResourceIdSegmentsCommand) Synopsis() string { + return "Determines the new Resource IDs and then outputs a unique, sorted list of Segments for review." +} diff --git a/tools/data-api-differ/main.go b/tools/data-api-differ/main.go index 6d20d360a43..ecdf4b30d07 100644 --- a/tools/data-api-differ/main.go +++ b/tools/data-api-differ/main.go @@ -1,7 +1,48 @@ package main -import "log" +import ( + "os" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/commands" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" + "github.com/mitchellh/cli" +) func main() { - log.Printf("Data API Differ - launched") + opts := hclog.DefaultOptions + if level := os.Getenv("DEBUG"); level != "" { + opts.Level = hclog.Debug + } + log.Logger = hclog.New(opts) + + if err := run(); err != nil { + log.Logger.Error(err.Error()) + os.Exit(1) + } +} + +func run() error { + log.Logger.Info("Data API Differ launched..") + + dataApiBinaryPath := "..." + initialPath := "..." + updatedPath := "..." + + c := cli.NewCLI("data-api-differ", "1.0.0") + c.Args = os.Args[1:] + c.Commands = map[string]cli.CommandFactory{ + "detect-breaking-changes": commands.NewDetectBreakingChangesCommand(dataApiBinaryPath, initialPath, updatedPath), + "detect-changes": commands.NewDetectChangesCommand(dataApiBinaryPath, initialPath, updatedPath), + "dev": commands.NewDevCommand(dataApiBinaryPath, initialPath, updatedPath), + "output-new-resource-id-segments": commands.NewOutputNewResourceIdSegmentsCommand(dataApiBinaryPath, initialPath, updatedPath), + } + + exitStatus, err := c.Run() + if err != nil { + log.Logger.Error(err.Error()) + } + + os.Exit(exitStatus) + return nil } From 1f84d5a09d914d8e101a3e28af3bef538fb99e8f Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Wed, 6 Dec 2023 19:01:51 +0100 Subject: [PATCH 08/20] tools/data-api-differ: updating the Resource ID Changes to include a list of Static Identifiers This allows us to compute the list of Resource ID Segments which are new/updated so we can output a static list --- ...model_discriminated_type_value_changed.go} | 0 .../internal/changes/resource_id_added.go | 4 + .../resource_id_segment_changed_value.go | 3 + .../resource_id_segments_changed_length.go | 10 ++- .../internal/differ/differ_operations.go | 4 +- .../internal/differ/differ_resource_ids.go | 74 ++++++++++++++----- .../differ/differ_resource_ids_test.go | 62 ++++++++++++---- .../internal/differ/differ_test.go | 13 ++++ .../data-api-differ/internal/differ/models.go | 10 +++ 9 files changed, 143 insertions(+), 37 deletions(-) rename tools/data-api-differ/internal/changes/{model_discriminated_value_changed.go => model_discriminated_type_value_changed.go} (100%) diff --git a/tools/data-api-differ/internal/changes/model_discriminated_value_changed.go b/tools/data-api-differ/internal/changes/model_discriminated_type_value_changed.go similarity index 100% rename from tools/data-api-differ/internal/changes/model_discriminated_value_changed.go rename to tools/data-api-differ/internal/changes/model_discriminated_type_value_changed.go diff --git a/tools/data-api-differ/internal/changes/resource_id_added.go b/tools/data-api-differ/internal/changes/resource_id_added.go index da56dd85431..9b4db994bfd 100644 --- a/tools/data-api-differ/internal/changes/resource_id_added.go +++ b/tools/data-api-differ/internal/changes/resource_id_added.go @@ -21,6 +21,10 @@ type ResourceIdAdded struct { // ResourceIdValue specifies the value used for this Resource ID e.g. `/foo/{bar}` ResourceIdValue string + + // StaticIdentifiersInNewValue specifies a unique, sorted list of Static Identifiers (such as Resource + // Provider Name and any Static Values) present within the new/updated value for this Resource ID. + StaticIdentifiersInNewValue []string } // IsBreaking returns whether this Change is considered a Breaking Change. diff --git a/tools/data-api-differ/internal/changes/resource_id_segment_changed_value.go b/tools/data-api-differ/internal/changes/resource_id_segment_changed_value.go index 73eb1edd03a..8bea6f59831 100644 --- a/tools/data-api-differ/internal/changes/resource_id_segment_changed_value.go +++ b/tools/data-api-differ/internal/changes/resource_id_segment_changed_value.go @@ -26,6 +26,9 @@ type ResourceIdSegmentChangedValue struct { // NewValue specifies the new/updated value for this Resource ID Segment. NewValue string + + // StaticIdentifierInNewValue specifies any static identifier present in the updated Resource ID Segment. + StaticIdentifierInNewValue *string } // IsBreaking returns whether this Change is considered a Breaking Change. diff --git a/tools/data-api-differ/internal/changes/resource_id_segments_changed_length.go b/tools/data-api-differ/internal/changes/resource_id_segments_changed_length.go index 5dd6f28ff51..84d43b53c3a 100644 --- a/tools/data-api-differ/internal/changes/resource_id_segments_changed_length.go +++ b/tools/data-api-differ/internal/changes/resource_id_segments_changed_length.go @@ -15,14 +15,18 @@ type ResourceIdSegmentsChangedLength struct { // ResourceName specifies the name of the API Resource which contains this Resource ID. ResourceName string - // ResourceIdName specifies the name of the Resource ID which contains the Segment that has changed. + // ResourceIdName specifies the name of the Resource ID which contains the Segments that has changed. ResourceIdName string - // OldValue specifies the old/existing value for this Resource ID Segment. + // OldValue specifies the old/existing value for this Resource ID. OldValue []string - // NewValue specifies the new/updated value for this Resource ID Segment. + // NewValue specifies the new/updated value for this Resource ID. NewValue []string + + // StaticIdentifiersInNewValue specifies a unique, sorted list of Static Identifiers (such as Resource + // Provider Name and any Static Values) present within the new/updated value for this Resource ID Segment. + StaticIdentifiersInNewValue []string } // IsBreaking returns whether this Change is considered a Breaking Change. diff --git a/tools/data-api-differ/internal/differ/differ_operations.go b/tools/data-api-differ/internal/differ/differ_operations.go index 0743ff581ef..5bb6d40abe1 100644 --- a/tools/data-api-differ/internal/differ/differ_operations.go +++ b/tools/data-api-differ/internal/differ/differ_operations.go @@ -279,7 +279,7 @@ func (d differ) changesForOperationRequestObject(serviceName, apiVersion, apiRes if err != nil { return nil, fmt.Errorf("stringifying the Updated Object Definition: %+v", err) } - if oldStringified != updatedStringified { + if *oldStringified != *updatedStringified { log.Logger.Trace("The updated Operation has a different Request Object") output = append(output, changes.OperationRequestObjectChanged{ ServiceName: serviceName, @@ -401,7 +401,7 @@ func (d differ) changesForOperationResponseObject(serviceName, apiVersion, apiRe if err != nil { return nil, fmt.Errorf("stringifying the Updated Object Definition: %+v", err) } - if oldStringified != updatedStringified { + if *oldStringified != *updatedStringified { log.Logger.Trace("The updated Operation has a different Response Object") output = append(output, changes.OperationResponseObjectChanged{ ServiceName: serviceName, diff --git a/tools/data-api-differ/internal/differ/differ_resource_ids.go b/tools/data-api-differ/internal/differ/differ_resource_ids.go index 65a28724466..627365468be 100644 --- a/tools/data-api-differ/internal/differ/differ_resource_ids.go +++ b/tools/data-api-differ/internal/differ/differ_resource_ids.go @@ -5,6 +5,7 @@ import ( "sort" "strings" + "github.com/hashicorp/go-azure-helpers/lang/pointer" "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" "github.com/hashicorp/pandora/tools/sdk/resourcemanager" @@ -40,12 +41,14 @@ func (d differ) changesForResourceId(serviceName, apiVersion, apiResource, resou } if !isInOld && isInUpdated { log.Logger.Trace(fmt.Sprintf("Resource ID %q was added", resourceIdName)) + staticIdentifiers := d.staticIdentifiersInResourceIdSegments(updatedData.Segments) output = append(output, changes.ResourceIdAdded{ - ServiceName: serviceName, - ApiVersion: apiVersion, - ResourceName: apiResource, - ResourceIdName: resourceIdName, - ResourceIdValue: updatedData.Id, + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ResourceIdName: resourceIdName, + ResourceIdValue: updatedData.Id, + StaticIdentifiersInNewValue: staticIdentifiers, }) return output } @@ -114,13 +117,17 @@ func (d differ) changesForResourceIdSegments(serviceName, apiVersion, apiResourc if len(initial.Segments) != len(updated.Segments) { log.Logger.Trace(fmt.Sprintf("The Resource ID has a different set of segments (old %d segments / new %d segments)", len(initial.Segments), len(updated.Segments))) + + staticIdentifiersInSegments := d.staticIdentifiersInResourceIdSegments(updated.Segments) + output = append(output, changes.ResourceIdSegmentsChangedLength{ - ServiceName: serviceName, - ApiVersion: apiVersion, - ResourceName: apiResource, - ResourceIdName: resourceIdName, - OldValue: oldStringified, - NewValue: updatedStringified, + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ResourceIdName: resourceIdName, + OldValue: oldStringified, + NewValue: updatedStringified, + StaticIdentifiersInNewValue: staticIdentifiersInSegments, }) return output } @@ -130,20 +137,30 @@ func (d differ) changesForResourceIdSegments(serviceName, apiVersion, apiResourc updatedValue := updatedStringified[i] if oldValue != updatedValue { log.Logger.Trace(fmt.Sprintf("Resource ID Segment Index %d differs", i)) + staticIdentifiers := d.staticIdentifiersInResourceIdSegments([]resourcemanager.ResourceIdSegment{ + updated.Segments[i], // only the changed Resource ID Segment + }) + var staticIdentifier *string + if len(staticIdentifiers) > 0 { + staticIdentifier = pointer.To(staticIdentifiers[0]) + } + output = append(output, changes.ResourceIdSegmentChangedValue{ - ServiceName: serviceName, - ApiVersion: apiVersion, - ResourceName: apiResource, - ResourceIdName: resourceIdName, - SegmentIndex: i, - OldValue: oldValue, - NewValue: updatedValue, + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ResourceIdName: resourceIdName, + SegmentIndex: i, + OldValue: oldValue, + NewValue: updatedValue, + StaticIdentifierInNewValue: staticIdentifier, }) } } return output } +// stringifyResourceIdSegments builds up a stringified version of the Resource ID segments for human understanding of the diff func (d differ) stringifyResourceIdSegments(input []resourcemanager.ResourceIdSegment) []string { output := make([]string, 0) @@ -166,6 +183,27 @@ func (d differ) stringifyResourceIdSegments(input []resourcemanager.ResourceIdSe return output } +// staticIdentifiersInResourceIdSegments retrieves a unique, sorted list of the static identifiers within the Resource ID Segments +// This comes from both Static Segments, Resource Provider Segments, +func (d differ) staticIdentifiersInResourceIdSegments(input []resourcemanager.ResourceIdSegment) []string { + segments := make(map[string]struct{}) + + // first pull out a unique list of fixed values from the different segment types + for _, item := range input { + if item.FixedValue != nil { + segments[*item.FixedValue] = struct{}{} + } + } + + // then sort it + output := make([]string, 0) + for k := range segments { + output = append(output, k) + } + sort.Strings(output) + return output +} + // uniqueResourceIdNames returns a unique, sorted list of Resource ID Names from the keys of initial and updated. func (d differ) uniqueResourceIdNames(initial, updated map[string]resourcemanager.ResourceIdDefinition) []string { uniqueNames := make(map[string]struct{}) diff --git a/tools/data-api-differ/internal/differ/differ_resource_ids_test.go b/tools/data-api-differ/internal/differ/differ_resource_ids_test.go index 47a5e453689..ee90026df24 100644 --- a/tools/data-api-differ/internal/differ/differ_resource_ids_test.go +++ b/tools/data-api-differ/internal/differ/differ_resource_ids_test.go @@ -30,6 +30,28 @@ func TestDiff_ResourceIdAdded(t *testing.T) { newIds := map[string]resourcemanager.ResourceIdDefinition{ "SomeId": { Id: "/some/resource/id", + Segments: []resourcemanager.ResourceIdSegment{ + { + // NOTE: it's not strictly representative to have 3 static segments and no user configurable + // segments in a Resource ID - but its worth it in this instance for a simpler/shorter testcase. + Name: "staticSome", + Type: resourcemanager.StaticSegment, + FixedValue: pointer.To("some"), + ExampleValue: "some", + }, + { + Name: "staticResource", + Type: resourcemanager.StaticSegment, + FixedValue: pointer.To("resource"), + ExampleValue: "resource", + }, + { + Name: "staticId", + Type: resourcemanager.StaticSegment, + FixedValue: pointer.To("id"), + ExampleValue: "id", + }, + }, }, } actual := differ{}.changesForResourceIds("Computer", "2020-01-01", "Example", oldIds, newIds) @@ -40,6 +62,12 @@ func TestDiff_ResourceIdAdded(t *testing.T) { ResourceName: "Example", ResourceIdName: "SomeId", ResourceIdValue: "/some/resource/id", + StaticIdentifiersInNewValue: []string{ + // this list is ordered + "id", + "resource", + "some", + }, }, } assertChanges(t, expected, actual) @@ -176,6 +204,9 @@ func TestDiff_ResourceIdSegmentsAdded(t *testing.T) { `Name "staticExample" / Type "Static" / FixedValue "example" / ExampleValue "example"`, `Name "name" / Type "UserSpecified" / ExampleValue "someName"`, }, + StaticIdentifiersInNewValue: []string{ + "example", + }, }, } assertChanges(t, expected, actual) @@ -212,13 +243,14 @@ func TestDiff_ResourceIdSegmentsChangedFixedValue(t *testing.T) { actual := differ{}.changesForResourceIds("Computer", "2020-01-01", "Example", oldIds, newIds) expected := []changes.Change{ changes.ResourceIdSegmentChangedValue{ - ServiceName: "Computer", - ApiVersion: "2020-01-01", - ResourceName: "Example", - ResourceIdName: "SomeId", - SegmentIndex: 0, - OldValue: `Name "staticExample" / Type "Static" / FixedValue "first" / ExampleValue "first"`, - NewValue: `Name "staticExample" / Type "Static" / FixedValue "second" / ExampleValue "second"`, + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ResourceIdName: "SomeId", + SegmentIndex: 0, + OldValue: `Name "staticExample" / Type "Static" / FixedValue "first" / ExampleValue "first"`, + NewValue: `Name "staticExample" / Type "Static" / FixedValue "second" / ExampleValue "second"`, + StaticIdentifierInNewValue: pointer.To("second"), }, } assertChanges(t, expected, actual) @@ -255,13 +287,14 @@ func TestDiff_ResourceIdSegmentsChangedName(t *testing.T) { actual := differ{}.changesForResourceIds("Computer", "2020-01-01", "Example", oldIds, newIds) expected := []changes.Change{ changes.ResourceIdSegmentChangedValue{ - ServiceName: "Computer", - ApiVersion: "2020-01-01", - ResourceName: "Example", - ResourceIdName: "SomeId", - SegmentIndex: 0, - OldValue: `Name "staticExample" / Type "Static" / FixedValue "first" / ExampleValue "first"`, - NewValue: `Name "updatedName" / Type "Static" / FixedValue "first" / ExampleValue "first"`, + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ResourceIdName: "SomeId", + SegmentIndex: 0, + OldValue: `Name "staticExample" / Type "Static" / FixedValue "first" / ExampleValue "first"`, + NewValue: `Name "updatedName" / Type "Static" / FixedValue "first" / ExampleValue "first"`, + StaticIdentifierInNewValue: pointer.To("first"), }, } assertChanges(t, expected, actual) @@ -359,6 +392,7 @@ func TestDiff_ResourceIdSegmentsRemoved(t *testing.T) { NewValue: []string{ `Name "name" / Type "UserSpecified" / ExampleValue "someName"`, }, + StaticIdentifiersInNewValue: []string{}, }, } assertChanges(t, expected, actual) diff --git a/tools/data-api-differ/internal/differ/differ_test.go b/tools/data-api-differ/internal/differ/differ_test.go index 33aa32f4f98..07bcb087277 100644 --- a/tools/data-api-differ/internal/differ/differ_test.go +++ b/tools/data-api-differ/internal/differ/differ_test.go @@ -740,6 +740,16 @@ func TestDiff_ResourceManager_ResourceIdAdded(t *testing.T) { ResourceIds: map[string]resourcemanager.ResourceIdDefinition{ "First": { Id: "/example", + Segments: []resourcemanager.ResourceIdSegment{ + { + // NOTE: it's not strictly representative to have 3 static segments and no user configurable + // segments in a Resource ID - but its worth it in this instance for a simpler/shorter testcase. + Name: "staticExample", + Type: resourcemanager.StaticSegment, + FixedValue: pointer.To("example"), + ExampleValue: "example", + }, + }, }, }, }, @@ -756,6 +766,9 @@ func TestDiff_ResourceManager_ResourceIdAdded(t *testing.T) { ResourceName: "Instances", ResourceIdName: "First", ResourceIdValue: "/example", + StaticIdentifiersInNewValue: []string{ + "example", + }, }, } containsBreakingChanges := false diff --git a/tools/data-api-differ/internal/differ/models.go b/tools/data-api-differ/internal/differ/models.go index 73ba836781e..872fa01dc3e 100644 --- a/tools/data-api-differ/internal/differ/models.go +++ b/tools/data-api-differ/internal/differ/models.go @@ -16,3 +16,13 @@ func (r Result) ContainsBreakingChanges() bool { return false } + +func (r Result) ContainsNonBreakingChanges() bool { + for _, change := range r.Changes { + if !change.IsBreaking() { + return true + } + } + + return false +} From f74475f4d16cd2f4b6f2adaa64b67dbb86da172b Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Wed, 6 Dec 2023 19:03:36 +0100 Subject: [PATCH 09/20] tools/data-api-differ: scaffolding out the three commands This makes use of a "View" concept similar to Terraform Core, the intention here being to allow us the ability to output this as JSON in the future for interoperability with other tooling (such as the `upgrade-go-azure-sdk` tool in `hashicorp/go-azure-sdk`). The Changes and Breaking Changes UIs require further work to clean these up - this is very much an MVP showing all of the available data but in the future I suspect we'll change this to be grouped by Data Type (e.g. Resource Manager/MS Graph) and Service and have the nested information expandable, or something. But this is fine for an MVP. Noteably this will output a LOT of data in the short-term - showing ALL changes that have happened to a Service/API Version/API Resource/etc when it's added - but shows only the top-level removal (e.g. the Removal of the Service, and not each API version) when the Service and all nested items are removed. --- .../commands/detect_breaking_changes.go | 33 +- .../internal/commands/detect_changes.go | 31 +- .../data-api-differ/internal/commands/dev.go | 77 ---- .../output_new_resource_id_segments.go | 43 -- .../commands/output_resource_id_segments.go | 68 ++++ .../internal/views/breaking_changes.go | 65 +++ .../internal/views/breaking_changes_test.go | 19 + .../data-api-differ/internal/views/changes.go | 114 ++++++ .../internal/views/changes_test.go | 19 + .../internal/views/markdown.go | 378 ++++++++++++++++++ .../internal/views/resource_id_segments.go | 89 +++++ .../views/resource_id_segments_test.go | 19 + tools/data-api-differ/internal/views/view.go | 9 + tools/data-api-differ/main.go | 8 +- 14 files changed, 842 insertions(+), 130 deletions(-) delete mode 100644 tools/data-api-differ/internal/commands/dev.go delete mode 100644 tools/data-api-differ/internal/commands/output_new_resource_id_segments.go create mode 100644 tools/data-api-differ/internal/commands/output_resource_id_segments.go create mode 100644 tools/data-api-differ/internal/views/breaking_changes.go create mode 100644 tools/data-api-differ/internal/views/breaking_changes_test.go create mode 100644 tools/data-api-differ/internal/views/changes.go create mode 100644 tools/data-api-differ/internal/views/changes_test.go create mode 100644 tools/data-api-differ/internal/views/markdown.go create mode 100644 tools/data-api-differ/internal/views/resource_id_segments.go create mode 100644 tools/data-api-differ/internal/views/resource_id_segments_test.go create mode 100644 tools/data-api-differ/internal/views/view.go diff --git a/tools/data-api-differ/internal/commands/detect_breaking_changes.go b/tools/data-api-differ/internal/commands/detect_breaking_changes.go index 2d9a91b0f5f..0a2e5727b07 100644 --- a/tools/data-api-differ/internal/commands/detect_breaking_changes.go +++ b/tools/data-api-differ/internal/commands/detect_breaking_changes.go @@ -1,7 +1,13 @@ package commands import ( - "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" + "fmt" + "log" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/differ" + internalLog "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/views" "github.com/mitchellh/cli" ) @@ -11,6 +17,7 @@ type DetectBreakingChangesCommand struct { dataApiBinaryPath string pathToInitialApiDefinitions string pathToUpdatedApiDefinitions string + logger hclog.Logger } func NewDetectBreakingChangesCommand(dataApiBinaryPath, initialPath, updatedPath string) func() (cli.Command, error) { @@ -19,6 +26,8 @@ func NewDetectBreakingChangesCommand(dataApiBinaryPath, initialPath, updatedPath dataApiBinaryPath: dataApiBinaryPath, pathToInitialApiDefinitions: initialPath, pathToUpdatedApiDefinitions: updatedPath, + + logger: internalLog.Logger, }, nil } } @@ -30,8 +39,26 @@ This command detects any breaking changes that exist between the existing and an ` } -func (DetectBreakingChangesCommand) Run(args []string) int { - log.Logger.Info("Running `detect-breaking-changes` command..") +func (c DetectBreakingChangesCommand) Run(args []string) int { + c.logger.Info("Running `detect-breaking-changes` command..") + + c.logger.Debug("Performing diff of the two data sources..") + result, err := differ.Diff(c.dataApiBinaryPath, c.pathToInitialApiDefinitions, c.pathToUpdatedApiDefinitions) + if err != nil { + c.logger.Error("performing diff: %+v", err) + return 1 + } + + // then render the output + c.logger.Debug("Rendering the Breaking Changes..") + view := views.NewBreakingChangesView(result.Changes) + rendered, err := view.RenderMarkdown() + if err != nil { + c.logger.Error(fmt.Sprintf("rendering markdown: %+v", err)) + return 1 + } + log.Print(*rendered) + return 0 } diff --git a/tools/data-api-differ/internal/commands/detect_changes.go b/tools/data-api-differ/internal/commands/detect_changes.go index c86524b9054..4dfd9e7df95 100644 --- a/tools/data-api-differ/internal/commands/detect_changes.go +++ b/tools/data-api-differ/internal/commands/detect_changes.go @@ -1,6 +1,11 @@ package commands import ( + "fmt" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/differ" + internalLog "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/views" "log" "github.com/mitchellh/cli" @@ -12,6 +17,7 @@ type DetectChangesCommand struct { dataApiBinaryPath string pathToInitialApiDefinitions string pathToUpdatedApiDefinitions string + logger hclog.Logger } func NewDetectChangesCommand(dataApiBinaryPath, initialPath, updatedPath string) func() (cli.Command, error) { @@ -20,6 +26,8 @@ func NewDetectChangesCommand(dataApiBinaryPath, initialPath, updatedPath string) dataApiBinaryPath: dataApiBinaryPath, pathToInitialApiDefinitions: initialPath, pathToUpdatedApiDefinitions: updatedPath, + + logger: internalLog.Logger, }, nil } } @@ -33,9 +41,26 @@ This includes both breaking and non-breaking changes. ` } -func (DetectChangesCommand) Run(args []string) int { - //TODO: implement me - log.Printf("TODO: Implement me") +func (c DetectChangesCommand) Run(args []string) int { + c.logger.Info("Running `detect-changes` command..") + + c.logger.Debug("Performing diff of the two data sources..") + result, err := differ.Diff(c.dataApiBinaryPath, c.pathToInitialApiDefinitions, c.pathToUpdatedApiDefinitions) + if err != nil { + c.logger.Error("performing diff: %+v", err) + return 1 + } + + // then render the output + c.logger.Debug("Rendering the Changes..") + view := views.NewChangesView(result.Changes) + rendered, err := view.RenderMarkdown() + if err != nil { + c.logger.Error(fmt.Sprintf("rendering markdown: %+v", err)) + return 1 + } + log.Print(*rendered) + return 0 } diff --git a/tools/data-api-differ/internal/commands/dev.go b/tools/data-api-differ/internal/commands/dev.go deleted file mode 100644 index c5b2baa92ed..00000000000 --- a/tools/data-api-differ/internal/commands/dev.go +++ /dev/null @@ -1,77 +0,0 @@ -package commands - -import ( - "fmt" - "reflect" - - "github.com/hashicorp/pandora/tools/data-api-differ/internal/differ" - "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" - "github.com/mitchellh/cli" -) - -var _ cli.Command = DevCommand{} - -type DevCommand struct { - dataApiBinaryPath string - pathToInitialApiDefinitions string - pathToUpdatedApiDefinitions string -} - -func NewDevCommand(dataApiBinaryPath, initialPath, updatedPath string) func() (cli.Command, error) { - return func() (cli.Command, error) { - return &DevCommand{ - dataApiBinaryPath: dataApiBinaryPath, - pathToInitialApiDefinitions: initialPath, - pathToUpdatedApiDefinitions: updatedPath, - }, nil - } -} - -func (d DevCommand) Help() string { - return "Dev Mode" -} - -func (d DevCommand) Run(args []string) int { - result, err := differ.Diff(d.dataApiBinaryPath, d.pathToInitialApiDefinitions, d.pathToUpdatedApiDefinitions) - if err != nil { - log.Logger.Error("performing diff: %+v", err) - return 1 - } - log.Logger.Info(fmt.Sprintf("Has Breaking Changes: %t", result.ContainsBreakingChanges())) - for _, change := range result.Changes { - log.Logger.Info(fmt.Sprintf("Change Type %q: %+v", reflect.TypeOf(change).Name(), change)) - } - - /* - We want to output three different reports here: - 1. Breaking Changes - 2. Resource ID Segments - 3. Summary of changes - - We also need to know: if a package is being removed - is it currently vendored into `AzureRM`? - ^ this should be determinable by whether the package is vendored into the provider at which - point we should be able to determine from the imports whether this is required or not - - Worth outputting this in GitHub's expandable format? - - ``` - ## Breaking Changes - - * Service `` API version `` - - -- - - ## Service `MyService` - * New API Version: `2020-01-01` - ``` - - In the future it'd also be nice to show the output of the updated Go SDK and Terraform Generator too, by showing - the diff result as a GitHub comment? - - */ - return 0 -} - -func (d DevCommand) Synopsis() string { - return "Dev Mode" -} diff --git a/tools/data-api-differ/internal/commands/output_new_resource_id_segments.go b/tools/data-api-differ/internal/commands/output_new_resource_id_segments.go deleted file mode 100644 index 652e653507b..00000000000 --- a/tools/data-api-differ/internal/commands/output_new_resource_id_segments.go +++ /dev/null @@ -1,43 +0,0 @@ -package commands - -import ( - "log" - - "github.com/mitchellh/cli" -) - -var _ cli.Command = &OutputNewResourceIdSegmentsCommand{} - -type OutputNewResourceIdSegmentsCommand struct { - dataApiBinaryPath string - pathToInitialApiDefinitions string - pathToUpdatedApiDefinitions string -} - -func NewOutputNewResourceIdSegmentsCommand(dataApiBinaryPath, initialPath, updatedPath string) func() (cli.Command, error) { - return func() (cli.Command, error) { - return &OutputNewResourceIdSegmentsCommand{ - dataApiBinaryPath: dataApiBinaryPath, - pathToInitialApiDefinitions: initialPath, - pathToUpdatedApiDefinitions: updatedPath, - }, nil - } -} - -func (OutputNewResourceIdSegmentsCommand) Help() string { - return `data-api-differ output-new-resource-id-segments - -This command detects any new Resource IDs have been added between the existing and updated set of API Definitions and -then outputs a unique, sorted list of Resource ID Segments for review. -` -} - -func (OutputNewResourceIdSegmentsCommand) Run(args []string) int { - //TODO: implement me - log.Printf("TODO: Implement me") - return 0 -} - -func (OutputNewResourceIdSegmentsCommand) Synopsis() string { - return "Determines the new Resource IDs and then outputs a unique, sorted list of Segments for review." -} diff --git a/tools/data-api-differ/internal/commands/output_resource_id_segments.go b/tools/data-api-differ/internal/commands/output_resource_id_segments.go new file mode 100644 index 00000000000..d2eff23a921 --- /dev/null +++ b/tools/data-api-differ/internal/commands/output_resource_id_segments.go @@ -0,0 +1,68 @@ +package commands + +import ( + "fmt" + "log" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/differ" + internalLog "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/views" + "github.com/mitchellh/cli" +) + +var _ cli.Command = &OutputResourceIdSegmentsCommand{} + +type OutputResourceIdSegmentsCommand struct { + dataApiBinaryPath string + pathToInitialApiDefinitions string + pathToUpdatedApiDefinitions string + logger hclog.Logger +} + +func NewOutputResourceIdSegmentsCommand(dataApiBinaryPath, initialPath, updatedPath string) func() (cli.Command, error) { + return func() (cli.Command, error) { + return &OutputResourceIdSegmentsCommand{ + dataApiBinaryPath: dataApiBinaryPath, + pathToInitialApiDefinitions: initialPath, + pathToUpdatedApiDefinitions: updatedPath, + + logger: internalLog.Logger, + }, nil + } +} + +func (OutputResourceIdSegmentsCommand) Help() string { + return `data-api-differ output-resource-id-segments + +This command detects any new Resource IDs that have been added between the existing and updated set of API Definitions +and then outputs a unique, sorted list of any Static Identifiers found within the Resource ID Segments for review. +` +} + +func (c OutputResourceIdSegmentsCommand) Run(args []string) int { + c.logger.Info("Running `output-resource-id-segments` command..") + + c.logger.Debug("Performing diff of the two data sources..") + result, err := differ.Diff(c.dataApiBinaryPath, c.pathToInitialApiDefinitions, c.pathToUpdatedApiDefinitions) + if err != nil { + c.logger.Error("performing diff: %+v", err) + return 1 + } + + // then render the output + c.logger.Debug("Rendering the Changes..") + view := views.NewResourceIdSegmentsView(result.Changes) + rendered, err := view.RenderMarkdown() + if err != nil { + c.logger.Error(fmt.Sprintf("rendering markdown: %+v", err)) + return 1 + } + log.Print(*rendered) + + return 0 +} + +func (OutputResourceIdSegmentsCommand) Synopsis() string { + return "Determines the new Resource IDs and then outputs a unique, sorted list of Static Identifiers found in the Resource ID Segments for review." +} diff --git a/tools/data-api-differ/internal/views/breaking_changes.go b/tools/data-api-differ/internal/views/breaking_changes.go new file mode 100644 index 00000000000..c873f8f0e6e --- /dev/null +++ b/tools/data-api-differ/internal/views/breaking_changes.go @@ -0,0 +1,65 @@ +package views + +import ( + "fmt" + "strings" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" +) + +var _ View = BreakingChangeView{} + +// BreakingChangeView renders the UI when Breaking Changes are detected. +type BreakingChangeView struct { + // breakingChanges is a slice of the breaking changes that should be rendered + breakingChanges []changes.Change +} + +func NewBreakingChangesView(input []changes.Change) BreakingChangeView { + // filter the list of changes to only breaking ones + breakingChanges := make([]changes.Change, 0) + for _, change := range input { + if change.IsBreaking() { + breakingChanges = append(breakingChanges, change) + } + } + + return BreakingChangeView{ + breakingChanges: breakingChanges, + } +} + +// RenderMarkdown renders the Breaking Changes View using Markdown, intended for both display +// in a Terminal and to be output as a GitHub Comment. +func (v BreakingChangeView) RenderMarkdown() (*string, error) { + if len(v.breakingChanges) == 0 { + return pointer.To("No Breaking Changes were found 👍"), nil + } + + diff := make([]string, 0) + for i, change := range v.breakingChanges { + log.Logger.Trace(fmt.Sprintf("Rendering Breaking Change %d", i)) + markdown, err := renderChangeToMarkdown(change) + if err != nil { + return nil, fmt.Errorf("rendering Breaking Change %d: %+v", i, err) + } + diff = append(diff, fmt.Sprintf("* ❌ %s", *markdown)) + } + + output := fmt.Sprintf(` +## Breaking Changes + +🛑 **%d Breaking Changes** were detected: + +--- + +Summary of changes: + +%s + +`, len(v.breakingChanges), strings.Join(diff, "\n")) + output = strings.TrimSpace(output) + return pointer.To(output), nil +} diff --git a/tools/data-api-differ/internal/views/breaking_changes_test.go b/tools/data-api-differ/internal/views/breaking_changes_test.go new file mode 100644 index 00000000000..681f5d81526 --- /dev/null +++ b/tools/data-api-differ/internal/views/breaking_changes_test.go @@ -0,0 +1,19 @@ +package views + +import ( + "testing" + + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/sdk/testhelpers" +) + +// Since we're relying on the output to be clear, it's worth sanity-checking what we're outputting here. + +func TestBreakingChangeView_Markdown_NoChanges(t *testing.T) { + actual, err := NewBreakingChangesView(make([]changes.Change, 0)).RenderMarkdown() + if err != nil { + t.Fatalf(err.Error()) + } + expected := "No Breaking Changes were found 👍" + testhelpers.AssertTemplatedCodeMatches(t, expected, *actual) +} diff --git a/tools/data-api-differ/internal/views/changes.go b/tools/data-api-differ/internal/views/changes.go new file mode 100644 index 00000000000..90c68df9b78 --- /dev/null +++ b/tools/data-api-differ/internal/views/changes.go @@ -0,0 +1,114 @@ +package views + +import ( + "fmt" + "strings" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" +) + +var _ View = ChangesView{} + +// ChangesView renders the UI when any Changes are detected. +type ChangesView struct { + // breakingChanges is a slice of the breaking changes that should be rendered + breakingChanges []changes.Change + + // nonBreakingChanges is a slice of the non-breaking changes that should be rendered + nonBreakingChanges []changes.Change +} + +func NewChangesView(input []changes.Change) ChangesView { + breakingChanges := make([]changes.Change, 0) + nonBreakingChanges := make([]changes.Change, 0) + for _, item := range input { + if item.IsBreaking() { + breakingChanges = append(breakingChanges, item) + continue + } + + nonBreakingChanges = append(nonBreakingChanges, item) + continue + } + + return ChangesView{ + breakingChanges: breakingChanges, + nonBreakingChanges: nonBreakingChanges, + } +} + +// RenderMarkdown renders the Changes View using Markdown, intended for both display +// in a Terminal and to be output as a GitHub Comment. +func (v ChangesView) RenderMarkdown() (*string, error) { + if len(v.breakingChanges) == 0 && len(v.nonBreakingChanges) == 0 { + return pointer.To("No Breaking or Non-Breaking Changes were found 👍"), nil + } + + summaryLines := make([]string, 0) + if len(v.breakingChanges) > 0 { + summaryLines = append(summaryLines, fmt.Sprintf("* 🛑 **%d Breaking Changes** were detected.", len(v.breakingChanges))) + } else { + summaryLines = append(summaryLines, "* 👍 No Breaking Changes were detected.") + } + if len(v.nonBreakingChanges) > 0 { + summaryLines = append(summaryLines, fmt.Sprintf("* 👀 %d Non-Breaking Changes were detected.", len(v.nonBreakingChanges))) + } else { + summaryLines = append(summaryLines, "* 👍 No Non-Breaking Changes were detected.") + } + + sections := []string{ + fmt.Sprintf(` +## Result of Diffing the API Definitions + +%s +`, strings.Join(summaryLines, "\n")), + } + + // TODO: we should look to format this output based on "groupings" of Data Source x Service so this is clearer + // but for an MVP this is sufficient + + breakingChangesLines := make([]string, 0) + for i, change := range v.breakingChanges { + log.Logger.Trace(fmt.Sprintf("Rendering Breaking Change %d", i)) + markdown, err := renderChangeToMarkdown(change) + if err != nil { + return nil, fmt.Errorf("rendering Breaking Change %d: %+v", i, err) + } + breakingChangesLines = append(breakingChangesLines, fmt.Sprintf("* ❌ %s", *markdown)) + } + if len(breakingChangesLines) > 0 { + sections = append(sections, fmt.Sprintf(` +## Breaking Changes + +**%[1]d Breaking Changes** were detected: + +%[2]s +`, len(v.breakingChanges), strings.Join(breakingChangesLines, "\n"))) + } + + nonBreakingChangesSummary := make([]string, 0) + for i, change := range v.nonBreakingChanges { + log.Logger.Trace(fmt.Sprintf("Rendering Non-Breaking Change %d", i)) + markdown, err := renderChangeToMarkdown(change) + if err != nil { + return nil, fmt.Errorf("rendering Breaking Change %d: %+v", i, err) + } + + nonBreakingChangesSummary = append(nonBreakingChangesSummary, fmt.Sprintf("* ✅ %s", *markdown)) + } + if len(nonBreakingChangesSummary) > 0 { + sections = append(sections, fmt.Sprintf(` +## Non-Breaking Changes + +**%d Non-Breaking Changes** were detected: + +%[2]s +`, len(v.nonBreakingChanges), strings.Join(nonBreakingChangesSummary, "\n"))) + } + + output := strings.Join(sections, "\n---\n\n") + output = strings.TrimSpace(output) + return pointer.To(output), nil +} diff --git a/tools/data-api-differ/internal/views/changes_test.go b/tools/data-api-differ/internal/views/changes_test.go new file mode 100644 index 00000000000..f6697df69a8 --- /dev/null +++ b/tools/data-api-differ/internal/views/changes_test.go @@ -0,0 +1,19 @@ +package views + +import ( + "testing" + + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/sdk/testhelpers" +) + +// Since we're relying on the output to be clear, it's worth sanity-checking what we're outputting here. + +func TestChangesView_Markdown_NoChanges(t *testing.T) { + actual, err := NewChangesView(make([]changes.Change, 0)).RenderMarkdown() + if err != nil { + t.Fatalf(err.Error()) + } + expected := "No Breaking or Non-Breaking Changes were found 👍" + testhelpers.AssertTemplatedCodeMatches(t, expected, *actual) +} diff --git a/tools/data-api-differ/internal/views/markdown.go b/tools/data-api-differ/internal/views/markdown.go new file mode 100644 index 00000000000..7359c72fade --- /dev/null +++ b/tools/data-api-differ/internal/views/markdown.go @@ -0,0 +1,378 @@ +package views + +import ( + "fmt" + "reflect" + "sort" + "strings" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" +) + +// renderChangeToMarkdown renders a summary of this change in Markdown +func renderChangeToMarkdown(input changes.Change) (*string, error) { + switch input.(type) { + + // Services + case changes.ServiceAdded: + { + v := input.(changes.ServiceAdded) + line := fmt.Sprintf("**New Service:** `%s`.", v.ServiceName) + return trimSpaceAround(line) + } + case changes.ServiceRemoved: + { + v := input.(changes.ServiceRemoved) + line := fmt.Sprintf("**Removed Service:** `%s`.", v.ServiceName) + return trimSpaceAround(line) + } + + // API Versions + case changes.ApiVersionAdded: + { + v := input.(changes.ApiVersionAdded) + line := fmt.Sprintf("**New API Version:** `%s` in `%s`.", v.ApiVersion, v.ServiceName) + return trimSpaceAround(line) + } + case changes.ApiVersionRemoved: + { + v := input.(changes.ApiVersionRemoved) + line := fmt.Sprintf("**Removed API Version:** `%s` in `%s`.", v.ApiVersion, v.ServiceName) + return trimSpaceAround(line) + } + + // API Resources + case changes.ApiResourceAdded: + { + v := input.(changes.ApiResourceAdded) + line := fmt.Sprintf("**New API Resource:** `%s` in `%s@%s`.", v.ResourceName, v.ServiceName, v.ApiVersion) + return trimSpaceAround(line) + } + case changes.ApiResourceRemoved: + { + v := input.(changes.ApiResourceRemoved) + line := fmt.Sprintf("**Removed API Resource:** `%s` in `%s@%s`.", v.ResourceName, v.ServiceName, v.ApiVersion) + return trimSpaceAround(line) + } + + // Constants + case changes.ConstantAdded: + { + v := input.(changes.ConstantAdded) + keysAndValues := strings.Join(sortConstantKeysAndValues(v.KeysAndValues), ", ") + line := fmt.Sprintf("**New Constant:** `%s` (Type `%s`) in `%s@%s/%s`. Possible Values: %s.", v.ConstantName, v.ConstantType, v.ServiceName, v.ApiVersion, v.ResourceName, keysAndValues) + return trimSpaceAround(line) + } + case changes.ConstantKeyValueAdded: + { + v := input.(changes.ConstantKeyValueAdded) + line := fmt.Sprintf("**New Key/Value for Constant:** `%s` - Key `%s` / Value `%s` in `%s@%s/%s`.", v.ConstantName, v.ConstantKey, v.ConstantValue, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.ConstantKeyValueChanged: + { + v := input.(changes.ConstantKeyValueChanged) + line := fmt.Sprintf("**Updated Value for Constant Key:** Constant `%s` Key `%s` - Old Value `%s` / New Value `%s` in `%s@%s/%s`.", v.ConstantName, v.ConstantKey, v.OldConstantValue, v.NewConstantValue, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.ConstantKeyValueRemoved: + { + v := input.(changes.ConstantKeyValueRemoved) + line := fmt.Sprintf("**Removed Key/Value for Constant:** `%s` - Key `%s` / Value `%s` in `%s@%s/%s`.", v.ConstantName, v.ConstantKey, v.ConstantValue, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.ConstantRemoved: + { + // intentionally not outputting the old values for now, but they're on the object if this is useful + v := input.(changes.ConstantRemoved) + line := fmt.Sprintf("**Removed Constant:** `%s` (Type `%s`) in `%s@%s/%s`.", v.ConstantName, v.ConstantType, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.ConstantTypeChanged: + { + v := input.(changes.ConstantTypeChanged) + line := fmt.Sprintf("**Updated Type for Constant:** `%s` - Old Type `%s` / New Type `%s` in `%s@%s/%s`.", v.ConstantName, v.OldType, v.NewType, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + + // Fields + case changes.FieldAdded: + { + v := input.(changes.FieldAdded) + line := fmt.Sprintf("**Field Added:** `%s` to Model `%s` in `%s@%s/%s`.", v.FieldName, v.ModelName, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + + case changes.FieldIsNowOptional: + { + v := input.(changes.FieldIsNowOptional) + line := fmt.Sprintf("**Field Now Optional:** `%s` in Model `%s` in `%s@%s/%s`.", v.FieldName, v.ModelName, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.FieldIsNowRequired: + { + v := input.(changes.FieldIsNowRequired) + line := fmt.Sprintf("**Field Now Required:** `%s` in Model `%s` in `%s@%s/%s`.", v.FieldName, v.ModelName, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.FieldJsonNameChanged: + { + v := input.(changes.FieldJsonNameChanged) + line := fmt.Sprintf("**Field JsonName Changed:** `%s` (was `%s` now `%s`) in Model `%s` in `%s@%s/%s`.", v.FieldName, v.OldValue, v.NewValue, v.ModelName, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.FieldObjectDefinitionChanged: + { + v := input.(changes.FieldObjectDefinitionChanged) + line := fmt.Sprintf("**Field Object Definition Changed:** `%s` (was `%s` now `%s`) in Model `%s` in `%s@%s/%s`.", v.FieldName, v.OldValue, v.NewValue, v.ModelName, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.FieldRemoved: + { + v := input.(changes.FieldRemoved) + line := fmt.Sprintf("**Field Removed:** `%s` from Model `%s` in `%s@%s/%s`.", v.FieldName, v.ModelName, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + + // Models + case changes.ModelAdded: + { + v := input.(changes.ModelAdded) + line := fmt.Sprintf("**Model Added:** `%s` in `%s@%s/%s`.", v.ModelName, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.ModelDiscriminatedParentTypeAdded: + { + v := input.(changes.ModelDiscriminatedParentTypeAdded) + line := fmt.Sprintf("**Parent Type was Added to Model:** `%s` (now `%s`) in `%s@%s/%s`.", v.ModelName, v.NewParentModelName, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.ModelDiscriminatedParentTypeChanged: + { + v := input.(changes.ModelDiscriminatedParentTypeChanged) + line := fmt.Sprintf("**Parent Type was Changed for Model:** `%s` (was `%s` now `%s`) in `%s@%s/%s`.", v.ModelName, v.OldParentModelName, v.NewParentModelName, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.ModelDiscriminatedParentTypeRemoved: + { + v := input.(changes.ModelDiscriminatedParentTypeRemoved) + line := fmt.Sprintf("**Parent Type was Removed for Model:** `%s` (was `%s`) in `%s@%s/%s`.", v.ModelName, v.OldParentModelName, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.ModelDiscriminatedTypeHintInChanged: + { + v := input.(changes.ModelDiscriminatedTypeHintInChanged) + line := fmt.Sprintf("**Model has an updated value for Discriminated TypeHintIn:** `%s` (was `%s` now `%s`) in `%s@%s/%s`.", v.ModelName, v.OldValue, v.NewValue, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.ModelDiscriminatedTypeValueChanged: + { + v := input.(changes.ModelDiscriminatedTypeValueChanged) + line := fmt.Sprintf("**Model has an updated value for Discriminated Type Value:** `%s` (was `%s` now `%s`) in `%s@%s/%s`.", v.ModelName, v.OldValue, v.NewValue, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.ModelRemoved: + { + v := input.(changes.ModelRemoved) + line := fmt.Sprintf("**Model Removed:** `%s` in `%s@%s/%s`.", v.ModelName, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + + // Operations + case changes.OperationAdded: + { + v := input.(changes.OperationAdded) + line := fmt.Sprintf("**Operation Added:** `%s` (URI `%s`) in `%s@%s/%s`.", v.OperationName, v.Uri, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.OperationContentTypeChanged: + { + v := input.(changes.OperationContentTypeChanged) + line := fmt.Sprintf("**Operation Content Type Changed:** `%s` (was `%s` now `%s`) in `%s@%s/%s`.", v.OperationName, v.OldContentType, v.NewContentType, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.OperationExpectedStatusCodesChanged: + { + v := input.(changes.OperationExpectedStatusCodesChanged) + line := fmt.Sprintf("**Operation Expected Status Codes Changed:** `%s` (was `%+v` now `%+v`) in `%s@%s/%s`.", v.OperationName, v.OldExpectedStatusCodes, v.NewExpectedStatusCodes, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.OperationLongRunningAdded: + { + v := input.(changes.OperationLongRunningAdded) + line := fmt.Sprintf("**Operation Is Now Long Running:** `%s` in `%s@%s/%s`.", v.OperationName, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.OperationLongRunningRemoved: + { + v := input.(changes.OperationLongRunningRemoved) + line := fmt.Sprintf("**Operation Is No Longer Long Running:** `%s` in `%s@%s/%s`.", v.OperationName, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.OperationMethodChanged: + { + v := input.(changes.OperationMethodChanged) + line := fmt.Sprintf("**Operation uses a different HTTP Method:** `%s` (was `%s` now `%s`) in `%s@%s/%s`.", v.OperationName, v.OldValue, v.NewValue, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.OperationPaginationFieldChanged: + { + v := input.(changes.OperationPaginationFieldChanged) + line := fmt.Sprintf("**Operation uses a new Field for Pagination:** `%s` (was `%s` now `%s`) in `%s@%s/%s`.", v.OperationName, v.OldValue, v.NewValue, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.OperationRemoved: + { + v := input.(changes.OperationRemoved) + line := fmt.Sprintf("**Operation Removed:** `%s` (URI `%s`) in `%s@%s/%s`.", v.OperationName, v.Uri, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.OperationRequestObjectAdded: + { + v := input.(changes.OperationRequestObjectAdded) + line := fmt.Sprintf("**Operation Request Object Added:** `%s` (Type `%s`) in `%s@%s/%s`.", v.OperationName, v.NewRequestObject, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.OperationRequestObjectChanged: + { + v := input.(changes.OperationRequestObjectChanged) + line := fmt.Sprintf("**Operation Request Object Changed:** `%s` (was `%s` now `%s`) in `%s@%s/%s`.", v.OperationName, v.OldRequestObject, v.NewRequestObject, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.OperationRequestObjectRemoved: + { + v := input.(changes.OperationRequestObjectRemoved) + line := fmt.Sprintf("**Operation Request Object Removed:** `%s` (Type `%s`) in `%s@%s/%s`.", v.OperationName, v.OldRequestObject, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.OperationResourceIdAdded: + { + v := input.(changes.OperationResourceIdAdded) + line := fmt.Sprintf("**Operation Resource ID Added:** `%s` (now `%s`) in `%s@%s/%s`.", v.OperationName, v.NewResourceIdName, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.OperationResourceIdChanged: + { + v := input.(changes.OperationResourceIdChanged) + line := fmt.Sprintf("**Operation Resource ID Changed:** `%s` (was `%s` now `%s`) in `%s@%s/%s`.", v.OperationName, v.OldResourceIdName, v.NewResourceIdName, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.OperationResourceIdRemoved: + { + v := input.(changes.OperationResourceIdRemoved) + line := fmt.Sprintf("**Operation Resource ID Removed:** `%s` (was `%s`) in `%s@%s/%s`.", v.OperationName, v.OldResourceIdName, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.OperationResourceIdRenamed: + { + v := input.(changes.OperationResourceIdRenamed) + line := fmt.Sprintf("**Operation Resource ID was Renamed:** `%s` (was `%s` now `%s`) in `%s@%s/%s`.", v.OperationName, v.OldResourceIdName, v.NewResourceIdName, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.OperationResponseObjectAdded: + { + v := input.(changes.OperationResponseObjectAdded) + line := fmt.Sprintf("**Operation Response Object Added:** `%s` (Type `%s`) in `%s@%s/%s`.", v.OperationName, v.NewResponseObject, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.OperationResponseObjectChanged: + { + v := input.(changes.OperationResponseObjectChanged) + line := fmt.Sprintf("**Operation Response Object Changed:** `%s` (was `%s` now `%s`) in `%s@%s/%s`.", v.OperationName, v.OldResponseObject, v.NewResponseObject, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.OperationResponseObjectRemoved: + { + v := input.(changes.OperationResponseObjectRemoved) + line := fmt.Sprintf("**Operation Response Object Removed:** `%s` (Type `%s`) in `%s@%s/%s`.", v.OperationName, v.OldResponseObject, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.OperationUriSuffixAdded: + { + v := input.(changes.OperationUriSuffixAdded) + line := fmt.Sprintf("**Operation UriSuffix Added:** `%s` (now `%s`) in `%s@%s/%s`.", v.OperationName, v.NewValue, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.OperationUriSuffixChanged: + { + v := input.(changes.OperationUriSuffixChanged) + line := fmt.Sprintf("**Operation UriSuffix Changed:** `%s` (was `%s` now `%s`) in `%s@%s/%s`.", v.OperationName, v.OldValue, v.NewValue, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + case changes.OperationUriSuffixRemoved: + { + v := input.(changes.OperationUriSuffixRemoved) + line := fmt.Sprintf("**Operation UriSuffix Removed:** `%s` (now `%s`) in `%s@%s/%s`.", v.OperationName, v.OldValue, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + + // Resource IDs + case changes.ResourceIdAdded: + { + v := input.(changes.ResourceIdAdded) + line := fmt.Sprintf("**New Resource ID:** `%s` (ID `%s`) in `%s@%s/%s`.", v.ResourceIdName, v.ResourceIdValue, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + + case changes.ResourceIdCommonIdAdded: + { + v := input.(changes.ResourceIdCommonIdAdded) + line := fmt.Sprintf("**Resource ID is now a Common ID:** `%s` (Alias `%s` / ID `%s`) in `%s@%s/%s`.", v.ResourceIdName, v.CommonAliasName, v.ResourceIdValue, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + + case changes.ResourceIdCommonIdChanged: + { + v := input.(changes.ResourceIdCommonIdChanged) + line := fmt.Sprintf("**Resource ID has changed it's Common ID:** `%s` (was `%s` now `%s`) in `%s@%s/%s`.", v.ResourceIdName, v.OldCommonAliasName, v.NewCommonAliasName, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + + case changes.ResourceIdCommonIdRemoved: + { + v := input.(changes.ResourceIdCommonIdRemoved) + line := fmt.Sprintf("**Resource ID is no longer a Common ID:** `%s` (Alias `%s` / ID `%s`) in `%s@%s/%s`.", v.ResourceIdName, v.CommonAliasName, v.ResourceIdValue, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + + case changes.ResourceIdRemoved: + { + v := input.(changes.ResourceIdRemoved) + line := fmt.Sprintf("**Removed Resource ID:** `%s` (ID `%s`) in `%s@%s/%s`.", v.ResourceIdName, v.ResourceIdValue, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + + case changes.ResourceIdSegmentChangedValue: + { + v := input.(changes.ResourceIdSegmentChangedValue) + line := fmt.Sprintf("**Resource ID Segment (Index %d) Changed Value:** `%s` (was `%s` now `%s`) in `%s@%s/%s`.", v.SegmentIndex, v.ResourceIdName, v.OldValue, v.NewValue, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + + case changes.ResourceIdSegmentsChangedLength: + { + v := input.(changes.ResourceIdSegmentsChangedLength) + line := fmt.Sprintf("**Resource ID Segments Changed:** `%s` (was `%+v` now `%+v`) in `%s@%s/%s`.", v.ResourceIdName, v.OldValue, v.NewValue, v.ServiceName, v.ApiVersion, v.ResourceName) + return trimSpaceAround(line) + } + } + + return nil, fmt.Errorf("internal-error: unimplemented change type %q", reflect.TypeOf(input).Name()) +} + +func sortConstantKeysAndValues(input map[string]string) []string { + output := make([]string, 0) + for k, v := range input { + item := fmt.Sprintf("`%s: %s`", k, v) + output = append(output, item) + } + sort.Strings(output) + return output +} + +func trimSpaceAround(input string) (*string, error) { + output := input + output = strings.TrimSpace(output) + return pointer.To(output), nil +} diff --git a/tools/data-api-differ/internal/views/resource_id_segments.go b/tools/data-api-differ/internal/views/resource_id_segments.go new file mode 100644 index 00000000000..9a8f4f97e4e --- /dev/null +++ b/tools/data-api-differ/internal/views/resource_id_segments.go @@ -0,0 +1,89 @@ +package views + +import ( + "fmt" + "sort" + "strings" + + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" +) + +var _ View = ResourceIdSegmentsView{} + +type ResourceIdSegmentsView struct { + // staticIdentifierSegments is a unique, sorted list of the Static Identifiers present within + // the detected set of Changes. + staticIdentifierSegments []string +} + +func NewResourceIdSegmentsView(input []changes.Change) ResourceIdSegmentsView { + // First we need to identify any changes which contain Resource ID Segments that contain identifiers + // As such let's build a unique list of Static Identifiers from the select Types of Changes that we need. + uniqueIdentifiers := make(map[string]struct{}) + for _, change := range input { + // When there's an entirely new Resource ID it can contain Static Identifiers + if v, ok := change.(changes.ResourceIdAdded); ok { + for _, segment := range v.StaticIdentifiersInNewValue { + uniqueIdentifiers[segment] = struct{}{} + } + } + + // When the Resource ID Segment has changed value it can become a Static Identifier + if v, ok := change.(changes.ResourceIdSegmentChangedValue); ok { + if v.StaticIdentifierInNewValue != nil { + uniqueIdentifiers[*v.StaticIdentifierInNewValue] = struct{}{} + } + } + + // When the Resource ID Segments change there can be new Static Identifiers present + if v, ok := change.(changes.ResourceIdSegmentsChangedLength); ok { + for _, segment := range v.StaticIdentifiersInNewValue { + uniqueIdentifiers[segment] = struct{}{} + } + } + } + + staticIdentifierSegments := make([]string, 0) + for key := range uniqueIdentifiers { + staticIdentifierSegments = append(staticIdentifierSegments, key) + } + sort.Strings(staticIdentifierSegments) + return ResourceIdSegmentsView{ + staticIdentifierSegments: staticIdentifierSegments, + } +} + +func (v ResourceIdSegmentsView) RenderMarkdown() (*string, error) { + if len(v.staticIdentifierSegments) == 0 { + output := ` +## New Resource ID Segments containing Static Identifiers + +No new Resource ID Segments containing Static Identifiers were identified in the set of changes 🤙. +` + return trimSpaceAround(output) + } + + lines := make([]string, 0) + for _, item := range v.staticIdentifierSegments { + lines = append(lines, fmt.Sprintf("* `%s`", item)) + } + output := fmt.Sprintf(` +## New Resource ID Segments containing Static Identifiers + +The following new Static Identifiers were detected from the set of changes (new/updated Resource IDs). + +> Note: Resource ID segments should **always** be 'camelCased' and not 'TitleCased', 'lowercased' or 'kebab-cased'. + +Please review the following list of Static Identifiers: + +--- + +%s + +--- + +> Note: Resource ID segments should **always** be 'camelCased' and not 'TitleCased', 'lowercased' or 'kebab-cased'. +`, strings.Join(lines, "\n")) + //TODO: add a "see the link for how to fix this" to the comment above when the associated documentation is available + return trimSpaceAround(output) +} diff --git a/tools/data-api-differ/internal/views/resource_id_segments_test.go b/tools/data-api-differ/internal/views/resource_id_segments_test.go new file mode 100644 index 00000000000..8a78a2f57a1 --- /dev/null +++ b/tools/data-api-differ/internal/views/resource_id_segments_test.go @@ -0,0 +1,19 @@ +package views + +import ( + "testing" + + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "github.com/hashicorp/pandora/tools/sdk/testhelpers" +) + +// Since we're relying on the output to be clear, it's worth sanity-checking what we're outputting here. + +func TestResourceIdSegmentsView_Markdown_NoChanges(t *testing.T) { + actual, err := NewResourceIdSegmentsView(make([]changes.Change, 0)).RenderMarkdown() + if err != nil { + t.Fatalf(err.Error()) + } + expected := "No New Resource ID Segments were found 👍" + testhelpers.AssertTemplatedCodeMatches(t, expected, *actual) +} diff --git a/tools/data-api-differ/internal/views/view.go b/tools/data-api-differ/internal/views/view.go new file mode 100644 index 00000000000..8c87d8df4a7 --- /dev/null +++ b/tools/data-api-differ/internal/views/view.go @@ -0,0 +1,9 @@ +package views + +type View interface { + // TODO: it'd likely be useful to have this information available as JSON in the future, hence the interface + + // RenderMarkdown renders the View using Markdown, intended for both display + // in a Terminal and to be output as a GitHub Comment. + RenderMarkdown() (*string, error) +} diff --git a/tools/data-api-differ/main.go b/tools/data-api-differ/main.go index ecdf4b30d07..ab8065eeb33 100644 --- a/tools/data-api-differ/main.go +++ b/tools/data-api-differ/main.go @@ -11,6 +11,7 @@ import ( func main() { opts := hclog.DefaultOptions + opts.Level = hclog.NoLevel if level := os.Getenv("DEBUG"); level != "" { opts.Level = hclog.Debug } @@ -32,10 +33,9 @@ func run() error { c := cli.NewCLI("data-api-differ", "1.0.0") c.Args = os.Args[1:] c.Commands = map[string]cli.CommandFactory{ - "detect-breaking-changes": commands.NewDetectBreakingChangesCommand(dataApiBinaryPath, initialPath, updatedPath), - "detect-changes": commands.NewDetectChangesCommand(dataApiBinaryPath, initialPath, updatedPath), - "dev": commands.NewDevCommand(dataApiBinaryPath, initialPath, updatedPath), - "output-new-resource-id-segments": commands.NewOutputNewResourceIdSegmentsCommand(dataApiBinaryPath, initialPath, updatedPath), + "detect-breaking-changes": commands.NewDetectBreakingChangesCommand(dataApiBinaryPath, initialPath, updatedPath), + "detect-changes": commands.NewDetectChangesCommand(dataApiBinaryPath, initialPath, updatedPath), + "output-resource-id-segments": commands.NewOutputResourceIdSegmentsCommand(dataApiBinaryPath, initialPath, updatedPath), } exitStatus, err := c.Run() From 4c0b50159b39fe23e91f60a93f7c87f84c879712 Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Wed, 6 Dec 2023 19:36:51 +0100 Subject: [PATCH 10/20] tools/data-api-differ: adding tests covering the common views These might seem excessive, but since we're relying on this output this is worth testing. --- .../internal/views/breaking_changes.go | 12 +- .../internal/views/breaking_changes_test.go | 85 +++++++++- .../data-api-differ/internal/views/changes.go | 12 +- .../internal/views/changes_test.go | 115 ++++++++++++- .../internal/views/helpers_test.go | 10 ++ .../internal/views/resource_id_segments.go | 1 + .../views/resource_id_segments_test.go | 151 +++++++++++++++++- tools/data-api-differ/internal/views/view.go | 2 - 8 files changed, 373 insertions(+), 15 deletions(-) create mode 100644 tools/data-api-differ/internal/views/helpers_test.go diff --git a/tools/data-api-differ/internal/views/breaking_changes.go b/tools/data-api-differ/internal/views/breaking_changes.go index c873f8f0e6e..9efc1de2344 100644 --- a/tools/data-api-differ/internal/views/breaking_changes.go +++ b/tools/data-api-differ/internal/views/breaking_changes.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "github.com/hashicorp/go-azure-helpers/lang/pointer" "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" ) @@ -35,7 +34,11 @@ func NewBreakingChangesView(input []changes.Change) BreakingChangeView { // in a Terminal and to be output as a GitHub Comment. func (v BreakingChangeView) RenderMarkdown() (*string, error) { if len(v.breakingChanges) == 0 { - return pointer.To("No Breaking Changes were found 👍"), nil + return trimSpaceAround(` +## Breaking Changes + +No Breaking Changes were found 👍 +`) } diff := make([]string, 0) @@ -51,7 +54,7 @@ func (v BreakingChangeView) RenderMarkdown() (*string, error) { output := fmt.Sprintf(` ## Breaking Changes -🛑 **%d Breaking Changes** were detected: +🛑 **%d Breaking Changes** were detected. --- @@ -60,6 +63,5 @@ Summary of changes: %s `, len(v.breakingChanges), strings.Join(diff, "\n")) - output = strings.TrimSpace(output) - return pointer.To(output), nil + return trimSpaceAround(output) } diff --git a/tools/data-api-differ/internal/views/breaking_changes_test.go b/tools/data-api-differ/internal/views/breaking_changes_test.go index 681f5d81526..fe02be35d6c 100644 --- a/tools/data-api-differ/internal/views/breaking_changes_test.go +++ b/tools/data-api-differ/internal/views/breaking_changes_test.go @@ -1,6 +1,7 @@ package views import ( + "strings" "testing" "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" @@ -14,6 +15,88 @@ func TestBreakingChangeView_Markdown_NoChanges(t *testing.T) { if err != nil { t.Fatalf(err.Error()) } - expected := "No Breaking Changes were found 👍" + expected := "## Breaking Changes\n\nNo Breaking Changes were found 👍" + testhelpers.AssertTemplatedCodeMatches(t, expected, *actual) +} + +func TestBreakingChangeView_Markdown_WithOnlyBreakingChanges(t *testing.T) { + diff := []changes.Change{ + changes.ServiceRemoved{ + ServiceName: "First", + }, + changes.ServiceRemoved{ + ServiceName: "Second", + }, + changes.ServiceRemoved{ + ServiceName: "Third", + }, + } + actual, err := NewBreakingChangesView(diff).RenderMarkdown() + if err != nil { + t.Fatalf(err.Error()) + } + expected := strings.ReplaceAll(` + ## Breaking Changes + +🛑 **3 Breaking Changes** were detected. + +--- + +Summary of changes: + +* ❌ **Removed Service:** 'First'. +* ❌ **Removed Service:** 'Second'. +* ❌ **Removed Service:** 'Third'. +`, "'", "`") + testhelpers.AssertTemplatedCodeMatches(t, expected, *actual) +} + +func TestBreakingChangeView_Markdown_WithOnlyNonBreakingChanges(t *testing.T) { + diff := []changes.Change{ + // Non-breaking changes should be filtered out + changes.ServiceAdded{ + ServiceName: "Example", + }, + } + actual, err := NewBreakingChangesView(diff).RenderMarkdown() + if err != nil { + t.Fatalf(err.Error()) + } + expected := ` +## Breaking Changes + +No Breaking Changes were found 👍 +` + testhelpers.AssertTemplatedCodeMatches(t, expected, *actual) +} + +func TestBreakingChangeView_Markdown_WithBreakingAndNonBreakingChanges(t *testing.T) { + diff := []changes.Change{ + changes.ServiceRemoved{ + ServiceName: "First", + }, + changes.ServiceAdded{ // the non-breaking change should be filtered out + ServiceName: "Second", + }, + changes.ServiceRemoved{ + ServiceName: "Third", + }, + } + actual, err := NewBreakingChangesView(diff).RenderMarkdown() + if err != nil { + t.Fatalf(err.Error()) + } + expected := strings.ReplaceAll(` + ## Breaking Changes + +🛑 **2 Breaking Changes** were detected. + +--- + +Summary of changes: + +* ❌ **Removed Service:** 'First'. +* ❌ **Removed Service:** 'Third'. +`, "'", "`") testhelpers.AssertTemplatedCodeMatches(t, expected, *actual) } diff --git a/tools/data-api-differ/internal/views/changes.go b/tools/data-api-differ/internal/views/changes.go index 90c68df9b78..9b1adada658 100644 --- a/tools/data-api-differ/internal/views/changes.go +++ b/tools/data-api-differ/internal/views/changes.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "github.com/hashicorp/go-azure-helpers/lang/pointer" "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" ) @@ -43,7 +42,11 @@ func NewChangesView(input []changes.Change) ChangesView { // in a Terminal and to be output as a GitHub Comment. func (v ChangesView) RenderMarkdown() (*string, error) { if len(v.breakingChanges) == 0 && len(v.nonBreakingChanges) == 0 { - return pointer.To("No Breaking or Non-Breaking Changes were found 👍"), nil + return trimSpaceAround(` +## Summary of Changes + +No Breaking or Non-Breaking Changes were found 👍 +`) } summaryLines := make([]string, 0) @@ -60,7 +63,7 @@ func (v ChangesView) RenderMarkdown() (*string, error) { sections := []string{ fmt.Sprintf(` -## Result of Diffing the API Definitions +## Summary of Changes %s `, strings.Join(summaryLines, "\n")), @@ -109,6 +112,5 @@ func (v ChangesView) RenderMarkdown() (*string, error) { } output := strings.Join(sections, "\n---\n\n") - output = strings.TrimSpace(output) - return pointer.To(output), nil + return trimSpaceAround(output) } diff --git a/tools/data-api-differ/internal/views/changes_test.go b/tools/data-api-differ/internal/views/changes_test.go index f6697df69a8..d89f8a5e5ab 100644 --- a/tools/data-api-differ/internal/views/changes_test.go +++ b/tools/data-api-differ/internal/views/changes_test.go @@ -1,6 +1,7 @@ package views import ( + "strings" "testing" "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" @@ -14,6 +15,118 @@ func TestChangesView_Markdown_NoChanges(t *testing.T) { if err != nil { t.Fatalf(err.Error()) } - expected := "No Breaking or Non-Breaking Changes were found 👍" + expected := "## Summary of Changes\n\nNo Breaking or Non-Breaking Changes were found 👍" + testhelpers.AssertTemplatedCodeMatches(t, expected, *actual) +} + +func TestChangesView_Markdown_WithOnlyBreakingChanges(t *testing.T) { + diff := []changes.Change{ + changes.ServiceRemoved{ + ServiceName: "First", + }, + changes.ServiceRemoved{ + ServiceName: "Second", + }, + changes.ServiceRemoved{ + ServiceName: "Third", + }, + } + actual, err := NewChangesView(diff).RenderMarkdown() + if err != nil { + t.Fatalf(err.Error()) + } + expected := strings.ReplaceAll(` + ## Summary of Changes + +* 🛑 **3 Breaking Changes** were detected. +* 👍 No Non-Breaking Changes were detected. + +--- + +## Breaking Changes + +**3 Breaking Changes** were detected: + +* ❌ **Removed Service:** 'First'. +* ❌ **Removed Service:** 'Second'. +* ❌ **Removed Service:** 'Third'. +`, "'", "`") + testhelpers.AssertTemplatedCodeMatches(t, expected, *actual) +} + +func TestChangesView_Markdown_WithOnlyNonBreakingChanges(t *testing.T) { + diff := []changes.Change{ + changes.ServiceAdded{ + ServiceName: "First", + }, + changes.ServiceAdded{ + ServiceName: "Second", + }, + changes.ServiceAdded{ + ServiceName: "Third", + }, + } + actual, err := NewChangesView(diff).RenderMarkdown() + if err != nil { + t.Fatalf(err.Error()) + } + expected := strings.ReplaceAll(` + ## Summary of Changes + +* 👍 No Breaking Changes were detected. +* 👀 3 Non-Breaking Changes were detected. + +--- + +## Non-Breaking Changes + +**3 Non-Breaking Changes** were detected: + +* ✅ **New Service:** 'First'. +* ✅ **New Service:** 'Second'. +* ✅ **New Service:** 'Third'. +`, "'", "`") + testhelpers.AssertTemplatedCodeMatches(t, expected, *actual) +} + +func TestChangesView_Markdown_WithBreakingAndNonBreakingChanges(t *testing.T) { + diff := []changes.Change{ + changes.ServiceAdded{ + ServiceName: "First", + }, + changes.ServiceRemoved{ + ServiceName: "Second", + }, + changes.ServiceAdded{ + ServiceName: "Third", + }, + } + actual, err := NewChangesView(diff).RenderMarkdown() + if err != nil { + t.Fatalf(err.Error()) + } + expected := strings.ReplaceAll(` + ## Summary of Changes + +* 🛑 **1 Breaking Changes** were detected. +* 👀 2 Non-Breaking Changes were detected. + +--- + +## Breaking Changes + +**1 Breaking Changes** were detected: + +* ❌ **Removed Service:** 'Second'. + +--- + +## Non-Breaking Changes + +**2 Non-Breaking Changes** were detected: + +* ✅ **New Service:** 'First'. +* ✅ **New Service:** 'Third'. +`, "'", "`") testhelpers.AssertTemplatedCodeMatches(t, expected, *actual) } diff --git a/tools/data-api-differ/internal/views/helpers_test.go b/tools/data-api-differ/internal/views/helpers_test.go new file mode 100644 index 00000000000..1fe92f14d9f --- /dev/null +++ b/tools/data-api-differ/internal/views/helpers_test.go @@ -0,0 +1,10 @@ +package views + +import ( + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" +) + +func init() { + log.Logger = hclog.Default() +} diff --git a/tools/data-api-differ/internal/views/resource_id_segments.go b/tools/data-api-differ/internal/views/resource_id_segments.go index 9a8f4f97e4e..ab46f2bfbcc 100644 --- a/tools/data-api-differ/internal/views/resource_id_segments.go +++ b/tools/data-api-differ/internal/views/resource_id_segments.go @@ -84,6 +84,7 @@ Please review the following list of Static Identifiers: > Note: Resource ID segments should **always** be 'camelCased' and not 'TitleCased', 'lowercased' or 'kebab-cased'. `, strings.Join(lines, "\n")) + output = strings.ReplaceAll(output, "'", "`") //TODO: add a "see the link for how to fix this" to the comment above when the associated documentation is available return trimSpaceAround(output) } diff --git a/tools/data-api-differ/internal/views/resource_id_segments_test.go b/tools/data-api-differ/internal/views/resource_id_segments_test.go index 8a78a2f57a1..b777a9a6cae 100644 --- a/tools/data-api-differ/internal/views/resource_id_segments_test.go +++ b/tools/data-api-differ/internal/views/resource_id_segments_test.go @@ -1,6 +1,8 @@ package views import ( + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "strings" "testing" "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" @@ -14,6 +16,153 @@ func TestResourceIdSegmentsView_Markdown_NoChanges(t *testing.T) { if err != nil { t.Fatalf(err.Error()) } - expected := "No New Resource ID Segments were found 👍" + expected := ` +## New Resource ID Segments containing Static Identifiers + +No new Resource ID Segments containing Static Identifiers were identified in the set of changes 🤙. +` + testhelpers.AssertTemplatedCodeMatches(t, expected, *actual) +} + +func TestResourceIdSegmentsView_Markdown_WithChanges(t *testing.T) { + // if the changes aren't adding a Resource ID Segment with a Static Identifier it should be ignored. + diff := []changes.Change{ + changes.ResourceIdAdded{ + ResourceIdValue: "/subscriptions/{subscriptionId}/machineTypes/physical/machines/{machineName}", + StaticIdentifiersInNewValue: []string{ + "subscriptions", + "machineTypes", + "physical", + "machines", + }, + }, + changes.ResourceIdAdded{ + // here to test uniqueness + ResourceIdValue: "/subscriptions/{subscriptionId}", + StaticIdentifiersInNewValue: []string{ + "subscriptions", + }, + }, + changes.ResourceIdSegmentChangedValue{ + StaticIdentifierInNewValue: pointer.To("customLocations"), + }, + } + actual, err := NewResourceIdSegmentsView(diff).RenderMarkdown() + if err != nil { + t.Fatalf(err.Error()) + } + expected := strings.ReplaceAll(` +## New Resource ID Segments containing Static Identifiers + +The following new Static Identifiers were detected from the set of changes (new/updated Resource IDs). + +> Note: Resource ID segments should **always** be 'camelCased' and not 'TitleCased', 'lowercased' or 'kebab-cased'. + +Please review the following list of Static Identifiers: + +--- + +* 'customLocations' +* 'machineTypes' +* 'machines' +* 'physical' +* 'subscriptions' + +--- + +> Note: Resource ID segments should **always** be 'camelCased' and not 'TitleCased', 'lowercased' or 'kebab-cased'. +`, "'", "`") + testhelpers.AssertTemplatedCodeMatches(t, expected, *actual) +} + +func TestResourceIdSegmentsView_Markdown_WithIrrelevantChanges(t *testing.T) { + // if the changes aren't adding a Resource ID Segment with a Static Identifier it should be ignored. + diff := []changes.Change{ + changes.ConstantAdded{ + ServiceName: "Compute", + ApiVersion: "2020-01-01", + ResourceName: "VirtualMachines", + ConstantName: "Performance", + ConstantType: "String", + KeysAndValues: map[string]string{ + "Slow": "Slow", + "Fast": "Fast", + "VaVaVoom": "ZoomZoom", + }, + }, + } + actual, err := NewResourceIdSegmentsView(diff).RenderMarkdown() + if err != nil { + t.Fatalf(err.Error()) + } + expected := ` +## New Resource ID Segments containing Static Identifiers + +No new Resource ID Segments containing Static Identifiers were identified in the set of changes 🤙. +` + testhelpers.AssertTemplatedCodeMatches(t, expected, *actual) +} + +func TestResourceIdSegmentsView_Markdown_WithRelevantAndIrrelevantChanges(t *testing.T) { + // Only the relevant changes (i.e. those adding a Resource ID Segment with a Static Identifier) + // should be output + diff := []changes.Change{ + changes.ResourceIdAdded{ + ResourceIdValue: "/subscriptions/{subscriptionId}/machineTypes/physical/machines/{machineName}", + StaticIdentifiersInNewValue: []string{ + "subscriptions", + "machineTypes", + "physical", + "machines", + }, + }, + changes.ResourceIdAdded{ + // here to test uniqueness + ResourceIdValue: "/subscriptions/{subscriptionId}", + StaticIdentifiersInNewValue: []string{ + "subscriptions", + }, + }, + changes.ConstantAdded{ + ServiceName: "Compute", + ApiVersion: "2020-01-01", + ResourceName: "VirtualMachines", + ConstantName: "Performance", + ConstantType: "String", + KeysAndValues: map[string]string{ + "Slow": "Slow", + "Fast": "Fast", + "VaVaVoom": "ZoomZoom", + }, + }, + changes.ResourceIdSegmentChangedValue{ + StaticIdentifierInNewValue: pointer.To("customLocations"), + }, + } + actual, err := NewResourceIdSegmentsView(diff).RenderMarkdown() + if err != nil { + t.Fatalf(err.Error()) + } + expected := strings.ReplaceAll(` +## New Resource ID Segments containing Static Identifiers + +The following new Static Identifiers were detected from the set of changes (new/updated Resource IDs). + +> Note: Resource ID segments should **always** be 'camelCased' and not 'TitleCased', 'lowercased' or 'kebab-cased'. + +Please review the following list of Static Identifiers: + +--- + +* 'customLocations' +* 'machineTypes' +* 'machines' +* 'physical' +* 'subscriptions' + +--- + +> Note: Resource ID segments should **always** be 'camelCased' and not 'TitleCased', 'lowercased' or 'kebab-cased'. +`, "'", "`") testhelpers.AssertTemplatedCodeMatches(t, expected, *actual) } diff --git a/tools/data-api-differ/internal/views/view.go b/tools/data-api-differ/internal/views/view.go index 8c87d8df4a7..0d41dfc1fef 100644 --- a/tools/data-api-differ/internal/views/view.go +++ b/tools/data-api-differ/internal/views/view.go @@ -1,8 +1,6 @@ package views type View interface { - // TODO: it'd likely be useful to have this information available as JSON in the future, hence the interface - // RenderMarkdown renders the View using Markdown, intended for both display // in a Terminal and to be output as a GitHub Comment. RenderMarkdown() (*string, error) From 609f157be3b560e622183408044a4fa3318925ce Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Thu, 7 Dec 2023 12:16:41 +0100 Subject: [PATCH 11/20] tools/data-api: adding logging when the Data API launches This helps determine when the Data API has launched, and on which port. --- tools/data-api/internal/commands/serve.go | 1 + tools/data-api/main.go | 2 ++ 2 files changed, 3 insertions(+) diff --git a/tools/data-api/internal/commands/serve.go b/tools/data-api/internal/commands/serve.go index 02807660b3c..5e1b4ee14aa 100644 --- a/tools/data-api/internal/commands/serve.go +++ b/tools/data-api/internal/commands/serve.go @@ -80,6 +80,7 @@ func (c ServeCommand) Run(args []string) int { r := chi.NewRouter() r.Use(middleware.Logger) r.Route("/", endpoints.Router(dataDirectory, serviceNames)) + c.Log.Info(fmt.Sprintf("Data API launched at http://localhost:%d", port)) http.ListenAndServe(fmt.Sprintf(":%d", port), r) return 0 } diff --git a/tools/data-api/main.go b/tools/data-api/main.go index c5559a80aaf..569e77a4e1d 100644 --- a/tools/data-api/main.go +++ b/tools/data-api/main.go @@ -14,6 +14,8 @@ func main() { logger := hclog.New(hclog.DefaultOptions) logger.SetLevel(hclog.Trace) + logger.Info("Data API launched") + c := cli.NewCLI("data-api", "1.0.0") c.Args = os.Args[1:] c.Commands = map[string]cli.CommandFactory{ From 1cdda0666077a805b536c4603052110c078a44ee Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Thu, 7 Dec 2023 12:17:49 +0100 Subject: [PATCH 12/20] tools/data-api-differ: support for specifying the arguments via the CLI In the event that `data-api-binary-path` is unspecified, we now look for this on the PATH which is helpful when running in automation. --- tools/data-api-differ/internal/README.md | 219 ++++++++++++++++++ .../data-api-differ/internal/commands/args.go | 92 ++++++++ .../commands/detect_breaking_changes.go | 31 ++- .../internal/commands/detect_changes.go | 31 ++- .../commands/output_resource_id_segments.go | 31 ++- tools/data-api-differ/main.go | 23 +- 6 files changed, 381 insertions(+), 46 deletions(-) create mode 100644 tools/data-api-differ/internal/README.md create mode 100644 tools/data-api-differ/internal/commands/args.go diff --git a/tools/data-api-differ/internal/README.md b/tools/data-api-differ/internal/README.md new file mode 100644 index 00000000000..85806a938ae --- /dev/null +++ b/tools/data-api-differ/internal/README.md @@ -0,0 +1,219 @@ +## Tool: `data-api-differ` + +This tool takes two sets of API Definitions and diffs them in several ways: + +1. Detects any Breaking Changes between the two sets of API Definitions. +2. Detects any Breaking and Non-Breaking Changes between the two sets of API Definitions. +3. Detects any new Resource ID Segments containing any new Static Identifiers which need to be reviewed (e.g. the fixed value associated with a Resource Provider or Static Resource ID Segment). + +These are available as three sub-commands and are described below. + +### Example Usage + +``` +$ ./data-api-differ +2023-12-07T12:16:15.106+0100 [INFO] Data API Differ launched.. +Usage: data-api-differ [--version] [--help] [] + +Available commands are: + detect-breaking-changes Retrieves two sets of API Definitions from the Data API and determines if there are any breaking changes + detect-changes Detects any changes between the existing and updated set of API Definitions + output-resource-id-segments Determines the new Resource IDs and then outputs a unique, sorted list of Static Identifiers found in the Resource ID Segments for review. +``` + +Specific examples for each command can be found below. + +### Supported Arguments + +All the subcommands support the same set of arguments: + +* (Required) `--initial-path` specifies the path to the directory containing the initial/existing set of API Definitions. +* (Required) `--updated-path` specifies the path to the directory containing the updated set of API Definitions. +* (Optional) `--data-api-binary-path` specifies the path to the Data API (V2) binary. If unspecified, it's assumed this exists on the PATH (e.g. sourced from `$GOPATH/bin`). + +Logging can be configured using the `LOG_LEVEL` environment variable (e.g. `LOG_LEVEL=trace`). + +### Example Usage: Detecting Breaking Changes + +This command detects both Breaking Changes that exist between the two sets of API Definitions. + +Command: + +``` +$ go build . && ./data-api-differ detect-breaking-changes --initial-path=/path/to/initial-api-definitions --updated-path=/path/to/updated-api-definitions +``` + +This command supports all of the arguments defined under `Supported Arguments` above. + +Example output: + +``` +2023-12-07T12:01:58.083+0100 [INFO] Data API Differ launched.. +2023-12-07T12:01:58.084+0100 [INFO] Running `detect-breaking-changes` command.. +2023-12-07T12:01:58.084+0100 [INFO] Data API Binary located at "data-api" +2023-12-07T12:01:58.084+0100 [INFO] Initial API Definitions located at: "/path/to/initial-api-definitions" +2023-12-07T12:01:58.084+0100 [INFO] Updated API Definitions located at: "/path/to/updated-api-definitions" +2023-12-07T12:01:58.084+0100 [INFO] Launching Data API.. +2023-12-07T12:01:59.097+0100 [INFO] Retrieving Services.. +2023-12-07T12:01:59.233+0100 [INFO] Launching Data API.. +2023-12-07T12:02:00.239+0100 [INFO] Retrieving Services.. +2023-12-07T12:02:00.240+0100 [INFO] Identifying a unique list of Service Names.. +2023-12-07T12:02:00.240+0100 [INFO] Detecting changes in Service "AADB2C".. +2023-12-07T12:02:00.240+0100 [INFO] Detecting changes in Service "Compute".. +2023/12/07 12:02:00 ## Breaking Changes + +🛑 **2 Breaking Changes** were detected. + +--- + +Summary of changes: + +* ❌ **Removed Service:** `AADB2C`. +* ❌ **Removed Service:** `Compute`. +``` + +Example of the Markdown Comment (rendered as Markdown): + +``` +## Breaking Changes + +🛑 **2 Breaking Changes** were detected. + +--- + +Summary of changes: + +* ❌ **Removed Service:** `AADB2C`. +* ❌ **Removed Service:** `Compute`. +``` + +### Example Usage: Detecting Changes + +This command detects both Breaking and Non-Breaking Changes that exist between the two sets of API Definitions. + +Command: + +``` +$ go build . && ./data-api-differ detect-changes --initial-path=/path/to/initial-api-definitions --updated-path=/path/to/updated-api-definitions +``` + +This command supports all of the arguments defined under `Supported Arguments` above. + +Example output: + +``` +2023-12-07T12:10:36.006+0100 [INFO] Data API Differ launched.. +2023-12-07T12:10:36.007+0100 [INFO] Running `detect-changes` command.. +2023-12-07T12:10:36.007+0100 [INFO] Data API Binary located at "data-api" +2023-12-07T12:10:36.007+0100 [INFO] Initial API Definitions located at: "/path/to/initial-api-definitions" +2023-12-07T12:10:36.007+0100 [INFO] Updated API Definitions located at: "/path/to/updated-api-definitions" +2023-12-07T12:10:36.007+0100 [INFO] Launching Data API.. +2023-12-07T12:10:37.021+0100 [INFO] Retrieving Services.. +2023-12-07T12:10:37.152+0100 [INFO] Launching Data API.. +2023-12-07T12:10:38.160+0100 [INFO] Retrieving Services.. +2023-12-07T12:10:38.291+0100 [INFO] Identifying a unique list of Service Names.. +2023-12-07T12:10:38.291+0100 [INFO] Detecting changes in Service "AADB2C".. +2023-12-07T12:10:38.291+0100 [INFO] Detecting changes in Service "Compute".. +2023/12/07 12:10:38 ## Summary of Changes + +* 👍 No Breaking Changes were detected. +* 👀 1 Non-Breaking Changes were detected. + +--- + + +## Non-Breaking Changes + +**1 Non-Breaking Changes** were detected: + +* ✅ **New Constant:** `PerformanceTier` (Type `string`) in `Compute@2021-07-01/DedicatedHost`. Possible Values: `Fast: Fast`, `None: None`, `VeryFast: VaVaVoom`. +``` + +Example of the Markdown Comment (rendered as Markdown): + +``` +## Summary of Changes + +* 👍 No Breaking Changes were detected. +* 👀 1 Non-Breaking Changes were detected. + +--- + + +## Non-Breaking Changes + +**1 Non-Breaking Changes** were detected: + +* ✅ **New Constant:** `PerformanceTier` (Type `string`) in `Compute@2021-07-01/DedicatedHost`. Possible Values: `Fast: Fast`, `None: None`, `VeryFast: VaVaVoom`. +``` + +### Example Usage: Outputting the list of new Static Identifiers found within any new Resource IDs + +Command: + +``` +$ go build . && ./data-api-differ output-resource-id-segments --initial-path=/path/to/initial-api-definitions --updated-path=/path/to/updated-api-definitions +``` + +Example output: + +``` + +2023-12-07T12:14:26.410+0100 [INFO] Data API Differ launched.. +2023-12-07T12:14:26.411+0100 [INFO] Running `output-resource-id-segments` command.. +2023-12-07T12:14:26.411+0100 [INFO] Data API Binary located at "data-api" +2023-12-07T12:14:26.411+0100 [INFO] Initial API Definitions located at: "/path/to/initial-api-definitions" +2023-12-07T12:14:26.411+0100 [INFO] Updated API Definitions located at: "/path/to/updated-api-definitions" +2023-12-07T12:14:26.411+0100 [INFO] Launching Data API.. +2023-12-07T12:14:27.421+0100 [INFO] Retrieving Services.. +2023-12-07T12:14:27.550+0100 [INFO] Launching Data API.. +2023-12-07T12:14:28.561+0100 [INFO] Retrieving Services.. +2023-12-07T12:14:28.712+0100 [INFO] Identifying a unique list of Service Names.. +2023-12-07T12:14:28.712+0100 [INFO] Detecting changes in Service "AADB2C".. +2023-12-07T12:14:28.712+0100 [INFO] Detecting changes in Service "Compute".. +2023/12/07 12:14:28 ## New Resource ID Segments containing Static Identifiers + +The following new Static Identifiers were detected from the set of changes (new/updated Resource IDs). + +> Note: Resource ID segments should **always** be `camelCased` and not `TitleCased`, `lowercased` or `kebab-cased`. + +Please review the following list of Static Identifiers: + +--- + +* `Microsoft.Compute` +* `favouriteHostGroups` +* `hostGroups` +* `providers` +* `resourceGroups` +* `subscriptions` + +--- + +> Note: Resource ID segments should **always** be `camelCased` and not `TitleCased`, `lowercased` or `kebab-cased`. +``` + +Example of the Markdown Comment (rendered as Markdown): + +``` +## New Resource ID Segments containing Static Identifiers + +The following new Static Identifiers were detected from the set of changes (new/updated Resource IDs). + +> Note: Resource ID segments should **always** be `camelCased` and not `TitleCased`, `lowercased` or `kebab-cased`. + +Please review the following list of Static Identifiers: + +--- + +* `Microsoft.Compute` +* `favouriteHostGroups` +* `hostGroups` +* `providers` +* `resourceGroups` +* `subscriptions` + +--- + +> Note: Resource ID segments should **always** be `camelCased` and not `TitleCased`, `lowercased` or `kebab-cased`. +``` diff --git a/tools/data-api-differ/internal/commands/args.go b/tools/data-api-differ/internal/commands/args.go new file mode 100644 index 00000000000..9951f67a0e0 --- /dev/null +++ b/tools/data-api-differ/internal/commands/args.go @@ -0,0 +1,92 @@ +package commands + +import ( + "flag" + "fmt" + "os" + "path/filepath" + + "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" +) + +type arguments struct { + // binaryName specifies the name of the binary + binaryName string + + // dataApiBinaryPath specifies the path to the Data API (v2) binary. + dataApiBinaryPath string + + // initialApiDefinitionsPath specifies the path to the initial set of API Definitions which should be compared against those within updatedPath. + initialApiDefinitionsPath string + + // updatedApiDefinitionsPath specifies the path to the updated set of API Definitions which should be compared against those within initialPath. + updatedApiDefinitionsPath string +} + +func (a *arguments) parse(input []string) error { + f := flag.NewFlagSet(a.binaryName, flag.ExitOnError) + + // this default allows for the binary to be on the path, helpful for automation purposes where the GOBIN is on the PATH + f.StringVar(&a.dataApiBinaryPath, "data-api-binary-path", "", "--data-api-binary-path=/path/to/the/data-api-binary") + f.StringVar(&a.initialApiDefinitionsPath, "initial-path", "", "--initial-path=/path/to/the/initial-api-definitions") + f.StringVar(&a.updatedApiDefinitionsPath, "updated-path", "", "--updated-path=/path/to/the/updated-api-definitions") + if err := f.Parse(input); err != nil { + return err + } + + var err error + + if a.dataApiBinaryPath != "" { + log.Logger.Debug(fmt.Sprintf("Determining the absolute path to %q", a.dataApiBinaryPath)) + a.dataApiBinaryPath, err = filepath.Abs(a.dataApiBinaryPath) + if err != nil { + return fmt.Errorf("determining absolute path to %q: %+v", a.dataApiBinaryPath, err) + } + } else { + log.Logger.Debug("A path to the Data API Binary was not specified - assuming this is installed onto the PATH") + a.dataApiBinaryPath = "data-api" + } + + if a.initialApiDefinitionsPath == "" { + return fmt.Errorf("`--initial-path` was not specified") + } + log.Logger.Debug(fmt.Sprintf("Determining the absolute path to %q", a.initialApiDefinitionsPath)) + a.initialApiDefinitionsPath, err = filepath.Abs(a.initialApiDefinitionsPath) + if err != nil { + return fmt.Errorf("determining absolute path to %q: %+v", a.initialApiDefinitionsPath, err) + } + + if a.updatedApiDefinitionsPath == "" { + return fmt.Errorf("`--updated-path` was not specified") + } + log.Logger.Debug(fmt.Sprintf("Determining the absolute path to %q", a.updatedApiDefinitionsPath)) + a.updatedApiDefinitionsPath, err = filepath.Abs(a.updatedApiDefinitionsPath) + if err != nil { + return fmt.Errorf("determining absolute path to %q: %+v", a.updatedApiDefinitionsPath, err) + } + + return nil +} + +// validate asserts that the arguments are valid +func (a *arguments) validate() error { + log.Logger.Trace("Validating the Initial API Definitions Path exists..") + if _, err := os.Stat(a.initialApiDefinitionsPath); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("validating `initial-path`: %q does not exist", a.initialApiDefinitionsPath) + } + + return fmt.Errorf("validating `initial-path`: %+v", err) + } + + log.Logger.Trace("Validating the Updated API Definitions Path exists..") + if _, err := os.Stat(a.updatedApiDefinitionsPath); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("validating `updated-path`: %q does not exist", a.updatedApiDefinitionsPath) + } + + return fmt.Errorf("validating `updated-path`: %+v", err) + } + + return nil +} diff --git a/tools/data-api-differ/internal/commands/detect_breaking_changes.go b/tools/data-api-differ/internal/commands/detect_breaking_changes.go index 0a2e5727b07..f83a02cb4cf 100644 --- a/tools/data-api-differ/internal/commands/detect_breaking_changes.go +++ b/tools/data-api-differ/internal/commands/detect_breaking_changes.go @@ -14,19 +14,12 @@ import ( var _ cli.Command = &DetectBreakingChangesCommand{} type DetectBreakingChangesCommand struct { - dataApiBinaryPath string - pathToInitialApiDefinitions string - pathToUpdatedApiDefinitions string - logger hclog.Logger + logger hclog.Logger } -func NewDetectBreakingChangesCommand(dataApiBinaryPath, initialPath, updatedPath string) func() (cli.Command, error) { +func NewDetectBreakingChangesCommand() func() (cli.Command, error) { return func() (cli.Command, error) { return &DetectBreakingChangesCommand{ - dataApiBinaryPath: dataApiBinaryPath, - pathToInitialApiDefinitions: initialPath, - pathToUpdatedApiDefinitions: updatedPath, - logger: internalLog.Logger, }, nil } @@ -42,10 +35,26 @@ This command detects any breaking changes that exist between the existing and an func (c DetectBreakingChangesCommand) Run(args []string) int { c.logger.Info("Running `detect-breaking-changes` command..") + a := arguments{} + c.logger.Debug("Parsing arguments..") + if err := a.parse(args); err != nil { + c.logger.Error(fmt.Sprintf("parsing arguments: %+v", err)) + return 1 + } + + if err := a.validate(); err != nil { + c.logger.Error(fmt.Sprintf("validating arguments: %+v", err)) + return 1 + } + + c.logger.Info(fmt.Sprintf("Data API Binary located at %q", a.dataApiBinaryPath)) + c.logger.Info(fmt.Sprintf("Initial API Definitions located at: %q", a.initialApiDefinitionsPath)) + c.logger.Info(fmt.Sprintf("Updated API Definitions located at: %q", a.updatedApiDefinitionsPath)) + c.logger.Debug("Performing diff of the two data sources..") - result, err := differ.Diff(c.dataApiBinaryPath, c.pathToInitialApiDefinitions, c.pathToUpdatedApiDefinitions) + result, err := differ.Diff(a.dataApiBinaryPath, a.initialApiDefinitionsPath, a.updatedApiDefinitionsPath) if err != nil { - c.logger.Error("performing diff: %+v", err) + c.logger.Error(fmt.Sprintf("performing diff: %+v", err)) return 1 } diff --git a/tools/data-api-differ/internal/commands/detect_changes.go b/tools/data-api-differ/internal/commands/detect_changes.go index 4dfd9e7df95..3b21283b01d 100644 --- a/tools/data-api-differ/internal/commands/detect_changes.go +++ b/tools/data-api-differ/internal/commands/detect_changes.go @@ -14,19 +14,12 @@ import ( var _ cli.Command = &DetectChangesCommand{} type DetectChangesCommand struct { - dataApiBinaryPath string - pathToInitialApiDefinitions string - pathToUpdatedApiDefinitions string - logger hclog.Logger + logger hclog.Logger } -func NewDetectChangesCommand(dataApiBinaryPath, initialPath, updatedPath string) func() (cli.Command, error) { +func NewDetectChangesCommand() func() (cli.Command, error) { return func() (cli.Command, error) { return &DetectChangesCommand{ - dataApiBinaryPath: dataApiBinaryPath, - pathToInitialApiDefinitions: initialPath, - pathToUpdatedApiDefinitions: updatedPath, - logger: internalLog.Logger, }, nil } @@ -44,10 +37,26 @@ This includes both breaking and non-breaking changes. func (c DetectChangesCommand) Run(args []string) int { c.logger.Info("Running `detect-changes` command..") + a := arguments{} + c.logger.Debug("Parsing arguments..") + if err := a.parse(args); err != nil { + c.logger.Error(fmt.Sprintf("parsing arguments: %+v", err)) + return 1 + } + + if err := a.validate(); err != nil { + c.logger.Error(fmt.Sprintf("validating arguments: %+v", err)) + return 1 + } + + c.logger.Info(fmt.Sprintf("Data API Binary located at %q", a.dataApiBinaryPath)) + c.logger.Info(fmt.Sprintf("Initial API Definitions located at: %q", a.initialApiDefinitionsPath)) + c.logger.Info(fmt.Sprintf("Updated API Definitions located at: %q", a.updatedApiDefinitionsPath)) + c.logger.Debug("Performing diff of the two data sources..") - result, err := differ.Diff(c.dataApiBinaryPath, c.pathToInitialApiDefinitions, c.pathToUpdatedApiDefinitions) + result, err := differ.Diff(a.dataApiBinaryPath, a.initialApiDefinitionsPath, a.updatedApiDefinitionsPath) if err != nil { - c.logger.Error("performing diff: %+v", err) + c.logger.Error(fmt.Sprintf("performing diff: %+v", err)) return 1 } diff --git a/tools/data-api-differ/internal/commands/output_resource_id_segments.go b/tools/data-api-differ/internal/commands/output_resource_id_segments.go index d2eff23a921..11905f526c0 100644 --- a/tools/data-api-differ/internal/commands/output_resource_id_segments.go +++ b/tools/data-api-differ/internal/commands/output_resource_id_segments.go @@ -14,19 +14,12 @@ import ( var _ cli.Command = &OutputResourceIdSegmentsCommand{} type OutputResourceIdSegmentsCommand struct { - dataApiBinaryPath string - pathToInitialApiDefinitions string - pathToUpdatedApiDefinitions string - logger hclog.Logger + logger hclog.Logger } -func NewOutputResourceIdSegmentsCommand(dataApiBinaryPath, initialPath, updatedPath string) func() (cli.Command, error) { +func NewOutputResourceIdSegmentsCommand() func() (cli.Command, error) { return func() (cli.Command, error) { return &OutputResourceIdSegmentsCommand{ - dataApiBinaryPath: dataApiBinaryPath, - pathToInitialApiDefinitions: initialPath, - pathToUpdatedApiDefinitions: updatedPath, - logger: internalLog.Logger, }, nil } @@ -43,10 +36,26 @@ and then outputs a unique, sorted list of any Static Identifiers found within th func (c OutputResourceIdSegmentsCommand) Run(args []string) int { c.logger.Info("Running `output-resource-id-segments` command..") + a := arguments{} + c.logger.Debug("Parsing arguments..") + if err := a.parse(args); err != nil { + c.logger.Error(fmt.Sprintf("parsing arguments: %+v", err)) + return 1 + } + + if err := a.validate(); err != nil { + c.logger.Error(fmt.Sprintf("validating arguments: %+v", err)) + return 1 + } + + c.logger.Info(fmt.Sprintf("Data API Binary located at %q", a.dataApiBinaryPath)) + c.logger.Info(fmt.Sprintf("Initial API Definitions located at: %q", a.initialApiDefinitionsPath)) + c.logger.Info(fmt.Sprintf("Updated API Definitions located at: %q", a.updatedApiDefinitionsPath)) + c.logger.Debug("Performing diff of the two data sources..") - result, err := differ.Diff(c.dataApiBinaryPath, c.pathToInitialApiDefinitions, c.pathToUpdatedApiDefinitions) + result, err := differ.Diff(a.dataApiBinaryPath, a.initialApiDefinitionsPath, a.updatedApiDefinitionsPath) if err != nil { - c.logger.Error("performing diff: %+v", err) + c.logger.Error(fmt.Sprintf("performing diff: %+v", err)) return 1 } diff --git a/tools/data-api-differ/main.go b/tools/data-api-differ/main.go index ab8065eeb33..dbfa65d1d0c 100644 --- a/tools/data-api-differ/main.go +++ b/tools/data-api-differ/main.go @@ -1,19 +1,20 @@ package main import ( - "os" - "github.com/hashicorp/go-hclog" "github.com/hashicorp/pandora/tools/data-api-differ/internal/commands" "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" "github.com/mitchellh/cli" + "os" ) +const binaryName = "data-api-differ" + func main() { opts := hclog.DefaultOptions - opts.Level = hclog.NoLevel - if level := os.Getenv("DEBUG"); level != "" { - opts.Level = hclog.Debug + opts.Level = hclog.Info + if level := os.Getenv("LOG_LEVEL"); level != "" { + opts.Level = hclog.LevelFromString(level) } log.Logger = hclog.New(opts) @@ -26,16 +27,12 @@ func main() { func run() error { log.Logger.Info("Data API Differ launched..") - dataApiBinaryPath := "..." - initialPath := "..." - updatedPath := "..." - - c := cli.NewCLI("data-api-differ", "1.0.0") + c := cli.NewCLI(binaryName, "1.0.0") c.Args = os.Args[1:] c.Commands = map[string]cli.CommandFactory{ - "detect-breaking-changes": commands.NewDetectBreakingChangesCommand(dataApiBinaryPath, initialPath, updatedPath), - "detect-changes": commands.NewDetectChangesCommand(dataApiBinaryPath, initialPath, updatedPath), - "output-resource-id-segments": commands.NewOutputResourceIdSegmentsCommand(dataApiBinaryPath, initialPath, updatedPath), + "detect-breaking-changes": commands.NewDetectBreakingChangesCommand(), + "detect-changes": commands.NewDetectChangesCommand(), + "output-resource-id-segments": commands.NewOutputResourceIdSegmentsCommand(), } exitStatus, err := c.Run() From 7f47dcfa4d422c87a2060fd2600d5fe880b952cc Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Thu, 7 Dec 2023 12:34:30 +0100 Subject: [PATCH 13/20] tools/data-api-differ: support for `--output-file-path` This allows outputting the rendered output to a file, which makes it possible to use the result in automation. --- tools/data-api-differ/internal/README.md | 91 ++++++++++--------- .../data-api-differ/internal/commands/args.go | 23 ++++- .../commands/detect_breaking_changes.go | 19 +++- .../internal/commands/detect_changes.go | 19 +++- .../commands/output_resource_id_segments.go | 19 +++- 5 files changed, 123 insertions(+), 48 deletions(-) diff --git a/tools/data-api-differ/internal/README.md b/tools/data-api-differ/internal/README.md index 85806a938ae..5d3035f2f75 100644 --- a/tools/data-api-differ/internal/README.md +++ b/tools/data-api-differ/internal/README.md @@ -30,6 +30,7 @@ All the subcommands support the same set of arguments: * (Required) `--initial-path` specifies the path to the directory containing the initial/existing set of API Definitions. * (Required) `--updated-path` specifies the path to the directory containing the updated set of API Definitions. * (Optional) `--data-api-binary-path` specifies the path to the Data API (V2) binary. If unspecified, it's assumed this exists on the PATH (e.g. sourced from `$GOPATH/bin`). +* (Optional) `--output-file-path` specifies the path where the result should be output to. If unspecified, this is output to the terminal. Logging can be configured using the `LOG_LEVEL` environment variable (e.g. `LOG_LEVEL=trace`). @@ -43,24 +44,25 @@ Command: $ go build . && ./data-api-differ detect-breaking-changes --initial-path=/path/to/initial-api-definitions --updated-path=/path/to/updated-api-definitions ``` -This command supports all of the arguments defined under `Supported Arguments` above. +This command supports each of the arguments defined under `Supported Arguments` above. Example output: ``` -2023-12-07T12:01:58.083+0100 [INFO] Data API Differ launched.. -2023-12-07T12:01:58.084+0100 [INFO] Running `detect-breaking-changes` command.. -2023-12-07T12:01:58.084+0100 [INFO] Data API Binary located at "data-api" -2023-12-07T12:01:58.084+0100 [INFO] Initial API Definitions located at: "/path/to/initial-api-definitions" -2023-12-07T12:01:58.084+0100 [INFO] Updated API Definitions located at: "/path/to/updated-api-definitions" -2023-12-07T12:01:58.084+0100 [INFO] Launching Data API.. -2023-12-07T12:01:59.097+0100 [INFO] Retrieving Services.. -2023-12-07T12:01:59.233+0100 [INFO] Launching Data API.. -2023-12-07T12:02:00.239+0100 [INFO] Retrieving Services.. -2023-12-07T12:02:00.240+0100 [INFO] Identifying a unique list of Service Names.. -2023-12-07T12:02:00.240+0100 [INFO] Detecting changes in Service "AADB2C".. -2023-12-07T12:02:00.240+0100 [INFO] Detecting changes in Service "Compute".. -2023/12/07 12:02:00 ## Breaking Changes +2023-12-07T12:32:07.937+0100 [INFO] Data API Differ launched.. +2023-12-07T12:32:07.937+0100 [INFO] Running `detect-breaking-changes` command.. +2023-12-07T12:32:07.941+0100 [INFO] Data API Binary located at "data-api" +2023-12-07T12:32:07.941+0100 [INFO] Initial API Definitions located at: "/path/to/initial-api-definitions" +2023-12-07T12:32:07.941+0100 [INFO] Updated API Definitions located at: "/path/to/updated-api-definitions" +2023-12-07T12:32:07.941+0100 [INFO] Output will be rendered to the console since no output file was specified +2023-12-07T12:32:07.941+0100 [INFO] Launching Data API.. +2023-12-07T12:32:08.952+0100 [INFO] Retrieving Services.. +2023-12-07T12:32:09.086+0100 [INFO] Launching Data API.. +2023-12-07T12:32:10.096+0100 [INFO] Retrieving Services.. +2023-12-07T12:32:10.098+0100 [INFO] Identifying a unique list of Service Names.. +2023-12-07T12:32:10.098+0100 [INFO] Detecting changes in Service "AADB2C".. +2023-12-07T12:32:10.098+0100 [INFO] Detecting changes in Service "Compute".. +2023/12/07 12:32:10## Breaking Changes 🛑 **2 Breaking Changes** were detected. @@ -97,24 +99,25 @@ Command: $ go build . && ./data-api-differ detect-changes --initial-path=/path/to/initial-api-definitions --updated-path=/path/to/updated-api-definitions ``` -This command supports all of the arguments defined under `Supported Arguments` above. +This command supports each of the arguments defined under `Supported Arguments` above. Example output: ``` -2023-12-07T12:10:36.006+0100 [INFO] Data API Differ launched.. -2023-12-07T12:10:36.007+0100 [INFO] Running `detect-changes` command.. -2023-12-07T12:10:36.007+0100 [INFO] Data API Binary located at "data-api" -2023-12-07T12:10:36.007+0100 [INFO] Initial API Definitions located at: "/path/to/initial-api-definitions" -2023-12-07T12:10:36.007+0100 [INFO] Updated API Definitions located at: "/path/to/updated-api-definitions" -2023-12-07T12:10:36.007+0100 [INFO] Launching Data API.. -2023-12-07T12:10:37.021+0100 [INFO] Retrieving Services.. -2023-12-07T12:10:37.152+0100 [INFO] Launching Data API.. -2023-12-07T12:10:38.160+0100 [INFO] Retrieving Services.. -2023-12-07T12:10:38.291+0100 [INFO] Identifying a unique list of Service Names.. -2023-12-07T12:10:38.291+0100 [INFO] Detecting changes in Service "AADB2C".. -2023-12-07T12:10:38.291+0100 [INFO] Detecting changes in Service "Compute".. -2023/12/07 12:10:38 ## Summary of Changes +2023-12-07T12:31:01.837+0100 [INFO] Data API Differ launched.. +2023-12-07T12:31:01.837+0100 [INFO] Running `detect-changes` command.. +2023-12-07T12:31:01.837+0100 [INFO] Data API Binary located at "data-api" +2023-12-07T12:31:01.837+0100 [INFO] Initial API Definitions located at: "/path/to/initial-api-definitions" +2023-12-07T12:31:01.837+0100 [INFO] Updated API Definitions located at: "/path/to/updated-api-definitions" +2023-12-07T12:31:01.837+0100 [INFO] Output will be rendered to the console since no output file was specified +2023-12-07T12:31:01.837+0100 [INFO] Launching Data API.. +2023-12-07T12:31:02.847+0100 [INFO] Retrieving Services.. +2023-12-07T12:31:02.983+0100 [INFO] Launching Data API.. +2023-12-07T12:31:03.989+0100 [INFO] Retrieving Services.. +2023-12-07T12:31:04.117+0100 [INFO] Identifying a unique list of Service Names.. +2023-12-07T12:31:04.117+0100 [INFO] Detecting changes in Service "AADB2C".. +2023-12-07T12:31:04.117+0100 [INFO] Detecting changes in Service "Compute".. +2023/12/07 12:31:04 ## Summary of Change## Summary of Changes * 👍 No Breaking Changes were detected. * 👀 1 Non-Breaking Changes were detected. @@ -149,29 +152,33 @@ Example of the Markdown Comment (rendered as Markdown): ### Example Usage: Outputting the list of new Static Identifiers found within any new Resource IDs +This command detects any new Resource IDs or Resource ID Segments which are new/have been updated between the two sets of API Definitions, identifies any Static Identifiers (e.g. the Fixed Value for a Resource Provider or Static Resource ID Segment) and then outputs a unique, sorted list of these for review. + Command: ``` $ go build . && ./data-api-differ output-resource-id-segments --initial-path=/path/to/initial-api-definitions --updated-path=/path/to/updated-api-definitions ``` +This command supports each of the arguments defined under `Supported Arguments` above. + Example output: ``` - -2023-12-07T12:14:26.410+0100 [INFO] Data API Differ launched.. -2023-12-07T12:14:26.411+0100 [INFO] Running `output-resource-id-segments` command.. -2023-12-07T12:14:26.411+0100 [INFO] Data API Binary located at "data-api" -2023-12-07T12:14:26.411+0100 [INFO] Initial API Definitions located at: "/path/to/initial-api-definitions" -2023-12-07T12:14:26.411+0100 [INFO] Updated API Definitions located at: "/path/to/updated-api-definitions" -2023-12-07T12:14:26.411+0100 [INFO] Launching Data API.. -2023-12-07T12:14:27.421+0100 [INFO] Retrieving Services.. -2023-12-07T12:14:27.550+0100 [INFO] Launching Data API.. -2023-12-07T12:14:28.561+0100 [INFO] Retrieving Services.. -2023-12-07T12:14:28.712+0100 [INFO] Identifying a unique list of Service Names.. -2023-12-07T12:14:28.712+0100 [INFO] Detecting changes in Service "AADB2C".. -2023-12-07T12:14:28.712+0100 [INFO] Detecting changes in Service "Compute".. -2023/12/07 12:14:28 ## New Resource ID Segments containing Static Identifiers +2023-12-07T12:29:36.823+0100 [INFO] Data API Differ launched.. +2023-12-07T12:29:36.823+0100 [INFO] Running `output-resource-id-segments` command.. +2023-12-07T12:29:36.823+0100 [INFO] Data API Binary located at "data-api" +2023-12-07T12:29:36.823+0100 [INFO] Initial API Definitions located at: "/path/to/initial-api-definitions" +2023-12-07T12:29:36.823+0100 [INFO] Updated API Definitions located at: "/path/to/updated-api-definitions" +2023-12-07T12:29:36.823+0100 [INFO] Output will be rendered to the console since no output file was specified +2023-12-07T12:29:36.823+0100 [INFO] Launching Data API.. +2023-12-07T12:29:37.845+0100 [INFO] Retrieving Services.. +2023-12-07T12:29:38.007+0100 [INFO] Launching Data API.. +2023-12-07T12:29:39.021+0100 [INFO] Retrieving Services.. +2023-12-07T12:29:39.176+0100 [INFO] Identifying a unique list of Service Names.. +2023-12-07T12:29:39.176+0100 [INFO] Detecting changes in Service "AADB2C".. +2023-12-07T12:29:39.177+0100 [INFO] Detecting changes in Service "Compute".. +2023/12/07 12:29:39 ## New Resource ID Segments containing Static Identifiers The following new Static Identifiers were detected from the set of changes (new/updated Resource IDs). diff --git a/tools/data-api-differ/internal/commands/args.go b/tools/data-api-differ/internal/commands/args.go index 9951f67a0e0..1a303910d15 100644 --- a/tools/data-api-differ/internal/commands/args.go +++ b/tools/data-api-differ/internal/commands/args.go @@ -19,6 +19,9 @@ type arguments struct { // initialApiDefinitionsPath specifies the path to the initial set of API Definitions which should be compared against those within updatedPath. initialApiDefinitionsPath string + // outputFilePath specifies the path to the output file where the Result should be rendered. + outputFilePath *string + // updatedApiDefinitionsPath specifies the path to the updated set of API Definitions which should be compared against those within initialPath. updatedApiDefinitionsPath string } @@ -30,12 +33,17 @@ func (a *arguments) parse(input []string) error { f.StringVar(&a.dataApiBinaryPath, "data-api-binary-path", "", "--data-api-binary-path=/path/to/the/data-api-binary") f.StringVar(&a.initialApiDefinitionsPath, "initial-path", "", "--initial-path=/path/to/the/initial-api-definitions") f.StringVar(&a.updatedApiDefinitionsPath, "updated-path", "", "--updated-path=/path/to/the/updated-api-definitions") + var outputFilePath string + f.StringVar(&outputFilePath, "output-file-path", "", "--output-file=/path/to/the/output/file") if err := f.Parse(input); err != nil { return err } - var err error + if outputFilePath != "" { + a.outputFilePath = &outputFilePath + } + var err error if a.dataApiBinaryPath != "" { log.Logger.Debug(fmt.Sprintf("Determining the absolute path to %q", a.dataApiBinaryPath)) a.dataApiBinaryPath, err = filepath.Abs(a.dataApiBinaryPath) @@ -53,7 +61,7 @@ func (a *arguments) parse(input []string) error { log.Logger.Debug(fmt.Sprintf("Determining the absolute path to %q", a.initialApiDefinitionsPath)) a.initialApiDefinitionsPath, err = filepath.Abs(a.initialApiDefinitionsPath) if err != nil { - return fmt.Errorf("determining absolute path to %q: %+v", a.initialApiDefinitionsPath, err) + return fmt.Errorf("determining the absolute path to %q: %+v", a.initialApiDefinitionsPath, err) } if a.updatedApiDefinitionsPath == "" { @@ -62,7 +70,16 @@ func (a *arguments) parse(input []string) error { log.Logger.Debug(fmt.Sprintf("Determining the absolute path to %q", a.updatedApiDefinitionsPath)) a.updatedApiDefinitionsPath, err = filepath.Abs(a.updatedApiDefinitionsPath) if err != nil { - return fmt.Errorf("determining absolute path to %q: %+v", a.updatedApiDefinitionsPath, err) + return fmt.Errorf("determining the absolute path to %q: %+v", a.updatedApiDefinitionsPath, err) + } + + if a.outputFilePath != nil { + log.Logger.Debug(fmt.Sprintf("Determining the absolute path to %q", *a.outputFilePath)) + path, err := filepath.Abs(*a.outputFilePath) + if err != nil { + return fmt.Errorf("determining the absolute path to %q: %+v", *a.outputFilePath, err) + } + a.outputFilePath = &path } return nil diff --git a/tools/data-api-differ/internal/commands/detect_breaking_changes.go b/tools/data-api-differ/internal/commands/detect_breaking_changes.go index f83a02cb4cf..dcb97e484b0 100644 --- a/tools/data-api-differ/internal/commands/detect_breaking_changes.go +++ b/tools/data-api-differ/internal/commands/detect_breaking_changes.go @@ -3,6 +3,7 @@ package commands import ( "fmt" "log" + "os" "github.com/hashicorp/go-hclog" "github.com/hashicorp/pandora/tools/data-api-differ/internal/differ" @@ -51,6 +52,12 @@ func (c DetectBreakingChangesCommand) Run(args []string) int { c.logger.Info(fmt.Sprintf("Initial API Definitions located at: %q", a.initialApiDefinitionsPath)) c.logger.Info(fmt.Sprintf("Updated API Definitions located at: %q", a.updatedApiDefinitionsPath)) + if a.outputFilePath != nil { + c.logger.Info(fmt.Sprintf("Output will be rendered to the file located at: %q", *a.outputFilePath)) + } else { + c.logger.Info("Output will be rendered to the console since no output file was specified") + } + c.logger.Debug("Performing diff of the two data sources..") result, err := differ.Diff(a.dataApiBinaryPath, a.initialApiDefinitionsPath, a.updatedApiDefinitionsPath) if err != nil { @@ -66,7 +73,17 @@ func (c DetectBreakingChangesCommand) Run(args []string) int { c.logger.Error(fmt.Sprintf("rendering markdown: %+v", err)) return 1 } - log.Print(*rendered) + + // Finally determine how to output that + if a.outputFilePath != nil { + c.logger.Trace(fmt.Sprintf("Writing output to %q..", *a.outputFilePath)) + if err := os.WriteFile(*a.outputFilePath, []byte(*rendered), 0644); err != nil { + c.logger.Error(fmt.Sprintf("writing output to %q: %+v", *a.outputFilePath, err)) + } + } else { + c.logger.Trace("Rendering output to Terminal since no output file was specified..") + log.Print(*rendered) + } return 0 } diff --git a/tools/data-api-differ/internal/commands/detect_changes.go b/tools/data-api-differ/internal/commands/detect_changes.go index 3b21283b01d..ee46eccbb93 100644 --- a/tools/data-api-differ/internal/commands/detect_changes.go +++ b/tools/data-api-differ/internal/commands/detect_changes.go @@ -7,6 +7,7 @@ import ( internalLog "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" "github.com/hashicorp/pandora/tools/data-api-differ/internal/views" "log" + "os" "github.com/mitchellh/cli" ) @@ -53,6 +54,12 @@ func (c DetectChangesCommand) Run(args []string) int { c.logger.Info(fmt.Sprintf("Initial API Definitions located at: %q", a.initialApiDefinitionsPath)) c.logger.Info(fmt.Sprintf("Updated API Definitions located at: %q", a.updatedApiDefinitionsPath)) + if a.outputFilePath != nil { + c.logger.Info(fmt.Sprintf("Output will be rendered to the file located at: %q", *a.outputFilePath)) + } else { + c.logger.Info("Output will be rendered to the console since no output file was specified") + } + c.logger.Debug("Performing diff of the two data sources..") result, err := differ.Diff(a.dataApiBinaryPath, a.initialApiDefinitionsPath, a.updatedApiDefinitionsPath) if err != nil { @@ -68,7 +75,17 @@ func (c DetectChangesCommand) Run(args []string) int { c.logger.Error(fmt.Sprintf("rendering markdown: %+v", err)) return 1 } - log.Print(*rendered) + + // Finally determine how to output that + if a.outputFilePath != nil { + c.logger.Trace(fmt.Sprintf("Writing output to %q..", *a.outputFilePath)) + if err := os.WriteFile(*a.outputFilePath, []byte(*rendered), 0644); err != nil { + c.logger.Error(fmt.Sprintf("writing output to %q: %+v", *a.outputFilePath, err)) + } + } else { + c.logger.Trace("Rendering output to Terminal since no output file was specified..") + log.Print(*rendered) + } return 0 } diff --git a/tools/data-api-differ/internal/commands/output_resource_id_segments.go b/tools/data-api-differ/internal/commands/output_resource_id_segments.go index 11905f526c0..a56a6b88d5c 100644 --- a/tools/data-api-differ/internal/commands/output_resource_id_segments.go +++ b/tools/data-api-differ/internal/commands/output_resource_id_segments.go @@ -3,6 +3,7 @@ package commands import ( "fmt" "log" + "os" "github.com/hashicorp/go-hclog" "github.com/hashicorp/pandora/tools/data-api-differ/internal/differ" @@ -52,6 +53,12 @@ func (c OutputResourceIdSegmentsCommand) Run(args []string) int { c.logger.Info(fmt.Sprintf("Initial API Definitions located at: %q", a.initialApiDefinitionsPath)) c.logger.Info(fmt.Sprintf("Updated API Definitions located at: %q", a.updatedApiDefinitionsPath)) + if a.outputFilePath != nil { + c.logger.Info(fmt.Sprintf("Output will be rendered to the file located at: %q", *a.outputFilePath)) + } else { + c.logger.Info("Output will be rendered to the console since no output file was specified") + } + c.logger.Debug("Performing diff of the two data sources..") result, err := differ.Diff(a.dataApiBinaryPath, a.initialApiDefinitionsPath, a.updatedApiDefinitionsPath) if err != nil { @@ -67,7 +74,17 @@ func (c OutputResourceIdSegmentsCommand) Run(args []string) int { c.logger.Error(fmt.Sprintf("rendering markdown: %+v", err)) return 1 } - log.Print(*rendered) + + // Finally determine how to output that + if a.outputFilePath != nil { + c.logger.Trace(fmt.Sprintf("Writing output to %q..", *a.outputFilePath)) + if err := os.WriteFile(*a.outputFilePath, []byte(*rendered), 0644); err != nil { + c.logger.Error(fmt.Sprintf("writing output to %q: %+v", *a.outputFilePath, err)) + } + } else { + c.logger.Trace("Rendering output to Terminal since no output file was specified..") + log.Print(*rendered) + } return 0 } From f1242bfbf33ecd57d3403212169bdea683467d51 Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Thu, 7 Dec 2023 14:26:40 +0100 Subject: [PATCH 14/20] automation: adding a GitHub Action to run the Data API Differ and then output the result on Pull Requests This triggers on changes to the API Definitions (to handle these changing), changes to the Data API (so we can see the result) and on changes to `importer-rest-api-specs`. This currently doesn't run for changes to `importer-msgraph-metadata` but can be added when the Data API SDK is updated in the near future. --- .../workflows/automation-data-api-differ.yaml | 41 ++++++++ ...on-determine-changes-to-api-definitions.sh | 97 +++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 .github/workflows/automation-data-api-differ.yaml create mode 100755 scripts/automation-determine-changes-to-api-definitions.sh diff --git a/.github/workflows/automation-data-api-differ.yaml b/.github/workflows/automation-data-api-differ.yaml new file mode 100644 index 00000000000..26c60f5f24c --- /dev/null +++ b/.github/workflows/automation-data-api-differ.yaml @@ -0,0 +1,41 @@ +--- +name: Detect changes to the API Definitions +on: + pull_request_target: + paths: + - 'api-definitions/**' # to detect changes when the API Definitions are updated + - 'tools/data-api/**' # to detect changes when the Data API is updated + - 'tools/importer-rest-api-specs/**' # to detect changes when the Importer is updated + types: ['opened', 'edited'] + +jobs: + detect-changes-to-the-api-definitions: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version: '1.21.3' + + - name: Detect Changes + run: | + ./scripts/automation-determine-changes-to-api-definitions.sh outputs/ + + - name: Post Comment containing Breaking Changes + uses: thollander/actions-comment-pull-request@1d3973dc4b8e1399c0620d3f2b1aa5e795465308 + with: + filePath: outputs/breaking-changes.md + + - name: Post Comment containing Changes + uses: thollander/actions-comment-pull-request@1d3973dc4b8e1399c0620d3f2b1aa5e795465308 + with: + filePath: outputs/changes.md + + - name: Post Comment with New Static Identifiers + uses: thollander/actions-comment-pull-request@1d3973dc4b8e1399c0620d3f2b1aa5e795465308 + with: + filePath: outputs/static-identifiers.md diff --git a/scripts/automation-determine-changes-to-api-definitions.sh b/scripts/automation-determine-changes-to-api-definitions.sh new file mode 100755 index 00000000000..7cdf5860e1b --- /dev/null +++ b/scripts/automation-determine-changes-to-api-definitions.sh @@ -0,0 +1,97 @@ +#!/bin/bash + +set -e + +DIR="$(cd "$(dirname "$0")" && pwd)/.." + +function buildAndInstallDependencies { + cd "${DIR}" + + echo "Building and Installing the Data API onto the GOPATH" + cd ./tools/data-api + go install + cd "${DIR}" + + echo "Building and Installing the Data API Differ onto the GOPATH" + cd ./tools/data-api-differ + go install + cd "${DIR}" +} + +function checkoutAPIDefinitionsFromMainInto { + local workingDirectory="$1" + local existingPandoraDirectory="${DIR}" + + cd "${DIR}" + + echo "Removing any existing directory at ${workingDirectory}.." + rm -rf "$workingDirectory" + + echo "Checking out a secondary copy of hashicorp/pandora from this repository.." + git clone --depth 1 --branch main "file://$existingPandoraDirectory" "$workingDirectory" + cd "$workingDirectory" + + echo "Resetting the secondary working directory" + git reset --hard + git clean -xdf + + echo "Checking out the 'main' branch in the copy" + git checkout main + + echo "Returning to the original working directory.." + cd "${DIR}" +} + +function ensureDirectoryExists { + local directory="$1" + + echo "Removing any existing directory at ${directory}.." + rm -rf "$directory" + + echo "Recreating the directory ${directory}" + mkdir -p "${directory}" +} + +function runBreakingChangeDetector { + local initialApiDefinitionsDirectory="$1" + local updatedApiDefinitionsDirectory="$2" + local outputFilePath="$3" + + echo "Detecting Breaking Changes between ${initialApiDefinitionsDirectory} and ${updatedApiDefinitionsDirectory}.." + data-api-differ detect-breaking-changes --initial-path="${initialApiDefinitionsDirectory}" --updated-path="${updatedApiDefinitionsDirectory}" --output-file-path="${outputFilePath}" +} + +function runChangeDetector { + local initialApiDefinitionsDirectory="$1" + local updatedApiDefinitionsDirectory="$2" + local outputFilePath="$3" + + echo "Detecting Changes between ${initialApiDefinitionsDirectory} and ${updatedApiDefinitionsDirectory}.." + data-api-differ detect-changes --initial-path="${initialApiDefinitionsDirectory}" --updated-path="${updatedApiDefinitionsDirectory}" --output-file-path="${outputFilePath}" +} + +function runStaticIdentifierDetector { + local initialApiDefinitionsDirectory="$1" + local updatedApiDefinitionsDirectory="$2" + local outputFilePath="$3" + + echo "Detecting any new Static Identifiers between ${initialApiDefinitionsDirectory} and ${updatedApiDefinitionsDirectory}.." + data-api-differ output-resource-id-segments --initial-path="${initialApiDefinitionsDirectory}" --updated-path="${updatedApiDefinitionsDirectory}" --output-file-path="${outputFilePath}" +} + +function main { + local tempDirectory="${TMPDIR}/pandora-from-main" + local initialApiDefinitionsDirectory="${DIR}/api-definitions" + local updatedApiDefinitionsDirectory="${tempDirectory}/api-definitions" + local outputDirectory="$1" + + buildAndInstallDependencies + checkoutAPIDefinitionsFromMainInto "$tempDirectory" + ensureDirectoryExists "$outputDirectory" + + runBreakingChangeDetector "$initialApiDefinitionsDirectory" "$updatedApiDefinitionsDirectory" "${outputDirectory}/breaking-changes.md" + runChangeDetector "$initialApiDefinitionsDirectory" "$updatedApiDefinitionsDirectory" "${outputDirectory}/changes.md" + runStaticIdentifierDetector "$initialApiDefinitionsDirectory" "$updatedApiDefinitionsDirectory" "${outputDirectory}/static-identifiers.md" +} + +main "$1" \ No newline at end of file From fefb00201e969d08bf7d8de3920091c5935e36a0 Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Thu, 7 Dec 2023 14:28:27 +0100 Subject: [PATCH 15/20] git: ignoring the `./outputs` directory This is used by the Data API Differ automation --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 267614fac62..eaadc1e34a7 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ tools/version-bumper/version-bumper tools/wrapper-automation/wrapper-automation vendor/ +# Temp directories +outputs/ + # .net binaries [Dd]ebug/ [Rr]elease/ From 21b544f3525ddbce43d1241350e1193b7b015340 Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Thu, 7 Dec 2023 14:29:09 +0100 Subject: [PATCH 16/20] tools/data-api-differ: moving the README up a level --- tools/data-api-differ/{internal => }/README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tools/data-api-differ/{internal => }/README.md (100%) diff --git a/tools/data-api-differ/internal/README.md b/tools/data-api-differ/README.md similarity index 100% rename from tools/data-api-differ/internal/README.md rename to tools/data-api-differ/README.md From 9d9d12e1e9ebaf48d4e8f361d7ba53ff10e14b3b Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Thu, 7 Dec 2023 14:31:07 +0100 Subject: [PATCH 17/20] readme: adding a reference to the Data API Differ --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 19307ade28b..23a837267d0 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ More information on [how to import a new Resource Manager Service/API Version fo - `./docs` - contains documentation. - `./submodules/msgraph-metadata` - contains the Git Submodule to [the `microsoftgraph/msgraph-metadata` repository](https://github.com/microsoftgraph/msgraph-metadata) - containing the OpenAPI/Swagger definitions for Microsoft Graph. - `./submodules/rest-api-specs` - contains the Git Submodule to [the `Azure/azure-rest-api-specs` repository](https://github.com/Azure/azure-rest-api-specs) - containing the OpenAPI/Swagger definitions for Azure Resource Manager. +- `./tools/data-api-differ` - contains the Data API Differ which detects changes to the API Definitions. - `./tools/generator-go-sdk` - contains the Go SDK Generator, pulling information from the Data API. - `./tools/generator-terraform` - contains the Terraform Generator, pulling information from the Data API. - `./tools/importer-msgraph-metadata` - contains the Importer for the Microsoft Graph API Definitions. From 10e7e777df32e3716d997fb6916b207c2ab56be0 Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Thu, 7 Dec 2023 14:33:25 +0100 Subject: [PATCH 18/20] automation: ensuring the entire git history is available This means that we have access to the `main` branch to be able to clone it --- .github/workflows/automation-data-api-differ.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/automation-data-api-differ.yaml b/.github/workflows/automation-data-api-differ.yaml index 26c60f5f24c..53780968b58 100644 --- a/.github/workflows/automation-data-api-differ.yaml +++ b/.github/workflows/automation-data-api-differ.yaml @@ -16,6 +16,8 @@ jobs: pull-requests: write steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + fetch-depth: 0 - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: From 5d816be707ccb83680bc182db9763628ffccbe70 Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Thu, 7 Dec 2023 14:34:32 +0100 Subject: [PATCH 19/20] automation: triggering the Data API Differ when changes happen to it too --- .github/workflows/automation-data-api-differ.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/automation-data-api-differ.yaml b/.github/workflows/automation-data-api-differ.yaml index 53780968b58..9976174497c 100644 --- a/.github/workflows/automation-data-api-differ.yaml +++ b/.github/workflows/automation-data-api-differ.yaml @@ -5,6 +5,7 @@ on: paths: - 'api-definitions/**' # to detect changes when the API Definitions are updated - 'tools/data-api/**' # to detect changes when the Data API is updated + - 'tools/data-api-differ/**' # to detect changes when the Data API Differ is updated - 'tools/importer-rest-api-specs/**' # to detect changes when the Importer is updated types: ['opened', 'edited'] From 547755b81362b045dcbf7bc918e5249f644ccc55 Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Thu, 7 Dec 2023 14:36:45 +0100 Subject: [PATCH 20/20] tools/data-api-differ: fixing `gofmt` and `goimports` --- tools/data-api-differ/internal/commands/detect_changes.go | 5 +++-- tools/data-api-differ/internal/differ/differ.go | 1 + .../internal/views/resource_id_segments_test.go | 2 +- tools/data-api-differ/main.go | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/tools/data-api-differ/internal/commands/detect_changes.go b/tools/data-api-differ/internal/commands/detect_changes.go index ee46eccbb93..731f3dca811 100644 --- a/tools/data-api-differ/internal/commands/detect_changes.go +++ b/tools/data-api-differ/internal/commands/detect_changes.go @@ -2,12 +2,13 @@ package commands import ( "fmt" + "log" + "os" + "github.com/hashicorp/go-hclog" "github.com/hashicorp/pandora/tools/data-api-differ/internal/differ" internalLog "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" "github.com/hashicorp/pandora/tools/data-api-differ/internal/views" - "log" - "os" "github.com/mitchellh/cli" ) diff --git a/tools/data-api-differ/internal/differ/differ.go b/tools/data-api-differ/internal/differ/differ.go index 3f2c6defba6..ab541f9f059 100644 --- a/tools/data-api-differ/internal/differ/differ.go +++ b/tools/data-api-differ/internal/differ/differ.go @@ -2,6 +2,7 @@ package differ import ( "fmt" + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" "github.com/hashicorp/pandora/tools/data-api-differ/internal/dataapi" "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" diff --git a/tools/data-api-differ/internal/views/resource_id_segments_test.go b/tools/data-api-differ/internal/views/resource_id_segments_test.go index b777a9a6cae..bec7aec3870 100644 --- a/tools/data-api-differ/internal/views/resource_id_segments_test.go +++ b/tools/data-api-differ/internal/views/resource_id_segments_test.go @@ -1,10 +1,10 @@ package views import ( - "github.com/hashicorp/go-azure-helpers/lang/pointer" "strings" "testing" + "github.com/hashicorp/go-azure-helpers/lang/pointer" "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" "github.com/hashicorp/pandora/tools/sdk/testhelpers" ) diff --git a/tools/data-api-differ/main.go b/tools/data-api-differ/main.go index dbfa65d1d0c..0e8a91cdcc1 100644 --- a/tools/data-api-differ/main.go +++ b/tools/data-api-differ/main.go @@ -1,11 +1,12 @@ package main import ( + "os" + "github.com/hashicorp/go-hclog" "github.com/hashicorp/pandora/tools/data-api-differ/internal/commands" "github.com/hashicorp/pandora/tools/data-api-differ/internal/log" "github.com/mitchellh/cli" - "os" ) const binaryName = "data-api-differ"