Issue #11706 (인생 처음으로 올린 감격적인 나의 소중한 Issue)

뚜비·2023년 8월 29일
0

Issue #11706 << 해당 이슈를 확인할 수 있습니다.



서론

Issue #3774를 발견하다

나는 Argo workflows 프로젝트에 장기적으로 기여하기로 계획하였는데, Argo workflows가 분야 상관없이 재밌는 이슈가 많기도 하고, 오픈소스 컨트리뷰션 끝나도 계속 이슈를 해결해볼 예정이기 때문!!
(Argo workflows 기여하면 프론트, 백엔드 분야 상관없이 모든 분야 커버 쌉가능..)

우선 UI, docs 수정 위주로 이슈 해결하면서 argo workflows에 익숙해진 후 템플릿이나 백엔드 위주 이슈로 찾고자 했다.


그래서 UI 관련 이슈를 탐색하던 중... Issue #3774를 발견하게 되었는데.. 두둥탁!

해당 이슈는 cron workflow에 Suspend/Resume toggle button을 추가했으면 좋겠다는 이슈였다. 아니 근데 설명이 부족하고 애매모호해서 이슈에서 말하는 바가 무엇인지 파악하기 어려웠다..


Issue 분석

한번 로컬에서 직접 실행해보면서 Issue를 분석하기로 했다.

cron workflow 탭에 들어가서 lovely-rhino workflows에 대해 RESUME 버튼을 클릭했더니 SUSPEND로 바뀐 것을 확인할 수 있다. 즉 resume/suspend 버튼이 토글 형식으로 바뀐다는 것을 알 수 있다.


workflow를 RESUME하고 Workflows 탭에 가서 확인해보면 해당 lovely-rhino workflow가 성공적으로 실행되었음을 알 수 있다.


이때 workflow는 1분마다 새롭게 실행되었고, cron-workflow 탭에서 해당 workflows에서 본 SUSPEND 버튼을 눌러야 더 이상 실행되지 않고 종료 되었다.

🤔 cron workflows??
cron workflow는 일정 스케쥴 주기마다 실행되는 workflow를 말한다.
(위에서 확인한 lovely-rhino workflows는 schedule이 every minute으로 되어있기 때문에 1분마다 workflow가 실행된다.)


❌ Issue를 파악하지 못 했습니다 ❌

이슈를 분석할 수록 더더욱 Issue에 대해서 파악하지 못 했다.. 나의 의문점은 다음과 같았다.


resume/suspend 버튼이 이미 Toggle 형식으로 바뀌고 있다. toggle button을 추가해달라는 것은 resume랑 suspend을 각각 따로 toggle 버튼으로 만들어달라는 요구사항인 것인가?

하지만 Issue를 분석해보니 resume/suspend이 toggle 형식으로 두는 것이 더 적절해보인다. 그렇다면 여기서 말하는 toggle 버튼은 무엇을 의미하는 것일까? 그리고 해당 toggle 버튼이 요구사항을 이미 만족한 것으로 보인다.

Suspend/Resume a cron workflow from the UI, and monitor what workflows are currently active 이 부분에서 toggle 버튼을 추가함으로써 현재 실행 중인 workflows를 모니터 할 수 있다했는데 그 둘은 무슨 연관이 있을까?


comment를 확인해보니 Issue #1975에서 이미 RESUME/SUSPEND toggle 버튼을 추가하자는 제안이 있었고.. 이미 해결된 Issue였는데 중복되는 것 같다는 메인테이너의 의견이 있었다.

그러나 그 이슈를 해결하신 분이 Issue #1975 이슈와는 좀 다른 이슈이다. cron-workflow에 의해 발생한 workflow를 보는 것을 요구하고 있다며 comment를 달아서 더 머리가 복잡해졌다.. toggle 버튼이랑 모니터링이랑 진짜 뭔 관계야!!!


멘토님의 조언

멘토님께 따로 DM을 통해 위와 같은 의문점을 제시했고 멘토님과 20분 가량 허들을 통해 해당 이슈에 대해 이야기를 나누었는데
멘토님께서는 토글버튼 기능 따로 워크플로 모니터링 기능을 따로 요구하는 것 같다고 추측하셨다.

