From 327448e1920c98b70dfc443575350c458f41394b Mon Sep 17 00:00:00 2001
From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com>
Date: Tue, 24 Jun 2025 09:10:30 -0500
Subject: [PATCH 1/7] Adding structure.
---
.../Actions/ControlTowerActions.csproj | 17 ++++
.../Actions/ControlTowerWrapper.cs | 93 ++++++++++++++++++
.../ControlTower/Actions/HelloControlTower.cs | 64 ++++++++++++
dotnetv4/ControlTower/Actions/Usings.cs | 13 +++
.../ControlTower/ControlTowerExamples.sln | 47 +++++++++
dotnetv4/ControlTower/README.md | 98 +++++++++++++++++++
.../ControlTowerBasics.csproj | 26 +++++
.../Tests/ControlTowerTests.csproj | 35 +++++++
dotnetv4/ControlTower/Tests/Usings.cs | 7 ++
9 files changed, 400 insertions(+)
create mode 100644 dotnetv4/ControlTower/Actions/ControlTowerActions.csproj
create mode 100644 dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs
create mode 100644 dotnetv4/ControlTower/Actions/HelloControlTower.cs
create mode 100644 dotnetv4/ControlTower/Actions/Usings.cs
create mode 100644 dotnetv4/ControlTower/ControlTowerExamples.sln
create mode 100644 dotnetv4/ControlTower/README.md
create mode 100644 dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.csproj
create mode 100644 dotnetv4/ControlTower/Tests/ControlTowerTests.csproj
create mode 100644 dotnetv4/ControlTower/Tests/Usings.cs
diff --git a/dotnetv4/ControlTower/Actions/ControlTowerActions.csproj b/dotnetv4/ControlTower/Actions/ControlTowerActions.csproj
new file mode 100644
index 00000000000..b2276e2a524
--- /dev/null
+++ b/dotnetv4/ControlTower/Actions/ControlTowerActions.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs
new file mode 100644
index 00000000000..870c87d17a2
--- /dev/null
+++ b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs
@@ -0,0 +1,93 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+// snippet-start:[ControlTower.dotnetv4.ControlTowerWrapper]
+using System.Net;
+
+namespace ControlTowerActions;
+
+///
+/// Methods to perform AWS Control Tower actions.
+///
+public class ControlTowerWrapper
+{
+ private readonly IAmazonControlTower _controlTowerService;
+
+ ///
+ /// Constructor for the wrapper class containing AWS Control Tower actions.
+ ///
+ /// The AWS Control Tower client object.
+ public ControlTowerWrapper(IAmazonControlTower controlTowerService)
+ {
+ _controlTowerService = controlTowerService;
+ }
+
+ // snippet-start:[ControlTower.dotnetv4.ListLandingZones]
+ ///
+ /// List the AWS Control Tower landing zones for an account.
+ ///
+ /// A list of LandingZoneSummary objects.
+ public async Task> ListLandingZonesAsync()
+ {
+ var landingZones = new List();
+
+ var landingZonesPaginator = _controlTowerService.Paginators.ListLandingZones(new ListLandingZonesRequest());
+
+ await foreach (var response in landingZonesPaginator.Responses)
+ {
+ landingZones.AddRange(response.LandingZones);
+ }
+
+ return landingZones;
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.ListLandingZones]
+
+ // snippet-start:[ControlTower.dotnetv4.GetLandingZone]
+ ///
+ /// Get details about a specific landing zone.
+ ///
+ /// The landing zone identifier.
+ /// The landing zone details.
+ public async Task GetLandingZoneAsync(string landingZoneIdentifier)
+ {
+ var request = new GetLandingZoneRequest
+ {
+ LandingZoneIdentifier = landingZoneIdentifier
+ };
+
+ var response = await _controlTowerService.GetLandingZoneAsync(request);
+ return response.LandingZone;
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.GetLandingZone]
+
+ // snippet-start:[ControlTower.dotnetv4.ListEnabledControls]
+ ///
+ /// List enabled controls for a target organizational unit.
+ ///
+ /// The target organizational unit identifier.
+ /// A list of enabled control summaries.
+ public async Task> ListEnabledControlsAsync(string targetIdentifier)
+ {
+ var request = new ListEnabledControlsRequest
+ {
+ TargetIdentifier = targetIdentifier
+ };
+
+ var enabledControls = new List();
+
+ var enabledControlsPaginator = _controlTowerService.Paginators.ListEnabledControls(request);
+
+ await foreach (var response in enabledControlsPaginator.Responses)
+ {
+ enabledControls.AddRange(response.EnabledControls);
+ }
+
+ return enabledControls;
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.ListEnabledControls]
+}
+
+// snippet-end:[ControlTower.dotnetv4.ControlTowerWrapper]
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/Actions/HelloControlTower.cs b/dotnetv4/ControlTower/Actions/HelloControlTower.cs
new file mode 100644
index 00000000000..d77c6dee60b
--- /dev/null
+++ b/dotnetv4/ControlTower/Actions/HelloControlTower.cs
@@ -0,0 +1,64 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+// snippet-start:[ControlTower.dotnetv4.HelloControlTower]
+
+using LogLevel = Microsoft.Extensions.Logging.LogLevel;
+
+namespace ControlTowerActions;
+
+///
+/// A class that introduces the AWS Control Tower by listing the
+/// landing zones for the account.
+///
+public class HelloControlTower
+{
+ private static ILogger logger = null!;
+
+ static async Task Main(string[] args)
+ {
+ // Set up dependency injection for AWS Control Tower.
+ using var host = Host.CreateDefaultBuilder(args)
+ .ConfigureLogging(logging =>
+ logging.AddFilter("System", LogLevel.Debug)
+ .AddFilter("Microsoft", LogLevel.Information)
+ .AddFilter("Microsoft", LogLevel.Trace))
+ .ConfigureServices((_, services) =>
+ services.AddAWSService()
+ .AddTransient()
+ )
+ .Build();
+
+ logger = LoggerFactory.Create(builder => { builder.AddConsole(); })
+ .CreateLogger();
+
+ var amazonClient = host.Services.GetRequiredService();
+
+ Console.Clear();
+ Console.WriteLine("Hello AWS Control Tower.");
+ Console.WriteLine("Let's get a list of your AWS Control Tower landing zones.");
+
+ var landingZones = new List();
+
+ var landingZonesPaginator = amazonClient.Paginators.ListLandingZones(new ListLandingZonesRequest());
+
+ await foreach (var response in landingZonesPaginator.Responses)
+ {
+ landingZones.AddRange(response.LandingZones);
+ }
+
+ if (landingZones.Count > 0)
+ {
+ landingZones.ForEach(landingZone =>
+ {
+ Console.WriteLine($"{landingZone.Arn}\t{landingZone.Status}");
+ });
+ }
+ else
+ {
+ Console.WriteLine("No landing zones were found.");
+ }
+ }
+}
+
+// snippet-end:[ControlTower.dotnetv4.HelloControlTower]
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/Actions/Usings.cs b/dotnetv4/ControlTower/Actions/Usings.cs
new file mode 100644
index 00000000000..4624044bcbf
--- /dev/null
+++ b/dotnetv4/ControlTower/Actions/Usings.cs
@@ -0,0 +1,13 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+// snippet-start:[ControlTower.dotnetv4.Usings]
+global using Amazon.ControlTower;
+global using Amazon.ControlTower.Model;
+global using Microsoft.Extensions.DependencyInjection;
+global using Microsoft.Extensions.Hosting;
+global using Microsoft.Extensions.Logging;
+global using Microsoft.Extensions.Logging.Console;
+global using Microsoft.Extensions.Logging.Debug;
+
+// snippet-end:[ControlTower.dotnetv4.Usings]
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/ControlTowerExamples.sln b/dotnetv4/ControlTower/ControlTowerExamples.sln
new file mode 100644
index 00000000000..b064902934c
--- /dev/null
+++ b/dotnetv4/ControlTower/ControlTowerExamples.sln
@@ -0,0 +1,47 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.2.32630.192
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Actions", "Actions", "{7907FB6A-1353-4735-95DC-EEC5DF8C0649}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scenarios", "Scenarios", "{B987097B-189C-4D0B-99BC-E67CD705BCA0}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{5455D423-2AFC-4BC6-B79D-9DC4270D8F7D}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlTowerActions", "Actions\ControlTowerActions.csproj", "{796910FA-6E94-460B-8CB4-97DF01B9ADC8}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlTowerBasics", "Scenarios\ControlTower_Basics\ControlTowerBasics.csproj", "{B1731AE1-381F-4044-BEBE-269FF7E24B1F}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlTowerTests", "Tests\ControlTowerTests.csproj", "{6046A2FC-6A39-4C2D-8DD9-AA3740B17B88}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {796910FA-6E94-460B-8CB4-97DF01B9ADC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {796910FA-6E94-460B-8CB4-97DF01B9ADC8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {796910FA-6E94-460B-8CB4-97DF01B9ADC8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {796910FA-6E94-460B-8CB4-97DF01B9ADC8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B1731AE1-381F-4044-BEBE-269FF7E24B1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B1731AE1-381F-4044-BEBE-269FF7E24B1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B1731AE1-381F-4044-BEBE-269FF7E24B1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B1731AE1-381F-4044-BEBE-269FF7E24B1F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6046A2FC-6A39-4C2D-8DD9-AA3740B17B88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6046A2FC-6A39-4C2D-8DD9-AA3740B17B88}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6046A2FC-6A39-4C2D-8DD9-AA3740B17B88}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6046A2FC-6A39-4C2D-8DD9-AA3740B17B88}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {796910FA-6E94-460B-8CB4-97DF01B9ADC8} = {7907FB6A-1353-4735-95DC-EEC5DF8C0649}
+ {B1731AE1-381F-4044-BEBE-269FF7E24B1F} = {B987097B-189C-4D0B-99BC-E67CD705BCA0}
+ {6046A2FC-6A39-4C2D-8DD9-AA3740B17B88} = {5455D423-2AFC-4BC6-B79D-9DC4270D8F7D}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {870D888D-5C8B-4057-8722-F73ECF38E513}
+ EndGlobalSection
+EndGlobal
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/README.md b/dotnetv4/ControlTower/README.md
new file mode 100644
index 00000000000..33f5b2ce51c
--- /dev/null
+++ b/dotnetv4/ControlTower/README.md
@@ -0,0 +1,98 @@
+# AWS Control Tower code examples for the SDK for .NET
+
+## Overview
+
+Shows how to use the AWS SDK for .NET to work with AWS Control Tower.
+
+
+
+
+*AWS Control Tower provides a pre-configured multi-account environment based on best practices to help organizations set up a secure, compliant, multi-account AWS environment.*
+
+## ⚠ Important
+
+* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/).
+* Running the tests might result in charges to your AWS account.
+* We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege).
+* This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services).
+
+
+
+
+## Code examples
+
+### Prerequisites
+
+For prerequisites, see the [README](../../README.md#Prerequisites) in the `dotnetv4` folder.
+
+
+
+
+### Get started
+
+- [Hello AWS Control Tower](Actions/HelloControlTower.cs#L15) (`ListLandingZones`)
+
+### Single actions
+
+Code excerpts that show you how to call individual service functions.
+
+- [GetLandingZone](Actions/ControlTowerWrapper.cs#L35)
+- [ListEnabledControls](Actions/ControlTowerWrapper.cs#L50)
+- [ListLandingZones](Actions/ControlTowerWrapper.cs#L20)
+
+
+
+
+## Run the examples
+
+### Instructions
+
+For general instructions to run the examples, see the [README](../../README.md#building-and-running-the-code-examples) in the `dotnetv4` folder.
+
+Some projects might include a settings file. Before compiling the project, you can change these settings to match your account and preferred Region. Alternatively, add a settings.local.json file with your preferred settings, which will be loaded automatically when the application runs.
+
+After the example compiles, you can run it from the command line. To do so, navigate to the folder that contains the .csproj file and run the following command:
+
+```
+dotnet run
+```
+
+Alternatively, you can run the example from within your IDE.
+
+
+
+
+#### Hello AWS Control Tower
+
+This example shows you how to get started using AWS Control Tower.
+
+```
+dotnet run
+```
+
+
+
+
+### Tests
+
+⚠ Running tests might result in charges to your AWS account.
+
+To find instructions for running these tests, see the [README](../../README.md#Tests) in the `dotnetv4` folder.
+
+
+
+
+## Additional resources
+
+- [AWS Control Tower User Guide](https://docs.aws.amazon.com/controltower/latest/userguide/what-is-control-tower.html)
+- [AWS Control Tower API Reference](https://docs.aws.amazon.com/controltower/latest/APIReference/Welcome.html)
+- [AWS SDK for .NET AWS Control Tower reference](https://docs.aws.amazon.com/sdkfornet/v3/apidocs/items/ControlTower/NControlTower.html)
+
+
+
+
+---
+
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+SPDX-License-Identifier: Apache-2.0
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.csproj b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.csproj
new file mode 100644
index 00000000000..779f4a83a70
--- /dev/null
+++ b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.csproj
@@ -0,0 +1,26 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+ settings.json
+
+
+
+
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj b/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj
new file mode 100644
index 00000000000..0e5a3a20150
--- /dev/null
+++ b/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj
@@ -0,0 +1,35 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+ PreserveNewest
+ testsettings.json
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/Tests/Usings.cs b/dotnetv4/ControlTower/Tests/Usings.cs
new file mode 100644
index 00000000000..0f64a5599c7
--- /dev/null
+++ b/dotnetv4/ControlTower/Tests/Usings.cs
@@ -0,0 +1,7 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+global using Xunit;
+
+// Optional.
+[assembly: CollectionBehavior(DisableTestParallelization = true)]
\ No newline at end of file
From 1b983c61d4a057102c8a78a57ab9eaa79e19c0c1 Mon Sep 17 00:00:00 2001
From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com>
Date: Tue, 24 Jun 2025 13:39:25 -0500
Subject: [PATCH 2/7] Add Scenario files.
---
.../Actions/ControlTowerActions.csproj | 1 +
.../Actions/ControlTowerWrapper.cs | 468 ++++++++++++++++--
.../ControlTower/Actions/HelloControlTower.cs | 3 +-
dotnetv4/ControlTower/Actions/Usings.cs | 2 +
.../ControlTower_Basics/ControlTowerBasics.cs | 265 ++++++++++
.../ControlTowerBasics.csproj | 2 +
.../Scenarios/ControlTower_Basics/Usings.cs | 5 +
.../ControlTower_Basics/settings.json | 6 +
.../Tests/ControlTowerBasicsTests.cs | 79 +++
.../Tests/ControlTowerTests.csproj | 1 +
dotnetv4/ControlTower/Tests/testsettings.json | 6 +
python/example_code/controltower/README.md | 134 +++++
.../controltower/controltower_wrapper.py | 453 +++++++++++++++++
.../controltower/hello/hello_controltower.py | 40 ++
.../controltower/requirements.txt | 4 +
.../controltower/scenario_controltower.py | 315 ++++++++++++
.../controltower/test/conftest.py | 73 +++
.../controltower/test/test_scenario_run.py | 266 ++++++++++
18 files changed, 2094 insertions(+), 29 deletions(-)
create mode 100644 dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs
create mode 100644 dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs
create mode 100644 dotnetv4/ControlTower/Scenarios/ControlTower_Basics/settings.json
create mode 100644 dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs
create mode 100644 dotnetv4/ControlTower/Tests/testsettings.json
create mode 100644 python/example_code/controltower/README.md
create mode 100644 python/example_code/controltower/controltower_wrapper.py
create mode 100644 python/example_code/controltower/hello/hello_controltower.py
create mode 100644 python/example_code/controltower/requirements.txt
create mode 100644 python/example_code/controltower/scenario_controltower.py
create mode 100644 python/example_code/controltower/test/conftest.py
create mode 100644 python/example_code/controltower/test/test_scenario_run.py
diff --git a/dotnetv4/ControlTower/Actions/ControlTowerActions.csproj b/dotnetv4/ControlTower/Actions/ControlTowerActions.csproj
index b2276e2a524..d989156a2c3 100644
--- a/dotnetv4/ControlTower/Actions/ControlTowerActions.csproj
+++ b/dotnetv4/ControlTower/Actions/ControlTowerActions.csproj
@@ -9,6 +9,7 @@
+
diff --git a/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs
index 870c87d17a2..2121133689d 100644
--- a/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs
+++ b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs
@@ -2,7 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
// snippet-start:[ControlTower.dotnetv4.ControlTowerWrapper]
-using System.Net;
namespace ControlTowerActions;
@@ -12,14 +11,17 @@ namespace ControlTowerActions;
public class ControlTowerWrapper
{
private readonly IAmazonControlTower _controlTowerService;
+ private readonly IAmazonControlCatalog _controlCatalogService;
///
/// Constructor for the wrapper class containing AWS Control Tower actions.
///
/// The AWS Control Tower client object.
- public ControlTowerWrapper(IAmazonControlTower controlTowerService)
+ /// The AWS Control Catalog client object.
+ public ControlTowerWrapper(IAmazonControlTower controlTowerService, IAmazonControlCatalog controlCatalogService)
{
_controlTowerService = controlTowerService;
+ _controlCatalogService = controlCatalogService;
}
// snippet-start:[ControlTower.dotnetv4.ListLandingZones]
@@ -29,38 +31,270 @@ public ControlTowerWrapper(IAmazonControlTower controlTowerService)
/// A list of LandingZoneSummary objects.
public async Task> ListLandingZonesAsync()
{
- var landingZones = new List();
+ try
+ {
+ var landingZones = new List();
+
+ var landingZonesPaginator = _controlTowerService.Paginators.ListLandingZones(new ListLandingZonesRequest());
- var landingZonesPaginator = _controlTowerService.Paginators.ListLandingZones(new ListLandingZonesRequest());
+ await foreach (var response in landingZonesPaginator.Responses)
+ {
+ landingZones.AddRange(response.LandingZones);
+ }
- await foreach (var response in landingZonesPaginator.Responses)
+ return landingZones;
+ }
+ catch (AmazonControlTowerException ex)
{
- landingZones.AddRange(response.LandingZones);
+ Console.WriteLine($"Couldn't list landing zones. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
}
-
- return landingZones;
}
// snippet-end:[ControlTower.dotnetv4.ListLandingZones]
- // snippet-start:[ControlTower.dotnetv4.GetLandingZone]
+ // snippet-start:[ControlTower.dotnetv4.ListBaselines]
///
- /// Get details about a specific landing zone.
+ /// List all baselines.
///
- /// The landing zone identifier.
- /// The landing zone details.
- public async Task GetLandingZoneAsync(string landingZoneIdentifier)
+ /// A list of baseline summaries.
+ public async Task> ListBaselinesAsync()
{
- var request = new GetLandingZoneRequest
+ try
{
- LandingZoneIdentifier = landingZoneIdentifier
- };
+ var baselines = new List();
+
+ var baselinesPaginator = _controlTowerService.Paginators.ListBaselines(new ListBaselinesRequest());
- var response = await _controlTowerService.GetLandingZoneAsync(request);
- return response.LandingZone;
+ await foreach (var response in baselinesPaginator.Responses)
+ {
+ baselines.AddRange(response.Baselines);
+ }
+
+ return baselines;
+ }
+ catch (AmazonControlTowerException ex)
+ {
+ Console.WriteLine($"Couldn't list baselines. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
+ }
}
- // snippet-end:[ControlTower.dotnetv4.GetLandingZone]
+ // snippet-end:[ControlTower.dotnetv4.ListBaselines]
+
+ // snippet-start:[ControlTower.dotnetv4.ListEnabledBaselines]
+ ///
+ /// List all enabled baselines.
+ ///
+ /// A list of enabled baseline summaries.
+ public async Task> ListEnabledBaselinesAsync()
+ {
+ try
+ {
+ var enabledBaselines = new List();
+
+ var enabledBaselinesPaginator = _controlTowerService.Paginators.ListEnabledBaselines(new ListEnabledBaselinesRequest());
+
+ await foreach (var response in enabledBaselinesPaginator.Responses)
+ {
+ enabledBaselines.AddRange(response.EnabledBaselines);
+ }
+
+ return enabledBaselines;
+ }
+ catch (AmazonControlTowerException ex)
+ {
+ Console.WriteLine($"Couldn't list enabled baselines. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
+ }
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.ListEnabledBaselines]
+
+ // snippet-start:[ControlTower.dotnetv4.EnableBaseline]
+ ///
+ /// Enable a baseline for the specified target.
+ ///
+ /// The ARN of the target.
+ /// The identifier of baseline to enable.
+ /// The version of baseline to enable.
+ /// The identifier of identity center baseline if it is enabled.
+ /// The enabled baseline ARN or null if already enabled.
+ public async Task EnableBaselineAsync(string targetIdentifier, string baselineIdentifier, string baselineVersion, string identityCenterBaseline)
+ {
+ try
+ {
+ var parameters = new List
+ {
+ new EnabledBaselineParameter
+ {
+ Key = "IdentityCenterEnabledBaselineArn",
+ Value = identityCenterBaseline
+ }
+ };
+
+ var request = new EnableBaselineRequest
+ {
+ BaselineIdentifier = baselineIdentifier,
+ BaselineVersion = baselineVersion,
+ TargetIdentifier = targetIdentifier,
+ Parameters = parameters
+ };
+
+ var response = await _controlTowerService.EnableBaselineAsync(request);
+ var operationId = response.OperationIdentifier;
+
+ // Wait for operation to complete
+ while (true)
+ {
+ var status = await GetBaselineOperationAsync(operationId);
+ Console.WriteLine($"Baseline operation status: {status}");
+ if (status == BaselineOperationStatus.SUCCEEDED || status == BaselineOperationStatus.FAILED)
+ {
+ break;
+ }
+ await Task.Delay(30000); // Wait 30 seconds
+ }
+
+ return response.Arn;
+ }
+ catch (Amazon.ControlTower.Model.ValidationException ex) when (ex.Message.Contains("already enabled"))
+ {
+ Console.WriteLine("Baseline is already enabled for this target");
+ return null;
+ }
+ catch (AmazonControlTowerException ex)
+ {
+ Console.WriteLine($"Couldn't enable baseline. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
+ }
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.EnableBaseline]
+
+ // snippet-start:[ControlTower.dotnetv4.DisableBaseline]
+ ///
+ /// Disable a baseline for a specific target and wait for the operation to complete.
+ ///
+ /// The identifier of the baseline to disable.
+ /// The operation ID or null if there was a conflict.
+ public async Task DisableBaselineAsync(string enabledBaselineIdentifier)
+ {
+ try
+ {
+ var request = new DisableBaselineRequest
+ {
+ EnabledBaselineIdentifier = enabledBaselineIdentifier
+ };
+
+ var response = await _controlTowerService.DisableBaselineAsync(request);
+ var operationId = response.OperationIdentifier;
+
+ // Wait for operation to complete
+ while (true)
+ {
+ var status = await GetBaselineOperationAsync(operationId);
+ Console.WriteLine($"Baseline operation status: {status}");
+ if (status == BaselineOperationStatus.SUCCEEDED || status == BaselineOperationStatus.FAILED)
+ {
+ break;
+ }
+ await Task.Delay(30000); // Wait 30 seconds
+ }
+
+ return operationId;
+ }
+ catch (ConflictException ex)
+ {
+ Console.WriteLine($"Conflict disabling baseline: {ex.Message}. Skipping disable step.");
+ return null;
+ }
+ catch (AmazonControlTowerException ex)
+ {
+ Console.WriteLine($"Couldn't disable baseline. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
+ }
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.DisableBaseline]
+
+ // snippet-start:[ControlTower.dotnetv4.ResetEnabledBaseline]
+ ///
+ /// Reset an enabled baseline for a specific target.
+ ///
+ /// The identifier of the enabled baseline to reset.
+ /// The operation ID.
+ public async Task ResetEnabledBaselineAsync(string enabledBaselineIdentifier)
+ {
+ try
+ {
+ var request = new ResetEnabledBaselineRequest
+ {
+ EnabledBaselineIdentifier = enabledBaselineIdentifier
+ };
+
+ var response = await _controlTowerService.ResetEnabledBaselineAsync(request);
+ var operationId = response.OperationIdentifier;
+
+ // Wait for operation to complete
+ while (true)
+ {
+ var status = await GetBaselineOperationAsync(operationId);
+ Console.WriteLine($"Baseline operation status: {status}");
+ if (status == BaselineOperationStatus.SUCCEEDED || status == BaselineOperationStatus.FAILED)
+ {
+ break;
+ }
+ await Task.Delay(30000); // Wait 30 seconds
+ }
+
+ return operationId;
+ }
+ catch (Amazon.ControlTower.Model.ResourceNotFoundException)
+ {
+ Console.WriteLine("Target not found, unable to reset enabled baseline.");
+ throw;
+ }
+ catch (AmazonControlTowerException ex)
+ {
+ Console.WriteLine($"Couldn't reset enabled baseline. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
+ }
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.ResetEnabledBaseline]
+
+ // snippet-start:[ControlTower.dotnetv4.GetBaselineOperation]
+ ///
+ /// Get the status of a baseline operation.
+ ///
+ /// The ID of the baseline operation.
+ /// The operation status.
+ public async Task GetBaselineOperationAsync(string operationId)
+ {
+ try
+ {
+ var request = new GetBaselineOperationRequest
+ {
+ OperationIdentifier = operationId
+ };
+
+ var response = await _controlTowerService.GetBaselineOperationAsync(request);
+ return response.BaselineOperation.Status;
+ }
+ catch (Amazon.ControlTower.Model.ResourceNotFoundException)
+ {
+ Console.WriteLine("Operation not found.");
+ throw;
+ }
+ catch (AmazonControlTowerException ex)
+ {
+ Console.WriteLine($"Couldn't get baseline operation status. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
+ }
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.GetBaselineOperation]
// snippet-start:[ControlTower.dotnetv4.ListEnabledControls]
///
@@ -70,24 +304,202 @@ public async Task GetLandingZoneAsync(string landingZoneIdent
/// A list of enabled control summaries.
public async Task> ListEnabledControlsAsync(string targetIdentifier)
{
- var request = new ListEnabledControlsRequest
+ try
{
- TargetIdentifier = targetIdentifier
- };
+ var request = new ListEnabledControlsRequest
+ {
+ TargetIdentifier = targetIdentifier
+ };
+
+ var enabledControls = new List();
- var enabledControls = new List();
+ var enabledControlsPaginator = _controlTowerService.Paginators.ListEnabledControls(request);
- var enabledControlsPaginator = _controlTowerService.Paginators.ListEnabledControls(request);
+ await foreach (var response in enabledControlsPaginator.Responses)
+ {
+ enabledControls.AddRange(response.EnabledControls);
+ }
- await foreach (var response in enabledControlsPaginator.Responses)
+ return enabledControls;
+ }
+ catch (Amazon.ControlTower.Model.ResourceNotFoundException ex) when (ex.Message.Contains("not registered with AWS Control Tower"))
{
- enabledControls.AddRange(response.EnabledControls);
+ Console.WriteLine("AWS Control Tower must be enabled to work with enabling controls.");
+ return new List();
+ }
+ catch (AmazonControlTowerException ex)
+ {
+ Console.WriteLine($"Couldn't list enabled controls. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
}
-
- return enabledControls;
}
// snippet-end:[ControlTower.dotnetv4.ListEnabledControls]
+
+ // snippet-start:[ControlTower.dotnetv4.EnableControl]
+ ///
+ /// Enable a control for a specified target.
+ ///
+ /// The ARN of the control to enable.
+ /// The identifier of the target (e.g., OU ARN).
+ /// The operation ID or null if already enabled.
+ public async Task EnableControlAsync(string controlArn, string targetIdentifier)
+ {
+ try
+ {
+ Console.WriteLine(controlArn);
+ Console.WriteLine(targetIdentifier);
+
+ var request = new EnableControlRequest
+ {
+ ControlIdentifier = controlArn,
+ TargetIdentifier = targetIdentifier
+ };
+
+ var response = await _controlTowerService.EnableControlAsync(request);
+ var operationId = response.OperationIdentifier;
+
+ // Wait for operation to complete
+ while (true)
+ {
+ var status = await GetControlOperationAsync(operationId);
+ Console.WriteLine($"Control operation status: {status}");
+ if (status == ControlOperationStatus.SUCCEEDED || status == ControlOperationStatus.FAILED)
+ {
+ break;
+ }
+ await Task.Delay(30000); // Wait 30 seconds
+ }
+
+ return operationId;
+ }
+ catch (Amazon.ControlTower.Model.ValidationException ex) when (ex.Message.Contains("already enabled"))
+ {
+ Console.WriteLine("Control is already enabled for this target");
+ return null;
+ }
+ catch (Amazon.ControlTower.Model.ResourceNotFoundException ex) when (ex.Message.Contains("not registered with AWS Control Tower"))
+ {
+ Console.WriteLine("AWS Control Tower must be enabled to work with enabling controls.");
+ return null;
+ }
+ catch (AmazonControlTowerException ex)
+ {
+ Console.WriteLine($"Couldn't enable control. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
+ }
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.EnableControl]
+
+ // snippet-start:[ControlTower.dotnetv4.DisableControl]
+ ///
+ /// Disable a control for a specified target.
+ ///
+ /// The ARN of the control to disable.
+ /// The identifier of the target (e.g., OU ARN).
+ /// The operation ID.
+ public async Task DisableControlAsync(string controlArn, string targetIdentifier)
+ {
+ try
+ {
+ var request = new DisableControlRequest
+ {
+ ControlIdentifier = controlArn,
+ TargetIdentifier = targetIdentifier
+ };
+
+ var response = await _controlTowerService.DisableControlAsync(request);
+ var operationId = response.OperationIdentifier;
+
+ // Wait for operation to complete
+ while (true)
+ {
+ var status = await GetControlOperationAsync(operationId);
+ Console.WriteLine($"Control operation status: {status}");
+ if (status == ControlOperationStatus.SUCCEEDED || status == ControlOperationStatus.FAILED)
+ {
+ break;
+ }
+ await Task.Delay(30000); // Wait 30 seconds
+ }
+
+ return operationId;
+ }
+ catch (Amazon.ControlTower.Model.ResourceNotFoundException)
+ {
+ Console.WriteLine("Control not found.");
+ throw;
+ }
+ catch (AmazonControlTowerException ex)
+ {
+ Console.WriteLine($"Couldn't disable control. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
+ }
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.DisableControl]
+
+ // snippet-start:[ControlTower.dotnetv4.GetControlOperation]
+ ///
+ /// Get the status of a control operation.
+ ///
+ /// The ID of the control operation.
+ /// The operation status.
+ public async Task GetControlOperationAsync(string operationId)
+ {
+ try
+ {
+ var request = new GetControlOperationRequest
+ {
+ OperationIdentifier = operationId
+ };
+
+ var response = await _controlTowerService.GetControlOperationAsync(request);
+ return response.ControlOperation.Status;
+ }
+ catch (Amazon.ControlTower.Model.ResourceNotFoundException)
+ {
+ Console.WriteLine("Operation not found.");
+ throw;
+ }
+ catch (AmazonControlTowerException ex)
+ {
+ Console.WriteLine($"Couldn't get control operation status. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
+ }
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.GetControlOperation]
+
+ // snippet-start:[ControlTower.dotnetv4.ListControls]
+ ///
+ /// List all controls in the Control Tower control catalog.
+ ///
+ /// A list of control summaries.
+ public async Task> ListControlsAsync()
+ {
+ try
+ {
+ var controls = new List();
+
+ var controlsPaginator = _controlCatalogService.Paginators.ListControls(new Amazon.ControlCatalog.Model.ListControlsRequest());
+
+ await foreach (var response in controlsPaginator.Responses)
+ {
+ controls.AddRange(response.Controls);
+ }
+
+ return controls;
+ }
+ catch (AmazonControlCatalogException ex)
+ {
+ Console.WriteLine($"Couldn't list controls. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
+ }
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.ListControls]
}
// snippet-end:[ControlTower.dotnetv4.ControlTowerWrapper]
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/Actions/HelloControlTower.cs b/dotnetv4/ControlTower/Actions/HelloControlTower.cs
index d77c6dee60b..b46920240f4 100644
--- a/dotnetv4/ControlTower/Actions/HelloControlTower.cs
+++ b/dotnetv4/ControlTower/Actions/HelloControlTower.cs
@@ -25,6 +25,7 @@ static async Task Main(string[] args)
.AddFilter("Microsoft", LogLevel.Trace))
.ConfigureServices((_, services) =>
services.AddAWSService()
+ .AddAWSService()
.AddTransient()
)
.Build();
@@ -51,7 +52,7 @@ static async Task Main(string[] args)
{
landingZones.ForEach(landingZone =>
{
- Console.WriteLine($"{landingZone.Arn}\t{landingZone.Status}");
+ Console.WriteLine($"Landing Zone \t{landingZone.Arn}");
});
}
else
diff --git a/dotnetv4/ControlTower/Actions/Usings.cs b/dotnetv4/ControlTower/Actions/Usings.cs
index 4624044bcbf..e0bb67d1aeb 100644
--- a/dotnetv4/ControlTower/Actions/Usings.cs
+++ b/dotnetv4/ControlTower/Actions/Usings.cs
@@ -4,6 +4,8 @@
// snippet-start:[ControlTower.dotnetv4.Usings]
global using Amazon.ControlTower;
global using Amazon.ControlTower.Model;
+global using Amazon.ControlCatalog;
+global using Amazon.ControlCatalog.Model;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Hosting;
global using Microsoft.Extensions.Logging;
diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs
new file mode 100644
index 00000000000..5a8640b89fa
--- /dev/null
+++ b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs
@@ -0,0 +1,265 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+// snippet-start:[ControlTower.dotnetv4.ControlTowerBasics]
+
+using Amazon.ControlTower.Model;
+using Amazon.Organizations;
+using Amazon.Organizations.Model;
+using Amazon.SecurityToken;
+using Amazon.SecurityToken.Model;
+using ControlTowerActions;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace ControlTowerBasics;
+
+///
+/// Scenario class for AWS Control Tower basics.
+///
+public class ControlTowerBasics
+{
+ private static ILogger logger = null!;
+ private static IAmazonOrganizations orgClient = null!;
+ private static string? ouArn;
+ private static bool useLandingZone = false;
+
+ static async Task Main(string[] args)
+ {
+ using var host = Host.CreateDefaultBuilder(args)
+ .ConfigureServices((_, services) =>
+ services.AddAWSService()
+ .AddAWSService()
+ .AddAWSService()
+ .AddAWSService()
+ .AddTransient()
+ )
+ .Build();
+
+ logger = LoggerFactory.Create(builder => { builder.AddConsole(); })
+ .CreateLogger();
+
+ var wrapper = host.Services.GetRequiredService();
+ orgClient = host.Services.GetRequiredService();
+ var stsClient = host.Services.GetRequiredService();
+
+ Console.WriteLine(new string('-', 88));
+ Console.WriteLine("\tWelcome to the AWS Control Tower with ControlCatalog example scenario.");
+ Console.WriteLine(new string('-', 88));
+ Console.WriteLine("This demo will walk you through working with AWS Control Tower for landing zones,");
+ Console.WriteLine("managing baselines, and working with controls.");
+
+ try
+ {
+ var accountId = (await stsClient.GetCallerIdentityAsync(new GetCallerIdentityRequest())).Account;
+ Console.WriteLine($"\nAccount ID: {accountId}");
+
+ Console.WriteLine("\nSome demo operations require the use of a landing zone.");
+ Console.WriteLine("You can use an existing landing zone or opt out of these operations in the demo.");
+ Console.WriteLine("For instructions on how to set up a landing zone,");
+ Console.WriteLine("see https://docs.aws.amazon.com/controltower/latest/userguide/getting-started-from-console.html");
+
+ // List available landing zones
+ var landingZones = await wrapper.ListLandingZonesAsync();
+ if (landingZones.Count > 0)
+ {
+ Console.WriteLine("\nAvailable Landing Zones:");
+ for (int i = 0; i < landingZones.Count; i++)
+ {
+ Console.WriteLine($"{i + 1}. {landingZones[i].Arn}");
+ }
+
+ Console.Write($"\nDo you want to use the first landing zone in the list ({landingZones[0].Arn})? (y/n): ");
+ if (Console.ReadLine()?.ToLower() == "y")
+ {
+ useLandingZone = true;
+ Console.WriteLine($"Using landing zone: {landingZones[0].Arn}");
+ ouArn = await SetupOrganizationAsync();
+ }
+ }
+
+ // Managing Baselines
+ Console.WriteLine("\nManaging Baselines:");
+ var baselines = await wrapper.ListBaselinesAsync();
+ Console.WriteLine("\nListing available Baselines:");
+ BaselineSummary? controlTowerBaseline = null;
+ foreach (var baseline in baselines)
+ {
+ if (baseline.Name == "AWSControlTowerBaseline")
+ controlTowerBaseline = baseline;
+ Console.WriteLine($" - {baseline.Name}");
+ }
+
+ EnabledBaselineSummary? identityCenterBaseline = null;
+ string? baselineArn = null;
+
+ if (useLandingZone && ouArn != null)
+ {
+ Console.WriteLine("\nListing enabled baselines:");
+ var enabledBaselines = await wrapper.ListEnabledBaselinesAsync();
+ foreach (var baseline in enabledBaselines)
+ {
+ if (baseline.BaselineIdentifier.Contains("baseline/LN25R72TTG6IGPTQ"))
+ identityCenterBaseline = baseline;
+ Console.WriteLine($" - {baseline.BaselineIdentifier}");
+ }
+
+ if (controlTowerBaseline != null)
+ {
+ Console.Write("\nDo you want to enable the Control Tower Baseline? (y/n): ");
+ if (Console.ReadLine()?.ToLower() == "y")
+ {
+ Console.WriteLine("\nEnabling Control Tower Baseline.");
+ var icBaselineArn = identityCenterBaseline?.Arn;
+ baselineArn = await wrapper.EnableBaselineAsync(ouArn,
+ controlTowerBaseline.Arn, "4.0", icBaselineArn ?? "");
+ var alreadyEnabled = false;
+ if (baselineArn != null)
+ {
+ Console.WriteLine($"Enabled baseline ARN: {baselineArn}");
+ }
+ else
+ {
+ // Find the enabled baseline
+ foreach (var enabled in enabledBaselines)
+ {
+ if (enabled.BaselineIdentifier == controlTowerBaseline.Arn)
+ {
+ baselineArn = enabled.Arn;
+ break;
+ }
+ }
+
+ alreadyEnabled = true;
+ Console.WriteLine("No change, the selected baseline was already enabled.");
+ }
+
+ if (baselineArn != null)
+ {
+ Console.Write("\nDo you want to reset the Control Tower Baseline? (y/n): ");
+ if (Console.ReadLine()?.ToLower() == "y")
+ {
+ Console.WriteLine($"\nResetting Control Tower Baseline: {baselineArn}");
+ var operationId = await wrapper.ResetEnabledBaselineAsync(baselineArn);
+ Console.WriteLine($"Reset baseline operation id: {operationId}");
+ }
+
+ Console.Write("\nDo you want to disable the Control Tower Baseline? (y/n): ");
+ if (Console.ReadLine()?.ToLower() == "y")
+ {
+ Console.WriteLine($"Disabling baseline ARN: {baselineArn}");
+ var operationId = await wrapper.DisableBaselineAsync(baselineArn);
+ Console.WriteLine($"Disabled baseline operation id: {operationId}");
+ if (alreadyEnabled)
+ {
+ Console.WriteLine($"\nRe-enabling Control Tower Baseline: {baselineArn}");
+ // Re-enable the Control Tower baseline if it was originally enabled.
+ await wrapper.EnableBaselineAsync(ouArn,
+ controlTowerBaseline.Arn, "4.0", icBaselineArn ?? "");
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Managing Controls
+ Console.WriteLine("\nManaging Controls:");
+ var controls = await wrapper.ListControlsAsync();
+ Console.WriteLine("\nListing first 5 available Controls:");
+ for (int i = 0; i < Math.Min(5, controls.Count); i++)
+ {
+ Console.WriteLine($"{i + 1}. {controls[i].Name} - {controls[i].Arn}");
+ }
+
+ if (useLandingZone && ouArn != null)
+ {
+ var enabledControls = await wrapper.ListEnabledControlsAsync(ouArn);
+ Console.WriteLine("\nListing enabled controls:");
+ for (int i = 0; i < enabledControls.Count; i++)
+ {
+ Console.WriteLine($"{i + 1}. {enabledControls[i].ControlIdentifier}");
+ }
+
+ // Find first non-enabled control
+ var enabledControlArns = enabledControls.Select(c => c.Arn).ToHashSet();
+ var controlArn = controls.FirstOrDefault(c => !enabledControlArns.Contains(c.Arn))?.Arn;
+
+ if (controlArn != null)
+ {
+ Console.Write($"\nDo you want to enable the control {controlArn}? (y/n): ");
+ if (Console.ReadLine()?.ToLower() == "y")
+ {
+ Console.WriteLine($"\nEnabling control: {controlArn}");
+ var operationId = await wrapper.EnableControlAsync(controlArn, ouArn);
+ if (operationId != null)
+ {
+ Console.WriteLine($"Enabled control with operation id: {operationId}");
+
+ Console.Write("\nDo you want to disable the control? (y/n): ");
+ if (Console.ReadLine()?.ToLower() == "y")
+ {
+ Console.WriteLine("\nDisabling the control...");
+ var disableOpId = await wrapper.DisableControlAsync(controlArn, ouArn);
+ Console.WriteLine($"Disable operation ID: {disableOpId}");
+ }
+ }
+ }
+ }
+ }
+
+ Console.WriteLine("\nThis concludes the example scenario.");
+ Console.WriteLine("Thanks for watching!");
+ Console.WriteLine(new string('-', 88));
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An error occurred during the Control Tower scenario.");
+ Console.WriteLine($"An error occurred: {ex.Message}");
+ }
+ }
+
+ private static async Task SetupOrganizationAsync()
+ {
+ Console.WriteLine("\nChecking organization status...");
+
+ try
+ {
+ var orgResponse = await orgClient.DescribeOrganizationAsync(new DescribeOrganizationRequest());
+ var orgId = orgResponse.Organization.Id;
+ Console.WriteLine($"Account is part of organization: {orgId}");
+ }
+ catch (AWSOrganizationsNotInUseException)
+ {
+ Console.WriteLine("No organization found. Creating a new organization...");
+ var createResponse = await orgClient.CreateOrganizationAsync(new CreateOrganizationRequest { FeatureSet = OrganizationFeatureSet.ALL });
+ var orgId = createResponse.Organization.Id;
+ Console.WriteLine($"Created new organization: {orgId}");
+ }
+
+ // Look for Sandbox OU
+ var roots = await orgClient.ListRootsAsync(new ListRootsRequest());
+ var rootId = roots.Roots[0].Id;
+
+ Console.WriteLine("Checking for Sandbox OU...");
+ var ous = await orgClient.ListOrganizationalUnitsForParentAsync(new ListOrganizationalUnitsForParentRequest { ParentId = rootId });
+ var sandboxOu = ous.OrganizationalUnits.FirstOrDefault(ou => ou.Name == "Sandbox");
+
+ if (sandboxOu == null)
+ {
+ Console.WriteLine("Creating Sandbox OU...");
+ var createOuResponse = await orgClient.CreateOrganizationalUnitAsync(new CreateOrganizationalUnitRequest { ParentId = rootId, Name = "Sandbox" });
+ sandboxOu = createOuResponse.OrganizationalUnit;
+ Console.WriteLine($"Created new Sandbox OU: {sandboxOu.Id}");
+ }
+ else
+ {
+ Console.WriteLine($"Found existing Sandbox OU: {sandboxOu.Id}");
+ }
+
+ return sandboxOu.Arn;
+ }
+}
+
+// snippet-end:[ControlTower.dotnetv4.ControlTowerBasics]
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.csproj b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.csproj
index 779f4a83a70..78d5b798214 100644
--- a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.csproj
+++ b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.csproj
@@ -9,6 +9,8 @@
+
+
diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs
new file mode 100644
index 00000000000..c8cc76f2bfa
--- /dev/null
+++ b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs
@@ -0,0 +1,5 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+global using Amazon.ControlTower;
+global using Amazon.ControlCatalog;
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/settings.json b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/settings.json
new file mode 100644
index 00000000000..9a17f2b6ffc
--- /dev/null
+++ b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/settings.json
@@ -0,0 +1,6 @@
+{
+ "TargetIdentifier": "arn:aws:organizations::123456789012:ou/o-example12345/ou-example12345",
+ "ControlArn": "arn:aws:controltower:us-east-1::control/AWS-GR_AUDIT_BUCKET_ENCRYPTION_ENABLED",
+ "BaselineIdentifier": "arn:aws:controltower:us-east-1::baseline/AWSControlTowerBaseline",
+ "BaselineVersion": "1.0"
+}
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs b/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs
new file mode 100644
index 00000000000..bf2533a0807
--- /dev/null
+++ b/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs
@@ -0,0 +1,79 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.ControlTower;
+using Amazon.ControlCatalog;
+using Microsoft.Extensions.Configuration;
+using Moq;
+using ControlTowerActions;
+
+namespace ControlTowerTests;
+
+///
+/// Tests for the ControlTowerWrapper class.
+///
+public class ControlTowerBasicsTests
+{
+ private readonly IConfiguration _configuration;
+
+ ///
+ /// Constructor for the test class.
+ ///
+ public ControlTowerBasicsTests()
+ {
+ _configuration = new ConfigurationBuilder()
+ .SetBasePath(Directory.GetCurrentDirectory())
+ .AddJsonFile("testsettings.json") // Load test settings from JSON file.
+ .AddJsonFile("testsettings.local.json",
+ true) // Load local test settings from JSON file.
+ .Build();
+ }
+
+ ///
+ /// Test that ListLandingZonesAsync returns a list.
+ ///
+ [Fact]
+ public async Task ListLandingZonesAsync_ShouldReturnList()
+ {
+ // Arrange
+ var mockControlTowerService = new Mock();
+ var mockControlCatalogService = new Mock();
+ var wrapper = new ControlTowerWrapper(mockControlTowerService.Object, mockControlCatalogService.Object);
+
+ // Act & Assert
+ var exception = await Record.ExceptionAsync(() => wrapper.ListLandingZonesAsync());
+ Assert.Null(exception);
+ }
+
+ ///
+ /// Test that ListBaselinesAsync returns a list.
+ ///
+ [Fact]
+ public async Task ListBaselinesAsync_ShouldReturnList()
+ {
+ // Arrange
+ var mockControlTowerService = new Mock();
+ var mockControlCatalogService = new Mock();
+ var wrapper = new ControlTowerWrapper(mockControlTowerService.Object, mockControlCatalogService.Object);
+
+ // Act & Assert
+ var exception = await Record.ExceptionAsync(() => wrapper.ListBaselinesAsync());
+ Assert.Null(exception);
+ }
+
+ ///
+ /// Test that ListControlsAsync returns a list.
+ ///
+ [Fact]
+ public async Task ListControlsAsync_ShouldReturnList()
+ {
+ // Arrange
+ var mockControlTowerService = new Mock();
+ var mockControlCatalogService = new Mock();
+ var wrapper = new ControlTowerWrapper(mockControlTowerService.Object, mockControlCatalogService.Object);
+
+ // Act & Assert
+ var exception = await Record.ExceptionAsync(() => wrapper.ListControlsAsync());
+ Assert.Null(exception);
+ }
+}
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj b/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj
index 0e5a3a20150..caec21bafe6 100644
--- a/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj
+++ b/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj
@@ -10,6 +10,7 @@
+
diff --git a/dotnetv4/ControlTower/Tests/testsettings.json b/dotnetv4/ControlTower/Tests/testsettings.json
new file mode 100644
index 00000000000..9a17f2b6ffc
--- /dev/null
+++ b/dotnetv4/ControlTower/Tests/testsettings.json
@@ -0,0 +1,6 @@
+{
+ "TargetIdentifier": "arn:aws:organizations::123456789012:ou/o-example12345/ou-example12345",
+ "ControlArn": "arn:aws:controltower:us-east-1::control/AWS-GR_AUDIT_BUCKET_ENCRYPTION_ENABLED",
+ "BaselineIdentifier": "arn:aws:controltower:us-east-1::baseline/AWSControlTowerBaseline",
+ "BaselineVersion": "1.0"
+}
\ No newline at end of file
diff --git a/python/example_code/controltower/README.md b/python/example_code/controltower/README.md
new file mode 100644
index 00000000000..0246c301ca9
--- /dev/null
+++ b/python/example_code/controltower/README.md
@@ -0,0 +1,134 @@
+# AWS Control Tower code examples for the SDK for Python
+
+## Overview
+
+Shows how to use the AWS SDK for Python (Boto3) to work with AWS Control Tower.
+
+
+
+
+_AWS Control Tower enables you to enforce and manage governance rules for security, operations, and compliance at scale across all your organizations and accounts._
+
+## ⚠ Important
+
+* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/).
+* Running the tests might result in charges to your AWS account.
+* We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege).
+* This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services).
+
+
+
+
+## Code examples
+
+### Prerequisites
+
+For prerequisites, see the [README](../../README.md#Prerequisites) in the `python` folder.
+
+Install the packages required by these examples by running the following in a virtual environment:
+
+```
+python -m pip install -r requirements.txt
+```
+
+
+Before running the example, set up a landing zone in order to run the baseline and control management sections.
+Follow the instructions provided by the [quick start](https://docs.aws.amazon.com/controltower/latest/userguide/quick-start.html) guide.
+
+
+### Get started
+
+- [Hello AWS Control Tower](hello/hello_controltower.py#L4) (`ListBaselines`)
+
+
+### Basics
+
+Code examples that show you how to perform the essential operations within a service.
+
+- [Learn the basics](scenario_controltower.py)
+
+
+### Single actions
+
+Code excerpts that show you how to call individual service functions.
+
+- [DisableBaseline](controltower_wrapper.py#L365)
+- [DisableControl](controltower_wrapper.py#L240)
+- [EnableBaseline](controltower_wrapper.py#L64)
+- [EnableControl](controltower_wrapper.py#L143)
+- [GetControlOperation](controltower_wrapper.py#L186)
+- [ListBaselines](controltower_wrapper.py#L36)
+- [ListEnabledBaselines](controltower_wrapper.py#L305)
+- [ListEnabledControls](controltower_wrapper.py#L401)
+- [ListLandingZones](controltower_wrapper.py#L278)
+- [ResetEnabledBaseline](controltower_wrapper.py#L332)
+
+
+
+
+
+## Run the examples
+
+### Instructions
+
+
+
+
+
+#### Hello AWS Control Tower
+
+This example shows you how to get started using AWS Control Tower.
+
+```
+python hello/hello_controltower.py
+```
+
+#### Learn the basics
+
+This example shows you how to do the following:
+
+- List landing zones.
+- List, enable, get, reset, and disable baselines.
+- List, enable, get, and disable controls.
+
+
+
+
+Start the example by running the following at a command prompt:
+
+```
+python scenario_controltower.py
+```
+
+
+
+
+
+
+### Tests
+
+⚠ Running tests might result in charges to your AWS account.
+
+
+To find instructions for running these tests, see the [README](../../README.md#Tests)
+in the `python` folder.
+
+
+
+
+
+
+## Additional resources
+
+- [AWS Control Tower User Guide](https://docs.aws.amazon.com/controltower/latest/userguide/what-is-control-tower.html)
+- [AWS Control Tower API Reference](https://docs.aws.amazon.com/controltower/latest/APIReference/Welcome.html)
+- [SDK for Python AWS Control Tower reference](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cognito-idp.html)
+
+
+
+
+---
+
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+SPDX-License-Identifier: Apache-2.0
diff --git a/python/example_code/controltower/controltower_wrapper.py b/python/example_code/controltower/controltower_wrapper.py
new file mode 100644
index 00000000000..8034b5ebb14
--- /dev/null
+++ b/python/example_code/controltower/controltower_wrapper.py
@@ -0,0 +1,453 @@
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+import logging
+import boto3
+import time
+
+from botocore.exceptions import ClientError
+
+logger = logging.getLogger(__name__)
+
+
+# snippet-start:[python.example_code.controltower.ControlTowerWrapper.class]
+# snippet-start:[python.example_code.controltower.ControlTowerWrapper.decl]
+
+
+class ControlTowerWrapper:
+ """Encapsulates AWS Control Tower and Control Catalog functionality."""
+
+ def __init__(self, controltower_client, controlcatalog_client):
+ """
+ :param controltower_client: A Boto3 Amazon ControlTower client.
+ :param controlcatalog_client: A Boto3 Amazon ControlCatalog client.
+ """
+ self.controltower_client = controltower_client
+ self.controlcatalog_client = controlcatalog_client
+
+ @classmethod
+ def from_client(cls):
+ controltower_client = boto3.client("controltower")
+ controlcatalog_client = boto3.client("controlcatalog")
+ return cls(controltower_client, controlcatalog_client)
+
+ # snippet-end:[python.example_code.controltower.ControlTowerWrapper.decl]
+
+ # snippet-start:[python.example_code.controltower.ListBaselines]
+ def list_baselines(self):
+ """
+ Lists all baselines.
+
+ :return: List of baselines.
+ :raises ClientError: If the listing operation fails.
+ """
+ try:
+ paginator = self.controltower_client.get_paginator("list_baselines")
+ baselines = []
+ for page in paginator.paginate():
+ baselines.extend(page["baselines"])
+ return baselines
+
+ except ClientError as err:
+ if err.response["Error"]["Code"] == "AccessDeniedException":
+ logger.error(
+ "Access denied. Please ensure you have the necessary permissions."
+ )
+ else:
+ logger.error(
+ "Couldn't list baselines. Here's why: %s: %s",
+ err.response["Error"]["Code"],
+ err.response["Error"]["Message"],
+ )
+ raise
+
+ # snippet-end:[python.example_code.controltower.ListBaselines]
+
+ # snippet-start:[python.example_code.controltower.EnableBaseline]
+ def enable_baseline(
+ self,
+ target_identifier,
+ identity_center_baseline,
+ baseline_identifier,
+ baseline_version,
+ ):
+ """
+ Enables a baseline for the specified target if it's not already enabled.
+
+ :param target_identifier: The ARN of the target.
+ :param baseline_identifier: The identifier of baseline to enable.
+ :param identity_center_baseline: The identifier of identity center baseline if it is enabled.
+ :param baseline_version: The version of baseline to enable.
+ :return: The enabled baseline ARN or None if already enabled.
+ :raises ClientError: If enabling the baseline fails for reasons other than it being already enabled.
+ """
+ try:
+ response = self.controltower_client.enable_baseline(
+ baselineIdentifier=baseline_identifier,
+ baselineVersion=baseline_version,
+ targetIdentifier=target_identifier,
+ parameters=[
+ {
+ "key": "IdentityCenterEnabledBaselineArn",
+ "value": identity_center_baseline,
+ }
+ ],
+ )
+
+ operation_id = response["operationIdentifier"]
+ while True:
+ status = self.get_baseline_operation(operation_id)
+ print(f"Baseline operation status: {status}")
+ if status in ["SUCCEEDED", "FAILED"]:
+ break
+ time.sleep(30)
+
+ return response["arn"]
+ except ClientError as err:
+ if err.response["Error"]["Code"] == "ValidationException":
+ if "already enabled" in err.response["Error"]["Message"]:
+ print("Baseline is already enabled for this target")
+ return None
+ else:
+ print(
+ "Unable to enable baseline due to validation exception: %s: %s",
+ err.response["Error"]["Code"],
+ err.response["Error"]["Message"],
+ )
+ logger.error(
+ "Couldn't enable baseline. Here's why: %s: %s",
+ err.response["Error"]["Code"],
+ err.response["Error"]["Message"],
+ )
+ raise
+
+ # snippet-end:[python.example_code.controltower.EnableBaseline]
+
+ # snippet-start:[python.example_code.controltower.ListControls]
+ def list_controls(self):
+ """
+ Lists all controls in the Control Tower control catalog.
+
+ :return: List of controls.
+ :raises ClientError: If the listing operation fails.
+ """
+ try:
+ paginator = self.controlcatalog_client.get_paginator("list_controls")
+ controls = []
+ for page in paginator.paginate():
+ controls.extend(page["Controls"])
+ return controls
+
+ except ClientError as err:
+ if err.response["Error"]["Code"] == "AccessDeniedException":
+ logger.error(
+ "Access denied. Please ensure you have the necessary permissions."
+ )
+ else:
+ logger.error(
+ "Couldn't list controls. Here's why: %s: %s",
+ err.response["Error"]["Code"],
+ err.response["Error"]["Message"],
+ )
+ raise
+
+ # snippet-end:[python.example_code.controltower.ListControls]
+
+ # snippet-start:[python.example_code.controltower.EnableControl]
+ def enable_control(self, control_arn, target_identifier):
+ """
+ Enables a control for a specified target.
+
+ :param control_arn: The ARN of the control to enable.
+ :param target_identifier: The identifier of the target (e.g., OU ARN).
+ :return: The operation ID.
+ :raises ClientError: If enabling the control fails.
+ """
+ try:
+ print(control_arn)
+ print(target_identifier)
+ response = self.controltower_client.enable_control(
+ controlIdentifier=control_arn, targetIdentifier=target_identifier
+ )
+
+ operation_id = response["operationIdentifier"]
+ while True:
+ status = self.get_control_operation(operation_id)
+ print(f"Control operation status: {status}")
+ if status in ["SUCCEEDED", "FAILED"]:
+ break
+ time.sleep(30)
+
+ return operation_id
+
+ except ClientError as err:
+ if (
+ err.response["Error"]["Code"] == "ValidationException"
+ and "already enabled" in err.response["Error"]["Message"]
+ ):
+ logger.info("Control is already enabled for this target")
+ return None
+ logger.error(
+ "Couldn't enable control. Here's why: %s: %s",
+ err.response["Error"]["Code"],
+ err.response["Error"]["Message"],
+ )
+ raise
+
+ # snippet-end:[python.example_code.controltower.EnableControl]
+
+ # snippet-start:[python.example_code.controltower.GetControlOperation]
+ def get_control_operation(self, operation_id):
+ """
+ Gets the status of a control operation.
+
+ :param operation_id: The ID of the control operation.
+ :return: The operation status.
+ :raises ClientError: If getting the operation status fails.
+ """
+ try:
+ response = self.controltower_client.get_control_operation(
+ operationIdentifier=operation_id
+ )
+ return response["controlOperation"]["status"]
+ except ClientError as err:
+ if err.response["Error"]["Code"] == "ResourceNotFoundException":
+ logger.error("Operation not found.")
+ else:
+ logger.error(
+ "Couldn't get control operation status. Here's why: %s: %s",
+ err.response["Error"]["Code"],
+ err.response["Error"]["Message"],
+ )
+ raise
+
+ # snippet-end:[python.example_code.controltower.GetControlOperation]
+
+ # snippet-start:[python.example_code.controltower.GetBaselineOperation]
+ def get_baseline_operation(self, operation_id):
+ """
+ Gets the status of a baseline operation.
+
+ :param operation_id: The ID of the baseline operation.
+ :return: The operation status.
+ :raises ClientError: If getting the operation status fails.
+ """
+ try:
+ response = self.controltower_client.get_baseline_operation(
+ operationIdentifier=operation_id
+ )
+ return response["baselineOperation"]["status"]
+ except ClientError as err:
+ if err.response["Error"]["Code"] == "ResourceNotFoundException":
+ logger.error("Operation not found.")
+ else:
+ logger.error(
+ "Couldn't get baseline operation status. Here's why: %s: %s",
+ err.response["Error"]["Code"],
+ err.response["Error"]["Message"],
+ )
+ raise
+
+ # snippet-end:[python.example_code.controltower.GetBaselineOperation]
+
+ # snippet-start:[python.example_code.controltower.DisableControl]
+ def disable_control(self, control_arn, target_identifier):
+ """
+ Disables a control for a specified target.
+
+ :param control_arn: The ARN of the control to disable.
+ :param target_identifier: The identifier of the target (e.g., OU ARN).
+ :return: The operation ID.
+ :raises ClientError: If disabling the control fails.
+ """
+ try:
+ response = self.controltower_client.disable_control(
+ controlIdentifier=control_arn, targetIdentifier=target_identifier
+ )
+
+ operation_id = response["operationIdentifier"]
+ while True:
+ status = self.get_control_operation(operation_id)
+ print(f"Control operation status: {status}")
+ if status in ["SUCCEEDED", "FAILED"]:
+ break
+ time.sleep(30)
+
+ return operation_id
+ except ClientError as err:
+ if err.response["Error"]["Code"] == "ResourceNotFoundException":
+ logger.error("Control not found.")
+ else:
+ logger.error(
+ "Couldn't disable control. Here's why: %s: %s",
+ err.response["Error"]["Code"],
+ err.response["Error"]["Message"],
+ )
+ raise
+
+ # snippet-end:[python.example_code.controltower.DisableControl]
+
+ # snippet-start:[python.example_code.controltower.ListLandingZones]
+ def list_landing_zones(self):
+ """
+ Lists all landing zones.
+
+ :return: List of landing zones.
+ :raises ClientError: If the listing operation fails.
+ """
+ try:
+ paginator = self.controltower_client.get_paginator("list_landing_zones")
+ landing_zones = []
+ for page in paginator.paginate():
+ landing_zones.extend(page["landingZones"])
+ return landing_zones
+
+ except ClientError as err:
+ if err.response["Error"]["Code"] == "AccessDeniedException":
+ logger.error(
+ "Access denied. Please ensure you have the necessary permissions."
+ )
+ else:
+ logger.error(
+ "Couldn't list landing zones. Here's why: %s: %s",
+ err.response["Error"]["Code"],
+ err.response["Error"]["Message"],
+ )
+ raise
+
+ # snippet-end:[python.example_code.controltower.ListLandingZones]
+
+ # snippet-start:[python.example_code.controltower.ListEnabledBaselines]
+ def list_enabled_baselines(self):
+ """
+ Lists all enabled baselines.
+
+ :return: List of enabled baselines.
+ :raises ClientError: If the listing operation fails.
+ """
+ try:
+ paginator = self.controltower_client.get_paginator("list_enabled_baselines")
+ enabled_baselines = []
+ for page in paginator.paginate():
+ enabled_baselines.extend(page["enabledBaselines"])
+ return enabled_baselines
+
+ except ClientError as err:
+ if err.response["Error"]["Code"] == "ResourceNotFoundException":
+ logger.error("Target not found.")
+ else:
+ logger.error(
+ "Couldn't list enabled baselines. Here's why: %s: %s",
+ err.response["Error"]["Code"],
+ err.response["Error"]["Message"],
+ )
+ raise
+
+ # snippet-end:[python.example_code.controltower.ListEnabledBaselines]
+
+ # snippet-start:[python.example_code.controltower.ResetEnabledBaseline]
+ def reset_enabled_baseline(self, enabled_baseline_identifier):
+ """
+ Resets an enabled baseline for a specific target.
+
+ :param enabled_baseline_identifier: The identifier of the enabled baseline to reset.
+ :return: The operation ID.
+ :raises ClientError: If resetting the baseline fails.
+ """
+ try:
+ response = self.controltower_client.reset_enabled_baseline(
+ enabledBaselineIdentifier=enabled_baseline_identifier
+ )
+ operation_id = response["operationIdentifier"]
+ while True:
+ status = self.get_baseline_operation(operation_id)
+ print(f"Baseline operation status: {status}")
+ if status in ["SUCCEEDED", "FAILED"]:
+ break
+ time.sleep(30)
+ return operation_id
+ except ClientError as err:
+ if err.response["Error"]["Code"] == "ResourceNotFoundException":
+ logger.error("Target not found.")
+ else:
+ logger.error(
+ "Couldn't reset enabled baseline. Here's why: %s: %s",
+ err.response["Error"]["Code"],
+ err.response["Error"]["Message"],
+ )
+ raise
+
+ # snippet-end:[python.example_code.controltower.ResetEnabledBaseline]
+
+ # snippet-start:[python.example_code.controltower.DisableBaseline]
+ def disable_baseline(self, enabled_baseline_identifier):
+ """
+ Disables a baseline for a specific target and waits for the operation to complete.
+
+ :param enabled_baseline_identifier: The identifier of the baseline to disable.
+ :return: The operation ID.
+ :raises ClientError: If disabling the baseline fails.
+ """
+ try:
+ response = self.controltower_client.disable_baseline(
+ enabledBaselineIdentifier=enabled_baseline_identifier
+ )
+
+ operation_id = response["operationIdentifier"]
+ while True:
+ status = self.get_baseline_operation(operation_id)
+ print(f"Baseline operation status: {status}")
+ if status in ["SUCCEEDED", "FAILED"]:
+ break
+ time.sleep(30)
+
+ return response["operationIdentifier"]
+ except ClientError as err:
+ if err.response["Error"]["Code"] == "ConflictException":
+ print(
+ f"Conflict disabling baseline: {err.response['Error']['Message']}. Skipping disable step."
+ )
+ return None
+ else:
+ logger.error(
+ "Couldn't disable baseline. Here's why: %s: %s",
+ err.response["Error"]["Code"],
+ err.response["Error"]["Message"],
+ )
+ raise
+
+ # snippet-end:[python.example_code.controltower.DisableBaseline]
+
+ # snippet-start:[python.example_code.controltower.ListEnabledControls]
+ def list_enabled_controls(self, target_identifier):
+ """
+ Lists all enabled controls for a specific target.
+
+ :param target_identifier: The identifier of the target (e.g., OU ARN).
+ :return: List of enabled controls.
+ :raises ClientError: If the listing operation fails.
+ """
+ try:
+ paginator = self.controltower_client.get_paginator("list_enabled_controls")
+ enabled_controls = []
+ for page in paginator.paginate(targetIdentifier=target_identifier):
+ enabled_controls.extend(page["enabledControls"])
+ return enabled_controls
+
+ except ClientError as err:
+ if err.response["Error"]["Code"] == "AccessDeniedException":
+ logger.error(
+ "Access denied. Please ensure you have the necessary permissions."
+ )
+ else:
+ logger.error(
+ "Couldn't list enabled controls. Here's why: %s: %s",
+ err.response["Error"]["Code"],
+ err.response["Error"]["Message"],
+ )
+ raise
+
+ # snippet-end:[python.example_code.controltower.ListEnabledControls]
+
+
+# snippet-end:[python.example_code.controltower.ControlTowerWrapper.class]
diff --git a/python/example_code/controltower/hello/hello_controltower.py b/python/example_code/controltower/hello/hello_controltower.py
new file mode 100644
index 00000000000..6a4d0dc3c0e
--- /dev/null
+++ b/python/example_code/controltower/hello/hello_controltower.py
@@ -0,0 +1,40 @@
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# snippet-start:[python.example_code.controltower.Hello]
+import boto3
+
+
+def hello_controltower(controltower_client):
+ """
+ Use the AWS SDK for Python (Boto3) to create an AWS Control Tower client
+ and list all available baselines.
+ This example uses the default settings specified in your shared credentials
+ and config files.
+
+ :param controltower_client: A Boto3 AWS Control Tower Client object. This object wraps
+ the low-level AWS Control Tower service API.
+ """
+ print("Hello, AWS Control Tower! Let's list available baselines:\n")
+ paginator = controltower_client.get_paginator("list_baselines")
+ page_iterator = paginator.paginate()
+
+ baseline_names: [str] = []
+ try:
+ for page in page_iterator:
+ for baseline in page["baselines"]:
+ baseline_names.append(baseline["name"])
+
+ print(f"{len(baseline_names)} baseline(s) retrieved.")
+ for baseline_name in baseline_names:
+ print(f"\t{baseline_name}")
+
+ except controltower_client.exceptions.AccessDeniedException:
+ print("Access denied. Please ensure you have the necessary permissions.")
+ except Exception as e:
+ print(f"An error occurred: {str(e)}")
+
+
+if __name__ == "__main__":
+ hello_controltower(boto3.client("controltower"))
+# snippet-end:[python.example_code.controltower.Hello]
diff --git a/python/example_code/controltower/requirements.txt b/python/example_code/controltower/requirements.txt
new file mode 100644
index 00000000000..e74f0c584b9
--- /dev/null
+++ b/python/example_code/controltower/requirements.txt
@@ -0,0 +1,4 @@
+boto3>=1.26.79
+pytest>=7.2.1
+qrcode>=7.4.2
+pycognito>=2022.12.0
diff --git a/python/example_code/controltower/scenario_controltower.py b/python/example_code/controltower/scenario_controltower.py
new file mode 100644
index 00000000000..83804892c52
--- /dev/null
+++ b/python/example_code/controltower/scenario_controltower.py
@@ -0,0 +1,315 @@
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+
+import logging
+import sys
+import time
+
+import boto3
+from botocore.exceptions import ClientError
+
+from controltower_wrapper import ControlTowerWrapper
+
+# Add relative path to include demo_tools in this code example without need for setup.
+sys.path.append("../..")
+import demo_tools.question as q # noqa
+
+logger = logging.getLogger(__name__)
+
+
+# snippet-start:[python.example_code.controltower.ControlTowerScenario]
+class ControlTowerScenario:
+ stack_name = ""
+
+ def __init__(self, controltower_wrapper, org_client):
+ """
+ :param controltower_wrapper: An instance of the ControlTowerWrapper class.
+ :param org_client: A Boto3 Organization client.
+ """
+ self.controltower_wrapper = controltower_wrapper
+ self.org_client = org_client
+ self.stack = None
+ self.ou_id = None
+ self.ou_arn = None
+ self.account_id = None
+ self.landing_zone_id = None
+ self.use_landing_zone = False
+
+ def run_scenario(self):
+ print("-" * 88)
+ print(
+ "\tWelcome to the AWS Control Tower with ControlCatalog example scenario."
+ )
+ print("-" * 88)
+
+ print(
+ "This demo will walk you through working with AWS Control Tower for landing zones,"
+ )
+ print("managing baselines, and working with controls.")
+
+ self.account_id = boto3.client("sts").get_caller_identity()["Account"]
+
+ print(
+ "Some demo operations require the use of a landing zone. "
+ "\nYou can use an existing landing zone or opt out of these operations in the demo."
+ "\nFor instructions on how to set up a landing zone, "
+ "\nsee https://docs.aws.amazon.com/controltower/latest/userguide/getting-started-from-console.html"
+ )
+ # List available landing zones
+ landing_zones = self.controltower_wrapper.list_landing_zones()
+ if landing_zones:
+ print("\nAvailable Landing Zones:")
+ for i, lz in enumerate(landing_zones, 1):
+ print(f"{i} {lz['arn']})")
+
+ # Ask if user wants to use the first landing zone in the list
+ if q.ask(
+ f"Do you want to use the first landing zone in the list ({landing_zones[0]['arn']})? (y/n) ",
+ q.is_yesno,
+ ):
+ self.use_landing_zone = True
+ self.landing_zone_id = landing_zones[0]["arn"]
+ print(f"Using landing zone ID: {self.landing_zone_id})")
+ # Set up organization and get Sandbox OU ID.
+ sandbox_ou_id = self.setup_organization()
+ # Store the OU ID for use in the CloudFormation template.
+ self.ou_id = sandbox_ou_id
+ elif q.ask(
+ f"Do you want to use a different existing Landing Zone for this demo? (y/n) ",
+ q.is_yesno,
+ ):
+ self.use_landing_zone = True
+ self.landing_zone_id = q.ask("Enter landing zone id: ", q.non_empty)
+ # Set up organization and get Sandbox OU ID.
+ sandbox_ou_id = self.setup_organization()
+ # Store the OU ID for use in the CloudFormation template.
+ self.ou_id = sandbox_ou_id
+
+ # List and Enable Baseline.
+ print("\nManaging Baselines:")
+ control_tower_baseline = None
+ identity_center_baseline = None
+ baselines = self.controltower_wrapper.list_baselines()
+ print("\nListing available Baselines:")
+ for baseline in baselines:
+ if baseline["name"] == "AWSControlTowerBaseline":
+ control_tower_baseline = baseline
+ print(f"{baseline['name']}")
+
+ if self.use_landing_zone:
+ print("\nListing enabled baselines:")
+ enabled_baselines = self.controltower_wrapper.list_enabled_baselines()
+ for baseline in enabled_baselines:
+ # If the Identity Center baseline is enabled, the identifier must be used for other baselines.
+ if "baseline/LN25R72TTG6IGPTQ" in baseline["baselineIdentifier"]:
+ identity_center_baseline = baseline
+ print(f"{baseline['baselineIdentifier']}")
+
+ if q.ask(
+ f"Do you want to enable the Control Tower Baseline? (y/n) ",
+ q.is_yesno,
+ ):
+ print("\nEnabling Control Tower Baseline.")
+ ic_baseline_arn = (
+ identity_center_baseline["arn"]
+ if identity_center_baseline
+ else None
+ )
+ baseline_arn = self.controltower_wrapper.enable_baseline(
+ self.ou_arn, ic_baseline_arn, control_tower_baseline["arn"], "4.0"
+ )
+ if baseline_arn:
+ print(f"Enabled baseline ARN: {baseline_arn}")
+ else:
+ # Find the enabled baseline so we can reset it.
+ for enabled_baseline in enabled_baselines:
+ if (
+ enabled_baseline["baselineIdentifier"]
+ == control_tower_baseline["arn"]
+ ):
+ baseline_arn = enabled_baseline["arn"]
+ print("No change, the selected baseline was already enabled.")
+
+ if q.ask(
+ f"Do you want to reset the Control Tower Baseline? (y/n) ",
+ q.is_yesno,
+ ):
+ print(f"\nResetting Control Tower Baseline. {baseline_arn}")
+ operation_id = self.controltower_wrapper.reset_enabled_baseline(
+ baseline_arn
+ )
+ print(f"\nReset baseline operation id {operation_id}.")
+
+ if baseline_arn and q.ask(
+ f"Do you want to disable the Control Tower Baseline? (y/n) ",
+ q.is_yesno,
+ ):
+ print(f"Disabling baseline ARN: {baseline_arn}")
+ operation_id = self.controltower_wrapper.disable_baseline(
+ baseline_arn
+ )
+ print(f"\nDisabled baseline operation id {operation_id}.")
+
+ # List and Enable Controls.
+ print("\nManaging Controls:")
+ controls = self.controltower_wrapper.list_controls()
+ print("\nListing first 5 available Controls:")
+ for i, control in enumerate(controls[:5], 1):
+ print(f"{i}. {control['Name']} - {control['Arn']}")
+
+ if self.use_landing_zone:
+ target_ou = self.ou_arn
+ enabled_controls = self.controltower_wrapper.list_enabled_controls(
+ target_ou
+ )
+ print("\nListing enabled controls:")
+ for i, control in enumerate(enabled_controls, 1):
+ print(f"{i}. {control['controlIdentifier']}")
+
+ # Enable first non-enabled control as an example.
+ enabled_control_arns = [control["arn"] for control in enabled_controls]
+ control_arn = next(
+ control["Arn"]
+ for control in controls
+ if control["Arn"] not in enabled_control_arns
+ )
+
+ if control_arn and q.ask(
+ f"Do you want to enable the control {control_arn}? (y/n) ",
+ q.is_yesno,
+ ):
+ print(f"\nEnabling control: {control_arn}")
+ operation_id = self.controltower_wrapper.enable_control(
+ control_arn, target_ou
+ )
+
+ if operation_id:
+ print(f"Enabled control with operation id {operation_id}")
+ else:
+ print("Control is already enabled for this target")
+
+ if q.ask(
+ f"Do you want to disable the control? (y/n) ",
+ q.is_yesno,
+ ):
+ print("\nDisabling the control...")
+ operation_id = self.controltower_wrapper.disable_control(
+ control_arn, target_ou
+ )
+ print(f"Disable operation ID: {operation_id}")
+
+ print("\nThis concludes the example scenario.")
+
+ print("Thanks for watching!")
+ print("-" * 88)
+
+ def setup_organization(self):
+ """
+ Checks if the current account is part of an organization and creates one if needed.
+ Also ensures a Sandbox OU exists and returns its ID.
+
+ :return: The ID of the Sandbox OU
+ """
+ print("\nChecking organization status...")
+
+ try:
+ # Check if account is part of an organization
+ org_response = self.org_client.describe_organization()
+ org_id = org_response["Organization"]["Id"]
+ print(f"Account is part of organization: {org_id}")
+
+ except ClientError as error:
+ if error.response["Error"]["Code"] == "AWSOrganizationsNotInUseException":
+ print("No organization found. Creating a new organization...")
+ try:
+ create_response = self.org_client.create_organization(
+ FeatureSet="ALL"
+ )
+ org_id = create_response["Organization"]["Id"]
+ print(f"Created new organization: {org_id}")
+
+ # Wait for organization to be available.
+ waiter = self.org_client.get_waiter("organization_active")
+ waiter.wait(
+ Organization=org_id,
+ WaiterConfig={"Delay": 5, "MaxAttempts": 12},
+ )
+
+ except ClientError as create_error:
+ logger.error(
+ "Couldn't create organization. Here's why: %s: %s",
+ create_error.response["Error"]["Code"],
+ create_error.response["Error"]["Message"],
+ )
+ raise
+ else:
+ logger.error(
+ "Couldn't describe organization. Here's why: %s: %s",
+ error.response["Error"]["Code"],
+ error.response["Error"]["Message"],
+ )
+ raise
+
+ # Look for Sandbox OU.
+ sandbox_ou_id = None
+ paginator = self.org_client.get_paginator(
+ "list_organizational_units_for_parent"
+ )
+
+ try:
+ # Get root ID first.
+ roots = self.org_client.list_roots()["Roots"]
+ if not roots:
+ raise ValueError("No root found in organization")
+ root_id = roots[0]["Id"]
+
+ # Search for existing Sandbox OU.
+ print("Checking for Sandbox OU...")
+ for page in paginator.paginate(ParentId=root_id):
+ for ou in page["OrganizationalUnits"]:
+ if ou["Name"] == "Sandbox":
+ sandbox_ou_id = ou["Id"]
+ self.ou_arn = ou["Arn"]
+ print(f"Found existing Sandbox OU: {sandbox_ou_id}")
+ break
+ if sandbox_ou_id:
+ break
+
+ # Create Sandbox OU if it doesn't exist.
+ if not sandbox_ou_id:
+ print("Creating Sandbox OU...")
+ create_ou_response = self.org_client.create_organizational_unit(
+ ParentId=root_id, Name="Sandbox"
+ )
+ sandbox_ou_id = create_ou_response["OrganizationalUnit"]["Id"]
+ print(f"Created new Sandbox OU: {sandbox_ou_id}")
+
+ # Wait for OU to be available.
+ waiter = self.org_client.get_waiter("organizational_unit_active")
+ waiter.wait(
+ OrganizationalUnitId=sandbox_ou_id,
+ WaiterConfig={"Delay": 5, "MaxAttempts": 12},
+ )
+
+ except ClientError as error:
+ logger.error(
+ "Couldn't set up Sandbox OU. Here's why: %s: %s",
+ error.response["Error"]["Code"],
+ error.response["Error"]["Message"],
+ )
+ raise
+
+ return sandbox_ou_id
+
+
+if __name__ == "__main__":
+ try:
+ org = boto3.client("organizations")
+ control_tower_wrapper = ControlTowerWrapper.from_client()
+
+ scenario = ControlTowerScenario(control_tower_wrapper, org)
+ scenario.run_scenario()
+ except Exception:
+ logging.exception("Something went wrong with the scenario.")
+# snippet-end:[python.example_code.controltower.ControlTowerScenario]
diff --git a/python/example_code/controltower/test/conftest.py b/python/example_code/controltower/test/conftest.py
new file mode 100644
index 00000000000..7307c5c8313
--- /dev/null
+++ b/python/example_code/controltower/test/conftest.py
@@ -0,0 +1,73 @@
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+Contains common test fixtures used to run unit tests.
+"""
+
+import sys
+import os
+import boto3
+import pytest
+
+script_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Add relative path to include ControlTowerWrapper.
+sys.path.append(script_dir)
+sys.path.append(os.path.dirname(script_dir))
+import scenario_controltower
+from controltower_wrapper import ControlTowerWrapper
+
+# Add relative path to include demo_tools in this code example without need for setup.
+sys.path.append(os.path.join(script_dir, "../.."))
+
+from test_tools.fixtures.common import *
+
+
+class ScenarioData:
+ def __init__(
+ self,
+ controltower_client,
+ controlcatalog_client,
+ organizations_client,
+ controltower_stubber,
+ controlcatalog_stubber,
+ organizations_stubber,
+ ):
+ self.controltower_client = controltower_client
+ self.controlcatalog_client = controlcatalog_client
+ self.organizations_client = organizations_client
+ self.controltower_stubber = controltower_stubber
+ self.controlcatalog_stubber = controlcatalog_stubber
+ self.organizations_stubber = organizations_stubber
+ self.scenario = scenario_controltower.ControlTowerScenario(
+ controltower_wrapper=ControlTowerWrapper(
+ self.controltower_client, self.controlcatalog_client
+ ),
+ org_client=self.organizations_client,
+ )
+
+
+@pytest.fixture
+def scenario_data(make_stubber):
+ controltower_client = boto3.client("controltower")
+ controlcatalog_client = boto3.client("controlcatalog")
+ organizations_client = boto3.client("organizations")
+
+ controltower_stubber = make_stubber(controltower_client)
+ controlcatalog_stubber = make_stubber(controlcatalog_client)
+ organizations_stubber = make_stubber(organizations_client)
+
+ return ScenarioData(
+ controltower_client,
+ controlcatalog_client,
+ organizations_client,
+ controltower_stubber,
+ controlcatalog_stubber,
+ organizations_stubber,
+ )
+
+
+@pytest.fixture
+def mock_wait(monkeypatch):
+ return
diff --git a/python/example_code/controltower/test/test_scenario_run.py b/python/example_code/controltower/test/test_scenario_run.py
new file mode 100644
index 00000000000..5b54e68809d
--- /dev/null
+++ b/python/example_code/controltower/test/test_scenario_run.py
@@ -0,0 +1,266 @@
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+Unit tests for the run_scenario method in scenario_controltower.py.
+"""
+
+import pytest
+from botocore.exceptions import ClientError
+import datetime
+import boto3
+
+from example_code.controltower.controltower_wrapper import ControlTowerWrapper
+from example_code.controltower.scenario_controltower import ControlTowerScenario
+
+
+class MockManager:
+ def __init__(self, stub_runner, scenario_data, input_mocker):
+ self.scenario_data = scenario_data
+ self.account_id = "123456789012"
+ self.org_id = "o-exampleorgid"
+ self.root_id = "r-examplerootid"
+ self.sandbox_ou_id = "ou-exampleouid123456"
+ self.sandbox_ou_arn = (
+ "arn:aws:organizations::123456789012:ou/o-exampleorgid/ou-exampleouid"
+ )
+ self.landing_zone_arn = (
+ "arn:aws:controltower:us-east-1:123456789012:landingzone/lz-example"
+ )
+ self.operation_id = "op-1234567890abcdef01234567890abcdef"
+ self.baseline_operation_id = "op-1234567890abcdef01234567890abcdef"
+ self.stack_id = (
+ "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abcdef"
+ )
+ self.baseline_arn = "arn:aws:controltower:us-east-1:123456789012:baseline/AWSControlTowerBaseline"
+ self.enabled_baseline_arn = "arn:aws:controltower:us-east-1:123456789012:baseline/AWSControlTowerBaseline/isenabled"
+ self.control_arn = (
+ "arn:aws:controlcatalog:us-east-1:123456789012:control/aws-control-1234"
+ )
+ self.control_arn_enabled = (
+ "arn:aws:controlcatalog:us-east-1:123456789012:control/aws-control-5678"
+ )
+
+ self.landing_zones = [{"arn": self.landing_zone_arn}]
+
+ self.baselines = [{"name": "AWSControlTowerBaseline", "arn": self.baseline_arn}]
+
+ self.enabled_baselines = [
+ {
+ "targetIdentifier": self.sandbox_ou_arn,
+ "baselineIdentifier": self.enabled_baseline_arn,
+ "arn": self.baseline_arn,
+ "statusSummary": {
+ "status": "SUCCEEDED",
+ "lastOperationIdentifier": self.baseline_operation_id,
+ },
+ }
+ ]
+
+ self.controls = [
+ {
+ "Arn": self.control_arn,
+ "Name": "TestControl1",
+ "Description": "Test control description",
+ }
+ ]
+
+ self.enabled_controls = [
+ {
+ "arn": self.control_arn_enabled,
+ "controlIdentifier": self.control_arn_enabled,
+ "statusSummary": {
+ "status": "SUCCEEDED",
+ "lastOperationIdentifier": self.baseline_operation_id,
+ },
+ "targetIdentifier": self.sandbox_ou_id,
+ }
+ ]
+
+ self.stub_runner = stub_runner
+ self.input_mocker = input_mocker
+
+ def setup_stubs(self, error, stop_on, monkeypatch):
+ """Setup stubs for the scenario"""
+ # Mock user inputs
+ answers = [
+ "y", # Use first landing zone in the list.
+ "y", # Enable baseline.
+ "y", # Reset baseline.
+ "y", # Disable baseline.
+ "y", # Enable control.
+ "y", # Disable control.
+ ]
+ self.input_mocker.mock_answers(answers)
+
+ # Mock STS get_caller_identity
+ def mock_get_caller_identity():
+ return {"Account": self.account_id}
+
+ monkeypatch.setattr(
+ boto3.client("sts"), "get_caller_identity", mock_get_caller_identity
+ )
+
+ with self.stub_runner(error, stop_on) as runner:
+ # List landing zones
+ runner.add(
+ self.scenario_data.controltower_stubber.stub_list_landing_zones,
+ self.landing_zones,
+ )
+
+ # Organization setup
+ runner.add(
+ self.scenario_data.organizations_stubber.stub_describe_organization,
+ self.org_id,
+ )
+ runner.add(
+ self.scenario_data.organizations_stubber.stub_list_roots,
+ [{"Id": self.root_id, "Name": "Root"}],
+ )
+ runner.add(
+ self.scenario_data.organizations_stubber.stub_list_organizational_units_for_parent,
+ self.root_id,
+ [
+ {
+ "Id": self.sandbox_ou_id,
+ "Name": "Sandbox",
+ "Arn": self.sandbox_ou_arn,
+ }
+ ],
+ )
+
+ # List and enable baselines
+ runner.add(
+ self.scenario_data.controltower_stubber.stub_list_baselines,
+ self.baselines,
+ )
+ runner.add(
+ self.scenario_data.controltower_stubber.stub_list_enabled_baselines,
+ self.enabled_baselines,
+ )
+ runner.add(
+ self.scenario_data.controltower_stubber.stub_enable_baseline,
+ self.baseline_arn,
+ "4.0",
+ self.sandbox_ou_arn,
+ self.enabled_baseline_arn,
+ self.baseline_operation_id,
+ )
+ runner.add(
+ self.scenario_data.controltower_stubber.stub_get_baseline_operation,
+ self.baseline_operation_id,
+ "SUCCEEDED",
+ )
+ runner.add(
+ self.scenario_data.controltower_stubber.stub_reset_enabled_baseline,
+ self.enabled_baseline_arn,
+ self.baseline_operation_id,
+ )
+ runner.add(
+ self.scenario_data.controltower_stubber.stub_get_baseline_operation,
+ self.baseline_operation_id,
+ "SUCCEEDED",
+ )
+ runner.add(
+ self.scenario_data.controltower_stubber.stub_disable_baseline,
+ self.enabled_baseline_arn,
+ self.baseline_operation_id,
+ )
+ runner.add(
+ self.scenario_data.controltower_stubber.stub_get_baseline_operation,
+ self.baseline_operation_id,
+ "SUCCEEDED",
+ )
+
+ # List and enable controls
+ runner.add(
+ self.scenario_data.controlcatalog_stubber.stub_list_controls,
+ self.controls,
+ )
+ runner.add(
+ self.scenario_data.controltower_stubber.stub_list_enabled_controls,
+ self.sandbox_ou_arn,
+ self.enabled_controls,
+ )
+ runner.add(
+ self.scenario_data.controltower_stubber.stub_enable_control,
+ self.control_arn,
+ self.sandbox_ou_arn,
+ self.operation_id,
+ )
+ runner.add(
+ self.scenario_data.controltower_stubber.stub_get_control_operation,
+ self.operation_id,
+ "SUCCEEDED",
+ )
+ runner.add(
+ self.scenario_data.controltower_stubber.stub_disable_control,
+ self.control_arn,
+ self.sandbox_ou_arn,
+ self.operation_id,
+ )
+ runner.add(
+ self.scenario_data.controltower_stubber.stub_get_control_operation,
+ self.operation_id,
+ "SUCCEEDED",
+ )
+
+ def setup_integ(self, error, stop_on):
+ """Set up the scenario for an integration test."""
+ # Mock user inputs for using the suggested landing zone
+ answers = [
+ "n", # Use first landing zone in the list.
+ "n", # Enable baseline.
+ ]
+ self.stub_runner = None
+ self.input_mocker.mock_answers(answers)
+
+
+@pytest.fixture
+def mock_mgr(stub_runner, scenario_data, input_mocker):
+ return MockManager(stub_runner, scenario_data, input_mocker)
+
+
+# Define ANY constant for template body matching
+ANY = object()
+
+
+def test_run_scenario(mock_mgr, capsys, monkeypatch):
+ """Test the scenario that uses the suggested landing zone."""
+ mock_mgr.setup_stubs(None, None, monkeypatch)
+
+ # Run the scenario
+ mock_mgr.scenario_data
+ mock_mgr.scenario_data.scenario.run_scenario()
+
+ # Verify the scenario completed successfully
+ captured = capsys.readouterr()
+ assert "This concludes the example scenario." in captured.out
+
+
+@pytest.mark.integ
+def test_run_scenario_integ(input_mocker, capsys):
+ """Test the scenario with an integration test."""
+ answers = [
+ "n", # Run the sections that don't require a landing zone.
+ "n",
+ ]
+
+ input_mocker.mock_answers(answers)
+ controltower_client = boto3.client("controltower")
+ controlcatalog_client = boto3.client("controlcatalog")
+ organizations_client = boto3.client("organizations")
+
+ scenario = ControlTowerScenario(
+ controltower_wrapper=ControlTowerWrapper(
+ controltower_client, controlcatalog_client
+ ),
+ org_client=organizations_client,
+ )
+
+ # Run the scenario
+ scenario.run_scenario()
+
+ # Verify the scenario completed successfully
+ captured = capsys.readouterr()
+ assert "This concludes the example scenario." in captured.out
From df6842738930ff852b31ec6b3d71287f612569d0 Mon Sep 17 00:00:00 2001
From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com>
Date: Wed, 25 Jun 2025 13:36:28 -0500
Subject: [PATCH 3/7] Update tests.
---
.../Actions/ControlTowerWrapper.cs | 2 +
.../ControlTower_Basics/ControlTowerBasics.cs | 60 +++--
.../Tests/ControlTowerBasicsTests.cs | 218 +++++++++++++++---
.../Tests/ControlTowerTests.csproj | 1 +
4 files changed, 228 insertions(+), 53 deletions(-)
diff --git a/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs
index 2121133689d..8024080b2a4 100644
--- a/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs
+++ b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs
@@ -3,6 +3,8 @@
// snippet-start:[ControlTower.dotnetv4.ControlTowerWrapper]
+using ValidationException = Amazon.ControlTower.Model.ValidationException;
+
namespace ControlTowerActions;
///
diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs
index 5a8640b89fa..b72e684272b 100644
--- a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs
+++ b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs
@@ -20,12 +20,19 @@ namespace ControlTowerBasics;
///
public class ControlTowerBasics
{
- private static ILogger logger = null!;
- private static IAmazonOrganizations orgClient = null!;
+ public static bool isInteractive = true;
+ public static ILogger logger = null!;
+ public static IAmazonOrganizations? orgClient = null;
+ public static IAmazonSecurityTokenService? stsClient = null;
+ public static ControlTowerWrapper? wrapper = null;
private static string? ouArn;
private static bool useLandingZone = false;
- static async Task Main(string[] args)
+ ///
+ /// Main entry point for the AWS Control Tower basics scenario.
+ ///
+ /// Command line arguments.
+ public static async Task Main(string[] args)
{
using var host = Host.CreateDefaultBuilder(args)
.ConfigureServices((_, services) =>
@@ -40,10 +47,18 @@ static async Task Main(string[] args)
logger = LoggerFactory.Create(builder => { builder.AddConsole(); })
.CreateLogger();
- var wrapper = host.Services.GetRequiredService();
- orgClient = host.Services.GetRequiredService();
- var stsClient = host.Services.GetRequiredService();
+ wrapper ??= host.Services.GetRequiredService();
+ orgClient ??= host.Services.GetRequiredService();
+ stsClient ??= host.Services.GetRequiredService();
+ await RunScenario();
+ }
+
+ ///
+ /// Runs the example scenario.
+ ///
+ public static async Task RunScenario()
+ {
Console.WriteLine(new string('-', 88));
Console.WriteLine("\tWelcome to the AWS Control Tower with ControlCatalog example scenario.");
Console.WriteLine(new string('-', 88));
@@ -71,7 +86,7 @@ static async Task Main(string[] args)
}
Console.Write($"\nDo you want to use the first landing zone in the list ({landingZones[0].Arn})? (y/n): ");
- if (Console.ReadLine()?.ToLower() == "y")
+ if (GetUserConfirmation())
{
useLandingZone = true;
Console.WriteLine($"Using landing zone: {landingZones[0].Arn}");
@@ -108,7 +123,7 @@ static async Task Main(string[] args)
if (controlTowerBaseline != null)
{
Console.Write("\nDo you want to enable the Control Tower Baseline? (y/n): ");
- if (Console.ReadLine()?.ToLower() == "y")
+ if (GetUserConfirmation())
{
Console.WriteLine("\nEnabling Control Tower Baseline.");
var icBaselineArn = identityCenterBaseline?.Arn;
@@ -138,7 +153,7 @@ static async Task Main(string[] args)
if (baselineArn != null)
{
Console.Write("\nDo you want to reset the Control Tower Baseline? (y/n): ");
- if (Console.ReadLine()?.ToLower() == "y")
+ if (GetUserConfirmation())
{
Console.WriteLine($"\nResetting Control Tower Baseline: {baselineArn}");
var operationId = await wrapper.ResetEnabledBaselineAsync(baselineArn);
@@ -146,7 +161,7 @@ static async Task Main(string[] args)
}
Console.Write("\nDo you want to disable the Control Tower Baseline? (y/n): ");
- if (Console.ReadLine()?.ToLower() == "y")
+ if (GetUserConfirmation())
{
Console.WriteLine($"Disabling baseline ARN: {baselineArn}");
var operationId = await wrapper.DisableBaselineAsync(baselineArn);
@@ -189,16 +204,16 @@ await wrapper.EnableBaselineAsync(ouArn,
if (controlArn != null)
{
Console.Write($"\nDo you want to enable the control {controlArn}? (y/n): ");
- if (Console.ReadLine()?.ToLower() == "y")
+ if (GetUserConfirmation())
{
Console.WriteLine($"\nEnabling control: {controlArn}");
var operationId = await wrapper.EnableControlAsync(controlArn, ouArn);
if (operationId != null)
{
Console.WriteLine($"Enabled control with operation id: {operationId}");
-
+
Console.Write("\nDo you want to disable the control? (y/n): ");
- if (Console.ReadLine()?.ToLower() == "y")
+ if (GetUserConfirmation())
{
Console.WriteLine("\nDisabling the control...");
var disableOpId = await wrapper.DisableControlAsync(controlArn, ouArn);
@@ -220,10 +235,14 @@ await wrapper.EnableBaselineAsync(ouArn,
}
}
+ ///
+ /// Sets up AWS Organizations and creates or finds a Sandbox OU.
+ ///
+ /// The ARN of the Sandbox organizational unit.
private static async Task SetupOrganizationAsync()
{
Console.WriteLine("\nChecking organization status...");
-
+
try
{
var orgResponse = await orgClient.DescribeOrganizationAsync(new DescribeOrganizationRequest());
@@ -241,11 +260,11 @@ private static async Task SetupOrganizationAsync()
// Look for Sandbox OU
var roots = await orgClient.ListRootsAsync(new ListRootsRequest());
var rootId = roots.Roots[0].Id;
-
+
Console.WriteLine("Checking for Sandbox OU...");
var ous = await orgClient.ListOrganizationalUnitsForParentAsync(new ListOrganizationalUnitsForParentRequest { ParentId = rootId });
var sandboxOu = ous.OrganizationalUnits.FirstOrDefault(ou => ou.Name == "Sandbox");
-
+
if (sandboxOu == null)
{
Console.WriteLine("Creating Sandbox OU...");
@@ -260,6 +279,15 @@ private static async Task SetupOrganizationAsync()
return sandboxOu.Arn;
}
+
+ ///
+ /// Gets user confirmation by waiting for input or returning true if not interactive.
+ ///
+ /// True if user enters 'y' or if isInteractive is false, otherwise false.
+ private static bool GetUserConfirmation()
+ {
+ return Console.ReadLine()?.ToLower() == "y" || !isInteractive;
+ }
}
// snippet-end:[ControlTower.dotnetv4.ControlTowerBasics]
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs b/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs
index bf2533a0807..69ef212ff29 100644
--- a/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs
+++ b/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs
@@ -1,16 +1,24 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-using Amazon.ControlTower;
using Amazon.ControlCatalog;
+using Amazon.ControlCatalog.Model;
+using Amazon.ControlTower;
+using Amazon.ControlTower.Model;
+using Amazon.Organizations;
+using Amazon.Organizations.Model;
+using Amazon.Runtime;
+using Amazon.SecurityToken;
+using Amazon.SecurityToken.Model;
+using ControlTowerActions;
using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
using Moq;
-using ControlTowerActions;
namespace ControlTowerTests;
///
-/// Tests for the ControlTowerWrapper class.
+/// Integration tests for the AWS Control Tower Basics scenario.
///
public class ControlTowerBasicsTests
{
@@ -23,57 +31,193 @@ public ControlTowerBasicsTests()
{
_configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
- .AddJsonFile("testsettings.json") // Load test settings from JSON file.
- .AddJsonFile("testsettings.local.json",
- true) // Load local test settings from JSON file.
.Build();
}
///
- /// Test that ListLandingZonesAsync returns a list.
+ /// Verifies the scenario with an integration test. No errors should be logged.
///
- [Fact]
- public async Task ListLandingZonesAsync_ShouldReturnList()
+ /// Async task.
+ //[Fact]
+ //[Trait("Category", "Integration")]
+ public async Task TestScenarioIntegration()
{
// Arrange
- var mockControlTowerService = new Mock();
- var mockControlCatalogService = new Mock();
- var wrapper = new ControlTowerWrapper(mockControlTowerService.Object, mockControlCatalogService.Object);
+ ControlTowerBasics.ControlTowerBasics.isInteractive = false;
+ var loggerScenarioMock = new Mock>();
- // Act & Assert
- var exception = await Record.ExceptionAsync(() => wrapper.ListLandingZonesAsync());
- Assert.Null(exception);
- }
+ loggerScenarioMock.Setup(logger => logger.Log(
+ It.Is(logLevel => logLevel == LogLevel.Error),
+ It.IsAny(),
+ It.Is((@object, @type) => true),
+ It.IsAny(),
+ It.IsAny>()
+ ));
- ///
- /// Test that ListBaselinesAsync returns a list.
- ///
- [Fact]
- public async Task ListBaselinesAsync_ShouldReturnList()
- {
- // Arrange
- var mockControlTowerService = new Mock();
- var mockControlCatalogService = new Mock();
- var wrapper = new ControlTowerWrapper(mockControlTowerService.Object, mockControlCatalogService.Object);
+ // Act
+ ControlTowerBasics.ControlTowerBasics.logger = loggerScenarioMock.Object;
+ await ControlTowerBasics.ControlTowerBasics.Main(new string[] { "" });
- // Act & Assert
- var exception = await Record.ExceptionAsync(() => wrapper.ListBaselinesAsync());
- Assert.Null(exception);
+ // Assert no errors logged
+ loggerScenarioMock.Verify(
+ logger => logger.Log(
+ It.Is(logLevel => logLevel == LogLevel.Error),
+ It.IsAny(),
+ It.Is((@object, @type) => true),
+ It.IsAny(),
+ It.IsAny>()),
+ Times.Never);
}
///
- /// Test that ListControlsAsync returns a list.
+ /// Scenario test using mocked AWS service clients.
///
+ /// Async task.
[Fact]
- public async Task ListControlsAsync_ShouldReturnList()
+ [Trait("Category", "Unit")]
+ public async Task TestScenarioLogic()
{
// Arrange
- var mockControlTowerService = new Mock();
- var mockControlCatalogService = new Mock();
- var wrapper = new ControlTowerWrapper(mockControlTowerService.Object, mockControlCatalogService.Object);
+ var mockControlTower = new Mock();
+ var mockControlCatalog = new Mock();
+ var mockOrganizations = new Mock();
+ var mockSts = new Mock();
+
+ // Setup paginator mocks
+ var mockLandingZonesPaginator = new Mock();
+ var mockLandingZonesEnumerable = new Mock>();
+ mockLandingZonesEnumerable.Setup(x => x.GetAsyncEnumerator(CancellationToken.None))
+ .Returns(new List
+ {
+ new ListLandingZonesResponse { LandingZones =
+ new List
+ {
+ new LandingZoneSummary { Arn = "arn:aws:controltower:us-east-1:123456789012:landingzone/test-lz" }
+ } }
+ }.ToAsyncEnumerable().GetAsyncEnumerator());
+ mockLandingZonesPaginator.Setup(x => x.Responses).Returns(mockLandingZonesEnumerable.Object);
+ mockControlTower.Setup(x => x.Paginators.ListLandingZones(It.IsAny()))
+ .Returns(mockLandingZonesPaginator.Object);
+
+ var mockBaselinesPaginator = new Mock();
+ var mockBaselinesEnumerable = new Mock>();
+ mockBaselinesEnumerable.Setup(x => x.GetAsyncEnumerator(CancellationToken.None))
+ .Returns(new List
+ {
+ new ListBaselinesResponse { Baselines =
+ new List
+ {
+ new BaselineSummary { Arn = "arn:aws:controltower:us-east-1:123456789012:baseline/test-baseline", Name = "AWSControlTowerBaseline" }
+ } }
+ }.ToAsyncEnumerable().GetAsyncEnumerator());
+ mockBaselinesPaginator.Setup(x => x.Responses).Returns(mockBaselinesEnumerable.Object);
+ mockControlTower.Setup(x => x.Paginators.ListBaselines(It.IsAny()))
+ .Returns(mockBaselinesPaginator.Object);
+
+ var mockEnabledBaselinesPaginator = new Mock();
+ var mockEnabledBaselinesEnumerable = new Mock>();
+ mockEnabledBaselinesEnumerable.Setup(x => x.GetAsyncEnumerator(CancellationToken.None))
+ .Returns(new List
+ {
+ new ListEnabledBaselinesResponse { EnabledBaselines =
+ new List
+ {
+ new EnabledBaselineSummary { Arn = "arn:aws:controltower:us-east-1:123456789012:enabledbaseline/test-enabled", BaselineIdentifier = "baseline/LN25R72TTG6IGPTQ" }
+ } }
+ }.ToAsyncEnumerable().GetAsyncEnumerator());
+ mockEnabledBaselinesPaginator.Setup(x => x.Responses).Returns(mockEnabledBaselinesEnumerable.Object);
+ mockControlTower.Setup(x => x.Paginators.ListEnabledBaselines(It.IsAny()))
+ .Returns(mockEnabledBaselinesPaginator.Object);
+
+ var mockEnabledControlsPaginator = new Mock();
+ var mockEnabledControlsEnumerable = new Mock>();
+ mockEnabledControlsEnumerable.Setup(x => x.GetAsyncEnumerator(CancellationToken.None))
+ .Returns(new List
+ {
+ new ListEnabledControlsResponse { EnabledControls =
+ new List
+ {
+ new EnabledControlSummary { Arn = "arn:aws:controltower:us-east-1:123456789012:control/test-control", ControlIdentifier = "arn:aws:controltower:us-east-1:123456789012:control/test-control-identifier" }
+ } }
+ }.ToAsyncEnumerable().GetAsyncEnumerator());
+ mockEnabledControlsPaginator.Setup(x => x.Responses).Returns(mockEnabledControlsEnumerable.Object);
+ mockControlTower.Setup(x => x.Paginators.ListEnabledControls(It.IsAny()))
+ .Returns(mockEnabledControlsPaginator.Object);
+
+ var mockControlsPaginator = new Mock();
+ var mockControlsEnumerable = new Mock>();
+ mockControlsEnumerable.Setup(x => x.GetAsyncEnumerator(CancellationToken.None))
+ .Returns(new List
+ {
+ new Amazon.ControlCatalog.Model.ListControlsResponse { Controls =
+ new List
+ {
+ new Amazon.ControlCatalog.Model.ControlSummary { Arn = "arn:aws:controlcatalog:us-east-1::control/ABCDEFG1234567", Name = "Test Control" }
+ } }
+ }.ToAsyncEnumerable().GetAsyncEnumerator());
+ mockControlsPaginator.Setup(x => x.Responses).Returns(mockControlsEnumerable.Object);
+ mockControlCatalog.Setup(x => x.Paginators.ListControls(It.IsAny()))
+ .Returns(mockControlsPaginator.Object);
+
+ // Setup individual service method mocks
+ mockControlTower.Setup(x => x.EnableBaselineAsync(It.IsAny(), default))
+ .ReturnsAsync(new EnableBaselineResponse { Arn = "arn:aws:controltower:us-east-1:123456789012:enabledbaseline/test-baseline", OperationIdentifier = "12345678-1234-1234-1234-123456789012" });
+ mockControlTower.Setup(x => x.DisableBaselineAsync(It.IsAny(), default))
+ .ReturnsAsync(new DisableBaselineResponse { OperationIdentifier = "12345678-1234-1234-1234-123456789012" });
+ mockControlTower.Setup(x => x.ResetEnabledBaselineAsync(It.IsAny(), default))
+ .ReturnsAsync(new ResetEnabledBaselineResponse { OperationIdentifier = "12345678-1234-1234-1234-123456789012" });
+ mockControlTower.Setup(x => x.GetBaselineOperationAsync(It.IsAny(), default))
+ .ReturnsAsync(new GetBaselineOperationResponse { BaselineOperation = new BaselineOperation { Status = BaselineOperationStatus.SUCCEEDED } });
+ mockControlTower.Setup(x => x.EnableControlAsync(It.IsAny(), default))
+ .ReturnsAsync(new EnableControlResponse { OperationIdentifier = "12345678-1234-1234-1234-123456789012" });
+ mockControlTower.Setup(x => x.DisableControlAsync(It.IsAny(), default))
+ .ReturnsAsync(new DisableControlResponse { OperationIdentifier = "12345678-1234-1234-1234-123456789012" });
+ mockControlTower.Setup(x => x.GetControlOperationAsync(It.IsAny(), default))
+ .ReturnsAsync(new GetControlOperationResponse { ControlOperation = new ControlOperation { Status = ControlOperationStatus.SUCCEEDED } });
+ mockControlTower.Setup(x => x.GetLandingZoneAsync(It.IsAny(), default))
+ .ReturnsAsync(new GetLandingZoneResponse { LandingZone = new LandingZoneDetail() });
+
+ // Setup Organizations mocks
+ mockOrganizations.Setup(x => x.DescribeOrganizationAsync(It.IsAny(), default))
+ .ReturnsAsync(new DescribeOrganizationResponse { Organization = new Organization { Id = "o-test123456" } });
+ mockOrganizations.Setup(x => x.ListRootsAsync(It.IsAny(), default))
+ .ReturnsAsync(new ListRootsResponse { Roots = new List { new Root { Id = "r-test123", Arn = "arn:aws:organizations::123456789012:root/o-test123456/r-test123" } } });
+ mockOrganizations.Setup(x => x.ListOrganizationalUnitsForParentAsync(It.IsAny(), default))
+ .ReturnsAsync(new ListOrganizationalUnitsForParentResponse { OrganizationalUnits = new List { new OrganizationalUnit { Id = "ou-test1234-abcd5678", Name = "Sandbox", Arn = "arn:aws:organizations::123456789012:ou/o-test123456/ou-test1234-abcd5678" } } });
+
+ // Setup STS mocks
+ mockSts.Setup(x => x.GetCallerIdentityAsync(It.IsAny(), default))
+ .ReturnsAsync(new GetCallerIdentityResponse { Account = "123456789012" });
+
+ ControlTowerBasics.ControlTowerBasics.isInteractive = false;
+ ControlTowerBasics.ControlTowerBasics.wrapper = new ControlTowerWrapper(mockControlTower.Object, mockControlCatalog.Object);
+ ControlTowerBasics.ControlTowerBasics.orgClient = mockOrganizations.Object;
+ ControlTowerBasics.ControlTowerBasics.stsClient = mockSts.Object;
+
+ var loggerScenarioMock = new Mock>();
+
+ loggerScenarioMock.Setup(logger => logger.Log(
+ It.Is(logLevel => logLevel == LogLevel.Error),
+ It.IsAny(),
+ It.Is((@object, @type) => true),
+ It.IsAny(),
+ It.IsAny>()
+ ));
+
+ // Act
+ ControlTowerBasics.ControlTowerBasics.logger = loggerScenarioMock.Object;
+
+ // Act
+ await ControlTowerBasics.ControlTowerBasics.RunScenario();
- // Act & Assert
- var exception = await Record.ExceptionAsync(() => wrapper.ListControlsAsync());
- Assert.Null(exception);
+ // Assert no errors logged
+ loggerScenarioMock.Verify(
+ logger => logger.Log(
+ It.Is(logLevel => logLevel == LogLevel.Error),
+ It.IsAny(),
+ It.Is((@object, @type) => true),
+ It.IsAny(),
+ It.IsAny>()),
+ Times.Never);
}
}
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj b/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj
index caec21bafe6..44b2474f876 100644
--- a/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj
+++ b/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj
@@ -14,6 +14,7 @@
+
runtime; build; native; contentfiles; analyzers; buildtransitive
From 8b1518bbdfe2ce2578483c7d78937d1681c423e7 Mon Sep 17 00:00:00 2001
From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com>
Date: Wed, 25 Jun 2025 15:30:27 -0500
Subject: [PATCH 4/7] Updates to test and formatting.
---
.../Actions/ControlTowerWrapper.cs | 2 +-
dotnetv4/ControlTower/Actions/Usings.cs | 4 +-
.../ControlTower_Basics/ControlTowerBasics.cs | 6 +-
.../Scenarios/ControlTower_Basics/Usings.cs | 2 +-
.../Tests/ControlTowerBasicsTests.cs | 157 ++++++++++++------
dotnetv4/ControlTower/Tests/testsettings.json | 6 -
6 files changed, 112 insertions(+), 65 deletions(-)
delete mode 100644 dotnetv4/ControlTower/Tests/testsettings.json
diff --git a/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs
index 8024080b2a4..9cafb90648d 100644
--- a/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs
+++ b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs
@@ -160,7 +160,7 @@ public async Task> ListEnabledBaselinesAsync()
return response.Arn;
}
- catch (Amazon.ControlTower.Model.ValidationException ex) when (ex.Message.Contains("already enabled"))
+ catch (ValidationException ex) when (ex.Message.Contains("already enabled"))
{
Console.WriteLine("Baseline is already enabled for this target");
return null;
diff --git a/dotnetv4/ControlTower/Actions/Usings.cs b/dotnetv4/ControlTower/Actions/Usings.cs
index e0bb67d1aeb..5d29be62880 100644
--- a/dotnetv4/ControlTower/Actions/Usings.cs
+++ b/dotnetv4/ControlTower/Actions/Usings.cs
@@ -2,10 +2,10 @@
// SPDX-License-Identifier: Apache-2.0
// snippet-start:[ControlTower.dotnetv4.Usings]
-global using Amazon.ControlTower;
-global using Amazon.ControlTower.Model;
global using Amazon.ControlCatalog;
global using Amazon.ControlCatalog.Model;
+global using Amazon.ControlTower;
+global using Amazon.ControlTower.Model;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Hosting;
global using Microsoft.Extensions.Logging;
diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs
index b72e684272b..bf7487c4bdc 100644
--- a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs
+++ b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs
@@ -47,9 +47,9 @@ public static async Task Main(string[] args)
logger = LoggerFactory.Create(builder => { builder.AddConsole(); })
.CreateLogger();
- wrapper ??= host.Services.GetRequiredService();
- orgClient ??= host.Services.GetRequiredService();
- stsClient ??= host.Services.GetRequiredService();
+ wrapper = host.Services.GetRequiredService();
+ orgClient = host.Services.GetRequiredService();
+ stsClient = host.Services.GetRequiredService();
await RunScenario();
}
diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs
index c8cc76f2bfa..e683f0911fd 100644
--- a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs
+++ b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs
@@ -1,5 +1,5 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
+global using Amazon.ControlCatalog;
global using Amazon.ControlTower;
-global using Amazon.ControlCatalog;
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs b/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs
index 69ef212ff29..3aac260da5b 100644
--- a/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs
+++ b/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs
@@ -11,7 +11,6 @@
using Amazon.SecurityToken;
using Amazon.SecurityToken.Model;
using ControlTowerActions;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Moq;
@@ -22,24 +21,12 @@ namespace ControlTowerTests;
///
public class ControlTowerBasicsTests
{
- private readonly IConfiguration _configuration;
-
- ///
- /// Constructor for the test class.
- ///
- public ControlTowerBasicsTests()
- {
- _configuration = new ConfigurationBuilder()
- .SetBasePath(Directory.GetCurrentDirectory())
- .Build();
- }
-
///
/// Verifies the scenario with an integration test. No errors should be logged.
///
/// Async task.
- //[Fact]
- //[Trait("Category", "Integration")]
+ [Fact]
+ [Trait("Category", "Integration")]
public async Task TestScenarioIntegration()
{
// Arrange
@@ -56,7 +43,13 @@ public async Task TestScenarioIntegration()
// Act
ControlTowerBasics.ControlTowerBasics.logger = loggerScenarioMock.Object;
- await ControlTowerBasics.ControlTowerBasics.Main(new string[] { "" });
+
+ ControlTowerBasics.ControlTowerBasics.wrapper = new ControlTowerWrapper(new AmazonControlTowerClient(), new AmazonControlCatalogClient());
+ ControlTowerBasics.ControlTowerBasics.orgClient = new AmazonOrganizationsClient();
+ ControlTowerBasics.ControlTowerBasics.stsClient = new AmazonSecurityTokenServiceClient();
+
+
+ await ControlTowerBasics.ControlTowerBasics.RunScenario();
// Assert no errors logged
loggerScenarioMock.Verify(
@@ -83,9 +76,82 @@ public async Task TestScenarioLogic()
var mockOrganizations = new Mock();
var mockSts = new Mock();
+ SetupMocks(mockControlTower, mockControlCatalog, mockOrganizations, mockSts);
+
+ ControlTowerBasics.ControlTowerBasics.isInteractive = false;
+ ControlTowerBasics.ControlTowerBasics.wrapper = new ControlTowerWrapper(mockControlTower.Object, mockControlCatalog.Object);
+ ControlTowerBasics.ControlTowerBasics.orgClient = mockOrganizations.Object;
+ ControlTowerBasics.ControlTowerBasics.stsClient = mockSts.Object;
+
+ var loggerScenarioMock = new Mock>();
+ ControlTowerBasics.ControlTowerBasics.logger = loggerScenarioMock.Object;
+
+ // Act
+ await ControlTowerBasics.ControlTowerBasics.RunScenario();
+
+ // Assert no errors logged
+ loggerScenarioMock.Verify(
+ logger => logger.Log(
+ It.Is(logLevel => logLevel == LogLevel.Error),
+ It.IsAny(),
+ It.Is((@object, @type) => true),
+ It.IsAny(),
+ It.IsAny>()),
+ Times.Never);
+ }
+
+ ///
+ /// Test scenario with ControlTowerException.
+ ///
+ /// Async task.
+ [Fact]
+ [Trait("Category", "Unit")]
+ public async Task TestScenarioWithException()
+ {
+ // Arrange
+ var mockControlTower = new Mock();
+ var mockControlCatalog = new Mock();
+ var mockOrganizations = new Mock();
+ var mockSts = new Mock();
+
+ SetupMocks(mockControlTower, mockControlCatalog, mockOrganizations, mockSts, throwException: true);
+
+ ControlTowerBasics.ControlTowerBasics.isInteractive = false;
+ ControlTowerBasics.ControlTowerBasics.wrapper = new ControlTowerWrapper(mockControlTower.Object, mockControlCatalog.Object);
+ ControlTowerBasics.ControlTowerBasics.orgClient = mockOrganizations.Object;
+ ControlTowerBasics.ControlTowerBasics.stsClient = mockSts.Object;
+
+ var loggerScenarioMock = new Mock>();
+ ControlTowerBasics.ControlTowerBasics.logger = loggerScenarioMock.Object;
+
+ // Act
+ await ControlTowerBasics.ControlTowerBasics.RunScenario();
+
+ // Assert the error is logged.
+ loggerScenarioMock.Verify(
+ logger => logger.Log(
+ It.Is(logLevel => logLevel == LogLevel.Error),
+ It.IsAny(),
+ It.Is((@object, @type) => true),
+ It.IsAny(),
+ It.IsAny>()),
+ Times.Once);
+ }
+
+ ///
+ /// Set up the mocks for testing.
+ ///
+ /// Mock ControlTower client.
+ /// Mock ControlCatalog client.
+ /// Mock Organizations client.
+ /// Mock Sts client.
+ /// True to force an exception.
+ private void SetupMocks(Mock mockControlTower, Mock mockControlCatalog, Mock mockOrganizations, Mock mockSts, bool throwException = false)
+ {
// Setup paginator mocks
var mockLandingZonesPaginator = new Mock();
var mockLandingZonesEnumerable = new Mock>();
+
mockLandingZonesEnumerable.Setup(x => x.GetAsyncEnumerator(CancellationToken.None))
.Returns(new List
{
@@ -95,6 +161,8 @@ public async Task TestScenarioLogic()
new LandingZoneSummary { Arn = "arn:aws:controltower:us-east-1:123456789012:landingzone/test-lz" }
} }
}.ToAsyncEnumerable().GetAsyncEnumerator());
+
+
mockLandingZonesPaginator.Setup(x => x.Responses).Returns(mockLandingZonesEnumerable.Object);
mockControlTower.Setup(x => x.Paginators.ListLandingZones(It.IsAny()))
.Returns(mockLandingZonesPaginator.Object);
@@ -159,11 +227,27 @@ public async Task TestScenarioLogic()
mockControlCatalog.Setup(x => x.Paginators.ListControls(It.IsAny()))
.Returns(mockControlsPaginator.Object);
- // Setup individual service method mocks
- mockControlTower.Setup(x => x.EnableBaselineAsync(It.IsAny(), default))
- .ReturnsAsync(new EnableBaselineResponse { Arn = "arn:aws:controltower:us-east-1:123456789012:enabledbaseline/test-baseline", OperationIdentifier = "12345678-1234-1234-1234-123456789012" });
- mockControlTower.Setup(x => x.DisableBaselineAsync(It.IsAny(), default))
- .ReturnsAsync(new DisableBaselineResponse { OperationIdentifier = "12345678-1234-1234-1234-123456789012" });
+ // Force an exception that should end the scenario.
+ if (throwException)
+ {
+ mockControlTower.Setup(x =>
+ x.DisableBaselineAsync(It.IsAny(), default))
+ .Throws(new AmazonControlTowerException("Test exception"));
+ }
+ else
+ {
+ mockControlTower.Setup(x => x.DisableBaselineAsync(It.IsAny(), default))
+ .ReturnsAsync(new DisableBaselineResponse { OperationIdentifier = "12345678-1234-1234-1234-123456789012" });
+ }
+ mockControlTower.Setup(x =>
+ x.EnableBaselineAsync(It.IsAny(), default))
+ .ReturnsAsync(new EnableBaselineResponse
+ {
+ Arn =
+ "arn:aws:controltower:us-east-1:123456789012:enabledbaseline/test-baseline",
+ OperationIdentifier = "12345678-1234-1234-1234-123456789012"
+ });
+
mockControlTower.Setup(x => x.ResetEnabledBaselineAsync(It.IsAny(), default))
.ReturnsAsync(new ResetEnabledBaselineResponse { OperationIdentifier = "12345678-1234-1234-1234-123456789012" });
mockControlTower.Setup(x => x.GetBaselineOperationAsync(It.IsAny(), default))
@@ -188,36 +272,5 @@ public async Task TestScenarioLogic()
// Setup STS mocks
mockSts.Setup(x => x.GetCallerIdentityAsync(It.IsAny(), default))
.ReturnsAsync(new GetCallerIdentityResponse { Account = "123456789012" });
-
- ControlTowerBasics.ControlTowerBasics.isInteractive = false;
- ControlTowerBasics.ControlTowerBasics.wrapper = new ControlTowerWrapper(mockControlTower.Object, mockControlCatalog.Object);
- ControlTowerBasics.ControlTowerBasics.orgClient = mockOrganizations.Object;
- ControlTowerBasics.ControlTowerBasics.stsClient = mockSts.Object;
-
- var loggerScenarioMock = new Mock>();
-
- loggerScenarioMock.Setup(logger => logger.Log(
- It.Is(logLevel => logLevel == LogLevel.Error),
- It.IsAny(),
- It.Is((@object, @type) => true),
- It.IsAny(),
- It.IsAny>()
- ));
-
- // Act
- ControlTowerBasics.ControlTowerBasics.logger = loggerScenarioMock.Object;
-
- // Act
- await ControlTowerBasics.ControlTowerBasics.RunScenario();
-
- // Assert no errors logged
- loggerScenarioMock.Verify(
- logger => logger.Log(
- It.Is(logLevel => logLevel == LogLevel.Error),
- It.IsAny(),
- It.Is((@object, @type) => true),
- It.IsAny(),
- It.IsAny>()),
- Times.Never);
}
}
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/Tests/testsettings.json b/dotnetv4/ControlTower/Tests/testsettings.json
deleted file mode 100644
index 9a17f2b6ffc..00000000000
--- a/dotnetv4/ControlTower/Tests/testsettings.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "TargetIdentifier": "arn:aws:organizations::123456789012:ou/o-example12345/ou-example12345",
- "ControlArn": "arn:aws:controltower:us-east-1::control/AWS-GR_AUDIT_BUCKET_ENCRYPTION_ENABLED",
- "BaselineIdentifier": "arn:aws:controltower:us-east-1::baseline/AWSControlTowerBaseline",
- "BaselineVersion": "1.0"
-}
\ No newline at end of file
From bc7e69d62ce640e8e7ed2b6c10b3bb7c97f814fe Mon Sep 17 00:00:00 2001
From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com>
Date: Mon, 30 Jun 2025 15:09:06 -0500
Subject: [PATCH 5/7] Updates to scenario.
---
.doc_gen/metadata/controltower_metadata.yaml | 121 ++++++++++++++++++
.../Actions/ControlTowerWrapper.cs | 4 +
.../ControlTower/Actions/HelloControlTower.cs | 46 ++++---
dotnetv4/ControlTower/Actions/Usings.cs | 15 ---
dotnetv4/ControlTower/README.md | 75 +++++++----
.../ControlTower_Basics/ControlTowerBasics.cs | 10 +-
.../Scenarios/ControlTower_Basics/Usings.cs | 5 -
.../ControlTower_Basics/settings.json | 6 -
dotnetv4/DotNetV4Examples.sln | 26 ++++
9 files changed, 233 insertions(+), 75 deletions(-)
delete mode 100644 dotnetv4/ControlTower/Actions/Usings.cs
delete mode 100644 dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs
delete mode 100644 dotnetv4/ControlTower/Scenarios/ControlTower_Basics/settings.json
diff --git a/.doc_gen/metadata/controltower_metadata.yaml b/.doc_gen/metadata/controltower_metadata.yaml
index 3cf49427046..083079b4db1 100644
--- a/.doc_gen/metadata/controltower_metadata.yaml
+++ b/.doc_gen/metadata/controltower_metadata.yaml
@@ -12,6 +12,14 @@ controltower_Hello:
- description:
snippet_tags:
- python.example_code.controltower.Hello
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.HelloControlTower
services:
controltower: {ListBaselines}
@@ -26,6 +34,14 @@ controltower_ListBaselines:
snippet_tags:
- python.example_code.controltower.ControlTowerWrapper.decl
- python.example_code.controltower.ListBaselines
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.ListBaselines
services:
controltower: {ListBaselines}
@@ -40,6 +56,14 @@ controltower_ListEnabledBaselines:
snippet_tags:
- python.example_code.controltower.ControlTowerWrapper.decl
- python.example_code.controltower.ListEnabledBaselines
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.ListEnabledBaselines
services:
controltower: {ListEnabledBaselines}
@@ -54,6 +78,14 @@ controltower_EnableBaseline:
snippet_tags:
- python.example_code.controltower.ControlTowerWrapper.decl
- python.example_code.controltower.EnableBaseline
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.EnableBaseline
services:
controltower: {EnableBaseline}
@@ -68,6 +100,14 @@ controltower_ResetEnabledBaseline:
snippet_tags:
- python.example_code.controltower.ControlTowerWrapper.decl
- python.example_code.controltower.ResetEnabledBaseline
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.ResetEnabledBaseline
services:
controltower: {ResetEnabledBaseline}
@@ -82,6 +122,14 @@ controltower_DisableBaseline:
snippet_tags:
- python.example_code.controltower.ControlTowerWrapper.decl
- python.example_code.controltower.DisableBaseline
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.DisableBaseline
services:
controltower: {DisableBaseline}
@@ -96,6 +144,14 @@ controltower_ListEnabledControls:
snippet_tags:
- python.example_code.controltower.ControlTowerWrapper.decl
- python.example_code.controltower.ListEnabledControls
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.ListEnabledControls
services:
controltower: {ListEnabledControls}
@@ -110,6 +166,14 @@ controltower_EnableControl:
snippet_tags:
- python.example_code.controltower.ControlTowerWrapper.decl
- python.example_code.controltower.EnableControl
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.EnableControl
services:
controltower: {EnableControl}
@@ -124,6 +188,14 @@ controltower_GetControlOperation:
snippet_tags:
- python.example_code.controltower.ControlTowerWrapper.decl
- python.example_code.controltower.GetControlOperation
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.GetControlOperation
services:
controltower: {GetControlOperation}
@@ -138,6 +210,14 @@ controltower_DisableControl:
snippet_tags:
- python.example_code.controltower.ControlTowerWrapper.decl
- python.example_code.controltower.DisableControl
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.DisableControl
services:
controltower: {DisableControl}
@@ -152,9 +232,39 @@ controltower_ListLandingZones:
snippet_tags:
- python.example_code.controltower.ControlTowerWrapper.decl
- python.example_code.controltower.ListLandingZones
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.ListLandingZones
services:
controltower: {ListLandingZones}
+controltower_GetBaselineOperation:
+ languages:
+ Python:
+ versions:
+ - sdk_version: 3
+ github: python/example_code/controltower
+ excerpts:
+ - description:
+ snippet_tags:
+ - python.example_code.controltower.ControlTowerWrapper.decl
+ - python.example_code.controltower.GetBaselineOperation
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.GetBaselineOperation
+ services:
+ controltower: {GetBaselineOperation}
+
controltower_Scenario:
synopsis_list:
- List landing zones.
@@ -172,5 +282,16 @@ controltower_Scenario:
snippet_tags:
- python.example_code.controltower.ControlTowerScenario
- python.example_code.controltower.ControlTowerWrapper.class
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description: Run an interactive scenario demonstrating &CTowerlong; features.
+ snippet_tags:
+ - ControlTower.dotnetv4.ControlTowerBasics
+ - description: Wrapper methods that are called by the scenario to manage &AUR; actions.
+ snippet_tags:
+ - ControlTower.dotnetv4.ControlTowerWrapper
services:
controltower: {CreateLandingZone, DeleteLandingZone, ListBaselines, ListEnabledBaselines, EnableBaseline, ResetEnabledBaseline, DisableBaseline, EnableControl, GetControlOperation, DisableControl, GetLandingZoneOperation, ListLandingZones, ListEnabledControls}
diff --git a/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs
index 9cafb90648d..baff9a65f54 100644
--- a/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs
+++ b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs
@@ -3,6 +3,10 @@
// snippet-start:[ControlTower.dotnetv4.ControlTowerWrapper]
+using Amazon.ControlCatalog;
+using Amazon.ControlCatalog.Model;
+using Amazon.ControlTower;
+using Amazon.ControlTower.Model;
using ValidationException = Amazon.ControlTower.Model.ValidationException;
namespace ControlTowerActions;
diff --git a/dotnetv4/ControlTower/Actions/HelloControlTower.cs b/dotnetv4/ControlTower/Actions/HelloControlTower.cs
index b46920240f4..bab047bf390 100644
--- a/dotnetv4/ControlTower/Actions/HelloControlTower.cs
+++ b/dotnetv4/ControlTower/Actions/HelloControlTower.cs
@@ -3,13 +3,20 @@
// snippet-start:[ControlTower.dotnetv4.HelloControlTower]
+using Amazon.ControlTower;
+using Amazon.ControlTower.Model;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Console;
+using Microsoft.Extensions.Logging.Debug;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace ControlTowerActions;
///
/// A class that introduces the AWS Control Tower by listing the
-/// landing zones for the account.
+/// available baselines for the account.
///
public class HelloControlTower
{
@@ -25,8 +32,6 @@ static async Task Main(string[] args)
.AddFilter("Microsoft", LogLevel.Trace))
.ConfigureServices((_, services) =>
services.AddAWSService()
- .AddAWSService()
- .AddTransient()
)
.Build();
@@ -36,28 +41,33 @@ static async Task Main(string[] args)
var amazonClient = host.Services.GetRequiredService();
Console.Clear();
- Console.WriteLine("Hello AWS Control Tower.");
- Console.WriteLine("Let's get a list of your AWS Control Tower landing zones.");
+ Console.WriteLine("Hello, AWS Control Tower! Let's list available baselines:");
+ Console.WriteLine();
- var landingZones = new List();
+ var baselines = new List();
- var landingZonesPaginator = amazonClient.Paginators.ListLandingZones(new ListLandingZonesRequest());
-
- await foreach (var response in landingZonesPaginator.Responses)
+ try
{
- landingZones.AddRange(response.LandingZones);
- }
+ var baselinesPaginator = amazonClient.Paginators.ListBaselines(new ListBaselinesRequest());
- if (landingZones.Count > 0)
- {
- landingZones.ForEach(landingZone =>
+ await foreach (var response in baselinesPaginator.Responses)
{
- Console.WriteLine($"Landing Zone \t{landingZone.Arn}");
- });
+ baselines.AddRange(response.Baselines);
+ }
+
+ Console.WriteLine($"{baselines.Count} baseline(s) retrieved.");
+ foreach (var baseline in baselines)
+ {
+ Console.WriteLine($"\t{baseline.Name}");
+ }
+ }
+ catch (Amazon.ControlTower.Model.AccessDeniedException)
+ {
+ Console.WriteLine("Access denied. Please ensure you have the necessary permissions.");
}
- else
+ catch (Exception ex)
{
- Console.WriteLine("No landing zones were found.");
+ Console.WriteLine($"An error occurred: {ex.Message}");
}
}
}
diff --git a/dotnetv4/ControlTower/Actions/Usings.cs b/dotnetv4/ControlTower/Actions/Usings.cs
deleted file mode 100644
index 5d29be62880..00000000000
--- a/dotnetv4/ControlTower/Actions/Usings.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-// snippet-start:[ControlTower.dotnetv4.Usings]
-global using Amazon.ControlCatalog;
-global using Amazon.ControlCatalog.Model;
-global using Amazon.ControlTower;
-global using Amazon.ControlTower.Model;
-global using Microsoft.Extensions.DependencyInjection;
-global using Microsoft.Extensions.Hosting;
-global using Microsoft.Extensions.Logging;
-global using Microsoft.Extensions.Logging.Console;
-global using Microsoft.Extensions.Logging.Debug;
-
-// snippet-end:[ControlTower.dotnetv4.Usings]
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/README.md b/dotnetv4/ControlTower/README.md
index 33f5b2ce51c..9fd6f547974 100644
--- a/dotnetv4/ControlTower/README.md
+++ b/dotnetv4/ControlTower/README.md
@@ -1,13 +1,13 @@
-# AWS Control Tower code examples for the SDK for .NET
+# AWS Control Tower code examples for the SDK for .NET (v4)
## Overview
-Shows how to use the AWS SDK for .NET to work with AWS Control Tower.
+Shows how to use the AWS SDK for .NET (v4) to work with AWS Control Tower.
-*AWS Control Tower provides a pre-configured multi-account environment based on best practices to help organizations set up a secure, compliant, multi-account AWS environment.*
+_AWS Control Tower enables you to enforce and manage governance rules for security, operations, and compliance at scale across all your organizations and accounts._
## ⚠ Important
@@ -23,22 +23,40 @@ Shows how to use the AWS SDK for .NET to work with AWS Control Tower.
### Prerequisites
-For prerequisites, see the [README](../../README.md#Prerequisites) in the `dotnetv4` folder.
+For prerequisites, see the [README](../README.md#Prerequisites) in the `dotnetv4` folder.
+
### Get started
-- [Hello AWS Control Tower](Actions/HelloControlTower.cs#L15) (`ListLandingZones`)
+- [Hello AWS Control Tower](Actions/HelloControlTower.cs#L4) (`ListBaselines`)
+
+
+### Basics
+
+Code examples that show you how to perform the essential operations within a service.
+
+- [Learn the basics](Scenarios/ControlTower_Basics/ControlTowerBasics.cs)
+
### Single actions
Code excerpts that show you how to call individual service functions.
-- [GetLandingZone](Actions/ControlTowerWrapper.cs#L35)
-- [ListEnabledControls](Actions/ControlTowerWrapper.cs#L50)
-- [ListLandingZones](Actions/ControlTowerWrapper.cs#L20)
+- [DisableBaseline](Actions/ControlTowerWrapper.cs#L177)
+- [DisableControl](Actions/ControlTowerWrapper.cs#L397)
+- [EnableBaseline](Actions/ControlTowerWrapper.cs#L116)
+- [EnableControl](Actions/ControlTowerWrapper.cs#L341)
+- [GetBaselineOperation](Actions/ControlTowerWrapper.cs#L269)
+- [GetControlOperation](Actions/ControlTowerWrapper.cs#L445)
+- [ListBaselines](Actions/ControlTowerWrapper.cs#L58)
+- [ListEnabledBaselines](Actions/ControlTowerWrapper.cs#L87)
+- [ListEnabledControls](Actions/ControlTowerWrapper.cs#L301)
+- [ListLandingZones](Actions/ControlTowerWrapper.cs#L29)
+- [ResetEnabledBaseline](Actions/ControlTowerWrapper.cs#L223)
+
@@ -47,17 +65,6 @@ Code excerpts that show you how to call individual service functions.
### Instructions
-For general instructions to run the examples, see the [README](../../README.md#building-and-running-the-code-examples) in the `dotnetv4` folder.
-
-Some projects might include a settings file. Before compiling the project, you can change these settings to match your account and preferred Region. Alternatively, add a settings.local.json file with your preferred settings, which will be loaded automatically when the application runs.
-
-After the example compiles, you can run it from the command line. To do so, navigate to the folder that contains the .csproj file and run the following command:
-
-```
-dotnet run
-```
-
-Alternatively, you can run the example from within your IDE.
@@ -66,18 +73,32 @@ Alternatively, you can run the example from within your IDE.
This example shows you how to get started using AWS Control Tower.
-```
-dotnet run
-```
-
-
+#### Learn the basics
+
+This example shows you how to do the following:
+
+- List landing zones.
+- List, enable, get, reset, and disable baselines.
+- List, enable, get, and disable controls.
+
+
+
+
+
+
+
+
### Tests
⚠ Running tests might result in charges to your AWS account.
-To find instructions for running these tests, see the [README](../../README.md#Tests) in the `dotnetv4` folder.
+
+To find instructions for running these tests, see the [README](../README.md#Tests)
+in the `dotnetv4` folder.
+
+
@@ -86,7 +107,7 @@ To find instructions for running these tests, see the [README](../../README.md#T
- [AWS Control Tower User Guide](https://docs.aws.amazon.com/controltower/latest/userguide/what-is-control-tower.html)
- [AWS Control Tower API Reference](https://docs.aws.amazon.com/controltower/latest/APIReference/Welcome.html)
-- [AWS SDK for .NET AWS Control Tower reference](https://docs.aws.amazon.com/sdkfornet/v3/apidocs/items/ControlTower/NControlTower.html)
+- [SDK for .NET (v4) AWS Control Tower reference](https://docs.aws.amazon.com/sdkfornet/v4/apidocs/items/Controltower/NControltower.html)
@@ -95,4 +116,4 @@ To find instructions for running these tests, see the [README](../../README.md#T
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-SPDX-License-Identifier: Apache-2.0
\ No newline at end of file
+SPDX-License-Identifier: Apache-2.0
diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs
index bf7487c4bdc..9d5e3e212da 100644
--- a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs
+++ b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs
@@ -3,6 +3,8 @@
// snippet-start:[ControlTower.dotnetv4.ControlTowerBasics]
+using Amazon.ControlCatalog;
+using Amazon.ControlTower;
using Amazon.ControlTower.Model;
using Amazon.Organizations;
using Amazon.Organizations.Model;
@@ -67,7 +69,7 @@ public static async Task RunScenario()
try
{
- var accountId = (await stsClient.GetCallerIdentityAsync(new GetCallerIdentityRequest())).Account;
+ var accountId = (await stsClient!.GetCallerIdentityAsync(new GetCallerIdentityRequest())).Account;
Console.WriteLine($"\nAccount ID: {accountId}");
Console.WriteLine("\nSome demo operations require the use of a landing zone.");
@@ -76,7 +78,7 @@ public static async Task RunScenario()
Console.WriteLine("see https://docs.aws.amazon.com/controltower/latest/userguide/getting-started-from-console.html");
// List available landing zones
- var landingZones = await wrapper.ListLandingZonesAsync();
+ var landingZones = await wrapper!.ListLandingZonesAsync();
if (landingZones.Count > 0)
{
Console.WriteLine("\nAvailable Landing Zones:");
@@ -245,14 +247,14 @@ private static async Task SetupOrganizationAsync()
try
{
- var orgResponse = await orgClient.DescribeOrganizationAsync(new DescribeOrganizationRequest());
+ var orgResponse = await orgClient!.DescribeOrganizationAsync(new DescribeOrganizationRequest());
var orgId = orgResponse.Organization.Id;
Console.WriteLine($"Account is part of organization: {orgId}");
}
catch (AWSOrganizationsNotInUseException)
{
Console.WriteLine("No organization found. Creating a new organization...");
- var createResponse = await orgClient.CreateOrganizationAsync(new CreateOrganizationRequest { FeatureSet = OrganizationFeatureSet.ALL });
+ var createResponse = await orgClient!.CreateOrganizationAsync(new CreateOrganizationRequest { FeatureSet = OrganizationFeatureSet.ALL });
var orgId = createResponse.Organization.Id;
Console.WriteLine($"Created new organization: {orgId}");
}
diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs
deleted file mode 100644
index e683f0911fd..00000000000
--- a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-global using Amazon.ControlCatalog;
-global using Amazon.ControlTower;
diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/settings.json b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/settings.json
deleted file mode 100644
index 9a17f2b6ffc..00000000000
--- a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/settings.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "TargetIdentifier": "arn:aws:organizations::123456789012:ou/o-example12345/ou-example12345",
- "ControlArn": "arn:aws:controltower:us-east-1::control/AWS-GR_AUDIT_BUCKET_ENCRYPTION_ENABLED",
- "BaselineIdentifier": "arn:aws:controltower:us-east-1::baseline/AWSControlTowerBaseline",
- "BaselineVersion": "1.0"
-}
\ No newline at end of file
diff --git a/dotnetv4/DotNetV4Examples.sln b/dotnetv4/DotNetV4Examples.sln
index 55584997279..57acbb70c60 100644
--- a/dotnetv4/DotNetV4Examples.sln
+++ b/dotnetv4/DotNetV4Examples.sln
@@ -137,6 +137,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ECSScenario", "ECS\ECSScena
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ECSActions", "ECS\ECSActions\ECSActions.csproj", "{7485EAED-F81C-4119-BABC-E009A21ACE46}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ControlTower", "ControlTower", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlTowerTests", "ControlTower\Tests\ControlTowerTests.csproj", "{43C5E98B-5EC4-9F2B-2676-8F1E34969855}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scenarios", "Scenarios", "{6BE1D9A4-1832-49F5-8682-6DEE4A7D6232}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlTowerBasics", "ControlTower\Scenarios\ControlTower_Basics\ControlTowerBasics.csproj", "{6B1F00FF-7F1D-C5D8-A8D3-E0EF2886B8C6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlTowerActions", "ControlTower\Actions\ControlTowerActions.csproj", "{9D601495-FDBA-C852-4ACB-EC54EDC9B3E5}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -335,6 +345,18 @@ Global
{7485EAED-F81C-4119-BABC-E009A21ACE46}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7485EAED-F81C-4119-BABC-E009A21ACE46}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7485EAED-F81C-4119-BABC-E009A21ACE46}.Release|Any CPU.Build.0 = Release|Any CPU
+ {43C5E98B-5EC4-9F2B-2676-8F1E34969855}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {43C5E98B-5EC4-9F2B-2676-8F1E34969855}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {43C5E98B-5EC4-9F2B-2676-8F1E34969855}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {43C5E98B-5EC4-9F2B-2676-8F1E34969855}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6B1F00FF-7F1D-C5D8-A8D3-E0EF2886B8C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6B1F00FF-7F1D-C5D8-A8D3-E0EF2886B8C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6B1F00FF-7F1D-C5D8-A8D3-E0EF2886B8C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6B1F00FF-7F1D-C5D8-A8D3-E0EF2886B8C6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9D601495-FDBA-C852-4ACB-EC54EDC9B3E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9D601495-FDBA-C852-4ACB-EC54EDC9B3E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9D601495-FDBA-C852-4ACB-EC54EDC9B3E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9D601495-FDBA-C852-4ACB-EC54EDC9B3E5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -398,6 +420,10 @@ Global
{3F159C49-3DE7-42F5-AF14-E64C03AF19E8} = {EE6D1933-1E38-406A-B691-446326310D1F}
{D44D50E1-EC65-4A1C-AAA1-C360E4FC563F} = {EE6D1933-1E38-406A-B691-446326310D1F}
{7485EAED-F81C-4119-BABC-E009A21ACE46} = {EE6D1933-1E38-406A-B691-446326310D1F}
+ {43C5E98B-5EC4-9F2B-2676-8F1E34969855} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {6BE1D9A4-1832-49F5-8682-6DEE4A7D6232} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {6B1F00FF-7F1D-C5D8-A8D3-E0EF2886B8C6} = {6BE1D9A4-1832-49F5-8682-6DEE4A7D6232}
+ {9D601495-FDBA-C852-4ACB-EC54EDC9B3E5} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {08502818-E8E1-4A91-A51C-4C8C8D4FF9CA}
From cd4a89c3e22b5bb2adf4db52ec5e2fae0e7b025b Mon Sep 17 00:00:00 2001
From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com>
Date: Wed, 2 Jul 2025 14:42:13 -0500
Subject: [PATCH 6/7] Update README.md
---
dotnetv4/ControlTower/README.md | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/dotnetv4/ControlTower/README.md b/dotnetv4/ControlTower/README.md
index 9fd6f547974..226fdc22170 100644
--- a/dotnetv4/ControlTower/README.md
+++ b/dotnetv4/ControlTower/README.md
@@ -45,17 +45,17 @@ Code examples that show you how to perform the essential operations within a ser
Code excerpts that show you how to call individual service functions.
-- [DisableBaseline](Actions/ControlTowerWrapper.cs#L177)
-- [DisableControl](Actions/ControlTowerWrapper.cs#L397)
-- [EnableBaseline](Actions/ControlTowerWrapper.cs#L116)
-- [EnableControl](Actions/ControlTowerWrapper.cs#L341)
-- [GetBaselineOperation](Actions/ControlTowerWrapper.cs#L269)
-- [GetControlOperation](Actions/ControlTowerWrapper.cs#L445)
-- [ListBaselines](Actions/ControlTowerWrapper.cs#L58)
-- [ListEnabledBaselines](Actions/ControlTowerWrapper.cs#L87)
-- [ListEnabledControls](Actions/ControlTowerWrapper.cs#L301)
-- [ListLandingZones](Actions/ControlTowerWrapper.cs#L29)
-- [ResetEnabledBaseline](Actions/ControlTowerWrapper.cs#L223)
+- [DisableBaseline](Actions/ControlTowerWrapper.cs#L181)
+- [DisableControl](Actions/ControlTowerWrapper.cs#L401)
+- [EnableBaseline](Actions/ControlTowerWrapper.cs#L120)
+- [EnableControl](Actions/ControlTowerWrapper.cs#L345)
+- [GetBaselineOperation](Actions/ControlTowerWrapper.cs#L273)
+- [GetControlOperation](Actions/ControlTowerWrapper.cs#L449)
+- [ListBaselines](Actions/ControlTowerWrapper.cs#L62)
+- [ListEnabledBaselines](Actions/ControlTowerWrapper.cs#L91)
+- [ListEnabledControls](Actions/ControlTowerWrapper.cs#L305)
+- [ListLandingZones](Actions/ControlTowerWrapper.cs#L33)
+- [ResetEnabledBaseline](Actions/ControlTowerWrapper.cs#L227)
From 445bc9390cd7a7bf3fbf6c2f5cee185ef87d09f4 Mon Sep 17 00:00:00 2001
From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com>
Date: Wed, 2 Jul 2025 14:51:43 -0500
Subject: [PATCH 7/7] Fix up missing line in python README.
---
python/example_code/controltower/README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/python/example_code/controltower/README.md b/python/example_code/controltower/README.md
index 596edf11dee..726705ff9ef 100644
--- a/python/example_code/controltower/README.md
+++ b/python/example_code/controltower/README.md
@@ -56,6 +56,7 @@ Code excerpts that show you how to call individual service functions.
- [DisableControl](controltower_wrapper.py#L263)
- [EnableBaseline](controltower_wrapper.py#L69)
- [EnableControl](controltower_wrapper.py#L159)
+- [GetBaselineOperation](controltower_wrapper.py#L236)
- [GetControlOperation](controltower_wrapper.py#L209)
- [ListBaselines](controltower_wrapper.py#L39)
- [ListEnabledBaselines](controltower_wrapper.py#L330)