1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-12-02 05:57:29 +01:00

Merge pull request #1696 from oceanprotocol/feature/enforce-docker-containers

Enforce docker container checksum in publish form
This commit is contained in:
Bogdan Fazakas 2022-10-12 15:35:58 +03:00 committed by GitHub
commit 8b108ea29f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 226 additions and 66 deletions

View File

@ -54,21 +54,22 @@
}, },
{ {
"name": "dockerImageCustom", "name": "dockerImageCustom",
"label": "Docker Image URL", "label": "Custom Docker Image",
"placeholder": "e.g. oceanprotocol/algo_dockers or https://example.com/image_path", "placeholder": "e.g. oceanprotocol/algo_dockers:node-vibrant or quay.io/startx/mariadb",
"help": "Provide the name of a public Docker image or the full url if you have it hosted in a 3rd party repo", "help": "Provide the name and the tag of a public Docker hub image or the custom image if you have it hosted in a 3rd party repository",
"type": "container",
"required": true "required": true
}, },
{ {
"name": "dockerImageCustomTag", "name": "dockerImageChecksum",
"label": "Docker Image Tag", "label": "Docker Image Checksum",
"placeholder": "e.g. latest", "placeholder": "e.g. sha256:xiXqb7Vet0FbN9q0GFMgUdi5C22wjJT0i2G6lYKC2jl6QxkKzVz7KaPDgqfTMjNF",
"help": "Provide the tag for your Docker image.", "help": "Provide the checksum(DIGEST) of your docker image.",
"required": true "required": true
}, },
{ {
"name": "dockerImageCustomEntrypoint", "name": "dockerImageCustomEntrypoint",
"label": "Docker Entrypoint", "label": "Docker Image Entrypoint",
"placeholder": "e.g. python $ALGO", "placeholder": "e.g. python $ALGO",
"help": "Provide the entrypoint for your algorithm.", "help": "Provide the entrypoint for your algorithm.",
"required": true "required": true

View File

@ -268,7 +268,7 @@ export async function getAlgorithmDatasetsForCompute(
const query = generateBaseQuery(baseQueryParams) const query = generateBaseQuery(baseQueryParams)
const computeDatasets = await queryMetadata(query, cancelToken) const computeDatasets = await queryMetadata(query, cancelToken)
if (computeDatasets.totalResults === 0) return [] if (computeDatasets?.totalResults === 0) return []
const datasets = await transformAssetToAssetSelection( const datasets = await transformAssetToAssetSelection(
datasetProviderUri, datasetProviderUri,

View File

@ -1,16 +1,27 @@
import { LoggerInstance } from '@oceanprotocol/lib' import { LoggerInstance } from '@oceanprotocol/lib'
import axios from 'axios' import axios from 'axios'
import isUrl from 'is-url-superb'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
async function isDockerHubImageValid( export interface dockerContainerInfo {
exists: boolean
checksum: string
}
export async function getContainerChecksum(
image: string, image: string,
tag: string tag: string
): Promise<boolean> { ): Promise<dockerContainerInfo> {
const containerInfo: dockerContainerInfo = {
exists: false,
checksum: null
}
try { try {
const response = await axios.post( const response = await axios.post(
`https://dockerhub-proxy.oceanprotocol.com`, `https://dockerhub-proxy.oceanprotocol.com`,
{ image, tag } {
image,
tag
}
) )
if ( if (
!response || !response ||
@ -18,46 +29,18 @@ async function isDockerHubImageValid(
response.data.status !== 'success' response.data.status !== 'success'
) { ) {
toast.error( toast.error(
'Could not fetch docker hub image info. Please check image name and tag and try again' 'Could not fetch docker hub image informations. If you have it hosted in a 3rd party repository please fill in the container checksum manually.'
) )
return false return containerInfo
} }
containerInfo.exists = true
return true containerInfo.checksum = response.data.result.checksum
return containerInfo
} catch (error) { } catch (error) {
LoggerInstance.error(error.message) LoggerInstance.error(error.message)
toast.error( toast.error(
'Could not fetch docker hub image info. Please check image name and tag and try again' 'Could not fetch docker hub image informations. If you have it hosted in a 3rd party repository please fill in the container checksum manually.'
) )
return false return containerInfo
} }
} }
async function is3rdPartyImageValid(imageURL: string): Promise<boolean> {
try {
const response = await axios.head(imageURL)
if (!response || response.status !== 200) {
toast.error(
'Could not fetch docker image info. Please check URL and try again'
)
return false
}
return true
} catch (error) {
LoggerInstance.error(error.message)
toast.error(
'Could not fetch docker image info. Please check URL and try again'
)
return false
}
}
export async function validateDockerImage(
dockerImage: string,
tag: string
): Promise<boolean> {
const isValid = isUrl(dockerImage)
? await is3rdPartyImageValid(dockerImage)
: await isDockerHubImageValid(dockerImage, tag)
return isValid
}

View File

@ -0,0 +1,48 @@
.info {
border-radius: var(--border-radius);
padding: calc(var(--spacer) / 2);
border: 1px solid var(--border-color);
background-color: var(--background-highlight);
position: relative;
}
.info ul {
margin: 0;
}
.info li {
display: inline-block;
font-size: var(--font-size-small);
margin-right: calc(var(--spacer) / 2);
color: var(--color-secondary);
}
.info li.success {
color: var(--brand-alert-green);
}
.info li.error {
color: var(--brand-alert-red);
}
.contianer {
margin: 0;
font-size: var(--font-size-base);
line-height: var(--line-height);
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
padding-right: calc(var(--spacer) / 2);
}
.removeButton {
cursor: pointer;
border: none;
position: absolute;
top: -0.2rem;
right: 0;
font-size: var(--font-size-h3);
cursor: pointer;
color: var(--font-color-text);
background-color: transparent;
}

View File

@ -0,0 +1,29 @@
import React, { ReactElement } from 'react'
import styles from './Info.module.css'
export default function ImageInfo({
image,
tag,
valid,
handleClose
}: {
image: string
tag: string
valid: boolean
handleClose(): void
}): ReactElement {
const displayText = valid
? '✓ Image found, container checksum automatically added!'
: 'x Container checksum could not be fetched automatically, please add it manually'
return (
<div className={styles.info}>
<h3 className={styles.contianer}>{`Image: ${image} Tag: ${tag}`}</h3>
<ul>
<li className={valid ? styles.success : styles.error}>{displayText}</li>
</ul>
<button className={styles.removeButton} onClick={handleClose}>
&times;
</button>
</div>
)
}

View File

@ -0,0 +1,83 @@
import React, { ReactElement, useState } from 'react'
import { useField, useFormikContext } from 'formik'
import UrlInput from '../URLInput'
import { InputProps } from '@shared/FormInput'
import { FormPublishData } from 'src/components/Publish/_types'
import { LoggerInstance } from '@oceanprotocol/lib'
import ImageInfo from './Info'
import { getContainerChecksum } from '@utils/docker'
export default function ContainerInput(props: InputProps): ReactElement {
const [field] = useField(props.name)
const [fieldChecksum, metaChecksum, helpersChecksum] = useField(
'metadata.dockerImageCustomChecksum'
)
const { values, setFieldError, setFieldValue } =
useFormikContext<FormPublishData>()
const [isLoading, setIsLoading] = useState(false)
const [isValid, setIsValid] = useState(false)
const [checked, setChecked] = useState(false)
async function handleValidation(e: React.SyntheticEvent, container: string) {
e.preventDefault()
try {
setIsLoading(true)
const parsedContainerValue = container?.split(':')
const imageName =
parsedContainerValue?.length > 1
? parsedContainerValue?.slice(0, -1).join(':')
: parsedContainerValue[0]
const tag =
parsedContainerValue?.length > 1 ? parsedContainerValue?.at(-1) : ''
const containerInfo = await getContainerChecksum(imageName, tag)
setFieldValue('metadata.dockerImageCustom', imageName)
setFieldValue('metadata.dockerImageCustomTag', tag)
setChecked(true)
if (containerInfo.checksum) {
setFieldValue(
'metadata.dockerImageCustomChecksum',
containerInfo.checksum
)
helpersChecksum.setTouched(false)
setIsValid(true)
}
} catch (error) {
setFieldError(`${field.name}[0].url`, error.message)
LoggerInstance.error(error.message)
} finally {
setIsLoading(false)
}
}
function handleClose() {
setFieldValue('metadata.dockerImageCustom', '')
setFieldValue('metadata.dockerImageCustomTag', '')
setFieldValue('metadata.dockerImageCustomChecksum', '')
setChecked(false)
setIsValid(false)
helpersChecksum.setTouched(true)
}
return (
<>
{checked ? (
<ImageInfo
image={values.metadata.dockerImageCustom}
tag={values.metadata.dockerImageCustomTag}
valid={isValid}
handleClose={handleClose}
/>
) : (
<UrlInput
submitText="Use"
{...props}
name={`${field.name}[0].url`}
checkUrl={false}
isLoading={isLoading}
handleButtonClick={handleValidation}
/>
)}
</>
)
}

View File

@ -12,12 +12,14 @@ export default function URLInput({
handleButtonClick, handleButtonClick,
isLoading, isLoading,
name, name,
checkUrl,
...props ...props
}: { }: {
submitText: string submitText: string
handleButtonClick(e: React.SyntheticEvent, data: string): void handleButtonClick(e: React.SyntheticEvent, data: string): void
isLoading: boolean isLoading: boolean
name: string name: string
checkUrl?: boolean
}): ReactElement { }): ReactElement {
const [field, meta] = useField(name) const [field, meta] = useField(name)
const [isButtonDisabled, setIsButtonDisabled] = useState(true) const [isButtonDisabled, setIsButtonDisabled] = useState(true)
@ -28,7 +30,7 @@ export default function URLInput({
setIsButtonDisabled( setIsButtonDisabled(
!field?.value || !field?.value ||
field.value === '' || field.value === '' ||
!isUrl(field.value) || (checkUrl && !isUrl(field.value)) ||
field.value.includes('javascript:') || field.value.includes('javascript:') ||
meta?.error meta?.error
) )

View File

@ -11,6 +11,7 @@ import AssetSelection, {
} from '../FormFields/AssetSelection' } from '../FormFields/AssetSelection'
import Nft from '../FormFields/Nft' import Nft from '../FormFields/Nft'
import InputRadio from './InputRadio' import InputRadio from './InputRadio'
import ContainerInput from '@shared/FormFields/ContainerInput'
import TagsAutoComplete from './TagsAutoComplete' import TagsAutoComplete from './TagsAutoComplete'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
@ -108,6 +109,8 @@ export default function InputElement({
) )
case 'files': case 'files':
return <FilesInput {...field} {...props} /> return <FilesInput {...field} {...props} />
case 'container':
return <ContainerInput {...field} {...props} />
case 'providerUrl': case 'providerUrl':
return <CustomProvider {...field} {...props} /> return <CustomProvider {...field} {...props} />
case 'nft': case 'nft':

View File

@ -1,6 +1,6 @@
import { BoxSelectionOption } from '@shared/FormFields/BoxSelection' import { BoxSelectionOption } from '@shared/FormFields/BoxSelection'
import Input from '@shared/FormInput' import Input from '@shared/FormInput'
import { Field, useFormikContext } from 'formik' import { Field, useField, useFormikContext } from 'formik'
import React, { ReactElement, useEffect } from 'react' import React, { ReactElement, useEffect } from 'react'
import content from '../../../../content/publish/form.json' import content from '../../../../content/publish/form.json'
import { FormPublishData } from '../_types' import { FormPublishData } from '../_types'
@ -23,6 +23,8 @@ export default function MetadataFields(): ReactElement {
// connect with Form state, use for conditional field rendering // connect with Form state, use for conditional field rendering
const { values, setFieldValue } = useFormikContext<FormPublishData>() const { values, setFieldValue } = useFormikContext<FormPublishData>()
const [field, meta] = useField('metadata.dockerImageCustomChecksum')
// BoxSelection component is not a Formik component // BoxSelection component is not a Formik component
// so we need to handle checked state manually. // so we need to handle checked state manually.
const assetTypeOptions: BoxSelectionOption[] = [ const assetTypeOptions: BoxSelectionOption[] = [
@ -124,11 +126,14 @@ export default function MetadataFields(): ReactElement {
/> />
<Field <Field
{...getFieldContent( {...getFieldContent(
'dockerImageCustomTag', 'dockerImageChecksum',
content.metadata.fields content.metadata.fields
)} )}
component={Input} component={Input}
name="metadata.dockerImageCustomTag" name="metadata.dockerImageCustomChecksum"
disabled={
values.metadata.dockerImageCustomChecksum && !meta.touched
}
/> />
<Field <Field
{...getFieldContent( {...getFieldContent(

View File

@ -96,17 +96,15 @@ export const initialValues: FormPublishData = {
export const algorithmContainerPresets: MetadataAlgorithmContainer[] = [ export const algorithmContainerPresets: MetadataAlgorithmContainer[] = [
{ {
image: 'node', image: 'node',
tag: '18.6.0', // TODO: Put this back to latest once merging the PR that fetches the container digest from docker hub via dockerhub-proxy tag: 'latest',
entrypoint: 'node $ALGO', entrypoint: 'node $ALGO',
checksum: checksum: ''
'sha256:c60726646352202d95de70d9e8393c15f382f8c6074afc5748b7e570ccd5995f'
}, },
{ {
image: 'python', image: 'python',
tag: '3.10.5', // TODO: Put this back to latest once merging the PR that fetches the container digest from docker hub via dockerhub-proxy tag: 'latest',
entrypoint: 'python $ALGO', entrypoint: 'python $ALGO',
checksum: checksum: ''
'sha256:607635763e54907fd75397fedfeb83890e62a0f9b54a1d99d27d748c5d269be4'
} }
] ]

View File

@ -26,20 +26,24 @@ import {
publisherMarketFixedSwapFee publisherMarketFixedSwapFee
} from '../../../app.config' } from '../../../app.config'
import { sanitizeUrl } from '@utils/url' import { sanitizeUrl } from '@utils/url'
import { getContainerChecksum } from '@utils/docker'
function getUrlFileExtension(fileUrl: string): string { function getUrlFileExtension(fileUrl: string): string {
const splittedFileUrl = fileUrl.split('.') const splittedFileUrl = fileUrl.split('.')
return splittedFileUrl[splittedFileUrl.length - 1] return splittedFileUrl[splittedFileUrl.length - 1]
} }
function getAlgorithmContainerPreset( async function getAlgorithmContainerPreset(
dockerImage: string dockerImage: string
): MetadataAlgorithmContainer { ): Promise<MetadataAlgorithmContainer> {
if (dockerImage === '') return if (dockerImage === '') return
const preset = algorithmContainerPresets.find( const preset = algorithmContainerPresets.find(
(preset) => `${preset.image}:${preset.tag}` === dockerImage (preset) => `${preset.image}:${preset.tag}` === dockerImage
) )
preset.checksum = await (
await getContainerChecksum(preset.image, preset.tag)
).checksum
return preset return preset
} }
@ -80,6 +84,11 @@ export async function transformPublishFormToDdo(
const currentTime = dateToStringNoMS(new Date()) const currentTime = dateToStringNoMS(new Date())
const isPreview = !datatokenAddress && !nftAddress const isPreview = !datatokenAddress && !nftAddress
const algorithmContainerPresets =
type === 'algorithm' && dockerImage !== '' && dockerImage !== 'custom'
? await getAlgorithmContainerPreset(dockerImage)
: null
// Transform from files[0].url to string[] assuming only 1 file // Transform from files[0].url to string[] assuming only 1 file
const filesTransformed = files?.length && const filesTransformed = files?.length &&
files[0].valid && [sanitizeUrl(files[0].url)] files[0].valid && [sanitizeUrl(files[0].url)]
@ -110,20 +119,19 @@ export async function transformPublishFormToDdo(
entrypoint: entrypoint:
dockerImage === 'custom' dockerImage === 'custom'
? dockerImageCustomEntrypoint ? dockerImageCustomEntrypoint
: getAlgorithmContainerPreset(dockerImage).entrypoint, : algorithmContainerPresets.entrypoint,
image: image:
dockerImage === 'custom' dockerImage === 'custom'
? dockerImageCustom ? dockerImageCustom
: getAlgorithmContainerPreset(dockerImage).image, : algorithmContainerPresets.image,
tag: tag:
dockerImage === 'custom' dockerImage === 'custom'
? dockerImageCustomTag ? dockerImageCustomTag
: getAlgorithmContainerPreset(dockerImage).tag, : algorithmContainerPresets.tag,
checksum: checksum:
dockerImage === 'custom' dockerImage === 'custom'
? // ? dockerImageCustomChecksum ? dockerImageCustomChecksum
'' : algorithmContainerPresets.checksum
: getAlgorithmContainerPreset(dockerImage).checksum
} }
} }
}) })