diff --git a/kale/compiler.py b/kale/compiler.py index 8da3e7aa2..8eebf81cc 100644 --- a/kale/compiler.py +++ b/kale/compiler.py @@ -24,7 +24,7 @@ from kale import __version__ as KALE_VERSION from kale.common import graphutils, kfputils, utils from kale.common.imports import get_packages_to_install -from kale.pipeline import DEFAULT_BASE_IMAGE, Pipeline, PipelineParam, Step +from kale.pipeline import Pipeline, PipelineParam, Step log = logging.getLogger(__name__) @@ -204,7 +204,7 @@ def _encode_source(s): step_inputs=step_inputs, step_outputs=step_outputs, kfp_dsl_artifact_imports=KFP_DSL_ARTIFACT_IMPORTS, - default_base_image=DEFAULT_BASE_IMAGE, + default_base_image=self.pipeline.config.base_image, **self.pipeline.config.to_dict(), ) return autopep8.fix_code(fn_code) diff --git a/kale/pipeline.py b/kale/pipeline.py index a327b9650..2fe9adcb4 100644 --- a/kale/pipeline.py +++ b/kale/pipeline.py @@ -17,8 +17,6 @@ import logging import os -from kubernetes.client.rest import ApiException -from kubernetes.config import ConfigException import networkx as nx from kale.common import graphutils, podutils, utils @@ -134,16 +132,18 @@ def _randomize_pipeline_name(self): self.pipeline_name = f"{self.pipeline_name}-{utils.random_string()}" def _set_base_image(self): - if not self.base_image: - try: - self.base_image = podutils.get_docker_base_image() - except (ConfigException, RuntimeError, FileNotFoundError, ApiException): - # * ConfigException: no K8s config found - # * RuntimeError, FileNotFoundError: this is not running in a - # pod - # * ApiException: K8s call to read pod raised exception; - # Use kfp default image - self.base_image = DEFAULT_BASE_IMAGE + # Priority 1: Settings (already in self.base_image) + if self.base_image: + return + + # Priority 2: Environment variable + env_image = os.getenv("DEFAULT_BASE_IMAGE") + if env_image: + self.base_image = env_image + return + + # Priority 3: Default + self.base_image = DEFAULT_BASE_IMAGE def _set_volume_storage_class(self): if not self.storage_class_name: diff --git a/kale/processors/nbprocessor.py b/kale/processors/nbprocessor.py index e530adba1..d28868ce0 100644 --- a/kale/processors/nbprocessor.py +++ b/kale/processors/nbprocessor.py @@ -376,7 +376,7 @@ def parse_notebook(self): limits=tags.get("limits", {}), labels=tags.get("labels", {}), annotations=tags.get("annotations", {}), - base_image=tags.get("base_image", ""), + base_image=tags.get("base_image") or self.pipeline.config.base_image, enable_caching=tags.get("enable_caching"), ) self.pipeline.add_step(step) diff --git a/labextension/schema/deploymentPanel.json b/labextension/schema/deploymentPanel.json new file mode 100644 index 000000000..a493be7d1 --- /dev/null +++ b/labextension/schema/deploymentPanel.json @@ -0,0 +1,13 @@ +{ + "title": "Kale Deployment Panel Settings", + "description": "Settings for default base image configuration", + "type": "object", + "properties": { + "defaultBaseImage": { + "type": "string", + "title": "Default Base Image", + "description": "Default Docker image used for pipeline steps", + "default": "python:3.12" + } + } +} diff --git a/labextension/src/widget.tsx b/labextension/src/widget.tsx index 535742a38..7dbb57049 100644 --- a/labextension/src/widget.tsx +++ b/labextension/src/widget.tsx @@ -11,7 +11,6 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - import { JupyterFrontEnd, JupyterFrontEndPlugin, @@ -55,10 +54,6 @@ export const KALE_PANEL_ID = 'jupyterlab-kubeflow-kale/kubeflowDeployment'; const id = 'jupyterlab-kubeflow-kale:deploymentPanel'; -const KALE_SETTINGS_PLUGIN_ID = 'jupyterlab-kubeflow-kale:kale-settings'; -const ENABLE_KALE_BY_DEFAULT_KEY = 'enableKaleByDefault'; -const AUTO_SAVE_ON_COMPILE_OR_RUN_KEY = 'autoSaveOnCompileOrRun'; - const kaleIcon = new LabIcon({ name: 'kale:logo', svgstr: kaleIconSvg }); let kalePanelWidget: ReactWidget | undefined; @@ -109,6 +104,31 @@ async function activate( // TODO: backend can become an Enum that indicates the type of // env we are in (like Local Laptop, MiniKF, GCP, UI without Kale, ...) const backend = await getBackend(kernel); + const settings = await settingRegistry.load( + 'jupyterlab-kubeflow-kale:deploymentPanel', + ); + + let defaultBaseImage = + (settings.get('default_base_image').composite as string) || 'python:3.12'; + + settings.changed.connect(() => { + defaultBaseImage = + (settings.get('default_base_image').composite as string) || ''; + + if (kalePanelWidget) { + const newWidget = createPanel(defaultBaseImage); + + newWidget.id = KALE_PANEL_ID; + newWidget.title.icon = kaleIcon; + newWidget.title.caption = 'Kubeflow Pipelines Deployment Panel'; + newWidget.node.classList.add('kale-panel'); + + labShell.add(newWidget, 'left'); + + kalePanelWidget = newWidget; + } + }); + if (backend) { try { await executeRpc(kernel, 'log.setup_logging'); @@ -118,71 +138,6 @@ async function activate( } } - // Load and react to Kale JupyterLab settings - const SettingsAwareLeftPanel = () => { - const [kaleSettings, setKaleSettings] = React.useState({ - enableKaleByDefault: false, - autoSaveOnCompileOrRun: false, - }); - - React.useEffect(() => { - let disposed = false; - let setting: any | null = null; - let onSettingChanged: (() => void) | null = null; - - settingRegistry - .load(KALE_SETTINGS_PLUGIN_ID) - .then(loadedSetting => { - setting = loadedSetting; - - const read = () => ({ - enableKaleByDefault: - (loadedSetting.get(ENABLE_KALE_BY_DEFAULT_KEY).composite as - | boolean - | undefined) ?? false, - autoSaveOnCompileOrRun: - (loadedSetting.get(AUTO_SAVE_ON_COMPILE_OR_RUN_KEY).composite as - | boolean - | undefined) ?? false, - }); - - const update = () => { - if (disposed) { - return; - } - setKaleSettings(read()); - }; - - update(); - onSettingChanged = () => update(); - (loadedSetting.changed as any).connect(onSettingChanged); - }) - .catch(reason => { - console.error('Failed to load Kale settings:', reason); - }); - - return () => { - disposed = true; - if (setting && onSettingChanged) { - (setting.changed as any).disconnect(onSettingChanged); - } - }; - }, []); - - return ( - setLeftPanelRef(ref)} - lab={lab} - tracker={tracker} - docManager={docManager} - backend={backend} - kernel={kernel} - enableKaleByDefault={kaleSettings.enableKaleByDefault} - autoSaveOnCompileOrRun={kaleSettings.autoSaveOnCompileOrRun} - /> - ); - }; - async function loadPanel() { let reveal_widget = undefined; if (backend) { @@ -208,12 +163,26 @@ async function activate( kalePanelWidget.activate(); } } - + function createPanel(defaultBaseImage: string) { + return ReactWidget.create( + setLeftPanelRef(ref)} + lab={lab} + tracker={tracker} + docManager={docManager} + backend={backend} + kernel={kernel} + enableKaleByDefault={false} + autoSaveOnCompileOrRun={false} + defaultBaseImage={defaultBaseImage} + />, + ); + } // Creates the left side bar widget once the app has fully started lab.started.then(() => { // show list of commands in the commandRegistry // console.log(lab.commands.listCommands()); - kalePanelWidget = ReactWidget.create(); + kalePanelWidget = createPanel(defaultBaseImage); kalePanelWidget.id = KALE_PANEL_ID; kalePanelWidget!.title.icon = kaleIcon; kalePanelWidget!.title.caption = 'Kubeflow Pipelines Deployment Panel'; diff --git a/labextension/src/widgets/LeftPanel.tsx b/labextension/src/widgets/LeftPanel.tsx index a91bb9b02..153759dfb 100644 --- a/labextension/src/widgets/LeftPanel.tsx +++ b/labextension/src/widgets/LeftPanel.tsx @@ -61,6 +61,7 @@ interface IProps { kernel: Kernel.IKernelConnection; enableKaleByDefault: boolean; autoSaveOnCompileOrRun: boolean; + defaultBaseImage?: string; } interface IState { @@ -789,6 +790,16 @@ export class KubeflowKaleLeftPanel extends React.Component { {pipeline_desc_input} {enable_caching_toggle} + { + this.setState({ defaultBaseImage: v }); + this.updateDockerImage(v); + }} + />
{ Base Image for Step

- Default:{' '} + System Default:{' '} {this.props.defaultBaseImage || DEFAULT_BASE_IMAGE}

+ +

+ Pipeline Default:{' '} + + {this.props.pipelineBaseImage || + 'Not set (uses system default)'} + +

this.updateBaseImage(v)} - placeholder={ - this.props.pipelineBaseImage || - this.props.defaultBaseImage || - DEFAULT_BASE_IMAGE - } + placeholder="e.g. python:3.12" style={{ width: '100%', marginTop: '8px' }} />