그리고 이슈 내용이 추상적이고 애매할 뿐더러, 이미 해결된 요구사항이 있기 때문에 새로운 이슈를 작성해보자고 하셨다. 그리고 제안주신 내용으로 내가 이슈를 작성하기로 했는데...
갓근삼(멘토님)!!!! 갓근삼(멘토님)!!!!



본론

Issue #11706를 제안하다

(감격스러운 나의 이슈..)

간단히 요약하자면 cron workflows에서 실행된 workflow를 worfklow 탭에서만 확인할 수 있는게 불편하고, 해당 cron-workflow 내에서 실행된 history를 모니터링 할 수 있도록 UI 개선하자는 이슈이다.


즉 해당 cron workflow에 들어가면 위와 같이 실행된 workflow history를 확인할 수 있도록 하자는 뜻!!



해당 Issue에 대해서 Argo Workflows 메인테이너 님도 긍정적으로 피드백을 주셨다.....(또 피드백이 빨라서 또 감동...)

감격...

UI 분석

사실 저 예시로 만든 이미지는 workflow 탭에서 생성된 cron-workflow를 가져온 것이다. 즉 Workflows 탭에서 생성되는 workflow List UI component를 적극 활용해볼 것이다.


참고로 Argo workflows의 프론트는 타입스크립트 언어 + React 프레임워크를 사용하고 있다.
또한 클래스형 컴포넌트와 함수형 컴포넌트를 함께 사용한다.


Workflows 탭

argo-workflows의 ui > src > app > workflows에 들어가면

다음과 같이 components 폴더와 index.ts 파일이 존재하는데

index.ts에 들어가보면 다음과 같은 코드가 있다.

import {WorkflowsContainer} from './components/workflows-container';

export default {
    component: WorkflowsContainer
};

즉 사용자가 Workflows 탭에 들어가면 components 폴더의 workflows-container를 가장 먼저 보여주게 된다.


workflows-container.tsx에 들어가보자

import * as React from 'react';
import {Route, RouteComponentProps, Switch} from 'react-router';
import {WorkflowDetails} from './workflow-details/workflow-details';
import {WorkflowsList} from './workflows-list/workflows-list';

export const WorkflowsContainer = (props: RouteComponentProps<any>) => (
    <Switch>
        <Route exact={true} path={`${props.match.path}/:namespace?`} component={WorkflowsList} />
        <Route exact={true} path={`${props.match.path}/:namespace/:name`} component={WorkflowDetails} />
    </Switch>
);

WorkflowsList와 WorkflowDetails를 컴포넌트를 Routing하는데, 이때 Switch 태그는 Route 태그 중 매치되는 첫번째만 렌더하게 된다(해당 탭에 접근할 때 Details 화면이 뜨는 경우가 있고 WorkflowList가 화면이 뜨는 경우가 있는데 Switch 태그 때문에 그렇다)
[출처 : [reactjs] React + Typescript - 4 (React-Router)]


  • workflow-details
    여기서 WorkflowDetails는 실행된 workflow를 클릭했을 때 해당 workflow의 detail한 정보를 확인할 수 있는 화면에 대한 것이다.

  • workflow-list
    Workflows-list는 화면상으로 보이는 Workflows 탭의 전체적인 UI component들이 구현되었다고 생각하면 된다. WorkflowList는 클래스형 컴포넌트로 구성.
export class WorkflowsList extends BasePage<RouteComponentProps<any>, State> {

	public render() {
      /*화면 상에 보이는 element를 렌더링한다.*/
      /*이때 위의 사진 속 노란색 박스 부분에 대한 element 코드가 담겨있다*/
      /*
      	<div> : 화면 상 왼쪽에 존재하는 Workflow 요약과 filter 부분 
      		<WorkflowsSummaryContainer>
        	<WorkflowFilters>
        <div>
        <div>{this.renderWorkflows()}</div> : 실행된 worfklow 정보를 보여주는 부분 / 우리가 보여주고자 하는 부분 
      */
    }


