Skip to content

Commit

Permalink
feat: [M3-8837] - Add LKE-E feature flag (linode#11259)
Browse files Browse the repository at this point in the history
* Add lkeEnterprise flag, hook, and account beta query

* Add WIP tests for hook

* Refactor added tests, trying to address failures

* Different attempt for debugging purposes; will likely revert this

* Fix the APL test error and some linter warnings

* Add changesets

* Address feedback: add MSW preset for LKE-E account capability
  • Loading branch information
mjac0bs authored Nov 19, 2024
1 parent efc7b22 commit 0314dc5
Show file tree
Hide file tree
Showing 13 changed files with 234 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/api-v4": Upcoming Features
---

Add v4beta/account endpoint and update Capabilities for LKE-E ([#11259](https://github.com/linode/manager/pull/11259))
12 changes: 12 additions & 0 deletions packages/api-v4/src/account/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ export const getAccountInfo = () => {
return Request<Account>(setURL(`${API_ROOT}/account`), setMethod('GET'));
};

/**
* getAccountInfoBeta
*
* Return beta endpoint account information,
* including contact and billing info.
*
* @TODO LKE-E - M3-8838: Clean up after released to GA, if not otherwise in use
*/
export const getAccountInfoBeta = () => {
return Request<Account>(setURL(`${BETA_API_ROOT}/account`), setMethod('GET'));
};

/**
* getNetworkUtilization
*
Expand Down
1 change: 1 addition & 0 deletions packages/api-v4/src/account/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export type AccountCapability =
| 'CloudPulse'
| 'Disk Encryption'
| 'Kubernetes'
| 'Kubernetes Enterprise'
| 'Linodes'
| 'LKE HA Control Planes'
| 'LKE Network Access Control List (IP ACL)'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add feature flag and hook for LKE-E enablement ([#11259](https://github.com/linode/manager/pull/11259))
1 change: 1 addition & 0 deletions packages/manager/src/dev-tools/FeatureFlagTool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const options: { flag: keyof Flags; label: string }[] = [
{ flag: 'imageServiceGen2', label: 'Image Service Gen2' },
{ flag: 'imageServiceGen2Ga', label: 'Image Service Gen2 GA' },
{ flag: 'linodeDiskEncryption', label: 'Linode Disk Encryption (LDE)' },
{ flag: 'lkeEnterprise', label: 'LKE-Enterprise' },
{ flag: 'objMultiCluster', label: 'OBJ Multi-Cluster' },
{ flag: 'objectStorageGen2', label: 'OBJ Gen2' },
{ flag: 'selfServeBetas', label: 'Self Serve Betas' },
Expand Down
6 changes: 6 additions & 0 deletions packages/manager/src/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ interface AclpFlag {
enabled: boolean;
}

interface LkeEnterpriseFlag extends BaseFeatureFlag {
ga: boolean;
la: boolean;
}

export interface CloudPulseResourceTypeMapFlag {
dimensionKey: string;
maxResourceSelections?: number;
Expand Down Expand Up @@ -109,6 +114,7 @@ export interface Flags {
imageServiceGen2Ga: boolean;
ipv6Sharing: boolean;
linodeDiskEncryption: boolean;
lkeEnterprise: LkeEnterpriseFlag;
mainContentBanner: MainContentBanner;
marketplaceAppOverrides: MarketplaceAppOverride[];
metadata: boolean;
Expand Down
123 changes: 111 additions & 12 deletions packages/manager/src/features/Kubernetes/kubeUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,54 @@
import { renderHook, waitFor } from '@testing-library/react';
import { renderHook } from '@testing-library/react';

import {
accountBetaFactory,
kubeLinodeFactory,
linodeTypeFactory,
nodePoolFactory,
} from 'src/factories';
import { HttpResponse, http, server } from 'src/mocks/testServer';
import { extendType } from 'src/utilities/extendType';
import { wrapWithTheme } from 'src/utilities/testHelpers';

import {
getLatestVersion,
getTotalClusterMemoryCPUAndStorage,
useAPLAvailability,
useIsLkeEnterpriseEnabled,
} from './kubeUtils';

const queryMocks = vi.hoisted(() => ({
useAccountBeta: vi.fn().mockReturnValue({}),
useAccountBetaQuery: vi.fn().mockReturnValue({}),
useFlags: vi.fn().mockReturnValue({}),
}));

vi.mock('src/queries/account/account', () => {
const actual = vi.importActual('src/queries/account/account');
return {
...actual,
useAccountBeta: queryMocks.useAccountBeta,
};
});

vi.mock('src/queries/account/betas', () => {
const actual = vi.importActual('src/queries/account/betas');
return {
...actual,
useAccountBetaQuery: queryMocks.useAccountBetaQuery,
};
});

vi.mock('src/hooks/useFlags', () => {
const actual = vi.importActual('src/hooks/useFlags');
return {
...actual,
useFlags: queryMocks.useFlags,
};
});

afterEach(() => {
vi.clearAllMocks();
});

describe('helper functions', () => {
const badPool = nodePoolFactory.build({
type: 'not-a-real-type',
Expand Down Expand Up @@ -73,25 +106,26 @@ describe('helper functions', () => {
});
});
});

describe('APL availability', () => {
it('should return true if the apl flag is true and beta is active', async () => {
const accountBeta = accountBetaFactory.build({
enrolled: '2023-01-15T00:00:00Z',
id: 'apl',
});
server.use(
http.get('*/account/betas/apl', () => {
return HttpResponse.json(accountBeta);
})
);
const { result } = renderHook(() => useAPLAvailability(), {
wrapper: (ui) => wrapWithTheme(ui, { flags: { apl: true } }),

queryMocks.useAccountBetaQuery.mockReturnValue({
data: accountBeta,
});
await waitFor(() => {
expect(result.current.showAPL).toBe(true);
queryMocks.useFlags.mockReturnValue({
apl: true,
});

const { result } = renderHook(() => useAPLAvailability());
expect(result.current.showAPL).toBe(true);
});
});

