Skip to content

Commit

Permalink
Merge pull request #67 from joshbeard/doc-get-slug
Browse files Browse the repository at this point in the history
  • Loading branch information
joshbeard authored Sep 22, 2023
2 parents d17a6da + 638a9d2 commit cbaf49e
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 48 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ lint: modverify vet gofumpt lines golangci-lint ## Run all linters
## Testing ##
.PHONY: test
test: ## Run unit and race tests with 'go test'
go test -count=1 -parallel=4 -coverprofile=coverage.txt -covermode count ./readme/...
go test -v -count=1 -parallel=4 -coverprofile=coverage.txt -covermode count ./readme/...
go test -race -short ./readme/...

## Coverage ##
Expand Down
50 changes: 35 additions & 15 deletions docs/resources/doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,39 @@ Refer to <https://docs.readme.com/main/docs/rdme> for more information about usi

```terraform
# Manage docs on ReadMe.
# Create a category
resource "readme_category" "example" {
title = "Example Category"
type = "guide"
}
# Create a doc in the category
resource "readme_doc" "example" {
# title can be specified as an attribute or in the body front matter.
title = "My Example Doc"
# title can be specified as an attribute or in the body front matter.
title = "My Example Doc"
# category can be specified as an attribute or in the body front matter.
# Use the `readme_category` resource to manage categories.
category = "633c5a54187d2c008e2e074c"
# category can be specified as an attribute or in the body front matter.
# Use the `readme_category` resource to manage categories.
category = readme_category.example.id
# category_slug can be specified as an attribute or in the body front matter.
# category_slug = "foo-bar"
# category_slug can be specified as an attribute or in the body front matter.
# category_slug = "foo-bar"
# hidden can be specified as an attribute or in the body front matter.
hidden = false
# hidden can be specified as an attribute or in the body front matter.
hidden = false
# order can be specified as an attribute or in the body front matter.
order = 99
# order can be specified as an attribute or in the body front matter.
order = 99
# type can be specified as an attribute or in the body front matter.
type = "basic"
# type can be specified as an attribute or in the body front matter.
type = "basic"
# body can be read from a file using Terraform's `file()` or `templatefile()` functions.
body = file("mydoc.md")
# body can be read from a file using Terraform's `file()` function.
# For best results, wrap the string with the `chomp()` function to remove
# trailing newlines. ReadMe's API trims these implicitly.
#body = chomp(file("mydoc.md"))
body = "Hello! Welcome to my document!"
}
```

Expand Down Expand Up @@ -210,3 +221,12 @@ Read-Only:
- `name` (String)
- `slug` (String)
- `type` (String)

## Import

Import is supported using the following syntax:

```shell
# Import a ReadMe doc using its slug.
terraform import readme_doc.example example-slug
```
2 changes: 2 additions & 0 deletions examples/resources/readme_doc/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Import a ReadMe doc using its slug.
terraform import readme_doc.example example-slug
43 changes: 27 additions & 16 deletions examples/resources/readme_doc/resource.tf
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
# Manage docs on ReadMe.

# Create a category
resource "readme_category" "example" {
title = "Example Category"
type = "guide"
}

# Create a doc in the category
resource "readme_doc" "example" {
# title can be specified as an attribute or in the body front matter.
title = "My Example Doc"
# title can be specified as an attribute or in the body front matter.
title = "My Example Doc"

# category can be specified as an attribute or in the body front matter.
# Use the `readme_category` resource to manage categories.
category = "633c5a54187d2c008e2e074c"
# category can be specified as an attribute or in the body front matter.
# Use the `readme_category` resource to manage categories.
category = readme_category.example.id

# category_slug can be specified as an attribute or in the body front matter.
# category_slug = "foo-bar"
# category_slug can be specified as an attribute or in the body front matter.
# category_slug = "foo-bar"

# hidden can be specified as an attribute or in the body front matter.
hidden = false
# hidden can be specified as an attribute or in the body front matter.
hidden = false

# order can be specified as an attribute or in the body front matter.
order = 99
# order can be specified as an attribute or in the body front matter.
order = 99

# type can be specified as an attribute or in the body front matter.
type = "basic"
# type can be specified as an attribute or in the body front matter.
type = "basic"

# body can be read from a file using Terraform's `file()` or `templatefile()` functions.
body = file("mydoc.md")
}
# body can be read from a file using Terraform's `file()` function.
# For best results, wrap the string with the `chomp()` function to remove
# trailing newlines. ReadMe's API trims these implicitly.
#body = chomp(file("mydoc.md"))
body = "Hello! Welcome to my document!"
}
46 changes: 32 additions & 14 deletions readme/doc_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (r *docResource) Metadata(
resp.TypeName = req.ProviderTypeName + "_doc"
}

