diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a172a4d..23887f4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,22 +21,22 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Unshallow run: git fetch --prune --unshallow - name: Set up Go - uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 + uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 with: go-version-file: "go.mod" cache: true - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@111c56156bcc6918c056dbef52164cfa583dc549 # v5.2.0 + uses: crazy-max/ghaction-import-gpg@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6.2.0 id: import_gpg with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.PASSPHRASE }} - name: Run GoReleaser - uses: goreleaser/goreleaser-action@f82d6c1c344bcacabba2c841718984797f664a6b # v4.2.0 + uses: goreleaser/goreleaser-action@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6.1.0 with: version: latest args: release --rm-dist diff --git a/docs/index.md b/docs/index.md index 6ddbbda..fc1ffbd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,7 +8,14 @@ description: |- # Tines Provider The Tines provider is used to interact with resources supported by Tines. -The provider needs to be configured with the proper credentials before it can be used. +The provider needs to be configured with the proper credentials before it can be used. + +## Upgrading Versions +Because the Tines Terraform provider is still under active development, breaking changes +may occur in minor versions for any 0.x.x release. Future stable versions (1.x.x and above) +will follow SemVer principles for backwards compatibility in minor and patch version updates. + +Be sure to read the version upgrade guides to understand any breaking changes before upgrading. ## Example Usage diff --git a/internal/provider/provider.go b/internal/provider/provider.go index dd3ea7c..03991e2 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -129,7 +129,7 @@ func (p *TinesProvider) Configure(ctx context.Context, req provider.ConfigureReq } // Create a new Tines client using the configuration values. - c, err := tines_cli.NewClient(&tenant, &apiKey, &p.version) + c, err := tines_cli.NewClient(tenant, apiKey, p.version) if err != nil { resp.Diagnostics.AddError( "Unable to Create Tines API Client", diff --git a/internal/provider/story_resource.go b/internal/provider/story_resource.go index b2b9714..23bdd26 100644 --- a/internal/provider/story_resource.go +++ b/internal/provider/story_resource.go @@ -121,16 +121,10 @@ func (r *storyResource) Schema(ctx context.Context, _ resource.SchemaRequest, re Description: "Tines tenant URL", Optional: true, DeprecationMessage: "Value will be overridden by the value set in the provider credentials. This field will be removed in a future version.", - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, }, "team_id": schema.Int64Attribute{ Description: "The ID of the team that this story belongs to.", Required: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, }, "folder_id": schema.Int64Attribute{ Description: "The ID of the folder where this story should be organized. The folder ID must belong to the associated team that owns this story.", @@ -138,7 +132,6 @@ func (r *storyResource) Schema(ctx context.Context, _ resource.SchemaRequest, re Computed: true, PlanModifiers: []planmodifier.Int64{ int64planmodifier.UseStateForUnknown(), - int64planmodifier.RequiresReplace(), }, }, "name": schema.StringAttribute{ @@ -152,6 +145,9 @@ func (r *storyResource) Schema(ctx context.Context, _ resource.SchemaRequest, re "user_id": schema.Int64Attribute{ Description: "ID of the story creator.", Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, }, "description": schema.StringAttribute{ Description: "A user-defined description of the story.", @@ -471,8 +467,17 @@ func (r *storyResource) Read(ctx context.Context, req resource.ReadRequest, resp return } - status, remoteState, err := r.client.GetStory(localState.ID.ValueInt64()) + remoteState, err := r.client.GetStory(localState.ID.ValueInt64()) if err != nil { + // Treat HTTP 404 Not Found status as a signal to recreate resource + // and return early. + if tinesErr, ok := err.(tines_cli.Error); ok { + if tinesErr.StatusCode == 404 { + resp.State.RemoveResource(ctx) + return + } + } + resp.Diagnostics.AddError( "Unable to Refresh Resource", "An unexpected error occurred while attempting to refresh resource state. "+ @@ -482,13 +487,6 @@ func (r *storyResource) Read(ctx context.Context, req resource.ReadRequest, resp return } - // Treat HTTP 404 Not Found status as a signal to recreate resource - // and return early. - if status == 404 { - resp.State.RemoveResource(ctx) - return - } - diags := r.convertStoryToPlan(ctx, &localState, remoteState) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -507,15 +505,20 @@ func (r *storyResource) Read(ctx context.Context, req resource.ReadRequest, resp func (r *storyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { tflog.Info(ctx, "Updating Story") - var plan storyResourceModel + var plan, state storyResourceModel var story *tines_cli.Story diags := req.Plan.Get(ctx, &plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } - if !plan.Data.IsNull() { + if !plan.Data.IsNull() && !plan.Data.Equal(state.Data) { tflog.Info(ctx, "Exported Story payload detected, using the Import strategy") story, diags = r.runImportStory(&plan) resp.Diagnostics.Append(diags...) @@ -675,6 +678,30 @@ func (r *storyResource) convertPlanToStory(ctx context.Context, plan *storyResou story.KeepEventsFor = plan.KeepEventsFor.ValueInt64() } + if !plan.Disabled.IsNull() && !plan.Disabled.IsUnknown() { + story.Disabled = plan.Disabled.ValueBool() + } + + if !plan.Locked.IsNull() && !plan.Locked.IsUnknown() { + story.Locked = plan.Locked.ValueBool() + } + + if !plan.Priority.IsNull() && !plan.Priority.IsUnknown() { + story.Priority = plan.Priority.ValueBool() + } + + if !plan.STSEnabled.IsNull() && !plan.Priority.IsUnknown() { + story.STSEnabled = plan.STSEnabled.ValueBool() + } + + if !plan.STSAccessSource.IsNull() && !plan.STSAccessSource.IsUnknown() { + story.STSAccessSource = plan.STSAccessSource.ValueString() + } + + if !plan.STSAccess.IsNull() && !plan.STSAccess.IsUnknown() { + story.STSAccess = plan.STSAccess.ValueString() + } + if !plan.SharedTeamSlugs.IsNull() && !plan.SharedTeamSlugs.IsUnknown() { diags = plan.SharedTeamSlugs.ElementsAs(ctx, story.SharedTeamSlugs, false) if diags.HasError() { @@ -682,6 +709,29 @@ func (r *storyResource) convertPlanToStory(ctx context.Context, plan *storyResou } } + if !plan.STSSkillConfirmation.IsNull() && !plan.STSSkillConfirmation.IsUnknown() { + story.STSSkillConfirmation = plan.STSSkillConfirmation.ValueBool() + } + + if !plan.EntryAgentID.IsNull() && !plan.EntryAgentID.IsUnknown() { + story.EntryAgentID = plan.EntryAgentID.ValueInt64() + } + + if !plan.ExitAgents.IsNull() && !plan.ExitAgents.IsUnknown() { + diags = plan.ExitAgents.ElementsAs(ctx, story.ExitAgents, false) + if diags.HasError() { + return + } + } + + if !plan.TeamID.IsNull() && !plan.TeamID.IsUnknown() { + story.TeamID = plan.TeamID.ValueInt64() + } + + if !plan.FolderID.IsNull() && !plan.FolderID.IsUnknown() { + story.FolderID = plan.FolderID.ValueInt64() + } + return diags } @@ -752,11 +802,14 @@ func (r *storyResource) runImportStory(plan *storyResourceModel) (story *tines_c } var importRequest = tines_cli.StoryImportRequest{ - NewName: name, - Data: data, - TeamID: plan.TeamID.ValueInt64(), - FolderID: plan.FolderID.ValueInt64(), - Mode: "versionReplace", + NewName: name, + Data: data, + TeamID: plan.TeamID.ValueInt64(), + Mode: "versionReplace", + } + + if !plan.FolderID.IsNull() && !plan.FolderID.IsUnknown() { + importRequest.FolderID = plan.FolderID.ValueInt64() } story, err = r.client.ImportStory(&importRequest) diff --git a/internal/provider/story_resource_test.go b/internal/provider/story_resource_test.go index 9fb08cf..937b39d 100644 --- a/internal/provider/story_resource_test.go +++ b/internal/provider/story_resource_test.go @@ -1,6 +1,7 @@ package provider import ( + "regexp" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -15,7 +16,7 @@ func TestAccTinesStory_fromExportNoFolder(t *testing.T) { ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { - Config: providerConfig + testAccCreateStoryResourceNoFolder(), + Config: providerConfig + testAccCreateImportStoryResourceNoFolder(), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectNonEmptyPlan(), @@ -47,11 +48,112 @@ func TestAccTinesStory_fromExportNoFolder(t *testing.T) { ), }, }, + { + RefreshState: true, + }, + { + Config: providerConfig + testAccUpdateImportStoryResourceNoFolder(), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNonEmptyPlan(), + plancheck.ExpectResourceAction("tines_story.test_create_from_export_no_folder", plancheck.ResourceActionUpdate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "tines_story.test_create_from_export_no_folder", + tfjsonpath.New("team_id"), + knownvalue.Int64Exact(32987), + ), + }, + }, + }, + }) +} + +func TestAccTinesStory_fromExportWithFolder(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: providerConfig + testAccCreateImportStoryResourceWithFolder(), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNonEmptyPlan(), + plancheck.ExpectUnknownValue( + "tines_story.test_create_from_export_with_folder", + tfjsonpath.New("id"), + ), + plancheck.ExpectKnownValue( + "tines_story.test_create_from_export_with_folder", + tfjsonpath.New("team_id"), + knownvalue.Int64Exact(30906), + ), + plancheck.ExpectKnownValue( + "tines_story.test_create_from_export_with_folder", + tfjsonpath.New("folder_id"), + knownvalue.Int64Exact(7993), + ), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "tines_story.test_create_from_export_with_folder", + tfjsonpath.New("id"), + knownvalue.NotNull(), + ), + statecheck.ExpectKnownValue( + "tines_story.test_create_from_export_with_folder", + tfjsonpath.New("name"), + knownvalue.StringExact("Test Story"), + ), + statecheck.ExpectKnownValue( + "tines_story.test_create_from_export_with_folder", + tfjsonpath.New("folder_id"), + knownvalue.Int64Exact(7993), + ), + }, + }, + }, + }) +} + +func TestAccTinesStory_fromConfigNoFolder(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: providerConfig + testAccCreateConfigStoryResourceBadConfig(), + ExpectError: regexp.MustCompile("Attribute \"data\" cannot be specified when \"name\" is specified"), + }, + { + Config: providerConfig + testAccCreateConfigStoryResourceOneStep(), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNonEmptyPlan(), + }, + }, + }, + { + Config: providerConfig + testAccCreateConfigStoryResourceMultiStep(), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNonEmptyPlan(), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "tines_story.test_create_multi_step", + tfjsonpath.New("change_control_enabled"), + knownvalue.Bool(true), + ), + }, + }, }, }) } -func testAccCreateStoryResourceNoFolder() string { +func testAccCreateImportStoryResourceNoFolder() string { return ` resource "tines_story" "test_create_from_export_no_folder" { data = file("${path.module}/testdata/test-story.json") @@ -59,3 +161,51 @@ resource "tines_story" "test_create_from_export_no_folder" { } ` } + +func testAccUpdateImportStoryResourceNoFolder() string { + return ` +resource "tines_story" "test_create_from_export_no_folder" { + data = file("${path.module}/testdata/test-story.json") + team_id = 32987 +} + ` +} + +func testAccCreateImportStoryResourceWithFolder() string { + return ` +resource "tines_story" "test_create_from_export_with_folder" { + data = file("${path.module}/testdata/test-story.json") + team_id = 30906 + folder_id = 7993 +} + ` +} + +func testAccCreateConfigStoryResourceBadConfig() string { + return ` +resource "tines_story" "test_create_invalid" { + data = file("${path.module}/testdata/test-story.json") + team_id = 30906 + name = "Example Bad Config" +} + ` +} + +func testAccCreateConfigStoryResourceOneStep() string { + return ` +resource "tines_story" "test_create_one_step" { + team_id = 30906 + name = "Example One Step" +} + ` +} + +func testAccCreateConfigStoryResourceMultiStep() string { + return ` +resource "tines_story" "test_create_multi_step" { + team_id = 30906 + name = "Example Multi Step" + change_control_enabled = true +} + ` +} diff --git a/internal/tines_cli/client.go b/internal/tines_cli/client.go index 6045a91..a3a08e8 100644 --- a/internal/tines_cli/client.go +++ b/internal/tines_cli/client.go @@ -2,6 +2,7 @@ package tines_cli import ( "bytes" + "encoding/json" "fmt" "io" "net/http" @@ -17,26 +18,65 @@ type Client struct { } // NewClient - Creates a new Tines API client. -func NewClient(tenant, apiKey, providerVersion *string) (*Client, error) { +func NewClient(tenant, apiKey, providerVersion string) (*Client, error) { + if tenant == "" { + return nil, Error{ + Type: ErrorTypeAuthentication, + Errors: []ErrorMessage{ + { + Message: "host error", + Details: errEmptyTenant, + }, + }, + } + } + + if apiKey == "" { + return nil, Error{ + Type: ErrorTypeAuthentication, + Errors: []ErrorMessage{ + { + Message: "credential error", + Details: errEmptyApiKey, + }, + }, + } + } c := Client{ - TenantUrl: *tenant, - ApiKey: *apiKey, - ProviderVersion: *providerVersion, + TenantUrl: tenant, + ApiKey: apiKey, + ProviderVersion: providerVersion, HTTPClient: &http.Client{}, } return &c, nil } -func (c *Client) doRequest(method, path string, data []byte) (int, []byte, error) { +func (c *Client) doRequest(method, path string, data []byte) ([]byte, error) { tenant, err := url.Parse(c.TenantUrl) if err != nil { - return 0, nil, err + return nil, Error{ + Type: ErrorTypeRequest, + Errors: []ErrorMessage{ + { + Message: errParseError, + Details: err.Error(), + }, + }, + } } fullUrl := tenant.JoinPath(path).String() req, err := http.NewRequest(method, fullUrl, bytes.NewBuffer(data)) if err != nil { - return 0, nil, err + return nil, Error{ + Type: ErrorTypeRequest, + Errors: []ErrorMessage{ + { + Message: errDoRequestError, + Details: err.Error(), + }, + }, + } } req.Header.Set("content-type", "application/json") @@ -45,21 +85,74 @@ func (c *Client) doRequest(method, path string, data []byte) (int, []byte, error req.Header.Set("x-tines-client-version", fmt.Sprintf("tines-terraform-provider-%s", c.ProviderVersion)) req.Header.Set("x-user-token", c.ApiKey) - res, err := c.HTTPClient.Do(req) - if err != nil { - return 0, nil, err + resp, respErr := c.HTTPClient.Do(req) + if respErr != nil { + return nil, Error{ + Type: ErrorTypeRequest, + Errors: []ErrorMessage{ + { + Message: errDoRequestError, + Details: respErr.Error(), + }, + }, + } } - defer res.Body.Close() + defer resp.Body.Close() - body, err := io.ReadAll(res.Body) - if err != nil { - return res.StatusCode, nil, err + body, readErr := io.ReadAll(resp.Body) + if readErr != nil { + return nil, Error{ + Type: ErrorTypeServer, + StatusCode: resp.StatusCode, + Errors: []ErrorMessage{ + { + Message: errReadBodyError, + Details: readErr.Error(), + }, + }, + } + } + + // Return a server error for 5XX responses + if resp.StatusCode >= http.StatusInternalServerError { + errMsgs := c.getErrorMessages(body) + + return nil, Error{ + Type: ErrorTypeServer, + StatusCode: resp.StatusCode, + Errors: errMsgs, + } + } + + // Return a request error for 4XX responses + if resp.StatusCode >= http.StatusBadRequest && resp.StatusCode < http.StatusInternalServerError { + errMsgs := c.getErrorMessages(body) + + return nil, Error{ + Type: ErrorTypeRequest, + StatusCode: resp.StatusCode, + Errors: errMsgs, + } } - if res.StatusCode > 499 { - return res.StatusCode, nil, fmt.Errorf("HTTP %d response: %s", res.StatusCode, body) + return body, nil +} + +func (c *Client) getErrorMessages(body []byte) []ErrorMessage { + var errorInfo Error + var errorMsgs []ErrorMessage + + // The structure of an error response body can be inconsistent between API endpoints, + // so we try a couple techniques to capture the error messages. + jsonErr := json.Unmarshal(body, &errorInfo) + if jsonErr != nil { + jsonErr := json.Unmarshal(body, &errorMsgs) + if jsonErr != nil && body != nil { + errorMsgs = []ErrorMessage{{Message: "message", Details: string(body)}} + } + return errorMsgs } - return res.StatusCode, body, err + return errorInfo.Errors } diff --git a/internal/tines_cli/client_test.go b/internal/tines_cli/client_test.go index 22267da..1b9d484 100644 --- a/internal/tines_cli/client_test.go +++ b/internal/tines_cli/client_test.go @@ -24,18 +24,17 @@ func TestTinesClient(t *testing.T) { defer ts.Close() // Validate that the Tines CLI gets instantiated correctly - tenant := &ts.URL + tenant := ts.URL apiKey := "foo" version := "test" - client, err := NewClient(tenant, &apiKey, &version) + client, err := NewClient(tenant, apiKey, version) assert.Nil(err, "the Tines CLI client should instantiate successfully") // Validate that a generic HTTP request fires as expected - status, res, err := client.doRequest("GET", "/", nil) + res, err := client.doRequest("GET", "/", nil) - assert.Equal(http.StatusOK, status, "the server should return a success response status") assert.Equal([]byte("ok"), res, "the server should return an expected response body") assert.Nil(err, "the request should not return an error") } diff --git a/internal/tines_cli/errors.go b/internal/tines_cli/errors.go new file mode 100644 index 0000000..2753410 --- /dev/null +++ b/internal/tines_cli/errors.go @@ -0,0 +1,49 @@ +package tines_cli + +import ( + "fmt" + "strings" +) + +const ( + errEmptyApiKey = "API Token must not be empty" + errEmptyTenant = "Tines Tenant must not be empty" + errInternalServerError = "internal server error" + errDoRequestError = "error while attempting to make the HTTP request" + errUnmarshalError = "error unmarshalling the JSON response" + errReadBodyError = "error reading the HTTP response body bytes" + errParseError = "error parsing the input" +) + +type ErrorType string + +const ( + ErrorTypeRequest ErrorType = "request" + ErrorTypeAuthentication ErrorType = "authentication" + ErrorTypeAuthorization ErrorType = "authorization" + ErrorTypeNotFound ErrorType = "not_found" + ErrorTypeRateLimit ErrorType = "rate_limit" + ErrorTypeServer ErrorType = "server" +) + +type Error struct { + Type ErrorType `json:"type,omitempty"` + StatusCode int `json:"status_code,omitempty"` + Errors []ErrorMessage `json:"errors,omitempty"` +} + +type ErrorMessage struct { + Message string `json:"message,omitempty"` + Details string `json:"details,omitempty"` +} + +func (e Error) Error() string { + var errString string + errMessages := []string{} + for _, err := range e.Errors { + msg := fmt.Sprintf("%s: %s", err.Message, err.Details) + errMessages = append(errMessages, msg) + } + errString = fmt.Sprintf("%d error(s) occurred: %s", len(e.Errors), strings.Join(errMessages, ", ")) + return errString +} diff --git a/internal/tines_cli/stories.go b/internal/tines_cli/stories.go index 2d6b543..77146e4 100644 --- a/internal/tines_cli/stories.go +++ b/internal/tines_cli/stories.go @@ -48,20 +48,18 @@ func (c *Client) CreateStory(new *Story) (*Story, error) { newStory := Story{} req, err := json.Marshal(&new) - fmt.Printf("REQUEST BODY: %s", string(req)) if err != nil { - return &newStory, err + return nil, err } - status, body, err := c.doRequest("POST", "/api/v1/stories", req) + body, err := c.doRequest("POST", "/api/v1/stories", req) if err != nil { - return &newStory, err + return nil, err } err = json.Unmarshal(body, &newStory) if err != nil { - fmt.Printf("HTTP STATUS: %d, BODY: %s", status, string(body)) - return &newStory, err + return nil, err } return &newStory, nil @@ -71,26 +69,26 @@ func (c *Client) CreateStory(new *Story) (*Story, error) { func (c *Client) DeleteStory(id int64) error { resource := fmt.Sprintf("/api/v1/stories/%d", id) - _, _, err := c.doRequest("DELETE", resource, nil) + _, err := c.doRequest("DELETE", resource, nil) return err } // Get current state for a story. -func (c *Client) GetStory(id int64) (status int, story *Story, e error) { +func (c *Client) GetStory(id int64) (story *Story, e error) { resource := fmt.Sprintf("/api/v1/stories/%d", id) - status, body, err := c.doRequest("GET", resource, nil) - if err != nil || status == 404 { - return status, nil, err + body, err := c.doRequest("GET", resource, nil) + if err != nil { + return nil, err } err = json.Unmarshal(body, &story) if err != nil { - return status, nil, err + return nil, err } - return status, story, err + return story, nil } // Import a new story, or override an existing one. @@ -99,17 +97,17 @@ func (c *Client) ImportStory(story *StoryImportRequest) (*Story, error) { req, err := json.Marshal(&story) if err != nil { - return &newStory, err + return nil, err } - _, body, err := c.doRequest("POST", "/api/v1/stories/import", req) + body, err := c.doRequest("POST", "/api/v1/stories/import", req) if err != nil { - return &newStory, err + return nil, err } err = json.Unmarshal(body, &newStory) if err != nil { - return &newStory, err + return nil, err } return &newStory, nil @@ -125,15 +123,15 @@ func (c *Client) UpdateStory(id int64, values *Story) (*Story, error) { return &updatedStory, err } - _, body, err := c.doRequest("PUT", resource, req) + body, err := c.doRequest("PUT", resource, req) if err != nil { - return &updatedStory, err + return nil, err } err = json.Unmarshal(body, &updatedStory) if err != nil { - return &updatedStory, err + return nil, err } - return &updatedStory, err + return &updatedStory, nil } diff --git a/internal/tines_cli/stories_test.go b/internal/tines_cli/stories_test.go index 94110b1..44bc0da 100644 --- a/internal/tines_cli/stories_test.go +++ b/internal/tines_cli/stories_test.go @@ -55,11 +55,11 @@ func TestImportStory(t *testing.T) { })) defer ts.Close() - tenant := &ts.URL + tenant := ts.URL apiKey := "foo" version := "test" - c, err := NewClient(tenant, &apiKey, &version) + c, err := NewClient(tenant, apiKey, version) assert.Nil(err, "should instantiate the Tines API client without errors") diff --git a/templates/guides/version-0.1-upgrade.md b/templates/guides/version-0.1-upgrade.md new file mode 100644 index 0000000..75460b3 --- /dev/null +++ b/templates/guides/version-0.1-upgrade.md @@ -0,0 +1,52 @@ +--- +layout: "" +page_title: "Upgrading to version 0.1.x (from 0.0.x)" +description: Terraform Tines Provider Version 0.1 Upgrade Guide +--- + +# Terraform Tines Provider Version 0.1 Upgrade Guide +Starting with version `0.1.0`, the Tines provider on Terraform introduces a new and simplified way to manage stories. This means that any Resource and its Schema with a version below `0.1.0` will no longer be compatible with `tines` Terraform provider version `0.1.0` or higher, as there are breaking changes in this version. If you have an older Terraform state file for a version below `0.1.0`, we recommend starting fresh by initializing a new state (via `terraform init`) for version `0.1.0` or higher. + + +## Provider Version Configuration +If you are not ready to make a move to version 0.1 of the Tines provider, you may keep the 0.0.x branch active for +your Terraform project by specifying: + +```terraform +provider "tines" { + version = "~> 0.0" + # ... any other configuration +} +``` + +## Getting Started With v0.1.0 +To export your Tines Story, follow [these instructions](https://www.tines.com/docs/stories/importing-and-exporting#exporting-stories) and place the exported filed in the same directory as your `main.tf` file. After that, you can define your story as a Terraform Resource using the following syntax: + +```terraform +# provider.tf +provider "tines" {} + +# main.tf +resource "tines_story" "dev_story_name" { + data = file("${path.module}/story-example.json") + tenant_url = "https://dev-tenant.tines.com" + tines_api_token = var.dev_tines_api_token + team_id = var.team_id # optional + folder_id = var.folder_id # optional +} + +# variable.tf +variable "dev_tines_api_token" { + type = string +} + +variable "team_id" { + type = number +} + +variable "folder_id" { + type = number +} +``` + +And that's all. You don't need to import the state into Terraform either. Running a `terraform apply` will automatically perform an upsert and set the state in Terraform accordingly. \ No newline at end of file diff --git a/templates/guides/version-0.2-upgrade.md b/templates/guides/version-0.2-upgrade.md new file mode 100644 index 0000000..3844f5f --- /dev/null +++ b/templates/guides/version-0.2-upgrade.md @@ -0,0 +1,39 @@ +--- +layout: "" +page_title: "Upgrading to version 0.2.x (from 0.1.x)" +description: Terraform Tines Provider Version 0.2 Upgrade Guide +--- + +# Terraform Tines Provider Version 0.2 Upgrade Guide + +Version 0.2 has made some architectural and configurability changes under the hood in preparation for some significant new functionality, which will be coming in a future release. While all existing resources are compatible with v0.2, some attributes have been deprecated and some new configuration values are required. + +## Provider Version Configuration +If you are not ready to make a move to version 0.2 of the Tines provider, you may keep the 0.1.x branch active for +your Terraform project by specifying: + +```terraform +provider "tines" { + version = "~> 0.1" + # ... any other configuration +} +``` + +We highly recommend that you review this guide, make necessary changes and move to 0.2.x branch, as further 0.1.x releases are +unlikely to happen. + +~> Before attempting to upgrade to version 0.2, you should first upgrade to the + latest version of 0.1 to ensure any transitional updates are applied to your + existing configuration. + +## Provider Global Configuration Changes +The following changes have been made at the provider level: + +- Added a new required configuration value for `tenant` which can be set either in the provider configuration or as the `TINES_TENANT` environment variable. +- Added a new required configuration value for `api_key` which can be set either in the provider configuration or as the `TINES_API_KEY` environment variable. + +## Tines Story Configuration Changes +The following changes have been made to the `tines_story` resource: + +- The `tines_api_token` resource attribute has been marked as deprecated and will be removed in a future release. In version 0.2, any value set here will be ignored and overridden by the provider-level `api_key` attribute. +- The `tenant_url` resource attribute has been marked as deprecated and will be removed in a future release. In version 0.2, any value set here will be ignored and overriden by the provider-level `tenant` attibute. \ No newline at end of file diff --git a/templates/index.md.tmpl b/templates/index.md.tmpl index 40c71ce..c018e46 100644 --- a/templates/index.md.tmpl +++ b/templates/index.md.tmpl @@ -15,7 +15,7 @@ Because the Tines Terraform provider is still under active development, breaking may occur in minor versions for any 0.x.x release. Future stable versions (1.x.x and above) will follow SemVer principles for backwards compatibility in minor and patch version updates. - +Be sure to read the version upgrade guides to understand any breaking changes before upgrading. ## Example Usage diff --git a/terraform-registry-manifest.json b/terraform-registry-manifest.json index 7acd86a..a2e0e53 100644 --- a/terraform-registry-manifest.json +++ b/terraform-registry-manifest.json @@ -1,6 +1,6 @@ { "version": 1, "metadata": { - "protocol_versions": ["5.0"] + "protocol_versions": ["6.0"] } } \ No newline at end of file