diff --git a/MountAws.UnitTests/DynamoDbRoutingTests.cs b/MountAws.UnitTests/DynamoDbRoutingTests.cs new file mode 100644 index 0000000..6183815 --- /dev/null +++ b/MountAws.UnitTests/DynamoDbRoutingTests.cs @@ -0,0 +1,31 @@ +using System; +using AwesomeAssertions; +using MountAnything; +using MountAnything.Routing; +using MountAws.Services.DynamoDb; +using Xunit; + +namespace MountAws.UnitTests; + +public class DynamoDbRoutingTests +{ + private readonly Router _router; + + public DynamoDbRoutingTests() + { + var provider = new MountAwsProvider(); + _router = provider.CreateRouter(); + } + + [Theory] + [InlineData("myprofile/us-east-1/dynamodb", typeof(DynamoDbRootHandler))] + [InlineData("myprofile/us-east-1/dynamodb/tables", typeof(TablesHandler))] + [InlineData("myprofile/us-east-1/dynamodb/tables/my-table", typeof(TableHandler))] + [InlineData("myprofile/us-east-1/dynamodb/tables/my-table/autoscaling", typeof(TableAutoscalingHandler))] + [InlineData("myprofile/us-east-1/dynamodb/tables/my-table/items", typeof(TableItemsHandler))] + public void DynamoDbRoutesResolveToCorrectHandlers(string path, Type expectedHandlerType) + { + var resolver = _router.GetResolver(new ItemPath(path)); + resolver.HandlerType.Should().Be(expectedHandlerType); + } +} diff --git a/MountAws/MountAws.csproj b/MountAws/MountAws.csproj index 9680e2b..95a6d1b 100644 --- a/MountAws/MountAws.csproj +++ b/MountAws/MountAws.csproj @@ -26,6 +26,7 @@ + diff --git a/MountAws/Services/AppAutoscaling/ApiExtensions.cs b/MountAws/Services/AppAutoscaling/ApiExtensions.cs new file mode 100644 index 0000000..fa922cd --- /dev/null +++ b/MountAws/Services/AppAutoscaling/ApiExtensions.cs @@ -0,0 +1,67 @@ +using Amazon.ApplicationAutoScaling; +using Amazon.ApplicationAutoScaling.Model; +using static MountAws.PagingHelper; + +namespace MountAws.Services.AppAutoscaling; + +public static class ApiExtensions +{ + public static IEnumerable DescribeScalableTargets(this IAmazonApplicationAutoScaling autoScaling, ServiceNamespace serviceNamespace) + { + return Paginate(nextToken => + { + var response = autoScaling.DescribeScalableTargetsAsync(new DescribeScalableTargetsRequest + { + ServiceNamespace = serviceNamespace, + NextToken = nextToken + }).GetAwaiter().GetResult(); + + return (response.ScalableTargets, response.NextToken); + }); + } + + public static IEnumerable DescribeScalingPolicies(this IAmazonApplicationAutoScaling autoScaling, ServiceNamespace serviceNamespace, string resourceId) + { + return Paginate(nextToken => + { + var response = autoScaling.DescribeScalingPoliciesAsync(new DescribeScalingPoliciesRequest + { + ServiceNamespace = serviceNamespace, + ResourceId = resourceId, + NextToken = nextToken + }).GetAwaiter().GetResult(); + + return (response.ScalingPolicies, response.NextToken); + }); + } + + public static IEnumerable DescribeScalingActivities(this IAmazonApplicationAutoScaling autoScaling, ServiceNamespace serviceNamespace, string resourceId) + { + return Paginate(nextToken => + { + var response = autoScaling.DescribeScalingActivitiesAsync(new DescribeScalingActivitiesRequest + { + ServiceNamespace = serviceNamespace, + ResourceId = resourceId, + NextToken = nextToken + }).GetAwaiter().GetResult(); + + return (response.ScalingActivities, response.NextToken); + }); + } + + public static IEnumerable DescribeScheduledActions(this IAmazonApplicationAutoScaling autoScaling, ServiceNamespace serviceNamespace, string resourceId) + { + return Paginate(nextToken => + { + var response = autoScaling.DescribeScheduledActionsAsync(new DescribeScheduledActionsRequest + { + ServiceNamespace = serviceNamespace, + ResourceId = resourceId, + NextToken = nextToken + }).GetAwaiter().GetResult(); + + return (response.ScheduledActions, response.NextToken); + }); + } +} diff --git a/MountAws/Services/AppAutoscaling/AutoscalingHandler.cs b/MountAws/Services/AppAutoscaling/AutoscalingHandler.cs new file mode 100644 index 0000000..a078bc9 --- /dev/null +++ b/MountAws/Services/AppAutoscaling/AutoscalingHandler.cs @@ -0,0 +1,25 @@ +using MountAnything; +using MountAws.Services.Core; + +namespace MountAws.Services.AppAutoscaling; + +public class AutoscalingHandler(ItemPath path, IPathHandlerContext context) : PathHandler(path, context) +{ + public static Item CreateItem(ItemPath parentPath) + { + return new GenericContainerItem(parentPath, "autoscaling", + "Navigate application autoscaling dimensions"); + } + + protected override IItem? GetItemImpl() + { + return CreateItem(ParentPath); + } + + protected override IEnumerable GetChildItemsImpl() + { + yield return ScalingPoliciesHandler.CreateItem(Path); + yield return ScalingActivitiesHandler.CreateItem(Path); + yield return ScheduledActionsHandler.CreateItem(Path); + } +} \ No newline at end of file diff --git a/MountAws/Services/AppAutoscaling/CurrentServiceNamespace.cs b/MountAws/Services/AppAutoscaling/CurrentServiceNamespace.cs new file mode 100644 index 0000000..fc13aec --- /dev/null +++ b/MountAws/Services/AppAutoscaling/CurrentServiceNamespace.cs @@ -0,0 +1,8 @@ +using MountAnything; + +namespace MountAws.Services.AppAutoscaling; + +public class CurrentServiceNamespace : TypedString +{ + public CurrentServiceNamespace(string value) : base(value) { } +} diff --git a/MountAws/Services/AppAutoscaling/Formats.ps1xml b/MountAws/Services/AppAutoscaling/Formats.ps1xml new file mode 100644 index 0000000..80633e1 --- /dev/null +++ b/MountAws/Services/AppAutoscaling/Formats.ps1xml @@ -0,0 +1,132 @@ + + + + ScalableTarget + + MountAws.Services.AppAutoscaling.ScalableTargetItem + + + + + + + + + + + + + ItemName + + + ScalableDimension + + + MinCapacity + + + MaxCapacity + + + + + + + + ScalingPolicy + + MountAws.Services.AppAutoscaling.ScalingPolicyItem + + + + + + + + + + + + + ItemName + + + PolicyType + + + ScalableDimension + + + Summary + + + + + + + + ScalingActivity + + MountAws.Services.AppAutoscaling.ScalingActivityItem + + + + + + + + + + + + + ItemName + + + StatusCode + + + Description + + + StartTime + + + + + + + + ScheduledAction + + MountAws.Services.AppAutoscaling.ScheduledActionItem + + + + + + + + + + + + + ItemName + + + Schedule + + + ScalableDimension + + + CreationTime + + + + + + + + diff --git a/MountAws/Services/AppAutoscaling/IResourceIdResolver.cs b/MountAws/Services/AppAutoscaling/IResourceIdResolver.cs new file mode 100644 index 0000000..8bbde56 --- /dev/null +++ b/MountAws/Services/AppAutoscaling/IResourceIdResolver.cs @@ -0,0 +1,6 @@ +namespace MountAws.Services.AppAutoscaling; + +public interface IResourceIdResolver +{ + string ResourceId { get; } +} \ No newline at end of file diff --git a/MountAws/Services/AppAutoscaling/ItemResourceIdResolver.cs b/MountAws/Services/AppAutoscaling/ItemResourceIdResolver.cs new file mode 100644 index 0000000..c3de7af --- /dev/null +++ b/MountAws/Services/AppAutoscaling/ItemResourceIdResolver.cs @@ -0,0 +1,9 @@ +using MountAnything; + +namespace MountAws.Services.AppAutoscaling; + +public class ItemResourceIdResolver(IItemAncestor itemAncestor, Func resourceIdAccessor) + : IResourceIdResolver where TItem : IItem +{ + public string ResourceId => resourceIdAccessor(itemAncestor.Item); +} \ No newline at end of file diff --git a/MountAws/Services/AppAutoscaling/Registrar.cs b/MountAws/Services/AppAutoscaling/Registrar.cs new file mode 100644 index 0000000..31492f5 --- /dev/null +++ b/MountAws/Services/AppAutoscaling/Registrar.cs @@ -0,0 +1,16 @@ +using Amazon; +using Amazon.ApplicationAutoScaling; +using Amazon.Runtime; +using Autofac; + +namespace MountAws.Services.AppAutoscaling; + +public class Registrar : IServiceRegistrar +{ + public void Register(ContainerBuilder builder) + { + builder.RegisterType() + .As() + .UsingConstructor(typeof(AWSCredentials), typeof(RegionEndpoint)); + } +} diff --git a/MountAws/Services/AppAutoscaling/RouteExtensions.cs b/MountAws/Services/AppAutoscaling/RouteExtensions.cs new file mode 100644 index 0000000..e2e7eb2 --- /dev/null +++ b/MountAws/Services/AppAutoscaling/RouteExtensions.cs @@ -0,0 +1,37 @@ +using Autofac; +using MountAnything; +using MountAnything.Routing; + +namespace MountAws.Services.AppAutoscaling; + +public static class RouteExtensions +{ + public static void MapAppAutoscaling(this Route route, string serviceNamespace, Func resourceIdSelector) where TItem : IItem + { + route.ConfigureContainer(c => + { + c.RegisterInstance(new CurrentServiceNamespace(serviceNamespace)); + c.Register(s => + { + var item = s.Resolve>(); + + return new ItemResourceIdResolver(item, resourceIdSelector); + }).As(); + }); + route.MapLiteral("autoscaling", autoscaling => + { + autoscaling.MapLiteral("scaling-policies", scalablePolicies => + { + scalablePolicies.Map(); + }); + autoscaling.MapLiteral("scaling-activities", scalingActivities => + { + scalingActivities.Map(); + }); + autoscaling.MapLiteral("scheduled-actions", scheduledActions => + { + scheduledActions.Map(); + }); + }); + } +} \ No newline at end of file diff --git a/MountAws/Services/AppAutoscaling/ScalableTargetItem.cs b/MountAws/Services/AppAutoscaling/ScalableTargetItem.cs new file mode 100644 index 0000000..1af99bf --- /dev/null +++ b/MountAws/Services/AppAutoscaling/ScalableTargetItem.cs @@ -0,0 +1,23 @@ +using Amazon.ApplicationAutoScaling.Model; +using MountAnything; + +namespace MountAws.Services.AppAutoscaling; + +public class ScalableTargetItem(ItemPath parentPath, ScalableTarget underlyingObject) + : AwsItem(parentPath, underlyingObject) +{ + public override string ItemName => UnderlyingObject.ResourceId.Replace("/", ":"); + public override bool IsContainer => true; + + [ItemProperty] + public string ScalableDimension => UnderlyingObject.ScalableDimension.Value; + + [ItemProperty] + public int MinCapacity => UnderlyingObject.MinCapacity; + + [ItemProperty] + public int MaxCapacity => UnderlyingObject.MaxCapacity; + + [ItemProperty] + public string ServiceNamespace => UnderlyingObject.ServiceNamespace.Value; +} diff --git a/MountAws/Services/AppAutoscaling/ScalableTargetsHandler.cs b/MountAws/Services/AppAutoscaling/ScalableTargetsHandler.cs new file mode 100644 index 0000000..d0cfddf --- /dev/null +++ b/MountAws/Services/AppAutoscaling/ScalableTargetsHandler.cs @@ -0,0 +1,26 @@ +using Amazon.ApplicationAutoScaling; +using MountAnything; +using MountAws.Services.Core; + +namespace MountAws.Services.AppAutoscaling; + +public class ScalableTargetsHandler(ItemPath path, IPathHandlerContext context, CurrentServiceNamespace currentServiceNamespace, IAmazonApplicationAutoScaling autoScaling) + : PathHandler(path, context) +{ + public static Item CreateItem(ItemPath parentPath) + { + return new GenericContainerItem(parentPath, "scalable-targets", + "Navigate the scalable targets of the current service namespace."); + } + + protected override IItem? GetItemImpl() + { + return CreateItem(ParentPath); + } + + protected override IEnumerable GetChildItemsImpl() + { + return autoScaling.DescribeScalableTargets(currentServiceNamespace.Value) + .Select(target => new ScalableTargetItem(Path, target)); + } +} diff --git a/MountAws/Services/AppAutoscaling/ScalingActivitiesHandler.cs b/MountAws/Services/AppAutoscaling/ScalingActivitiesHandler.cs new file mode 100644 index 0000000..6bf9391 --- /dev/null +++ b/MountAws/Services/AppAutoscaling/ScalingActivitiesHandler.cs @@ -0,0 +1,32 @@ +using Amazon.ApplicationAutoScaling; +using MountAnything; +using MountAws.Services.Core; + +namespace MountAws.Services.AppAutoscaling; + +public class ScalingActivitiesHandler( + ItemPath path, + IPathHandlerContext context, + CurrentServiceNamespace currentServiceNamespace, + IResourceIdResolver resourceIdResolver, + IAmazonApplicationAutoScaling autoScaling) + : PathHandler(path, context) +{ + public static Item CreateItem(ItemPath parentPath) + { + return new GenericContainerItem(parentPath, "scaling-activities", + "Navigate the scaling activities for this scalable target"); + } + + protected override IItem? GetItemImpl() + { + return CreateItem(ParentPath); + } + + protected override IEnumerable GetChildItemsImpl() + { + var serviceNamespace = new ServiceNamespace(currentServiceNamespace.Value); + return autoScaling.DescribeScalingActivities(serviceNamespace, resourceIdResolver.ResourceId) + .Select(a => new ScalingActivityItem(Path, a)); + } +} diff --git a/MountAws/Services/AppAutoscaling/ScalingActivityHandler.cs b/MountAws/Services/AppAutoscaling/ScalingActivityHandler.cs new file mode 100644 index 0000000..bd1e9c5 --- /dev/null +++ b/MountAws/Services/AppAutoscaling/ScalingActivityHandler.cs @@ -0,0 +1,25 @@ +using Amazon.ApplicationAutoScaling; +using MountAnything; + +namespace MountAws.Services.AppAutoscaling; + +public class ScalingActivityHandler( + ItemPath path, + IPathHandlerContext context, + CurrentServiceNamespace currentServiceNamespace, + IResourceIdResolver resourceIdResolver, + IAmazonApplicationAutoScaling autoScaling) : PathHandler(path, context) +{ + protected override IItem? GetItemImpl() + { + var scalingActivity = autoScaling.DescribeScalingActivities(currentServiceNamespace.Value, resourceIdResolver.ResourceId) + .FirstOrDefault(a => a.ActivityId == ItemName); + + return scalingActivity == null ? null : new ScalingActivityItem(ParentPath, scalingActivity); + } + + protected override IEnumerable GetChildItemsImpl() + { + yield break; + } +} \ No newline at end of file diff --git a/MountAws/Services/AppAutoscaling/ScalingActivityItem.cs b/MountAws/Services/AppAutoscaling/ScalingActivityItem.cs new file mode 100644 index 0000000..fea538a --- /dev/null +++ b/MountAws/Services/AppAutoscaling/ScalingActivityItem.cs @@ -0,0 +1,32 @@ +using Amazon.ApplicationAutoScaling.Model; +using MountAnything; + +namespace MountAws.Services.AppAutoscaling; + +public class ScalingActivityItem(ItemPath parentPath, ScalingActivity underlyingObject) + : AwsItem(parentPath, underlyingObject) +{ + public override string ItemName => UnderlyingObject.ActivityId; + public override bool IsContainer => false; + + [ItemProperty] + public string StatusCode => UnderlyingObject.StatusCode.Value; + + [ItemProperty] + public string Description => UnderlyingObject.Description; + + [ItemProperty] + public string Cause => UnderlyingObject.Cause; + + [ItemProperty] + public DateTime StartTime => UnderlyingObject.StartTime; + + [ItemProperty] + public DateTime? EndTime => UnderlyingObject.EndTime; + + [ItemProperty] + public string ResourceId => UnderlyingObject.ResourceId; + + [ItemProperty] + public string ScalableDimension => UnderlyingObject.ScalableDimension.Value; +} diff --git a/MountAws/Services/AppAutoscaling/ScalingPoliciesHandler.cs b/MountAws/Services/AppAutoscaling/ScalingPoliciesHandler.cs new file mode 100644 index 0000000..a85a876 --- /dev/null +++ b/MountAws/Services/AppAutoscaling/ScalingPoliciesHandler.cs @@ -0,0 +1,32 @@ +using Amazon.ApplicationAutoScaling; +using MountAnything; +using MountAws.Services.Core; + +namespace MountAws.Services.AppAutoscaling; + +public class ScalingPoliciesHandler( + ItemPath path, + IPathHandlerContext context, + CurrentServiceNamespace currentServiceNamespace, + IResourceIdResolver resourceIdResolver, + IAmazonApplicationAutoScaling autoScaling) + : PathHandler(path, context) +{ + public static Item CreateItem(ItemPath parentPath) + { + return new GenericContainerItem(parentPath, "scaling-policies", + "Navigate the scaling policies for this scalable target"); + } + + protected override IItem? GetItemImpl() + { + return CreateItem(ParentPath); + } + + protected override IEnumerable GetChildItemsImpl() + { + var serviceNamespace = new ServiceNamespace(currentServiceNamespace.Value); + return autoScaling.DescribeScalingPolicies(serviceNamespace, resourceIdResolver.ResourceId) + .Select(p => new ScalingPolicyItem(Path, p)); + } +} diff --git a/MountAws/Services/AppAutoscaling/ScalingPolicyHandler.cs b/MountAws/Services/AppAutoscaling/ScalingPolicyHandler.cs new file mode 100644 index 0000000..19ac20d --- /dev/null +++ b/MountAws/Services/AppAutoscaling/ScalingPolicyHandler.cs @@ -0,0 +1,27 @@ +using Amazon.ApplicationAutoScaling; +using MountAnything; + +namespace MountAws.Services.AppAutoscaling; + +public class ScalingPolicyHandler( + ItemPath path, + IPathHandlerContext context, + CurrentServiceNamespace currentServiceNamespace, + IResourceIdResolver resourceIdResolver, + IAmazonApplicationAutoScaling autoScaling) + : PathHandler(path, context) +{ + protected override IItem? GetItemImpl() + { + var serviceNamespace = new ServiceNamespace(currentServiceNamespace.Value); + var policy = autoScaling.DescribeScalingPolicies(serviceNamespace, resourceIdResolver.ResourceId) + .FirstOrDefault(p => p.PolicyName == ItemName); + + return policy != null ? new ScalingPolicyItem(ParentPath, policy) : null; + } + + protected override IEnumerable GetChildItemsImpl() + { + return Enumerable.Empty(); + } +} diff --git a/MountAws/Services/AppAutoscaling/ScalingPolicyItem.cs b/MountAws/Services/AppAutoscaling/ScalingPolicyItem.cs new file mode 100644 index 0000000..599d704 --- /dev/null +++ b/MountAws/Services/AppAutoscaling/ScalingPolicyItem.cs @@ -0,0 +1,49 @@ +using System.Globalization; +using Amazon.ApplicationAutoScaling.Model; +using MountAnything; + +namespace MountAws.Services.AppAutoscaling; + +public class ScalingPolicyItem(ItemPath parentPath, ScalingPolicy underlyingObject) + : AwsItem(parentPath, underlyingObject) +{ + public override string ItemName => UnderlyingObject.PolicyName; + public override bool IsContainer => false; + + [ItemProperty] + public string ScalableDimension => UnderlyingObject.ScalableDimension.Value; + + [ItemProperty] + public string Summary => UnderlyingObject switch + { + { TargetTrackingScalingPolicyConfiguration: not null } => GetTargetTrackingSummary(UnderlyingObject + .TargetTrackingScalingPolicyConfiguration), + { PredictiveScalingPolicyConfiguration: not null } => GetPredictiveScalingSummary(UnderlyingObject + .PredictiveScalingPolicyConfiguration), + { StepScalingPolicyConfiguration: not null } => GetStepScalingSummary(UnderlyingObject + .StepScalingPolicyConfiguration), + _ => UnderlyingObject.PolicyType.Value + }; + + private string GetTargetTrackingSummary(TargetTrackingScalingPolicyConfiguration targetTracking) + { + return targetTracking switch + { + { PredefinedMetricSpecification: not null } => + $"{targetTracking.PredefinedMetricSpecification.PredefinedMetricType} {targetTracking.TargetValue}", + { CustomizedMetricSpecification: not null } => + $"{targetTracking.CustomizedMetricSpecification.MetricName} {targetTracking.TargetValue}", + _ => targetTracking.TargetValue.ToString(CultureInfo.CurrentCulture) + }; + } + + private string GetPredictiveScalingSummary(PredictiveScalingPolicyConfiguration predectiveScaling) + { + return predectiveScaling.Mode.Value; + } + + private string GetStepScalingSummary(StepScalingPolicyConfiguration stepScaling) + { + return $"{stepScaling.MetricAggregationType} {stepScaling.AdjustmentType}"; + } +} diff --git a/MountAws/Services/AppAutoscaling/ScheduledActionHandler.cs b/MountAws/Services/AppAutoscaling/ScheduledActionHandler.cs new file mode 100644 index 0000000..b4ab3cd --- /dev/null +++ b/MountAws/Services/AppAutoscaling/ScheduledActionHandler.cs @@ -0,0 +1,27 @@ +using Amazon.ApplicationAutoScaling; +using MountAnything; + +namespace MountAws.Services.AppAutoscaling; + +public class ScheduledActionHandler( + ItemPath path, + IPathHandlerContext context, + CurrentServiceNamespace currentServiceNamespace, + IResourceIdResolver resourceIdResolver, + IAmazonApplicationAutoScaling autoScaling) + : PathHandler(path, context) +{ + protected override IItem? GetItemImpl() + { + var serviceNamespace = new ServiceNamespace(currentServiceNamespace.Value); + var action = autoScaling.DescribeScheduledActions(serviceNamespace, resourceIdResolver.ResourceId) + .FirstOrDefault(a => a.ScheduledActionName == ItemName); + + return action != null ? new ScheduledActionItem(ParentPath, action) : null; + } + + protected override IEnumerable GetChildItemsImpl() + { + return Enumerable.Empty(); + } +} diff --git a/MountAws/Services/AppAutoscaling/ScheduledActionItem.cs b/MountAws/Services/AppAutoscaling/ScheduledActionItem.cs new file mode 100644 index 0000000..5780300 --- /dev/null +++ b/MountAws/Services/AppAutoscaling/ScheduledActionItem.cs @@ -0,0 +1,29 @@ +using Amazon.ApplicationAutoScaling.Model; +using MountAnything; + +namespace MountAws.Services.AppAutoscaling; + +public class ScheduledActionItem(ItemPath parentPath, ScheduledAction underlyingObject) + : AwsItem(parentPath, underlyingObject) +{ + public override string ItemName => UnderlyingObject.ScheduledActionName; + public override bool IsContainer => false; + + [ItemProperty] + public string Schedule => UnderlyingObject.Schedule; + + [ItemProperty] + public string ResourceId => UnderlyingObject.ResourceId; + + [ItemProperty] + public string ScalableDimension => UnderlyingObject.ScalableDimension.Value; + + [ItemProperty] + public DateTime? StartTime => UnderlyingObject.StartTime; + + [ItemProperty] + public DateTime? EndTime => UnderlyingObject.EndTime; + + [ItemProperty] + public DateTime CreationTime => UnderlyingObject.CreationTime; +} diff --git a/MountAws/Services/AppAutoscaling/ScheduledActionsHandler.cs b/MountAws/Services/AppAutoscaling/ScheduledActionsHandler.cs new file mode 100644 index 0000000..203e568 --- /dev/null +++ b/MountAws/Services/AppAutoscaling/ScheduledActionsHandler.cs @@ -0,0 +1,32 @@ +using Amazon.ApplicationAutoScaling; +using MountAnything; +using MountAws.Services.Core; + +namespace MountAws.Services.AppAutoscaling; + +public class ScheduledActionsHandler( + ItemPath path, + IPathHandlerContext context, + CurrentServiceNamespace currentServiceNamespace, + IResourceIdResolver resourceIdResolver, + IAmazonApplicationAutoScaling autoScaling) + : PathHandler(path, context) +{ + public static Item CreateItem(ItemPath parentPath) + { + return new GenericContainerItem(parentPath, "scheduled-actions", + "Navigate the scheduled actions for this scalable target"); + } + + protected override IItem? GetItemImpl() + { + return CreateItem(ParentPath); + } + + protected override IEnumerable GetChildItemsImpl() + { + var serviceNamespace = new ServiceNamespace(currentServiceNamespace.Value); + return autoScaling.DescribeScheduledActions(serviceNamespace, resourceIdResolver.ResourceId) + .Select(a => new ScheduledActionItem(Path, a)); + } +} diff --git a/MountAws/Services/AppAutoscaling/ServiceHandler.cs b/MountAws/Services/AppAutoscaling/ServiceHandler.cs new file mode 100644 index 0000000..904a4af --- /dev/null +++ b/MountAws/Services/AppAutoscaling/ServiceHandler.cs @@ -0,0 +1,23 @@ +using Amazon.ApplicationAutoScaling; +using MountAnything; + +namespace MountAws.Services.AppAutoscaling; + +public class ServiceHandler : PathHandler +{ + public ServiceHandler(ItemPath path, IPathHandlerContext context) : base(path, context) + { + } + + protected override IItem? GetItemImpl() + { + var serviceNamespace = new ServiceNamespace(Path.Name); + + return new ServiceItem(ParentPath, serviceNamespace); + } + + protected override IEnumerable GetChildItemsImpl() + { + yield return ScalableTargetsHandler.CreateItem(Path); + } +} \ No newline at end of file diff --git a/MountAws/Services/AppAutoscaling/ServiceItem.cs b/MountAws/Services/AppAutoscaling/ServiceItem.cs new file mode 100644 index 0000000..2c23e6f --- /dev/null +++ b/MountAws/Services/AppAutoscaling/ServiceItem.cs @@ -0,0 +1,15 @@ +using Amazon.ApplicationAutoScaling; +using MountAnything; + +namespace MountAws.Services.AppAutoscaling; + +public class ServiceItem : AwsItem +{ + public ServiceItem(ItemPath parentPath, ServiceNamespace underlyingObject) : base(parentPath, underlyingObject) + { + + } + + public override string ItemName => UnderlyingObject.Value; + public override bool IsContainer => true; +} \ No newline at end of file diff --git a/MountAws/Services/AppAutoscaling/ServicesHandler.cs b/MountAws/Services/AppAutoscaling/ServicesHandler.cs new file mode 100644 index 0000000..c37c085 --- /dev/null +++ b/MountAws/Services/AppAutoscaling/ServicesHandler.cs @@ -0,0 +1,47 @@ +using Amazon.ApplicationAutoScaling; +using MountAnything; +using MountAws.Services.Core; + +namespace MountAws.Services.AppAutoscaling; + +public class ServicesHandler : PathHandler +{ + internal static readonly ServiceNamespace[] KnownServiceNamespaces = + [ + ServiceNamespace.Appstream, + ServiceNamespace.Cassandra, + ServiceNamespace.Comprehend, + ServiceNamespace.CustomResource, + ServiceNamespace.Dynamodb, + ServiceNamespace.Ec2, + ServiceNamespace.Ecs, + ServiceNamespace.Elasticache, + ServiceNamespace.Elasticmapreduce, + ServiceNamespace.Kafka, + ServiceNamespace.Lambda, + ServiceNamespace.Neptune, + ServiceNamespace.Rds, + ServiceNamespace.Sagemaker, + ServiceNamespace.Workspaces + ]; + + public static Item CreateItem(ItemPath parentPath) + { + return new GenericContainerItem(parentPath, "services", + "Navigate application autoscaling service namespaces"); + } + + public ServicesHandler(ItemPath path, IPathHandlerContext context) : base(path, context) + { + } + + protected override IItem? GetItemImpl() + { + return CreateItem(ParentPath); + } + + protected override IEnumerable GetChildItemsImpl() + { + return KnownServiceNamespaces.Select(ns => new ServiceItem(Path, ns)); + } +} diff --git a/MountAws/Services/DynamoDb/CurrentTable.cs b/MountAws/Services/DynamoDb/CurrentTable.cs new file mode 100644 index 0000000..0dd4926 --- /dev/null +++ b/MountAws/Services/DynamoDb/CurrentTable.cs @@ -0,0 +1,8 @@ +using MountAnything; + +namespace MountAws.Services.DynamoDb; + +public class CurrentTable(string tableName) : TypedString(tableName) +{ + public string Name => Value; +} \ No newline at end of file diff --git a/MountAws/Services/DynamoDb/Routes.cs b/MountAws/Services/DynamoDb/Routes.cs index 20f959d..be396c0 100644 --- a/MountAws/Services/DynamoDb/Routes.cs +++ b/MountAws/Services/DynamoDb/Routes.cs @@ -1,4 +1,5 @@ using MountAnything.Routing; +using MountAws.Services.AppAutoscaling; namespace MountAws.Services.DynamoDb; @@ -10,9 +11,13 @@ public void AddServiceRoutes(Route regionRoute) { dynamodb.MapLiteral("tables", tables => { - tables.Map(table => + tables.Map(table => { - table.MapRegex(@"[a-z0-9-_\.\,]+"); + table.MapAppAutoscaling("dynamodb", item => $"table/{item.ItemName}"); + table.MapLiteral("items", items => + { + items.MapRegex(@"[a-z0-9-_\.\,]+"); + }); }); }); }); diff --git a/MountAws/Services/DynamoDb/TableAutoscalingHandler.cs b/MountAws/Services/DynamoDb/TableAutoscalingHandler.cs new file mode 100644 index 0000000..4a16abf --- /dev/null +++ b/MountAws/Services/DynamoDb/TableAutoscalingHandler.cs @@ -0,0 +1,30 @@ +using Amazon.ApplicationAutoScaling; +using Amazon.ApplicationAutoScaling.Model; +using MountAnything; +using MountAws.Services.AppAutoscaling; +using MountAws.Services.Core; + +namespace MountAws.Services.DynamoDb; + +public class TableAutoscalingHandler(ItemPath path, IPathHandlerContext context, IAmazonApplicationAutoScaling autoScaling) : PathHandler(path, context) +{ + public static IItem CreateItem(ItemPath parentPath) + { + return new GenericContainerItem(parentPath, "autoscaling", + "Navigate DynamoDB autoscaling dimensions, policies and activities for this table"); + } + + protected override IItem? GetItemImpl() + { + return CreateItem(ParentPath); + } + + protected override IEnumerable GetChildItemsImpl() + { + var tableName = ParentPath.Name; + return autoScaling.DescribeScalableTargets(ServiceNamespace.Dynamodb) + .Where(t => t.ResourceId == $"table/{tableName}") + .Select(t => new ScalableTargetItem(Path, t)) + .ToList(); + } +} diff --git a/MountAws/Services/DynamoDb/TableHandler.cs b/MountAws/Services/DynamoDb/TableHandler.cs index 21d4b55..2e7230c 100644 --- a/MountAws/Services/DynamoDb/TableHandler.cs +++ b/MountAws/Services/DynamoDb/TableHandler.cs @@ -5,7 +5,7 @@ namespace MountAws.Services.DynamoDb; -public class TableHandler : PathHandler, IGetChildItemParameters +public class TableHandler : PathHandler { private readonly IAmazonDynamoDB _dynamo; @@ -29,19 +29,7 @@ public TableHandler(ItemPath path, IPathHandlerContext context, IAmazonDynamoDB protected override IEnumerable GetChildItemsImpl() { - var table = _dynamo.DescribeTable(ItemName); - - return _dynamo.Scan(ItemName, GetChildItemParameters.Limit).Select(v => new DynamoItem(Path, table.KeySchema, v)); + yield return TableAutoscalingHandler.CreateItem(Path); + yield return TableItemsHandler.CreateItem(Path); } - - // since we don't return the full child item set, we don't want the list of children cached - protected override bool CacheChildren => false; - - public TableChildItemParameters GetChildItemParameters { get; set; } = null!; -} - -public class TableChildItemParameters -{ - [Parameter()] - public int? Limit { get; set; } } \ No newline at end of file diff --git a/MountAws/Services/DynamoDb/ItemHandler.cs b/MountAws/Services/DynamoDb/TableItemHandler.cs similarity index 50% rename from MountAws/Services/DynamoDb/ItemHandler.cs rename to MountAws/Services/DynamoDb/TableItemHandler.cs index ccd4c4e..c747da8 100644 --- a/MountAws/Services/DynamoDb/ItemHandler.cs +++ b/MountAws/Services/DynamoDb/TableItemHandler.cs @@ -3,20 +3,18 @@ namespace MountAws.Services.DynamoDb; -public class ItemHandler : PathHandler +public class TableItemHandler( + ItemPath path, + IPathHandlerContext context, + CurrentTable currentTable, + IAmazonDynamoDB dynamo) + : PathHandler(path, context) { - private readonly IAmazonDynamoDB _dynamo; - - public ItemHandler(ItemPath path, IPathHandlerContext context, IAmazonDynamoDB dynamo) : base(path, context) - { - _dynamo = dynamo; - } - protected override IItem? GetItemImpl() { - var table = _dynamo.DescribeTable(ParentPath.Name); + var table = dynamo.DescribeTable(currentTable.Name); var keys = ItemName.Split(","); - var item = _dynamo.GetItem(table, keys); + var item = dynamo.GetItem(table, keys); return new DynamoItem(ParentPath, table.KeySchema, item); } diff --git a/MountAws/Services/DynamoDb/TableItemsHandler.cs b/MountAws/Services/DynamoDb/TableItemsHandler.cs new file mode 100644 index 0000000..2e068b5 --- /dev/null +++ b/MountAws/Services/DynamoDb/TableItemsHandler.cs @@ -0,0 +1,41 @@ +using System.Management.Automation; +using Amazon.DynamoDBv2; +using MountAnything; +using MountAws.Services.Core; + +namespace MountAws.Services.DynamoDb; + +public class TableItemsHandler( + ItemPath path, + IPathHandlerContext context, + CurrentTable currentTable, + IAmazonDynamoDB dynamo) : PathHandler(path, context), IGetChildItemParameters +{ + public static IItem CreateItem(ItemPath parentPath) + { + return new GenericContainerItem(parentPath, "items", "Navigate the items in this table"); + } + + protected override IItem? GetItemImpl() + { + return CreateItem(ParentPath); + } + + protected override IEnumerable GetChildItemsImpl() + { + var table = dynamo.DescribeTable(currentTable.Name); + + return dynamo.Scan(currentTable.Name, GetChildItemParameters.Limit).Select(v => new DynamoItem(Path, table.KeySchema, v)); + } + + // since we don't return the full child item set, we don't want the list of children cached + protected override bool CacheChildren => false; + + public TableItemsChildItemParameters GetChildItemParameters { get; set; } = null!; +} + +public class TableItemsChildItemParameters +{ + [Parameter] + public int? Limit { get; set; } +} \ No newline at end of file diff --git a/MountAws/Services/Ecs/ClustersHandler.cs b/MountAws/Services/Ecs/ClustersHandler.cs index 664aeba..1dfaa67 100644 --- a/MountAws/Services/Ecs/ClustersHandler.cs +++ b/MountAws/Services/Ecs/ClustersHandler.cs @@ -7,20 +7,13 @@ namespace MountAws.Services.Ecs; -public class ClustersHandler : PathHandler +public class ClustersHandler(ItemPath path, IPathHandlerContext context, IAmazonECS ecs) : PathHandler(path, context) { - private readonly IAmazonECS _ecs; - public static Item CreateItem(ItemPath parentPath) { return new GenericContainerItem(parentPath, "clusters", "Navigate the ECS clusters in this account and region"); } - - public ClustersHandler(ItemPath path, IPathHandlerContext context, IAmazonECS ecs) : base(path, context) - { - _ecs = ecs; - } protected override IItem? GetItemImpl() { @@ -29,8 +22,8 @@ public ClustersHandler(ItemPath path, IPathHandlerContext context, IAmazonECS ec protected override IEnumerable GetChildItemsImpl() { - var clusterArns = _ecs.ListClusters(); - var clusters = _ecs.DescribeClusters(clusterArns, new []{"TAGS"}); + var clusterArns = ecs.ListClusters(); + var clusters = ecs.DescribeClusters(clusterArns, new []{"TAGS"}); return clusters.Select(c => new ClusterItem(Path, c)).OrderBy(c => c.ItemName); } diff --git a/MountAws/Services/Ecs/CurrentService.cs b/MountAws/Services/Ecs/CurrentService.cs new file mode 100644 index 0000000..e74604e --- /dev/null +++ b/MountAws/Services/Ecs/CurrentService.cs @@ -0,0 +1,8 @@ +using MountAnything; + +namespace MountAws.Services.Ecs; + +public class CurrentService(string serviceName) : TypedString(serviceName) +{ + public string Name => Value; +} \ No newline at end of file diff --git a/MountAws/Services/Ecs/EcsApiExtensions.cs b/MountAws/Services/Ecs/EcsApiExtensions.cs index ccbe6a7..30ed70a 100644 --- a/MountAws/Services/Ecs/EcsApiExtensions.cs +++ b/MountAws/Services/Ecs/EcsApiExtensions.cs @@ -35,11 +35,11 @@ public static IEnumerable ListClusters(this IAmazonECS ecs) public static IEnumerable DescribeClusters(this IAmazonECS ecs, IEnumerable clusters, IEnumerable? include = null) { - return ecs.DescribeClustersAsync(new DescribeClustersRequest + return clusters.Chunk(100).SelectMany(clustersPage => ecs.DescribeClustersAsync(new DescribeClustersRequest { - Clusters = clusters.ToList(), + Clusters = clustersPage.ToList(), Include = include?.ToList() - }).GetAwaiter().GetResult().Clusters; + }).GetAwaiter().GetResult().Clusters); } public static Cluster DescribeCluster(this IAmazonECS ecs, string cluster, IEnumerable? include = null) diff --git a/MountAws/Services/Ecs/EcsRoutes.cs b/MountAws/Services/Ecs/EcsRoutes.cs index b35881e..f180951 100644 --- a/MountAws/Services/Ecs/EcsRoutes.cs +++ b/MountAws/Services/Ecs/EcsRoutes.cs @@ -1,4 +1,5 @@ using MountAnything.Routing; +using MountAws.Services.AppAutoscaling; namespace MountAws.Services.Ecs; @@ -21,9 +22,13 @@ public void AddServiceRoutes(Route route) }); cluster.MapLiteral("services", services => { - services.Map(service => + services.Map(service => { - service.Map(); + service.MapAppAutoscaling("ecs", item => $"service/{item.UnderlyingObject.ClusterName()}/{item.ItemName}"); + service.MapLiteral("tasks", tasks => + { + tasks.Map(); + }); }); }); }); diff --git a/MountAws/Services/Ecs/ServiceHandler.cs b/MountAws/Services/Ecs/ServiceHandler.cs index b05717d..a049359 100644 --- a/MountAws/Services/Ecs/ServiceHandler.cs +++ b/MountAws/Services/Ecs/ServiceHandler.cs @@ -3,6 +3,7 @@ using Amazon.ECS; using MountAnything; using MountAws.Api.AwsSdk.Ecs; +using MountAws.Services.AppAutoscaling; using MountAws.Services.Ec2; namespace MountAws.Services.Ecs; @@ -36,37 +37,8 @@ public ServiceHandler(ItemPath path, IPathHandlerContext context, IAmazonECS ecs protected override IEnumerable GetChildItemsImpl() { - var taskArns = _ecs.ListTasksByService(_currentCluster.Name, ItemName); - - var tasks = taskArns.Chunk(10).SelectMany(taskArnChunk => - { - return _ecs.DescribeTasks(_currentCluster.Name, - taskArnChunk, - new[] { "TAGS" }); - }); - - var containerInstanceArns = tasks - .Where(t => !string.IsNullOrEmpty(t.ContainerInstanceArn)) - .Select(t => t.ContainerInstanceArn) - .Distinct() - .ToArray(); - Dictionary ec2InstancesByContainerInstanceArn = new Dictionary(); - if (containerInstanceArns.Any()) - { - var containerInstances = _ecs.DescribeContainerInstances(_currentCluster.Name, containerInstanceArns) - .Where(c => !string.IsNullOrEmpty(c.Ec2InstanceId)) - .ToDictionary(i => i.Ec2InstanceId); - - var ec2InstanceIds = containerInstances.Values.Select(c => c.Ec2InstanceId); - ec2InstancesByContainerInstanceArn = _ec2.GetInstancesByIds(ec2InstanceIds) - .Where(instance => containerInstances.ContainsKey(instance.InstanceId)) - .ToDictionary(i => containerInstances[i.InstanceId].ContainerInstanceArn); - } - - return tasks.Select(t => new TaskItem(Path, t, - t.ContainerInstanceArn != null - ? ec2InstancesByContainerInstanceArn.GetValueOrDefault(t.ContainerInstanceArn) - : null, LinkGenerator, useServiceView:true)); + yield return TasksHandler.CreateItem(Path); + yield return AutoscalingHandler.CreateItem(Path); } public void RemoveItem() diff --git a/MountAws/Services/Ecs/TasksHandler.cs b/MountAws/Services/Ecs/TasksHandler.cs new file mode 100644 index 0000000..6e19dc7 --- /dev/null +++ b/MountAws/Services/Ecs/TasksHandler.cs @@ -0,0 +1,64 @@ +using Amazon.EC2; +using Amazon.EC2.Model; +using Amazon.ECS; +using MountAnything; +using MountAws.Api.AwsSdk.Ecs; +using MountAws.Services.Core; +using MountAws.Services.Ec2; + +namespace MountAws.Services.Ecs; + +public class TasksHandler( + ItemPath path, + IPathHandlerContext context, + CurrentCluster currentCluster, + CurrentService currentService, + IAmazonECS ecs, + IAmazonEC2 ec2) : PathHandler(path, context) +{ + public static Item CreateItem(ItemPath parentPath) + { + return new GenericContainerItem(parentPath, "tasks", + "Navigate the ECS tasks in this service"); + } + + protected override IItem? GetItemImpl() + { + return CreateItem(ParentPath); + } + + protected override IEnumerable GetChildItemsImpl() + { + var taskArns = ecs.ListTasksByService(currentCluster.Name, currentService.Name); + + var tasks = taskArns.Chunk(10).SelectMany(taskArnChunk => + { + return ecs.DescribeTasks(currentCluster.Name, + taskArnChunk, + new[] { "TAGS" }); + }); + + var containerInstanceArns = tasks + .Where(t => !string.IsNullOrEmpty(t.ContainerInstanceArn)) + .Select(t => t.ContainerInstanceArn) + .Distinct() + .ToArray(); + Dictionary ec2InstancesByContainerInstanceArn = new Dictionary(); + if (containerInstanceArns.Any()) + { + var containerInstances = ecs.DescribeContainerInstances(currentCluster.Name, containerInstanceArns) + .Where(c => !string.IsNullOrEmpty(c.Ec2InstanceId)) + .ToDictionary(i => i.Ec2InstanceId); + + var ec2InstanceIds = containerInstances.Values.Select(c => c.Ec2InstanceId); + ec2InstancesByContainerInstanceArn = ec2.GetInstancesByIds(ec2InstanceIds) + .Where(instance => containerInstances.ContainsKey(instance.InstanceId)) + .ToDictionary(i => containerInstances[i.InstanceId].ContainerInstanceArn); + } + + return tasks.Select(t => new TaskItem(Path, t, + t.ContainerInstanceArn != null + ? ec2InstancesByContainerInstanceArn.GetValueOrDefault(t.ContainerInstanceArn) + : null, LinkGenerator, useServiceView:true)); + } +} \ No newline at end of file