Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changes/nextrelease/rus.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"type": "feature",
"category": "Script",
"description": "Support for removing unused AWS services via Composer."
}
]
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
```
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"aws/aws-crt-php": "^1.0.2"
},
"require-dev": {
"composer/composer" : "^1.10.22",
"ext-openssl": "*",
"ext-dom": "*",
"ext-pcntl": "*",
Expand Down
98 changes: 98 additions & 0 deletions src/Script/Composer/Composer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php
namespace Aws\Script\Composer;

require_once __DIR__ . '/../../functions.php';

use Aws;
use Composer\Script\Event;
use Symfony\Component\Filesystem\Filesystem;

class Composer
{
public static function removeUnusedServices(
Event $event,
Filesystem $filesystem = null
)
{
$composer = $event->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"
);
}
}
41 changes: 41 additions & 0 deletions src/Script/Composer/README.md
Original file line number Diff line number Diff line change
@@ -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": "<version here>"
},
"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`.





171 changes: 171 additions & 0 deletions tests/Script/ComposerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<?php
namespace Aws\Test\Script;

use Aws;
use Aws\Script\Composer\Composer;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Filesystem\Filesystem;

class ComposerTest extends TestCase
{
public function invalidServiceNameProvider()
{
return [
[['foo'], 'foo'],
[['S3', 'foo'], 'foo'],
[[''], ''],
[['S3', ''], '']
];
}

/**
* @dataProvider invalidServiceNameProvider
*
* @param $serviceList
* @param $invalidService
*/
public function testListInvalidServiceName($serviceList, $invalidService)
{
if (method_exists($this, 'expectException')) {
$this->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;
}
}