Skip to content

Commit

Permalink
feat: Handle Dynatrace Configurations with non-unique names
Browse files Browse the repository at this point in the history
Some Dynatrace APIs/Configurations do not fulfill the monaco assumption of having a unique name. 
For these configurations it is possible or even required to create several configurations with the same name. 

This causes problems for monaco during deployment - being unable to update the correct configuration - and on download - being unable to download all configurations. 

This adds logic of addressing such non-unique-name configurations by their UUID - either from Dynatrace if downloaded, or generated from monaco if first uploaded. 

To stay backward compatible this handling of non-unique APIs is introduced in -v2 configuration folders, which existing configuration will keep functioning in the existing - but wrong if more than one configuration of a name exists - way.

## Commits: 
* feat: Flag APIs that don't work with unique names

* feat: Add function to test and generate UUID

* feat: Allow force updates by entity id

* feat: Allow generating project unique uuids

* feat: Use id json if name not unique

* feat: Read/update config instead of overwriting

* feat: Add deployment handler. add tests

* fix: Skip deleting non-uniquely named entities

* docs: Add info about non-unique name APIs

* fix: Fix several golangci-lint complaints

* feat: Resolve several review sugestions

* feat: Remove uuid generator from project interface

* feat: Mitigate entity duplication with v2 configs

* test: Fix non-nullable attributes

* feat: Flag APIs that don't work with unique names

* feat: Add function to test and generate UUID

* feat: Allow force updates by entity id

* feat: Allow generating project unique uuids

* feat: Use id json if name not unique

* feat: Read/update config instead of overwriting

* feat: Add deployment handler. add tests

* fix: Skip deleting non-uniquely named entities

* docs: Add info about non-unique name APIs

* fix: Fix several golangci-lint complaints

* feat: Resolve several review sugestions

* feat: Remove uuid generator from project interface

* feat: Mitigate entity duplication with v2 configs

* test: Fix non-nullable attributes

* feat: Explicitly test UUID format

* docs: Add deprecation infos

* docs: Fix broken link

* docs: Update documentation/docs/Guides/add_new_api.md

Co-authored-by: Nico Riedmann <[email protected]>

* docs: Update documentation/docs/Guides/add_new_api.md

Co-authored-by: Nico Riedmann <[email protected]>

* feat: Fix comment; Remove obsolete func parameters

* test: Add TestIsDeprecatedApi

* feat: Remove NewIdValue function

* feat: Undo deploy refactoring;

* feat: Reduce calculated cyclomatic complexity

* refactor: Simplify config upload error handling

Move the logic of whether deployment errors of a single config are returned or just appended for later,
out into the main execute method.

This removes the need for executeConfig to make this decision and changes it to just return an error or not.
With that the side-effect of executeConfig modifying the errors slice is removed.

The previous if/else of handling the error is also simplified so that the appending of the new error,
which has to happen in either case happens once, before returning if not explicitly continuing on errors.

Fixes #642, fixes #377, fixes #254, fixes #306

Co-authored-by: Tobi Gremmer <[email protected]>
Co-authored-by: tobigremmer-dt <[email protected]>
  • Loading branch information
