-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
Showing
5 changed files
with
1,525 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
Oops, something went wrong.