Skip to content

Commit

Permalink
support feature flag overrides via query string
Browse files Browse the repository at this point in the history
  • Loading branch information
christianvogt committed Jun 21, 2024
1 parent 2cafeff commit c335bd2
Show file tree
Hide file tree
Showing 52 changed files with 612 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const updateClusterSettings = async (
notebookTolerationSettings,
modelServingPlatformEnabled,
} = request.body;
const dashConfig = getDashboardConfig();
const dashConfig = getDashboardConfig(request);
const isJupyterEnabled = checkJupyterEnabled();
try {
if (
Expand Down Expand Up @@ -124,10 +124,11 @@ export const updateClusterSettings = async (

export const getClusterSettings = async (
fastify: KubeFastifyInstance,
request: FastifyRequest,
): Promise<ClusterSettings | string> => {
const coreV1Api = fastify.kube.coreV1Api;
const namespace = fastify.kube.namespace;
const dashConfig = getDashboardConfig();
const dashConfig = getDashboardConfig(request);
const isJupyterEnabled = checkJupyterEnabled();
const clusterSettings: ClusterSettings = {
...DEFAULT_CLUSTER_SETTINGS,
Expand Down
2 changes: 1 addition & 1 deletion backend/src/routes/api/cluster-settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default async (fastify: FastifyInstance): Promise<void> => {
fastify.get(
'/',
secureAdminRoute(fastify)(async (request: FastifyRequest, reply: FastifyReply) => {
return getClusterSettings(fastify)
return getClusterSettings(fastify, request)
.then((res) => {
return res;
})
Expand Down
4 changes: 2 additions & 2 deletions backend/src/routes/api/storage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default async (fastify: FastifyInstance): Promise<void> => {
reply: FastifyReply,
) => {
try {
const dashConfig = getDashboardConfig();
const dashConfig = getDashboardConfig(request);
if (dashConfig?.spec.dashboardConfig.disableS3Endpoint !== false) {
reply.code(404).send('Not found');
return reply;
Expand Down Expand Up @@ -49,7 +49,7 @@ export default async (fastify: FastifyInstance): Promise<void> => {
reply: FastifyReply,
) => {
try {
const dashConfig = getDashboardConfig();
const dashConfig = getDashboardConfig(request);
if (dashConfig?.spec.dashboardConfig.disableS3Endpoint !== false) {
reply.code(404).send('Not found');
return reply;
Expand Down
29 changes: 27 additions & 2 deletions backend/src/utils/resourceUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { getIsAppEnabled, getRouteForApplication, getRouteForClusterId } from '.
import { createCustomError } from './requestUtils';
import { getDetectedAccelerators } from '../routes/api/accelerators/acceleratorUtils';
import { RecursivePartial } from '../typeHelpers';
import { FastifyRequest } from 'fastify';

const dashboardConfigMapName = 'odh-dashboard-config';
const consoleLinksGroup = 'console.openshift.io';
Expand Down Expand Up @@ -548,8 +549,32 @@ export const initializeWatchedResources = (fastify: KubeFastifyInstance): void =
consoleLinksWatcher = new ResourceWatcher<ConsoleLinkKind>(fastify, fetchConsoleLinks);
};

export const getDashboardConfig = (): DashboardConfig => {
return dashboardConfigWatcher.getResources()?.[0];
const FEATURE_FLAGS_HEADER = 'x-odh-feature-flags';

// if inspecting feature flags, provide the request to ensure overridden feature flags are considered
export const getDashboardConfig = (request?: FastifyRequest): DashboardConfig => {
const dashboardConfig = dashboardConfigWatcher.getResources()?.[0];
if (request) {
const flagsHeader = request.headers[FEATURE_FLAGS_HEADER];
if (typeof flagsHeader === 'string') {
try {
const featureFlags = JSON.parse(flagsHeader);
return {
...dashboardConfig,
spec: {
...dashboardConfig.spec,
dashboardConfig: {
...dashboardConfig.spec.dashboardConfig,
...featureFlags,
},
},
};
} catch {
// ignore
}
}
}
return dashboardConfig;
};

export const updateDashboardConfig = (): Promise<void> => {
Expand Down
18 changes: 14 additions & 4 deletions frontend/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -111,18 +111,25 @@
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "^axios$",
"importNames": ["default"],
"message": "Import from `~/utilities/axios` instead."
}
],
"patterns": [
{
"group": ["~/api/**"],
"message": "Read from '~/api' instead."
"message": "Import from '~/api' instead."
},
{
"group": ["~/components/table/**", "!~/components/table/useTableColumnSort"],
"message": "Read from '~/components/table' instead."
"message": "Import from '~/components/table' instead."
},
{
"group": ["~/concepts/area/**"],
"message": "Read from '~/concepts/area' instead."
"message": "Import from '~/concepts/area' instead."
},
{
"group": ["~/components/table/useTableColumnSort"],
Expand Down Expand Up @@ -286,7 +293,10 @@
],
"overrides": [
{
"files": ["./src/__tests__/cypress/cypress/pages/*.ts", "./src/__tests__/cypress/cypress/tests/e2e/*.ts"],
"files": [
"./src/__tests__/cypress/cypress/pages/*.ts",
"./src/__tests__/cypress/cypress/tests/e2e/*.ts"
],
"rules": {
"no-restricted-syntax": [
"error",
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/api/k8s/__tests__/projects.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
k8sUpdateResource,
k8sDeleteResource,
} from '@openshift/dynamic-plugin-sdk-utils';
import axios from 'axios';
import axios from '~/utilities/axios';
import { mockProjectK8sResource } from '~/__mocks__/mockProjectK8sResource';
import { mockK8sResourceList } from '~/__mocks__/mockK8sResourceList';
import { mockAxiosError } from '~/__mocks__/mockAxiosError';
Expand Down Expand Up @@ -40,7 +40,7 @@ jest.mock('~/api/k8s/servingRuntimes.ts', () => ({
listServingRuntimes: jest.fn(),
}));

jest.mock('axios');
jest.mock('~/utilities/axios');

const mockedAxios = jest.mocked(axios);
const k8sListResourceMock = jest.mocked(k8sListResource<ProjectKind>);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/api/k8s/projects.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import axios from 'axios';
import {
k8sCreateResource,
k8sDeleteResource,
k8sListResource,
K8sResourceCommon,
k8sUpdateResource,
} from '@openshift/dynamic-plugin-sdk-utils';
import axios from '~/utilities/axios';
import { CustomWatchK8sResult } from '~/types';
import { K8sAPIOptions, ProjectKind } from '~/k8sTypes';
import { ProjectModel, ProjectRequestModel } from '~/api/models';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { act } from '@testing-library/react';
import axios from 'axios';
import axios from '~/utilities/axios';
import { mockPrometheusQueryVectorResponse } from '~/__mocks__/mockPrometheusQueryVectorResponse';
import { mockWorkloadK8sResource } from '~/__mocks__/mockWorkloadK8sResource';
import { WorkloadKind, WorkloadOwnerType } from '~/k8sTypes';
Expand Down Expand Up @@ -310,7 +310,7 @@ describe('getTopResourceConsumingWorkloads', () => {
});
});

jest.mock('axios', () => ({
jest.mock('~/utilities/axios', () => ({
post: jest.fn(),
}));

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/api/prometheus/__tests__/pvcs.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { act } from '@testing-library/react';
import axios from 'axios';
import axios from '~/utilities/axios';
import { mockPVCK8sResource } from '~/__mocks__/mockPVCK8sResource';
import { mockPrometheusQueryResponse } from '~/__mocks__/mockPrometheusQueryResponse';
import { testHook } from '~/__tests__/unit/testUtils/hooks';
import { usePVCFreeAmount } from '~/api/prometheus/pvcs';
import { POLL_INTERVAL } from '~/utilities/const';

jest.mock('axios', () => ({
jest.mock('~/utilities/axios', () => ({
post: jest.fn(),
}));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { act } from '@testing-library/react';
import axios from 'axios';
import axios from '~/utilities/axios';
import { mockPrometheusQueryResponse } from '~/__mocks__/mockPrometheusQueryResponse';
import { standardUseFetchState, testHook } from '~/__tests__/unit/testUtils/hooks';
import usePrometheusQuery from '~/api/prometheus/usePrometheusQuery';

jest.mock('axios', () => ({
jest.mock('~/utilities/axios', () => ({
post: jest.fn(),
}));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import axios from 'axios';
import { act } from '@testing-library/react';
import axios from '~/utilities/axios';
import { testHook } from '~/__tests__/unit/testUtils/hooks';
import { mockPrometheusServing } from '~/__mocks__/mockPrometheusServing';
import usePrometheusQueryRange from '~/api/prometheus/usePrometheusQueryRange';
import { PrometheusQueryRangeResponseData } from '~/types';

jest.mock('axios', () => ({
jest.mock('~/utilities/axios', () => ({
post: jest.fn(),
}));

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/api/prometheus/usePrometheusQuery.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import axios from 'axios';
import axios from '~/utilities/axios';
import { PrometheusQueryResponse } from '~/types';
import useFetchState, { FetchOptions, FetchState, NotReadyError } from '~/utilities/useFetchState';

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/api/prometheus/usePrometheusQueryRange.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import axios from 'axios';
import axios from '~/utilities/axios';

import useFetchState, {
FetchOptions,
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import useDetectUser from '~/utilities/useDetectUser';
import ProjectsContextProvider from '~/concepts/projects/ProjectsContext';
import useStorageClasses from '~/concepts/k8s/useStorageClasses';
import AreaContextProvider from '~/concepts/areas/AreaContext';
import useDevFeatureFlags from './useDevFeatureFlags';
import Header from './Header';
import AppRoutes from './AppRoutes';
import NavSidebar from './NavSidebar';
Expand All @@ -29,6 +30,7 @@ import { useApplicationSettings } from './useApplicationSettings';
import TelemetrySetup from './TelemetrySetup';
import { logout } from './appUtils';
import QuickStarts from './QuickStarts';
import DevFeatureFlagsBanner from './DevFeatureFlagsBanner';

import './App.scss';

Expand All @@ -38,11 +40,13 @@ const App: React.FC = () => {

const buildStatuses = useWatchBuildStatus();
const {
dashboardConfig,
dashboardConfig: dashboardConfigFromServer,
loaded: configLoaded,
loadError: fetchConfigError,
} = useApplicationSettings();

const { dashboardConfig, ...devFeatureFlags } = useDevFeatureFlags(dashboardConfigFromServer);

const [storageClasses] = useStorageClasses();

useDetectUser();
Expand Down Expand Up @@ -115,6 +119,10 @@ const App: React.FC = () => {
data-testid={DASHBOARD_MAIN_CONTAINER_ID}
>
<ErrorBoundary>
<DevFeatureFlagsBanner
dashboardConfig={dashboardConfig.spec.dashboardConfig}
{...devFeatureFlags}
/>
<ProjectsContextProvider>
<QuickStarts>
<AppRoutes />
Expand Down
92 changes: 92 additions & 0 deletions frontend/src/app/DevFeatureFlagsBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Banner, Button, Checkbox, Grid, GridItem, Modal } from '@patternfly/react-core';
import * as React from 'react';
import { allFeatureFlags } from '~/concepts/areas/const';
import { isFeatureFlag } from '~/concepts/areas/utils';
import { DashboardCommonConfig } from '~/k8sTypes';
import { DevFeatureFlags } from '~/types';

type Props = { dashboardConfig: Partial<DashboardCommonConfig> } & DevFeatureFlags;

const DevdevFeatureFlagsBanner: React.FC<Props> = ({
dashboardConfig,
devFeatureFlags,
setDevFeatureFlag,
resetDevFeatureFlags,
}) => {
const [isModalOpen, setModalOpen] = React.useState(false);
if (!devFeatureFlags) {
return null;
}
const renderDevFeatureFlags = () => (
<Grid hasGutter span={6} md={3}>
{allFeatureFlags
.filter(isFeatureFlag)
.toSorted()
.map((key) => {
const value = devFeatureFlags[key] ?? dashboardConfig[key];
return (
<React.Fragment key={key}>
<GridItem>
<Checkbox
id={key}
data-testid={`${key}-checkbox`}
label={key}
isChecked={value ?? null}
onChange={(_, checked) => setDevFeatureFlag(key, checked)}
/>
</GridItem>
<GridItem data-testid={`${key}-value`}>{`${value ?? ''}${
key in devFeatureFlags ? ' (overridden)' : ''
}`}</GridItem>
</React.Fragment>
);
})}
</Grid>
);
return (
<>
<Banner variant="blue">
Feature flags are{' '}
<Button
data-testid="override-feature-flags-button"
variant="link"
isInline
onClick={() => setModalOpen(true)}
>
overridden
</Button>{' '}
in the current session.{' '}
<Button
data-testid="reset-feature-flags-button"
variant="link"
isInline
onClick={resetDevFeatureFlags}
>
Click here
</Button>{' '}
to reset back to default.
</Banner>
<Modal
data-testid="dev-feature-flags-modal"
variant="large"
title="Feature flags"
isOpen={isModalOpen}
onClose={() => setModalOpen(false)}
actions={[
<Button
data-testid="reset-feature-flags-modal-button"
key="confirm"
variant="link"
onClick={() => resetDevFeatureFlags()}
>
Reset to defaults
</Button>,
]}
>
{renderDevFeatureFlags()}
</Modal>
</>
);
};

export default DevdevFeatureFlagsBanner;
Loading

0 comments on commit c335bd2

Please sign in to comment.