describe('getLatestVersion', () => {
it('should return the correct latest version from a list of versions', () => {
const versions = [
Expand Down Expand Up @@ -128,3 +162,68 @@ describe('helper functions', () => {
});
});
});

describe('useIsLkeEnterpriseEnabled', () => {
it('returns false if the account does not have the capability', () => {
queryMocks.useAccountBeta.mockReturnValue({
data: {
capabilities: [],
},
});
queryMocks.useFlags.mockReturnValue({
lkeEnterprise: {
enabled: true,
ga: true,
la: true,
},
});

const { result } = renderHook(() => useIsLkeEnterpriseEnabled());
expect(result.current).toStrictEqual({
isLkeEnterpriseGAEnabled: false,
isLkeEnterpriseLAEnabled: false,
});
});

it('returns true for LA if the account has the capability + enabled LA feature flag values', () => {
queryMocks.useAccountBeta.mockReturnValue({
data: {
capabilities: ['Kubernetes Enterprise'],
},
});
queryMocks.useFlags.mockReturnValue({
lkeEnterprise: {
enabled: true,
ga: false,
la: true,
},
});

const { result } = renderHook(() => useIsLkeEnterpriseEnabled());
expect(result.current).toStrictEqual({
isLkeEnterpriseGAEnabled: false,
isLkeEnterpriseLAEnabled: true,
});
});

it('returns true for GA if the account has the capability + enabled GA feature flag values', () => {
queryMocks.useAccountBeta.mockReturnValue({
data: {
capabilities: ['Kubernetes Enterprise'],
},
});
queryMocks.useFlags.mockReturnValue({
lkeEnterprise: {
enabled: true,
ga: true,
la: true,
},
});

const { result } = renderHook(() => useIsLkeEnterpriseEnabled());
expect(result.current).toStrictEqual({
isLkeEnterpriseGAEnabled: true,
isLkeEnterpriseLAEnabled: true,
});
});
});
33 changes: 33 additions & 0 deletions packages/manager/src/features/Kubernetes/kubeUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useFlags } from 'src/hooks/useFlags';
import { useAccountBeta } from 'src/queries/account/account';
import { useAccountBetaQuery } from 'src/queries/account/betas';
import { getBetaStatus } from 'src/utilities/betaUtils';
import { sortByVersion } from 'src/utilities/sort-by';
Expand All @@ -11,6 +12,7 @@ import type {
} from '@linode/api-v4/lib/kubernetes';
import type { Region } from '@linode/api-v4/lib/regions';
import type { ExtendedType } from 'src/utilities/extendType';
import { isFeatureEnabledV2 } from 'src/utilities/accountCapabilities';
export const nodeWarning = `We recommend a minimum of 3 nodes in each Node Pool to avoid downtime during upgrades and maintenance.`;
export const nodesDeletionWarning = `All nodes will be deleted and new nodes will be created to replace them.`;
export const localStorageWarning = `Any local storage (such as \u{2019}hostPath\u{2019} volumes) will be erased.`;
Expand Down Expand Up @@ -184,3 +186,34 @@ export const getLatestVersion = (

return { label: `${latestVersion.value}`, value: `${latestVersion.value}` };
};

