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 location resource #2

Merged
merged 5 commits into from
Jan 27, 2023
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
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* text=auto eol=lf

go.sum linguist-generated=true
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ jobs:
name: Acceptance Tests
needs: build
runs-on: ubuntu-latest
env:
TF_ACC: "1"
services:
backstage:
image: roadiehq/community-backstage-image
Expand All @@ -82,13 +84,11 @@ jobs:
terraform_wrapper: false
- run: go mod download
- env:
TF_ACC: "1"
ACCTEST_SKIP_RESOURCE_TEST: "1"
BACKSTAGE_BASE_URL: "https://demo.backstage.io"
run: go test -v -cover ./backstage
timeout-minutes: 10
- env:
TF_ACC: "1"
BACKSTAGE_BASE_URL: "http://localhost:${{ job.services.backstage.ports[7000] }}"
run: go test -v -cover ./backstage/resource_location_test.go
run: go test -v -cover ./backstage -run TestAccResourceLocation
timeout-minutes: 10
4 changes: 3 additions & 1 deletion backstage/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ func (p *backstageProvider) Configure(ctx context.Context, req provider.Configur
}

func (p *backstageProvider) Resources(context.Context) []func() resource.Resource {
return []func() resource.Resource{}
return []func() resource.Resource{
NewLocationResource,
}
}

func (p *backstageProvider) DataSources(context.Context) []func() datasource.DataSource {
Expand Down
199 changes: 199 additions & 0 deletions backstage/resource_location.go
Original file line number Diff line number Diff line change
@@ -1 +1,200 @@
package backstage

import (
"context"
"fmt"
"net/http"
"time"

"github.com/hashicorp/terraform-plugin-framework/path"
"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/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/tdabasinskas/go-backstage/backstage"
)

var (
_ resource.Resource = &locationResource{}
_ resource.ResourceWithConfigure = &locationResource{}
_ resource.ResourceWithImportState = &locationResource{}
)

// NewLocationResource is a helper function to simplify the provider implementation.
func NewLocationResource() resource.Resource {
return &locationResource{}
}

// locationResource is the resource implementation.
type locationResource struct {
client *backstage.Client
}

// locationResourceModel maps the resource schema data.
type locationResourceModel struct {
ID types.String `tfsdk:"id"`
Type types.String `tfsdk:"type"`
Target types.String `tfsdk:"target"`
LastUpdated types.String `tfsdk:"last_updated"`
}

const (
descriptionLocationID = "Identifier of the location."
descriptionLocationType = "Type of the location. Always `url`."
descriptionLocationTarget = "Target as a string. Should be a valid URL."
descriptionLocationLastUpdated = "Timestamp of the last Terraform update of the location."
)

// Metadata returns the data source type name.
func (r *locationResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_location"
}

// Schema defines the schema for the resource.
func (r *locationResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Use this resource to manage Backstage locations. \n\n" +
"In order for this resource to work, Backstage instance must NOT be running in " +
"[read-only mode](https://backstage.io/docs/features/software-catalog/configuration#readonly-mode).",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{Computed: true, Description: descriptionLocationID, PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
}},
"type": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: descriptionLocationType, Validators: []validator.String{}, PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
}},
"target": schema.StringAttribute{Required: true, Description: descriptionLocationTarget,
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
"last_updated": schema.StringAttribute{Computed: true, Description: descriptionLocationLastUpdated},
},
}
}

// Configure adds the provider configured client to the data source.
func (r *locationResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}

r.client = req.ProviderData.(*backstage.Client)
}

// Create registers a new location in Backstage and sets the initial Terraform state.
func (r *locationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan locationResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

location, response, err := r.client.Catalog.Locations.Create(ctx, plan.Target.ValueString(), false)
if err != nil {
resp.Diagnostics.AddError("Error creating location",
fmt.Sprintf("Could not create location, unexpected error: %s", err.Error()),
)
return
}

if response.StatusCode != http.StatusCreated {
resp.Diagnostics.AddError("Error creating location",
fmt.Sprintf("Could not create location, unexpected status code: %d", response.StatusCode),
)
return
}

plan.ID = types.StringValue(location.Location.ID)
plan.Type = types.StringValue(location.Location.Type)
plan.Target = types.StringValue(location.Location.Target)
plan.LastUpdated = types.StringValue(time.Now().Format(time.RFC850))

diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}

