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

feat: lock config for k8s manifest (KubeCon 2024) #2176

Merged
merged 11 commits into from
Nov 7, 2024
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"homepage": "/dashboard",
"dependencies": {
"@devtron-labs/devtron-fe-common-lib": "0.6.0-patch-1-beta-10",
"@devtron-labs/devtron-fe-common-lib": "0.6.2-beta-8",
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
"@rjsf/core": "^5.13.3",
"@rjsf/utils": "^5.13.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -516,10 +516,11 @@ export const K8SResourceList = ({
)

const emptyStateActionHandler = () => {
setSearchText('')
const pathname = `${URLS.RESOURCE_BROWSER}/${clusterId}/${ALL_NAMESPACE_OPTION.value}/${selectedResource.gvk.Kind.toLowerCase()}/${group}`
updateK8sResourceTab(pathname)
updateK8sResourceTab(pathname, '', false)
push(pathname)
setFilteredResourceList(resourceList?.data ?? null)
setResourceListOffset(0)
setSelectedNamespace(ALL_NAMESPACE_OPTION)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,8 @@ const ResourceList = () => {
const updateK8sResourceTabLastSyncMoment = () =>
updateTabLastSyncMoment(tabs[fixedTabIndices.K8S_RESOURCE_LIST]?.id)

const getUpdateTabUrlForId = (id: string) => (_url: string, dynamicTitle?: string) =>
updateTabUrl(id, _url, dynamicTitle)
const getUpdateTabUrlForId = (id: string) => (_url: string, dynamicTitle?: string, retainSearchParams?: boolean) =>
updateTabUrl(id, _url, dynamicTitle, retainSearchParams)

const getRemoveTabByIdentifierForId = (id: string) => () => removeTabByIdentifier(id)

Expand Down
8 changes: 3 additions & 5 deletions src/components/ResourceBrowser/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,10 @@ export interface CreateResourceType {
clusterId: string
}

export interface SidebarType {
export interface SidebarType extends Pick<K8SResourceTabComponentProps, 'updateK8sResourceTab'> {
apiResources: ApiResourceGroupType[]
selectedResource: ApiResourceGroupType
setSelectedResource: React.Dispatch<React.SetStateAction<ApiResourceGroupType>>
updateK8sResourceTab: (url: string, dynamicTitle: string) => void
updateK8sResourceTabLastSyncMoment: () => void
isOpen: boolean
isClusterError?: boolean
Expand All @@ -118,7 +117,7 @@ export interface ClusterOptionType extends OptionType {
isProd: boolean
}

export interface ResourceFilterOptionsProps {
export interface ResourceFilterOptionsProps extends Pick<K8SResourceTabComponentProps, 'updateK8sResourceTab'> {
selectedResource: ApiResourceGroupType
resourceList?: K8sResourceDetailType
selectedCluster?: ClusterOptionType
Expand All @@ -128,7 +127,6 @@ export interface ResourceFilterOptionsProps {
isOpen: boolean
setSearchText?: (text: string) => void
isSearchInputDisabled?: boolean
updateK8sResourceTab: (url: string, dynamicTitle?: string) => void
renderRefreshBar?: () => JSX.Element
}

Expand Down Expand Up @@ -201,7 +199,7 @@ export interface K8SResourceTabComponentProps {
renderRefreshBar: () => JSX.Element
addTab: ReturnType<typeof useTabs>['addTab']
showStaleDataWarning: boolean
updateK8sResourceTab: (url: string, dynamicTitle: string) => void
updateK8sResourceTab: (url: string, dynamicTitle?: string, retainSearchParams?: boolean) => void
updateK8sResourceTabLastSyncMoment: () => void
isOpen: boolean
clusterName: string
Expand Down
1 change: 1 addition & 0 deletions src/components/v2/appDetails/appDetails.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@ export interface ManifestViewRefType {
activeManifestEditorData: string
modifiedManifest: string
guiSchema: Record<string, string>
lockedKeys: string[] | null
}
id: string
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ const NodeDetailComponent = ({
activeManifestEditorData: '',
modifiedManifest: '',
guiSchema: {},
lockedKeys: null,
},
id: '',
})
Expand Down Expand Up @@ -381,6 +382,7 @@ const NodeDetailComponent = ({

const handleManifestCancel = () => {
handleManifestGUIError([])
handleUpdateUnableToParseManifest(false)
setManifestCodeEditorMode(ManifestCodeEditorMode.CANCEL)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useHistory, useLocation, useParams, useRouteMatch } from 'react-router-dom'
import YAML from 'yaml'
import {
Expand All @@ -34,6 +34,7 @@ import {
ConfigurationType,
YAMLStringify,
InfoColourBar,
logExceptionToSentry,
} from '@devtron-labs/devtron-fe-common-lib'
import Tippy from '@tippyjs/react'
import { ReactComponent as ICClose } from '@Icons/ic-close.svg'
Expand Down Expand Up @@ -70,7 +71,10 @@ import { getDecodedEncodedSecretManifestData, getTrimmedManifestData } from '../
import { importComponentFromFELibrary } from '@Components/common'

const getManifestGUISchema = importComponentFromFELibrary('getManifestGUISchema', null, 'function')
const getLockedManifestKeys = importComponentFromFELibrary('getLockedManifestKeys', null, 'function')
const ManifestGUIView = importComponentFromFELibrary('ManifestGUIView', null, 'function')
const checkForIneligibleChanges = importComponentFromFELibrary('checkForIneligibleChanges', null, 'function')
const ShowIneligibleChangesModal = importComponentFromFELibrary('ShowIneligibleChangesModal', null, 'function')

const ManifestComponent = ({
selectedTab,
Expand Down Expand Up @@ -124,6 +128,9 @@ const ManifestComponent = ({
const [secretViewAccess, setSecretViewAccess] = useState(false)
const [guiSchema, setGUISchema] = useState<ManifestViewRefType['data']['guiSchema']>({})

const [lockedKeys, setLockedKeys] = useState<string[]>(null)
const [showLockedDiffModal, setShowLockedDiffModal] = useState(false)

const { isSuperAdmin } = useMainContext() // to show the cluster meta data at the bottom
// Cancel is an intermediate state wherein edit is true
const isEditMode =
Expand All @@ -137,6 +144,7 @@ const ManifestComponent = ({
setManifest(manifestViewRef.current.data.manifest)
setModifiedManifest(manifestViewRef.current.data.modifiedManifest)
setGUISchema(manifestViewRef.current.data.guiSchema)
setLockedKeys(manifestViewRef.current.data.lockedKeys)

if (showManifestCompareView) {
setActiveManifestEditorData(manifestViewRef.current.data.manifest)
Expand All @@ -157,11 +165,22 @@ const ManifestComponent = ({
activeManifestEditorData,
modifiedManifest,
guiSchema,
lockedKeys,
},
/* NOTE: id is unlikely to change but still kept as dep */
id,
}
}, [error, secretViewAccess, desiredManifest, activeManifestEditorData, manifest, modifiedManifest, id, guiSchema])
}, [
error,
secretViewAccess,
desiredManifest,
activeManifestEditorData,
manifest,
modifiedManifest,
id,
guiSchema,
lockedKeys
])

const handleInitializeGUISchema = async (abortSignal: AbortSignal) => {
if (!getManifestGUISchema || isExternalApp) {
Expand All @@ -185,6 +204,28 @@ const ManifestComponent = ({
setGUISchema(guiSchemaResponse)
}

const handleInitializeLockedManifestKeys = async (signal: AbortSignal) => {
if (!getLockedManifestKeys || isExternalApp) {
return
}

const resourceRequestPayload = getResourceRequestPayload({
appDetails,
nodeName: params.podName,
nodeType: params.nodeType,
isResourceBrowserView,
selectedResource,
})

const lockedKeysResponse = await getLockedManifestKeys({
clusterId: resourceRequestPayload.clusterId,
gvk: resourceRequestPayload.k8sRequest.resourceIdentifier.groupVersionKind,
signal,
})

setLockedKeys(lockedKeysResponse)
}

useEffect(() => {
selectedTab(NodeDetailTab.MANIFEST, url)
if (isDeleted) {
Expand Down Expand Up @@ -243,6 +284,7 @@ const ManifestComponent = ({
_showDesiredAndCompareManifest &&
getDesiredManifestResource(appDetails, params.podName, params.nodeType, abortController.signal),
handleInitializeGUISchema(abortController.signal),
handleInitializeLockedManifestKeys(abortController.signal),
])
.then((response) => {
setSecretViewAccess(response[0]?.result?.secretViewAccess || false)
Expand Down Expand Up @@ -352,38 +394,23 @@ const ManifestComponent = ({
setActiveManifestEditorData(modifiedManifest)
}

const handleApplyChanges = () => {
setLoading(true)
setLoadingMsg('Applying changes')
setShowDecodedData(false)
setManifestCodeEditorMode(null)

let manifestString
try {
if (!modifiedManifest) {
setErrorText(`${SAVE_DATA_VALIDATION_ERROR_MSG} "${EMPTY_YAML_ERROR}"`)
// Handled for blocking API call
manifestString = ''
} else {
manifestString = JSON.stringify(YAML.parse(modifiedManifest))
}
} catch (err2) {
setErrorText(`${SAVE_DATA_VALIDATION_ERROR_MSG} “${err2}”`)
}
if (!manifestString) {
setLoading(false)
} else {
const handleCallApplyChangesAPI = (manifest: string): Promise<void> =>
new Promise<void>((resolve) => {
updateManifestResourceHelmApps(
appDetails,
params.podName,
params.nodeType,
manifestString,
manifest,
isResourceBrowserView,
selectedResource,
)
.then((response) => {
setManifestCodeEditorMode(ManifestCodeEditorMode.READ)
const _manifest = JSON.stringify(response?.result?.manifest)
ToastManager.showToast({
variant: ToastVariantType.success,
description: 'Manifest is updated',
})
if (_manifest) {
setManifest(_manifest)
setActiveManifestEditorData(_manifest)
Expand All @@ -410,7 +437,48 @@ const ManifestComponent = ({
} else {
showError(err)
}
})
}).finally(resolve)
})

const uneditedManifest = useMemo(() => {
try {
const object = YAML.parse(manifest)

return object?.metadata?.managedFields && hideManagedFields ? getTrimmedManifestData(object) : object
} catch (err) {
logExceptionToSentry(new Error(`Error: in parsing manifest - ${err.message}`))

return {}
}
}, [manifest, hideManagedFields])

const handleApplyChanges = async () => {
setLoading(true)
setLoadingMsg('Applying changes')
Elessar1802 marked this conversation as resolved.
Show resolved Hide resolved
setShowDecodedData(false)
setManifestCodeEditorMode(null)

let modifiedManifestString: string = ''
let modifiedManifestDocument: object = null
try {
if (!modifiedManifest) {
setErrorText(`${SAVE_DATA_VALIDATION_ERROR_MSG} "${EMPTY_YAML_ERROR}"`)
} else {
modifiedManifestDocument = YAML.parse(modifiedManifest)
modifiedManifestString = JSON.stringify(modifiedManifestDocument)
}
} catch (err2) {
setErrorText(`${SAVE_DATA_VALIDATION_ERROR_MSG} “${err2}”`)
}
if (!modifiedManifestString) {
setLoading(false)
setManifestCodeEditorMode(ManifestCodeEditorMode.EDIT)
} else if (!isSuperAdmin && checkForIneligibleChanges && lockedKeys && checkForIneligibleChanges(uneditedManifest, modifiedManifestDocument, lockedKeys)) {
setLoading(false)
setShowLockedDiffModal(true)
setManifestCodeEditorMode(ManifestCodeEditorMode.EDIT)
} else {
await handleCallApplyChangesAPI(modifiedManifestString)
}
}

Expand Down Expand Up @@ -476,6 +544,10 @@ const ManifestComponent = ({

const handleDesiredManifestClose = () => setShowManifestCompareView(false)

const handleCloseShowLockedDiffModal = () => {
setShowLockedDiffModal(false)
}

const renderShowDecodedValueCheckbox = () => {
let jsonManifestData
try {
Expand Down Expand Up @@ -669,6 +741,17 @@ const ManifestComponent = ({
)}
</div>
)}

{showLockedDiffModal && ShowIneligibleChangesModal && (
<ShowIneligibleChangesModal
handleCallApplyChangesAPI={handleCallApplyChangesAPI}
uneditedManifest={uneditedManifest}
// NOTE: a check on modifiedManifest is made before this component is rendered
editedManifest={YAML.parse(modifiedManifest)}
handleModalClose={handleCloseShowLockedDiffModal}
lockedKeys={lockedKeys}
/>
)}
</div>
)
}
Expand Down
29 changes: 29 additions & 0 deletions src/css/base.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5085,3 +5085,32 @@ textarea::placeholder {
bottom: 120px;
right: 110px;
}


.read-only-expression {
counter-reset: line;
}

.read-only-expression span {
display: block;
// JUST A HACK, since need span for setting line number
width: 1px;
}

.read-only-expression span:before {
counter-increment: line;
content: counter(line);
display: inline-block;
padding: 0 8px;
color: var(--N700);
width: 24px;
margin-right: 8px;
}

.read-only-expression.dark span:before {
color: var(--N400);
}

.read-only-expression.dark {
background-color: #0C0F21;
}
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1061,10 +1061,10 @@
dependencies:
"@jridgewell/trace-mapping" "0.3.9"

"@devtron-labs/[email protected].0-patch-1-beta-10":
version "0.6.0-patch-1-beta-10"
resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-0.6.0-patch-1-beta-10.tgz#af5952af9e5f9da2fdadb746acc2003c9ba04b23"
integrity sha512-SH2JV/ne4aK91maz1qPxwNauQff2FCdv1mZdyTOymfHY8fcGaffeCtvln2osxbX+5znwKYLOfaDkVHuDiNRiMg==
"@devtron-labs/[email protected].2-beta-8":
version "0.6.2-beta-8"
resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-0.6.2-beta-8.tgz#830c27796020d5669612d9b2868e0dce42341a73"
integrity sha512-SMcRb9N1413mo3nTjK8xWL7ANgjTFy+nWTpbwsaG3j+JlHHCzktPaRpyEMr6k4adcyiIATgo7ATFu+dQrgYBhw==
dependencies:
"@types/react-dates" "^21.8.6"
ansi_up "^5.2.1"
Expand Down
Loading