diff --git a/.github/workflows/automation-data-api-differ.yaml b/.github/workflows/automation-data-api-differ.yaml new file mode 100644 index 00000000000..9976174497c --- /dev/null +++ b/.github/workflows/automation-data-api-differ.yaml @@ -0,0 +1,44 @@ +--- +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/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'] + +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 + with: + fetch-depth: 0 + + - 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/.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/.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/ 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. 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 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/README.md b/tools/data-api-differ/README.md new file mode 100644 index 00000000000..5d3035f2f75 --- /dev/null +++ b/tools/data-api-differ/README.md @@ -0,0 +1,226 @@ +## 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`). +* (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`). + +### 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 each of the arguments defined under `Supported Arguments` above. + +Example output: + +``` +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. + +--- + +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 each of the arguments defined under `Supported Arguments` above. + +Example output: + +``` +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. + +--- + + +## 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 + +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: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). + +> 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/go.mod b/tools/data-api-differ/go.mod new file mode 100644 index 00000000000..d26cd612dd6 --- /dev/null +++ b/tools/data-api-differ/go.mod @@ -0,0 +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/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_type_value_changed.go b/tools/data-api-differ/internal/changes/model_discriminated_type_value_changed.go new file mode 100644 index 00000000000..804d822a609 --- /dev/null +++ b/tools/data-api-differ/internal/changes/model_discriminated_type_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..9b4db994bfd --- /dev/null +++ b/tools/data-api-differ/internal/changes/resource_id_added.go @@ -0,0 +1,33 @@ +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 + + // 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. +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..8bea6f59831 --- /dev/null +++ b/tools/data-api-differ/internal/changes/resource_id_segment_changed_value.go @@ -0,0 +1,37 @@ +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 + + // StaticIdentifierInNewValue specifies any static identifier present in the updated Resource ID Segment. + StaticIdentifierInNewValue *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..84d43b53c3a --- /dev/null +++ b/tools/data-api-differ/internal/changes/resource_id_segments_changed_length.go @@ -0,0 +1,37 @@ +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 Segments that has changed. + ResourceIdName 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 + + // 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. +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/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 +} 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..1a303910d15 --- /dev/null +++ b/tools/data-api-differ/internal/commands/args.go @@ -0,0 +1,109 @@ +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 + + // 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 +} + +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") + 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 + } + + 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) + 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 the 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 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 +} + +// 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 new file mode 100644 index 00000000000..dcb97e484b0 --- /dev/null +++ b/tools/data-api-differ/internal/commands/detect_breaking_changes.go @@ -0,0 +1,93 @@ +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" + "github.com/mitchellh/cli" +) + +var _ cli.Command = &DetectBreakingChangesCommand{} + +type DetectBreakingChangesCommand struct { + logger hclog.Logger +} + +func NewDetectBreakingChangesCommand() func() (cli.Command, error) { + return func() (cli.Command, error) { + return &DetectBreakingChangesCommand{ + logger: internalLog.Logger, + }, 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 (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)) + + 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 { + c.logger.Error(fmt.Sprintf("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 + } + + // 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 +} + +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..731f3dca811 --- /dev/null +++ b/tools/data-api-differ/internal/commands/detect_changes.go @@ -0,0 +1,96 @@ +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" + + "github.com/mitchellh/cli" +) + +var _ cli.Command = &DetectChangesCommand{} + +type DetectChangesCommand struct { + logger hclog.Logger +} + +func NewDetectChangesCommand() func() (cli.Command, error) { + return func() (cli.Command, error) { + return &DetectChangesCommand{ + logger: internalLog.Logger, + }, 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 (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)) + + 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 { + c.logger.Error(fmt.Sprintf("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 + } + + // 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 +} + +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/output_resource_id_segments.go b/tools/data-api-differ/internal/commands/output_resource_id_segments.go new file mode 100644 index 00000000000..a56a6b88d5c --- /dev/null +++ b/tools/data-api-differ/internal/commands/output_resource_id_segments.go @@ -0,0 +1,94 @@ +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" + "github.com/mitchellh/cli" +) + +var _ cli.Command = &OutputResourceIdSegmentsCommand{} + +type OutputResourceIdSegmentsCommand struct { + logger hclog.Logger +} + +func NewOutputResourceIdSegmentsCommand() func() (cli.Command, error) { + return func() (cli.Command, error) { + return &OutputResourceIdSegmentsCommand{ + 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..") + + 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)) + + 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 { + c.logger.Error(fmt.Sprintf("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 + } + + // 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 +} + +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/dataapi/data.go b/tools/data-api-differ/internal/dataapi/data.go new file mode 100644 index 00000000000..67fd1a2a54c --- /dev/null +++ b/tools/data-api-differ/internal/dataapi/data.go @@ -0,0 +1,134 @@ +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" +) + +// 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: pointer.To(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..5482272ac47 --- /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 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..ab541f9f059 --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ.go @@ -0,0 +1,29 @@ +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..be9c406b520 --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_constants_test.go @@ -0,0 +1,273 @@ +package differ + +import ( + "testing" + + "github.com/hashicorp/pandora/tools/data-api-differ/internal/changes" + "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": {}, + } + 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..0ac22152197 --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_fields_test.go @@ -0,0 +1,246 @@ +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_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": { + 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..06c229439ec --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_models_test.go @@ -0,0 +1,234 @@ +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_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": {}, + } + 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..5bb6d40abe1 --- /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..3cf7f3ccc2c --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_operations_test.go @@ -0,0 +1,1093 @@ +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_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": {}, + } + 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..627365468be --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_resource_ids.go @@ -0,0 +1,223 @@ +package differ + +import ( + "fmt" + "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" +) + +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)) + staticIdentifiers := d.staticIdentifiersInResourceIdSegments(updatedData.Segments) + output = append(output, changes.ResourceIdAdded{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ResourceIdName: resourceIdName, + ResourceIdValue: updatedData.Id, + StaticIdentifiersInNewValue: staticIdentifiers, + }) + 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))) + + staticIdentifiersInSegments := d.staticIdentifiersInResourceIdSegments(updated.Segments) + + output = append(output, changes.ResourceIdSegmentsChangedLength{ + ServiceName: serviceName, + ApiVersion: apiVersion, + ResourceName: apiResource, + ResourceIdName: resourceIdName, + OldValue: oldStringified, + NewValue: updatedStringified, + StaticIdentifiersInNewValue: staticIdentifiersInSegments, + }) + 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)) + 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, + 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) + + 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 +} + +// 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{}) + 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..ee90026df24 --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_resource_ids_test.go @@ -0,0 +1,421 @@ +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_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{ + "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) + expected := []changes.Change{ + changes.ResourceIdAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Example", + ResourceIdName: "SomeId", + ResourceIdValue: "/some/resource/id", + StaticIdentifiersInNewValue: []string{ + // this list is ordered + "id", + "resource", + "some", + }, + }, + } + 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"`, + }, + StaticIdentifiersInNewValue: []string{ + "example", + }, + }, + } + 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"`, + StaticIdentifierInNewValue: pointer.To("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"`, + StaticIdentifierInNewValue: pointer.To("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"`, + }, + StaticIdentifiersInNewValue: []string{}, + }, + } + 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..6d00876107c --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_services_test.go @@ -0,0 +1,64 @@ +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_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 + } + 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..07bcb087277 --- /dev/null +++ b/tools/data-api-differ/internal/differ/differ_test.go @@ -0,0 +1,824 @@ +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_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{ + // 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", + 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", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + expected := []changes.Change{ + changes.ResourceIdAdded{ + ServiceName: "Computer", + ApiVersion: "2020-01-01", + ResourceName: "Instances", + ResourceIdName: "First", + ResourceIdValue: "/example", + StaticIdentifiersInNewValue: []string{ + "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..872fa01dc3e --- /dev/null +++ b/tools/data-api-differ/internal/differ/models.go @@ -0,0 +1,28 @@ +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 +} + +func (r Result) ContainsNonBreakingChanges() bool { + for _, change := range r.Changes { + if !change.IsBreaking() { + return true + } + } + + return false +} 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 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..9efc1de2344 --- /dev/null +++ b/tools/data-api-differ/internal/views/breaking_changes.go @@ -0,0 +1,67 @@ +package views + +import ( + "fmt" + "strings" + + "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 trimSpaceAround(` +## Breaking Changes + +No Breaking Changes were found 👍 +`) + } + + 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")) + 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 new file mode 100644 index 00000000000..fe02be35d6c --- /dev/null +++ b/tools/data-api-differ/internal/views/breaking_changes_test.go @@ -0,0 +1,102 @@ +package views + +import ( + "strings" + "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 := "## 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 new file mode 100644 index 00000000000..9b1adada658 --- /dev/null +++ b/tools/data-api-differ/internal/views/changes.go @@ -0,0 +1,116 @@ +package views + +import ( + "fmt" + "strings" + + "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 trimSpaceAround(` +## Summary of Changes + +No Breaking or Non-Breaking Changes were found 👍 +`) + } + + 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(` +## Summary of Changes + +%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") + 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 new file mode 100644 index 00000000000..d89f8a5e5ab --- /dev/null +++ b/tools/data-api-differ/internal/views/changes_test.go @@ -0,0 +1,132 @@ +package views + +import ( + "strings" + "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 := "## 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/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..ab46f2bfbcc --- /dev/null +++ b/tools/data-api-differ/internal/views/resource_id_segments.go @@ -0,0 +1,90 @@ +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")) + 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 new file mode 100644 index 00000000000..bec7aec3870 --- /dev/null +++ b/tools/data-api-differ/internal/views/resource_id_segments_test.go @@ -0,0 +1,168 @@ +package views + +import ( + "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" +) + +// 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 := ` +## 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 new file mode 100644 index 00000000000..0d41dfc1fef --- /dev/null +++ b/tools/data-api-differ/internal/views/view.go @@ -0,0 +1,7 @@ +package views + +type View 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 new file mode 100644 index 00000000000..0e8a91cdcc1 --- /dev/null +++ b/tools/data-api-differ/main.go @@ -0,0 +1,46 @@ +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" +) + +const binaryName = "data-api-differ" + +func main() { + opts := hclog.DefaultOptions + opts.Level = hclog.Info + if level := os.Getenv("LOG_LEVEL"); level != "" { + opts.Level = hclog.LevelFromString(level) + } + 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..") + + c := cli.NewCLI(binaryName, "1.0.0") + c.Args = os.Args[1:] + c.Commands = map[string]cli.CommandFactory{ + "detect-breaking-changes": commands.NewDetectBreakingChangesCommand(), + "detect-changes": commands.NewDetectChangesCommand(), + "output-resource-id-segments": commands.NewOutputResourceIdSegmentsCommand(), + } + + exitStatus, err := c.Run() + if err != nil { + log.Logger.Error(err.Error()) + } + + os.Exit(exitStatus) + return nil +} 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{