Skip to content

Commit

Permalink
Merge pull request #35758 from bschaatsbergen/f/start-deployment-app-…
Browse files Browse the repository at this point in the history
…runner

[New Resource] - Implement `aws_apprunner_deployment`
  • Loading branch information
ewbankkit authored Feb 23, 2024
2 parents 9449d39 + c970543 commit b9d9303
Show file tree
Hide file tree
Showing 6 changed files with 354 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .changelog/35758.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
aws_apprunner_deployment
```
6 changes: 6 additions & 0 deletions internal/framework/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ func (w *WithNoOpUpdate[T]) Update(ctx context.Context, request resource.UpdateR
response.Diagnostics.Append(response.State.Set(ctx, &t)...)
}

// WithNoOpUpdate is intended to be embedded in resources which have no need of a custom Delete method.
type WithNoOpDelete struct{}

func (w *WithNoOpDelete) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) {
}

// DataSourceWithConfigure is a structure to be embedded within a DataSource that implements the DataSourceWithConfigure interface.
type DataSourceWithConfigure struct {
withMeta
Expand Down
247 changes: 247 additions & 0 deletions internal/service/apprunner/deployment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package apprunner

import (
"context"
"fmt"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/apprunner"
awstypes "github.com/aws/aws-sdk-go-v2/service/apprunner/types"
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"github.com/hashicorp/terraform-provider-aws/internal/enum"
"github.com/hashicorp/terraform-provider-aws/internal/errs"
"github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag"
"github.com/hashicorp/terraform-provider-aws/internal/framework"
fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex"
fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types"
tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
"github.com/hashicorp/terraform-provider-aws/names"
)

// @FrameworkResource(name="Deployment")
func newDeploymentResource(context.Context) (resource.ResourceWithConfigure, error) {
r := &deploymentResource{}

r.SetDefaultCreateTimeout(20 * time.Minute)

return r, nil
}

type deploymentResource struct {
framework.ResourceWithConfigure
framework.WithNoUpdate
framework.WithNoOpDelete
framework.WithTimeouts
}

func (r *deploymentResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) {
response.TypeName = "aws_apprunner_deployment"
}

func (r *deploymentResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) {
response.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
names.AttrID: framework.IDAttribute(),
"operation_id": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"service_arn": schema.StringAttribute{
CustomType: fwtypes.ARNType,
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"status": schema.StringAttribute{
Computed: true,
},
},
Blocks: map[string]schema.Block{
"timeouts": timeouts.Block(ctx, timeouts.Opts{
Create: true,
}),
},
}
}

func (r *deploymentResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data deploymentResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

conn := r.Meta().AppRunnerClient(ctx)

serviceARN := data.ServiceARN.ValueString()
input := &apprunner.StartDeploymentInput{
ServiceArn: aws.String(serviceARN),
}

output, err := conn.StartDeployment(ctx, input)

if err != nil {
resp.Diagnostics.AddError(fmt.Sprintf("starting App Runner Deployment (%s)", serviceARN), err.Error())

return
}

// Set values for unknowns.
operationID := aws.ToString(output.OperationId)
data.OperationID = types.StringValue(operationID)
data.setID()

createTimeout := r.CreateTimeout(ctx, data.Timeouts)

op, err := waitDeploymentSucceeded(ctx, conn, serviceARN, operationID, createTimeout)

if err != nil {
resp.Diagnostics.AddError(fmt.Sprintf("waiting for App Runner Deployment (%s/%s)", serviceARN, operationID), err.Error())

return
}

// Set values for unknowns.
data.Status = fwflex.StringValueToFramework(ctx, op.Status)

resp.Diagnostics.Append(resp.State.Set(ctx, data)...)
}

func (r *deploymentResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data deploymentResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

conn := r.Meta().AppRunnerClient(ctx)

serviceARN, operationID := data.ServiceARN.ValueString(), data.OperationID.ValueString()
output, err := findOperationByTwoPartKey(ctx, conn, serviceARN, operationID)

if tfresource.NotFound(err) {
resp.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err))
resp.State.RemoveResource(ctx)

return
}

if err != nil {
resp.Diagnostics.AddError(fmt.Sprintf("reading App Runner Deployment (%s/%s)", serviceARN, operationID), err.Error())

return
}

data.Status = fwflex.StringValueToFramework(ctx, output.Status)

resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func findOperationByTwoPartKey(ctx context.Context, conn *apprunner.Client, serviceARN, operationID string) (*awstypes.OperationSummary, error) {
input := &apprunner.ListOperationsInput{
ServiceArn: aws.String(serviceARN),
}

return findOperation(ctx, conn, input, func(v *awstypes.OperationSummary) bool {
return aws.ToString(v.Id) == operationID
})
}

func findOperation(ctx context.Context, conn *apprunner.Client, input *apprunner.ListOperationsInput, filter tfslices.Predicate[*awstypes.OperationSummary]) (*awstypes.OperationSummary, error) {
output, err := findOperations(ctx, conn, input, filter)

if err != nil {
return nil, err
}

return tfresource.AssertSingleValueResult(output)
}

func findOperations(ctx context.Context, conn *apprunner.Client, input *apprunner.ListOperationsInput, filter tfslices.Predicate[*awstypes.OperationSummary]) ([]awstypes.OperationSummary, error) {
var output []awstypes.OperationSummary

pages := apprunner.NewListOperationsPaginator(conn, input)
for pages.HasMorePages() {
page, err := pages.NextPage(ctx)

if errs.IsA[*awstypes.ResourceNotFoundException](err) {
return nil, &retry.NotFoundError{
LastError: err,
LastRequest: input,
}
}

if err != nil {
return nil, err
}

for _, v := range page.OperationSummaryList {
if filter(&v) {
output = append(output, v)
}
}
}

return output, nil
}

func statusOperation(ctx context.Context, conn *apprunner.Client, serviceARN, operationID string) retry.StateRefreshFunc {
return func() (interface{}, string, error) {
output, err := findOperationByTwoPartKey(ctx, conn, serviceARN, operationID)

if tfresource.NotFound(err) {
return nil, "", nil
}

if err != nil {
return nil, "", err
}

return output, string(output.Status), nil
}
}

func waitDeploymentSucceeded(ctx context.Context, conn *apprunner.Client, serviceARN, operationID string, timeout time.Duration) (*awstypes.OperationSummary, error) {
stateConf := &retry.StateChangeConf{
Pending: enum.Slice(awstypes.OperationStatusPending, awstypes.OperationStatusInProgress),
Target: enum.Slice(awstypes.OperationStatusSucceeded),
Refresh: statusOperation(ctx, conn, serviceARN, operationID),
Timeout: timeout,
PollInterval: 30 * time.Second,
NotFoundChecks: 30,
}

outputRaw, err := stateConf.WaitForStateContext(ctx)

if output, ok := outputRaw.(*awstypes.OperationSummary); ok {
return output, err
}

return nil, err
}

type deploymentResourceModel struct {
ID types.String `tfsdk:"id"`
OperationID types.String `tfsdk:"operation_id"`
ServiceARN fwtypes.ARN `tfsdk:"service_arn"`
Status types.String `tfsdk:"status"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
}

