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

chore(controller): refine the exposed links #2504

Merged
merged 1 commit into from
Jul 14, 2023
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
26 changes: 26 additions & 0 deletions console/src/domain/job/components/ExposedLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react'
import { ExposedLinkType, IExposedLinkSchema } from '@job/schemas/job'
import IconFont, { IconTypesT } from '@starwhale/ui/IconFont'

export interface IExposedLinkProps {
data: IExposedLinkSchema
}

const VSCODE_NAME = 'VS_CODE'

function ExposedLink({ data: { type, name, link } }: IExposedLinkProps) {
let font: IconTypesT = 'global'
let title = name
if (type === ExposedLinkType.DEV_MODE && name === VSCODE_NAME) {
font = 'vscode'
title = 'vscode'
}

return (
<a key={link} target='_blank' href={link} rel='noreferrer' title={title}>
<IconFont type={font} size={16} />
</a>
)
}

export default ExposedLink
13 changes: 12 additions & 1 deletion console/src/domain/job/schemas/job.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ export enum JobStatusType {
UNKNOWN = 'UNKNOWN',
}

export enum ExposedLinkType {
DEV_MODE = 'DEV_MODE',
WEB_HANDLER = 'WEB_HANDLER',
}

export interface IExposedLinkSchema {
type: ExposedLinkType
name: string
link: string
}

export interface IJobSchema extends IResourceSchema {
uuid: string
name: string
Expand All @@ -39,7 +50,7 @@ export interface IJobSchema extends IResourceSchema {
stopTime?: number
createdTime?: number
pinnedTime?: number
exposedLinks?: string[]
exposedLinks?: IExposedLinkSchema[]
isTimeToLiveInSec?: boolean
timeToLiveInSec?: number
}
Expand Down
20 changes: 5 additions & 15 deletions console/src/pages/Job/JobListCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ import JobStatus from '@/domain/job/components/JobStatus'
import Button from '@starwhale/ui/Button'
import { useAuthPrivileged, WithCurrentAuth } from '@/api/WithAuth'
import { IconTooltip } from '@starwhale/ui/Tooltip'
import IconFont from '@starwhale/ui/IconFont'
import { useProjectRole } from '@project/hooks/useProjectRole'
import { ConfigurationOverride } from '@starwhale/ui/base/helpers/overrides'
import { ConfirmButton } from '@starwhale/ui'
import ExposedLink from '@job/components/ExposedLink'

interface IActionButtonProps {
jobId: string
Expand Down Expand Up @@ -226,21 +226,11 @@ export default function JobListCard() {
typeof job.duration === 'string' ? '-' : durationToStr(job.duration),
job?.stopTime && job?.stopTime > 0 ? formatTimestampDateTime(job?.stopTime) : '-',
<JobStatus key='jobStatus' status={job.jobStatus as any} />,
<div key='action' style={{ display: 'flex', gap: '8px' }}>
<div key='action' style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
{actions[job.jobStatus] ?? ''}
{job.exposedLinks?.map((link) => {
return (
<a
key={link}
target='_blank'
href={link}
rel='noreferrer'
title={t('job.expose.title')}
>
<IconFont type='global' size={16} />
</a>
)
})}
{job.exposedLinks?.map((exposed) => (
<ExposedLink key={exposed.link} data={exposed} />
))}
</div>,
]
}) ?? []
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2022 Starwhale, Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* 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.
*/

package ai.starwhale.mlops.api.protocol.job;