/**
* Hook to determine if the LKE-Enterprise feature should be visible to the user.
* Based on the user's account capability and the feature flag.
*
* @returns {boolean, boolean} - Whether the LKE-Enterprise feature is enabled for the current user in LA and GA, respectively.
*/
export const useIsLkeEnterpriseEnabled = () => {
const flags = useFlags();
const { data: account } = useAccountBeta();

const isLkeEnterpriseLA = Boolean(
flags?.lkeEnterprise?.enabled && flags.lkeEnterprise.la
);
const isLkeEnterpriseGA = Boolean(
flags.lkeEnterprise?.enabled && flags.lkeEnterprise.ga
);

const isLkeEnterpriseLAEnabled = isFeatureEnabledV2(
'Kubernetes Enterprise',
isLkeEnterpriseLA,
account?.capabilities ?? []
);
const isLkeEnterpriseGAEnabled = isFeatureEnabledV2(
'Kubernetes Enterprise',
isLkeEnterpriseGA,
account?.capabilities ?? []
);

return { isLkeEnterpriseLAEnabled, isLkeEnterpriseGAEnabled };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// New file at `src/mocks/presets/extra/account/lkeEnterpriseEnabled.ts` or similar

import { http } from 'msw';

import { accountFactory } from 'src/factories';
import { makeResponse } from 'src/mocks/utilities/response';

import type { MockPresetExtra } from 'src/mocks/types';

const mockLkeEnabledCapability = () => {
return [
http.get('*/v4*/account', async ({ request }) => {
return makeResponse(
accountFactory.build({
capabilities: [
// Other account capabilities might be necessary here, too...
// TODO Make a `defaultAccountCapabilities` factory.
'Kubernetes',
'Kubernetes Enterprise',
],
})
);
}),
];
};

export const lkeEnterpriseEnabledPreset: MockPresetExtra = {
desc: 'Mock account with LKE Enterprise capability',
group: { id: 'Account', type: 'select' },
handlers: [mockLkeEnabledCapability],
id: 'account:lke-enterprise-enabled',
label: 'LKE Enterprise Enabled',
};
2 changes: 2 additions & 0 deletions packages/manager/src/mocks/presets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { baselineCrudPreset } from './baseline/crud';
import { baselineLegacyPreset } from './baseline/legacy';
import { baselineNoMocksPreset } from './baseline/noMocks';
import { childAccountPreset } from './extra/account/childAccount';
import { lkeEnterpriseEnabledPreset } from './extra/account/lkeEnterpriseEnabled';
import { managedDisabledPreset } from './extra/account/managedDisabled';
import { managedEnabledPreset } from './extra/account/managedEnabled';
import { parentAccountPreset } from './extra/account/parentAccount';
Expand Down Expand Up @@ -43,6 +44,7 @@ export const extraMockPresets: MockPresetExtra[] = [
childAccountPreset,
linodeLimitsPreset,
lkeLimitsPreset,
lkeEnterpriseEnabledPreset,
managedEnabledPreset,
managedDisabledPreset,
coreAndDistributedRegionsPreset,
Expand Down
1 change: 1 addition & 0 deletions packages/manager/src/mocks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export type MockPresetExtraGroup = {
};
export type MockPresetExtraId =
| 'account:child-proxy'
| 'account:lke-enterprise-enabled'
| 'account:managed-disabled'
| 'account:managed-enabled'
| 'account:parent'
Expand Down
16 changes: 16 additions & 0 deletions packages/manager/src/queries/account/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { useSnackbar } from 'notistack';

import { useIsTaxIdEnabled } from 'src/features/Account/utils';
import { useFlags } from 'src/hooks/useFlags';
import { useGrants, useProfile } from 'src/queries/profile/profile';

import { queryPresets } from '../base';
Expand All @@ -36,6 +37,21 @@ export const useAccount = () => {
});
};

/**
* @TODO LKE-E - M3-8838: Clean up after released to GA, if not otherwise in use
*/
export const useAccountBeta = () => {
const { data: profile } = useProfile();
const flags = useFlags();

return useQuery<Account, APIError[]>({
...accountQueries.accountBeta,
...queryPresets.oneTimeFetch,
...queryPresets.noRetry,
enabled: !profile?.restricted && flags.lkeEnterprise?.enabled,
});
};

export const useMutateAccount = () => {
const queryClient = useQueryClient();
const { enqueueSnackbar } = useSnackbar();
Expand Down
8 changes: 8 additions & 0 deletions packages/manager/src/queries/account/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
getAccountBeta,
getAccountBetas,
getAccountInfo,
getAccountInfoBeta,
getAccountLogins,
getAccountMaintenance,
getAccountSettings,
Expand Down Expand Up @@ -32,6 +33,13 @@ export const accountQueries = createQueryKeys('account', {
queryFn: getAccountInfo,
queryKey: null,
},
/**
* @TODO LKE-E - M3-8838: Clean up after released to GA, if not otherwise in use
*/
accountBeta: {
queryFn: getAccountInfoBeta,
queryKey: null,
},
agreements: {
queryFn: getAccountAgreements,
queryKey: null,
Expand Down

0 comments on commit 0314dc5

Please sign in to comment.