Skip to content

Commit

Permalink
Adds a service for supported SKUs
Browse files Browse the repository at this point in the history
  • Loading branch information
gingi committed Mar 1, 2024
1 parent 5d4a2c4 commit ee416a3
Show file tree
Hide file tree
Showing 17 changed files with 419 additions and 3 deletions.
4 changes: 3 additions & 1 deletion desktop/src/app/environment/desktop-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { DefaultFormLayoutProvider } from "@azure/bonito-ui/lib/components/form"
import { BrowserDependencyName, BrowserEnvironmentConfig, DefaultBrowserEnvironment } from "@azure/bonito-ui/lib/environment";
import BatchExplorerHttpClient from "@batch-flask/core/batch-explorer-http-client";
import { BatchBrowserDependencyFactories, BatchFormControlResolver } from "@batch/ui-react";
import { LiveNodeService, LivePoolService } from "@batch/ui-service";
import { LiveNodeService, LivePoolService, LiveSkuService } from "@batch/ui-service";
import { BatchDependencyName } from "@batch/ui-service/lib/environment";
import { DesktopLocalizer } from "app/localizer/desktop-localizer";
import { AppTranslationsLoaderService, AuthService, BatchExplorerService } from "app/services";
Expand Down Expand Up @@ -46,6 +46,8 @@ export function initDesktopEnvironment(
new LivePoolService(),
[BatchDependencyName.NodeService]: () =>
new LiveNodeService(),
[BatchDependencyName.SkuService]: () =>
new LiveSkuService(),
[DependencyName.Notifier]: () =>
new AlertNotifier(), // TODO: update with real notification implementation
[DependencyName.ResourceGroupService]: () =>
Expand Down
5 changes: 4 additions & 1 deletion packages/service/i18n/resources.resjson
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
{}
{
"lib.service.sku.eolWarning": "This SKU is scheduled for retirement on {eolDate}",
"lib.service.sku.notFound": "SKU '{skuName}' not found"
}
3 changes: 3 additions & 0 deletions packages/service/src/environment/batch-dependencies.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { DependencyFactories } from "@azure/bonito-core/lib/environment";
import { NodeService } from "../node";
import { PoolService } from "../pool";
import { SkuService } from "../sku";

export enum BatchDependencyName {
PoolService = "poolService",
NodeService = "nodeService",
SkuService = "skuService",
}

export interface BatchDependencyFactories extends DependencyFactories {
[BatchDependencyName.PoolService]: () => PoolService;
[BatchDependencyName.NodeService]: () => NodeService;
[BatchDependencyName.SkuService]: () => SkuService;
}
2 changes: 2 additions & 0 deletions packages/service/src/environment/environment-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import {
mockEnvironmentConfig,
} from "@azure/bonito-core/lib/environment";
import { FakePoolService } from "../pool";
import { FakeSkuService } from "../sku";
import {
BatchDependencyFactories,
BatchDependencyName,
} from "./batch-dependencies";

export const mockBatchDepFactories: Partial<BatchDependencyFactories> = {
[BatchDependencyName.PoolService]: () => new FakePoolService(),
[BatchDependencyName.SkuService]: () => new FakeSkuService(),
};

/**
Expand Down
1 change: 1 addition & 0 deletions packages/service/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from "./certificate";
export * from "./pool";
export * from "./node";
export * from "./constants";
export * from "./sku";
31 changes: 31 additions & 0 deletions packages/service/src/sku/__tests__/fake-sku-service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { initMockBatchEnvironment } from "../../environment";
import {
BasicBatchFakeSet,
BatchFakeSet,
FakeLocations,
} from "../../test-util/fakes";
import { FakeSkuService } from "../fake-sku-service";
import { SupportedSkuType } from "../sku-models";

describe("FakeSkuService", () => {
let fakeSet: BatchFakeSet;
let service: FakeSkuService;

beforeEach(() => {
initMockBatchEnvironment();
fakeSet = new BasicBatchFakeSet();
service = new FakeSkuService();
service.setFakes(fakeSet);
});

test("List by type", async () => {
const skus = await service.list({
subscriptionId: "00000000-0000-0000-0000-000000000000",
type: SupportedSkuType.CloudService,
locationName: FakeLocations.Arrakis.name,
});
expect(skus.length).toEqual(2);
expect(skus[1].name).toEqual("A7");
expect(skus[1].batchSupportEndOfLife).toEqual("2024-08-31T00:00:00Z");
});
});
58 changes: 58 additions & 0 deletions packages/service/src/sku/__tests__/live-sku-service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { MockHttpClient, MockHttpResponse } from "@azure/bonito-core/lib/http";
import {
BasicBatchFakeSet,
BatchFakeSet,
FakeLocations,
} from "../../test-util/fakes";
import { LiveSkuService } from "../live-sku-service";
import { initMockBatchEnvironment } from "../../environment";
import { getMockEnvironment } from "@azure/bonito-core/lib/environment";
import { getArmUrl } from "@azure/bonito-core";
import { BatchApiVersion } from "../../constants";
import { SupportedSkuType } from "../sku-models";

describe("LiveSkuService", () => {
const arrakisLoc =
"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Batch/locations/arrakis";
let service: LiveSkuService;
let fakeSet: BatchFakeSet;

let httpClient: MockHttpClient;

beforeEach(() => {
initMockBatchEnvironment();
httpClient = getMockEnvironment().getHttpClient();
service = new LiveSkuService();
fakeSet = new BasicBatchFakeSet();
});

test("List virtual machine SKUs", async () => {
httpClient.addExpected(
new MockHttpResponse(
`${getArmUrl()}${arrakisLoc}/virtualMachineSkus?api-version=${
BatchApiVersion.arm
}`,
{
status: 200,
body: JSON.stringify({
value: fakeSet.listSupportedSkus(
SupportedSkuType.VirtualMachine,
FakeLocations.Arrakis.name
),
}),
}
)
);

const skus = await service.list({
subscriptionId: "00000000-0000-0000-0000-000000000000",
locationName: FakeLocations.Arrakis.name,
});
expect(skus.length).toEqual(4);
expect(skus[2]).toEqual({
name: "Standard_NC8as_T4_v3",
familyName: "Standard NCASv3_T4 Family",
batchSupportEndOfLife: "2024-08-31T00:00:00Z",
});
});
});
43 changes: 43 additions & 0 deletions packages/service/src/sku/__tests__/sku-util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { initMockBatchEnvironment } from "../../environment";
import {
BasicBatchFakeSet,
BatchFakeSet,
FakeLocations,
} from "../../test-util/fakes";
import { FakeSkuService } from "../fake-sku-service";
import { SupportedSkuType } from "../sku-models";
import { endOfLifeStatus } from "../sku-util";

describe("SKU Utilities", () => {
let fakeSet: BatchFakeSet;
let service: FakeSkuService;

const options = {
subscriptionId: "00000000-0000-0000-0000-000000000000",
type: SupportedSkuType.VirtualMachine,
locationName: FakeLocations.Arrakis.name,
};

beforeEach(() => {
initMockBatchEnvironment();
fakeSet = new BasicBatchFakeSet();
service = new FakeSkuService();
service.setFakes(fakeSet);
});

test("returns an EOL date for a given SKU", async () => {
const eolSku = "Standard_NC8as_T4_v3";
const nonEolSku = "Standard_NC64as_T4_v3";
const nonExistingSku = "non-SKU";

const eol = (skuName: string) =>
endOfLifeStatus({ ...options, skuName });

expect(await eol(eolSku)).toEqual({
eolDate: new Date("2024-08-31T00:00:00Z"),
warning: "This SKU is scheduled for retirement on August 30, 2024",
});
expect(await eol(nonEolSku)).toBeNull();
expect(eol(nonExistingSku)).rejects.toThrow(`SKU 'non-SKU' not found`);
});
});
17 changes: 17 additions & 0 deletions packages/service/src/sku/fake-sku-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { BasicBatchFakeSet, BatchFakeSet } from "../test-util/fakes";
import { SupportedSku, SupportedSkuType } from "./sku-models";
import { ListSkusOptions, SkuService } from "./sku-service";

export class FakeSkuService implements SkuService {
fakeSet: BatchFakeSet = new BasicBatchFakeSet();

setFakes(fakeSet: BatchFakeSet): void {
this.fakeSet = fakeSet;
}

async list(options: ListSkusOptions): Promise<SupportedSku[]> {
const { type = SupportedSkuType.VirtualMachine, locationName } =
options;
return this.fakeSet.listSupportedSkus(type, locationName);
}
}
5 changes: 5 additions & 0 deletions packages/service/src/sku/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from "./fake-sku-service";
export * from "./live-sku-service";
export * from "./sku-service";
export * from "./sku-models";
export * from "./sku-util";
39 changes: 39 additions & 0 deletions packages/service/src/sku/live-sku-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { AbstractHttpService, getArmUrl } from "@azure/bonito-core";
import { ListSkusOptions, SkuService } from "./sku-service";
import { SupportedSku, SupportedSkuType } from "./sku-models";
import { createARMBatchClient, isUnexpected } from "../internal/arm-batch-rest";
import { createArmUnexpectedStatusCodeError } from "../utils";

const VIRTUAL_MACHINE_SKU_PATH =
"/subscriptions/{subscriptionId}/providers/Microsoft.Batch/locations/{locationName}/virtualMachineSkus";
const CLOUD_SERVICE_SKU_PATH =
"/subscriptions/{subscriptionId}/providers/Microsoft.Batch/locations/{locationName}/cloudServiceSkus";

export class LiveSkuService extends AbstractHttpService implements SkuService {
async list(options: ListSkusOptions): Promise<SupportedSku[]> {
const {
subscriptionId,
locationName,
type = SupportedSkuType.VirtualMachine,
} = options;

const armBatchClient = createARMBatchClient({
baseUrl: getArmUrl(),
});

let res;
if (type === SupportedSkuType.CloudService) {
res = await armBatchClient
.path(CLOUD_SERVICE_SKU_PATH, subscriptionId, locationName)
.get();
} else {
res = await armBatchClient
.path(VIRTUAL_MACHINE_SKU_PATH, subscriptionId, locationName)
.get();
}
if (isUnexpected(res)) {
throw createArmUnexpectedStatusCodeError(res);
}
return res.body.value ?? [];
}
}
6 changes: 6 additions & 0 deletions packages/service/src/sku/sku-models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { SupportedSkuOutput as SupportedSku } from "../internal/arm-batch-rest";

export const enum SupportedSkuType {
CloudService = "CloudService",
VirtualMachine = "VirtualMachine",
}
11 changes: 11 additions & 0 deletions packages/service/src/sku/sku-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { SupportedSku, SupportedSkuType } from "./sku-models";

export interface ListSkusOptions {
subscriptionId: string;
type?: SupportedSkuType;
locationName: string;
}

export interface SkuService {
list(options: ListSkusOptions): Promise<SupportedSku[]>;
}
51 changes: 51 additions & 0 deletions packages/service/src/sku/sku-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { getLocalizer, translate } from "@azure/bonito-core/lib/localization";
import { BatchDependencyName } from "../environment";
import { ListSkusOptions, SkuService } from "./sku-service";
import { inject } from "@azure/bonito-core/lib/environment";

export interface SkuEndOfLifeStatus {
eolDate: Date;
warning: string;
}

export interface EndOfLifeStatusQueryOptions extends ListSkusOptions {
skuName: string;
}

/**
* Get the end of life status for a SKU
*
* @param options - The options for the query
* @returns The end of life status for the SKU
* @throws If the SKU is not found
*/
export async function endOfLifeStatus(
options: EndOfLifeStatusQueryOptions
): Promise<SkuEndOfLifeStatus | null> {
const { skuName } = options;

const skuService: SkuService = inject(BatchDependencyName.SkuService);
const skus = await skuService.list(options);
const sku = skus.find((s) => s.name === skuName);
if (!sku) {
throw new Error(translate("lib.service.sku.notFound", { skuName }));
}
if (sku.batchSupportEndOfLife) {
const eolDate = new Date(sku.batchSupportEndOfLife);
return {
eolDate,
warning: translate("lib.service.sku.eolWarning", {
skuName,
eolDate: eolDate.toLocaleDateString(
getLocalizer().getLocale(),
{
day: "numeric",
month: "long",
year: "numeric",
}
),
}),
};
}
return null;
}
3 changes: 3 additions & 0 deletions packages/service/src/sku/sku.i18n.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
sku:
notFound: "SKU '{skuName}' not found"
eolWarning: "This SKU is scheduled for retirement on {eolDate}"
Loading

0 comments on commit ee416a3

Please sign in to comment.