Skip to content

Commit

Permalink
Wrap up kubeflow deployment
Browse files Browse the repository at this point in the history
  • Loading branch information
lucferbux committed Dec 20, 2024
1 parent 9f68ddc commit c9fa4de
Show file tree
Hide file tree
Showing 24 changed files with 173 additions and 58 deletions.
3 changes: 3 additions & 0 deletions clients/ui/.env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
APP_ENV=development
MOCK_AUTH=true
DEPLOYMENT_MODE=standalone
1 change: 1 addition & 0 deletions clients/ui/.env.production
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
APP_ENV=production
2 changes: 1 addition & 1 deletion clients/ui/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ dev-install-dependencies:

.PHONY: dev-bff
dev-bff:
cd bff && make run PORT=4000 MOCK_K8S_CLIENT=true MOCK_MR_CLIENT=true
cd bff && make run PORT=4000 MOCK_K8S_CLIENT=true MOCK_MR_CLIENT=true DEV_MODE=true STANDALONE_MODE=true

.PHONY: dev-frontend
dev-frontend:
Expand Down
3 changes: 2 additions & 1 deletion clients/ui/bff/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ MOCK_K8S_CLIENT ?= false
MOCK_MR_CLIENT ?= false
DEV_MODE ?= false
DEV_MODE_PORT ?= 8080
STANDALONE_MODE ?= true
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
ENVTEST_K8S_VERSION = 1.29.0

Expand Down Expand Up @@ -47,7 +48,7 @@ build: fmt vet test ## Builds the project to produce a binary executable.
.PHONY: run
run: fmt vet envtest ## Runs the project.
ENVTEST_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" \
go run ./cmd/main.go --port=$(PORT) --mock-k8s-client=$(MOCK_K8S_CLIENT) --mock-mr-client=$(MOCK_MR_CLIENT) --dev-mode=$(DEV_MODE) --dev-mode-port=$(DEV_MODE_PORT)
go run ./cmd/main.go --port=$(PORT) --mock-k8s-client=$(MOCK_K8S_CLIENT) --mock-mr-client=$(MOCK_MR_CLIENT) --dev-mode=$(DEV_MODE) --dev-mode-port=$(DEV_MODE_PORT) --standalone-mode=$(STANDALONE_MODE)