func (data *deploymentResourceModel) setID() {
data.ID = data.OperationID
}
61 changes: 61 additions & 0 deletions internal/service/apprunner/deployment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package apprunner_test

import (
"fmt"
"testing"

"github.com/aws/aws-sdk-go-v2/service/apprunner/types"
sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-provider-aws/internal/acctest"
"github.com/hashicorp/terraform-provider-aws/names"
)

func TestAccAppRunnerDeployment_basic(t *testing.T) {
ctx := acctest.Context(t)
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
resourceName := "aws_apprunner_deployment.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.AppRunnerServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: acctest.CheckDestroyNoop,
Steps: []resource.TestStep{
{
Config: testAccDeployment_basic(rName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(resourceName, "id"),
resource.TestCheckResourceAttrSet(resourceName, "operation_id"),
resource.TestCheckResourceAttr(resourceName, "status", string(types.OperationStatusSucceeded)),
),
},
},
})
}

func testAccDeployment_basic(rName string) string {
return fmt.Sprintf(`
resource "aws_apprunner_service" "test" {
service_name = %[1]q
source_configuration {
auto_deployments_enabled = false
image_repository {
image_configuration {
port = "80"
}
image_identifier = "public.ecr.aws/nginx/nginx:latest"
image_repository_type = "ECR_PUBLIC"
}
}
}
resource "aws_apprunner_deployment" "test" {
service_arn = aws_apprunner_service.test.arn
}
`, rName)
}
4 changes: 4 additions & 0 deletions internal/service/apprunner/service_package_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions website/docs/r/apprunner_deployment.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
subcategory: "App Runner"
layout: "aws"
page_title: "AWS: aws_apprunner_deployment"
description: |-
Manages an App Runner Deployment Operation.
---

# Resource: aws_apprunner_deployment

Manages an App Runner Deployment Operation.

## Example Usage

```terraform
resource "aws_apprunner_deployment" "example" {
service_arn = aws_apprunner_service.example.arn
}
```

## Argument Reference

The following arguments supported:

* `service_arn` - (Required) The Amazon Resource Name (ARN) of the App Runner service to start the deployment for.

## Attribute Reference

This resource exports the following attributes in addition to the arguments above:

* `id` - A unique identifier for the deployment.
* `operation_id` - The unique ID of the operation associated with deployment.
* `status` - The current status of the App Runner service deployment.

0 comments on commit b9d9303

Please sign in to comment.