Skip to content

Commit

Permalink
fix:permission change check for launchpad (#5072)
Browse files Browse the repository at this point in the history
* update desktop billing

* add checkPermission api

* Change Location

* update
  • Loading branch information
zjy365 authored Sep 12, 2024
1 parent 1decf9a commit e91bce9
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 94 deletions.
13 changes: 9 additions & 4 deletions frontend/desktop/src/pages/api/desktop/getBilling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import { jsonRes } from '@/services/backend/response';
import { switchKubeconfigNamespace } from '@/utils/switchKubeconfigNamespace';
import type { NextApiRequest, NextApiResponse } from 'next';

type ConsumptionResult = {
allAmount: number;
regionAmount: Record<string, number>;
};

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const payload = await verifyAccessToken(req.headers);
Expand All @@ -25,9 +30,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
timeOneMonthAgo.setMonth(currentTime.getMonth() - 1);

const base = global.AppConfig.desktop.auth.billingUrl as string;
const consumptionUrl = base + '/account/v1alpha1/costs/consumption';
const consumptionUrl = base + '/account/v1alpha1/costs/all-region-consumption';

const results = await Promise.all([
const results: ConsumptionResult[] = await Promise.all([
(
await fetch(consumptionUrl, {
method: 'POST',
Expand Down Expand Up @@ -56,8 +61,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)

jsonRes(res, {
data: {
prevMonthTime: results[0].amount || 0,
prevDayTime: results[1].amount || 0
prevMonthTime: results[0].allAmount || 0,
prevDayTime: results[1].allAmount || 0
}
});
} catch (err: any) {
Expand Down
4 changes: 3 additions & 1 deletion frontend/providers/applaunchpad/src/api/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export const getInitData = () => GET<InitDataType>('/api/platform/getInitData');

export const getUserQuota = () =>
GET<{
balance: string;
quota: UserQuotaItemType[];
}>('/api/platform/getQuota');

Expand All @@ -26,3 +25,6 @@ export const updateDesktopGuide = (payload: UpdateUserGuideParams) =>
export const getUserAccount = () => GET<AccountCRD>('/api/guide/getAccount');

export const getPriceBonus = () => GET('/api/guide/getBonus');

export const checkPermission = (payload: { appName: string; resourceType: 'deploy' | 'sts' }) =>
GET('/api/platform/checkPermission', payload);
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { ApiResp } from '@/services/kubernet';
import { authSession } from '@/services/backend/auth';
import { getK8s } from '@/services/backend/kubernetes';
import { jsonRes } from '@/services/backend/response';
import * as k8s from '@kubernetes/client-node';

export default async function handler(req: NextApiRequest, res: NextApiResponse<ApiResp>) {
try {
const { appName, resourceType } = req.query as {
appName: string;
resourceType: 'deploy' | 'sts';
};

if (!appName || !resourceType) {
throw new Error('appName or resourceType is empty');
}

const { k8sApp, namespace } = await getK8s({
kubeconfig: await authSession(req.headers)
});

const patchBody = {
metadata: {
annotations: {
'update-time': new Date().toISOString()
}
}
};

const options = {
headers: { 'Content-type': k8s.PatchUtils.PATCH_FORMAT_STRATEGIC_MERGE_PATCH }
};

if (resourceType === 'deploy') {
await k8sApp.patchNamespacedDeployment(
appName,
namespace,
patchBody,
undefined,
undefined,
undefined,
undefined,
undefined,
options
);
} else if (resourceType === 'sts') {
await k8sApp.patchNamespacedStatefulSet(
appName,
namespace,
patchBody,
undefined,
undefined,
undefined,
undefined,
undefined,
options
);
}

jsonRes(res, {
code: 200,
data: 'success'
});
} catch (err: any) {
if (err?.body?.code === 403 && err?.body?.message.includes('40001')) {
return jsonRes(res, {
code: 200,
data: 'insufficient_funds',
message: err?.body?.message
});
}

jsonRes(res, {
code: 500,
error: err?.body
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export const defaultAppConfig: AppConfigType = {
components: {
monitor: {
url: 'http://launchpad-monitor.sealos.svc.cluster.local:8428'
},
billing: {
url: 'http://account-service.account-system.svc:2333'
}
},
appResourceFormSliderConfig: {
Expand All @@ -62,7 +65,7 @@ process.on('uncaughtException', (err) => {

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
if (!global.AppConfig || process.env.NODE_ENV !== 'production') {
if (!global.AppConfig) {
const filename =
process.env.NODE_ENV === 'development' ? 'data/config.yaml.local' : '/app/data/config.yaml';
const res: any = yaml.load(readFileSync(filename, 'utf-8'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,8 @@ import { authSession } from '@/services/backend/auth';
import { getK8s } from '@/services/backend/kubernetes';
import { jsonRes } from '@/services/backend/response';
import { UserQuotaItemType } from '@/types/user';
import Decimal from 'decimal.js';
import type { NextApiRequest, NextApiResponse } from 'next';

async function getAmount(req: NextApiRequest): Promise<{
data?: {
balance: number;
deductionBalance: number;
};
}> {
const domain = global.AppConfig.cloud.domain;
const base = `https://account-api.${domain}`;

if (!base) throw Error('not base url');
const { kube_user, kc } = await getK8s({
kubeconfig: await authSession(req.headers)
});

if (kube_user === null) {
return { data: undefined };
}

const body = JSON.stringify({
kubeConfig: kc.exportConfig()
});

const response = await fetch(base + '/account/v1alpha1/account', {
method: 'POST',
body,
headers: {
'Content-Type': 'application/json'
}
});

const data = (await response.json()) as {
account?: {
UserUID: string;
ActivityBonus: number;
EncryptBalance: string;
EncryptDeductionBalance: string;
CreatedAt: Date;
Balance: number;
DeductionBalance: number;
};
};

if (!kc || !data?.account) return { data: undefined };

return {
data: {
balance: data.account.Balance,
deductionBalance: data.account.DeductionBalance
}
};
}

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
// source price
Expand All @@ -68,26 +15,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const gpuEnabled = global.AppConfig.common.gpuEnabled;
const filteredQuota = gpuEnabled ? quota : quota.filter((item) => item.type !== 'gpu');

let balance = '0';
try {
const { data } = await getAmount(req);
if (data) {
balance = new Decimal(data.balance)
.minus(new Decimal(data.deductionBalance))
.dividedBy(1000000)
.toFixed(2);
}
} catch (error) {
console.log(error, 'getAmount Error');
}

jsonRes<{
balance: string;
quota: UserQuotaItemType[];
}>(res, {
data: {
quota: filteredQuota,
balance
quota: filteredQuota
}
});
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,11 @@ function countGpuSource(rawData: ResourcePriceType['data']['properties'], gpuNod
}

const getResourcePrice = async () => {
const res = await fetch(
`https://account-api.${global.AppConfig.cloud.domain}/account/v1alpha1/properties`,
{
method: 'POST'
}
);
const url = global.AppConfig.launchpad.components.billing.url;

const res = await fetch(`${url}/account/v1alpha1/properties`, {
method: 'POST'
});
const data: ResourcePriceType = await res.json();

return data.data.properties;
Expand Down
35 changes: 24 additions & 11 deletions frontend/providers/applaunchpad/src/pages/app/edit/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { postDeployApp, putApp } from '@/api/app';
import { updateDesktopGuide } from '@/api/platform';
import { checkPermission, updateDesktopGuide } from '@/api/platform';
import { defaultSliderKey } from '@/constants/app';
import { defaultEditVal, editModeMap } from '@/constants/editApp';
import { useConfirm } from '@/hooks/useConfirm';
Expand Down Expand Up @@ -98,7 +98,7 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) =>
const [forceUpdate, setForceUpdate] = useState(false);
const { setAppDetail } = useAppStore();
const { screenWidth, formSliderListConfig } = useGlobalStore();
const { userSourcePrice, loadUserSourcePrice, checkQuotaAllow, balance } = useUserStore();
const { userSourcePrice, loadUserSourcePrice, checkQuotaAllow } = useUserStore();
const { title, applyBtnText, applyMessage, applySuccess, applyError } = editModeMap(!!appName);
const [yamlList, setYamlList] = useState<YamlItemType[]>([]);
const [errorMessage, setErrorMessage] = useState('');
Expand Down Expand Up @@ -320,18 +320,10 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) =>
applyBtnText={applyBtnText}
applyCb={() => {
closeGuide();
formHook.handleSubmit((data) => {
formHook.handleSubmit(async (data) => {
const parseYamls = formData2Yamls(data);
setYamlList(parseYamls);

// balance check
if (balance <= 0) {
return toast({
status: 'warning',
title: t('user.Insufficient account balance')
});
}

// gpu inventory check
if (data.gpu?.type) {
const inventory = countGpuInventory(data.gpu?.type);
Expand Down Expand Up @@ -362,6 +354,27 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) =>
});
}

// check permission
if (appName) {
try {
const result = await checkPermission({
appName: data.appName,
resourceType: !!data.storeList?.length ? 'sts' : 'deploy'
});
if (result === 'insufficient_funds') {
return toast({
status: 'warning',
title: t('user.Insufficient account balance')
});
}
} catch (error: any) {
return toast({
status: 'warning',
title: error
});
}
}

openConfirm(() => submitSuccess(parseYamls))();
}, submitError)();
}}
Expand Down
2 changes: 0 additions & 2 deletions frontend/providers/applaunchpad/src/store/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type { userPriceType } from '@/types/user';
import { AppEditType } from '@/types/app';

type State = {
balance: number;
userQuota: UserQuotaItemType[];
loadUserQuota: () => Promise<null>;
userSourcePrice: userPriceType | undefined;
Expand Down Expand Up @@ -45,7 +44,6 @@ export const useUserStore = create<State>()(
const response = await getUserQuota();
set((state) => {
state.userQuota = response.quota;
state.balance = parseFloat(response.balance);
});
return null;
},
Expand Down
3 changes: 3 additions & 0 deletions frontend/providers/applaunchpad/src/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export type AppConfigType = {
monitor: {
url: string;
};
billing: {
url: string;
};
};
appResourceFormSliderConfig: FormSliderListType;
fileManger: FileMangerType;
Expand Down

0 comments on commit e91bce9

Please sign in to comment.