Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Amazon S3 Bucket feed #804

Merged
merged 5 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/data-sources/feeds.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ data "octopusdeploy_feeds" "example" {

### Optional

- `feed_type` (String) A filter to search by feed type. Valid feed types are `AwsElasticContainerRegistry`, `BuiltIn`, `Docker`, `GitHub`, `Helm`, `Maven`, `NuGet`, `OciRegistry` or `OctopusProject`.
- `feed_type` (String) A filter to search by feed type. Valid feed types are `AwsElasticContainerRegistry`, `BuiltIn`, `Docker`, `GitHub`, `Helm`, `Maven`, `NuGet`, `S3`, `OciRegistry` or `OctopusProject`.
- `ids` (List of String) A filter to search by a list of IDs.
- `name` (String) The name of this resource.
- `partial_name` (String) A filter to search by a partial name.
Expand All @@ -49,7 +49,7 @@ Read-Only:
- `delete_unreleased_packages_after_days` (Number)
- `download_attempts` (Number) The number of times a deployment should attempt to download a package from this feed before failing.
- `download_retry_backoff_seconds` (Number) The number of seconds to apply as a linear back off between download attempts.
- `feed_type` (String) A filter to search by feed type. Valid feed types are `AwsElasticContainerRegistry`, `BuiltIn`, `Docker`, `GitHub`, `Helm`, `Maven`, `NuGet`, `OciRegistry` or `OctopusProject`.
- `feed_type` (String) A filter to search by feed type. Valid feed types are `AwsElasticContainerRegistry`, `BuiltIn`, `Docker`, `GitHub`, `Helm`, `Maven`, `NuGet`, `S3`, `OciRegistry` or `OctopusProject`.
- `feed_uri` (String)
- `id` (String) The unique ID for this resource.
- `is_enhanced_mode` (Boolean)
Expand All @@ -60,4 +60,5 @@ Read-Only:
- `registry_path` (String)
- `secret_key` (String, Sensitive)
- `space_id` (String) The space ID associated with this feeds.
- `use_machine_credentials` (Boolean) When true will use Amazon Web Services credentials configured on the worker.
- `username` (String, Sensitive) The username associated with this resource.
50 changes: 50 additions & 0 deletions docs/resources/s3_feed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "octopusdeploy_s3_feed Resource - terraform-provider-octopusdeploy"
subcategory: ""
description: |-
This resource manages a OCI Registry feed in Octopus Deploy.
---

# octopusdeploy_s3_feed (Resource)

This resource manages a OCI Registry feed in Octopus Deploy.

## Example Usage

```terraform
resource "octopusdeploy_s3_feed" "example" {
name = "AWS S3 Bucket (Ok Delete)"
use_machine_credentials = false
access_key = "given_access_key"
secret_key = "some_secret_key"
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `name` (String) The name of this resource.
- `use_machine_credentials` (Boolean) When true will use credentials configured on the worker

### Optional

- `access_key` (String) The AWS access key to use when authenticating against Amazon Web Services
- `password` (String, Sensitive) The password associated with this resource.
- `secret_key` (String, Sensitive) The AWS secret key to use when authenticating against Amazon Web Services.
- `space_id` (String) The space ID associated with this AWS S3 Bucket Feed.
- `username` (String, Sensitive) The username associated with this resource.

### Read-Only

- `id` (String) The unique ID for this resource.

## Import

Import is supported using the following syntax:

```shell
terraform import [options] octopusdeploy_s3_feed.<name> <feed-id>
```
1 change: 1 addition & 0 deletions examples/resources/octopusdeploy_s3_feed/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import [options] octopusdeploy_s3_feed.<name> <feed-id>
6 changes: 6 additions & 0 deletions examples/resources/octopusdeploy_s3_feed/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
resource "octopusdeploy_s3_feed" "example" {
name = "AWS S3 Bucket (Ok Delete)"
use_machine_credentials = false
access_key = "given_access_key"
secret_key = "some_secret_key"
}
1 change: 1 addition & 0 deletions octopusdeploy_framework/framework_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func (p *octopusDeployFrameworkProvider) Resources(ctx context.Context) []func()
NewProjectGroupResource,
NewMavenFeedResource,
NewOCIRegistryFeedResource,
NewS3FeedResource,
NewLifecycleResource,
NewEnvironmentResource,
NewStepTemplateResource,
Expand Down
170 changes: 170 additions & 0 deletions octopusdeploy_framework/resource_s3_feed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package octopusdeploy_framework

import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/path"

"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/feeds"
"github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/errors"
"github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/schemas"
"github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)

type s3FeedTypeResource struct {
*Config
}

func NewS3FeedResource() resource.Resource {
return &s3FeedTypeResource{}
}

var _ resource.ResourceWithImportState = &s3FeedTypeResource{}

func (r *s3FeedTypeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = util.GetTypeName("s3_feed")
}

func (r *s3FeedTypeResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schemas.S3FeedSchema{}.GetResourceSchema()
}

func (r *s3FeedTypeResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
r.Config = ResourceConfiguration(req, resp)
}

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

feed, err := createS3ResourceFromData(data)
if err != nil {
return
}

tflog.Info(ctx, fmt.Sprintf("creating S3 feed: %s", feed.GetName()))

client := r.Config.Client
createdFeed, err := feeds.Add(client, feed)
if err != nil {
resp.Diagnostics.AddError("unable to create S3 feed", err.Error())
return
}

updateDataFromS3Feed(data, data.SpaceID.ValueString(), createdFeed.(*feeds.S3Feed))

tflog.Info(ctx, fmt.Sprintf("S3 feed created (%s)", data.ID))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

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

tflog.Info(ctx, fmt.Sprintf("reading S3 feed (%s)", data.ID))

client := r.Config.Client
feed, err := feeds.GetByID(client, data.SpaceID.ValueString(), data.ID.ValueString())
if err != nil {
if err := errors.ProcessApiErrorV2(ctx, resp, data, err, "S3 feed"); err != nil {
resp.Diagnostics.AddError("unable to load S3 feed", err.Error())
}
return
}

loadedFeed := feed.(*feeds.S3Feed)
updateDataFromS3Feed(data, data.SpaceID.ValueString(), loadedFeed)

tflog.Info(ctx, fmt.Sprintf("S3 feed read (%s)", loadedFeed.GetID()))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *s3FeedTypeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data, state *schemas.S3FeedTypeResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

tflog.Debug(ctx, fmt.Sprintf("updating S3 feed '%s'", data.ID.ValueString()))

feed, err := createS3ResourceFromData(data)
feed.ID = state.ID.ValueString()
if err != nil {
resp.Diagnostics.AddError("unable to load S3 feed", err.Error())
return
}

tflog.Info(ctx, fmt.Sprintf("updating S3 feed (%s)", data.ID))

client := r.Config.Client
updatedFeed, err := feeds.Update(client, feed)
if err != nil {
resp.Diagnostics.AddError("unable to update S3 feed", err.Error())
return
}

updateDataFromS3Feed(data, state.SpaceID.ValueString(), updatedFeed.(*feeds.S3Feed))

tflog.Info(ctx, fmt.Sprintf("S3 feed updated (%s)", data.ID))

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

func (r *s3FeedTypeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data schemas.S3FeedTypeResourceModel

resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

if err := feeds.DeleteByID(r.Config.Client, data.SpaceID.ValueString(), data.ID.ValueString()); err != nil {
resp.Diagnostics.AddError("unable to delete S3 feed", err.Error())
return
}
}

func createS3ResourceFromData(data *schemas.S3FeedTypeResourceModel) (*feeds.S3Feed, error) {
feed, err := feeds.NewS3Feed(data.Name.ValueString(), data.AccessKey.ValueString(), core.NewSensitiveValue(data.SecretKey.ValueString()), data.UseMachineCredentials.ValueBool())
if err != nil {
return nil, err
}

feed.ID = data.ID.ValueString()

feed.Username = data.Username.ValueString()
feed.Password = core.NewSensitiveValue(data.Password.ValueString())
feed.SpaceID = data.SpaceID.ValueString()

return feed, nil
}

func updateDataFromS3Feed(data *schemas.S3FeedTypeResourceModel, spaceId string, feed *feeds.S3Feed) {
data.UseMachineCredentials = types.BoolValue(feed.UseMachineCredentials)
if feed.AccessKey != "" {
data.AccessKey = types.StringValue(feed.AccessKey)
}
data.Name = types.StringValue(feed.Name)
data.SpaceID = types.StringValue(spaceId)
if feed.Username != "" {
data.Username = types.StringValue(feed.Username)
}

data.ID = types.StringValue(feed.ID)
}

func (*s3FeedTypeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
122 changes: 122 additions & 0 deletions octopusdeploy_framework/resource_s3_feed_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package octopusdeploy_framework

import (
"fmt"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/feeds"
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"strconv"
"testing"
)

type s3FeedTestData struct {
name string
useMachineCredentials bool
accessKey string
secretKey string
}

func TestAccOctopusDeployS3Feed(t *testing.T) {
localName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
prefix := "octopusdeploy_s3_feed." + localName
createData := s3FeedTestData{
name: acctest.RandStringFromCharSet(20, acctest.CharSetAlpha),
useMachineCredentials: false,
accessKey: acctest.RandStringFromCharSet(20, acctest.CharSetAlpha),
secretKey: acctest.RandStringFromCharSet(20, acctest.CharSetAlphaNum),
}
updateData := s3FeedTestData{
name: createData.name + "-updated",
useMachineCredentials: true,
accessKey: createData.accessKey + "-changed",
secretKey: createData.secretKey + "-generated",
}
withoutKeysData := s3FeedTestData{
name: "AWS S3 Without Keys",
useMachineCredentials: true,
}

resource.Test(t, resource.TestCase{
CheckDestroy: func(s *terraform.State) error { return testS3FeedCheckDestroy(s) },
PreCheck: func() { TestAccPreCheck(t) },
ProtoV6ProviderFactories: ProtoV6ProviderFactories(),
Steps: []resource.TestStep{
{
Config: testS3FeedBasic(createData, localName),
Check: testAssertS3FeedAttributes(createData, prefix),
},
{
Config: testS3FeedBasic(updateData, localName),
Check: testAssertS3FeedAttributes(updateData, prefix),
},
{
Config: testS3FeedWithoutKeys(withoutKeysData, localName),
Check: testAssertS3FeedWithoutKeysAttributes(withoutKeysData, prefix),
},
},
})
}

func testAssertS3FeedAttributes(expected s3FeedTestData, prefix string) resource.TestCheckFunc {
return resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(prefix, "name", expected.name),
resource.TestCheckResourceAttr(prefix, "use_machine_credentials", strconv.FormatBool(expected.useMachineCredentials)),
resource.TestCheckResourceAttr(prefix, "access_key", expected.accessKey),
resource.TestCheckResourceAttr(prefix, "secret_key", expected.secretKey),
)
}

func testS3FeedBasic(data s3FeedTestData, localName string) string {
return fmt.Sprintf(`
resource "octopusdeploy_s3_feed" "%s" {
name = "%s"
use_machine_credentials = "%s"
access_key = "%s"
secret_key = "%s"
}
`,
localName,
data.name,
strconv.FormatBool(data.useMachineCredentials),
data.accessKey,
data.secretKey,
)
}

func testS3FeedWithoutKeys(data s3FeedTestData, localName string) string {
return fmt.Sprintf(`
resource "octopusdeploy_s3_feed" "%s" {
name = "%s"
use_machine_credentials = "%s"
}
`,
localName,
data.name,
strconv.FormatBool(data.useMachineCredentials),
)
}

func testAssertS3FeedWithoutKeysAttributes(expected s3FeedTestData, prefix string) resource.TestCheckFunc {
return resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(prefix, "name", expected.name),
resource.TestCheckResourceAttr(prefix, "use_machine_credentials", strconv.FormatBool(expected.useMachineCredentials)),
resource.TestCheckNoResourceAttr(prefix, "access_key"),
resource.TestCheckNoResourceAttr(prefix, "secret_key"),
)
}

func testS3FeedCheckDestroy(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != "octopusdeploy_s3_feed" {
continue
}

feed, err := feeds.GetByID(octoClient, octoClient.GetSpaceID(), rs.Primary.ID)
if err == nil && feed != nil {
return fmt.Errorf("s3 feed (%s) still exists", rs.Primary.ID)
}
}

return nil
}
Loading
Loading