Skip to content

Commit

Permalink
src: Add on prem blueprints import support
Browse files Browse the repository at this point in the history
  • Loading branch information
avitova committed Oct 15, 2024
1 parent e0b590a commit a6dacf6
Show file tree
Hide file tree
Showing 6 changed files with 321 additions and 43 deletions.
9 changes: 8 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
148 changes: 148 additions & 0 deletions src/Components/Blueprints/ImportBlueprintModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> => {
const user = userEvent.setup();
const fileInput: HTMLElement | null =
Expand Down
91 changes: 51 additions & 40 deletions src/Components/Blueprints/ImportBlueprintModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<React.SetStateAction<boolean>>;
Expand All @@ -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<wizardState>();
const [isInvalidFormat, setIsInvalidFormat] = React.useState(false);
Expand All @@ -50,52 +51,63 @@ export const ImportBlueprintModal: React.FunctionComponent<
_event: React.ChangeEvent<HTMLInputElement> | React.DragEvent<HTMLElement>,
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<HTMLTextAreaElement>,
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 = () => {
Expand All @@ -119,20 +131,19 @@ export const ImportBlueprintModal: React.FunctionComponent<
<FileUpload
id="import-blueprint-file-upload"
type="text"
value={jsonContent}
value={fileContent}
filename={filename}
filenamePlaceholder="Drag and drop a file or upload one"
onFileInputChange={handleFileInputChange}
onDataChange={handleDataChange}
onTextChange={handleTextChange}
onReadStarted={handleFileReadStarted}
onReadFinished={handleFileReadFinished}
onClearClick={handleClear}
isLoading={isLoading}
isReadOnly={true}
browseButtonText="Upload"
dropzoneProps={{
accept: { 'text/json': ['.json'] },
accept: { 'text/json': ['.json'], 'text/plain': ['.toml'] },
maxSize: 25000,
onDropRejected: handleFileRejected,
}}
Expand All @@ -153,7 +164,7 @@ export const ImportBlueprintModal: React.FunctionComponent<
<ActionGroup>
<Button
type="button"
isDisabled={isRejected || isInvalidFormat || !jsonContent}
isDisabled={isRejected || isInvalidFormat || !fileContent}
onClick={() =>
navigate(resolveRelPath(`imagewizard/import`), {
state: { blueprint: importedBlueprint },
Expand Down
Loading

0 comments on commit a6dacf6

Please sign in to comment.