From a86885a81b4eb406544f5fbf4e6ffbb8ffee0969 Mon Sep 17 00:00:00 2001
From: Jamie Magee <jamie.magee@gmail.com>
Date: Thu, 6 Oct 2022 09:13:22 -0700
Subject: [PATCH] feat(datasource/kubernetes-api): add kubernetes-api
 datasource (#17420)

---
 data/kubernetes-api.json5                     | 113 ++++++++++++++++++
 lib/modules/datasource/api.ts                 |   2 +
 .../datasource/kubernetes-api/index.spec.ts   |  35 ++++++
 .../datasource/kubernetes-api/index.ts        |  31 +++++
 .../datasource/kubernetes-api/readme.md       |   4 +
 .../manager/kubernetes/extract.spec.ts        |   8 ++
 lib/modules/manager/kubernetes/extract.ts     |   4 +
 lib/modules/manager/kubernetes/index.ts       |   6 +-
 8 files changed, 202 insertions(+), 1 deletion(-)
 create mode 100644 data/kubernetes-api.json5
 create mode 100644 lib/modules/datasource/kubernetes-api/index.spec.ts
 create mode 100644 lib/modules/datasource/kubernetes-api/index.ts
 create mode 100644 lib/modules/datasource/kubernetes-api/readme.md

diff --git a/data/kubernetes-api.json5 b/data/kubernetes-api.json5
new file mode 100644
index 0000000000..027ecda7c3
--- /dev/null
+++ b/data/kubernetes-api.json5
@@ -0,0 +1,113 @@
+{
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-16
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#networkpolicy-v116
+  NetworkPolicy: ['extensions/v1beta1', 'networking.k8s.io/v1'],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#daemonset-v116
+  DaemonSet: ['extensions/v1beta1', 'apps/v1beta2', 'apps/v1'],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#deployment-v116
+  Deployment: ['extensions/v1beta1', 'apps/v1beta1', 'apps/v1beta2', 'apps/v1'],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#statefulset-v116
+  StatefulSet: ['apps/v1beta1', 'apps/v1beta2', 'apps/v1'],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#replicaset-v116
+  ReplicaSet: ['extensions/v1beta1', 'apps/v1beta1', 'apps/v1beta2', 'apps/v1'],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#psp-v116
+  PodSecurityPolicy: ['extensions/v1beta1', 'policy/v1beta1'],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-22
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#webhook-resources-v122
+  MutatingWebhookConfiguration: [
+    'admissionregistration.k8s.io/v1beta1',
+    'admissionregistration.k8s.io/v1',
+  ],
+  ValidatingWebhookConfiguration: [
+    'admissionregistration.k8s.io/v1beta1',
+    'admissionregistration.k8s.io/v1',
+  ],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#customresourcedefinition-v122
+  CustomResourceDefinition: [
+    'apiextensions.k8s.io/v1beta1',
+    'apiextensions.k8s.io/v1',
+  ],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#apiservice-v122
+  APIService: ['apiregistration.k8s.io/v1beta1', 'apiregistration.k8s.io/v1'],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#tokenreview-v122
+  TokenReview: ['authentication.k8s.io/v1beta1', 'authentication.k8s.io/v1'],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#subjectaccessreview-resources-v122
+  LocalSubjectAccessReview: [
+    'authorization.k8s.io/v1beta1',
+    'authorization.k8s.io/v1',
+  ],
+  SelfSubjectAccessReview: [
+    'authorization.k8s.io/v1beta1',
+    'authorization.k8s.io/v1',
+  ],
+  SubjectAccessReview: [
+    'authorization.k8s.io/v1beta1',
+    'authorization.k8s.io/v1',
+  ],
+  SelfSubjectRulesReview: [
+    'authorization.k8s.io/v1beta1',
+    'authorization.k8s.io/v1',
+  ],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#certificatesigningrequest-v122
+  CertificateSigningRequest: [
+    'certificates.k8s.io/v1beta1',
+    'certificates.k8s.io/v1',
+  ],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#lease-v122
+  Lease: ['coordination.k8s.io/v1beta1', 'coordination.k8s.io/v1'],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#ingress-v122
+  Ingress: [
+    'extensions/v1beta1',
+    'networking.k8s.io/v1beta1',
+    'networking.k8s.io/v1',
+  ],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#ingressclass-v122
+  IngressClass: ['networking.k8s.io/v1beta1', 'networking.k8s.io/v1'],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#rbac-resources-v122
+  ClusterRole: [
+    'rbac.authorization.k8s.io/v1beta1',
+    'rbac.authorization.k8s.io/v1',
+  ],
+  ClusterRoleBinding: [
+    'rbac.authorization.k8s.io/v1beta1',
+    'rbac.authorization.k8s.io/v1',
+  ],
+  Role: ['rbac.authorization.k8s.io/v1beta1', 'rbac.authorization.k8s.io/v1'],
+  RoleBinding: [
+    'rbac.authorization.k8s.io/v1beta1',
+    'rbac.authorization.k8s.io/v1',
+  ],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#priorityclass-v122
+  PriorityClass: ['scheduling.k8s.io/v1beta1', 'scheduling.k8s.io/v1'],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#storage-resources-v122
+  CSIDriver: ['storage.k8s.io/v1beta1', 'storage.k8s.io/v1'],
+  CSINode: ['storage.k8s.io/v1beta1', 'storage.k8s.io/v1'],
+  StorageClass: ['storage.k8s.io/v1beta1', 'storage.k8s.io/v1'],
+  VolumeAttachment: ['storage.k8s.io/v1beta1', 'storage.k8s.io/v1'],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-25
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#cronjob-v125
+  CronJob: ['batch/v1beta1', 'batch/v1'],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#endpointslice-v125
+  EndpointSlice: ['discovery.k8s.io/v1beta1', 'discovery.k8s.io/v1'],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#event-v125
+  Event: ['events.k8s.io/v1beta1', 'events.k8s.io/v1'],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#horizontalpodautoscaler-v125
+  HorizontalPodAutoscaler: ['autoscaling/v2beta1', 'autoscaling/v2'],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#poddisruptionbudget-v125
+  PodDisruptionBudget: ['policy/v1beta1', 'policy/v1'],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#runtimeclass-v125
+  RuntimeClass: ['node.k8s.io/v1beta1', 'node.k8s.io/v1'],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-26
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#flowcontrol-resources-v126
+  FlowSchema: [
+    'flowcontrol.apiserver.k8s.io/v1beta1',
+    'flowcontrol.apiserver.k8s.io/v1beta2',
+  ],
+  PriorityLevelConfiguration: [
+    'flowcontrol.apiserver.k8s.io/v1beta1',
+    'flowcontrol.apiserver.k8s.io/v1beta2',
+  ],
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-27
+  // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#csistoragecapacity-v127
+  CSIStorageCapacity: ['storage.k8s.io/v1beta1', 'storage.k8s.io/v1'],
+}
diff --git a/lib/modules/datasource/api.ts b/lib/modules/datasource/api.ts
index 130107de4c..9056d2fd62 100644
--- a/lib/modules/datasource/api.ts
+++ b/lib/modules/datasource/api.ts
@@ -29,6 +29,7 @@ import { HelmDatasource } from './helm';
 import { HermitDatasource } from './hermit';
 import { HexDatasource } from './hex';
 import { JenkinsPluginsDatasource } from './jenkins-plugins';
+import { KubernetesApiDatasource } from './kubernetes-api';
 import { MavenDatasource } from './maven';
 import { NodeDatasource } from './node';
 import { NpmDatasource } from './npm';
@@ -81,6 +82,7 @@ api.set(HelmDatasource.id, new HelmDatasource());
 api.set(HermitDatasource.id, new HermitDatasource());
 api.set(HexDatasource.id, new HexDatasource());
 api.set(JenkinsPluginsDatasource.id, new JenkinsPluginsDatasource());
+api.set(KubernetesApiDatasource.id, new KubernetesApiDatasource());
 api.set(MavenDatasource.id, new MavenDatasource());
 api.set(NodeDatasource.id, new NodeDatasource());
 api.set(NpmDatasource.id, new NpmDatasource());
diff --git a/lib/modules/datasource/kubernetes-api/index.spec.ts b/lib/modules/datasource/kubernetes-api/index.spec.ts
new file mode 100644
index 0000000000..73542d86e2
--- /dev/null
+++ b/lib/modules/datasource/kubernetes-api/index.spec.ts
@@ -0,0 +1,35 @@
+import { getPkgReleases } from '../index';
+import { KubernetesApiDatasource } from '.';
+
+const datasource = KubernetesApiDatasource.id;
+
+describe('modules/datasource/kubernetes-api/index', () => {
+  describe('getReleases', () => {
+    it('returns null for an unknown Kubernetes API type', async () => {
+      const res = await getPkgReleases({ datasource, depName: 'Unknown' });
+      expect(res).toBeNull();
+    });
+
+    it('returns for a known Kubernetes API type', async () => {
+      const res = await getPkgReleases({
+        datasource,
+        depName: 'CSIStorageCapacity',
+      });
+      expect(res).not.toBeNull();
+      expect(res).toStrictEqual({
+        releases: [
+          { version: 'storage.k8s.io/v1beta1' },
+          { version: 'storage.k8s.io/v1' },
+        ],
+      });
+    });
+
+    it('is case sensitive', async () => {
+      const res = await getPkgReleases({
+        datasource,
+        depName: 'csistoragecapacity',
+      });
+      expect(res).toBeNull();
+    });
+  });
+});
diff --git a/lib/modules/datasource/kubernetes-api/index.ts b/lib/modules/datasource/kubernetes-api/index.ts
new file mode 100644
index 0000000000..044a5b88bf
--- /dev/null
+++ b/lib/modules/datasource/kubernetes-api/index.ts
@@ -0,0 +1,31 @@
+import JSON5 from 'json5';
+import dataFiles from '../../../data-files.generated';
+import * as kubernetesApiVersioning from '../../versioning/kubernetes-api';
+import { Datasource } from '../datasource';
+import type { GetReleasesConfig, ReleaseResult } from '../types';
+
+export class KubernetesApiDatasource extends Datasource {
+  static readonly id = 'kubernetes-api';
+  private readonly kubernetesApiVersions: Record<string, string[]>;
+
+  constructor() {
+    super(KubernetesApiDatasource.id);
+    this.kubernetesApiVersions = JSON5.parse(
+      dataFiles.get('data/kubernetes-api.json5')!
+    );
+  }
+
+  override defaultVersioning = kubernetesApiVersioning.id;
+
+  getReleases({
+    packageName,
+  }: GetReleasesConfig): Promise<ReleaseResult | null> {
+    const versions = this.kubernetesApiVersions[packageName];
+    if (versions) {
+      const releases = versions.map((version) => ({ version }));
+      return Promise.resolve({ releases });
+    }
+
+    return Promise.resolve(null);
+  }
+}
diff --git a/lib/modules/datasource/kubernetes-api/readme.md b/lib/modules/datasource/kubernetes-api/readme.md
new file mode 100644
index 0000000000..f9315b7e43
--- /dev/null
+++ b/lib/modules/datasource/kubernetes-api/readme.md
@@ -0,0 +1,4 @@
+Kubernetes API upgrade versions are manually transcribed from the [Kubernetes API deprecation guide][1].
+The Kubernetes API deprecation guide is updated regularly, so this list may be out of date.
+
+[1]: https://kubernetes.io/docs/reference/using-api/deprecation-guide/
diff --git a/lib/modules/manager/kubernetes/extract.spec.ts b/lib/modules/manager/kubernetes/extract.spec.ts
index 897e8dab55..21b63f4bd3 100644
--- a/lib/modules/manager/kubernetes/extract.spec.ts
+++ b/lib/modules/manager/kubernetes/extract.spec.ts
@@ -18,7 +18,9 @@ describe('modules/manager/kubernetes/extract', () => {
       expect(res?.deps).toStrictEqual([
         {
           currentValue: 'v1',
+          datasource: 'kubernetes-api',
           depName: 'ConfigMap',
+          versioning: 'kubernetes-api',
         },
       ]);
     });
@@ -46,11 +48,15 @@ describe('modules/manager/kubernetes/extract', () => {
         },
         {
           currentValue: 'apps/v1',
+          datasource: 'kubernetes-api',
           depName: 'Deployment',
+          versioning: 'kubernetes-api',
         },
         {
           currentValue: 'extensions/v1beta1',
+          datasource: 'kubernetes-api',
           depName: 'DaemonSet',
+          versioning: 'kubernetes-api',
         },
       ]);
     });
