Repository: cloudquery/cloudquery (Go)

Issue:
Add support for SageMaker apps
### Why do you need this resource or plugin? Please describe.
We're currently running an automated resource deletion process that uses CQ to pull a list of resources with specific tags to determine what resources should be allowlisted. We would like to pull the info for SageMaker apps as part of this process.

### Describe the solution you'd like
Add support for SageMaker apps (need to be able to pull **app name** and **tags**)


Discussion:

Patch:
diff --git a/plugins/source/aws/docs/tables/README.md b/plugins/source/aws/docs/tables/README.md
index 0bd82a328ea063..8411b8cbc58eaf 100644
--- a/plugins/source/aws/docs/tables/README.md
+++ b/plugins/source/aws/docs/tables/README.md
@@ -481,6 +481,7 @@
   - [aws_s3_bucket_grants](../../../../../website/tables/aws/aws_s3_bucket_grants.md)
   - [aws_s3_bucket_lifecycles](../../../../../website/tables/aws/aws_s3_bucket_lifecycles.md)
   - [aws_s3_bucket_websites](../../../../../website/tables/aws/aws_s3_bucket_websites.md)
+- [aws_sagemaker_apps](../../../../../website/tables/aws/aws_sagemaker_apps.md)
 - [aws_sagemaker_endpoint_configurations](../../../../../website/tables/aws/aws_sagemaker_endpoint_configurations.md)
 - [aws_sagemaker_models](../../../../../website/tables/aws/aws_sagemaker_models.md)
 - [aws_sagemaker_notebook_instances](../../../../../website/tables/aws/aws_sagemaker_notebook_instances.md)
