Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "feature",
"description": "Add apiConfig to the aws.api#tagEnabled trait so services can declare non-default operation names (e.g., AddTagsToResource) for the TagResource, UntagResource, and ListTagsForResource slots; tag API discovery and validation honor the override before falling back to default-named operations.",
Comment thread
kstich marked this conversation as resolved.
Outdated
"pull_requests": []
}
Comment thread
taylorlo-aws marked this conversation as resolved.
60 changes: 59 additions & 1 deletion docs/source-2.0/aws/aws-core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1320,11 +1320,17 @@ members:
- ``boolean``
- Set to true to indicate that the service does not have default tagging
operations that create, read, update, and delete tags on resources.
* - apiConfig
- :ref:`Taggable service API config structure <service-taggable-apiconfig-structure>`
- Specifies non-default operation names for the service-wide tagging
APIs. Any unset member falls back to the default-named operation.

The default operations for resource tagging operations are named TagResource,
UntagResource, and ListTagsForResource. All three operations are required to be
in the service, and each operation must satisfy corresponding validation
constraints unless ``disableDefaultOperations`` is set to **true**.
constraints unless ``disableDefaultOperations`` is set to **true**. The
``apiConfig`` member allows a service to use non-default operation names for
any of the three slots; an unset slot falls back to the default name.

The following is a minimal snippet showing the inclusion of the named required
operations for the ``aws.api#tagEnabled`` Weather service.
Expand Down Expand Up @@ -1461,6 +1467,58 @@ through the operates attached to the service.
}
}

A service that uses non-default operation names can specify them via
``apiConfig``. Each member is independently optional; a slot left unset falls
back to the default-named operation.

.. code-block:: smithy

@tagEnabled(apiConfig: {
tagApi: AddTagsToResource
untagApi: RemoveTagsFromResource
listTagsApi: DescribeTagsForResource
})
service Weather {
resources: [Forecast]
operations: [AddTagsToResource, RemoveTagsFromResource, DescribeTagsForResource]
}


.. _service-taggable-apiconfig-structure:

Taggable service API config structure
=====================================

Configuration structure for specifying service-wide tagging operations when
non-default operation names are used. Any unset member falls back to the
default-named operation (``TagResource``, ``UntagResource``,
``ListTagsForResource`` respectively).

**Properties**

.. list-table::
:header-rows: 1
:widths: 30 10 60

* - Property
- Type
- Description
* - tagApi
- ``ShapeID``
- **Optional** Defines the service-wide operation that creates and
updates tags on resources. The value MUST be a valid :ref:`shape-id`
that targets an ``operation`` shape bound to the service.
* - untagApi
- ``ShapeID``
- **Optional** Defines the service-wide operation that removes tags
from resources. The value MUST be a valid :ref:`shape-id` that
targets an ``operation`` shape bound to the service.
* - listTagsApi
- ``ShapeID``
- **Optional** Defines the service-wide operation that lists tags on
resources. The value MUST be a valid :ref:`shape-id` that targets an
``operation`` shape bound to the service.


.. smithy-trait:: aws.api#taggable
.. _taggable-trait:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.function.Function;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.knowledge.KnowledgeIndex;
import software.amazon.smithy.model.knowledge.OperationIndex;
Expand Down Expand Up @@ -126,9 +128,12 @@ public boolean serviceHasTagApis(ToShapeId serviceShapeId) {
}

