Skip to content

Commit

Permalink
feat(dev): add generate library options to add-component command (goo…
Browse files Browse the repository at this point in the history
  • Loading branch information
ajupazhamayil authored Dec 19, 2023
1 parent bf39664 commit 3ce0749
Show file tree
Hide file tree
Showing 4 changed files with 377 additions and 13 deletions.
219 changes: 209 additions & 10 deletions dev/src/Command/AddComponentCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,23 @@
use Google\Cloud\Dev\Composer;
use Google\Cloud\Dev\Component;
use Google\Cloud\Dev\NewComponent;
use Google\Cloud\Dev\RunProcess;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\Process;
use Symfony\Component\Yaml\Yaml;
use GuzzleHttp\Client;
use Twig\Loader\FilesystemLoader;
use Twig\Environment;
use RuntimeException;
use Exception;

/**
* Add a Component
Expand All @@ -53,25 +58,45 @@ class AddComponentCommand extends Command
'phpunit.xml.dist.twig',
'README.md.twig',
];
private const BAZEL_VERSION = '6.0.0';
private const OWLBOT_CLI_IMAGE = 'gcr.io/cloud-devrel-public-resources/owlbot-cli:latest';
private const OWLBOT_PHP_IMAGE = 'gcr.io/cloud-devrel-public-resources/owlbot-php:latest';

private $input;
private $output;
private $rootPath;
private $httpClient;
private RunProcess $runProcess;

/**
* @param string $rootPath The path to the repository root directory.
* @param Client $httpClient specify the HTTP client, useful for tests.
* @param RunProcess $runProcess Instance to execute Symfony Process commands, useful for tests.
*/
public function __construct($rootPath)
public function __construct($rootPath, Client $httpClient = null, RunProcess $runProcess = null)
{
$this->rootPath = realpath($rootPath);
$this->httpClient = $httpClient ?: new Client();
$this->runProcess = $runProcess ?: new RunProcess();
parent::__construct();
}

protected function configure()
{
$this->setName('add-component')
->setDescription('Add a Component')
->addArgument('proto', InputArgument::REQUIRED, 'Path to service proto.');
->addArgument('proto', InputArgument::REQUIRED, 'Path to service proto.')
->addOption(
'googleapis-gen-path',
null,
InputOption::VALUE_REQUIRED,
'Path to googleapis-gen repo. Option to generate the library using Owlbot:copy-code'
)->addOption(
'bazel-path',
null,
InputOption::VALUE_REQUIRED,
'Path to bazel (googleapis) workspace. Option to generate the library using Bazel'
);
}

protected function execute(InputInterface $input, OutputInterface $output)
Expand Down Expand Up @@ -111,16 +136,19 @@ protected function execute(InputInterface $input, OutputInterface $output)
->render();
}

$productHomepage = $this->getHelper('question')->ask(
$productDocumentation = null;
$yamlFileContent = $this->loadYamlConfigContent($new, dirname($proto));
$productDocumentation = $yamlFileContent['publishing']['documentation_uri'] ?? null;
$productDocumentation = $productDocumentation ?: $this->getHelper('question')->ask(
$input,
$output,
new Question('What is the product homepage? ')
new Question('What is the product documentation URL? ')
);

$productDocumentation = $this->getHelper('question')->ask(
$productHomePage = $this->getHomePageFromDocsUrl($productDocumentation);
$productHomePage = $productHomePage ?: $this->getHelper('question')->ask(
$input,
$output,
new Question('What is the product documentation URL? ')
new Question('What is the product homepage? ')
);

$documentationUrl = $new->getDocumentationUrl();
Expand Down Expand Up @@ -155,7 +183,7 @@ protected function execute(InputInterface $input, OutputInterface $output)
'version' => $new->version,
'github_repo' => $new->githubRepo,
'documentation' => $documentationUrl,
'product_homepage' => $productHomepage,
'product_homepage' => $productHomePage,
'product_documentation' => $productDocumentation,
]));
}
Expand Down Expand Up @@ -184,21 +212,192 @@ protected function execute(InputInterface $input, OutputInterface $output)
$composer->updateMainComposer();
$composer->createComponentComposer($new->displayName, $new->githubRepo);

if ($input->getOption('bazel-path') || $input->getOption('googleapis-gen-path')) {
$this->validateOptions($input, $output);
$this->checkDockerAvailable();
if ($input->getOption('bazel-path')) {
$googleApisDir = realpath($input->getOption('bazel-path'));
$output->writeln("\n\nbazel build library");
$output->writeln($this->bazelQueryAndBuildLibrary(dirname($protoFile), $googleApisDir));
$output->writeln("\n\nCopying the library code from bazel-bin");
$output->writeln($this->owlbotCopyBazelBin($new->componentName, $googleApisDir));
} else {
$googleApisGenDir = realpath($input->getOption('googleapis-gen-path'));
$output->writeln("\n\nCopying the library code from googleapis-gen");
$output->writeln($this->owlbotCopyCode($new->componentName, $googleApisGenDir));
}
// run owlbot post-processor if a bazel-path or googleapis-gen-path was supplied
$output->writeln("\n\nOwlbot post processing");
$output->writeln($this->owlbotPostProcessor());
}

$output->writeln('');
$output->writeln('');
$output->writeln('Success!');

return 0;
}