import ai.starwhale.mlops.domain.job.step.ExposedType;
import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class ExposedLinkVo {
private ExposedType type;
private String name;
private String link;
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public class JobVo implements Serializable {
// expose links is used to get the serving url of the model, it may contain:
// 1. vscode url when the model is running under dev mode
// 2. serving url when the model is running a web handler (which using the non-zero expose handler decorator)
private List<String> exposedLinks;
private List<ExposedLinkVo> exposedLinks;

@JsonProperty("duration")
public Long getDuration() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,22 @@
package ai.starwhale.mlops.domain.job.converter;


import ai.starwhale.mlops.api.protocol.job.ExposedLinkVo;
import ai.starwhale.mlops.api.protocol.job.JobVo;
import ai.starwhale.mlops.api.protocol.runtime.RuntimeVo;
import ai.starwhale.mlops.api.protocol.user.UserVo;
import ai.starwhale.mlops.common.IdConverter;
import ai.starwhale.mlops.common.proxy.WebServerInTask;
import ai.starwhale.mlops.domain.dataset.DatasetDao;
import ai.starwhale.mlops.domain.dataset.bo.DatasetVersion;
import ai.starwhale.mlops.domain.job.DevWay;
import ai.starwhale.mlops.domain.job.bo.Job;
import ai.starwhale.mlops.domain.job.cache.HotJobHolder;
import ai.starwhale.mlops.domain.job.po.JobEntity;
import ai.starwhale.mlops.domain.job.spec.JobSpecParser;
import ai.starwhale.mlops.domain.job.spec.StepSpec;
import ai.starwhale.mlops.domain.job.status.JobStatus;
import ai.starwhale.mlops.domain.job.step.ExposedType;
import ai.starwhale.mlops.domain.runtime.RuntimeService;
import ai.starwhale.mlops.domain.system.SystemSettingService;
import ai.starwhale.mlops.domain.task.bo.Task;
Expand All @@ -40,12 +43,12 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import reactor.function.Consumer4;

@Component
public class JobConverter {
Expand Down Expand Up @@ -99,38 +102,38 @@ private List<String> findDatasetVersionNamesByJobId(Long jobId) {
* @param jobId job id
* @return exposed links
*/
private List<String> generateJobExposedLinks(Long jobId) {
var links = new ArrayList<String>();
private List<ExposedLinkVo> generateJobExposedLinks(Long jobId) {
var exposed = new ArrayList<ExposedLinkVo>();
var jobs = hotJobHolder.ofIds(List.of(jobId));
if (CollectionUtils.isEmpty(jobs)) {
return links;
return exposed;
}
var job = jobs.stream().findAny().get();

// only running job should generate exposed links
var jobStatus = job.getStatus();
if (jobStatus != JobStatus.RUNNING) {
return links;
return exposed;
}
// check if job has exposed port in step spec
var stepSpecStr = job.getStepSpec();
if (!StringUtils.hasText(stepSpecStr)) {
return links;
return exposed;
}

List<StepSpec> stepSpecs;
try {
stepSpecs = jobSpecParser.parseAndFlattenStepFromYaml(stepSpecStr);
} catch (JsonProcessingException e) {
return links;
return exposed;
}
if (CollectionUtils.isEmpty(stepSpecs)) {
return links;
return exposed;
}

var steps = job.getSteps();

BiConsumer<Task, Integer> addRunningTask = (task, port) -> {
Consumer4<Task, Integer, ExposedType, String> addRunningTask = (task, port, type, name) -> {
if (task.getStatus() != TaskStatus.RUNNING) {
return;
}
Expand All @@ -139,13 +142,13 @@ private List<String> generateJobExposedLinks(Long jobId) {
return;
}
var link = webServerInTask.generateGatewayUrl(task.getId(), task.getIp(), port);
links.add(link);
exposed.add(ExposedLinkVo.builder().type(type).name(name).link(link).build());
};

// dev mode
if (job.isDevMode()) {
job.getSteps().stream().flatMap(s -> s.getTasks().stream())
.forEach(task -> addRunningTask.accept(task, devPort));
.forEach(task -> addRunningTask.accept(task, devPort, ExposedType.DEV_MODE, DevWay.VS_CODE.name()));
}

// web handler
Expand All @@ -165,10 +168,17 @@ private List<String> generateJobExposedLinks(Long jobId) {
if (CollectionUtils.isEmpty(tasks)) {
return;
}
tasks.forEach(task -> addRunningTask.accept(task, exposedPort));
String name;
// https://github.com/star-whale/starwhale/blob/c924313166d065ab941a99fa1dec04b7bfbe5fa7/client/starwhale/core/model/model.py#L246-L252
if (Boolean.TRUE.equals(stepSpec.getVirtual()) && stepSpec.getName().equals("serving")) {
name = "online evaluation";
} else {
name = stepSpec.getName();
}
tasks.forEach(t -> addRunningTask.accept(t, exposedPort, ExposedType.WEB_HANDLER, name));
});

return links;
return exposed;
}

public JobVo convert(Job job) throws ConvertException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2022 Starwhale, Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* 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.
*/

package ai.starwhale.mlops.domain.job.step;

public enum ExposedType {
DEV_MODE,
WEB_HANDLER,
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import static org.mockito.BDDMockito.mock;
import static org.mockito.Mockito.when;

import ai.starwhale.mlops.api.protocol.job.ExposedLinkVo;
import ai.starwhale.mlops.api.protocol.runtime.RuntimeVo;
import ai.starwhale.mlops.api.protocol.user.UserVo;
import ai.starwhale.mlops.common.IdConverter;
Expand All @@ -39,6 +40,7 @@
import ai.starwhale.mlops.domain.job.spec.JobSpecParser;
import ai.starwhale.mlops.domain.job.spec.StepSpec;
import ai.starwhale.mlops.domain.job.status.JobStatus;
import ai.starwhale.mlops.domain.job.step.ExposedType;
import ai.starwhale.mlops.domain.job.step.bo.Step;
import ai.starwhale.mlops.domain.model.po.ModelVersionEntity;
import ai.starwhale.mlops.domain.runtime.RuntimeService;
Expand Down Expand Up @@ -166,13 +168,15 @@ public void testConvertJobExposedLinks() throws JsonProcessingException {
entity.setJobStatus(JobStatus.RUNNING);
job.setStatus(JobStatus.RUNNING);
res = jobConvertor.convert(entity);
assertThat(res.getExposedLinks(), is(List.of("/foo/bar/")));
var expectWeb = ExposedLinkVo.builder().type(ExposedType.WEB_HANDLER).link("/foo/bar/").name("step").build();
assertThat(res.getExposedLinks(), is(List.of(expectWeb)));

// get debug mode links when dev mode is on
when(webServerInTask.generateGatewayUrl(7L, "1.1.1.1", 1234)).thenReturn("/baz/");
entity.setDevMode(true);
job.setDevMode(true);
res = jobConvertor.convert(entity);
assertThat(res.getExposedLinks(), is(List.of("/baz/", "/foo/bar/")));
var expectDevMode = ExposedLinkVo.builder().type(ExposedType.DEV_MODE).link("/baz/").name("VS_CODE").build();
assertThat(res.getExposedLinks(), is(List.of(expectDevMode, expectWeb)));
}
}
Loading