Skip to content
Open
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
19 changes: 12 additions & 7 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,22 @@
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"argo-rollouts": "git+https://github.com/argoproj/argo-rollouts.git#a4d50d8e65d5fa33a6e7f33f43be9a3df83b06f7",
"argo-rollouts": "git+https://github.com/sergeyshevch/argo-rollouts.git#f2824c42a3b5c54526f1c8a1359234398d7b2003",
"argo-ui": "git+https://github.com/argoproj/argo-ui.git#5ff344ac9692c14dd108468bd3c020c3c75181cb",
"axios": "^1.6.2",
"classnames": "2.2.6",
"isomorphic-fetch": "^3.0.0",
"json-loader": "^0.5.7",
"axios": "^1.6.2",
"recharts": "^2.9.0",
"null-loader": "^4.0.1",
"react": "^16.9.3",
"react-dom": "^16.9.3",
"react-helmet": "^6.1.0",
"react-router-dom": "5.2.0",
"react-scripts": "4.0.3",
"recharts": "^2.9.0",
"ts-loader": "^8.2.0",
"typescript": "^4.9.5",
"web-vitals": "^1.0.1",
"isomorphic-fetch": "^3.0.0"
"web-vitals": "^1.0.1"
},
"peerDeependencies": {
"argo-ui": "git+https://github.com/argoproj/argo-ui.git#5ff344ac9692c14dd108468bd3c020c3c75181cb",
Expand All @@ -30,6 +30,7 @@
},
"scripts": {
"start": "NODE_OPTIONS=--openssl-legacy-provider webpack --config ./webpack.config.js --watch",
"serve": "webpack-dev-server --config ./webpack.config.js --mode development",
"build": "NODE_OPTIONS=--openssl-legacy-provider webpack --config ./webpack.config.js && tar -C dist -cvf dist/extension.tar resources"
},
"eslintConfig": {
Expand Down Expand Up @@ -58,9 +59,13 @@
"raw-loader": "^4.0.2",
"react-keyhooks": "^0.2.3",
"rxjs": "^7.1.0",
"sass": "^1.34.1",
"ts-loader": "8.2.0",
"webpack-cli": "^4.9.2"
"sass": "^1.49.9",
"sass-loader": "^14.2.1",
"style-loader": "^1",
"webpack": "^5.94.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.4"
},
"resolutions": {
"@types/react": "16.9.3"
Expand Down
42 changes: 42 additions & 0 deletions ui/src/components/AnalysisRun.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as React from 'react';
import { AnalysisWidget } from 'argo-rollouts/ui/src/app/components/analysis-modal/analysis-widget';
// import { RolloutAnalysisRunInfo } from 'argo-rollouts/ui/src/models/rollout/generated';
import { State, ApplicationResourceTree } from '../shared';


export const AnalysisRunExtension = (props: { application: any; tree: ApplicationResourceTree; resource: State }) => {
// const ro = parseInfoFromResourceNode(props.application, props.tree, props.resource);
// const an = parseAnalysisRuns

console.log(props.resource);

const { resource } = props

const analysis = {
objectMeta: {
creationTimestamp: {
seconds: resource.metadata.creationTimestamp
},
name: resource.metadata.name,
namespace: resource.metadata.namespace,
resourceVersion: resource.metadata.resourceVersion,
uid: resource.metadata.uid
},
specAndStatus: {
spec: resource.spec,
status: resource.status || null
},
// revision: parseRevision(node),
// status: parseAnalysisRunStatus(node.health.status)
};

return (
<div style={{ display: 'flex', margin: '0 auto' }}>
<div className='rollout__row rollout__row--top'>
<div className='info' style={{ width: '100%' }}>
<AnalysisWidget analysis={analysis} images={[]} revision={''} />
</div>
</div>
</div>
);
};
262 changes: 262 additions & 0 deletions ui/src/components/Rollout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
import * as React from 'react';
import { RolloutWidget } from 'argo-rollouts/ui/src/app/components/rollout/rollout';
import { RolloutAnalysisRunInfo, RolloutReplicaSetInfo, RolloutRolloutInfo } from 'argo-rollouts/ui/src/models/rollout/generated';
import { default as axios } from 'axios';
import { ApplicationResourceTree, State } from '../shared';

const parseInfoFromResourceNode = (app: any, tree: any, resource: State) => {
const ro: RolloutRolloutInfo = {};
const { spec, status, metadata } = resource;
ro.objectMeta = metadata as any;

ro.analysisRuns = parseAnalysisRuns(app, tree, resource);

ro.replicaSets = parseReplicaSets(tree, resource);

if (spec.strategy.canary) {
ro.strategy = 'Canary';
const steps = spec.strategy?.canary?.steps || [];
ro.steps = steps;

if (steps && status.currentStepIndex !== null && steps.length > 0) {
ro.step = `${status.currentStepIndex}/${steps.length}`;
}

const { currentStep, currentStepIndex } = parseCurrentCanaryStep(resource);
ro.setWeight = parseCurrentSetWeight(resource, currentStepIndex);

ro.actualWeight = '0';

if (!currentStep) {
ro.actualWeight = '100';
} else if (status.availableReplicas > 0) {
if (!spec.strategy.canary.trafficRouting) {
for (const rs of ro.replicaSets) {
if (rs.canary) {
ro.actualWeight = `${rs.available / status.availableReplicas}`;
}
}
} else {
ro.actualWeight = ro.setWeight;
}
}
} else {
ro.strategy = 'BlueGreen';
}

ro.containers = [];
if (spec.template) {
for (const c of spec.template?.spec?.containers) {
ro.containers.push({ name: c.name, image: c.image });
}
}

ro.current = status.replicas;
ro.available = status.availableReplicas;
return ro;
};

const parseCurrentCanaryStep = (resource: State): { currentStep: any; currentStepIndex: number } => {
const { status, spec } = resource;
const canary = spec.strategy?.canary;
if (!canary || !canary.steps || canary.steps.length === 0) {
return { currentStep: null, currentStepIndex: -1 };
}
let currentStepIndex = 0;
if (status.currentStepIndex) {
currentStepIndex = status.currentStepIndex;
}
if (canary?.steps?.length <= currentStepIndex) {
return { currentStep: null, currentStepIndex };
}
const currentStep = canary?.steps[currentStepIndex];
return { currentStep, currentStepIndex };
};

const parseCurrentSetWeight = (resource: State, currentStepIndex: number): string => {
const { status, spec } = resource;
if (status.abort) {
return '0';
}

for (let i = currentStepIndex; i >= 0; i--) {
const step = spec.strategy?.canary?.steps[i];
if (step?.setWeight) {
return step.setWeight;
}
}
return '0';
};

const parseRevision = (node: any) => {
for (const item of node.info || []) {
if (item.name === 'Revision') {
const parts = item.value.split(':') || [];
return parts.length === 2 ? parts[1] : '0';
}
}
};

const parsePodStatus = (pod: any) => {
for (const item of pod.info || []) {
if (item.name === 'Status Reason') {
return item.value;
}
}
};

const parsePodReady = (pod: any) => {
for (const item of pod.info || []) {
if (item.name === "Containers") {
return item.value;
}
}
}

const parseAnalysisRuns = (app: any, tree: any, rollout: any): RolloutAnalysisRunInfo[] => {
const [analysisRunResults, setAnalysisRunResults] = React.useState<RolloutAnalysisRunInfo[]>([]);
const [analysisRunNodeIds, setAnalysisRunNodeIds] = React.useState<string[]>([]);
const [isRefresh, setIsRefresh] = React.useState<boolean>(false);

// Get the list of AnalysisRun node IDs whenever the tree or rollout props change
React.useMemo(() => {
const filteredNodes = tree.nodes.filter(node => node.kind === 'AnalysisRun' && node.parentRefs.some(ref => ref.name === rollout.metadata.name));
const nodeIds = filteredNodes.map(node => node.uid);

// Check if there are any new AnalysisRun node IDs or if the count has changed from previous node IDs
if (nodeIds.length !== analysisRunNodeIds.length || !analysisRunNodeIds.every((value, index) => value === nodeIds[index])) {
setIsRefresh(true);
}
setAnalysisRunNodeIds(nodeIds);
}, [tree.nodes]);

const rolloutAnalysisRunInfo = async () => {
const promises: Promise<RolloutAnalysisRunInfo>[] = analysisRunNodeIds.map(async nodeId => {
const node: any = tree.nodes.find(node => node.uid === nodeId);

const state = await getResource(app.metadata.name, app.metadata.namespace, node as any);
return {
objectMeta: {
creationTimestamp: {
seconds: node.createdAt
},
name: node.name,
namespace: node.namespace,
resourceVersion: node.version,
uid: node.uid
},
specAndStatus: {
spec: state.spec,
status: state.status || null
},
revision: parseRevision(node),
status: parseAnalysisRunStatus(node.health.status)
};
});

const newAnalysisRunResults = await Promise.all(promises);
setIsRefresh(false);
setAnalysisRunResults(newAnalysisRunResults);
};
// Call the API call function only when isRefresh is true and AnalysisRun node IDs exist
React.useEffect(() => {
if (isRefresh && analysisRunNodeIds.length > 0) {
rolloutAnalysisRunInfo();
}
}, [isRefresh, analysisRunNodeIds]);

return analysisRunResults;
};

const parseAnalysisRunStatus = (status: string): string => {
switch (status) {
case 'Healthy':
return 'Successful';
case 'Progressing':
return 'Running';
case 'Degraded':
return 'Error';
default:
return 'Failure';
}
};

const parseReplicaSets = (tree: any, rollout: any): RolloutReplicaSetInfo[] => {
const allReplicaSets = [];
const allPods = [];
for (const node of tree.nodes) {
if (node.kind === 'ReplicaSet') {
allReplicaSets.push(node);
} else if (node.kind === 'Pod') {
allPods.push(node);
}
}

const ownedReplicaSets: { [key: string]: any } = {};

for (const rs of allReplicaSets) {
for (const parentRef of rs.parentRefs) {
if (parentRef?.kind === 'Rollout' && parentRef?.name === rollout?.metadata?.name) {
const pods = [];
for (const pod of allPods) {
const [parentRef] = pod.parentRefs;
if (parentRef && parentRef.kind === 'ReplicaSet' && parentRef.name === rs.name) {
const ownedPod = {
objectMeta: {
name: pod.name,
uid: pod.uid,
namespace: pod.namespace,
creationTimestamp: pod.creationTimestamp
},
images: pod.images,
status: parsePodStatus(pod),
revision: parseRevision(rs),
ready: parsePodReady(pod),
canary: true
};
pods.push(ownedPod);
}
}
ownedReplicaSets[rs?.name] = {
objectMeta: {
name: rs.name,
uid: rs.uid,
namespace: rs.namespace
},
status: rs?.health.status,
revision: parseRevision(rs),
canary: true
};
if (pods.length > 0) {
ownedReplicaSets[rs?.name].pods = pods;
}
}
}
}

return (Object.values(ownedReplicaSets) || []).map(rs => {
return rs;
});
};

const getResource = (name: string | undefined, appNamespace: string | undefined, resource: any): Promise<any> => {
const params = {
name,
appNamespace,
namespace: resource.namespace,
resourceName: resource.name,
version: resource.version,
kind: resource.kind,
group: resource.group || ''
};

return axios.get(`/api/v1/applications/${name}/resource`, { params }).then(response => {
const { manifest } = response.data;
return JSON.parse(manifest);
});
};

export const RolloutExtension = (props: { application: any; tree: ApplicationResourceTree; resource: State }) => {
const ro = parseInfoFromResourceNode(props.application, props.tree, props.resource);
return <RolloutWidget rollout={ro} />;
};
Loading