    private renderWorkflows() {
      /* 실행된 workflows를 확인할 수 있는 UI 부분 */
      /* workflow-row, checkbox, pagination까지 다 포함되어 있음 */
    }
}

  • workflow-row
    workflow-row는 workflow list 중 한 행(실행된 workflow의 한 객체)에 대한 컴포넌트를 의미한다. WorkflowRow는 클래스형 컴포넌트이다.
    workflow-list는 workflow row Component를 활용해 보여준다.

Cron Workflows 탭

argo-workflows의 ui > src > app > cron-workflows에 들어가면 workflow 탭과 동일한 파일을 가진 것을 확인할 수 있었다.
다만 CronWorkflowList, CronWorkflowDetails은 모두 함수형 컴포넌트로 이루어져있으며 Workflow 탭과 비슷하지만 CronWorkflow에서 List는 Link 컴포넌트를 사용하고 CronWorkflowDetails는 UI가 아예 달랐다(아래 사진 참고)


우선 history UI가 추가되어야 하는 부분은 CronWorkflowDetails의 Status 부분.. 우선 실행 완료된 workflow history만 확인할 수 있게 setting할 생각이다.

  • cron-workflow-status-viewr
export const CronWorkflowStatusViewer = ({spec, status}: {spec: CronWorkflowSpec; status: CronWorkflowStatus}) => {
    if (status === null) {
        return null;
    }

    return (
        <>
        <div className='white-box'>
            <div className='white-box__details'>
                {[
                    {title: 'Active', value: status.active ? getCronWorkflowActiveWorkflowList(status.active) : <i>No Workflows Active</i>},
                    {
                        title: 'Schedule',
                        value: (
                            <>
                                <code>{spec.schedule}</code> <PrettySchedule schedule={spec.schedule} />
                            </>
                        )
                    },
                    {title: 'Last Scheduled Time', value: <Timestamp date={status.lastScheduledTime} />},
                    {title: 'Conditions', value: <ConditionsPanel conditions={status.conditions} />}
                ].map(attr => (
                    <div className='row white-box__details-row' key={attr.title}>
                        <div className='columns small-3'>{attr.title}</div>
                        <div className='columns small-9'>{attr.value}</div>
                    </div>
                ))}
            </div>
        </div>
    <!-- 여기부터 history component -->
        <h1>THIS IS HISTORY FOR CRON-WORKFLOW</h1>
        </>
        
    );
};

해당 Component는 STATUS를 클릭했을 때 보여지는 아래 화면(white box부터)에 대한 담당으로 여기에서 실행 완료된 history UI 컴포넌트를 추가해볼 것이다.


cron-workflows의 실행 원리 분석


우선 cron workflow 탭에 들어가서namespace는 argo, name은 lovely-rhino인 cron-workflow를 resume 해보겠다. url을 보면 cron-workflows/{namespace}/{name}임을 알 수 있다.


workflow 탭에 들어가면 실행된 cron-workflow를 확인할 수 있고 namelovely-rhino-1693585680이다.

Details에 들어가면 url이 아래와 같다.
localhost:8080/workflows/argo/lovely-rhino-1693585680?tab=workflow&uid=be6b672e-d2d6-4c26-a958-a70c9e501921


1분 후의 다른 cron-workflow와 Details의 url은 아래와 같다.
http://localhost:8080/workflows/argo/lovely-rhino-1693585800?tab=workflow&uid=83bfc5fd-e2a6-42a0-8186-4e4ed58b0dab

즉 name과 uid가 각각 다름을 확인할 수 있다.

따라서 reports.tsx의 Dropdown 처럼 cronworkflows의 list를 활용해서 리스트업을 하는게 좋을 듯

services.cronWorkflows.list(this.state.namespace).then(list => list.map(x => x.metadata.name))}

API 분석

  • workflows
    list workflow list
    get workflow 객체 하나

  • cron-workflows
    list cron workflow template??


의문

  • workflows list를 가져와서 map 형식으로 보여주는 방식
  • ListWatch.start()와 ListWatch.stop()이 또 존재
  • ScopedLocalStorage는 또 무엇인가?
  • info는 또 어떤 정보를 공유하는가?

HISTORY 컴포넌트 제작

  • cron-workflow-status-history
    해당 컴포넌트는 함수형으로 제작
import * as React from 'react';
import {useEffect, useState} from 'react';
import {kubernetes} from '../../../models';

