From 90f85b995839ed6713ddadd8ffeae4373e009cb9 Mon Sep 17 00:00:00 2001
From: Sebastian Poxhofer <secustor@users.noreply.github.com>
Date: Tue, 26 Jul 2022 10:48:50 +0200
Subject: [PATCH] feat(manager/fleet): implement Rancher Fleet manager (#16138)

* feat(manager/fleet): implement Rancher Fleet manager

* Apply suggestions from code review

Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com>

* Update lib/modules/manager/fleet/extract.spec.ts

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>

* docs(fleet): add tsdocs

* fixup test null checks

* docs: add default filematch

* chore: rename tempDep to dep

* apply change requests

* catch exceptions from yaml-js

* trigger yaml parse error

Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com>
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
---
 lib/modules/manager/api.ts                    |   2 +
 .../fleet/__fixtures__/invalid_fleet.yaml     |  26 +++
 .../fleet/__fixtures__/invalid_gitrepo.yaml   |  19 +++
 .../fleet/__fixtures__/valid_fleet.yaml       |  17 ++
 .../fleet/__fixtures__/valid_gitrepo.yaml     |  21 +++
 lib/modules/manager/fleet/extract.spec.ts     | 152 ++++++++++++++++++
 lib/modules/manager/fleet/extract.ts          | 114 +++++++++++++
 lib/modules/manager/fleet/index.ts            |  10 ++
 lib/modules/manager/fleet/readme.md           |  10 ++
 lib/modules/manager/fleet/types.ts            |  29 ++++
 10 files changed, 400 insertions(+)
 create mode 100644 lib/modules/manager/fleet/__fixtures__/invalid_fleet.yaml
 create mode 100644 lib/modules/manager/fleet/__fixtures__/invalid_gitrepo.yaml
 create mode 100644 lib/modules/manager/fleet/__fixtures__/valid_fleet.yaml
 create mode 100644 lib/modules/manager/fleet/__fixtures__/valid_gitrepo.yaml
 create mode 100644 lib/modules/manager/fleet/extract.spec.ts
 create mode 100644 lib/modules/manager/fleet/extract.ts
 create mode 100644 lib/modules/manager/fleet/index.ts
 create mode 100644 lib/modules/manager/fleet/readme.md
 create mode 100644 lib/modules/manager/fleet/types.ts

diff --git a/lib/modules/manager/api.ts b/lib/modules/manager/api.ts
index cc7ff295fc..e450e35e3d 100644
--- a/lib/modules/manager/api.ts
+++ b/lib/modules/manager/api.ts
@@ -21,6 +21,7 @@ import * as depsEdn from './deps-edn';
 import * as dockerCompose from './docker-compose';
 import * as dockerfile from './dockerfile';
 import * as droneci from './droneci';
+import * as fleet from './fleet';
 import * as flux from './flux';
 import * as fvm from './fvm';
 import * as gitSubmodules from './git-submodules';
@@ -97,6 +98,7 @@ api.set('deps-edn', depsEdn);
 api.set('docker-compose', dockerCompose);
 api.set('dockerfile', dockerfile);
 api.set('droneci', droneci);
+api.set('fleet', fleet);
 api.set('flux', flux);
 api.set('fvm', fvm);
 api.set('git-submodules', gitSubmodules);
diff --git a/lib/modules/manager/fleet/__fixtures__/invalid_fleet.yaml b/lib/modules/manager/fleet/__fixtures__/invalid_fleet.yaml
new file mode 100644
index 0000000000..ed4329b7de
--- /dev/null
+++ b/lib/modules/manager/fleet/__fixtures__/invalid_fleet.yaml
@@ -0,0 +1,26 @@
+defaultNamespace: cert-manager
+helm:
+  chart: cert-manager
+  repo: https://charts.jetstack.io
+  releaseName: cert-manager
+  values:
+    installCRDs: true
+---
+defaultNamespace: logging-system
+helm:
+  chart: logging-operator
+  releaseName: logging-operator
+  version: 3.17.7
+  values:
+---
+defaultNamespace: monitoring
+helm:
+  repo: https://prometheus-community.github.io/helm-charts
+  releaseName: kube-prometheus
+  version: 35.4.2
+---
+defaultNamespace: monitoring
+helm:
+  chart: './charts/example'
+  releaseName: anExample
+  version: 35.4.2
diff --git a/lib/modules/manager/fleet/__fixtures__/invalid_gitrepo.yaml b/lib/modules/manager/fleet/__fixtures__/invalid_gitrepo.yaml
new file mode 100644
index 0000000000..ed6cad3463
--- /dev/null
+++ b/lib/modules/manager/fleet/__fixtures__/invalid_gitrepo.yaml
@@ -0,0 +1,19 @@
+kind: GitRepo
+apiVersion: fleet.cattle.io/v1alpha1
+metadata:
+  name: renovate
+  namespace: fleet-local
+spec:
+  revision: 32.89.2
+  paths:
+    - simple
+---
+kind: GitRepo
+apiVersion: fleet.cattle.io/v1alpha1
+metadata:
+  name: rancher
+  namespace: fleet-local
+spec:
+  repo: https://github.com/rancher/rancher
+  paths:
+    - simple
diff --git a/lib/modules/manager/fleet/__fixtures__/valid_fleet.yaml b/lib/modules/manager/fleet/__fixtures__/valid_fleet.yaml
new file mode 100644
index 0000000000..88126be690
--- /dev/null
+++ b/lib/modules/manager/fleet/__fixtures__/valid_fleet.yaml
@@ -0,0 +1,17 @@
+defaultNamespace: cert-manager
+helm:
+  chart: cert-manager
+  repo: https://charts.jetstack.io
+  releaseName: cert-manager
+  version: v1.8.0
+  values:
+    installCRDs: true
+---
+defaultNamespace: logging-system
+helm:
+  chart: logging-operator
+  repo: "https://kubernetes-charts.banzaicloud.com"
+  releaseName: logging-operator
+  version: 3.17.7
+  values:
+
diff --git a/lib/modules/manager/fleet/__fixtures__/valid_gitrepo.yaml b/lib/modules/manager/fleet/__fixtures__/valid_gitrepo.yaml
new file mode 100644
index 0000000000..418eb075e9
--- /dev/null
+++ b/lib/modules/manager/fleet/__fixtures__/valid_gitrepo.yaml
@@ -0,0 +1,21 @@
+kind: GitRepo
+apiVersion: fleet.cattle.io/v1alpha1
+metadata:
+  name: my-repo
+  namespace: fleet-local
+spec:
+  repo: https://github.com/rancher/fleet-examples
+  revision: v0.3.0
+  paths:
+    - simple
+---
+kind: GitRepo
+apiVersion: fleet.cattle.io/v1alpha1
+metadata:
+  name: renovate
+  namespace: fleet-local
+spec:
+  repo: https://github.com/renovatebot/renovate
+  revision: 32.89.2
+  paths:
+    - simple
diff --git a/lib/modules/manager/fleet/extract.spec.ts b/lib/modules/manager/fleet/extract.spec.ts
new file mode 100644
index 0000000000..2f040fbda9
--- /dev/null
+++ b/lib/modules/manager/fleet/extract.spec.ts
@@ -0,0 +1,152 @@
+import { Fixtures } from '../../../../test/fixtures';
+import { extractPackageFile } from '.';
+
+const validFleetYaml = Fixtures.get('valid_fleet.yaml');
+const inValidFleetYaml = Fixtures.get('invalid_fleet.yaml');
+
+const validGitRepoYaml = Fixtures.get('valid_gitrepo.yaml');
+const invalidGitRepoYaml = Fixtures.get('invalid_gitrepo.yaml');
+
+const configMapYaml = Fixtures.get('configmap.yaml', '../kubernetes');
+
+describe('modules/manager/fleet/extract', () => {
+  afterEach(() => {
+    jest.restoreAllMocks();
+  });
+
+  describe('extractPackageFile()', () => {
+    it('should return null if empty content', () => {
+      const result = extractPackageFile('', 'fleet.yaml');
+
+      expect(result).toBeNull();
+    });
+
+    it('should return null if a unknown manifest is supplied', () => {
+      const result = extractPackageFile(configMapYaml, 'fleet.yaml');
+
+      expect(result).toBeNull();
+    });
+
+    describe('fleet.yaml', () => {
+      it('should return null if content is a malformed YAML', () => {
+        const result = extractPackageFile(
+          `apiVersion: v1
+kind: Fleet
+< `,
+          'fleet.yaml'
+        );
+
+        expect(result).toBeNull();
+      });
+
+      it('should parse valid configuration', () => {
+        const result = extractPackageFile(validFleetYaml, 'fleet.yaml');
+
+        expect(result).not.toBeNull();
+        expect(result?.deps).toMatchObject([
+          {
+            currentValue: 'v1.8.0',
+            datasource: 'helm',
+            depName: 'cert-manager',
+            registryUrls: ['https://charts.jetstack.io'],
+            depType: 'fleet',
+          },
+          {
+            currentValue: '3.17.7',
+            datasource: 'helm',
+            depName: 'logging-operator',
+            registryUrls: ['https://kubernetes-charts.banzaicloud.com'],
+            depType: 'fleet',
+          },
+        ]);
+      });
+
+      it('should parse parse invalid configurations', () => {
+        const result = extractPackageFile(inValidFleetYaml, 'fleet.yaml');
+
+        expect(result).not.toBeNull();
+        expect(result?.deps).toMatchObject([
+          {
+            skipReason: 'no-version',
+            datasource: 'helm',
+            depName: 'cert-manager',
+            registryUrls: ['https://charts.jetstack.io'],
+            depType: 'fleet',
+          },
+          {
+            datasource: 'helm',
+            depName: 'logging-operator',
+            skipReason: 'no-repository',
+            depType: 'fleet',
+          },
+          {
+            datasource: 'helm',
+            skipReason: 'missing-depname',
+            depType: 'fleet',
+          },
+          {
+            datasource: 'helm',
+            depName: './charts/example',
+            skipReason: 'local-chart',
+            depType: 'fleet',
+          },
+        ]);
+      });
+    });
+
+    describe('GitRepo', () => {
+      it('should return null if content is a malformed YAML', () => {
+        const result = extractPackageFile(
+          `apiVersion: v1
+ kind: GitRepo
+ < `,
+          'test.yaml'
+        );
+
+        expect(result).toBeNull();
+      });
+
+      it('should parse valid configuration', () => {
+        const result = extractPackageFile(validGitRepoYaml, 'test.yaml');
+
+        expect(result).not.toBeNull();
+        expect(result?.deps).toMatchObject([
+          {
+            currentValue: 'v0.3.0',
+            datasource: 'git-tags',
+            depName: 'https://github.com/rancher/fleet-examples',
+            depType: 'git_repo',
+            sourceUrl: 'https://github.com/rancher/fleet-examples',
+          },
+          {
+            currentValue: '32.89.2',
+            datasource: 'git-tags',
+            depName: 'https://github.com/renovatebot/renovate',
+            depType: 'git_repo',
+            sourceUrl: 'https://github.com/renovatebot/renovate',
+          },
+        ]);
+      });
+
+      it('should parse invalid configuration', () => {
+        const result = extractPackageFile(invalidGitRepoYaml, 'test.yaml');
+
+        expect(result).not.toBeNull();
+        expect(result?.deps).toMatchObject([
+          {
+            datasource: 'git-tags',
+            depType: 'git_repo',
+            skipReason: 'missing-depname',
+          },
+          {
+            datasource: 'git-tags',
+            depName: 'https://github.com/rancher/rancher',
+            depType: 'git_repo',
+            skipReason: 'no-version',
+            sourceUrl: 'https://github.com/rancher/rancher',
+          },
+        ]);
+      });
+    });
+  });
+});
diff --git a/lib/modules/manager/fleet/extract.ts b/lib/modules/manager/fleet/extract.ts
new file mode 100644
index 0000000000..2716749ae6
--- /dev/null
+++ b/lib/modules/manager/fleet/extract.ts
@@ -0,0 +1,114 @@
+import is from '@sindresorhus/is';
+import { loadAll } from 'js-yaml';
+import { logger } from '../../../logger';
+import { regEx } from '../../../util/regex';
+import { GitTagsDatasource } from '../../datasource/git-tags';
+import { HelmDatasource } from '../../datasource/helm';
+import { checkIfStringIsPath } from '../terraform/util';
+import type { PackageDependency, PackageFile } from '../types';
+import type { FleetFile, FleetFileHelm, GitRepo } from './types';
+
+function extractGitRepo(doc: GitRepo): PackageDependency {
+  const dep: PackageDependency = {
+    depType: 'git_repo',
+    datasource: GitTagsDatasource.id,
+  };
+
+  const repo = doc.spec?.repo;
+  if (!repo) {
+    return {
+      ...dep,
+      skipReason: 'missing-depname',
+    };
+  }
+  dep.sourceUrl = repo;
+  dep.depName = repo;
+
+  const currentValue = doc.spec.revision;
+  if (!currentValue) {
+    return {
+      ...dep,
+      skipReason: 'no-version',
+    };
+  }
+
+  return {
+    ...dep,
+    currentValue,
+  };
+}
+
+function extractFleetFile(doc: FleetFileHelm): PackageDependency {
+  const dep: PackageDependency = {
+    depType: 'fleet',
+    datasource: HelmDatasource.id,
+  };
+
+  if (!doc.chart) {
+    return {
+      ...dep,
+      skipReason: 'missing-depname',
+    };
+  }
+  dep.depName = doc.chart;
+
+  if (!doc.repo) {
+    if (checkIfStringIsPath(doc.chart)) {
+      return {
+        ...dep,
+        skipReason: 'local-chart',
+      };
+    }
+    return {
+      ...dep,
+      skipReason: 'no-repository',
+    };
+  }
+  dep.registryUrls = [doc.repo];
+
+  const currentValue = doc.version;
+  if (!doc.version) {
+    return {
+      ...dep,
+      skipReason: 'no-version',
+    };
+  }
+
+  return {
+    ...dep,
+    currentValue,
+  };
+}
+
+export function extractPackageFile(
+  content: string,
+  packageFile: string
+): PackageFile | null {
+  if (!content) {
+    return null;
+  }
+  const deps: PackageDependency[] = [];
+
+  try {
+    if (regEx('fleet.ya?ml').test(packageFile)) {
+      // TODO: fix me (#9610)
+      const docs = loadAll(content, null, { json: true }) as FleetFile[];
+      const fleetDeps = docs
+        .filter((doc) => is.truthy(doc?.helm))
+        .flatMap((doc) => extractFleetFile(doc.helm));
+
+      deps.push(...fleetDeps);
+    } else {
+      // TODO: fix me (#9610)
+      const docs = loadAll(content, null, { json: true }) as GitRepo[];
+      const gitRepoDeps = docs
+        .filter((doc) => doc.kind === 'GitRepo') // ensure only GitRepo manifests are processed
+        .flatMap((doc) => extractGitRepo(doc));
+      deps.push(...gitRepoDeps);
+    }
+  } catch (err) {
+    logger.error({ error: err, packageFile }, 'Failed to parse fleet YAML');
+  }
+
+  return deps.length ? { deps } : null;
+}
diff --git a/lib/modules/manager/fleet/index.ts b/lib/modules/manager/fleet/index.ts
new file mode 100644
index 0000000000..997a8501ac
--- /dev/null
+++ b/lib/modules/manager/fleet/index.ts
@@ -0,0 +1,10 @@
+import { GitTagsDatasource } from '../../datasource/git-tags';
+import { HelmDatasource } from '../../datasource/helm';
+
+export { extractPackageFile } from './extract';
+
+export const defaultConfig = {
+  fileMatch: ['(^|/)fleet.ya?ml'],
+};
+
+export const supportedDatasources = [GitTagsDatasource.id, HelmDatasource.id];
diff --git a/lib/modules/manager/fleet/readme.md b/lib/modules/manager/fleet/readme.md
new file mode 100644
index 0000000000..606e240030
--- /dev/null
+++ b/lib/modules/manager/fleet/readme.md
@@ -0,0 +1,10 @@
+Can upgrade bundle definitions and GitRepo YAML manifests of Rancher Fleet.
+
+By default, only bundles with Helm references will be upgraded.
+To enable GitRepo updates you have to extend your [`fileMatch`](https://docs.renovatebot.com/configuration-options/#filematch) configuration.
+
+```json
+{
+  "fileMatch": ["'(^|/)fleet.ya?ml", "myGitRepoManifests\\.yaml"]
+}
+```
diff --git a/lib/modules/manager/fleet/types.ts b/lib/modules/manager/fleet/types.ts
new file mode 100644
index 0000000000..01451edd8e
--- /dev/null
+++ b/lib/modules/manager/fleet/types.ts
@@ -0,0 +1,29 @@
+/**
+  Represent a GitRepo Kubernetes manifest of Fleet.
+  @link https://fleet.rancher.io/gitrepo-add/#create-gitrepo-instance
+ */
+export interface GitRepo {
+  metadata: {
+    name: string;
+  };
+  kind: string;
+  spec: {
+    repo: string;
+    revision?: string;
+  };
+}
+
+/**
+ Represent a Bundle configuration of Fleet, which is located in `fleet.yaml` files.
+ @link https://fleet.rancher.io/gitrepo-structure/#fleetyaml
+ */
+export interface FleetFile {
+  helm: FleetFileHelm;
+}
+
+export interface FleetFileHelm {
+  chart: string;
+  repo?: string;
+  version: string;
+  releaseName: string;
+}
-- 
GitLab