Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [M3-8837] - Add LKE-E feature flag #11259

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Comment on lines -82 to -91
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to modify this test with the addition of the useIsLkeEnterpriseEnabled tests, due to the use of feature flags in both.

In the words of the wise @jdamore-linode, the APL test needed to be modified because:

the vi.mock for useFlags that technically broke the APL availability test, because the useAccountBetaQuery function calls useFlags. And since useAccountBetaQuery wasn’t getting mocked but useFlags was, useFlags was responding with an empty object causing useAccountBetaQuery to always return an object indicating that APL is disabled.

(And so the APL test would fail.)

In the interest of using one mocking solution in this file (not both server.use and vi.mock), I switched this one over to using the queryMocks, as well.

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 = () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not to be confused with the useAccountBetaQuery... if there's a suggestion for a better name here, I'm open to it!

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