Skip to content

Commit

Permalink
fix: register cli tool (#78)
Browse files Browse the repository at this point in the history
* fix: register cli tool

Signed-off-by: lstocchi <lstocchi@redhat.com>

* fix: add tests

Signed-off-by: lstocchi <lstocchi@redhat.com>

---------

Signed-off-by: lstocchi <lstocchi@redhat.com>
  • Loading branch information
lstocchi authored Jul 25, 2024
1 parent 3782297 commit 52c33f4
Show file tree
Hide file tree
Showing 5 changed files with 1,525 additions and 7 deletions.
134 changes: 134 additions & 0 deletions src/download.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

import * as fs from 'node:fs';
import * as path from 'node:path';
import { beforeEach } from 'node:test';

import type * as extensionApi from '@podman-desktop/api';
import { afterEach, expect, test, vi } from 'vitest';

import type { MinikubeGithubReleaseArtifactMetadata } from './download';
import { MinikubeDownload } from './download';
import type { Octokit } from '@octokit/rest';

// Create the OS class as well as fake extensionContext
const extensionContext: extensionApi.ExtensionContext = {
storagePath: '/fake/path',
subscriptions: [],
} as unknown as extensionApi.ExtensionContext;

// We are also testing fs, but we need fs for reading the JSON file, so we will use "vi.importActual"
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const fsActual = await vi.importActual<typeof import('node:fs')>('node:fs');

const releases: MinikubeGithubReleaseArtifactMetadata[] = [
JSON.parse(
fsActual.readFileSync(path.resolve(__dirname, '../tests/resources/minikube-github-release-all.json'), 'utf8'),
),
].map((release: { name: string; tag_name: string; id: number }) => {
return {
label: release.name || release.tag_name,
tag: release.tag_name,
id: release.id,
};
});

const listReleaseAssetsMock = vi.fn();
const listReleasesMock = vi.fn();
const getReleaseAssetMock = vi.fn();
const octokitMock: Octokit = {
repos: {
listReleases: listReleasesMock,
listReleaseAssets: listReleaseAssetsMock,
getReleaseAsset: getReleaseAssetMock,
},
} as unknown as Octokit;

beforeEach(() => {
vi.resetAllMocks();
});

afterEach(() => {
vi.resetAllMocks();
vi.restoreAllMocks();
});

test('expect getLatestVersionAsset to return the first release from a list of releases', async () => {
// Expect the test to return the first release from the list (as the function simply returns the first one)
const minikubeDownload = new MinikubeDownload(extensionContext, octokitMock);
vi.spyOn(minikubeDownload, 'grabLatestsReleasesMetadata').mockResolvedValue(releases);
const result = await minikubeDownload.getLatestVersionAsset();
expect(result).toBeDefined();
expect(result).toEqual(releases[0]);
});

test('get release asset id should return correct id', async () => {
const resultREST = JSON.parse(
fsActual.readFileSync(path.resolve(__dirname, '../tests/resources/minikube-github-release-assets.json'), 'utf8'),
);

listReleaseAssetsMock.mockImplementation(() => {
return { data: resultREST };
});

const minikubeDownload = new MinikubeDownload(extensionContext, octokitMock);
const assetId = await minikubeDownload.getReleaseAssetId(167707968, 'linux', 'x64');

expect(assetId).equals(167708030);
});

test('throw if there is no release asset for that os and arch', async () => {
const resultREST = JSON.parse(
fsActual.readFileSync(path.resolve(__dirname, '../tests/resources/minikube-github-release-assets.json'), 'utf8'),
);

listReleaseAssetsMock.mockImplementation(() => {
return { data: resultREST };
});

const minikubeDownload = new MinikubeDownload(extensionContext, octokitMock);
await expect(minikubeDownload.getReleaseAssetId(167707968, 'windows', 'x64')).rejects.toThrowError(
'No asset found for windows and amd64',
);
});

test('test download of minikube passes and that mkdir and executable mocks are called', async () => {
const minikubeDownload = new MinikubeDownload(extensionContext, octokitMock);

vi.spyOn(minikubeDownload, 'getReleaseAssetId').mockResolvedValue(167707925);
vi.spyOn(minikubeDownload, 'downloadReleaseAsset').mockResolvedValue();
vi.spyOn(minikubeDownload, 'makeExecutable').mockResolvedValue();
const makeExecutableMock = vi.spyOn(minikubeDownload, 'makeExecutable');
const mkdirMock = vi.spyOn(fs.promises, 'mkdir');

// Mock that the storage path does not exist
vi.mock('node:fs');
vi.spyOn(fs, 'existsSync').mockImplementation(() => {
return false;
});

// Mock the mkdir to return "success"
mkdirMock.mockResolvedValue(undefined);

await minikubeDownload.download(releases[0]);

// Expect the mkdir and executables to have been called
expect(mkdirMock).toHaveBeenCalled();
expect(makeExecutableMock).toHaveBeenCalled();
});
154 changes: 154 additions & 0 deletions src/download.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

import { existsSync, promises } from 'node:fs';
import { arch, platform } from 'node:os';
import * as path from 'node:path';
import * as fs from 'node:fs';

import type * as extensionApi from '@podman-desktop/api';

import type { Octokit } from '@octokit/rest';

export interface MinikubeGithubReleaseArtifactMetadata {
tag: string;
id: number;
}

const githubOrganization = 'kubernetes';
const githubRepo = 'minikube';

export class MinikubeDownload {
constructor(
private readonly extensionContext: extensionApi.ExtensionContext,
private readonly octokit: Octokit,
) {}

// Provides last 5 majors releases from GitHub using the GitHub API
// return name, tag and id of the release
async grabLatestsReleasesMetadata(): Promise<MinikubeGithubReleaseArtifactMetadata[]> {
// Grab last 5 majors releases from GitHub using the GitHub API
const lastReleases = await this.octokit.repos.listReleases({
owner: githubOrganization,
repo: githubRepo,
per_page: 10,
});

return lastReleases.data
.filter(release => !release.prerelease)
.map(release => {
return {
label: release.name ?? release.tag_name,
tag: release.tag_name,
id: release.id,
};
})
.slice(0, 5);
}

async getLatestVersionAsset(): Promise<MinikubeGithubReleaseArtifactMetadata> {
const latestReleases = await this.grabLatestsReleasesMetadata();
return latestReleases[0];
}

// Download minikube from the artifact metadata: MinikubeGithubReleaseArtifactMetadata
// this will download it to the storage bin folder as well as make it executable
// return the path where the file has been downloaded
async download(release: MinikubeGithubReleaseArtifactMetadata): Promise<string> {
// Get asset id
const assetId = await this.getReleaseAssetId(release.id, platform(), arch());

// Get the storage and check to see if it exists before we download kubectl
const storageData = this.extensionContext.storagePath;
const storageBinFolder = path.resolve(storageData, 'bin');
if (!existsSync(storageBinFolder)) {
await promises.mkdir(storageBinFolder, { recursive: true });
}

// Correct the file extension and path resolution
let fileExtension = '';
if (process.platform === 'win32') {
fileExtension = '.exe';
}
const minikubeDownloadLocation = path.resolve(storageBinFolder, `minikube${fileExtension}`);

// Download the asset and make it executable
await this.downloadReleaseAsset(assetId, minikubeDownloadLocation);
await this.makeExecutable(minikubeDownloadLocation);

return minikubeDownloadLocation;
}

async makeExecutable(filePath: string): Promise<void> {
if (process.platform === 'darwin' || process.platform === 'linux') {
await promises.chmod(filePath, 0o755);
}
}

// Get the asset id of a given release number for a given operating system and architecture
// operatingSystem: win32, darwin, linux (see os.platform())
// arch: x64, arm64 (see os.arch())
async getReleaseAssetId(releaseId: number, operatingSystem: string, arch: string): Promise<number> {
let extension = '';
if (operatingSystem === 'win32') {
operatingSystem = 'windows';
extension = '.exe';
}
if (arch === 'x64') {
arch = 'amd64';
}

const listOfAssets = await this.octokit.repos.listReleaseAssets({
owner: githubOrganization,
repo: githubRepo,
release_id: releaseId,
per_page: 60,
});

const searchedAssetName = `minikube-${operatingSystem}-${arch}${extension}`;

// search for the right asset
const asset = listOfAssets.data.find(asset => searchedAssetName === asset.name);
if (!asset) {
throw new Error(`No asset found for ${operatingSystem} and ${arch}`);
}

return asset.id;
}

// download the given asset id
async downloadReleaseAsset(assetId: number, destination: string): Promise<void> {
const asset = await this.octokit.repos.getReleaseAsset({
owner: githubOrganization,
repo: githubRepo,
asset_id: assetId,
headers: {
accept: 'application/octet-stream',
},
});

// check the parent folder exists
const parentFolder = path.dirname(destination);

if (!fs.existsSync(parentFolder)) {
await fs.promises.mkdir(parentFolder, { recursive: true });
}
// write the file
await fs.promises.writeFile(destination, Buffer.from(asset.data as unknown as ArrayBuffer));
}
}
Loading

0 comments on commit 52c33f4

Please sign in to comment.