diff --git a/config.yaml b/config.yaml index 7a94c96b95..89c27da28d 100644 --- a/config.yaml +++ b/config.yaml @@ -80,6 +80,10 @@ service: - standard - medium - large + catalog: + - https://github.com/platformsh/template-builder/blob/master/templates/akeneo/.platform.template.yaml + - https://github.com/platformsh/template-builder/blob/master/templates/beego/.platform.template.yaml + - https://github.com/platformsh/template-builder/blob/master/templates/django1/.platform.template.yaml # Configuration relating to API calls. # This can be overridden in the user config file. diff --git a/src/Command/Project/CreateConsoleForm.php b/src/Command/Project/CreateConsoleForm.php new file mode 100644 index 0000000000..2b3e492797 --- /dev/null +++ b/src/Command/Project/CreateConsoleForm.php @@ -0,0 +1,67 @@ +getErrorOutput() : $output; + print_r('catalog: ' . $input->getOption('catalog')); + print_r('template: ' . $input->getOption('template')); + foreach ($this->fields as $key => $field) { + $field->onChange($values); + + // Check for the catalog flag. + if ($field->getOptionName() == 'catalog_url' && $input->getOption('catalog')!==true) { + continue; + } + // Check if the initialize field should be shown. + if ($field->getOptionName() == 'initialized') { + // Do not show if the neither catalog or template flags are present. + if ($input->getOption('catalog')==false && $input->getOption('template')==false) { + continue; + } + // Do not show is the initialize flag is present. + if ($input->getOption('initialize')==true) { + continue; + } + } + if (!$this->includeField($field, $values)) { + continue; + } + + // Get the value from the command-line options. + $value = $field->getValueFromInput($input, false); + if ($value !== null) { + $field->validate($value); + } elseif ($input->isInteractive()) { + // Get the value interactively. + $value = $helper->ask($input, $stdErr, $field->getAsQuestion()); + $stdErr->writeln(''); + } elseif ($field->isRequired()) { + throw new MissingValueException('--' . $field->getOptionName() . ' is required'); + } + + self::setNestedArrayValue( + $values, + $field->getValueKeys() ?: [$key], + $field->getFinalValue($value), + true + ); + } + + return $values; + } +} diff --git a/src/Command/Project/ProjectCreateCommand.php b/src/Command/Project/ProjectCreateCommand.php index ad4df76b9c..1c5a1008ea 100644 --- a/src/Command/Project/ProjectCreateCommand.php +++ b/src/Command/Project/ProjectCreateCommand.php @@ -7,7 +7,8 @@ use Platformsh\Cli\Console\Bot; use Platformsh\ConsoleForm\Field\Field; use Platformsh\ConsoleForm\Field\OptionsField; -use Platformsh\ConsoleForm\Form; +use Platformsh\ConsoleForm\Field\BooleanField; +use Platformsh\Cli\Command\Project\CreateConsoleForm; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -27,11 +28,15 @@ protected function configure() ->setAliases(['create']) ->setDescription('Create a new project'); - $this->form = Form::fromArray($this->getFields()); + $this->form = CreateConsoleForm::fromArray($this->getFields()); $this->form->configureInputDefinition($this->getDefinition()); $this->addOption('check-timeout', null, InputOption::VALUE_REQUIRED, 'The API timeout while checking the project status', 30) - ->addOption('timeout', null, InputOption::VALUE_REQUIRED, 'The total timeout for all API checks (0 to disable the timeout)', 900); + ->addOption('timeout', null, InputOption::VALUE_REQUIRED, 'The total timeout for all API checks (0 to disable the timeout)', 900) + ->addOption('template', null, InputOption::VALUE_REQUIRED, 'Choose a starting template or provide a url of one.') + ->addOption('catalog', null, InputOption::VALUE_NONE, 'Choose a template from the catalog') + ->addOption('initialize', null, InputOption::VALUE_NONE, 'Initialize the project after it has been created.'); + $this->setHelp(<<getOption('initialize')==true && + ($input->getOption('catalog')==false && + $input->getOption('template')==false)) { + + $this->stdErr->writeln("Projects cannot be initialized without a template file. +If you would like to use the --initialize option please provide a template file by utilizing +the --template or --catalog options. For more information on this command please type project:create --help."); + return 0; + } /** @var \Platformsh\Cli\Service\QuestionHelper $questionHelper */ $questionHelper = $this->getService('question_helper'); @@ -63,6 +77,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $estimate = $this->api() ->getClient() ->getSubscriptionEstimate($options['plan'], $options['storage'], $options['environments'], 1); + $costConfirm = sprintf( 'The estimated monthly cost of this project is: %s', $estimate['total'] @@ -77,9 +92,17 @@ protected function execute(InputInterface $input, OutputInterface $output) if (!$questionHelper->confirm($costConfirm)) { return 1; } + if (!empty($options['catalog_url'])) { + $options['catalog'] = $options['catalog_url']; + } + else if (!empty($input->getOption('template'))) { + $options['catalog'] = $input->getOption('template'); + } + $subscription = $this->api()->getClient() ->createSubscription( + $options['catalog'], $options['region'], $options['plan'], $options['title'], @@ -129,7 +152,6 @@ protected function execute(InputInterface $input, OutputInterface $output) $timedOut = $totalTimeout ? time() - $start > $totalTimeout : false; } $this->stdErr->writeln(''); - if (!$subscription->isActive()) { if ($timedOut) { $this->stdErr->writeln('The project failed to activate on time'); @@ -146,14 +168,34 @@ protected function execute(InputInterface $input, OutputInterface $output) return 1; } - $this->stdErr->writeln("The project is now ready!"); + if ($options['initialize']) { + // Check that the profile and repository are present and initializable. + if (empty($subscription->project_options['initialize']['repository'])) { + $this->stdErr->writeln("The project has been created but cannot be initialized because the project repository is empty."); + } + // Use the existing initialize command. + $project = $this->selectProject($subscription->project_id); + $environment = $this->api()->getEnvironment('master', $project, null, true); + $environment->initialize($subscription->project_options['initialize']['profile'], $subscription->project_options['initialize']['repository']); + $this->api()->clearEnvironmentsCache($environment->project); + $this->stdErr->writeln("The project has been initialized and is ready!"); + } + else { + $this->stdErr->writeln("The project is now ready!"); + } + $output->writeln($subscription->project_id); $this->stdErr->writeln(''); + if (!empty($subscription->project_options['initialize'])) { + $this->stdErr->writeln(" Template: {$subscription->project_options[initialize][repository] }"); + } $this->stdErr->writeln(" Region: {$subscription->project_region}"); $this->stdErr->writeln(" Project ID: {$subscription->project_id}"); $this->stdErr->writeln(" Project title: {$subscription->project_title}"); $this->stdErr->writeln(" URL: {$subscription->project_ui}"); + + return 0; } @@ -216,6 +258,33 @@ protected function getAvailableRegions($runtime = false) return $regions; } + /** + * Return the catalog. + * + * The default list is in the config `service.catalog`. This is + * replaced at runtime by an API call. + * + * @param bool $runtime + * + * @return array + */ + protected function getAvailableCatalog($runtime = false) + { + if ($runtime) { + $catalog = []; + foreach ($this->api()->getClient()->getCatalog()->getData() as $item) { + if ($item['info'] && $item['template']) { + $catalog[$item['template']] = $item['info']['name']; + } + } + $catalog['empty'] = 'Empty Project'; + } else { + $catalog = (array) $this->config()->get('service.catalog'); + } + + return $catalog; + } + /** * Returns a list of ConsoleForm form fields for this command. * @@ -223,22 +292,32 @@ protected function getAvailableRegions($runtime = false) */ protected function getFields() { - return [ - 'title' => new Field('Project title', [ + $fields = []; + + $fields['title'] = new Field('Project title', [ 'optionName' => 'title', 'description' => 'The initial project title', 'questionLine' => '', 'default' => 'Untitled Project', - ]), - 'region' => new OptionsField('Region', [ + ]); + $fields['catalog_url'] = new OptionsField('Catalog', [ + 'optionName' => 'catalog_url', + 'description' => 'The template from which to create your project or your own blank project.', + 'options' => $this->getAvailableCatalog(), + 'asChoice' => FALSE, + 'optionsCallback' => function () { + return $this->getAvailableCatalog(true); + }, + ]); + $fields['region'] = new OptionsField('Region', [ 'optionName' => 'region', 'description' => 'The region where the project will be hosted', 'options' => $this->getAvailableRegions(), 'optionsCallback' => function () { return $this->getAvailableRegions(true); }, - ]), - 'plan' => new OptionsField('Plan', [ + ]); + $fields['plan'] = new OptionsField('Plan', [ 'optionName' => 'plan', 'description' => 'The subscription plan', 'options' => $this->getAvailablePlans(), @@ -247,23 +326,29 @@ protected function getFields() }, 'default' => in_array('development', $this->getAvailablePlans()) ? 'development' : null, 'allowOther' => true, - ]), - 'environments' => new Field('Environments', [ + ]); + $fields['environments'] = new Field('Environments', [ 'optionName' => 'environments', 'description' => 'The number of environments', 'default' => 3, 'validator' => function ($value) { return is_numeric($value) && $value > 0 && $value < 50; }, - ]), - 'storage' => new Field('Storage', [ + ]); + $fields['storage'] = new Field('Storage', [ 'description' => 'The amount of storage per environment, in GiB', 'default' => 5, 'validator' => function ($value) { return is_numeric($value) && $value > 0 && $value < 1024; }, - ]), - ]; + ]); + $fields['initialize'] = new BooleanField('Initialize', [ + 'optionName' => 'initialized', + 'description' => 'Initialize this environment?', + 'questionLine' => 'Initialize this environment?', + ]); + + return $fields; } /**