import {ErrorNotice} from '../../shared/components/error-notice';
import {Loading} from '../../shared/components/loading';
import {ZeroState} from '../../shared/components/zero-state';
import {Link} from 'react-router-dom';
import { Column } from '../../../models';
import {uiUrl} from '../../shared/base';
import {useCollectEvent} from '../../shared/components/use-collect-event';
import {services} from '../../shared/services';
import {Timestamp} from '../../shared/components/timestamp';
import {Ticker} from 'argo-ui/src/index';
import {DurationPanel} from '../../shared/components/duration-panel';
import {wfDuration} from '../../shared/duration';
import {isArchivedWorkflow, Workflow} from '../../../models';
import {WorkflowDrawer} from '../../workflows/components/workflow-drawer/workflow-drawer';

export const CronWorkflowStatusHistory = ({namespace, name} : {namespace : string, name : string}) => {
    if (status === null) {
        return null;
    }

    const [hideDrawer, setHideDrawer] = useState<boolean>(true);
    const [cronWorkflows, setCronWorkflows] = useState<Workflow[]>();

    const [error, setError] = useState<Error>();
    const [columns, setColumns] = useState<Column[]>();

    /* workflows list를 받아와야 함, 여러 파라미터 값 필요 */
    useEffect(() => {
        services.workflows
            .get(namespace, name)
            .then(x => {setCronWorkflows(cronWorkflows.concat(x)); }) // 추가
            .then(() => setError(null))
            .catch(setError);
    }, [namespace]);

    useCollectEvent('openedCronWorkflowList'); /* 왜 있는걸까? */

    return (
        <div className='history'>
            <ErrorNotice error={error} />
                    {!cronWorkflows ? (
                        <Loading />
                    ) : cronWorkflows.length === 0 ? (
                        <ZeroState title='No cron workflows'>
                        </ZeroState>
                    ) : (
                    <>
                        <div className='argo-table-list'>
                            <div className='row argo-table-list__head'>
                                    <div className='columns small-2'>NAME</div>
                                    <div className='columns small-1'>NAMESPACE</div>
                                    <div className='columns small-1'>STARTED</div>
                                    <div className='columns small-1'>FINISHED</div>
                                    <div className='columns small-1'>DURATION</div>
                                    <div className='columns small-1'>PROGRESS</div>
                                    <div className='columns small-2'>MESSAGE</div>
                                    <div className='columns small-1'>DETAILS</div>
                                </div>
                            {cronWorkflows.map(w => (
                                <Link
                                className='row argo-table-list__row'
                                key={`${w.metadata.namespace}/${w.metadata.name}`}
                                to={{
                                    pathname: uiUrl(`workflows/${w.metadata.namespace}/${w.metadata.name}`),
                                    search: `?uid=${w.metadata.uid}`
                                }}>
                                <div className='columns small-2'>{w.metadata.name}</div>
                                <div className='columns small-1'>{w.metadata.namespace}</div>
                                <div className='columns small-1'>
                                    <Timestamp date={w.status.startedAt} />
                                </div>
                                <div className='columns small-1'>
                                    <Timestamp date={w.status.startedAt} />
                                </div>
                                <div className='columns small-1'>
                                    <Ticker>{() => <DurationPanel phase={w.status.phase} duration={wfDuration(w.status)} estimatedDuration={w.status.estimatedDuration} />}</Ticker>
                                </div>
                                <div className='columns small-1'>{w.status.progress || '-'}</div>
                                <div className='columns small-2'>{w.status.message || '-'}</div>
                                <div className='columns small-1'>
                                    <div className='workflows-list__labels-container'>
                                        <div
                                            onClick={e => {
                                                e.preventDefault();
                                                setHideDrawer(!hideDrawer);
                                            }}
                                            className={`workflows-row__action workflows-row__action--${hideDrawer ? 'show' : 'hide'}`}>
                                            {hideDrawer ? (
                                                <span>
                                                    SHOW <i className='fas fa-caret-down' />{' '}
                                                </span>
                                            ) : (
                                                <span>
                                                    HIDE <i className='fas fa-caret-up' />
                                                </span>
                                            )}
                                        </div>
                                    </div>
                                </div>
                                <div className='columns small-1'>{isArchivedWorkflow(w) ? 'true' : 'false'}</div>
                                {(columns || []).map(column => {
                                    const value = w.metadata?.labels[column.key];
                                    return (
                                        <div key={column.name} className='columns small-1'>
                                            {value}
                                        </div>
                                    );
                                })}
                                {hideDrawer ? (
                                    <span />
                                ) : (
                                    <WorkflowDrawer
                                        name={w.metadata.name}
                                        namespace={w.metadata.namespace}
                                        onChange={key => {
                                        }}
                                    />
                                )}
                            </Link>
                            ))}
                        </div>
                    </>
                )}
        </div>
        
    );
};
import * as kubernetes from 'argo-ui/src/models/kubernetes';
import * as React from 'react';
import {CronWorkflow, CronWorkflowSpec, CronWorkflowStatus} from '../../../models';
import {Timestamp} from '../../shared/components/timestamp';
import {ConditionsPanel} from '../../shared/conditions-panel';
import {WorkflowLink} from '../../workflows/components/workflow-link';
import {PrettySchedule} from './pretty-schedule';
import { CronWorkflowStatusHistory } from './cron-workflow-status-history';