private function validateOptions(InputInterface $input, OutputInterface $output): void
{
if ($input->getOption('bazel-path') && $input->getOption('googleapis-gen-path')) {
throw new \InvalidArgumentException(
'The options --googleapis-gen-path and --bazel-path cannot be used together.' .
' Please provide only one path option.'
);
}
}

private function loadYamlConfigContent(NewComponent $new, string $protoDir): ?array
{
$yamlFilePath = sprintf('%s/%s%s%s.yaml',
$protoDir,
strtolower($new->shortName),
$new->version ? '_' : '',
$new->version ?? ''
);

try {
return Yaml::parse($this->loadProtoContent($yamlFilePath));
} catch (Exception $e) {
// Handle error gracefully.
return null;
}
}

private function loadProtoContent(string $proto): string
{
if (file_exists($proto)) {
return file_get_contents($proto);
}
$protoUrl = 'https://raw.githubusercontent.com/googleapis/googleapis/master/' . $proto;
$client = new Client();
$response = $client->get($protoUrl);
$response = $this->httpClient->get($protoUrl);
return (string) $response->getBody();
}

private function bazelQueryAndBuildLibrary(string $protoDir, string $googleApisDir): string
{
$command = ['bazel', '--version'];
$output = $this->runProcess->execute($command);
// Extract the version number from the output
$match = preg_match('/bazel\s+(?P<version>\d+\.\d+\.\d+)/', $output, $matches);
if (!$match || $matches['version'] !== self::BAZEL_VERSION) {
throw new RuntimeException('Bazel 6.0.0 is not available');
}

$command = [
'bazel',
'query',
'filter("-(php)$", kind("rule", //' . $protoDir .'/...:*))'
];
$output = $this->runProcess->execute($command, $googleApisDir);
// Get componenets starting with //google/ and
// not ending with :(proto|grpc|gapic)-.*-php
$components = array_filter(
explode("\n", $output),
fn ($line) => $line && preg_match('/^\/\/google\/(?!:(proto|grpc|gapic)-.*-php$)/', $line)
);
if (count($components) !== 1) {
throw new Exception(
'expected only one bazel component, found ' .
(implode(' ', $components) ?: '0')
);
}

$command = ['bazel', 'build', $components[0]];
return $this->runProcess->execute($command, $googleApisDir);
}

private function owlbotCopyCode(string $componentName, string $googleApisGenDir): string
{
list($userId, $groupId) = $this->getUserAndGroupId();
$command = [
'docker',
'run',
'--rm',
'--user',
sprintf('%s::%s', $userId, $groupId),
'-v',
$this->rootPath . ':/repo',
'-v',
$googleApisGenDir . ':/googleapis-gen',
'-w',
'/repo',
'--env',
'HOME=/tmp',
self::OWLBOT_CLI_IMAGE,
'copy-code',
sprintf('--config-file=%s/.OwlBot.yaml', $componentName),
'--source-repo=/googleapis-gen'
];
return $this->runProcess->execute($command);
}

private function owlbotCopyBazelBin(string $componentName, string $googleApisDir): string
{
list($userId, $groupId) = $this->getUserAndGroupId();
$command = [
'docker',
'run',
'--rm',
'--user',
sprintf('%s::%s', $userId, $groupId),
'-v',
$this->rootPath . ':/repo',
'-v',
$googleApisDir . '/bazel-bin:/bazel-bin',
self::OWLBOT_CLI_IMAGE,
'copy-bazel-bin',
sprintf('--config-file=%s/.OwlBot.yaml', $componentName),
'--source-dir',
'/bazel-bin',
'--dest',
'/repo'
];
return $this->runProcess->execute($command);
}

private function owlbotPostProcessor(): string
{
list($userId, $groupId) = $this->getUserAndGroupId();
$command = [
'docker',
'run',
'--rm',
'--user',
sprintf('%s::%s', $userId, $groupId),
'-v',
$this->rootPath . ':/repo',
'-w',
'/repo',
self::OWLBOT_PHP_IMAGE
];
return $this->runProcess->execute($command);
}

private function getHomePageFromDocsUrl(?string $url): ?string
{
$productHomePage = !empty($url) ? explode('/docs', $url)[0] : null;
$response = $this->httpClient->get($productHomePage, ['http_errors' => false]);
return $response->getStatusCode() >= 400 ? null : $productHomePage;
}

private function checkDockerAvailable(): void
{
$command = ['which', 'docker'];
$output = $this->runProcess->execute($command);
if (strlen($output) == 0) {
throw new RuntimeException(
'Error: Docker is not available.'
);
}
}

private function getUserAndGroupId(): array
{
// Get the user ID and group ID
$userId = posix_getuid();
$groupId = posix_getgid();
return [$userId, $groupId];
}
}
45 changes: 45 additions & 0 deletions dev/src/RunProcess.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
/**
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Google\Cloud\Dev;

use Symfony\Component\Process\Process;

/**
* Execute Symfony Process commands and return the results.
*
* Helps unit testing.
*
* @internal
*/
class RunProcess
{
/**
* Executing commands, in Windows may behave differently.
*
* @param string $command
* @param string|null $cwd
* @return string $shellOutput
*/
public function execute(array $command, string $cwd = null): string
{
$process = new Process($command, $cwd);
$process->mustRun();

return $process->getOutput() . $process->getErrorOutput();
}
}
Loading

0 comments on commit 3ce0749

Please sign in to comment.