diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index eaff415..e4ca88f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,8 +1,8 @@ // For format details, see https://aka.ms/devcontainer.json. { "name": "Codefresh Support Package", - "image": "denoland/deno:2.0.3", - "onCreateCommand": "apt-get update && apt-get install git zip -y", + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "onCreateCommand": "curl -fsSL https://deno.land/install.sh | sh -s -- -y", "customizations": { "vscode": { "settings": { @@ -11,7 +11,8 @@ }, "extensions": [ "denoland.vscode-deno", - "davidanson.vscode-markdownlint" + "davidanson.vscode-markdownlint", + "redhat.vscode-yaml" ] } } diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..c2903ec --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,49 @@ +name: Release + +on: + push: + tags: + - 'v*' # Triggers the workflow on new tags that start with 'v' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Deno + uses: denoland/setup-deno@v2 + with: + deno-version: vx.x.x + + - name: Compile binaries + run: | + sed -i "s/__APP_VERSION__/$TAG_NAME/g" main.js + deno task compile + env: + TAG_NAME: ${{ github.ref_name }} + + - name: Package binaries + run: | + zip ./bin/cf-support_windows_x86_64.zip ./bin/cf-support_windows_x86_64.exe + tar -czvf ./bin/cf-support_darwin_x86_64.tar.gz ./bin/cf-support_darwin_x86_64 + tar -czvf ./bin/cf-support_darwin_arm64.tar.gz ./bin/cf-support_darwin_arm64 + tar -czvf ./bin/cf-support_linux_x86_64.tar.gz ./bin/cf-support_linux_x86_64 + + - name: Create GitHub Release + id: create_release + uses: ncipollo/release-action@v1 + with: + artifacts: | + ./bin/cf-support_windows_x86_64.zip + ./bin/cf-support_darwin_x86_64.tar.gz + ./bin/cf-support_darwin_arm64.tar.gz + ./bin/cf-support_linux_x86_64.tar.gz + token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ github.ref_name }} + name: ${{ github.ref_name }} + body: ${{ github.event.head_commit.message }} + draft: false + prerelease: false diff --git a/CONTRIBUTIONS.md b/CONTRIBUTIONS.md deleted file mode 100644 index bb4a328..0000000 --- a/CONTRIBUTIONS.md +++ /dev/null @@ -1,40 +0,0 @@ -# Contributing to Our Project - -We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: - -- Fixing a bug -- Proposing new features -- Correct misspellings -- etc. - -## We Use [Github Flow](https://docs.github.com/en/get-started/quickstart/github-flow), So All Code Changes Happen Through Pull Requests - -Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests: - -1. Fork the repo and create your branch from `main`. -2. If you've added code that should be tested, add tests. -3. If you've changed APIs, update the documentation. -4. Ensure the test suite passes. -5. Make sure your code lints. -6. Issue that pull request! - -## Any contributions you make will be under the MIT Software License - -In short, when you submit code changes, your submissions are understood to be under the same -[MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if -that's a concern. - -## Development - -- We utilize Dev Containers for development and VS Code as our IDE. - - VSCode extension is `ms-vscode-remote.remote-containers`. - - Docker needs to be installed to run the containers. - - If extensions are stuck on installing utilize `ctrl + shift + p` and utilize `Developer: Reload Window` command. -- 2 spaces for indentation rather than tabs. -- You can try running `deno lint` && `deno fmt` for style unification. - - suggest running this command under `/src`. -- Update [VERSIONS](./VERSION) file when you need a new release version. - -## License - -By contributing, you agree that your contributions will be licensed under its MIT License. diff --git a/README.md b/README.md index 471ea1a..2740232 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # Codefresh Support Package -This project is designed to gather data from Hybrid Runtimes for Codefresh SaaS platform, and Hybrid Runtimes and OnPrem isntallation on the OnPrem Platform. It -collects information about various Kubernetes resources such as Pods, Nodes, Configmaps, Services, and Events. For Classic and OnPrem we gather some informtion -from the platform itself. +This project is designed to gather data from Hybrid Runtimes for Codefresh SaaS platform, and Hybrid Runtimes and OnPrem isntallation on the OnPrem Platform. It collects information about various Kubernetes resources such as Pods, Nodes, Configmaps, Services, and Events. For Classic and OnPrem we gather some informtion from the platform itself. ## Prereqs @@ -15,9 +13,6 @@ from the platform itself. - `CF_URL`: URL of the platform (ex: `https://g.codefresh.io`) - Need an Account Admin Token for Claasic Hybrid Runtime. - Need a System Admin Token for the OnPrem Installation. -- Helm - - Version 3. - - Used to get the helm release version of the installation. - JQ - Used only to get the latest version of the binary for *nix systems. @@ -76,10 +71,3 @@ chmod +x cf-support 1. Go the the [Latest](https://github.com/codefresh-support/codefresh-support-package/releases/latest) release. 1. Download the cf-support_windows_x86_64.zip file 1. Run the `.exe` file via CMD or PowerShell - -## Exit Codes - -- 10 - Failed to get codefresh credentials. Please set the enviroment variables (CF_API_KEY and CF_BASE_URL) or make sure you have a valid codefresh config file. -- 20 - Failed to Create Demo Pipeline / Project or Failed to run Demo Pipeline. -- 30 - Failed to Delete Demo Pipeline / Project -- 40 - Invalid Runtime Type. ex: Selecting On-Prem for a SaaS Account. diff --git a/VERSION b/VERSION deleted file mode 100644 index 1454f6e..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -4.0.1 diff --git a/ci/codefresh.yaml b/ci/codefresh.yaml deleted file mode 100644 index 8e9128c..0000000 --- a/ci/codefresh.yaml +++ /dev/null @@ -1,46 +0,0 @@ -version: '1.0' - -stages: - - Clone - - Build - - Release - -steps: - main_clone: - title: Cloning repository - stage: Clone - type: git-clone - arguments: - repo: ${{CF_REPO_OWNER}}/${{CF_REPO_NAME}} - revision: ${{CF_BRANCH}} - git: codefresh-support-bot - depth: 1 - - build: - title: Compiling - stage: Build - arguments: - image: denoland/deno:alpine-2.0.3 - commands: - - cf_export VERSION=$(cat VERSION) - - deno task compile - - apk add zip - - zip ./bin/cf-support_windows_x86_64 ./bin/cf-support_windows_x86_64.exe - - tar -czvf ./bin/cf-support_darwin_x86_64.tar.gz ./bin/cf-support_darwin_x86_64 - - tar -czvf ./bin/cf-support_darwin_arm64.tar.gz ./bin/cf-support_darwin_arm64 - - tar -czvf ./bin/cf-support_linux_x86_64.tar.gz ./bin/cf-support_linux_x86_64 - - github_release: - title: Publishing release - stage: Release - type: github-release - arguments: - git_context_name: codefresh-support-bot - release_name: ${{VERSION}} - release_description: ${{CF_COMMIT_MESSAGE}} - release_tag: ${{VERSION}} - files: - - ${{CF_VOLUME_PATH}}/${{CF_REPO_NAME}}/bin/cf-support_windows_x86_64.zip - - ${{CF_VOLUME_PATH}}/${{CF_REPO_NAME}}/bin/cf-support_darwin_x86_64.tar.gz - - ${{CF_VOLUME_PATH}}/${{CF_REPO_NAME}}/bin/cf-support_darwin_arm64.tar.gz - - ${{CF_VOLUME_PATH}}/${{CF_REPO_NAME}}/bin/cf-support_linux_x86_64.tar.gz diff --git a/deno.json b/deno.json index e700157..9acc5c4 100644 --- a/deno.json +++ b/deno.json @@ -12,10 +12,10 @@ }, "tasks": { "pre-compile": "rm -rf ./bin && mkdir ./bin", - "compile:linux": "deno compile --config=./deno.json --allow-env --allow-read --allow-write --allow-net --unsafely-ignore-certificate-errors --allow-run --output=./bin/cf-support_linux_x86_64 --target=x86_64-unknown-linux-gnu ./src/main.ts", - "compile:windows": "deno compile --config=./deno.json --allow-env --allow-read --allow-write --allow-net --unsafely-ignore-certificate-errors --allow-run --output=./bin/cf-support_windows_x86_64 --target=x86_64-pc-windows-msvc ./src/main.ts", - "compile:apple": "deno compile --config=./deno.json --allow-env --allow-read --allow-write --allow-net --unsafely-ignore-certificate-errors --allow-run --output=./bin/cf-support_darwin_x86_64 --target=x86_64-apple-darwin ./src/main.ts", - "compile:apple_arm64": "deno compile --config=./deno.json --allow-env --allow-read --allow-write --allow-net --unsafely-ignore-certificate-errors --allow-run --output=./bin/cf-support_darwin_arm64 --target=aarch64-apple-darwin ./src/main.ts", + "compile:linux": "deno compile --config=./deno.json --allow-env --allow-read --allow-write --allow-net --unsafely-ignore-certificate-errors --allow-run --output=./bin/cf-support_linux_x86_64 --target=x86_64-unknown-linux-gnu ./main.js", + "compile:windows": "deno compile --config=./deno.json --allow-env --allow-read --allow-write --allow-net --unsafely-ignore-certificate-errors --allow-run --output=./bin/cf-support_windows_x86_64 --target=x86_64-pc-windows-msvc ./main.js", + "compile:apple": "deno compile --config=./deno.json --allow-env --allow-read --allow-write --allow-net --unsafely-ignore-certificate-errors --allow-run --output=./bin/cf-support_darwin_x86_64 --target=x86_64-apple-darwin ./main.js", + "compile:apple_arm64": "deno compile --config=./deno.json --allow-env --allow-read --allow-write --allow-net --unsafely-ignore-certificate-errors --allow-run --output=./bin/cf-support_darwin_arm64 --target=aarch64-apple-darwin ./main.js", "compile": "deno task pre-compile && deno task compile:linux && deno task compile:windows && deno task compile:apple && deno task compile:apple_arm64" }, "imports": { @@ -23,6 +23,7 @@ "@cloudydeno/kubernetes-apis": "jsr:@cloudydeno/kubernetes-apis@^0.5.2", "@cloudydeno/kubernetes-client": "jsr:@cloudydeno/kubernetes-client@^0.7.3", "@fakoua/zip-ts": "jsr:@fakoua/zip-ts@^1.3.1", + "@henrygd/semaphore": "jsr:@henrygd/semaphore@^0.0.2", "@std/encoding": "jsr:@std/encoding@^1.0.5", "@std/yaml": "jsr:@std/yaml@^1.0.5", "pako": "npm:pako@^2.1.0" diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..a4f59ab --- /dev/null +++ b/deno.lock @@ -0,0 +1,109 @@ +{ + "version": "4", + "specifiers": { + "jsr:@cliffy/table@1.0.0-rc.7": "1.0.0-rc.7", + "jsr:@cloudydeno/kubernetes-apis@~0.5.2": "0.5.2", + "jsr:@cloudydeno/kubernetes-client@0.7.3": "0.7.3", + "jsr:@cloudydeno/kubernetes-client@~0.7.3": "0.7.3", + "jsr:@fakoua/zip-ts@^1.3.1": "1.3.1", + "jsr:@henrygd/semaphore@^0.0.2": "0.0.2", + "jsr:@std/assert@~0.218.2": "0.218.2", + "jsr:@std/bytes@^1.0.2": "1.0.4", + "jsr:@std/encoding@^1.0.5": "1.0.5", + "jsr:@std/fmt@~1.0.2": "1.0.3", + "jsr:@std/fs@0.218.2": "0.218.2", + "jsr:@std/path@0.218.2": "0.218.2", + "jsr:@std/path@1.0.6": "1.0.6", + "jsr:@std/path@~0.218.2": "0.218.2", + "jsr:@std/streams@1.0.5": "1.0.5", + "jsr:@std/yaml@1.0.5": "1.0.5", + "jsr:@std/yaml@^1.0.5": "1.0.5", + "npm:pako@^2.1.0": "2.1.0" + }, + "jsr": { + "@cliffy/table@1.0.0-rc.7": { + "integrity": "9fdd9776eda28a0b397981c400eeb1aa36da2371b43eefe12e6ff555290e3180", + "dependencies": [ + "jsr:@std/fmt" + ] + }, + "@cloudydeno/kubernetes-apis@0.5.2": { + "integrity": "f5fd9c94987f54fde72c6103fa0663ccb71cb6e328e7be44af1d59145adb31ee", + "dependencies": [ + "jsr:@cloudydeno/kubernetes-client@0.7.3" + ] + }, + "@cloudydeno/kubernetes-client@0.7.3": { + "integrity": "eb9fd12ac6c58e7995cc2d41a72ea9d153970900e184a5bd6fa2f2b19da5ec1a", + "dependencies": [ + "jsr:@std/path@1.0.6", + "jsr:@std/streams", + "jsr:@std/yaml@1.0.5" + ] + }, + "@fakoua/zip-ts@1.3.1": { + "integrity": "b66f813015a59f40143c2bac337aed9abac7827da5e39c34549108aa129f33e7", + "dependencies": [ + "jsr:@std/fs", + "jsr:@std/path@0.218.2" + ] + }, + "@henrygd/semaphore@0.0.2": { + "integrity": "cdf678250474b9445648d4fb8d8c636fe313f54893a50de616bf8d3e14f3b51e" + }, + "@std/assert@0.218.2": { + "integrity": "7f0a5a1a8cf86607cd6c2c030584096e1ffad27fc9271429a8cb48cfbdee5eaf" + }, + "@std/bytes@1.0.4": { + "integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc" + }, + "@std/encoding@1.0.5": { + "integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04" + }, + "@std/fmt@1.0.3": { + "integrity": "97765c16aa32245ff4e2204ecf7d8562496a3cb8592340a80e7e554e0bb9149f" + }, + "@std/fs@0.218.2": { + "integrity": "dd9431453f7282e8c577cc22c9e6d036055a9a980b5549f887d6012969fabcca", + "dependencies": [ + "jsr:@std/assert", + "jsr:@std/path@~0.218.2" + ] + }, + "@std/path@0.218.2": { + "integrity": "b568fd923d9e53ad76d17c513e7310bda8e755a3e825e6289a0ce536404e2662", + "dependencies": [ + "jsr:@std/assert" + ] + }, + "@std/path@1.0.6": { + "integrity": "ab2c55f902b380cf28e0eec501b4906e4c1960d13f00e11cfbcd21de15f18fed" + }, + "@std/streams@1.0.5": { + "integrity": "74e5c73d7d68eeab0d7fba3b05cbee3ba4ac5ae37c5f4e675f67e62f8f53edc4", + "dependencies": [ + "jsr:@std/bytes" + ] + }, + "@std/yaml@1.0.5": { + "integrity": "71ba3d334305ee2149391931508b2c293a8490f94a337eef3a09cade1a2a2742" + } + }, + "npm": { + "pako@2.1.0": { + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + } + }, + "workspace": { + "dependencies": [ + "jsr:@cliffy/table@1.0.0-rc.7", + "jsr:@cloudydeno/kubernetes-apis@~0.5.2", + "jsr:@cloudydeno/kubernetes-client@~0.7.3", + "jsr:@fakoua/zip-ts@^1.3.1", + "jsr:@henrygd/semaphore@^0.0.2", + "jsr:@std/encoding@^1.0.5", + "jsr:@std/yaml@^1.0.5", + "npm:pako@^2.1.0" + ] + } +} diff --git a/main.js b/main.js new file mode 100644 index 0000000..e0891d0 --- /dev/null +++ b/main.js @@ -0,0 +1,730 @@ +'use strict'; + +import { autoDetectClient } from '@cloudydeno/kubernetes-client'; +import { AppsV1Api } from '@cloudydeno/kubernetes-apis/apps/v1'; +import { BatchV1Api } from '@cloudydeno/kubernetes-apis/batch/v1'; +import { CoreV1Api } from '@cloudydeno/kubernetes-apis/core/v1'; +import { StorageV1Api } from '@cloudydeno/kubernetes-apis/storage.k8s.io/v1'; +import { ArgoprojIoV1alpha1Api } from '@cloudydeno/kubernetes-apis/argoproj.io/v1alpha1'; +import { ungzip } from 'pako'; +import { compress } from '@fakoua/zip-ts'; +import { parse, stringify as toYaml } from '@std/yaml'; +import { decodeBase64 } from '@std/encoding'; +import { Table } from '@cliffy/table'; +import { getSemaphore } from '@henrygd/semaphore'; + +const VERSION = "__APP_VERSION__"; + +const RuntimeTypes = { + pipelines: 'Pipelines Runtime', + gitops: 'GitOps Runtime', + onprem: 'On-Prem', +}; + +const timestamp = new Date().getTime(); +const dirPath = `./codefresh-support-${timestamp}`; +const numOfProcesses = 5; + +const kubeConfig = await autoDetectClient(); +const appsApi = new AppsV1Api(kubeConfig); +const coreApi = new CoreV1Api(kubeConfig); +const storageApi = new StorageV1Api(kubeConfig); +const batchApi = new BatchV1Api(kubeConfig); +const argoProj = new ArgoprojIoV1alpha1Api(kubeConfig); + +// ############################## +// KUBERNETES +// ############################## +export async function selectNamespace() { + const namespaceList = await coreApi.getNamespaceList(); + console.log(''); + namespaceList.items.forEach((namespace, index) => { + console.log(`${index + 1}. ${namespace.metadata?.name}`); + }); + + let selection = Number(prompt('\nWhich Namespace Is Codefresh Installed In? (Number): ')); + while (isNaN(selection) || selection < 1 || selection > namespaceList.items.length) { + console.log('Invalid selection. Please enter a number corresponding to one of the listed namespaces.'); + selection = Number(prompt('\nWhich Namespace Is Codefresh Installed In? (Number): ')); + } + + const namespace = namespaceList.items[selection - 1].metadata?.name; + + if (!namespace) { + throw new Error('Selected namespace is invalid.'); + } + + return namespace; +} + +export function getK8sResources(runtimeType, namespace) { + switch (runtimeType) { + case RuntimeTypes.pipelines: + return { + 'CronJobs': () => batchApi.namespace(namespace).getCronJobList(), + 'Jobs': () => batchApi.namespace(namespace).getJobList(), + 'Deployments': () => appsApi.namespace(namespace).getDeploymentList(), + 'Daemonsets': () => appsApi.namespace(namespace).getDaemonSetList(), + 'Nodes': () => coreApi.getNodeList(), + 'Volumes': () => coreApi.getPersistentVolumeList({ labelSelector: 'io.codefresh.accountName' }), + 'Volumeclaims': () => + coreApi.namespace(namespace).getPersistentVolumeClaimList({ labelSelector: 'io.codefresh.accountName' }), + 'Configmaps': () => + coreApi.namespace(namespace).getConfigMapList({ labelSelector: 'app.kubernetes.io/name=cf-runtime' }), + 'Services': () => coreApi.namespace(namespace).getServiceList(), + 'Pods': () => coreApi.namespace(namespace).getPodList(), + 'Storageclass': () => storageApi.getStorageClassList(), + 'Events': () => coreApi.namespace(namespace).getEventList(), + 'HelmReleases': () => coreApi.namespace(namespace).getSecretList({ labelSelector: 'owner=helm' }), + }; + case RuntimeTypes.gitops: + return { + 'Apps': () => argoProj.namespace(namespace).getApplicationList(), + 'AppSets': () => argoProj.namespace(namespace).getApplicationSetList(), + 'CronJobs': () => batchApi.namespace(namespace).getCronJobList(), + 'Jobs': () => batchApi.namespace(namespace).getJobList(), + 'Deployments': () => appsApi.namespace(namespace).getDeploymentList(), + 'Daemonsets': () => appsApi.namespace(namespace).getDaemonSetList(), + 'Statefulsets': () => appsApi.namespace(namespace).getStatefulSetList(), + 'Nodes': () => coreApi.getNodeList(), + 'Configmaps': () => coreApi.namespace(namespace).getConfigMapList(), + 'Services': () => coreApi.namespace(namespace).getServiceList(), + 'Pods': () => coreApi.namespace(namespace).getPodList(), + 'Events': () => coreApi.namespace(namespace).getEventList(), + 'HelmReleases': () => coreApi.namespace(namespace).getSecretList({ labelSelector: 'owner=helm' }), + }; + case RuntimeTypes.onprem: + return { + 'CronJobs': () => batchApi.namespace(namespace).getCronJobList(), + 'Jobs': () => batchApi.namespace(namespace).getJobList(), + 'Deployments': () => appsApi.namespace(namespace).getDeploymentList(), + 'Daemonsets': () => appsApi.namespace(namespace).getDaemonSetList(), + 'Nodes': () => coreApi.getNodeList(), + 'Volumes': () => coreApi.getPersistentVolumeList({ labelSelector: 'io.codefresh.accountName' }), + 'Volumeclaims': () => + coreApi.namespace(namespace).getPersistentVolumeClaimList({ labelSelector: 'io.codefresh.accountName' }), + 'Configmaps': () => coreApi.namespace(namespace).getConfigMapList(), + 'Services': () => coreApi.namespace(namespace).getServiceList(), + 'Pods': () => coreApi.namespace(namespace).getPodList(), + 'Storageclass': () => storageApi.getStorageClassList(), + 'Events': () => coreApi.namespace(namespace).getEventList(), + 'HelmReleases': () => coreApi.namespace(namespace).getSecretList({ labelSelector: 'owner=helm' }), + }; + default: + console.error('Invalid runtime type selected'); + return; + } +} + +function calculateAge(creationTimestamp) { + const now = new Date(); + const diffMs = now.getTime() - creationTimestamp.getTime(); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + const diffHours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)); + return `${diffDays}d ${diffHours}h ${diffMinutes}m`; +} + +export function getFormattedEvents(events) { + // Sort the events by .metadata.creationTimestamp + const sortedEvents = events.items.sort((a, b) => { + const dateA = a.metadata.creationTimestamp ? new Date(a.metadata.creationTimestamp).getTime() : 0; + const dateB = b.metadata.creationTimestamp ? new Date(b.metadata.creationTimestamp).getTime() : 0; + return dateA - dateB; + }); + + const formattedEvents = sortedEvents.length > 0 + ? sortedEvents.map((event) => { + const lastSeen = event.lastTimestamp ? calculateAge(event.lastTimestamp) : 'N/A'; + const type = event.type ?? 'N/A'; + const reason = event.reason ?? 'N/A'; + const kind = event.involvedObject.kind ?? 'N/A'; + const name = event.involvedObject.name ?? 'N/A'; + const message = event.message ?? 'N/A'; + return { lastSeen, type, reason, kind, name, message }; + }) + : [{ lastSeen: 'N/A', type: 'N/A', reason: 'N/A', kind: 'N/A', name: 'N/A', message: 'N/A' }]; + + const table = new Table(); + table.fromJson(formattedEvents); + return table.toString(); +} + +export function getHelmReleases(secrets) { + const helmReleases = secrets.items.map((secret) => { + const releaseData = secret.data?.release; + if (!releaseData) { + throw new Error('Release data is undefined'); + } + const firstDecodedData = decodeBase64(releaseData); + const secondDecodedData = decodeBase64(new TextDecoder().decode(firstDecodedData)); + const extractedData = JSON.parse(ungzip(secondDecodedData, { to: 'string' })); + + const helmInfo = { + 'name': extractedData.name, + 'namespace': extractedData.namespace, + 'revision': extractedData.version, + 'updated': extractedData.info.last_deployed, + 'status': extractedData.info.status, + 'chart': `${extractedData.chart.metadata.name}-${extractedData.chart.metadata.version}`, + 'appVersion': extractedData.chart.metadata.appVersion, + }; + return helmInfo; + }); + + return helmReleases; +} + +// TODO: convert using the kubernetes sdk +export async function describeK8sResources(resourceType, namespace, name) { + const describe = new Deno.Command('kubectl', { + args: ['describe', resourceType.toLowerCase(), '-n', namespace, name], + }); + return new TextDecoder().decode((await describe.output()).stdout); +} + +export async function getK8sLogs(namespace, podName, containerName) { + try { + const logs = await coreApi.namespace(namespace).getPodLog(podName, { + container: containerName, + timestamps: true, + }); + return logs; + } catch (error) { + return error.message; + } +} + +export function getPodList(pods) { + const podList = pods.items.length > 0 + ? pods.items.map((pod) => { + const name = pod.metadata?.name ?? 'N/A'; + const ready = `${pod.status?.containerStatuses?.filter((cs) => cs.ready).length ?? 0}/${ + pod.status?.containerStatuses?.length ?? 0 + }`; + const status = pod.status?.phase ?? 'Unknown'; + const restarts = pod.status?.containerStatuses?.reduce((acc, cur) => acc + (cur.restartCount ?? 0), 0) ?? 0; + const age = pod.metadata?.creationTimestamp ? calculateAge(pod.metadata.creationTimestamp) : 'N/A'; + return { name, ready, status, restarts, age }; + }) + : [{ name: 'N/A', ready: 'N/A', status: 'N/A', restarts: 'N/A', age: 'N/A' }]; + const table = new Table(); + table.fromJson(podList); + return table.toString(); +} + +export function getPVCList(Volumeclaims) { + const formattedPVC = Volumeclaims.items.length > 0 + ? Volumeclaims.items.map((pvc) => { + const name = pvc.metadata?.name ?? 'N/A'; + const status = pvc.status?.phase ?? 'Unknown'; + const volume = pvc.spec?.volumeName ?? 'N/A'; + const capacity = `${pvc.spec?.resources?.requests?.storage?.number ?? 'N/A'} ${ + pvc.spec?.resources?.requests?.storage.suffix ?? 'N/A' + }`; + const accessModes = pvc.spec?.accessModes?.join(', ') ?? 'N/A'; + const storageClass = pvc.spec?.storageClassName ?? 'N/A'; + const age = pvc.metadata?.creationTimestamp ? calculateAge(pvc.metadata.creationTimestamp) : 'N/A'; + return { name, status, volume, capacity, accessModes, storageClass, age }; + }) + : [{ + name: 'N/A', + status: 'N/A', + volume: 'N/A', + capacity: 'N/A', + accessModes: 'N/A', + storageClass: 'N/A', + age: 'N/A', + }]; + + const table = new Table(); + table.fromJson(formattedPVC); + return table.toString(); +} + +export function getPVList(Volumes) { + const formattedPV = Volumes.items.length > 0 + ? Volumes.items.map((pv) => { + const name = pv.metadata?.name ?? 'N/A'; + const capacity = `${pv.spec?.capacity?.storage?.number ?? 'N/A'} ${pv.spec?.capacity?.storage.suffix ?? 'N/A'}`; + const accessModes = pv.spec?.accessModes?.join(', ') ?? 'N/A'; + const reclaimPolicy = pv.spec?.persistentVolumeReclaimPolicy ?? 'N/A'; + const status = pv.status?.phase ?? 'Unknown'; + const claim = `${pv.spec?.claimRef?.namespace ?? 'N/A'}/${pv.spec?.claimRef?.name ?? 'N/A'}`; + const storageClass = pv.spec?.storageClassName ?? 'N/A'; + const age = pv.metadata?.creationTimestamp ? calculateAge(pv.metadata.creationTimestamp) : 'N/A'; + return { name, capacity, accessModes, reclaimPolicy, status, claim, storageClass, age }; + }) + : [{ + name: 'N/A', + capacity: 'N/A', + accessModes: 'N/A', + reclaimPolicy: 'N/A', + status: 'N/A', + claim: 'N/A', + storageClass: 'N/A', + age: 'N/A', + }]; + + const table = new Table(); + table.fromJson(formattedPV); + return table.toString(); +} + +// ############################## +// CODEFRESH +// ############################## + +async function getCodefreshCredentials() { + try { + const envToken = Deno.env.get('CF_API_KEY'); + const envUrl = Deno.env.get('CF_BASE_URL'); + + if (envToken && envUrl) { + return { + headers: { Authorization: envToken }, + baseUrl: `${envUrl}/api`, + }; + } + + const configPath = Deno.build.os === 'windows' + ? `${Deno.env.get('USERPROFILE')}/.cfconfig` + : `${Deno.env.get('HOME')}/.cfconfig`; + + const configFileContent = await Deno.readTextFile(configPath); + const config = parse(configFileContent); + + return { + headers: { Authorization: config.contexts[config['current-context']]['token'] }, + baseUrl: `${config.contexts[config['current-context']]['url']}/api`, + }; + } catch (error) { + console.error('Failed to get Codefresh credentials:', error); + console.error( + 'Please set the environment variables (CF_API_KEY and CF_BASE_URL) or make sure you have a valid Codefresh config file.', + ); + Deno.exit(1); + } +} + +function getUserRuntimeSelection() { + const runtimes = Object.values(RuntimeTypes); + + runtimes.forEach((runtimeName, index) => { + console.log(`${index + 1}. ${runtimeName}`); + }); + + let selection = Number(prompt('\nWhich Type Of Runtime Are We Using? (Number):')); + while (isNaN(selection) || selection < 1 || selection > runtimes.length) { + console.log('Invalid selection. Please enter a number corresponding to one of the listed options.'); + selection = Number(prompt('\nWhich Type Of Runtime Are We Using? (Number):')); + } + return runtimes[selection - 1]; +} + +// ############################## +// CODEFRESH PIPELINES +// ############################## +async function getAccountRuntimes(config) { + const response = await fetch(`${config.baseUrl}/runtime-environments`, { + method: 'GET', + headers: config.headers, + }); + const runtimes = await response.json(); + return runtimes; +} + +async function runTestPipeline(config, runtimeName) { + let selection = String( + prompt( + '\nTo troubleshoot, we would like to create a Demo Pipeline and run it.\nAfter creating this pipeline we will clean up the resources\n\nWould you like to proceed with the demo pipeline? (y/n): ', + ), + ); + while (selection !== 'y' && selection !== 'n') { + console.log('Invalid selection. Please enter "y" or "n".'); + selection = String(prompt('\nWould you like to proceed with the demo pipeline? (y/n): ')); + } + if (selection === 'n') { + return; + } + + console.log(`\nCreating a demo pipeline to test the ${runtimeName} runtime.`); + + const projectName = 'codefresh-support-package'; + const pipelineName = 'TEST-PIPELINE-FOR-SUPPORT'; + + const project = JSON.stringify({ + projectName: projectName, + }); + + const pipeline = JSON.stringify({ + version: '1.0', + kind: 'pipeline', + metadata: { + name: `${projectName}/${pipelineName}`, + project: projectName, + originalYamlString: + 'version: "1.0"\n\nsteps:\n\n test:\n title: Running test\n type: freestyle\n arguments:\n image: alpine\n commands:\n - echo "Hello Test"', + }, + spec: { + concurrency: 1, + runtimeEnvironment: { + name: runtimeName, + }, + }, + }); + + const createProjectResponse = await fetch(`${config.baseUrl}/projects`, { + method: 'POST', + headers: { + ...config.headers, + 'Content-Type': 'application/json', + }, + body: project, + }); + + const projectStatus = await createProjectResponse.json(); + + if (!createProjectResponse.ok) { + console.error('Error creating project:', createProjectResponse.statusText); + console.error(projectStatus); + const getProjectID = await fetch(`${config.baseUrl}/projects/name/${projectName}`, { + method: 'GET', + headers: config.headers, + }); + const projectResponse = await getProjectID.json(); + projectStatus.id = projectResponse.id; + } + + const createPipelineResponse = await fetch(`${config.baseUrl}/pipelines`, { + method: 'POST', + headers: { + ...config.headers, + 'Content-Type': 'application/json', + }, + body: pipeline, + }); + + const pipelineStatus = await createPipelineResponse.json(); + + if (!createPipelineResponse.ok) { + console.error('Error creating pipeline:', createPipelineResponse.statusText); + console.error(pipelineStatus); + const getPipelineID = await fetch(`${config.baseUrl}/pipelines/${projectName}%2f${pipelineName}`, { + method: 'GET', + headers: config.headers, + }); + const pipelineResponse = await getPipelineID.json(); + pipelineStatus.metadata = {}; + pipelineStatus.metadata.id = pipelineResponse.metadata.id; + } + + const runPipelineResponse = await fetch(`${config.baseUrl}/pipelines/run/${pipelineStatus.metadata.id}`, { + method: 'POST', + headers: { + ...config.headers, + 'Content-Type': 'application/json', + }, + }); + + const runPipelineStatus = await runPipelineResponse.json(); + + if (!runPipelineResponse.ok) { + console.error('Error running pipeline:', runPipelineResponse.statusText); + console.error(runPipelineStatus); + return { pipelineID: pipelineStatus.metadata.id, projectID: projectStatus.id }; + } + + console.log(`Demo pipeline created and running build with id of ${runPipelineStatus}.`); + + return { pipelineID: pipelineStatus.metadata.id, projectID: projectStatus.id }; +} + +async function deleteTestPipeline(config, pipelineID, projectID) { + const deletePipelineResponse = await fetch(`${config.baseUrl}/pipelines/${pipelineID}`, { + method: 'DELETE', + headers: config.headers, + }); + + if (!deletePipelineResponse.ok) { + console.error('Error deleting pipeline:', await deletePipelineResponse.text()); + Deno.exit(1); + } + + const deleteProjectResponse = await fetch(`${config.baseUrl}/projects/${projectID}`, { + method: 'DELETE', + headers: config.headers, + }); + + if (!deleteProjectResponse.ok) { + console.error('Error deleting project:', await deleteProjectResponse.text()); + Deno.exit(1); + } + + console.log('Demo pipeline and project deleted successfully.'); +} + +async function gatherPipelinesRuntime(config) { + try { + const runtimes = await getAccountRuntimes(config); + console.log(''); + runtimes.forEach((re, index) => { + console.log(`${index + 1}. ${re.metadata.name}`); + }); + + let namespace; + let reSpec; + let pipelineExecutionOutput; + + if (runtimes.length !== 0) { + let selection = Number(prompt('\nWhich Pipelines Runtime Are We Working With? (Number): ')); + while (isNaN(selection) || selection < 1 || selection > runtimes.length) { + console.log('Invalid selection. Please enter a number corresponding to one of the listed runtimes.'); + selection = Number(prompt('\nWhich Pipelines Runtime Are We Working With? (Number): ')); + } + + reSpec = runtimes[selection - 1]; + namespace = reSpec.runtimeScheduler.cluster.namespace; + + pipelineExecutionOutput = await runTestPipeline(config, reSpec.metadata.name); + } else { + console.log('No Pipelines Runtimes found in the account.'); + namespace = await selectNamespace(); + } + + console.log(`\nGathering Data For ${reSpec.metadata.name ?? 'Pipelines Runtime'} in the "${namespace}" namespace.`); + + // Wait 15 seconds to allow the pipeline to run + await new Promise((resolve) => setTimeout(resolve, 15000)); + + await fetchAndSaveData(RuntimeTypes.pipelines, namespace); + + if (reSpec) { + await writeCodefreshFiles(reSpec, 'pipelines-runtime-spec'); + } + + console.log('Data Gathered Successfully.'); + + if (pipelineExecutionOutput) { + await deleteTestPipeline(config, pipelineExecutionOutput?.pipelineID, pipelineExecutionOutput?.projectID); + } + + await prepareAndCleanup(); + } catch (error) { + console.error(`Error gathering Pipelines Runtime data:`, error); + } +} + +// ############################## +// CODEFRESH GITOPS +// ############################## +async function gatherGitopsRuntime() { + try { + const namespace = await selectNamespace(); + console.log(`\nGathering data in "${namespace}" namespace for the GitOps Runtime.`); + await fetchAndSaveData(RuntimeTypes.gitops, namespace); + console.log('\nData Gathered Successfully.'); + await prepareAndCleanup(); + } catch (error) { + console.error(`Error gathering GitOps runtime data:`, error); + Deno.exit(1); + } +} +// ############################## +// CODEFRESH ONPREM +// ############################## +async function getAllAccounts(config) { + const response = await fetch(`${config.baseUrl}/admin/accounts`, { + method: 'GET', + headers: config.headers, + }); + const accounts = await response.json(); + await writeCodefreshFiles(accounts, 'onPrem-accounts'); +} + +async function getAllRuntimes(config) { + const response = await fetch(`${config.baseUrl}/admin/runtime-environments`, { + method: 'GET', + headers: config.headers, + }); + const onPremRuntimes = await response.json(); + await writeCodefreshFiles(onPremRuntimes, 'onPrem-runtimes'); +} + +async function getTotalUsers(config) { + const response = await fetch(`${config.baseUrl}/admin/user?limit=1&page=1`, { + method: 'GET', + headers: config.headers, + }); + const users = await response.json(); + await writeCodefreshFiles({ total: users.total }, 'onPrem-totalUsers'); +} + +async function getSystemFeatureFlags(config) { + const response = await fetch(`${config.baseUrl}/admin/features`, { + method: 'GET', + headers: config.headers, + }); + const onPremSystemFF = await response.json(); + await writeCodefreshFiles(onPremSystemFF, 'onPrem-systemFeatureFlags'); +} + +async function gatherOnPrem(config) { + if (config.baseUrl === 'https://g.codefresh.io/api') { + console.error( + `\nCannot gather On-Prem data for Codefresh SaaS. Please select either ${RuntimeTypes.pipelines} or ${RuntimeTypes.gitops}.`, + ); + console.error( + 'If you need to gather data for Codefresh On-Prem, please update your ./cfconfig conext (or Envs) to point to an On-Prem instance.', + ); + Deno.exit(1); + } + try { + const namespace = await selectNamespace(); + console.log(`\nGathering data in "${namespace}" namespace for Codefresh On-Prem.`); + await fetchAndSaveData(RuntimeTypes.onprem, namespace); + await Promise.all([ + getAllAccounts(config), + getAllRuntimes(config), + getTotalUsers(config), + getSystemFeatureFlags(config), + ]); + console.log('\nData Gathered Successfully.'); + await prepareAndCleanup(); + } catch (error) { + console.error(`Error gathering On-Prem data:`, error); + } +} + +// ############################## +// HELPER FUNCTIONS +// ############################## +async function creatDirectory(path) { + await Deno.mkdir(`${dirPath}/${path}/`, { recursive: true }); +} + +async function writeCodefreshFiles(data, name) { + const filePath = `${dirPath}/${name}.yaml`; + const fileContent = toYaml(data, { skipInvalid: true }); + await Deno.writeTextFile(filePath, fileContent); +} + +async function writeGetApiCalls(resources, path) { + const sem = getSemaphore(path, numOfProcesses); + await Promise.all(resources.map(async (item) => { + await sem.acquire(); + try { + const filePath = `${dirPath}/${path}/${item.metadata.name}_get.yaml`; + const fileContent = toYaml(item, { skipInvalid: true }); + await Deno.writeTextFile(filePath, fileContent); + } finally { + sem.release(); + } + })); +} + +async function prepareAndCleanup() { + console.log(`Saving data to ./codefresh-support-package-${timestamp}.zip`); + await compress(dirPath, `./codefresh-support-package-${timestamp}.zip`, { overwrite: true }); + + console.log('Cleaning up temp directory'); + await Deno.remove(dirPath, { recursive: true }); + + console.log(`\nPlease attach ./codefresh-support-package-${timestamp}.zip to your support ticket.`); +} + +export async function fetchAndSaveData(type, namespace) { + await Deno.mkdir(`${dirPath}/`, { recursive: true }); + + await Deno.writeTextFile(`${dirPath}/cf-support-version.txt`, VERSION); + + for (const [itemType, fetcher] of Object.entries(getK8sResources(type, namespace) || {})) { + const resources = await fetcher(); + + if (itemType === 'Events') { + const formattedEvents = getFormattedEvents(resources); + await Deno.writeTextFile(`${dirPath}/Events.txt`, formattedEvents); + continue; + } + + if (itemType === 'HelmReleases') { + const helmReleases = getHelmReleases(resources); + await writeCodefreshFiles(helmReleases, 'HelmReleases'); + continue; + } + + await creatDirectory(itemType); + + if (itemType === 'Pods') { + const podList = getPodList(resources); + await Deno.writeTextFile(`${dirPath}/PodList.txt`, podList); + + await Promise.all( + resources.items.map(async (resource) => { + const podName = resource.metadata.name; + const containers = resource.spec.containers; + + await Promise.all(containers.map(async (container) => { + const log = await getK8sLogs(namespace, podName, container.name); + const logFileName = `${dirPath}/${itemType}/${podName}_${container.name}_log.log`; + await Deno.writeTextFile(logFileName, log); + })); + }), + ); + } + + if (itemType === 'Volumeclaims') { + const pvcList = getPVCList(resources); + await Deno.writeTextFile(`${dirPath}/VolumeClaimsList.txt`, pvcList); + await writeGetApiCalls(resources.items, itemType); + continue; + } + + if (itemType === 'Volumes') { + const pvList = getPVList(resources); + await Deno.writeTextFile(`${dirPath}/VolumesList.txt`, pvList); + await writeGetApiCalls(resources.items, itemType); + continue; + } + + const sem = getSemaphore(itemType, numOfProcesses); + await Promise.all(resources.items.map(async (resource) => { + await sem.acquire(); + try { + const describeOutput = await describeK8sResources(itemType, namespace, resource.metadata.name); + const describeFileName = `${dirPath}/${itemType}/${resource.metadata.name}_describe.yaml`; + await Deno.writeTextFile(describeFileName, describeOutput); + } finally { + sem.release(); + } + })); + } +} + +// ############################## +// MAIN +// ############################## + +async function main() { + try { + console.log(`App Version: ${VERSION}\n`); + const runtimeSelected = getUserRuntimeSelection(); + const cfConfig = await getCodefreshCredentials(); + + switch (runtimeSelected) { + case RuntimeTypes.pipelines: + await gatherPipelinesRuntime(cfConfig); + break; + case RuntimeTypes.gitops: + await gatherGitopsRuntime(); + break; + case RuntimeTypes.onprem: + await gatherOnPrem(cfConfig); + break; + } + } catch (error) { + console.error(`Error:`, error); + } +} + +await main(); diff --git a/src/codefresh/codefresh.ts b/src/codefresh/codefresh.ts deleted file mode 100644 index 2eb0042..0000000 --- a/src/codefresh/codefresh.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { parse } from '../deps.ts'; - -enum ContextKeys { - Token = 'token', - Url = 'url', -} - -interface Context { - [ContextKeys.Token]: string; - [ContextKeys.Url]: string; -} - -interface CodefreshConfig { - contexts: { - [key: string]: Context; - }; - 'current-context': string; -} - -async function readConfigFile() { - const configPath = Deno.build.os === 'windows' - ? `${Deno.env.get('USERPROFILE')}/.cfconfig` - : `${Deno.env.get('HOME')}/.cfconfig`; - const configFileContent = await Deno.readTextFile(configPath); - return parse(configFileContent) as CodefreshConfig; -} - -async function getCodefreshCredentials(envVar: string, configKey: ContextKeys) { - const envValue = Deno.env.get(envVar); - if (envValue) { - return envValue; - } - - try { - const cfConfig = await readConfigFile(); - return cfConfig.contexts[cfConfig['current-context']][configKey]; - } catch (error) { - console.error('Failed to get Codefresh credentials:', error); - console.error( - 'Please set the environment variables (CF_API_KEY and CF_BASE_URL) or make sure you have a valid Codefresh config file.', - ); - Deno.exit(10); - } -} - -export async function autoDetectCodefreshClient() { - const headers = { - Authorization: await getCodefreshCredentials('CF_API_KEY', ContextKeys.Token), - }; - const baseUrl = `${await getCodefreshCredentials('CF_BASE_URL', ContextKeys.Url)}/api`; - return { headers, baseUrl }; -} diff --git a/src/codefresh/gitops.ts b/src/codefresh/gitops.ts deleted file mode 100644 index 57aff14..0000000 --- a/src/codefresh/gitops.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { fetchAndSaveData, prepareAndCleanup, RuntimeType, selectNamespace } from '../deps.ts'; - -export async function gitopsRuntime() { - try { - const namespace = await selectNamespace(); - console.log(`\nGathering data in "${namespace}" namespace for the GitOps Runtime.`); - await fetchAndSaveData(RuntimeType.gitops, namespace); - console.log('\nData Gathered Successfully.'); - await prepareAndCleanup(); - } catch (error) { - console.error(`Error gathering GitOps runtime data:`, error); - } -} diff --git a/src/codefresh/onprem.ts b/src/codefresh/onprem.ts deleted file mode 100644 index 3a5c8f7..0000000 --- a/src/codefresh/onprem.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { fetchAndSaveData, prepareAndCleanup, RuntimeType, selectNamespace, writeCodefreshFiles } from '../deps.ts'; - -async function getAllAccounts(config: { headers: { Authorization: string }; baseUrl: string }) { - const response = await fetch(`${config.baseUrl}/admin/accounts`, { - method: 'GET', - headers: config.headers, - }); - const accounts = await response.json(); - await writeCodefreshFiles(accounts, 'onPrem-accounts'); -} - -async function getAllRuntimes(config: { headers: { Authorization: string }; baseUrl: string }) { - const response = await fetch(`${config.baseUrl}/admin/runtime-environments`, { - method: 'GET', - headers: config.headers, - }); - const onPremRuntimes = await response.json(); - await writeCodefreshFiles(onPremRuntimes, 'onPrem-runtimes'); -} - -async function getTotalUsers(config: { headers: { Authorization: string }; baseUrl: string }) { - const response = await fetch(`${config.baseUrl}/admin/user?limit=1&page=1`, { - method: 'GET', - headers: config.headers, - }); - const users = await response.json(); - await writeCodefreshFiles({ total: users.total }, 'onPrem-totalUsers'); -} - -async function getSystemFeatureFlags(config: { headers: { Authorization: string }; baseUrl: string }) { - const response = await fetch(`${config.baseUrl}/admin/features`, { - method: 'GET', - headers: config.headers, - }); - const onPremSystemFF = await response.json(); - await writeCodefreshFiles(onPremSystemFF, 'onPrem-systemFeatureFlags'); -} - -export async function onPrem(config: { headers: { Authorization: string }; baseUrl: string }) { - if (config.baseUrl === 'https://g.codefresh.io/api') { - console.error( - `\nCannot gather On-Prem data for Codefresh SaaS. Please select either ${RuntimeType.pipelines} or ${RuntimeType.gitops}.`, - ); - console.error( - 'If you need to gather data for Codefresh On-Prem, please update your ./cfconfig conext (or Envs) to point to an On-Prem instance.', - ); - Deno.exit(40); - } - try { - const namespace = await selectNamespace(); - console.log(`\nGathering data in "${namespace}" namespace for Codefresh On-Prem.`); - await fetchAndSaveData(RuntimeType.onprem, namespace); - await Promise.all([ - getAllAccounts(config), - getAllRuntimes(config), - getTotalUsers(config), - getSystemFeatureFlags(config), - ]); - console.log('\nData Gathered Successfully.'); - await prepareAndCleanup(); - } catch (error) { - console.error(`Error gathering On-Prem data:`, error); - } -} diff --git a/src/codefresh/pipelines.ts b/src/codefresh/pipelines.ts deleted file mode 100644 index d51f717..0000000 --- a/src/codefresh/pipelines.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { fetchAndSaveData, prepareAndCleanup, RuntimeType, writeCodefreshFiles } from '../deps.ts'; - -async function getRuntimes(config: { headers: { Authorization: string }; baseUrl: string }) { - const response = await fetch(`${config.baseUrl}/runtime-environments`, { - method: 'GET', - headers: config.headers, - }); - const runtimes = await response.json(); - return runtimes; -} - -async function runTestPipeline( - config: { headers: { Authorization: string }; baseUrl: string }, - runtimeName: string, -) { - let selection = String( - prompt( - '\nTo troubleshoot, we would like to create a Demo Pipeline and run it.\nAfter creating this pipeline we will clean up the resources\n\nWould you like to proceed with the demo pipeline? (y/n): ', - ), - ); - while (selection !== 'y' && selection !== 'n') { - console.log('Invalid selection. Please enter "y" or "n".'); - selection = String(prompt('\nWould you like to proceed with the demo pipeline? (y/n): ')); - } - if (selection === 'n') { - return; - } - - console.log(`\nCreating a demo pipeline to test the ${runtimeName} runtime.`); - - const project = JSON.stringify({ - projectName: 'codefresh-support-package', - }); - - const pipeline = JSON.stringify({ - version: '1.0', - kind: 'pipeline', - metadata: { - name: 'codefresh-support-package/TEST-PIPELINE-FOR-SUPPORT', - project: 'codefresh-support-package', - originalYamlString: - 'version: "1.0"\n\nsteps:\n\n test:\n title: Running test\n type: freestyle\n arguments:\n image: alpine\n commands:\n - echo "Hello Test"', - }, - spec: { - concurrency: 1, - runtimeEnvironment: { - name: runtimeName, - }, - }, - }); - - const createProjectResponse = await fetch(`${config.baseUrl}/projects`, { - method: 'POST', - headers: { - ...config.headers, - 'Content-Type': 'application/json', - }, - body: project, - }); - - const projectStatus = await createProjectResponse.json(); - - if (!createProjectResponse.ok) { - console.error('Error creating project:', createProjectResponse.statusText); - console.error(projectStatus); - Deno.exit(20); - } - - const createPipelineResponse = await fetch(`${config.baseUrl}/pipelines`, { - method: 'POST', - headers: { - ...config.headers, - 'Content-Type': 'application/json', - }, - body: pipeline, - }); - - const pipelineStatus = await createPipelineResponse.json(); - - if (!createPipelineResponse.ok) { - console.error('Error creating pipeline:', createPipelineResponse.statusText); - console.error(pipelineStatus); - Deno.exit(20); - } - - const runPipelineResponse = await fetch(`${config.baseUrl}/pipelines/run/${pipelineStatus.metadata.id}`, { - method: 'POST', - headers: { - ...config.headers, - 'Content-Type': 'application/json', - }, - }); - - const runPipelineStatus = await runPipelineResponse.json(); - - if (!runPipelineResponse.ok) { - console.error('Error running pipeline:', runPipelineResponse.statusText); - Deno.exit(20); - } - - console.log(`Demo pipeline created and running build with id of ${runPipelineStatus}.`); - - return { pipelineID: pipelineStatus.metadata.id, projectID: projectStatus.id }; -} - -async function deleteTestPipeline( - config: { headers: { Authorization: string }; baseUrl: string }, - pipelineID: string, - projectID: string, -) { - const deletePipelineResponse = await fetch(`${config.baseUrl}/pipelines/${pipelineID}`, { - method: 'DELETE', - headers: config.headers, - }); - - if (!deletePipelineResponse.ok) { - console.error('Error deleting pipeline:', await deletePipelineResponse.text()); - Deno.exit(30); - } - - const deleteProjectResponse = await fetch(`${config.baseUrl}/projects/${projectID}`, { - method: 'DELETE', - headers: config.headers, - }); - - if (!deleteProjectResponse.ok) { - console.error('Error deleting project:', await deleteProjectResponse.text()); - Deno.exit(30); - } - - console.log('Demo pipeline and project deleted successfully.'); -} - -export async function pipelinesRuntime(config: { headers: { Authorization: string }; baseUrl: string }) { - try { - const runtimes = await getRuntimes(config); - console.log(''); - runtimes.forEach((re: any, index: number) => { - console.log(`${index + 1}. ${re.metadata.name}`); - }); - - let selection = Number(prompt('\nWhich Pipelines Runtime Are We Working With? (Number): ')); - while (isNaN(selection) || selection < 1 || selection > runtimes.length) { - console.log('Invalid selection. Please enter a number corresponding to one of the listed runtimes.'); - selection = Number(prompt('\nWhich Pipelines Runtime Are We Working With? (Number): ')); - } - - const reSpec = runtimes[selection - 1]; - const namespace = reSpec.runtimeScheduler.cluster.namespace; - - const pipelineExecutionOutput = await runTestPipeline(config, reSpec.metadata.name); - - console.log(`\nGathering Data For ${reSpec.metadata.name} in the "${namespace}" namespace.`); - - // Wait 15 seconds to allow the pipeline to run - await new Promise((resolve) => setTimeout(resolve, 15000)); - - await fetchAndSaveData(RuntimeType.pipelines, namespace); - await writeCodefreshFiles(reSpec, 'pipelines-runtime-spec'); - console.log('Data Gathered Successfully.'); - - if (pipelineExecutionOutput) { - await deleteTestPipeline(config, pipelineExecutionOutput?.pipelineID, pipelineExecutionOutput?.projectID); - } - - await prepareAndCleanup(); - } catch (error) { - console.error(`Error gathering Pipelines Runtime data:`, error); - } -} diff --git a/src/codefresh/runtime-type.ts b/src/codefresh/runtime-type.ts deleted file mode 100644 index 292be98..0000000 --- a/src/codefresh/runtime-type.ts +++ /dev/null @@ -1,19 +0,0 @@ -export enum RuntimeType { - pipelines = 'Pipelines Runtime', - gitops = 'GitOps Runtime', - onprem = 'On-Prem', -} - -export function getUserRuntimeSelection(runtimes: string[]) { - runtimes.forEach((runtimeName, index) => { - console.log(`${index + 1}. ${runtimeName}`); - }); - - let selection = Number(prompt('\nWhich Type Of Runtime Are We Using? (Number):')); - while (isNaN(selection) || selection < 1 || selection > runtimes.length) { - console.log('Invalid selection. Please enter a number corresponding to one of the listed options.'); - selection = Number(prompt('\nWhich Type Of Runtime Are We Using? (Number):')); - } - - return runtimes[selection - 1]; -} diff --git a/src/deps.ts b/src/deps.ts deleted file mode 100644 index 8fc452d..0000000 --- a/src/deps.ts +++ /dev/null @@ -1,37 +0,0 @@ -export { autoDetectClient } from '@cloudydeno/kubernetes-client'; -export { AppsV1Api } from '@cloudydeno/kubernetes-apis/apps/v1'; -export { BatchV1Api } from '@cloudydeno/kubernetes-apis/batch/v1'; -export { CoreV1Api } from '@cloudydeno/kubernetes-apis/core/v1'; -export type { - EventList, - PersistentVolumeClaimList, - PersistentVolumeList, - PodList, - SecretList, -} from '@cloudydeno/kubernetes-apis/core/v1'; -export { StorageV1Api } from '@cloudydeno/kubernetes-apis/storage.k8s.io/v1'; -export { ArgoprojIoV1alpha1Api } from '@cloudydeno/kubernetes-apis/argoproj.io/v1alpha1'; -export { ungzip } from 'pako'; -export { compress } from '@fakoua/zip-ts'; -export { parse, stringify as toYaml } from '@std/yaml'; -export { decodeBase64 } from '@std/encoding'; -export { Table } from '@cliffy/table'; - -// Internal dependencies -export { getUserRuntimeSelection, RuntimeType } from './codefresh/runtime-type.ts'; -export { autoDetectCodefreshClient } from './codefresh/codefresh.ts'; -export { - describeK8sResources, - getFormattedEvents, - getHelmReleases, - getK8sLogs, - getK8sResources, - getPodList, - getPVCList, - getPVList, - selectNamespace, -} from './kubernetes/kubernetes.ts'; -export { fetchAndSaveData, prepareAndCleanup, writeCodefreshFiles } from './utils/file-io.ts'; -export { gitopsRuntime } from './codefresh/gitops.ts'; -export { onPrem } from './codefresh/onprem.ts'; -export { pipelinesRuntime } from './codefresh/pipelines.ts'; diff --git a/src/kubernetes/kubernetes.ts b/src/kubernetes/kubernetes.ts deleted file mode 100644 index 7bfac5c..0000000 --- a/src/kubernetes/kubernetes.ts +++ /dev/null @@ -1,302 +0,0 @@ -import type { EventList, PersistentVolumeClaimList, PersistentVolumeList, PodList, SecretList } from '../deps.ts'; - -import { - AppsV1Api, - ArgoprojIoV1alpha1Api, - autoDetectClient, - BatchV1Api, - CoreV1Api, - decodeBase64, - RuntimeType, - StorageV1Api, - Table, - ungzip, -} from '../deps.ts'; - -const kubeConfig = await autoDetectClient(); -const appsApi = new AppsV1Api(kubeConfig); -const coreApi = new CoreV1Api(kubeConfig); -const storageApi = new StorageV1Api(kubeConfig); -const batchApi = new BatchV1Api(kubeConfig); -const argoProj = new ArgoprojIoV1alpha1Api(kubeConfig); - -export async function selectNamespace() { - const namespaceList = await coreApi.getNamespaceList(); - console.log(''); - namespaceList.items.forEach((namespace, index: number) => { - console.log(`${index + 1}. ${namespace.metadata?.name}`); - }); - - let selection = Number(prompt('\nWhich Namespace Is Codefresh Installed In? (Number): ')); - while (isNaN(selection) || selection < 1 || selection > namespaceList.items.length) { - console.log('Invalid selection. Please enter a number corresponding to one of the listed namespaces.'); - selection = Number(prompt('\nWhich Namespace Is Codefresh Installed In? (Number): ')); - } - - const namespace = namespaceList.items[selection - 1].metadata?.name; - - if (!namespace) { - throw new Error('Selected namespace is invalid.'); - } - - return namespace; -} - -export function getK8sResources(runtimeType: RuntimeType, namespace: string) { - switch (runtimeType) { - case RuntimeType.pipelines: - return { - 'CronJobs': () => batchApi.namespace(namespace).getCronJobList(), - 'Jobs': () => batchApi.namespace(namespace).getJobList(), - 'Deployments': () => appsApi.namespace(namespace).getDeploymentList(), - 'Daemonsets': () => appsApi.namespace(namespace).getDaemonSetList(), - 'Nodes': () => coreApi.getNodeList(), - 'Volumes': () => coreApi.getPersistentVolumeList({ labelSelector: 'io.codefresh.accountName' }), - 'Volumeclaims': () => - coreApi.namespace(namespace).getPersistentVolumeClaimList({ labelSelector: 'io.codefresh.accountName' }), - 'Configmaps': () => - coreApi.namespace(namespace).getConfigMapList({ labelSelector: 'app.kubernetes.io/name=cf-runtime' }), - 'Services': () => coreApi.namespace(namespace).getServiceList(), - 'Pods': () => coreApi.namespace(namespace).getPodList(), - 'Storageclass': () => storageApi.getStorageClassList(), - 'Events': () => coreApi.namespace(namespace).getEventList(), - 'HelmReleases': () => coreApi.namespace(namespace).getSecretList({ labelSelector: 'owner=helm' }), - }; - case RuntimeType.gitops: - return { - 'Apps': () => argoProj.namespace(namespace).getApplicationList(), - 'AppSets': () => argoProj.namespace(namespace).getApplicationSetList(), - 'CronJobs': () => batchApi.namespace(namespace).getCronJobList(), - 'Jobs': () => batchApi.namespace(namespace).getJobList(), - 'Deployments': () => appsApi.namespace(namespace).getDeploymentList(), - 'Daemonsets': () => appsApi.namespace(namespace).getDaemonSetList(), - 'Statefulsets': () => appsApi.namespace(namespace).getStatefulSetList(), - 'Nodes': () => coreApi.getNodeList(), - 'Configmaps': () => coreApi.namespace(namespace).getConfigMapList(), - 'Services': () => coreApi.namespace(namespace).getServiceList(), - 'Pods': () => coreApi.namespace(namespace).getPodList(), - 'Events': () => coreApi.namespace(namespace).getEventList(), - 'HelmReleases': () => coreApi.namespace(namespace).getSecretList({ labelSelector: 'owner=helm' }), - }; - case RuntimeType.onprem: - return { - 'CronJobs': () => batchApi.namespace(namespace).getCronJobList(), - 'Jobs': () => batchApi.namespace(namespace).getJobList(), - 'Deployments': () => appsApi.namespace(namespace).getDeploymentList(), - 'Daemonsets': () => appsApi.namespace(namespace).getDaemonSetList(), - 'Nodes': () => coreApi.getNodeList(), - 'Volumes': () => coreApi.getPersistentVolumeList({ labelSelector: 'io.codefresh.accountName' }), - 'Volumeclaims': () => - coreApi.namespace(namespace).getPersistentVolumeClaimList({ labelSelector: 'io.codefresh.accountName' }), - 'Configmaps': () => coreApi.namespace(namespace).getConfigMapList(), - 'Services': () => coreApi.namespace(namespace).getServiceList(), - 'Pods': () => coreApi.namespace(namespace).getPodList(), - 'Storageclass': () => storageApi.getStorageClassList(), - 'Events': () => coreApi.namespace(namespace).getEventList(), - 'HelmReleases': () => coreApi.namespace(namespace).getSecretList({ labelSelector: 'owner=helm' }), - }; - default: - console.error('Invalid runtime type selected'); - return; - } -} - -function calculateAge(creationTimestamp: Date) { - const now = new Date(); - const diffMs = now.getTime() - creationTimestamp.getTime(); - const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); - const diffHours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); - const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)); - return `${diffDays}d ${diffHours}h ${diffMinutes}m`; -} - -export function getFormattedEvents(events: EventList) { - // Sort the events by .metadata.creationTimestamp - const sortedEvents = events.items.sort((a, b) => { - const dateA = a.metadata.creationTimestamp ? new Date(a.metadata.creationTimestamp).getTime() : 0; - const dateB = b.metadata.creationTimestamp ? new Date(b.metadata.creationTimestamp).getTime() : 0; - return dateA - dateB; - }); - - const formattedEvents = sortedEvents.length > 0 - ? sortedEvents.map((event) => { - const lastSeen = event.lastTimestamp ? calculateAge(event.lastTimestamp) : 'N/A'; - const type = event.type ?? 'N/A'; - const reason = event.reason ?? 'N/A'; - const kind = event.involvedObject.kind ?? 'N/A'; - const name = event.involvedObject.name ?? 'N/A'; - const message = event.message ?? 'N/A'; - return { - lastSeen, - type, - reason, - kind, - name, - message, - }; - }) - : [{ - lastSeen: 'N/A', - type: 'N/A', - reason: 'N/A', - kind: 'N/A', - name: 'N/A', - message: 'N/A', - }]; - - const table = new Table(); - table.fromJson(formattedEvents); - return table.toString(); -} - -export function getHelmReleases(secrets: SecretList) { - const helmReleases = secrets.items.map((secret) => { - const releaseData = secret.data?.release; - if (!releaseData) { - throw new Error('Release data is undefined'); - } - const firstDecodedData = decodeBase64(releaseData); - const secondDecodedData = decodeBase64(new TextDecoder().decode(firstDecodedData)); - const extractedData = JSON.parse(ungzip(secondDecodedData, { to: 'string' })); - - const helmInfo = { - 'name': extractedData.name, - 'namespace': extractedData.namespace, - 'revision': extractedData.version, - 'updated': extractedData.info.last_deployed, - 'status': extractedData.info.status, - 'chart': `${extractedData.chart.metadata.name}-${extractedData.chart.metadata.version}`, - 'appVersion': extractedData.chart.metadata.appVersion, - }; - return helmInfo; - }); - - return helmReleases; -} - -// TODO: convert using the kubernetes sdk - -export async function describeK8sResources(resourceType: string, namespace: string, name: string) { - const describe = new Deno.Command('kubectl', { - args: ['describe', resourceType.toLowerCase(), '-n', namespace, name], - }); - - return new TextDecoder().decode((await describe.output()).stdout); -} - -export async function getK8sLogs(namespace: string, podName: string, containerName: string) { - try { - const logs = await coreApi.namespace(namespace).getPodLog(podName, { - container: containerName, - timestamps: true, - }); - return logs; - } catch (error) { - return (error as any).message; - } -} - -export function getPodList(pods: PodList) { - const podList = pods.items.length > 0 - ? pods.items.map((pod) => { - const name = pod.metadata?.name ?? 'N/A'; - const ready = `${pod.status?.containerStatuses?.filter((cs) => cs.ready).length ?? 0}/${ - pod.status?.containerStatuses?.length ?? 0 - }`; - const status = pod.status?.phase ?? 'Unknown'; - const restarts = pod.status?.containerStatuses?.reduce((acc, cur) => acc + (cur.restartCount ?? 0), 0) ?? 0; - const age = pod.metadata?.creationTimestamp ? calculateAge(pod.metadata.creationTimestamp) : 'N/A'; - return { - name, - ready, - status, - restarts, - age, - }; - }) - : [{ - name: 'N/A', - ready: 'N/A', - status: 'N/A', - restarts: 'N/A', - age: 'N/A', - }]; - const table = new Table(); - table.fromJson(podList); - return table.toString(); -} - -export function getPVCList(Volumeclaims: PersistentVolumeClaimList) { - const formattedPVC = Volumeclaims.items.length > 0 - ? Volumeclaims.items.map((pvc) => { - const name = pvc.metadata?.name ?? 'N/A'; - const status = pvc.status?.phase ?? 'Unknown'; - const volume = pvc.spec?.volumeName ?? 'N/A'; - const capacity = `${pvc.spec?.resources?.requests?.storage?.number ?? 'N/A'} ${ - pvc.spec?.resources?.requests?.storage.suffix ?? 'N/A' - }`; - const accessModes = pvc.spec?.accessModes?.join(', ') ?? 'N/A'; - const storageClass = pvc.spec?.storageClassName ?? 'N/A'; - const age = pvc.metadata?.creationTimestamp ? calculateAge(pvc.metadata.creationTimestamp) : 'N/A'; - return { - name, - status, - volume, - capacity, - accessModes, - storageClass, - age, - }; - }) - : [{ - name: 'N/A', - status: 'N/A', - volume: 'N/A', - capacity: 'N/A', - accessModes: 'N/A', - storageClass: 'N/A', - age: 'N/A', - }]; - - const table = new Table(); - table.fromJson(formattedPVC); - return table.toString(); -} - -export function getPVList(Volumes: PersistentVolumeList) { - const formattedPV = Volumes.items.length > 0 - ? Volumes.items.map((pv) => { - const name = pv.metadata?.name ?? 'N/A'; - const capacity = `${pv.spec?.capacity?.storage?.number ?? 'N/A'} ${pv.spec?.capacity?.storage.suffix ?? 'N/A'}`; - const accessModes = pv.spec?.accessModes?.join(', ') ?? 'N/A'; - const reclaimPolicy = pv.spec?.persistentVolumeReclaimPolicy ?? 'N/A'; - const status = pv.status?.phase ?? 'Unknown'; - const claim = `${pv.spec?.claimRef?.namespace ?? 'N/A'}/${pv.spec?.claimRef?.name ?? 'N/A'}`; - const storageClass = pv.spec?.storageClassName ?? 'N/A'; - const age = pv.metadata?.creationTimestamp ? calculateAge(pv.metadata.creationTimestamp) : 'N/A'; - return { - name, - capacity, - accessModes, - reclaimPolicy, - status, - claim, - storageClass, - age, - }; - }) - : [{ - name: 'N/A', - capacity: 'N/A', - accessModes: 'N/A', - reclaimPolicy: 'N/A', - status: 'N/A', - claim: 'N/A', - storageClass: 'N/A', - age: 'N/A', - }]; - - const table = new Table(); - table.fromJson(formattedPV); - return table.toString(); -} diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index 20dfa9b..0000000 --- a/src/main.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { - autoDetectCodefreshClient, - getUserRuntimeSelection, - gitopsRuntime, - onPrem, - pipelinesRuntime, - RuntimeType, -} from './deps.ts'; - -async function main() { - try { - const runtimeTypes = Object.values(RuntimeType); - const runtimeSelected = getUserRuntimeSelection(runtimeTypes); - const cfConfig = await autoDetectCodefreshClient(); - - switch (runtimeSelected) { - case RuntimeType.pipelines: - await pipelinesRuntime(cfConfig); - break; - case RuntimeType.gitops: - await gitopsRuntime(); - break; - case RuntimeType.onprem: - await onPrem(cfConfig); - break; - } - } catch (error) { - console.error(`Error:`, error); - } -} - -await main(); diff --git a/src/utils/file-io.ts b/src/utils/file-io.ts deleted file mode 100644 index 50922b9..0000000 --- a/src/utils/file-io.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { - compress, - describeK8sResources, - getFormattedEvents, - getHelmReleases, - getK8sLogs, - getK8sResources, - getPodList, - getPVCList, - getPVList, - RuntimeType, - toYaml, -} from '../deps.ts'; - -const timestamp = new Date().getTime(); -const dirPath = `./codefresh-support-${timestamp}`; - -async function creatDirectory(path: string) { - await Deno.mkdir(`${dirPath}/${path}/`, { recursive: true }); -} - -export async function writeCodefreshFiles(data: any, name: string) { - const filePath = `${dirPath}/${name}.yaml`; - const fileContent = toYaml(data, { skipInvalid: true }); - await Deno.writeTextFile(filePath, fileContent); -} - -async function writeGetApiCalls(resources: any, path: string) { - const writePromises = resources.map(async (item: any) => { - const filePath = `${dirPath}/${path}/${item.metadata.name}_get.yaml`; - const fileContent = toYaml(item, { skipInvalid: true }); - await Deno.writeTextFile(filePath, fileContent); - }); - await Promise.all(writePromises); -} - -export async function prepareAndCleanup() { - console.log(`Saving data to ./codefresh-support-package-${timestamp}.zip`); - await compress(dirPath, `./codefresh-support-package-${timestamp}.zip`, { overwrite: true }); - - console.log('Cleaning up temp directory'); - await Deno.remove(dirPath, { recursive: true }); - - console.log(`\nPlease attach ./codefresh-support-package-${timestamp}.zip to your support ticket.`); -} - -export async function fetchAndSaveData(type: RuntimeType, namespace: string) { - await Deno.mkdir(`${dirPath}/`, { recursive: true }); - - for (const [itemType, fetcher] of Object.entries(getK8sResources(type, namespace) || {})) { - const resources = await fetcher(); - - if (itemType === 'Events') { - const formattedEvents = getFormattedEvents(resources); - await Deno.writeTextFile(`${dirPath}/Events.txt`, formattedEvents); - continue; - } - - if (itemType === 'HelmReleases') { - const helmReleases = getHelmReleases(resources); - await writeCodefreshFiles(helmReleases, 'HelmReleases'); - continue; - } - - await creatDirectory(itemType); - - if (itemType === 'Pods') { - const podList = getPodList(resources); - await Deno.writeTextFile(`${dirPath}/PodList.txt`, podList); - - await Promise.all( - resources.items.map(async (resource: { metadata: { name: string }; spec: { containers: any } }) => { - const podName = resource.metadata.name; - const containers = resource.spec.containers; - - await Promise.all(containers.map(async (container: { name: string }) => { - const log = await getK8sLogs(namespace, podName, container.name); - const logFileName = `${dirPath}/${itemType}/${podName}_${container.name}_log.log`; - await Deno.writeTextFile(logFileName, log); - })); - }), - ); - } - - if (itemType === 'Volumeclaims') { - const pvcList = getPVCList(resources); - await Deno.writeTextFile(`${dirPath}/VolumeClaimsList.txt`, pvcList); - // await writeGetApiCalls(resources.items, itemType); - continue; - } - - if (itemType === 'Volumes') { - const pvList = getPVList(resources); - await Deno.writeTextFile(`${dirPath}/VolumesList.txt`, pvList); - // await writeGetApiCalls(resources.items, itemType); - continue; - } - - await Promise.all(resources.items.map(async (resource: { metadata: { name: string } }) => { - const describeOutput = await describeK8sResources(itemType, namespace, resource.metadata.name); - const describeFileName = `${dirPath}/${itemType}/${resource.metadata.name}_describe.yaml`; - await Deno.writeTextFile(describeFileName, describeOutput); - })); - } -}