/**
* Gets the ShapeID of the TagResource operation on the shape if one is found by name.
* If a resource ID is passed in, it will return the qualifiying service wide TagResource operation ID rather than
* the operation ID specified by the tagApi property.
* Gets the ShapeID of the TagResource operation on the shape if one is found.
* For services, this returns the operation referenced by the service-level
* {@link TagEnabledTrait} apiConfig if set, otherwise the default-named
* {@code TagResource} operation if bound to the service. For resources, this
* returns the resource-level {@link TaggableTrait} apiConfig if set, otherwise
* the service-wide resolved operation.
*
* @param serviceOrResourceId ShapeID of the service shape to retrieve the qualifying TagResource operation for.
* @return The ShapeID of a qualifying TagResource operation if one is found. Returns an empty optional otherwise.
Expand All @@ -138,9 +143,12 @@ public Optional<ShapeId> getTagResourceOperation(ToShapeId serviceOrResourceId)
}

/**
* Gets the ShapeID of the UntagResource operation on the shape if one is found meeting the criteria.
* If a resource ID is passed in, it will return the qualifiying service wide TagResource operation ID rather than
* the operation ID specified by the tagApi property.
* Gets the ShapeID of the UntagResource operation on the shape if one is found.
* For services, this returns the operation referenced by the service-level
* {@link TagEnabledTrait} apiConfig if set, otherwise the default-named
* {@code UntagResource} operation if bound to the service. For resources, this
* returns the resource-level {@link TaggableTrait} apiConfig if set, otherwise
* the service-wide resolved operation.
*
* @param serviceOrResourceId ShapeID of the service shape to retrieve the qualifying UntagResource operation for.
* @return The ShapeID of a qualifying UntagResource operation if one is found. Returns an empty optional
Expand All @@ -151,9 +159,12 @@ public Optional<ShapeId> getUntagResourceOperation(ToShapeId serviceOrResourceId
}

/**
* Gets the ShapeID of the ListTagsForResource operation on the service shape if one is found meeting the criteria.
* If a resource ID is passed in, it will return the qualifiying service wide TagResource operation ID rather than
* the operation ID specified by the tagApi property.
* Gets the ShapeID of the ListTagsForResource operation on the shape if one is found.
* For services, this returns the operation referenced by the service-level
* {@link TagEnabledTrait} apiConfig if set, otherwise the default-named
* {@code ListTagsForResource} operation if bound to the service. For resources,
* this returns the resource-level {@link TaggableTrait} apiConfig if set,
* otherwise the service-wide resolved operation.
*
* @param serviceOrResourceId ShapeID of the service shape to retrieve the qualifying ListTagsForResource
* operation for.
Expand Down Expand Up @@ -205,84 +216,80 @@ private void computeTaggingApis(Model model, ServiceShape service) {
for (ShapeId operationId : service.getOperations()) {
operationMap.put(operationId.getName(), operationId);
}

calculateTagApi(model, service, operationMap);
calculateUntagApi(model, service, operationMap);
calculateListTagsApi(model, service, operationMap);
}

private void calculateTagApi(
Model model,
ServiceShape service,
Map<String, ShapeId> operationMap
) {
TopDownIndex topDownIndex = TopDownIndex.of(model);
OperationIndex operationIndex = OperationIndex.of(model);

if (operationMap.containsKey(TaggingShapeUtils.TAG_RESOURCE_OPNAME)) {
ShapeId tagOperationId = operationMap.get(TaggingShapeUtils.TAG_RESOURCE_OPNAME);
shapeToTagOperation.put(service.getId(), tagOperationId);
OperationShape tagOperation = model.expectShape(tagOperationId, OperationShape.class);
if (TaggingShapeUtils.verifyTagResourceOperation(model, tagOperation, operationIndex)) {
serviceTagOperationIsValid.add(service.getId());
}
}
for (ResourceShape resourceShape : topDownIndex.getContainedResources(service)) {
shapeToTagOperation.put(resourceShape.getId(),
resourceShape.getTrait(TaggableTrait.class)
.flatMap(TaggableTrait::getApiConfig)
.map(TaggableApiConfig::getTagApi)
.orElse(shapeToTagOperation.get(service.getId())));
}
}

private void calculateUntagApi(
Model model,
ServiceShape service,
Map<String, ShapeId> operationMap
) {
TopDownIndex topDownIndex = TopDownIndex.of(model);
OperationIndex operationIndex = OperationIndex.of(model);

if (operationMap.containsKey(TaggingShapeUtils.UNTAG_RESOURCE_OPNAME)) {
ShapeId untagOperationId = operationMap.get(TaggingShapeUtils.UNTAG_RESOURCE_OPNAME);
shapeToUntagOperation.put(service.getId(), untagOperationId);
OperationShape untagOperation = model.expectShape(untagOperationId, OperationShape.class);
if (TaggingShapeUtils.verifyUntagResourceOperation(model, untagOperation, operationIndex)) {
serviceUntagOperationIsValid.add(service.getId());
}
}
for (ResourceShape resourceShape : topDownIndex.getContainedResources(service)) {
shapeToUntagOperation.put(resourceShape.getId(),
resourceShape.getTrait(TaggableTrait.class)
.flatMap(TaggableTrait::getApiConfig)
.map(TaggableApiConfig::getUntagApi)
.orElse(shapeToUntagOperation.get(service.getId())));
}
calculateServiceTagOp(model,
service,
operationMap,
operationIndex,
topDownIndex,
TaggingShapeUtils.TAG_RESOURCE_OPNAME,
TaggableServiceApiConfig::getTagApi,
TaggableApiConfig::getTagApi,
(m, op) -> TaggingShapeUtils.verifyTagResourceOperation(m, op, operationIndex),
shapeToTagOperation,
serviceTagOperationIsValid);
calculateServiceTagOp(model,
service,
operationMap,
operationIndex,
topDownIndex,
TaggingShapeUtils.UNTAG_RESOURCE_OPNAME,
TaggableServiceApiConfig::getUntagApi,
TaggableApiConfig::getUntagApi,
(m, op) -> TaggingShapeUtils.verifyUntagResourceOperation(m, op, operationIndex),
shapeToUntagOperation,
serviceUntagOperationIsValid);
calculateServiceTagOp(model,
service,
operationMap,
operationIndex,
topDownIndex,
TaggingShapeUtils.LIST_TAGS_OPNAME,
TaggableServiceApiConfig::getListTagsApi,
TaggableApiConfig::getListTagsApi,
(m, op) -> TaggingShapeUtils.verifyListTagsOperation(m, op, operationIndex),
shapeToListTagsOperation,
serviceListTagsOperationIsValid);
}

private void calculateListTagsApi(
private void calculateServiceTagOp(
Model model,
ServiceShape service,
Map<String, ShapeId> operationMap
Map<String, ShapeId> operationMap,
OperationIndex operationIndex,
TopDownIndex topDownIndex,
String defaultOpName,
Function<TaggableServiceApiConfig, Optional<ShapeId>> serviceCfg,
Function<TaggableApiConfig, ShapeId> resourceCfg,
BiPredicate<Model, OperationShape> verifier,
Map<ShapeId, ShapeId> shapeToOpMap,
Set<ShapeId> validitySet
) {
TopDownIndex topDownIndex = TopDownIndex.of(model);
OperationIndex operationIndex = OperationIndex.of(model);
// Service-level apiConfig wins; otherwise fall back to the default-named operation if bound.
Optional<ShapeId> resolved = service.getTrait(TagEnabledTrait.class)
.flatMap(TagEnabledTrait::getApiConfig)
.flatMap(serviceCfg)
.filter(opId -> service.getOperations().contains(opId))
.map(Optional::of)
.orElseGet(() -> Optional.ofNullable(operationMap.get(defaultOpName)));

if (operationMap.containsKey(TaggingShapeUtils.LIST_TAGS_OPNAME)) {
ShapeId listTagsOperationId = operationMap.get(TaggingShapeUtils.LIST_TAGS_OPNAME);
shapeToListTagsOperation.put(service.getId(), listTagsOperationId);
OperationShape listTagsOperation = model.expectShape(listTagsOperationId, OperationShape.class);
if (TaggingShapeUtils.verifyListTagsOperation(model, listTagsOperation, operationIndex)) {
serviceListTagsOperationIsValid.add(service.getId());
resolved.ifPresent(opId -> {
shapeToOpMap.put(service.getId(), opId);
OperationShape op = model.expectShape(opId, OperationShape.class);
if (verifier.test(model, op)) {
validitySet.add(service.getId());
}
}
});

// Resource-level apiConfig still wins; otherwise fall through to the (possibly renamed) service-level op.
for (ResourceShape resourceShape : topDownIndex.getContainedResources(service)) {
shapeToListTagsOperation.put(resourceShape.getId(),
shapeToOpMap.put(resourceShape.getId(),
resourceShape.getTrait(TaggableTrait.class)
.flatMap(TaggableTrait::getApiConfig)
.map(TaggableApiConfig::getListTagsApi)
.orElse(shapeToListTagsOperation.get(service.getId())));
.map(resourceCfg)
.orElse(shapeToOpMap.get(service.getId())));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.ServiceShape;
Expand Down Expand Up @@ -42,7 +43,10 @@ private List<ValidationEvent> validateService(ServiceShape service, AwsTagIndex
events.add(getInvalidOperationEvent(service, trait, tagResourceId.get(), TAG_RESOURCE_OPNAME));
}
} else {
events.add(getMissingOperationEvent(service, trait, TAG_RESOURCE_OPNAME));
events.add(getMissingOperationEvent(service,
trait,
TAG_RESOURCE_OPNAME,
TaggableServiceApiConfig::getTagApi));
}

Optional<ShapeId> untagResourceId = awsTagIndex.getUntagResourceOperation(service.getId());
Expand All @@ -51,7 +55,10 @@ private List<ValidationEvent> validateService(ServiceShape service, AwsTagIndex
events.add(getInvalidOperationEvent(service, trait, untagResourceId.get(), UNTAG_RESOURCE_OPNAME));
}
} else {
events.add(getMissingOperationEvent(service, trait, UNTAG_RESOURCE_OPNAME));
events.add(getMissingOperationEvent(service,
trait,
UNTAG_RESOURCE_OPNAME,
TaggableServiceApiConfig::getUntagApi));
}

Optional<ShapeId> listTagsId = awsTagIndex.getListTagsForResourceOperation(service.getId());
Expand All @@ -60,17 +67,32 @@ private List<ValidationEvent> validateService(ServiceShape service, AwsTagIndex
events.add(getInvalidOperationEvent(service, trait, listTagsId.get(), LIST_TAGS_OPNAME));
}
} else {
events.add(getMissingOperationEvent(service, trait, LIST_TAGS_OPNAME));
events.add(getMissingOperationEvent(service,
trait,
LIST_TAGS_OPNAME,
TaggableServiceApiConfig::getListTagsApi));
}

return events;
}

private ValidationEvent getMissingOperationEvent(ServiceShape service, FromSourceLocation location, String opName) {
private ValidationEvent getMissingOperationEvent(
ServiceShape service,
TagEnabledTrait trait,
String defaultOpName,
Function<TaggableServiceApiConfig, Optional<ShapeId>> accessor
) {
Optional<ShapeId> configured = trait.getApiConfig().flatMap(accessor);
if (configured.isPresent()) {
return warning(service,
trait,
"Service marked `aws.api#TagEnabled` is missing an operation named '"
+ configured.get().getName() + "' (specified by `apiConfig`).");
}
return warning(service,
location,
trait,
"Service marked `aws.api#TagEnabled` is missing an operation named "
+ "'" + opName + ".'");
+ "'" + defaultOpName + ".'");
}

private ValidationEvent getInvalidOperationEvent(
Expand Down
Loading
Loading