@@ -18,7 +18,7 @@ import { Popover } from "@patternfly/react-core/dist/esm/components/Popover";
1818import { MinusIcon , OutlinedQuestionCircleIcon } from '@patternfly/react-icons' ;
1919import * as dockerNames from 'docker-names' ;
2020
21- import { ErrorNotification } from './Notification.jsx' ;
21+ import { ErrorNotification , WarningNotification } from './Notification.jsx' ;
2222import * as utils from './util.js' ;
2323import * as client from './client.js' ;
2424import rest from './rest.js' ;
@@ -54,10 +54,10 @@ const units = {
5454
5555// healthchecks.go HealthCheckOnFailureAction
5656const HealthCheckOnFailureActionOrder = [
57- { value : 0 , label : _ ( "No action" ) } ,
58- { value : 3 , label : _ ( "Restart" ) } ,
59- { value : 4 , label : _ ( "Stop" ) } ,
60- { value : 2 , label : _ ( "Force stop" ) } ,
57+ { value : 0 , label : _ ( "No action" ) , apiName : "none" } ,
58+ { value : 3 , label : _ ( "Restart" ) , apiName : "restart" } ,
59+ { value : 4 , label : _ ( "Stop" ) , apiName : "stop" } ,
60+ { value : 2 , label : _ ( "Force stop" ) , apiName : "kill" } ,
6161] ;
6262
6363const handleEnvValue = ( key , value , idx , onChange , additem , itemCount , companionField ) => {
@@ -175,6 +175,9 @@ export class ImageRunModal extends React.Component {
175175 componentDidMount ( ) {
176176 this . _isMounted = true ;
177177 this . onSearchTriggered ( this . state . searchText ) ;
178+
179+ if ( this . props . prefill )
180+ this . prefillModal ( ) ;
178181 }
179182
180183 componentWillUnmount ( ) {
@@ -635,6 +638,75 @@ export class ImageRunModal extends React.Component {
635638 return owner === systemOwner ;
636639 } ;
637640
641+ prefillModal ( ) {
642+ const container = this . props . container ;
643+ const containerDetail = this . props . containerDetail ;
644+ const image = this . props . localImages . find ( img => img . Id === container . ImageID ) ;
645+ const owner = container . isSystem ? 'system' : this . props . user ;
646+
647+ if ( containerDetail . Config . CreateCommand ) {
648+ this . setState ( {
649+ dialogWarning : _ ( "This container was not created by cockpit" ) ,
650+ dialogWarningDetail : _ ( "Some options may not be copied to the new container." ) ,
651+ } ) ;
652+ }
653+
654+ const env = containerDetail . Config . Env . filter ( variable => {
655+ if ( image . Env . includes ( variable ) ) {
656+ return false ;
657+ }
658+
659+ return ! variable . match ( / ( ( H O M E | T E R M | H O S T N A M E ) = .* ) | c o n t a i n e r = p o d m a n / ) ;
660+ } ) . map ( ( variable , index ) => {
661+ const split = variable . split ( '=' ) ;
662+ return { key : index , envKey : split [ 0 ] , envValue : split [ 1 ] } ;
663+ } ) ;
664+
665+ const publish = container . Ports
666+ ? container . Ports . map ( ( port , index ) => {
667+ return { key : index , IP : port . hostIP || port . host_ip , containerPort : port . containerPort || port . container_port , hostPort : port . hostPort || port . host_port , protocol : port . protocol } ;
668+ } )
669+ : [ ] ;
670+
671+ const volumes = containerDetail . Mounts . map ( ( mount , index ) => {
672+ // podman does not expose SELinux labels
673+ return { key : index , containerPath : mount . Destination , hostPath : mount . Source , mode : ( mount . RW ? 'rw' : 'ro' ) , selinux : '' } ;
674+ } ) ;
675+
676+ // check if memory and cpu limitations or healthcheck are used
677+ const memoryConfigure = containerDetail . HostConfig . Memory > 0 ;
678+ const cpuSharesConfigure = containerDetail . HostConfig . CpuShares > 0 ;
679+ const healthcheck = ! ! containerDetail . Config . Healthcheck ;
680+ const healthCheckOnFailureAction = ( this . props . version . split ( "." ) ) >= [ 4 , 3 , 0 ]
681+ ? HealthCheckOnFailureActionOrder . find ( item => item . apiName === containerDetail . Config . HealthcheckOnFailureAction ) . value
682+ : null ;
683+
684+ this . setState ( {
685+ command : container . Command ? container . Command . join ( ' ' ) : "" ,
686+ containerName : container . Names [ 0 ] + "_copy" ,
687+ env,
688+ hasTTY : containerDetail . Config . Tty ,
689+ publish,
690+ // memory in MB
691+ memory : memoryConfigure ? ( containerDetail . HostConfig . Memory / 1000000 ) : 512 ,
692+ cpuShares : cpuSharesConfigure ? containerDetail . HostConfig . CpuShares : 1024 ,
693+ memoryConfigure,
694+ cpuSharesConfigure,
695+ volumes,
696+ owner,
697+ // unless-stopped: Identical to always
698+ restartPolicy : containerDetail . HostConfig . RestartPolicy . Name === 'unless-stopped' ? 'always' : containerDetail . HostConfig . RestartPolicy . Name ,
699+ selectedImage : image ,
700+ healthcheck_command : healthcheck ? containerDetail . Config . Healthcheck . Test . join ( ' ' ) : "" ,
701+ // convert to seconds
702+ healthcheck_interval : healthcheck ? ( containerDetail . Config . Healthcheck . Interval / 1000000000 ) : 30 ,
703+ healthcheck_timeout : healthcheck ? ( containerDetail . Config . Healthcheck . Timeout / 1000000000 ) : 30 ,
704+ healthcheck_start_period : healthcheck ? ( containerDetail . Config . Healthcheck . StartPeriod / 1000000000 ) : 0 ,
705+ healthcheck_retries : healthcheck ? containerDetail . Config . Healthcheck . Retries : 3 ,
706+ healthcheck_action : healthcheck ? healthCheckOnFailureAction : 0 ,
707+ } ) ;
708+ }
709+
638710 render ( ) {
639711 const Dialogs = this . context ;
640712 const { image } = this . props ;
@@ -688,6 +760,7 @@ export class ImageRunModal extends React.Component {
688760
689761 const defaultBody = (
690762 < Form >
763+ { this . state . dialogWarning && < WarningNotification warningMessage = { this . state . dialogWarning } warningDetail = { this . state . dialogWarningDetail } /> }
691764 { this . state . dialogError && < ErrorNotification errorMessage = { this . state . dialogError } errorDetail = { this . state . dialogErrorDetail } /> }
692765 < FormGroup fieldId = 'run-image-dialog-name' label = { _ ( "Name" ) } className = "ct-m-horizontal" >
693766 < TextInput id = 'run-image-dialog-name'
@@ -938,6 +1011,7 @@ export class ImageRunModal extends React.Component {
9381011 actionLabel = { _ ( "Add port mapping" ) }
9391012 onChange = { value => this . onValueChanged ( 'publish' , value ) }
9401013 default = { { IP : null , containerPort : null , hostPort : null , protocol : 'tcp' } }
1014+ prefill = { this . state . publish }
9411015 itemcomponent = { < PublishPort /> } />
9421016
9431017 < DynamicListForm id = 'run-image-dialog-volume'
@@ -948,6 +1022,7 @@ export class ImageRunModal extends React.Component {
9481022 onChange = { value => this . onValueChanged ( 'volumes' , value ) }
9491023 default = { { containerPath : null , hostPath : null , mode : 'rw' } }
9501024 options = { { selinuxAvailable : this . props . selinuxAvailable } }
1025+ prefill = { this . state . volumes }
9511026 itemcomponent = { < Volume /> } />
9521027
9531028 < DynamicListForm id = 'run-image-dialog-env'
@@ -958,6 +1033,7 @@ export class ImageRunModal extends React.Component {
9581033 onChange = { value => this . onValueChanged ( 'env' , value ) }
9591034 default = { { envKey : null , envValue : null } }
9601035 helperText = { _ ( "Paste one or more lines of key=value pairs into any field for bulk import" ) }
1036+ prefill = { this . state . env }
9611037 itemcomponent = { < EnvVar /> } />
9621038 </ Tab >
9631039 < Tab eventKey = { 2 } title = { < TabTitleText > { _ ( "Health check" ) } </ TabTitleText > } id = "create-image-dialog-tab-healthcheck" className = "pf-c-form pf-m-horizontal" >
@@ -1089,6 +1165,44 @@ export class ImageRunModal extends React.Component {
10891165 </ Tabs >
10901166 </ Form >
10911167 ) ;
1168+
1169+ const cardFooter = ( ) => {
1170+ let createRunText = _ ( "Create and run" ) ;
1171+ let createText = _ ( "Create" ) ;
1172+
1173+ if ( this . props . prefill ) {
1174+ createRunText = _ ( "Clone and run" ) ;
1175+ createText = _ ( "Clone" ) ;
1176+ }
1177+
1178+ return (
1179+ < >
1180+ < Button variant = 'primary' id = "create-image-create-run-btn" onClick = { ( ) => this . onCreateClicked ( true ) } isDisabled = { ( ! image && selectedImage === "" ) } >
1181+ { createRunText }
1182+ </ Button >
1183+ < Button variant = 'secondary' id = "create-image-create-btn" onClick = { ( ) => this . onCreateClicked ( false ) } isDisabled = { ( ! image && selectedImage === "" ) } >
1184+ { createText }
1185+ </ Button >
1186+ < Button variant = 'link' className = 'btn-cancel' onClick = { Dialogs . close } >
1187+ { _ ( "Cancel" ) }
1188+ </ Button >
1189+ </ >
1190+ ) ;
1191+ } ;
1192+
1193+ const modalTitle = ( ) => {
1194+ let titleText = _ ( "Create container" ) ;
1195+
1196+ if ( this . props . prefill && this . props . pod )
1197+ titleText = _ ( "Clone container in $0" ) ;
1198+ else if ( this . props . prefill )
1199+ titleText = _ ( "Clone container" ) ;
1200+ else if ( this . props . pod )
1201+ titleText = _ ( "Create container in $0" ) ;
1202+
1203+ return this . props . pod ? cockpit . format ( titleText , this . props . pod . Name ) : titleText ;
1204+ } ;
1205+
10921206 return (
10931207 < Modal isOpen
10941208 position = "top" variant = "medium"
@@ -1101,18 +1215,8 @@ export class ImageRunModal extends React.Component {
11011215 Dialogs . close ( ) ;
11021216 }
11031217 } }
1104- title = { this . props . pod ? cockpit . format ( _ ( "Create container in $0" ) , this . props . pod . Name ) : _ ( "Create container" ) }
1105- footer = { < >
1106- < Button variant = 'primary' id = "create-image-create-run-btn" onClick = { ( ) => this . onCreateClicked ( true ) } isDisabled = { ! image && selectedImage === "" } >
1107- { _ ( "Create and run" ) }
1108- </ Button >
1109- < Button variant = 'secondary' id = "create-image-create-btn" onClick = { ( ) => this . onCreateClicked ( false ) } isDisabled = { ! image && selectedImage === "" } >
1110- { _ ( "Create" ) }
1111- </ Button >
1112- < Button variant = 'link' className = 'btn-cancel' onClick = { Dialogs . close } >
1113- { _ ( "Cancel" ) }
1114- </ Button >
1115- </ > }
1218+ title = { modalTitle ( ) }
1219+ footer = { cardFooter ( ) }
11161220 >
11171221 { defaultBody }
11181222 </ Modal >
0 commit comments