3 people authored Jul 14, 2022
1 parent 191458b commit de9304b
Show file tree
Hide file tree
Showing 30 changed files with 1,179 additions and 330 deletions.
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
{
"name": "{{ .name }}",
"enabled": true,
"rules": [
{
"enabled": true,
"fileName": "",
"className": "com.dynatrace.easytravel.servicex",
"matcher": "EQUALS",
"methodRules": [
{
"methodName": "methody",
"returnType": "void"
}
],
"annotations": []
}
],
"queueEntryPoint": false,
"queueEntryPointType": null,
"processGroups": []
}
"name": "{{ .name }}",
"enabled": true,
"rules": [
{
"enabled": true,
"fileName": "",
"className": "com.dynatrace.easytravel.servicex",
"matcher": "EQUALS",
"methodRules": [
{
"methodName": "methody",
"returnType": "void",
"visibility": "PUBLIC"
}
],
"annotations": []
}
],
"queueEntryPoint": false,
"queueEntryPointType": null,
"processGroups": []
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
{
"name": "{{ .name }}",
"enabled": true,
"rules": [
"name": "{{ .name }}",
"enabled": true,
"rules": [
{
"enabled": true,
"className": "AClass",
"methodRules": [
{
"enabled": true,
"className": "AClass",
"methodRules": [
{
"methodName": "AMethod",
"argumentTypes": [
"java.lang.String"
],
"returnType": "void"
}
]
"methodName": "AMethod",
"argumentTypes": [
"java.lang.String"
],
"returnType": "void",
"visibility": "PUBLIC"
}
],
"queueEntryPoint": false
]
}
],
"queueEntryPoint": false
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
{
"name": "{{ .name }}",
"enabled": true,
"rules": [
"name": "{{ .name }}",
"enabled": true,
"rules": [
{
"enabled": true,
"fileName": "",
"className": "com.dynatrace.expamle.enterprise.service.v1.WarpDrive",
"matcher": "EQUALS",
"methodRules": [
{
"enabled": true,
"fileName": "",
"className": "com.dynatrace.expamle.enterprise.service.v1.WarpDrive",
"matcher": "EQUALS",
"methodRules": [
{
"methodName": "enable",
"argumentTypes": [
"com.dynatrace.expamle.enterprise.service.v1.WarpDrive"
],
"returnType": "javax.ws.rs.core.Response"
}
],
"annotations": []
"methodName": "enable",
"argumentTypes": [
"com.dynatrace.expamle.enterprise.service.v1.WarpDrive"
],
"returnType": "javax.ws.rs.core.Response",
"visibility": "PUBLIC"
}
],
"queueEntryPoint": false,
"queueEntryPointType": null,
"processGroups": []
],
"annotations": []
}
],
"queueEntryPoint": false,
"queueEntryPointType": null,
"processGroups": []
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
{
"name": "{{ .name }}",
"enabled": true,
"rules": [
{
"enabled": true,
"fileName": "AFile.js",
"methodRules": [
{
"methodName": "AMethod",
"argumentTypes": [
"java.lang.String"
],
"returnType": "void"
}
]
}
],
"queueEntryPoint": false
}
"name": "{{ .name }}",
"enabled": true,
"rules": [
{
"enabled": true,
"fileName": "AFile.js",
"methodRules": [
{
"methodName": "AMethod",
"argumentTypes": [
"java.lang.String"
],
"returnType": "void",
"visibility": "PUBLIC"
}
]
}
],
"queueEntryPoint": false
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
{
"name": "{{ .name }}",
"enabled": true,
"rules": [
{
"enabled": true,
"fileName": "phpFile",
"fileNameMatcher": "ENDS_WITH",
"className": "",
"matcher": "EQUALS",
"methodRules": [
{
"methodName": "myMethod",
"returnType": "void"
}
],
"annotations": []
}
],
"queueEntryPoint": false,
"queueEntryPointType": null,
"processGroups": []
}
"name": "{{ .name }}",
"enabled": true,
"rules": [
{
"enabled": true,
"fileName": "phpFile",
"fileNameMatcher": "ENDS_WITH",
"className": "",
"matcher": "EQUALS",
"methodRules": [
{
"methodName": "myMethod",
"returnType": "void",
"visibility": "PUBLIC"
}
],
"annotations": []
}
],
"queueEntryPoint": false,
"queueEntryPointType": null,
"processGroups": []
}
25 changes: 25 additions & 0 deletions documentation/docs/Guides/add_new_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Take the following steps to add a new API to Monaco.
"<my-api-folder-name>": {
apiPath: "<path-to-my-api>", // mandatory
isSingleConfigurationApi: <is-single-configuration-api>, // only necessary if API is of single configuration format
isNonUniqueNameApi: <is-non-unique-name-api>, // only necessary if API doesn't have unique name attribute
propertyNameOfGetAllResponse: "<property-name>", // only necessary if API returns no "values" envelope (see below)
},
```
Expand All @@ -84,6 +85,7 @@ Take the following steps to add a new API to Monaco.
| <nobr>`<my-api-folder-name>`</nobr> | The name of the API, also used for the folder name for the configurations. Please take a look at the existing API names to get a feeling for the naming conventions and choose one accordingly.|
| <nobr>`<path-to-my-api>`</nobr> | This path points to your API. Monaco prefixes it with the environment URL to access the configs of your API. |
| <nobr>`<is-single-configuration-api>`</nobr> | Boolean value specifying if an API is of single configuration format (optional, default: *false*). |
| <nobr>`<is-non-unique-name-api>`</nobr> | Boolean value specifying if an API doesn't have a unique name attribute (optional, default: *false*). |
| <nobr>`<property-name>`</nobr> | This names the json property used in the `GET ALL` REST call to return the list of configs. E.g. it would be `extensions`, if the response of your API's `GET ALL` REST call looks like the snippet below|

Expand All @@ -108,3 +110,26 @@ Take the following steps to add a new API to Monaco.
3. Add your API to the [table of supported APIs](../configuration/configTypes_tokenPermissions).
> :rocket: After performing these steps, please create the pull request in the upstream repository to share it with the community!
## Deprecating configuration types

Sometimes it's necessary to deprecate certain configuration types. This could have several reasons such as the DT API being deprecated or breaking functional changes (e.g. *dashboard-v2* resolves name uniqueness challenges of the *dashboard* configuration type).

As a best practise, such configurations are kept in the format: `<deprecated-configuration>-v<version increment>` (e.g. *dashboard-v2*).

In addition, deprecated configs can be flagged as such by specifying *isDeprecatedBy*, e.g.:

```
"<my-deprecated-api-folder-name>": {
apiPath: "<path-to-my-api>", // mandatory
isDeprecatedBy: "<my-new-api-folder-name>" // only necessary if API is deprecated
},
```

Deprecated APIs are handled differently. For example, they are not downloaded anymore (unless explicitly specified by providing the *--downloadSpecificAPI* flag). During deployment a warning message is shown, however resources are still deployed.

### When and when not to deprecate?

As a rule of thumb, the same configuration types shall be re-used when e.g. new features are added. However, if a feature breaks functionality of an existing configuration type it is usually better to create a new vesion increment and deprecate the previously existing one.

For example, more info about the *dashboard*, *request-naming-service*, *app-detection-rule* deprecation can be found [here](./deprecated_migration).
117 changes: 117 additions & 0 deletions documentation/docs/Guides/deprecated_migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
---
sidebar_position: 2
title: Migrating deprecated configuration types
---
This guide shows you how to migrate deprecated configuration types.

## *dashboard*, *request-naming-service*, *app-detection-rule*

Initial *dashboard*, *request-naming-service*, *app-detection-rule* configurations were all affected by conflicts between their DT entities name attributes.

Dashboards for example (same applies to *request-naming-service*, *app-detection-rule*) don't have a unique name within a Dynatrace environment. Unfortunately, Monaco depends on name uniqueness in order to identify resources. In the case of dashboards, this resulted in missed/invalid downloads and conflicts during deployments. The solution to this was generating custom UUIDs based on Monaco configuration metadata. As many advantages this brings, the one downside is that Monaco lost track of already deployed dashboards. A dashboard deployment would therefore result in a redeployment (and duplicating - isn't it ironic) of potentially dozens of dashboards in Dynatrace.

The following guide is referencing *dashboard* configurations. However, the same applies to *request-naming-service* and *app-detection-rule* configurations.

1) Existing *dashboard* configurations usually look similar to this:

*config.yaml*
```
---
config:
- DashboardConfigId: config.json
DashboardConfigId:
- name: Monaco Test
- owner: Monaco User
- isShared: true
```
With *DashboardConfigId* as the user defined key that links configuration details and config.json. *name*, *owner* and *isShared* are custom properties which are subsituted in config.json:
*config.json*
```
{
"dashboardMetadata": {
"dashboardFilter": null,
"name": "{{ .name }}",
"owner": "{{ .owner }}",
"shared": {{ .isShared }},
"tilesNameSize": null
},
"tiles": [
...
]
}
```
In a folder structure similar to this:
```
workdir/
project/
app-detection-rule/
...
dashboard/
config.json
config.yaml
environment.yaml
```
2. Recommended: Since the user defined key (*DashboardConfigId* in our example) is used to automatically generate DT entity ids in version 2, the easiest way to migrate existing configuration is to substitute it with the actual Dynatrace enitity id. Dashboard entity ids can be looked up either via API or UI:
*config.yaml*
```
---
config:
- <DT entity UUID>: config.json
<DT entity UUID>:
- name: Monaco Test
- owner: Monaco User
- isShared: true
```
The configuration is now compatible with version 2 of the dashboard configuration type.
Alternatively: Once a configuration is deprecated and a new version provided, all subsequent downloads create configurations of the new version. Existing configuration is kept, but not updated anymore:
```
workdir/
project/
app-detection-rule-v2/
...
dashboard/
config.json
config.yaml
dashboard-v2/
config.json
config.yaml
environment.yaml
```
Although the newly downloaded *config.yaml* includes valid configuration keys, other custom properties (e.g. owner, ...) are dropped:
*dashboard-v2/config.yaml*
```
---
config:
- <DT entity UUID>: config.json
<DT entity UUID>:
- name: Monaco Test
```
This method however allows us to identify configuration instances by their name property and copy/paste their existing DT entity ids instead of retrieving them by API or UI.
3. In order for Monaco to recognize version 2 configurations as such, the incremental version has to be appended to the config folder, *dashboard* becomes *dashboard-v2*:
```
workdir/
project/
app-detection-rule-v2/
...
dashboard-v2/
config.json
config.yaml
environment.yaml
```
2 changes: 1 addition & 1 deletion documentation/docs/configuration/delete_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ You must specify the API and the `name` (not id) of the configuration to be dele

> :warning: if the same name is used for the new config and the config defined in delete.yaml, then the config will be deleted right after deployment.
> :warning: Due to the nature of single configuration endpoints (i.e. global oppossed to entity configuration), these configurations can not be deleted.
> :warning: Due to the nature of single configuration endpoints (i.e. global oppossed to entity configuration) and non-uniquely named configurations (i.e. *dashboard* and *request-naming-service*) these configurations can not be deleted.
Loading

0 comments on commit de9304b

Please sign in to comment.