// Read reads the existing location and refreshes the Terraform state with the latest data.
func (r *locationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state locationResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

location, response, err := r.client.Catalog.Locations.GetByID(ctx, state.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Error reading Backstage location",
fmt.Sprintf("Could not read Backstage location ID %s: %s", state.ID.ValueString(), err.Error()),
)
return
}

if response.StatusCode != http.StatusOK {
resp.Diagnostics.AddError("Error reading Backstage location",
fmt.Sprintf("Could not read Backstage location ID %s, unexpected status code: %d", state.ID.ValueString(), response.StatusCode),
)
return
}

state.Target = types.StringValue(location.Target)
state.Type = types.StringValue(location.Type)

diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}

// Update updates the resource and sets the updated Terraform state on success.
func (r *locationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
// Retrieve values from plan
var plan locationResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

var state locationResourceModel
diags = req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}

// Delete deletes the location and removes the Terraform state on success.
func (r *locationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state locationResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

response, err := r.client.Catalog.Locations.DeleteByID(ctx, state.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Error deleting Backstage location",
fmt.Sprintf("Could not delete location, unexpected error: %s", err.Error()),
)
return
}

if response.StatusCode != http.StatusNoContent {
resp.Diagnostics.AddError("Error deleting Backstage location",
fmt.Sprintf("Could not delete location, unexpected status code: %d", response.StatusCode),
)
return
}
}

// ImportState imports the resource into Terraform state.
func (r *locationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
47 changes: 47 additions & 0 deletions backstage/resource_location_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,59 @@
//go:build !resources

package backstage

import (
"os"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func TestAccResourceLocation(t *testing.T) {
if os.Getenv("ACCTEST_SKIP_RESOURCE_TEST") != "" {
t.Skip("Skipping as ACCTEST_SKIP_RESOURCE_LOCATION is set")
}

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create testing
{
Config: testAccProviderConfig + testAccResourceLocationConfig1,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("backstage_location.test", "type", "url"),
resource.TestCheckResourceAttr("backstage_location.test", "target", "http://test1"),
resource.TestCheckResourceAttrSet("backstage_location.test", "id"),
resource.TestCheckResourceAttrSet("backstage_location.test", "last_updated"),
),
},
// ImportState testing
{
ResourceName: "backstage_location.test",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"last_updated"},
},
// Update and Read testing
{
Config: testAccProviderConfig + testAccResourceLocationConfig2,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("backstage_location.test", "type", "url"),
resource.TestCheckResourceAttr("backstage_location.test", "target", "http://test2"),
),
},
},
})

}

const testAccResourceLocationConfig1 = `
resource "backstage_location" "test" {
target = "http://test1"
}
`
const testAccResourceLocationConfig2 = `
resource "backstage_location" "test" {
target = "http://test2"
}
`
42 changes: 42 additions & 0 deletions docs/resources/location.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "backstage_location Resource - terraform-provider-backstage"
subcategory: ""
description: |-
Use this resource to manage Backstage locations.
In order for this resource to work, Backstage instance must NOT be running in read-only mode https://backstage.io/docs/features/software-catalog/configuration#readonly-mode.
---

# backstage_location (Resource)

Use this resource to manage Backstage locations.

In order for this resource to work, Backstage instance must NOT be running in [read-only mode](https://backstage.io/docs/features/software-catalog/configuration#readonly-mode).

## Example Usage

```terraform
# Ensures the location exists in Backstage.
resource "backstage_location" "example" {
# URL to the location target:
target = "http://example-target"
}
```

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

### Required

- `target` (String) Target as a string. Should be a valid URL.

### Optional

- `type` (String) Type of the location. Always `url`.

### Read-Only

- `id` (String) Identifier of the location.
- `last_updated` (String) Timestamp of the last Terraform update of the location.


5 changes: 5 additions & 0 deletions examples/resources/backstage_location/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Ensures the location exists in Backstage.
resource "backstage_location" "example" {
# URL to the location target:
target = "http://example-target"
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ require (
github.com/shopspring/decimal v1.3.1 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/stretchr/testify v1.8.1 // indirect
github.com/tdabasinskas/go-backstage v1.0.2 // indirect
github.com/tdabasinskas/go-backstage v1.1.0 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect
github.com/vmihailenco/tagparser v0.1.2 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum

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