From fb72204a28e664d6dd3e58bda7fc33f533e45bd7 Mon Sep 17 00:00:00 2001
From: Dan Ports <danports@gmail.com>
Date: Fri, 26 Aug 2022 04:44:53 -0700
Subject: [PATCH] feat(datasource): New datasource that pulls database engine
 versions available for use on AWS RDS (#17345)

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
---
 lib/modules/datasource/api.ts                |   2 +
 lib/modules/datasource/aws-rds/index.spec.ts | 157 +++++++++++++++++++
 lib/modules/datasource/aws-rds/index.ts      |  43 +++++
 lib/modules/datasource/aws-rds/readme.md     |  78 +++++++++
 package.json                                 |   1 +
 yarn.lock                                    |  55 +++++++
 6 files changed, 336 insertions(+)
 create mode 100644 lib/modules/datasource/aws-rds/index.spec.ts
 create mode 100644 lib/modules/datasource/aws-rds/index.ts
 create mode 100644 lib/modules/datasource/aws-rds/readme.md

diff --git a/lib/modules/datasource/api.ts b/lib/modules/datasource/api.ts
index 4bb76b0b3c..130107de4c 100644
--- a/lib/modules/datasource/api.ts
+++ b/lib/modules/datasource/api.ts
@@ -1,6 +1,7 @@
 import { AdoptiumJavaDatasource } from './adoptium-java';
 import { ArtifactoryDatasource } from './artifactory';
 import { AwsMachineImageDataSource } from './aws-machine-image';
+import { AwsRdsDataSource } from './aws-rds';
 import { AzurePipelinesTasksDatasource } from './azure-pipelines-tasks';
 import { BitBucketTagsDatasource } from './bitbucket-tags';
 import { CdnJsDatasource } from './cdnjs';
@@ -52,6 +53,7 @@ export default api;
 api.set(AdoptiumJavaDatasource.id, new AdoptiumJavaDatasource());
 api.set(ArtifactoryDatasource.id, new ArtifactoryDatasource());
 api.set(AwsMachineImageDataSource.id, new AwsMachineImageDataSource());
+api.set(AwsRdsDataSource.id, new AwsRdsDataSource());
 api.set(AzurePipelinesTasksDatasource.id, new AzurePipelinesTasksDatasource());
 api.set(BitBucketTagsDatasource.id, new BitBucketTagsDatasource());
 api.set(CdnJsDatasource.id, new CdnJsDatasource());
diff --git a/lib/modules/datasource/aws-rds/index.spec.ts b/lib/modules/datasource/aws-rds/index.spec.ts
new file mode 100644
index 0000000000..8a5015e689
--- /dev/null
+++ b/lib/modules/datasource/aws-rds/index.spec.ts
@@ -0,0 +1,157 @@
+import {
+  DBEngineVersion,
+  DescribeDBEngineVersionsCommand,
+  DescribeDBEngineVersionsCommandOutput,
+  RDSClient,
+} from '@aws-sdk/client-rds';
+import { mockClient } from 'aws-sdk-client-mock';
+import { getPkgReleases } from '..';
+import { AwsRdsDataSource } from '.';
+
+const rdsMock = mockClient(RDSClient);
+
+const version1: DBEngineVersion = {
+  Engine: 'mysql',
+  EngineVersion: '8.0.26',
+  DBParameterGroupFamily: 'mysql8.0',
+  DBEngineDescription: 'MySQL Community Edition',
+  DBEngineVersionDescription: 'MySQL 8.0.26',
+  ValidUpgradeTarget: [
+    {
+      Engine: 'mysql',
+      EngineVersion: '8.0.27',
+      Description: 'MySQL 8.0.27',
+      AutoUpgrade: false,
+      IsMajorVersionUpgrade: false,
+    },
+    {
+      Engine: 'mysql',
+      EngineVersion: '8.0.28',
+      Description: 'MySQL 8.0.28',
+      AutoUpgrade: false,
+      IsMajorVersionUpgrade: false,
+    },
+  ],
+  ExportableLogTypes: ['audit', 'error', 'general', 'slowquery'],
+  SupportsLogExportsToCloudwatchLogs: true,
+  SupportsReadReplica: true,
+  SupportedFeatureNames: [],
+  Status: 'available',
+  SupportsParallelQuery: false,
+  SupportsGlobalDatabases: false,
+  MajorEngineVersion: '8.0',
+  SupportsBabelfish: false,
+};
+
+const version2: DBEngineVersion = {
+  Engine: 'mysql',
+  EngineVersion: '8.0.27',
+  DBParameterGroupFamily: 'mysql8.0',
+  DBEngineDescription: 'MySQL Community Edition',
+  DBEngineVersionDescription: 'MySQL 8.0.27',
+  ValidUpgradeTarget: [
+    {
+      Engine: 'mysql',
+      EngineVersion: '8.0.28',
+      Description: 'MySQL 8.0.28',
+      AutoUpgrade: false,
+      IsMajorVersionUpgrade: false,
+    },
+  ],
+  ExportableLogTypes: ['audit', 'error', 'general', 'slowquery'],
+  SupportsLogExportsToCloudwatchLogs: true,
+  SupportsReadReplica: true,
+  SupportedFeatureNames: [],
+  Status: 'deprecated',
+  SupportsParallelQuery: false,
+  SupportsGlobalDatabases: false,
+  MajorEngineVersion: '8.0',
+  SupportsBabelfish: false,
+};
+
+const version3: DBEngineVersion = {
+  Engine: 'mysql',
+  EngineVersion: '8.0.28',
+  DBParameterGroupFamily: 'mysql8.0',
+  DBEngineDescription: 'MySQL Community Edition',
+  DBEngineVersionDescription: 'MySQL 8.0.28',
+  ValidUpgradeTarget: [],
+  ExportableLogTypes: ['audit', 'error', 'general', 'slowquery'],
+  SupportsLogExportsToCloudwatchLogs: true,
+  SupportsReadReplica: true,
+  SupportedFeatureNames: [],
+  Status: 'available',
+  SupportsParallelQuery: false,
+  SupportsGlobalDatabases: false,
+  MajorEngineVersion: '8.0',
+  SupportsBabelfish: false,
+};
+
+function mockDescribeVersionsCommand(
+  result: DescribeDBEngineVersionsCommandOutput
+): void {
+  rdsMock.on(DescribeDBEngineVersionsCommand).resolves(result);
+}
+
+describe('modules/datasource/aws-rds/index', () => {
+  beforeEach(() => rdsMock.reset());
+
+  describe('getPkgReleases()', () => {
+    it('without returned versions', async () => {
+      mockDescribeVersionsCommand({
+        $metadata: {},
+      });
+      const res = await getPkgReleases({
+        datasource: AwsRdsDataSource.id,
+        depName: '[{"Name":"engine","Values":["mysql"]}]',
+      });
+      expect(res).toBeNull();
+    });
+
+    it('with one deprecated version', async () => {
+      mockDescribeVersionsCommand({
+        $metadata: {},
+        DBEngineVersions: [version2],
+      });
+      const res = await getPkgReleases({
+        datasource: AwsRdsDataSource.id,
+        depName: '[{"Name":"engine","Values":["mysql"]}]',
+      });
+      expect(res).toStrictEqual({
+        releases: [
+          {
+            isDeprecated: true,
+            version: version2.EngineVersion,
+          },
+        ],
+      });
+    });
+
+    it('with 3 matching versions', async () => {
+      mockDescribeVersionsCommand({
+        $metadata: {},
+        DBEngineVersions: [version1, version2, version3],
+      });
+      const res = await getPkgReleases({
+        datasource: AwsRdsDataSource.id,
+        depName: '[{"Name":"engine","Values":["mysql"]}]',
+      });
+      expect(res).toStrictEqual({
+        releases: [
+          {
+            isDeprecated: false,
+            version: version1.EngineVersion,
+          },
+          {
+            isDeprecated: true,
+            version: version2.EngineVersion,
+          },
+          {
+            isDeprecated: false,
+            version: version3.EngineVersion,
+          },
+        ],
+      });
+    });
+  });
+});
diff --git a/lib/modules/datasource/aws-rds/index.ts b/lib/modules/datasource/aws-rds/index.ts
new file mode 100644
index 0000000000..1ad325f5e3
--- /dev/null
+++ b/lib/modules/datasource/aws-rds/index.ts
@@ -0,0 +1,43 @@
+import {
+  DescribeDBEngineVersionsCommand,
+  RDSClient,
+} from '@aws-sdk/client-rds';
+import { cache } from '../../../util/cache/package/decorator';
+import { Lazy } from '../../../util/lazy';
+import { Datasource } from '../datasource';
+import type { GetReleasesConfig, ReleaseResult } from '../types';
+
+export class AwsRdsDataSource extends Datasource {
+  static readonly id = 'aws-rds';
+
+  override readonly caching = true;
+
+  private readonly rds: Lazy<RDSClient>;
+
+  constructor() {
+    super(AwsRdsDataSource.id);
+    this.rds = new Lazy(() => new RDSClient({}));
+  }
+
+  @cache({
+    namespace: `datasource-${AwsRdsDataSource.id}`,
+    key: ({ packageName }: GetReleasesConfig) => `getReleases:${packageName}`,
+  })
+  async getReleases({
+    packageName: serializedFilter,
+  }: GetReleasesConfig): Promise<ReleaseResult | null> {
+    const cmd = new DescribeDBEngineVersionsCommand({
+      Filters: JSON.parse(serializedFilter),
+    });
+    const response = await this.rds.getValue().send(cmd);
+    const versions = response.DBEngineVersions ?? [];
+    return {
+      releases: versions
+        .filter((version) => version.EngineVersion)
+        .map((version) => ({
+          version: version.EngineVersion!,
+          isDeprecated: version.Status === 'deprecated',
+        })),
+    };
+  }
+}
diff --git a/lib/modules/datasource/aws-rds/readme.md b/lib/modules/datasource/aws-rds/readme.md
new file mode 100644
index 0000000000..fa074ab260
--- /dev/null
+++ b/lib/modules/datasource/aws-rds/readme.md
@@ -0,0 +1,78 @@
+This datasource returns the database engine versions available for use on [AWS RDS](https://aws.amazon.com/rds/) via the AWS API.
+Generally speaking, all publicly released database versions are available for use on RDS.
+However, new versions may not be available on RDS for a few weeks or months after their release while AWS tests them.
+In addition, AWS may pull existing versions if serious problems arise during their use.
+
+**AWS API configuration**
+
+Since the datasource uses the AWS SDK for JavaScript, you can configure it like other AWS Tools.
+You can use common AWS configuration options, for example:
+
+- Set the region via the `AWS_REGION` environment variable or your `~/.aws/config` file
+- Provide credentials via the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables or your `~/.aws/credentials` file
+- Select the profile to use via `AWS_PROFILE` environment variable
+
+Read the [AWS Developer Guide - Configuring the SDK for JavaScript](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/configuring-the-jssdk.html) for more information on these configuration options.
+
+The minimal IAM privileges required for this datasource are:
+
+```json
+{
+  "Sid": "AllowDBEngineVersionLookup",
+  "Effect": "Allow",
+  "Action": ["rds:DescribeDBEngineVersions"],
+  "Resource": "*"
+}
+```
+
+Read the [AWS RDS IAM reference](https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonrds.html) for more information.
+
+**Usage**
+
+Because Renovate has no manager for the AWS RDS datasource, you need to help Renovate by configuring the regex manager to identify the RDS dependencies you want updated.
+
+When configuring the regex manager, you have to pass a [filter](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-rds/interfaces/describedbengineversionscommandinput.html#filters) as minified JSON as the `packageName`.
+For example:
+
+```yaml
+# Getting the latest supported MySQL 5.7 version from RDS as a filter would look like:
+
+[
+  {
+    "Name": "engine",
+    "Values": [ "mysql" ]
+  },
+  {
+    "Name": "engine-version",
+    "Values": [ "5.7" ]
+  }
+]
+
+# In order to use it with this datasource, you have to minify it:
+
+[{"Name":"engine","Values":["mysql"]},{"Name":"engine-version","Values":["5.7"]}]
+```
+
+Here's an example of using the regex manager to configure this datasource:
+
+```json
+{
+  "regexManagers": [
+    {
+      "fileMatch": ["\\.yaml$"],
+      "matchStrings": [
+        ".*amiFilter=(?<lookupName>.+?)[ ]*\n[ ]*(?<depName>[a-zA-Z0-9-_:]*)[ ]*?:[ ]*?[\"|']?(?<currentValue>[.\\d]+)[\"|']?.*"
+      ],
+      "datasourceTemplate": "aws-rds"
+    }
+  ]
+}
+```
+
+The configuration above matches every YAML file, and recognizes these lines:
+
+```yaml
+spec:
+  # amiFilter=[{"Name":"engine","Values":["mysql"]},{"Name":"engine-version","Values":["5.7"]}]
+  engineVersion: 5.7.34
+```
diff --git a/package.json b/package.json
index f256fa6adf..f6d2accc14 100644
--- a/package.json
+++ b/package.json
@@ -138,6 +138,7 @@
   "dependencies": {
     "@aws-sdk/client-ec2": "3.112.0",
     "@aws-sdk/client-ecr": "3.112.0",
+    "@aws-sdk/client-rds": "3.112.0",
     "@aws-sdk/client-s3": "3.113.0",
     "@breejs/later": "4.1.0",
     "@cheap-glitch/mi-cron": "1.0.1",
diff --git a/yarn.lock b/yarn.lock
index f6512004a2..038737f046 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -226,6 +226,50 @@
     "@aws-sdk/util-waiter" "3.110.0"
     tslib "^2.3.1"
 
+"@aws-sdk/client-rds@3.112.0":
+  version "3.112.0"
+  resolved "https://registry.yarnpkg.com/@aws-sdk/client-rds/-/client-rds-3.112.0.tgz#3f9d32b20c5d7bf1d0d738d7c17f3d7e70418d91"
+  integrity sha512-A8Q+QTjaV2y5gK+3YGE6D1PLzcCswZPgQv5HjR2u6gw6WlzDu0iYTTFSLcDG8aW7TyS9r3Ugjj2kMYVJy8FIJA==
+  dependencies:
+    "@aws-crypto/sha256-browser" "2.0.0"
+    "@aws-crypto/sha256-js" "2.0.0"
+    "@aws-sdk/client-sts" "3.112.0"
+    "@aws-sdk/config-resolver" "3.110.0"
+    "@aws-sdk/credential-provider-node" "3.112.0"
+    "@aws-sdk/fetch-http-handler" "3.110.0"
+    "@aws-sdk/hash-node" "3.110.0"
+    "@aws-sdk/invalid-dependency" "3.110.0"
+    "@aws-sdk/middleware-content-length" "3.110.0"
+    "@aws-sdk/middleware-host-header" "3.110.0"
+    "@aws-sdk/middleware-logger" "3.110.0"
+    "@aws-sdk/middleware-recursion-detection" "3.110.0"
+    "@aws-sdk/middleware-retry" "3.110.0"
+    "@aws-sdk/middleware-sdk-rds" "3.110.0"
+    "@aws-sdk/middleware-serde" "3.110.0"
+    "@aws-sdk/middleware-signing" "3.110.0"
+    "@aws-sdk/middleware-stack" "3.110.0"
+    "@aws-sdk/middleware-user-agent" "3.110.0"
+    "@aws-sdk/node-config-provider" "3.110.0"
+    "@aws-sdk/node-http-handler" "3.110.0"
+    "@aws-sdk/protocol-http" "3.110.0"
+    "@aws-sdk/smithy-client" "3.110.0"
+    "@aws-sdk/types" "3.110.0"
+    "@aws-sdk/url-parser" "3.110.0"
+    "@aws-sdk/util-base64-browser" "3.109.0"
+    "@aws-sdk/util-base64-node" "3.55.0"
+    "@aws-sdk/util-body-length-browser" "3.55.0"
+    "@aws-sdk/util-body-length-node" "3.55.0"
+    "@aws-sdk/util-defaults-mode-browser" "3.110.0"
+    "@aws-sdk/util-defaults-mode-node" "3.110.0"
+    "@aws-sdk/util-user-agent-browser" "3.110.0"
+    "@aws-sdk/util-user-agent-node" "3.110.0"
+    "@aws-sdk/util-utf8-browser" "3.109.0"
+    "@aws-sdk/util-utf8-node" "3.109.0"
+    "@aws-sdk/util-waiter" "3.110.0"
+    entities "2.2.0"
+    fast-xml-parser "3.19.0"
+    tslib "^2.3.1"
+
 "@aws-sdk/client-s3@3.113.0":
   version "3.113.0"
   resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.113.0.tgz#fc416139a1d1f5a8e07ac6f3d453042c6d84c4f9"
@@ -664,6 +708,17 @@
     "@aws-sdk/util-format-url" "3.110.0"
     tslib "^2.3.1"
 
+"@aws-sdk/middleware-sdk-rds@3.110.0":
+  version "3.110.0"
+  resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-rds/-/middleware-sdk-rds-3.110.0.tgz#ada55b2f7fee4f2291d96c6563cfa166db29f345"
+  integrity sha512-NM5fwxoWZtKhy0cMMgptp2FkmUkHlFkWNpi6m5NDJDq5AP+aCaZR0fBT6HEF5kR3DRZm1Mp+mFa4pRELjETP2Q==
+  dependencies:
+    "@aws-sdk/protocol-http" "3.110.0"
+    "@aws-sdk/signature-v4" "3.110.0"
+    "@aws-sdk/types" "3.110.0"
+    "@aws-sdk/util-format-url" "3.110.0"
+    tslib "^2.3.1"
+
 "@aws-sdk/middleware-sdk-s3@3.110.0":
   version "3.110.0"
   resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.110.0.tgz#069603d33fbc349661facb0aaa131a95263e1b88"
-- 
GitLab