diff --git a/plugins/source/aws/resources/plugin/tables.go b/plugins/source/aws/resources/plugin/tables.go
index fcd8f4c2e8f774..ca65a639338f20 100644
--- a/plugins/source/aws/resources/plugin/tables.go
+++ b/plugins/source/aws/resources/plugin/tables.go
@@ -415,6 +415,7 @@ func tables() []*schema.Table {
 		s3.AccessPoints(),
 		s3.Accounts(),
 		s3.Buckets(),
+		sagemaker.Apps(),
 		sagemaker.EndpointConfigurations(),
 		sagemaker.Models(),
 		sagemaker.NotebookInstances(),
diff --git a/plugins/source/aws/resources/services/sagemaker/apps.go b/plugins/source/aws/resources/services/sagemaker/apps.go
new file mode 100644
index 00000000000000..54409cca63398e
--- /dev/null
+++ b/plugins/source/aws/resources/services/sagemaker/apps.go
@@ -0,0 +1,95 @@
+package sagemaker
+
+import (
+	"context"
+
+	"github.com/aws/aws-sdk-go-v2/service/sagemaker"
+	"github.com/aws/aws-sdk-go-v2/service/sagemaker/types"
+	"github.com/cloudquery/cloudquery/plugins/source/aws/client"
+	"github.com/cloudquery/plugin-sdk/schema"
+	"github.com/cloudquery/plugin-sdk/transformers"
+)
+
+func Apps() *schema.Table {
+	tableName := "aws_sagemaker_apps"
+	return &schema.Table{
+		Name:                tableName,
+		Description:         `https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_DescribeApp.html`,
+		Resolver:            fetchSagemakerApps,
+		PreResourceResolver: getApp,
+		Transform:           transformers.TransformWithStruct(&sagemaker.DescribeAppOutput{}, transformers.WithSkipFields("ResultMetadata")),
+		Multiplex:           client.ServiceAccountRegionMultiplexer(tableName, "api.sagemaker"),
+		Columns: []schema.Column{
+			client.DefaultAccountIDColumn(false),
+			client.DefaultRegionColumn(false),
+			{
+				Name:     "arn",
+				Type:     schema.TypeString,
+				Resolver: schema.PathResolver("AppArn"),
+				CreationOptions: schema.ColumnCreationOptions{
+					PrimaryKey: true,
+				},
+			},
+			{
+				Name:        "tags",
+				Type:        schema.TypeJSON,
+				Resolver:    resolveSagemakerAppTags,
+				Description: `The tags associated with the app.`,
+			},
+		},
+	}
+}
+func fetchSagemakerApps(ctx context.Context, meta schema.ClientMeta, _ *schema.Resource, res chan<- any) error {
+	c := meta.(*client.Client)
+	svc := c.Services().Sagemaker
+	paginator := sagemaker.NewListAppsPaginator(svc, &sagemaker.ListAppsInput{})
+	for paginator.HasMorePages() {
+		page, err := paginator.NextPage(ctx)
+		if err != nil {
+			return err
+		}
+		res <- page.Apps
+	}
+	return nil
+}
+
+func getApp(ctx context.Context, meta schema.ClientMeta, resource *schema.Resource) error {
+	c := meta.(*client.Client)
+	svc := c.Services().Sagemaker
+	n := resource.Item.(types.AppDetails)
+	input := &sagemaker.DescribeAppInput{
+		AppName:  n.AppName,
+		AppType:  n.AppType,
+		DomainId: n.DomainId,
+	}
+	if n.UserProfileName != nil {
+		input.UserProfileName = n.UserProfileName
+	}
+
+	if n.SpaceName != nil {
+		input.SpaceName = n.SpaceName
+	}
+
+	response, err := svc.DescribeApp(ctx, input)
+	if err != nil {
+		return err
+	}
+
+	resource.Item = response
+	return nil
+}
+
+func resolveSagemakerAppTags(ctx context.Context, meta schema.ClientMeta, resource *schema.Resource, _ schema.Column) error {
+	r := resource.Item.(*sagemaker.DescribeAppOutput)
+	c := meta.(*client.Client)
+	svc := c.Services().Sagemaker
+
+	response, err := svc.ListTags(ctx, &sagemaker.ListTagsInput{
+		ResourceArn: r.AppArn,
+	})
+	if err != nil {
+		return err
+	}
+
+	return resource.Set("tags", client.TagsToMap(response.Tags))
+}
diff --git a/website/pages/docs/plugins/sources/aws/tables.md b/website/pages/docs/plugins/sources/aws/tables.md
index 8cd404f55f5ab1..cde1aa254d0fe9 100644
--- a/website/pages/docs/plugins/sources/aws/tables.md
+++ b/website/pages/docs/plugins/sources/aws/tables.md
@@ -481,6 +481,7 @@
   - [aws_s3_bucket_grants](tables/aws_s3_bucket_grants)
   - [aws_s3_bucket_lifecycles](tables/aws_s3_bucket_lifecycles)
   - [aws_s3_bucket_websites](tables/aws_s3_bucket_websites)
+- [aws_sagemaker_apps](tables/aws_sagemaker_apps)
 - [aws_sagemaker_endpoint_configurations](tables/aws_sagemaker_endpoint_configurations)
 - [aws_sagemaker_models](tables/aws_sagemaker_models)
 - [aws_sagemaker_notebook_instances](tables/aws_sagemaker_notebook_instances)
diff --git a/website/tables/aws/aws_sagemaker_apps.md b/website/tables/aws/aws_sagemaker_apps.md
new file mode 100644
index 00000000000000..79becd82a48368
--- /dev/null
+++ b/website/tables/aws/aws_sagemaker_apps.md
@@ -0,0 +1,32 @@
+# Table: aws_sagemaker_apps
+
+This table shows data for Amazon SageMaker Apps.
+
+https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_DescribeApp.html
+
+The primary key for this table is **arn**.
+
+## Columns
+
+| Name          | Type          |
+| ------------- | ------------- |
+|_cq_source_name|String|
+|_cq_sync_time|Timestamp|
+|_cq_id|UUID|
+|_cq_parent_id|UUID|
+|account_id|String|
+|region|String|
+|arn (PK)|String|
+|tags|JSON|
+|app_arn|String|
+|app_name|String|
+|app_type|String|
+|creation_time|Timestamp|
+|domain_id|String|
+|failure_reason|String|
+|last_health_check_timestamp|Timestamp|
+|last_user_activity_timestamp|Timestamp|
+|resource_spec|JSON|
+|space_name|String|
+|status|String|
+|user_profile_name|String|
\ No newline at end of file


Test patch:
diff --git a/plugins/source/aws/resources/services/sagemaker/apps_mock_test.go b/plugins/source/aws/resources/services/sagemaker/apps_mock_test.go
new file mode 100644
index 00000000000000..5d6b8013b5c417
--- /dev/null
+++ b/plugins/source/aws/resources/services/sagemaker/apps_mock_test.go
@@ -0,0 +1,52 @@
+package sagemaker
+
+import (
+	"testing"
+
+	"github.com/aws/aws-sdk-go-v2/service/sagemaker"
+	types "github.com/aws/aws-sdk-go-v2/service/sagemaker/types"
+	"github.com/cloudquery/cloudquery/plugins/source/aws/client"
+	"github.com/cloudquery/cloudquery/plugins/source/aws/client/mocks"
+	"github.com/cloudquery/plugin-sdk/faker"
+	"github.com/golang/mock/gomock"
+)
+
+func buildSageMakerApps(t *testing.T, ctrl *gomock.Controller) client.Services {
+	m := mocks.NewMockSagemakerClient(ctrl)
+
+	appDets := types.AppDetails{}
+	if err := faker.FakeObject(&appDets); err != nil {
+		t.Fatal(err)
+	}
+
+	m.EXPECT().ListApps(gomock.Any(), gomock.Any(), gomock.Any()).Return(
+		&sagemaker.ListAppsOutput{Apps: []types.AppDetails{appDets}},
+		nil,
+	)
+
+	app := sagemaker.DescribeAppOutput{}
+	if err := faker.FakeObject(&app); err != nil {
+		t.Fatal(err)
+	}
+
+	m.EXPECT().DescribeApp(gomock.Any(), gomock.Any(), gomock.Any()).Return(
+		&app,
+		nil,
+	)
+
+	var tagsOut sagemaker.ListTagsOutput
+	if err := faker.FakeObject(&tagsOut); err != nil {
+		t.Fatal(err)
+	}
+	m.EXPECT().ListTags(gomock.Any(), &sagemaker.ListTagsInput{ResourceArn: app.AppArn}, gomock.Any()).Return(
+		&tagsOut, nil,
+	)
+
+	return client.Services{
+		Sagemaker: m,
+	}
+}
+
+func TestSageMakerApps(t *testing.T) {
+	client.AwsMockTestHelper(t, Apps(), buildSageMakerApps, client.TestOptions{})
+}