.PHONY: docker-build
docker-build: ## Builds a container for the project.
Expand Down
1 change: 1 addition & 0 deletions clients/ui/bff/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func main() {
flag.BoolVar(&cfg.MockMRClient, "mock-mr-client", false, "Use mock Model Registry client")
flag.BoolVar(&cfg.DevMode, "dev-mode", false, "Use development mode for access to local K8s cluster")
flag.IntVar(&cfg.DevModePort, "dev-mode-port", getEnvAsInt("DEV_MODE_PORT", 8080), "Use port when in development mode")
flag.BoolVar(&cfg.StandaloneMode, "standalone-mode", false, "Use standalone mode for enabling endpoints in standalone mode")
flag.Parse()

logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
Expand Down
7 changes: 4 additions & 3 deletions clients/ui/bff/internal/api/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ package api
import (
"context"
"fmt"
"log/slog"
"net/http"

"github.com/kubeflow/model-registry/ui/bff/internal/config"
"github.com/kubeflow/model-registry/ui/bff/internal/integrations"
"github.com/kubeflow/model-registry/ui/bff/internal/repositories"
"log/slog"
"net/http"

"github.com/julienschmidt/httprouter"
"github.com/kubeflow/model-registry/ui/bff/internal/mocks"
Expand Down Expand Up @@ -110,7 +111,7 @@ func (app *App) Routes() http.Handler {
router.GET(UserPath, app.UserHandler)
// Perform SAR to Get List Services by Namspace
router.GET(ModelRegistryListPath, app.AttachNamespace(app.PerformSARonGetListServicesByNamespace(app.ModelRegistryHandler)))
if app.config.DevMode {
if app.config.StandaloneMode {
router.GET(NamespaceListPath, app.AttachNamespace(app.GetNamespacesHandler))
}

Expand Down
11 changes: 6 additions & 5 deletions clients/ui/bff/internal/config/environment.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package config

type EnvConfig struct {
Port int
MockK8Client bool
MockMRClient bool
DevMode bool
DevModePort int
Port int
MockK8Client bool
MockMRClient bool
DevMode bool
StandaloneMode bool
DevModePort int
}
2 changes: 1 addition & 1 deletion clients/ui/frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ COPY . /usr/src/app

RUN npm cache clean --force
RUN npm ci --omit=optional
RUN npm run build
RUN npm run build:prod

FROM nginxinc/nginx-unprivileged

Expand Down
11 changes: 9 additions & 2 deletions clients/ui/frontend/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from '@patternfly/react-core';
import ToastNotifications from '~/shared/components/ToastNotifications';
import { useSettings } from '~/shared/hooks/useSettings';
import { isMUITheme, Theme, AUTH_HEADER, DEV_MODE } from '~/shared/utilities/const';
import { isMUITheme, Theme, AUTH_HEADER, MOCK_AUTH } from '~/shared/utilities/const';
import { logout } from '~/shared/utilities/appUtils';
import NavSidebar from './NavSidebar';
import AppRoutes from './AppRoutes';
Expand All @@ -25,11 +25,17 @@ const App: React.FC = () => {
const {
configSettings,
userSettings,
namespaceSettings,
loaded: configLoaded,
loadError: configError,
} = useSettings();

const username = userSettings?.userId;
const options =
namespaceSettings?.map((namespace) => ({
content: namespace.name,
value: namespace.name,
})) || [];

React.useEffect(() => {
// Apply the theme based on the value of STYLE_THEME
Expand All @@ -41,7 +47,7 @@ const App: React.FC = () => {
}, []);

React.useEffect(() => {
if (DEV_MODE && username) {
if (MOCK_AUTH && username) {
localStorage.setItem(AUTH_HEADER, username);
} else {
localStorage.removeItem(AUTH_HEADER);
Expand Down Expand Up @@ -103,6 +109,7 @@ const App: React.FC = () => {
onLogout={() => {
logout().then(() => window.location.reload());
}}
options={options}
/>
}
isManagedSidebar
Expand Down
30 changes: 22 additions & 8 deletions clients/ui/frontend/src/app/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,34 @@ import {
ToolbarGroup,
ToolbarItem,
} from '@patternfly/react-core';
import { useNavigate } from 'react-router-dom';
import { SimpleSelect, SimpleSelectOption } from '@patternfly/react-templates';
import { useDeepCompareMemoize } from '~/shared/utilities/useDeepCompareMemoize';

interface NavBarProps {
username?: string;
onLogout: () => void;
options: SimpleSelectOption[];
onNamespaceSelect?: (namespace: string) => void;
}

const Options: SimpleSelectOption[] = [{ content: 'All Namespaces', value: 'All' }];

const NavBar: React.FC<NavBarProps> = ({ username, onLogout }) => {
const [selected, setSelected] = React.useState<string | undefined>('All');
const NavBar: React.FC<NavBarProps> = ({ username, onLogout, options, onNamespaceSelect }) => {
const [selected, setSelected] = React.useState<string | undefined>(String(options[0]?.value));
const [userMenuOpen, setUserMenuOpen] = React.useState(false);
const optionsMemo = useDeepCompareMemoize(options);
const navigate = useNavigate();

const initialOptions = React.useMemo<SimpleSelectOption[]>(
() => Options.map((o) => ({ ...o, selected: o.value === selected })),
[selected],
() => optionsMemo.map((o) => ({ ...o, selected: o.value === selected })),
[selected, optionsMemo],
);

React.useEffect(() => {
if (selected) {
navigate(`?namespace=${selected}`);
}
}, [selected, navigate]);

const handleLogout = () => {
setUserMenuOpen(false);
onLogout();
Expand All @@ -51,9 +61,13 @@ const NavBar: React.FC<NavBarProps> = ({ username, onLogout }) => {
<ToolbarGroup variant="action-group-plain" align={{ default: 'alignStart' }}>
<ToolbarItem>
<SimpleSelect
isDisabled
initialOptions={initialOptions}
onSelect={(_ev, selection) => setSelected(String(selection))}
onSelect={(_ev, selection) => {
setSelected(String(selection));
if (onNamespaceSelect) {
onNamespaceSelect(String(selection));
}
}}
/>
</ToolbarItem>
</ToolbarGroup>
Expand Down
7 changes: 5 additions & 2 deletions clients/ui/frontend/src/shared/api/apiUtils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { APIOptions } from '~/shared/api/types';
import { EitherOrNone } from '~/shared/typeHelpers';
import { ModelRegistryBody } from '~/app/types';
import { DEV_MODE, AUTH_HEADER } from '~/shared/utilities/const';
import { AUTH_HEADER, MOCK_AUTH } from '~/shared/utilities/const';

export const mergeRequestInit = (
opts: APIOptions = {},
Expand Down Expand Up @@ -65,11 +65,14 @@ const callRestJSON = <T>(
requestData = JSON.stringify(data);
}

// Workaround if we wanna force in a call to add the AUTH_HEADER
const authHeader = Object.keys(otherOptions.headers || {}).some((key) => key === AUTH_HEADER);

return fetch(`${host}${path}${searchParams ? `?${searchParams}` : ''}`, {
...otherOptions,
headers: {
...otherOptions.headers,
...(DEV_MODE && { [AUTH_HEADER]: localStorage.getItem(AUTH_HEADER) }),
...(MOCK_AUTH && !authHeader && { [AUTH_HEADER]: localStorage.getItem(AUTH_HEADER) }),
...(contentType && { 'Content-Type': contentType }),
},
method,
Expand Down
14 changes: 13 additions & 1 deletion clients/ui/frontend/src/shared/api/k8s.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { handleRestFailures } from '~/shared/api/errorUtils';
import { isModelRegistryResponse, restGET } from '~/shared/api/apiUtils';
import { ModelRegistry } from '~/app/types';
import { BFF_API_VERSION } from '~/app/const';
import { UserSettings } from '~/shared/types';
import { Namespace, UserSettings } from '~/shared/types';

export const getListModelRegistries =
(hostPath: string) =>
Expand All @@ -28,3 +28,15 @@ export const getUser =
throw new Error('Invalid response format');
},
);

export const getNamespaces =
(hostPath: string) =>
(opts: APIOptions): Promise<Namespace[]> =>
handleRestFailures(restGET(hostPath, `/api/${BFF_API_VERSION}/namespaces`, {}, opts)).then(
(response) => {
if (isModelRegistryResponse<Namespace[]>(response)) {
return response.data;
}
throw new Error('Invalid response format');
},
);
39 changes: 30 additions & 9 deletions clients/ui/frontend/src/shared/hooks/useSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,49 @@
import * as React from 'react';
import { USERNAME, POLL_INTERVAL, AUTH_HEADER, DEV_MODE } from '~/shared/utilities/const';
import {
USERNAME,
POLL_INTERVAL,
AUTH_HEADER,
MOCK_AUTH,
isStandalone,
} from '~/shared/utilities/const';
import { useDeepCompareMemoize } from '~/shared/utilities/useDeepCompareMemoize';
import { ConfigSettings, UserSettings } from '~/shared/types';
import { ConfigSettings, Namespace, UserSettings } from '~/shared/types';
import useTimeBasedRefresh from '~/shared/hooks/useTimeBasedRefresh';
import { getUser } from '~/shared/api/k8s';
import { getNamespaces, getUser } from '~/shared/api/k8s';

export const useSettings = (): {
configSettings: ConfigSettings | null;
userSettings: UserSettings | null;
namespaceSettings: Namespace[] | null;
loaded: boolean;
loadError: Error | undefined;
} => {
const [loaded, setLoaded] = React.useState(false);
const [loadError, setLoadError] = React.useState<Error>();
const [config, setConfig] = React.useState<ConfigSettings | null>(null);
const [namespaces, setNamespaces] = React.useState<Namespace[] | null>(null);
const [user, setUser] = React.useState<UserSettings | null>(null);
const userSettings = React.useMemo(() => getUser(''), []);
const userRest = React.useMemo(() => getUser(''), []);
const namespaceRest = React.useMemo(() => getNamespaces(''), []);
const setRefreshMarker = useTimeBasedRefresh();

React.useEffect(() => {
let watchHandle: ReturnType<typeof setTimeout>;
let cancelled = false;
const watchConfig = () => {
const headers = DEV_MODE ? { [AUTH_HEADER]: USERNAME } : undefined;
Promise.all([fetchConfig(), userSettings({ headers })])
.then(([fetchedConfig, fetchedUser]) => {
const headers = MOCK_AUTH ? { [AUTH_HEADER]: USERNAME } : undefined;
Promise.all([
fetchConfig(),
userRest({ headers }),
...(isStandalone() ? [namespaceRest({ headers })] : []),
])
.then(([fetchedConfig, fetchedUser, fetchNamespace]) => {
if (cancelled) {
return;
}
setConfig(fetchedConfig);
setUser(fetchedUser);
setNamespaces(fetchNamespace);
setLoaded(true);
setLoadError(undefined);
})
Expand All @@ -56,12 +70,19 @@ export const useSettings = (): {
cancelled = true;
clearTimeout(watchHandle);
};
}, [setRefreshMarker, userSettings]);
}, [setRefreshMarker, userRest, namespaceRest]);

const retConfig = useDeepCompareMemoize<ConfigSettings | null>(config);
const retUser = useDeepCompareMemoize<UserSettings | null>(user);
const retNamespace = useDeepCompareMemoize<Namespace[] | null>(namespaces);

return { configSettings: retConfig, userSettings: retUser, loaded, loadError };
return {
configSettings: retConfig,
userSettings: retUser,
namespaceSettings: retNamespace,
loaded,
loadError,
};
};

// Mock a settings config call
Expand Down
4 changes: 4 additions & 0 deletions clients/ui/frontend/src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export type CommonConfig = {
featureFlags: FeatureFlag;
};

export type Namespace = {
name: string;
};

export type FeatureFlag = {
modelRegistry: boolean;
};
Expand Down
11 changes: 10 additions & 1 deletion clients/ui/frontend/src/shared/utilities/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,23 @@ export enum Theme {
// Future themes can be added here
}

export enum DeploymentMode {
Standalone = 'standalone',
Integrated = 'integrated',
}

export const isMUITheme = (): boolean => STYLE_THEME === Theme.MUI;
export const isStandalone = (): boolean => DEPLOYMENT_MODE === DeploymentMode.Standalone;
export const isIntegrated = (): boolean => DEPLOYMENT_MODE === DeploymentMode.Integrated;

const STYLE_THEME = process.env.STYLE_THEME || Theme.MUI;
const DEV_MODE = process.env.APP_ENV === 'development';
const MOCK_AUTH = process.env.MOCK_AUTH === 'true';
const DEPLOYMENT_MODE = process.env.DEPLOYMENT_MODE || DeploymentMode.Integrated;
const POLL_INTERVAL = process.env.POLL_INTERVAL ? parseInt(process.env.POLL_INTERVAL) : 30000;
const AUTH_HEADER = process.env.AUTH_HEADER || 'kubeflow-userid';
const USERNAME = process.env.USERNAME || 'user@example.com';
const IMAGE_DIR = process.env.IMAGE_DIR || 'images';
const LOGO_LIGHT = process.env.LOGO || 'logo-light-theme.svg';

export { POLL_INTERVAL, DEV_MODE, AUTH_HEADER, USERNAME, IMAGE_DIR, LOGO_LIGHT };
export { POLL_INTERVAL, DEV_MODE, AUTH_HEADER, USERNAME, IMAGE_DIR, LOGO_LIGHT, MOCK_AUTH };
16 changes: 16 additions & 0 deletions clients/ui/manifests/kubeflow/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

patchesJson6902:
- path: model-registry-ui-deployment.yaml
target:
group: apps
version: v1
kind: Deployment
name: model-registry-ui-deployment
- path: deployment.yaml
target:
group: apps
version: v1
kind: Deployment
name: model-registry-bff-deployment
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- op: add
path: /spec/template/spec/containers/0/args
value:
- "--standalone-mode=false"
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
- op: add
path: /spec/template/spec/containers/0/env
value:
- name: API_URL
value: "http://model-registry-bff-service:4000"
- name: MOCK_AUTH
value: "false"
- name: DEPLOYMENT_MODE
value: "integrated"
Loading

0 comments on commit c9fa4de

Please sign in to comment.