@@ -74,7 +80,9 @@ describe('modules/manager/kubernetes/extract', () => {
         },
         {
           currentValue: 'apps/v1',
+          datasource: 'kubernetes-api',
           depName: 'DaemonSet',
+          versioning: 'kubernetes-api',
         },
       ]);
     });
diff --git a/lib/modules/manager/kubernetes/extract.ts b/lib/modules/manager/kubernetes/extract.ts
index 4c98462e26..e5813ea73c 100644
--- a/lib/modules/manager/kubernetes/extract.ts
+++ b/lib/modules/manager/kubernetes/extract.ts
@@ -2,6 +2,8 @@ import is from '@sindresorhus/is';
 import { loadAll } from 'js-yaml';
 import { logger } from '../../../logger';
 import { newlineRegex, regEx } from '../../../util/regex';
+import { KubernetesApiDatasource } from '../../datasource/kubernetes-api';
+import * as kubernetesApiVersioning from '../../versioning/kubernetes-api';
 import { getDep } from '../dockerfile/extract';
 import type { ExtractConfig, PackageDependency, PackageFile } from '../types';
 import type { KubernetesConfiguration } from './types';
@@ -74,5 +76,7 @@ function extractApis(content: string, fileName: string): PackageDependency[] {
     .map((configuration) => ({
       depName: configuration.kind,
       currentValue: configuration.apiVersion,
+      datasource: KubernetesApiDatasource.id,
+      versioning: kubernetesApiVersioning.id,
     }));
 }
diff --git a/lib/modules/manager/kubernetes/index.ts b/lib/modules/manager/kubernetes/index.ts
index 7151ce782c..9e0bb7442e 100644
--- a/lib/modules/manager/kubernetes/index.ts
+++ b/lib/modules/manager/kubernetes/index.ts
@@ -1,5 +1,6 @@
 import { ProgrammingLanguage } from '../../../constants';
 import { DockerDatasource } from '../../datasource/docker';
+import { KubernetesApiDatasource } from '../../datasource/kubernetes-api';
 
 export { extractPackageFile } from './extract';
 
@@ -9,4 +10,7 @@ export const defaultConfig = {
   fileMatch: [],
 };
 
-export const supportedDatasources = [DockerDatasource.id];
+export const supportedDatasources = [
+  DockerDatasource.id,
+  KubernetesApiDatasource.id,
+];
-- 
GitLab