export const CronWorkflowStatusViewer = ({spec, status, cronWorkflow}: {spec: CronWorkflowSpec; status: CronWorkflowStatus; cronWorkflow : CronWorkflow}) => {
    if (status === null) {
        return null;
    }

    return (
        <>
        <div className='white-box'>
            <div className='white-box__details'>
                {[
                    {title: 'Active', value: status.active ? getCronWorkflowActiveWorkflowList(status.active) : <i>No Workflows Active</i>},
                    {
                        title: 'Schedule',
                        value: (
                            <>
                                <code>{spec.schedule}</code> <PrettySchedule schedule={spec.schedule} />
                            </>
                        )
                    },
                    {title: 'Last Scheduled Time', value: <Timestamp date={status.lastScheduledTime} />},
                    {title: 'Conditions', value: <ConditionsPanel conditions={status.conditions} />}
                ].map(attr => (
                    <div className='row white-box__details-row' key={attr.title}>
                        <div className='columns small-3'>{attr.title}</div>
                        <div className='columns small-9'>{attr.value}</div>
                    </div>
                ))}
            </div>
        </div>
          <CronWorkflowStatusHistory namespace={cronWorkflow.metadata.namespace} name={cronWorkflow.metadata.name}  />
        </>
        
    );
};

function getCronWorkflowActiveWorkflowList(active: kubernetes.ObjectReference[]) {
    return active.reverse().map(activeWf => <WorkflowLink key={activeWf.uid} namespace={activeWf.namespace} name={activeWf.name} />);
}

수정한 코드 빌드는 어떻게?!

코드 수정한 것을 확인하기 위해 매번 make start UI=true로 실행시켜야 빌드가 되는지 궁금하여 갓수녕(님)께 여쭤보고 해답을 얻었다.

자동으로 다시 빌드되기 때문에 다시 실행시킬 필요는 없다!

make start UI=true로 하면 UI가 포함되어 있어 너무 무거울 수 있으니 make start API=true로 백단을 올리고 argo-workflows 프로젝트의 ui 디렉토리에 들어가서 yarn install, yarn start를 하여 백과 프론트를 따로 실행할 수 있어 빠르게 반영이 가능하다!



이를 위해서는 bash가 2개 필요한데 한 bash는 make start UI=true를 실행하고 다른 bash(vscode에서 터미널 창을 하나 추가)는 ui 디렉토리에 접근하여 yarn install, yarn start를 실행한다.


yarn start 후 localhost:8080에 접근하면 잘 실행된다!


결론

사실 욕심을 부려서 뭔가 멋있는 이슈를 해결하고 싶은 마음에 무작정 어려운 이슈 찾고.. 그러다보니 이슈를 제시하고 PR을 날리는게 사실 겁이 났었다. 이런식으로 직접 사용해보고 이슈도 찾고 해보니 진짜 별거 아니네?! 싶다.. 그리고 기여하는 거 재미있는 걸??

profile
SW Engineer 꿈나무 / 자의식이 있는 컴퓨터

0개의 댓글