From 4b7caef24ffcfaf19e764ae30e46c1744bc3e215 Mon Sep 17 00:00:00 2001 From: denys-octopus <102932057+denys-octopus@users.noreply.github.com> Date: Mon, 21 Oct 2024 13:04:00 +1300 Subject: [PATCH] feat: Add support for OCI Registry feed (#801) --- docs/data-sources/feeds.md | 4 +- docs/resources/oci_registry_feed.md | 48 +++++ .../octopusdeploy_oci_registry_feed/import.sh | 1 + .../resource.tf | 6 + go.mod | 2 +- go.sum | 4 +- octopusdeploy_framework/framework_provider.go | 1 + .../resource_oci_registry_feed.go | 168 ++++++++++++++++++ .../resource_oci_registry_feed_test.go | 91 ++++++++++ octopusdeploy_framework/schemas/feeds.go | 6 +- .../schemas/oci_registry_feed.go | 41 +++++ 11 files changed, 365 insertions(+), 7 deletions(-) create mode 100644 docs/resources/oci_registry_feed.md create mode 100644 examples/resources/octopusdeploy_oci_registry_feed/import.sh create mode 100644 examples/resources/octopusdeploy_oci_registry_feed/resource.tf create mode 100644 octopusdeploy_framework/resource_oci_registry_feed.go create mode 100644 octopusdeploy_framework/resource_oci_registry_feed_test.go create mode 100644 octopusdeploy_framework/schemas/oci_registry_feed.go diff --git a/docs/data-sources/feeds.md b/docs/data-sources/feeds.md index 3cc2e6fd9..a87cebe57 100644 --- a/docs/data-sources/feeds.md +++ b/docs/data-sources/feeds.md @@ -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`, or `OctopusProject`. +- `feed_type` (String) A filter to search by feed type. Valid feed types are `AwsElasticContainerRegistry`, `BuiltIn`, `Docker`, `GitHub`, `Helm`, `Maven`, `NuGet`, `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. @@ -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`, or `OctopusProject`. +- `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_uri` (String) - `id` (String) The unique ID for this resource. - `is_enhanced_mode` (Boolean) diff --git a/docs/resources/oci_registry_feed.md b/docs/resources/oci_registry_feed.md new file mode 100644 index 000000000..989b475ef --- /dev/null +++ b/docs/resources/oci_registry_feed.md @@ -0,0 +1,48 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "octopusdeploy_oci_registry_feed Resource - terraform-provider-octopusdeploy" +subcategory: "" +description: |- + This resource manages a OCI Registry feed in Octopus Deploy. +--- + +# octopusdeploy_oci_registry_feed (Resource) + +This resource manages a OCI Registry feed in Octopus Deploy. + +## Example Usage + +```terraform +resource "octopusdeploy_oci_registry_feed" "example" { + feed_uri = "oci://test-registry.docker.io" + password = "test-password" + name = "Test oci Registry Feed (OK to Delete)" + username = "test-username" +} +``` + + +## Schema + +### Required + +- `feed_uri` (String) +- `name` (String) The name of this resource. + +### Optional + +- `password` (String, Sensitive) The password associated with this resource. +- `space_id` (String) The space ID associated with this OCI registry. +- `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_oci_registry_feed. +``` diff --git a/examples/resources/octopusdeploy_oci_registry_feed/import.sh b/examples/resources/octopusdeploy_oci_registry_feed/import.sh new file mode 100644 index 000000000..82dd4eace --- /dev/null +++ b/examples/resources/octopusdeploy_oci_registry_feed/import.sh @@ -0,0 +1 @@ +terraform import [options] octopusdeploy_oci_registry_feed. \ No newline at end of file diff --git a/examples/resources/octopusdeploy_oci_registry_feed/resource.tf b/examples/resources/octopusdeploy_oci_registry_feed/resource.tf new file mode 100644 index 000000000..d5fa314ff --- /dev/null +++ b/examples/resources/octopusdeploy_oci_registry_feed/resource.tf @@ -0,0 +1,6 @@ +resource "octopusdeploy_oci_registry_feed" "example" { + feed_uri = "oci://test-registry.docker.io" + password = "test-password" + name = "Test oci Registry Feed (OK to Delete)" + username = "test-username" +} diff --git a/go.mod b/go.mod index 287316132..d04cf17c4 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/OctopusDeploy/terraform-provider-octopusdeploy go 1.21 require ( - github.com/OctopusDeploy/go-octopusdeploy/v2 v2.52.1 + github.com/OctopusDeploy/go-octopusdeploy/v2 v2.53.1 github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework v0.0.0-20240729041805-46db6fb717b4 github.com/google/uuid v1.6.0 github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637 diff --git a/go.sum b/go.sum index 03720a904..178816a3a 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,8 @@ github.com/Microsoft/hcsshim v0.12.4 h1:Ev7YUMHAHoWNm+aDSPzc5W9s6E2jyL1szpVDJeZ/ github.com/Microsoft/hcsshim v0.12.4/go.mod h1:Iyl1WVpZzr+UkzjekHZbV8o5Z9ZkxNGx6CtY2Qg/JVQ= github.com/OctopusDeploy/go-octodiff v1.0.0 h1:U+ORg6azniwwYo+O44giOw6TiD5USk8S4VDhOQ0Ven0= github.com/OctopusDeploy/go-octodiff v1.0.0/go.mod h1:Mze0+EkOWTgTmi8++fyUc6r0aLZT7qD9gX+31t8MmIU= -github.com/OctopusDeploy/go-octopusdeploy/v2 v2.52.1 h1:GeWNIPn59JZggkjZD/VKpt3oJNuYezdJPbIqyl+MVRw= -github.com/OctopusDeploy/go-octopusdeploy/v2 v2.52.1/go.mod h1:ggvOXzMnq+w0pLg6C9zdjz6YBaHfO3B3tqmmB7JQdaw= +github.com/OctopusDeploy/go-octopusdeploy/v2 v2.53.1 h1:9c5qKji5R/sFmjqVQ1Nxt+vKITsj42CCCs0bfqJvETc= +github.com/OctopusDeploy/go-octopusdeploy/v2 v2.53.1/go.mod h1:ggvOXzMnq+w0pLg6C9zdjz6YBaHfO3B3tqmmB7JQdaw= github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework v0.0.0-20240729041805-46db6fb717b4 h1:QfbVf0bOIRMp/WHAWsuVDB7KHoWnRsGbvDuOf2ua7k4= github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework v0.0.0-20240729041805-46db6fb717b4/go.mod h1:Oq9KbiRNDBB5jFmrwnrgLX0urIqR/1ptY18TzkqXm7M= github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg= diff --git a/octopusdeploy_framework/framework_provider.go b/octopusdeploy_framework/framework_provider.go index 4ec3df754..39280ab13 100644 --- a/octopusdeploy_framework/framework_provider.go +++ b/octopusdeploy_framework/framework_provider.go @@ -84,6 +84,7 @@ func (p *octopusDeployFrameworkProvider) Resources(ctx context.Context) []func() NewSpaceResource, NewProjectGroupResource, NewMavenFeedResource, + NewOCIRegistryFeedResource, NewLifecycleResource, NewEnvironmentResource, NewStepTemplateResource, diff --git a/octopusdeploy_framework/resource_oci_registry_feed.go b/octopusdeploy_framework/resource_oci_registry_feed.go new file mode 100644 index 000000000..47edca613 --- /dev/null +++ b/octopusdeploy_framework/resource_oci_registry_feed.go @@ -0,0 +1,168 @@ +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 ociRegistryFeedTypeResource struct { + *Config +} + +func NewOCIRegistryFeedResource() resource.Resource { + return &ociRegistryFeedTypeResource{} +} + +var _ resource.ResourceWithImportState = &ociRegistryFeedTypeResource{} + +func (r *ociRegistryFeedTypeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = util.GetTypeName("oci_registry_feed") +} + +func (r *ociRegistryFeedTypeResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schemas.OCIRegistryFeedSchema{}.GetResourceSchema() +} + +func (r *ociRegistryFeedTypeResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.Config = ResourceConfiguration(req, resp) +} + +func (r *ociRegistryFeedTypeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data *schemas.OCIRegistryFeedTypeResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + feed, err := createOCIRegistryResourceFromData(data) + if err != nil { + return + } + + tflog.Info(ctx, fmt.Sprintf("creating OCI Registry feed: %s", feed.GetName())) + + client := r.Config.Client + createdFeed, err := feeds.Add(client, feed) + if err != nil { + resp.Diagnostics.AddError("unable to create OCI Registry feed", err.Error()) + return + } + + updateDataFromOCIRegistryFeed(data, data.SpaceID.ValueString(), createdFeed.(*feeds.OCIRegistryFeed)) + + tflog.Info(ctx, fmt.Sprintf("OCI Registry feed created (%s)", data.ID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ociRegistryFeedTypeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data *schemas.OCIRegistryFeedTypeResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Info(ctx, fmt.Sprintf("reading OCI Registry 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, "OCI Registry feed"); err != nil { + resp.Diagnostics.AddError("unable to load OCI Registry feed", err.Error()) + } + return + } + + loadedFeed := feed.(*feeds.OCIRegistryFeed) + updateDataFromOCIRegistryFeed(data, data.SpaceID.ValueString(), loadedFeed) + + tflog.Info(ctx, fmt.Sprintf("OCI Registry feed read (%s)", loadedFeed.GetID())) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ociRegistryFeedTypeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data, state *schemas.OCIRegistryFeedTypeResourceModel + 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 OCI Registry feed '%s'", data.ID.ValueString())) + + feed, err := createOCIRegistryResourceFromData(data) + feed.ID = state.ID.ValueString() + if err != nil { + resp.Diagnostics.AddError("unable to load OCI Registry feed", err.Error()) + return + } + + tflog.Info(ctx, fmt.Sprintf("updating OCI Registry feed (%s)", data.ID)) + + client := r.Config.Client + updatedFeed, err := feeds.Update(client, feed) + if err != nil { + resp.Diagnostics.AddError("unable to update OCI Registry feed", err.Error()) + return + } + + updateDataFromOCIRegistryFeed(data, state.SpaceID.ValueString(), updatedFeed.(*feeds.OCIRegistryFeed)) + + tflog.Info(ctx, fmt.Sprintf("OCI Registry feed updated (%s)", data.ID)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ociRegistryFeedTypeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data schemas.OCIRegistryFeedTypeResourceModel + + 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 OCI Registry feed", err.Error()) + return + } +} + +func createOCIRegistryResourceFromData(data *schemas.OCIRegistryFeedTypeResourceModel) (*feeds.OCIRegistryFeed, error) { + feed, err := feeds.NewOCIRegistryFeed(data.Name.ValueString()) + if err != nil { + return nil, err + } + + feed.ID = data.ID.ValueString() + feed.FeedURI = data.FeedUri.ValueString() + + feed.Username = data.Username.ValueString() + feed.Password = core.NewSensitiveValue(data.Password.ValueString()) + feed.SpaceID = data.SpaceID.ValueString() + + return feed, nil +} + +func updateDataFromOCIRegistryFeed(data *schemas.OCIRegistryFeedTypeResourceModel, spaceId string, feed *feeds.OCIRegistryFeed) { + data.FeedUri = types.StringValue(feed.FeedURI) + 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 (*ociRegistryFeedTypeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/octopusdeploy_framework/resource_oci_registry_feed_test.go b/octopusdeploy_framework/resource_oci_registry_feed_test.go new file mode 100644 index 000000000..6c65f0a87 --- /dev/null +++ b/octopusdeploy_framework/resource_oci_registry_feed_test.go @@ -0,0 +1,91 @@ +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" + "testing" +) + +type ociRegistryFeedTestData struct { + name string + uri string + username string + password string +} + +func TestAccOctopusDeployOCIRegistryFeed(t *testing.T) { + localName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) + prefix := "octopusdeploy_oci_registry_feed." + localName + createData := ociRegistryFeedTestData{ + name: acctest.RandStringFromCharSet(20, acctest.CharSetAlpha), + uri: "oci://integration-test-registry.docker.io", + username: acctest.RandStringFromCharSet(20, acctest.CharSetAlpha), + password: acctest.RandStringFromCharSet(20, acctest.CharSetAlphaNum), + } + updateData := ociRegistryFeedTestData{ + name: createData.name + "-updated", + uri: "oci://integration-test-registry-updated.docker.io", + username: createData.username + "-changed", + password: createData.password + "-generated", + } + + resource.Test(t, resource.TestCase{ + CheckDestroy: func(s *terraform.State) error { return testOCIRegistryFeedCheckDestroy(s) }, + PreCheck: func() { TestAccPreCheck(t) }, + ProtoV6ProviderFactories: ProtoV6ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: testOCIRegistryFeedBasic(createData, localName), + Check: testAssertOCIRegistryAttributes(createData, prefix), + }, + { + Config: testOCIRegistryFeedBasic(updateData, localName), + Check: testAssertOCIRegistryAttributes(updateData, prefix), + }, + }, + }) +} + +func testAssertOCIRegistryAttributes(expected ociRegistryFeedTestData, prefix string) resource.TestCheckFunc { + return resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(prefix, "name", expected.name), + resource.TestCheckResourceAttr(prefix, "feed_uri", expected.uri), + resource.TestCheckResourceAttr(prefix, "username", expected.username), + resource.TestCheckResourceAttr(prefix, "password", expected.password), + ) +} + +func testOCIRegistryFeedBasic(data ociRegistryFeedTestData, localName string) string { + return fmt.Sprintf(` + resource "octopusdeploy_oci_registry_feed" "%s" { + name = "%s" + feed_uri = "%s" + username = "%s" + password = "%s" + } + `, + localName, + data.name, + data.uri, + data.username, + data.password, + ) +} + +func testOCIRegistryFeedCheckDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "octopusdeploy_oci_registry_feed" { + continue + } + + feed, err := feeds.GetByID(octoClient, octoClient.GetSpaceID(), rs.Primary.ID) + if err == nil && feed != nil { + return fmt.Errorf("OCI Registry feed (%s) still exists", rs.Primary.ID) + } + } + + return nil +} diff --git a/octopusdeploy_framework/schemas/feeds.go b/octopusdeploy_framework/schemas/feeds.go index b0c9d775e..04ab6ab27 100644 --- a/octopusdeploy_framework/schemas/feeds.go +++ b/octopusdeploy_framework/schemas/feeds.go @@ -21,7 +21,7 @@ func (f FeedsSchema) GetDatasourceSchema() datasourceSchema.Schema { Description: "Provides information about existing feeds.", Attributes: map[string]datasourceSchema.Attribute{ "feed_type": datasourceSchema.StringAttribute{ - Description: "A filter to search by feed type. Valid feed types are `AwsElasticContainerRegistry`, `BuiltIn`, `Docker`, `GitHub`, `Helm`, `Maven`, `NuGet`, or `OctopusProject`.", + Description: "A filter to search by feed type. Valid feed types are `AwsElasticContainerRegistry`, `BuiltIn`, `Docker`, `GitHub`, `Helm`, `Maven`, `NuGet`, `OciRegistry` or `OctopusProject`.", Optional: true, Validators: []validator.String{ stringvalidator.OneOf( @@ -32,6 +32,7 @@ func (f FeedsSchema) GetDatasourceSchema() datasourceSchema.Schema { "Helm", "Maven", "NuGet", + "OciRegistry", "OctopusProject"), }, }, @@ -50,7 +51,7 @@ func (f FeedsSchema) GetDatasourceSchema() datasourceSchema.Schema { NestedObject: datasourceSchema.NestedAttributeObject{ Attributes: map[string]datasourceSchema.Attribute{ "feed_type": datasourceSchema.StringAttribute{ - Description: "A filter to search by feed type. Valid feed types are `AwsElasticContainerRegistry`, `BuiltIn`, `Docker`, `GitHub`, `Helm`, `Maven`, `NuGet`, or `OctopusProject`.", + Description: "A filter to search by feed type. Valid feed types are `AwsElasticContainerRegistry`, `BuiltIn`, `Docker`, `GitHub`, `Helm`, `Maven`, `NuGet`, `OciRegistry` or `OctopusProject`.", Computed: true, Validators: []validator.String{ stringvalidator.OneOf( @@ -61,6 +62,7 @@ func (f FeedsSchema) GetDatasourceSchema() datasourceSchema.Schema { "Helm", "Maven", "NuGet", + "OciRegistry", "OctopusProject"), }, }, diff --git a/octopusdeploy_framework/schemas/oci_registry_feed.go b/octopusdeploy_framework/schemas/oci_registry_feed.go new file mode 100644 index 000000000..f6df51e55 --- /dev/null +++ b/octopusdeploy_framework/schemas/oci_registry_feed.go @@ -0,0 +1,41 @@ +package schemas + +import ( + datasourceSchema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + resourceSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +const ociRegistryFeedDescription = "OCI registry" + +type OCIRegistryFeedSchema struct{} + +func (m OCIRegistryFeedSchema) GetResourceSchema() resourceSchema.Schema { + return resourceSchema.Schema{ + Description: "This resource manages a OCI Registry feed in Octopus Deploy.", + Attributes: map[string]resourceSchema.Attribute{ + "feed_uri": GetFeedUriResourceSchema(), + "id": GetIdResourceSchema(), + "name": GetNameResourceSchema(true), + "password": GetPasswordResourceSchema(false), + "space_id": GetSpaceIdResourceSchema(ociRegistryFeedDescription), + "username": GetUsernameResourceSchema(false), + }, + } +} + +func (m OCIRegistryFeedSchema) GetDatasourceSchema() datasourceSchema.Schema { + return datasourceSchema.Schema{} +} + +var _ EntitySchema = OCIRegistryFeedSchema{} + +type OCIRegistryFeedTypeResourceModel struct { + FeedUri types.String `tfsdk:"feed_uri"` + Name types.String `tfsdk:"name"` + Password types.String `tfsdk:"password"` + SpaceID types.String `tfsdk:"space_id"` + Username types.String `tfsdk:"username"` + + ResourceModel +}