diff --git a/.changes/nextrelease/rus.json b/.changes/nextrelease/rus.json new file mode 100644 index 0000000000..f197fff503 --- /dev/null +++ b/.changes/nextrelease/rus.json @@ -0,0 +1,7 @@ +[ + { + "type": "feature", + "category": "Script", + "description": "Support for removing unused AWS services via Composer." + } +] \ No newline at end of file diff --git a/README.md b/README.md index 89b4668f9b..44999ef27b 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,11 @@ Jump To: 1. **Sign up for AWS** – Before you begin, you need to sign up for an AWS account and retrieve your [AWS credentials][docs-signup]. -1. **Minimum requirements** – To run the SDK, your system will need to meet the +2. **Minimum requirements** – To run the SDK, your system will need to meet the [minimum requirements][docs-requirements], including having **PHP >= 5.5**. We highly recommend having it compiled with the cURL extension and cURL 7.16.2+ compiled with a TLS backend (e.g., NSS or OpenSSL). -1. **Install the SDK** – Using [Composer] is the recommended way to install the +3. **Install the SDK** – Using [Composer] is the recommended way to install the AWS SDK for PHP. The SDK is available via [Packagist] under the [`aws/aws-sdk-php`][install-packagist] package. If Composer is installed globally on your system, you can run the following in the base directory of your project to add the SDK as a dependency: ``` @@ -40,10 +40,14 @@ Jump To: [Installation section of the User Guide][docs-installation] for more detailed information about installing the SDK through Composer and other means. -1. **Using the SDK** – The best way to become familiar with how to use the SDK +4. **Using the SDK** – The best way to become familiar with how to use the SDK is to read the [User Guide][docs-guide]. The [Getting Started Guide][docs-quickstart] will help you become familiar with the basic concepts. +5. **Beta: Removing unused services** — To date, there are over 300 AWS services available for use with this SDK. + You will likely not need them all. If you use Composer and would like to learn more about this feature, + please read the [linked documentation][docs-script-composer]. + ## Quick Examples @@ -190,6 +194,7 @@ We work hard to provide a high-quality and useful SDK for our AWS services, and [docs-s3-transfer]: https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/s3-transfer.html [docs-s3-multipart]: https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/s3-multipart-upload.html [docs-s3-encryption]: https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/s3-encryption-client.html +[docs-script-composer]: https://github.com/aws/aws-sdk-php/tree/master/src/Script/Composer [aws]: http://aws.amazon.com [aws-iam-credentials]: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/UsingIAM.html#UsingIAMrolesWithAmazonEC2Instances diff --git a/composer.json b/composer.json index 0e716eed51..a0b763fe53 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "aws/aws-crt-php": "^1.0.2" }, "require-dev": { + "composer/composer" : "^1.10.22", "ext-openssl": "*", "ext-dom": "*", "ext-pcntl": "*", diff --git a/src/Script/Composer/Composer.php b/src/Script/Composer/Composer.php new file mode 100644 index 0000000000..137c1b5586 --- /dev/null +++ b/src/Script/Composer/Composer.php @@ -0,0 +1,98 @@ +getComposer(); + $extra = $composer->getPackage()->getExtra(); + $listedServices = isset($extra['aws/aws-sdk-php']) + ? $extra['aws/aws-sdk-php'] + : []; + + if ($listedServices) { + $serviceMapping = self::buildServiceMapping(); + self::verifyListedServices($serviceMapping, $listedServices); + $filesystem = $filesystem ?: new Filesystem(); + $vendorPath = $composer->getConfig()->get('vendor-dir'); + self::removeServiceDirs( + $event, + $filesystem, + $serviceMapping, + $listedServices, + $vendorPath + ); + } else { + throw new \InvalidArgumentException( + 'There are no services listed. Did you intend to use this script?' + ); + } + } + + public static function buildServiceMapping() + { + $serviceMapping = []; + $source = Aws\manifest(); + + foreach ($source as $key => $value) { + $serviceMapping[$value['namespace']] = $key; + } + + return $serviceMapping; + } + + private static function verifyListedServices($serviceMapping, $listedServices) + { + foreach ($listedServices as $serviceToKeep) { + if (!isset($serviceMapping[$serviceToKeep])) { + throw new \InvalidArgumentException( + "'$serviceToKeep' is not a valid AWS service namespace. Please check spelling and casing." + ); + } + } + } + + private static function removeServiceDirs( + $event, + $filesystem, + $serviceMapping, + $listedServices, + $vendorPath + ) { + $unsafeForDeletion = ['Kms', 'S3', 'SSO', 'Sts']; + if (in_array('DynamoDbStreams', $listedServices)) { + $unsafeForDeletion[] = 'DynamoDb'; + } + + $clientPath = $vendorPath . '/aws/aws-sdk-php/src/'; + $modelPath = $clientPath . 'data/'; + $deleteCount = 0; + + foreach ($serviceMapping as $clientName => $modelName) { + if (!in_array($clientName, $listedServices) && + !in_array($clientName, $unsafeForDeletion) + ) { + $clientDir = $clientPath . $clientName; + $modelDir = $modelPath . $modelName; + + if ($filesystem->exists([$clientDir, $modelDir])) { + $filesystem->remove([$clientDir, $modelDir]);; + $deleteCount++; + } + } + } + $event->getIO()->write( + "Removed $deleteCount AWS services" + ); + } +} \ No newline at end of file diff --git a/src/Script/Composer/README.md b/src/Script/Composer/README.md new file mode 100644 index 0000000000..742f7e01d4 --- /dev/null +++ b/src/Script/Composer/README.md @@ -0,0 +1,41 @@ +## Removing Unused Services +**NOTE:** This feature is currently in beta. If you have general questions about usage or would like to report a +bug, please open an issue with us [here](https://github.com/aws/aws-sdk-php/issues/new/choose). If +you have feedback on the implementation, please visit the [open discussion](https://github.com/aws/aws-sdk-php/discussions/2420) +we have on the topic. + +To avoid shipping unused services, specify which services you would like to keep in your `composer.json` file and +use the `Aws\\Script\\Composer::removeUnusedServices` script: + +``` +{ + "require": { + "aws/aws-sdk-php": "" + }, + "scripts": { + "pre-autoload-dump": "Aws\\Script\\Composer\\Composer::removeUnusedServices" + }, + "extra": { + "aws/aws-sdk-php": [ + "Ec2", + "CloudWatch" + ] + } +} +``` + +In this example, all services deemed safe for deletion will be removed except for Ec2 and CloudWatch. When listing a +service, keep in mind that an exact match is needed on the client namespace, otherwise, an error will be +thrown. For a list of client namespaces, please see the `Namespaces` list in the +[documentation](https://docs.aws.amazon.com/aws-sdk-php/v3/api/index.html). Run `composer install` or `composer update` +to start service removal. + +**NOTE:** S3, Kms, SSO and Sts are used by core SDK functionality and thus are unsafe for deletion. They are excluded +from deletion in this script. +If you accidentally remove a service you'd like to keep, you will need to reinstall the SDK. +We suggest using `composer reinstall aws/aws-sdk-php`. + + + + + diff --git a/tests/Script/ComposerTest.php b/tests/Script/ComposerTest.php new file mode 100644 index 0000000000..168b9e0fbd --- /dev/null +++ b/tests/Script/ComposerTest.php @@ -0,0 +1,171 @@ +expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage( + "'$invalidService' is not a valid AWS service namespace. Please check spelling and casing." + ); + } else { + $this->setExpectedException(\InvalidArgumentException::class); + } + Composer::removeUnusedServices($this->getMockEvent($serviceList)); + } + + public function testNoListedServices() + { + if (method_exists($this, 'expectException')) { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage( + "There are no services listed. Did you intend to use this script?" + ); + } else { + $this->setExpectedException(\InvalidArgumentException::class); + } + Composer::removeUnusedServices($this->getMockEvent([])); + } + + public function servicesToKeepProvider() + { + return [ + [['S3']], + [['S3', 'Rds']], + [['signer']], + [['signer', 'kendra']], + [['CloudFront', 'SageMaker']], + [['DynamoDbStreams']] + ]; + } + + /** + * @dataProvider servicesToKeepProvider + * + * @param $servicesToKeep + */ + public function testRemoveServices($servicesToKeep) + { + $filesystem = new Filesystem(); + + $tempDir = sys_get_temp_dir(); + $vendorDir = $tempDir . '/aws/aws-sdk-php'; + $clientPath = $vendorDir . '/src/'; + $modelPath = $clientPath . 'data/'; + + $serviceList = composer::buildServiceMapping(); + + foreach ($serviceList as $client => $data) { + $clientDir = $clientPath . $client; + $modelDir = $modelPath . $data; + + $filesystem->mkdir($clientDir); + $filesystem->mkdir($modelDir); + } + $filesystem->mkdir( $clientPath . 'Api'); + + $unsafeForDeletion = ['Kms', 'S3', 'SSO', 'Sts']; + if (in_array('DynamoDbStreams', $servicesToKeep)) { + $unsafeForDeletion[] = 'DynamoDb'; + } + //offset to allow for values listed as unsafe and also to keep + $servicesKept = count($servicesToKeep); + $unsafeAndNotKept = count($unsafeForDeletion) - count(array_intersect($servicesToKeep, $unsafeForDeletion)); + $keptActual = $servicesKept + $unsafeAndNotKept; + $servicesToRemove = (count($serviceList) - $keptActual); + $message = 'Removed ' . $servicesToRemove . ' AWS services'; + + Composer::removeUnusedServices( + $this->getMockEvent($servicesToKeep, $tempDir, $message), + $filesystem + ); + + $this->assertTrue($filesystem->exists($clientPath . 'Api')); + foreach ($serviceList as $client => $data) { + $clientDir = $clientPath . $client; + $modelDir = $modelPath . $data; + + if (!in_array($client, $servicesToKeep) && + !in_array($client, $unsafeForDeletion) + ) { + $this->assertFalse($filesystem->exists([$clientDir, $modelDir])); + } else { + $this->assertTrue($filesystem->exists([$clientDir, $modelDir])); + } + } + } + + private function getMockEvent( + array $servicesToKeep, + $vendorDir = '', + $message = null + ) { + $mockPackage = $this->getMockBuilder('Composer\Package\RootPackage') + ->disableOriginalConstructor() + ->getMock(); + $mockPackage->expects($this->any()) + ->method('getExtra') + ->willReturn(['aws/aws-sdk-php' => $servicesToKeep]); + + $mockConfig = $this->getMockBuilder('Composer\Config') + ->disableOriginalConstructor() + ->getMock(); + $mockConfig->expects($this->any()) + ->method('get') + ->willReturn($vendorDir); + + $mockComposer = $this->getMockBuilder('Composer\Composer') + ->disableOriginalConstructor() + ->getMock(); + $mockComposer->expects($this->any()) + ->method('getPackage') + ->willReturn($mockPackage); + $mockComposer->expects($this->any()) + ->method('getConfig') + ->willReturn($mockConfig); + + $mockEvent = $this->getMockBuilder('Composer\Script\Event') + ->disableOriginalConstructor() + ->getMock(); + $mockEvent->expects($this->any()) + ->method('getComposer') + ->willReturn($mockComposer); + + if ($message) { + $mockIO = $this->getMockBuilder('Composer\IO\ConsoleIO') + ->disableOriginalConstructor() + ->getMock(); + $mockIO->expects($this->once()) + ->method('write') + ->with($message); + $mockEvent->expects($this->any()) + ->method('getIO') + ->willReturn($mockIO); + } + + return $mockEvent; + } +} \ No newline at end of file