// Configure adds the provider configured client to the data source.
// Configure adds the provider configured client to the resource.
func (r *docResource) Configure(
_ context.Context,
req resource.ConfigureRequest,
Expand Down Expand Up @@ -205,32 +205,50 @@ func (r *docResource) Read(
resp *resource.ReadResponse,
) {
// Get current state.
var plan, state docModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
resp.Diagnostics.Append(req.State.Get(ctx, &plan)...)
var state docModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

requestOpts := apiRequestOptions(plan.Version)
tflog.Info(ctx, fmt.Sprintf("retrieving doc with request options=%+v", requestOpts))
requestOpts := apiRequestOptions(state.Version)
logMsg := fmt.Sprintf("retrieving doc %s with request options=%+v", state.Slug.ValueString(), requestOpts)
tflog.Info(ctx, logMsg)

slug := state.Slug.ValueString()
id := state.ID.ValueString()

// Get the doc.
state, apiResponse, err := getDoc(r.client, ctx, state.Slug.ValueString(), plan, requestOpts)
state, apiResponse, err := getDoc(r.client, ctx, slug, state, requestOpts)
if err != nil {
if apiResponse.APIErrorResponse.Error == "DOC_NOTFOUND" {
resp.State.RemoveResource(ctx)
if apiResponse != nil && apiResponse.HTTPResponse.StatusCode == 404 {
// Attempt to find the doc by ID by searching all docs.
// While the slug is the primary identifier to request a doc, the
// slug is not stable and can be changed through the web UI.
tflog.Info(ctx, fmt.Sprintf("doc %s not found when looking up by slug, performing search", slug))
state, apiResponse, err = getDoc(r.client, ctx, "id:"+id, state, requestOpts)
if err != nil {
if strings.Contains(err.Error(), "no doc found matching id") {
tflog.Info(ctx, fmt.Sprintf("doc %s not found when searching by slug or ID %s, removing from state", slug, id))
resp.State.RemoveResource(ctx)

return
}
resp.Diagnostics.AddError("Unable to search for doc.", clientError(err, apiResponse))

return
}
} else {
resp.Diagnostics.AddError("Unable to retrieve doc.", clientError(err, apiResponse))

return
}

resp.Diagnostics.AddError("Unable to read doc.", err.Error())

return
}

// Set refreshed state.
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
resp.Diagnostics.AddError(
"Unable to refresh doc.",
Expand Down
94 changes: 92 additions & 2 deletions readme/doc_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ func TestDocResource(t *testing.T) {
mockAPIError.Error = "DOC_NOTFOUND"
gock.New(testURL).Get("/docs/" + mockDoc.Slug).Times(1).Reply(404).JSON(mockAPIError)

gock.New(testURL).
Post("/docs").
Path("search").
Times(1).
Reply(200).
JSON(readme.DocSearchResult{})

// Mock the request to create the resource.
gock.New(testURL).Post("/docs").Times(1).Reply(201).JSON(mockDoc)
// Mock the request to get and refresh the resource.
Expand All @@ -80,9 +87,9 @@ func TestDocResource(t *testing.T) {
),
PreConfig: func() {
gock.OffAll()
gock.New(testURL).Get("/docs/" + mockDoc.Slug).Times(1).Reply(400).JSON(mockDoc)
gock.New(testURL).Get("/docs/" + mockDoc.Slug).Times(1).Reply(400).JSON(mockAPIError)
},
ExpectError: regexp.MustCompile("Unable to read doc"),
ExpectError: regexp.MustCompile("API responded with a non-OK status: 400"),
},

// Test update results in error when the update action fails.
Expand Down Expand Up @@ -645,3 +652,86 @@ func TestDocResource_FrontMatter(t *testing.T) {
})
}
}

// TestDocRenamedSlugResource tests that a doc can be created and continue to
// be managed by Terraform when the slug is changed outside of Terraform by
// using the "get_slug" attribute.
func TestDocRenamedSlugResource(t *testing.T) {
// Close all gocks after completion.
defer gock.OffAll()

renamed := mockDoc
renamed.Slug = "new-slug"
renamedSearch := mockDocSearchResponse
renamedSearch.Results[0].Slug = "new-slug"

resource.Test(t, resource.TestCase{
IsUnitTest: true,
ProtoV6ProviderFactories: testProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Test successful creation.
{
Config: providerConfig + fmt.Sprintf(`
resource "readme_doc" "test" {
title = "%s"
body = "%s"
category = "%s"
type = "%s"
}`,
mockDoc.Title, mockDoc.Body, mockDoc.Category, mockDoc.Type,
),
PreConfig: func() {
docCommonGocks()
// Mock the request to create the resource.
gock.New(testURL).Post("/docs").Times(1).Reply(201).JSON(mockDoc)
// Mock the request to get and refresh the resource.
gock.New(testURL).Get("/docs/" + mockDoc.Slug).Times(2).Reply(200).JSON(mockDoc)
},
Check: docResourceCommonChecks(mockDoc, ""),
},

// Test that the doc can be renamed outside of Terraform and
// continue to be managed by Terraform.
{
Config: providerConfig + fmt.Sprintf(`
resource "readme_doc" "test" {
title = "%s"
body = "%s"
category = "%s"
type = "%s"
}`,
renamed.Title, renamed.Body, renamed.Category, renamed.Type,
),
PreConfig: func() {
gock.OffAll()
docCommonGocks()

// Original slug is not found.
docNotFoundAPIError := mockAPIError
docNotFoundAPIError.Error = "DOC_NOTFOUND"
docNotFoundAPIError.Message = "Doc not found"
gock.New(testURL).Get("/docs/" + "a-test-doc").Times(1).Reply(404).JSON(docNotFoundAPIError)

// The slug won't exist, so the provider does a search by ID.
gock.New(testURL).
Post("/docs").
Path("search").
Times(1).
Reply(200).
JSON(mockDocSearchResponse)

// The matched doc is requested from the search results.
// It's also requested again after the rename.
gock.New(testURL).Get("/docs/" + "new-slug").Times(3).Reply(200).JSON(renamed)

// An update is triggered to match state with the new slug.
gock.New(testURL).Put("/docs/" + "new-slug").Times(1).Reply(200).JSON(renamed)

// Post-test deletion
gock.New(testURL).Delete("/docs/" + "new-slug").Times(1).Reply(204)
},
Check: docResourceCommonChecks(renamed, ""),
},
},
})
}

0 comments on commit cbaf49e

Please sign in to comment.