From a6dacf658a3c63cdfc97c85a50dbf1f0afbfe55d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anna=20V=C3=ADtov=C3=A1?= Date: Tue, 15 Oct 2024 18:33:04 +0300 Subject: [PATCH] src: Add on prem blueprints import support --- package-lock.json | 9 +- package.json | 3 +- .../Blueprints/ImportBlueprintModal.test.tsx | 148 ++++++++++++++++++ .../Blueprints/ImportBlueprintModal.tsx | 91 ++++++----- .../OnPremToHostedBlueprintMapper.tsx | 111 +++++++++++++ src/store/imageBuilderApi.ts | 2 +- 6 files changed, 321 insertions(+), 43 deletions(-) create mode 100644 src/Components/Blueprints/OnPremToHostedBlueprintMapper.tsx diff --git a/package-lock.json b/package-lock.json index 25647468e..bc0cc05cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,8 @@ "react-redux": "9.1.2", "react-router-dom": "6.26.1", "redux": "5.0.1", - "redux-promise-middleware": "6.2.0" + "redux-promise-middleware": "6.2.0", + "toml": "^3.0.0" }, "devDependencies": { "@patternfly/react-icons": "5.4.0", @@ -15719,6 +15720,12 @@ "node": ">=0.6" } }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "license": "MIT" + }, "node_modules/toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", diff --git a/package.json b/package.json index 59262d580..a57447fea 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "react-redux": "9.1.2", "react-router-dom": "6.26.1", "redux": "5.0.1", - "redux-promise-middleware": "6.2.0" + "redux-promise-middleware": "6.2.0", + "toml": "^3.0.0" }, "devDependencies": { "@patternfly/react-icons": "5.4.0", diff --git a/src/Components/Blueprints/ImportBlueprintModal.test.tsx b/src/Components/Blueprints/ImportBlueprintModal.test.tsx index f80b01ae6..0fa8690fa 100644 --- a/src/Components/Blueprints/ImportBlueprintModal.test.tsx +++ b/src/Components/Blueprints/ImportBlueprintModal.test.tsx @@ -121,6 +121,154 @@ const INVALID_JSON = `{ "name": "Blueprint test" }`; +const ONPREM_BLUEPRINT_TOML = ` +name = "tmux" +description = "tmux image with openssh" +version = "1.2.16" +distro = "fedora-38" + +[[packages]] +name = "tmux" +version = "*" + +[[packages]] +name = "openssh-server" +version = "*" + +[[groups]] +name = "anaconda-tools" + +[[containers]] +source = "quay.io/fedora/fedora:latest" + +[[containers]] +source = "localhost/test:latest" +local-storage = true + +[customizations] +hostname = "baseimage" + +[customizations.kernel] +name = "kernel-debug" +append = "nosmt=force" + +[customizations.rhsm.config.dnf_plugins.product_id] +enabled = true + +[customizations.rpm.import_keys] +files = [ + "/etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-18-primary", + "/etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-19-primary" +] + +[[customizations.sshkey]] +user = "root" +key = "PUBLIC SSH KEY" + +[[customizations.user]] +name = "admin" +description = "Administrator account" +password = "$6$CHO2$3rN8eviE2t50lmVyBYihTgVRHcaecmeCk31L..." +key = "PUBLIC SSH KEY" +home = "/srv/widget/" +shell = "/usr/bin/bash" +groups = ["widget", "users", "wheel"] +uid = 1200 +gid = 1200 +expiredate = 12345 + +[[customizations.group]] +name = "widget" +gid = 1130 + +[customizations.timezone] +timezone = "US/Eastern" +ntpservers = ["0.north-america.pool.ntp.org", "1.north-america.pool.ntp.org"] + +[customizations.locale] +languages = ["en_US.UTF-8"] +keyboard = "us" + +[customizations.firewall] +ports = ["22:tcp", "80:tcp", "imap:tcp", "53:tcp", "53:udp", "30000-32767:tcp", "30000-32767:udp"] + +[customizations.firewall.services] +enabled = ["ftp", "ntp", "dhcp"] +disabled = ["telnet"] + +[customizations.services] +enabled = ["sshd", "cockpit.socket", "httpd"] +disabled = ["postfix", "telnetd"] +masked = ["rpcbind"] + +[[customizations.directories]] +path = "/etc/foobar" +mode = "0755" +user = "root" +group = "root" +ensure_parents = false + +[[customizations.files]] +path = "/etc/foobar" +mode = "0644" +user = "root" +group = "root" +data = "Hello world!" + +[customizations] +installation_device = "/dev/sda" + +[customizations.ignition.embedded] +config = "eyJpZ25pdGlvbiI6eyJ2ZXJzaW9uIjoiMy4zLjAifSwicGFzc3dkIjp7InVzZXJzIjpbeyJncm91cHMiOlsid2hlZWwiXSwibmFtZSI6ImNvcmUiLCJwYXNzd29yZEhhc2giOiIkNiRqZnVObk85dDFCdjdOLjdrJEhxUnhxMmJsdFIzek15QUhqc1N6YmU3dUJIWEVyTzFZdnFwaTZsamNJMDZkUUJrZldYWFpDdUUubUpja2xQVHdhQTlyL3hwSmlFZFdEcXR4bGU3aDgxIn1dfX0=" + +[customizations.ignition.firstboot] +url = "http://some-server/configuration.ig" + +[customizations.fdo] +manufacturing_server_url = "http://192.168.122.199:8080" +diun_pub_key_insecure = "true" +di_mfg_string_type_mac_iface = "enp2s0" + +[[customizations.repositories]] +id = "example" +name="Example repo" +baseurls=[ "https://example.com/yum/download" ] +gpgcheck=true +gpgkeys = [ "https://example.com/public-key.asc" ] +enabled=true + +[customizations] +partitioning_mode = "lvm" + +[[customizations.filesystem]] +mountpoint = "/var" +minsize = 2147483648 + +[customizations.openscap] +datastream = "/usr/share/xml/scap/ssg/content/ssg-rhel8-ds.xml" +profile_id = "xccdf_org.ssgproject.content_profile_cis" + +[customizations.openscap.tailoring] +selected = [ "xccdf_org.ssgproject.content_bind_crypto_policy" ] +unselected = [ "grub2_password" ] + +[customizations] +fips = true + +[customizations.installer] +unattended = true +sudo-nopasswd = ["user", "%wheel"] + +[customizations.installer.kickstart] +contents = """ +text --non-interactive +zerombr +clearpart --all --initlabel --disklabel=gpt +autopart --noswap --type=lvm +network --bootproto=dhcp --device=link --activate --onboot=on +""" +` + const uploadFile = async (filename: string, content: string): Promise => { const user = userEvent.setup(); const fileInput: HTMLElement | null = diff --git a/src/Components/Blueprints/ImportBlueprintModal.tsx b/src/Components/Blueprints/ImportBlueprintModal.tsx index 21808c33d..3c8eb757e 100644 --- a/src/Components/Blueprints/ImportBlueprintModal.tsx +++ b/src/Components/Blueprints/ImportBlueprintModal.tsx @@ -17,10 +17,11 @@ import { addNotification } from '@redhat-cloud-services/frontend-components-noti import { useNavigate } from 'react-router-dom'; import { useAppDispatch } from '../../store/hooks'; -import { BlueprintExportResponse } from '../../store/imageBuilderApi'; +import { BlueprintExportResponse, Container, Directory, Distributions, Fdo, Filesystem, FirewallCustomization, Ignition, Installer, Kernel, Locale, OpenScap, Services, Timezone } from '../../store/imageBuilderApi'; import { wizardState } from '../../store/wizardSlice'; import { resolveRelPath } from '../../Utilities/path'; import { mapExportRequestToState } from '../CreateImageWizard/utilities/requestMapper'; +import { mapOnPremToHosted } from './OnPremToHostedBlueprintMapper'; interface ImportBlueprintModalProps { setShowImportModal: React.Dispatch>; @@ -33,11 +34,11 @@ export const ImportBlueprintModal: React.FunctionComponent< const onImportClose = () => { setShowImportModal(false); setFilename(''); - setJsonContent(''); + setFileContent(''); setIsRejected(false); setIsInvalidFormat(false); }; - const [jsonContent, setJsonContent] = React.useState(''); + const [fileContent, setFileContent] = React.useState(''); const [importedBlueprint, setImportedBlueprint] = React.useState(); const [isInvalidFormat, setIsInvalidFormat] = React.useState(false); @@ -50,52 +51,63 @@ export const ImportBlueprintModal: React.FunctionComponent< _event: React.ChangeEvent | React.DragEvent, file: File ) => { + setFileContent(''); setFilename(file.name); setIsRejected(false); setIsInvalidFormat(false); }; + React.useEffect(() => { + if (filename && fileContent) { + try { + const isToml = filename.endsWith('.toml'); + const isJson = filename.endsWith('.json'); + if (isToml) { + var toml = require('toml'); + const tomlBlueprint = toml.parse(fileContent); + const blueprintFromFile = mapOnPremToHosted(tomlBlueprint); + const importBlueprintState = mapExportRequestToState( + blueprintFromFile, [] + ); + setImportedBlueprint(importBlueprintState); + } else if (isJson) { + const blueprintFromFile = JSON.parse(fileContent); + const blueprintExportedResponse: BlueprintExportResponse = { + name: blueprintFromFile.name, + description: blueprintFromFile.description, + distribution: blueprintFromFile.distribution, + customizations: blueprintFromFile.customizations, + metadata: blueprintFromFile.metadata, + }; + const importBlueprintState = mapExportRequestToState( + blueprintExportedResponse, + blueprintFromFile.image_requests || [] + ); + setImportedBlueprint(importBlueprintState); + } + } catch (error) { + setIsInvalidFormat(true); + dispatch( + addNotification({ + variant: 'warning', + title: 'No blueprint was build', + description: error?.data?.error?.message, + }) + ); + } + } + }, [filename, fileContent]); const handleClear = () => { setFilename(''); - setJsonContent(''); + setFileContent(''); setIsRejected(false); setIsInvalidFormat(false); }; - const handleTextChange = ( - _: React.ChangeEvent, - value: string - ) => { - setJsonContent(value); - }; const handleDataChange = (_: DropEvent, value: string) => { - try { - const blueprintFromFile = JSON.parse(value); - const blueprintExportedResponse: BlueprintExportResponse = { - name: blueprintFromFile.name, - description: blueprintFromFile.description, - distribution: blueprintFromFile.distribution, - customizations: blueprintFromFile.customizations, - metadata: blueprintFromFile.metadata, - }; - const importBlueprintState = mapExportRequestToState( - blueprintExportedResponse, - blueprintFromFile.image_requests || [] - ); - setImportedBlueprint(importBlueprintState); - setJsonContent(value); - } catch (error) { - setIsInvalidFormat(true); - dispatch( - addNotification({ - variant: 'warning', - title: 'No blueprint was build', - description: error?.data?.error?.message, - }) - ); - } + setFileContent(value); }; const handleFileRejected = () => { setIsRejected(true); - setJsonContent(''); + setFileContent(''); setFilename(''); }; const handleFileReadStarted = () => { @@ -119,12 +131,11 @@ export const ImportBlueprintModal: React.FunctionComponent<