From 7c965730b6893e0784a1185e552c61d4e864fb8a Mon Sep 17 00:00:00 2001 From: Evangelos Karvounis Date: Tue, 12 Mar 2024 11:24:33 +0200 Subject: [PATCH 01/61] feat: add support for run task results callback --- errors.go | 13 ++++ run_task_request.go | 53 ++++++++++++++++ run_task_results_callback.go | 114 +++++++++++++++++++++++++++++++++++ tfe.go | 4 +- 4 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 run_task_request.go create mode 100644 run_task_results_callback.go diff --git a/errors.go b/errors.go index d096b80f4..b9abda96c 100644 --- a/errors.go +++ b/errors.go @@ -5,6 +5,7 @@ package tfe import ( "errors" + "fmt" ) // Generic errors applicable to all resources. @@ -219,6 +220,14 @@ var ( ErrInvalidModuleID = errors.New("invalid value for module ID") ErrInvalidRegistryName = errors.New(`invalid value for registry-name. It must be either "private" or "public"`) + + ErrInvalidCallbackURL = errors.New("invalid value for callback URL") + + ErrInvalidAccessToken = errors.New("invalid value for access token") + + ErrInvalidTaskResultsCallbackType = errors.New("invalid value for task result type") + + ErrInvalidTaskResultsCallbackStatus = errors.New(fmt.Sprintf("invalid value for task result status. Must be either `%s`, `%s`, or `%s`", TaskFailed, TaskPassed, TaskRunning)) ) var ( @@ -372,4 +381,8 @@ var ( ErrRequiredRawState = errors.New("RawState is required") ErrStateVersionUploadNotSupported = errors.New("upload not supported by this version of Terraform Enterprise") + + ErrRequiredCallbackData = errors.New("data object is required for TFE run task callback") + + ErrRequiredCallbackDataAttributes = errors.New("data attributes object is required for TFE run task callback") ) diff --git a/run_task_request.go b/run_task_request.go new file mode 100644 index 000000000..227de7188 --- /dev/null +++ b/run_task_request.go @@ -0,0 +1,53 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfe + +import ( + "time" +) + +const ( + // RunTaskRegistrationRunId is the string that TFC/E sends when it just wants to register the Run Task. + RunTaskRegistrationRunId = "run-xxxxxxxxxxxxxxxx" +) + +// RunTaskRequest is the payload object that TFC/E sends to the Run Task's URL. +// https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#common-properties +type RunTaskRequest struct { + AccessToken string `json:"access_token"` + Capabilitites RunTaskRequestCapabilitites `json:"capabilitites,omitempty"` + ConfigurationVersionDownloadURL string `json:"configuration_version_download_url,omitempty"` + ConfigurationVersionID string `json:"configuration_version_id,omitempty"` + IsSpeculative bool `json:"is_speculative"` + OrganizationName string `json:"organization_name"` + PayloadVersion int `json:"payload_version"` + PlanJSONAPIURL string `json:"plan_json_api_url,omitempty"` // Specific to post_plan, pre_apply or post_apply stage + RunAppURL string `json:"run_app_url"` + RunCreatedAt time.Time `json:"run_created_at"` + RunCreatedBy string `json:"run_created_by"` + RunID string `json:"run_id"` + RunMessage string `json:"run_message"` + Stage string `json:"stage"` + TaskResultCallbackURL string `json:"task_result_callback_url"` + TaskResultEnforcementLevel string `json:"task_result_enforcement_level"` + TaskResultID string `json:"task_result_id"` + VcsBranch string `json:"vcs_branch,omitempty"` + VcsCommitURL string `json:"vcs_commit_url,omitempty"` + VcsPullRequestURL string `json:"vcs_pull_request_url,omitempty"` + VcsRepoURL string `json:"vcs_repo_url,omitempty"` + WorkspaceAppURL string `json:"workspace_app_url"` + WorkspaceID string `json:"workspace_id"` + WorkspaceName string `json:"workspace_name"` + WorkspaceWorkingDirectory string `json:"workspace_working_directory,omitempty"` +} + +// RunTaskRequestCapabilitites defines the capabilities that the caller supports. +type RunTaskRequestCapabilitites struct { + Outcomes bool `json:"outcomes"` +} + +// IsEndpointValidation returns true if this is a Request from TFC/E to validate and register this API endpoint. +func (r RunTaskRequest) IsEndpointValidation() bool { + return r.RunID == RunTaskRegistrationRunId +} diff --git a/run_task_results_callback.go b/run_task_results_callback.go new file mode 100644 index 000000000..8c939cf82 --- /dev/null +++ b/run_task_results_callback.go @@ -0,0 +1,114 @@ +package tfe + +import ( + "context" + "net/http" +) + +// Compile-time proof of interface implementation. +var _ RunTasksCallback = (*taskResultsCallback)(nil) + +// RunTasksCallback describes all the Run Tasks Integration Callback API methods. +// +// TFE API docs: +// https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration +type RunTasksCallback interface { + // Update sends updates to TFC/E Run Task Callback URL.. + Update(ctx context.Context, callbackURL string, accessToken string, options TaskResultsCallbackOptions) error +} + +// taskResultsCallback implements RunTasksCallback. +type taskResultsCallback struct { + client *Client +} + +const ( + TaskResultsCallbackType = "task-results" +) + +// Update sends updates to TFC/E Run Task Callback URL +func (s *taskResultsCallback) Update(ctx context.Context, callbackURL string, accessToken string, options TaskResultsCallbackOptions) error { + if !validString(&callbackURL) { + return ErrInvalidCallbackURL + } + if !validString(&accessToken) { + return ErrInvalidAccessToken + } + req, err := s.client.NewRequest(http.MethodPatch, callbackURL, &options) + if err != nil { + return err + } + // The PATCH request must use the token supplied in the originating request (access_token) for authentication. + // https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#request-headers-1 + req.Header.Set("Authorization", "Bearer "+accessToken) + return req.Do(ctx, nil) +} + +// TaskResultsCallbackOptions represents the options for a TFE Task result callback request +// https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#request-body-1 +type TaskResultsCallbackOptions struct { + Data *TaskResultsCallbackData `json:"data"` +} + +type TaskResultsCallbackData struct { + // Required: Must be set to `task-results` + Type *string `json:"type"` + // Required: Attributes of the Task Results Callback Response + Attributes *TaskResultsCallbackDataAttributes `json:"attributes"` + Relationships *TaskResultsCallbackRelationships `json:"relationships,omitempty"` +} + +type TaskResultsCallbackDataAttributes struct { + // Status Must be one of TaskFailed, TaskPassed or TaskRunning + Status TaskResultStatus `json:"status"` + // Message A short message describing the status of the task. + Message string `json:"message,omitempty"` + // URL that the user can use to get more information from the external service + URL string `json:"url,omitempty"` +} + +type TaskResultsCallbackRelationships struct { + // Outcomes A run task result may optionally contain one or more detailed outcomes, which improves result visibility and content in the Terraform Cloud user interface. + // https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#outcomes-payload-body + Outcomes *TaskResultsCallbackRelationshipsOutcomes `json:"outcomes"` +} + +type TaskResultsCallbackRelationshipsOutcomes struct { + Data []*TaskResultsCallbackRelationshipsOutcomesData `json:"data"` +} + +type TaskResultsCallbackRelationshipsOutcomesData struct { + Type string `json:"type"` + Attributes *TaskResultsCallbackRelationshipsOutcomesDataAttributes `json:"attributes"` +} + +type TaskResultsCallbackRelationshipsOutcomesDataAttributes struct { + OutcomeID string `json:"outcome-id"` + Description string `json:"description"` + Body string `json:"body,omitempty"` + URL string `json:"url,omitempty"` + Tags map[string][]*TaskResultsCallbackRelationshipsOutcomesDataTagsAttributes `json:"tags,omitempty"` +} + +// TaskResultsCallbackRelationshipsOutcomesDataTagsAttributes can be used to enrich outcomes display list in TFC/E. +// https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#severity-and-status-tags +type TaskResultsCallbackRelationshipsOutcomesDataTagsAttributes struct { + Label string `json:"label"` + Level string `json:"level,omitempty"` +} + +func (o *TaskResultsCallbackOptions) valid() error { + if o.Data == nil { + return ErrRequiredCallbackData + } + if validStringID(o.Data.Type) && o.Data.Type != String(TaskResultsCallbackType) { + return ErrInvalidTaskResultsCallbackType + } + if o.Data.Attributes == nil { + return ErrRequiredCallbackDataAttributes + } + if o.Data.Attributes.Status != TaskFailed || o.Data.Attributes.Status != TaskPassed || o.Data.Attributes.Status != TaskRunning { + return ErrInvalidTaskResultsCallbackStatus + } + return nil +} diff --git a/tfe.go b/tfe.go index d849f59bc..c4f4b44b0 100644 --- a/tfe.go +++ b/tfe.go @@ -158,6 +158,7 @@ type Client struct { Runs Runs RunEvents RunEvents RunTasks RunTasks + RunTasksCallback RunTasksCallback RunTriggers RunTriggers SSHKeys SSHKeys StateVersionOutputs StateVersionOutputs @@ -458,6 +459,7 @@ func NewClient(cfg *Config) (*Client, error) { client.Runs = &runs{client: client} client.RunEvents = &runEvents{client: client} client.RunTasks = &runTasks{client: client} + client.RunTasksCallback = &taskResultsCallback{client: client} client.RunTriggers = &runTriggers{client: client} client.SSHKeys = &sshKeys{client: client} client.StateVersionOutputs = &stateVersionOutputs{client: client} @@ -605,7 +607,7 @@ func (c *Client) retryHTTPBackoff(min, max time.Duration, attemptNum int, resp * // // min and max are mainly used for bounding the jitter that will be added to // the reset time retrieved from the headers. But if the final wait time is -// less then min, min will be used instead. +// less than min, min will be used instead. func rateLimitBackoff(min, max time.Duration, resp *http.Response) time.Duration { // rnd is used to generate pseudo-random numbers. rnd := rand.New(rand.NewSource(time.Now().UnixNano())) From 75e6c6886ee66a4c5a487b8cb5e2381b1cd1b506 Mon Sep 17 00:00:00 2001 From: Evangelos Karvounis Date: Tue, 12 Mar 2024 11:33:26 +0200 Subject: [PATCH 02/61] chore: set Run Tasks Integration as supported --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 13c31ea18..2ca2250a7 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ This API client covers most of the existing Terraform Cloud API calls and is upd - [x] Runs - [x] Run Events - [x] Run Tasks -- [ ] Run Tasks Integration +- [x] Run Tasks Integration - [x] Run Triggers - [x] SSH Keys - [x] Stability Policy From 8e5aad75836539b425a06534cbba6c00ad829522 Mon Sep 17 00:00:00 2001 From: Evangelos Karvounis Date: Wed, 3 Apr 2024 15:33:34 +0300 Subject: [PATCH 03/61] chore: replace json structs with jsonapi ones --- errors.go | 4 -- run_task_results_callback.go | 83 +++++++++++------------------------- 2 files changed, 26 insertions(+), 61 deletions(-) diff --git a/errors.go b/errors.go index b9abda96c..eec21102f 100644 --- a/errors.go +++ b/errors.go @@ -381,8 +381,4 @@ var ( ErrRequiredRawState = errors.New("RawState is required") ErrStateVersionUploadNotSupported = errors.New("upload not supported by this version of Terraform Enterprise") - - ErrRequiredCallbackData = errors.New("data object is required for TFE run task callback") - - ErrRequiredCallbackDataAttributes = errors.New("data attributes object is required for TFE run task callback") ) diff --git a/run_task_results_callback.go b/run_task_results_callback.go index 8c939cf82..1b6044484 100644 --- a/run_task_results_callback.go +++ b/run_task_results_callback.go @@ -13,8 +13,8 @@ var _ RunTasksCallback = (*taskResultsCallback)(nil) // TFE API docs: // https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration type RunTasksCallback interface { - // Update sends updates to TFC/E Run Task Callback URL.. - Update(ctx context.Context, callbackURL string, accessToken string, options TaskResultsCallbackOptions) error + // Update sends updates to TFC/E Run Task Callback URL + Update(ctx context.Context, callbackURL string, accessToken string, options TaskResultCallbackRequestOptions) error } // taskResultsCallback implements RunTasksCallback. @@ -27,7 +27,7 @@ const ( ) // Update sends updates to TFC/E Run Task Callback URL -func (s *taskResultsCallback) Update(ctx context.Context, callbackURL string, accessToken string, options TaskResultsCallbackOptions) error { +func (s *taskResultsCallback) Update(ctx context.Context, callbackURL string, accessToken string, options TaskResultCallbackRequestOptions) error { if !validString(&callbackURL) { return ErrInvalidCallbackURL } @@ -44,70 +44,39 @@ func (s *taskResultsCallback) Update(ctx context.Context, callbackURL string, ac return req.Do(ctx, nil) } -// TaskResultsCallbackOptions represents the options for a TFE Task result callback request +// TaskResultCallbackRequestOptions represents the TFC/E Task result callback request // https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#request-body-1 -type TaskResultsCallbackOptions struct { - Data *TaskResultsCallbackData `json:"data"` +type TaskResultCallbackRequestOptions struct { + Type string `jsonapi:"primary,task-results"` + Status TaskResultStatus `jsonapi:"attr,status"` + Message string `jsonapi:"attr,message,omitempty"` + URL string `jsonapi:"attr,url,omitempty"` + Outcomes []*TaskResultOutcome `jsonapi:"relation,outcomes,omitempty"` } -type TaskResultsCallbackData struct { - // Required: Must be set to `task-results` - Type *string `json:"type"` - // Required: Attributes of the Task Results Callback Response - Attributes *TaskResultsCallbackDataAttributes `json:"attributes"` - Relationships *TaskResultsCallbackRelationships `json:"relationships,omitempty"` +// TaskResultOutcome represents a detailed TFC/E run task outcome, which improves result visibility and content in the TFC/E UI. +// https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#outcomes-payload-body +type TaskResultOutcome struct { + Type string `jsonapi:"primary,task-result-outcomes"` + OutcomeID string `jsonapi:"attr,outcome-id,omitempty"` + Description string `jsonapi:"attr,description,omitempty"` + Body string `jsonapi:"attr,body,omitempty"` + URL string `jsonapi:"attr,url,omitempty"` + Tags map[string][]*TaskResultTag `jsonapi:"attr,tags,omitempty"` } -type TaskResultsCallbackDataAttributes struct { - // Status Must be one of TaskFailed, TaskPassed or TaskRunning - Status TaskResultStatus `json:"status"` - // Message A short message describing the status of the task. - Message string `json:"message,omitempty"` - // URL that the user can use to get more information from the external service - URL string `json:"url,omitempty"` -} - -type TaskResultsCallbackRelationships struct { - // Outcomes A run task result may optionally contain one or more detailed outcomes, which improves result visibility and content in the Terraform Cloud user interface. - // https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#outcomes-payload-body - Outcomes *TaskResultsCallbackRelationshipsOutcomes `json:"outcomes"` -} - -type TaskResultsCallbackRelationshipsOutcomes struct { - Data []*TaskResultsCallbackRelationshipsOutcomesData `json:"data"` -} - -type TaskResultsCallbackRelationshipsOutcomesData struct { - Type string `json:"type"` - Attributes *TaskResultsCallbackRelationshipsOutcomesDataAttributes `json:"attributes"` -} - -type TaskResultsCallbackRelationshipsOutcomesDataAttributes struct { - OutcomeID string `json:"outcome-id"` - Description string `json:"description"` - Body string `json:"body,omitempty"` - URL string `json:"url,omitempty"` - Tags map[string][]*TaskResultsCallbackRelationshipsOutcomesDataTagsAttributes `json:"tags,omitempty"` -} - -// TaskResultsCallbackRelationshipsOutcomesDataTagsAttributes can be used to enrich outcomes display list in TFC/E. +// TaskResultTag can be used to enrich outcomes display list in TFC/E. // https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#severity-and-status-tags -type TaskResultsCallbackRelationshipsOutcomesDataTagsAttributes struct { - Label string `json:"label"` - Level string `json:"level,omitempty"` +type TaskResultTag struct { + Label string `json:"label"` + Level *string `json:"level,omitempty"` } -func (o *TaskResultsCallbackOptions) valid() error { - if o.Data == nil { - return ErrRequiredCallbackData - } - if validStringID(o.Data.Type) && o.Data.Type != String(TaskResultsCallbackType) { +func (o *TaskResultCallbackRequestOptions) valid() error { + if !validStringID(&o.Type) || o.Type != TaskResultsCallbackType { return ErrInvalidTaskResultsCallbackType } - if o.Data.Attributes == nil { - return ErrRequiredCallbackDataAttributes - } - if o.Data.Attributes.Status != TaskFailed || o.Data.Attributes.Status != TaskPassed || o.Data.Attributes.Status != TaskRunning { + if !validStringID(String(string(o.Status))) || (o.Status != TaskFailed && o.Status != TaskPassed && o.Status != TaskRunning) { return ErrInvalidTaskResultsCallbackStatus } return nil From 5b9e41c4403dd430657a3833d912d7e8a4c8cda9 Mon Sep 17 00:00:00 2001 From: Evangelos Karvounis Date: Wed, 3 Apr 2024 15:35:56 +0300 Subject: [PATCH 04/61] chore: endpoint validation uses access token verification --- run_task_request.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/run_task_request.go b/run_task_request.go index 227de7188..84280a7d9 100644 --- a/run_task_request.go +++ b/run_task_request.go @@ -8,8 +8,8 @@ import ( ) const ( - // RunTaskRegistrationRunId is the string that TFC/E sends when it just wants to register the Run Task. - RunTaskRegistrationRunId = "run-xxxxxxxxxxxxxxxx" + // VerificationToken is a nonsense Terraform Cloud API token that should NEVER be valid. + VerificationToken = "test-token" ) // RunTaskRequest is the payload object that TFC/E sends to the Run Task's URL. @@ -48,6 +48,7 @@ type RunTaskRequestCapabilitites struct { } // IsEndpointValidation returns true if this is a Request from TFC/E to validate and register this API endpoint. +// Function copied from: https://github.com/hashicorp/terraform-run-task-scaffolding-go/blob/d7ed63b7d8eacf0897ab687d35d353386e4bd0ac/internal/sdk/api/structs.go#L55-L60 func (r RunTaskRequest) IsEndpointValidation() bool { - return r.RunID == RunTaskRegistrationRunId + return r.AccessToken == VerificationToken } From d9e1f41ee118e5a23bb04cb7c29c0f1ea15edc2b Mon Sep 17 00:00:00 2001 From: Evangelos Karvounis Date: Thu, 4 Apr 2024 08:30:34 +0300 Subject: [PATCH 05/61] chore(task-results-options): validate options in Update function --- run_task_results_callback.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/run_task_results_callback.go b/run_task_results_callback.go index 1b6044484..d24b8d27d 100644 --- a/run_task_results_callback.go +++ b/run_task_results_callback.go @@ -22,10 +22,6 @@ type taskResultsCallback struct { client *Client } -const ( - TaskResultsCallbackType = "task-results" -) - // Update sends updates to TFC/E Run Task Callback URL func (s *taskResultsCallback) Update(ctx context.Context, callbackURL string, accessToken string, options TaskResultCallbackRequestOptions) error { if !validString(&callbackURL) { @@ -34,6 +30,9 @@ func (s *taskResultsCallback) Update(ctx context.Context, callbackURL string, ac if !validString(&accessToken) { return ErrInvalidAccessToken } + if err := options.valid(); err != nil { + return err + } req, err := s.client.NewRequest(http.MethodPatch, callbackURL, &options) if err != nil { return err @@ -73,9 +72,6 @@ type TaskResultTag struct { } func (o *TaskResultCallbackRequestOptions) valid() error { - if !validStringID(&o.Type) || o.Type != TaskResultsCallbackType { - return ErrInvalidTaskResultsCallbackType - } if !validStringID(String(string(o.Status))) || (o.Status != TaskFailed && o.Status != TaskPassed && o.Status != TaskRunning) { return ErrInvalidTaskResultsCallbackStatus } From 7110deddd75435c1bccc6438b11603b2ddcd4bf2 Mon Sep 17 00:00:00 2001 From: Evangelos Karvounis Date: Mon, 17 Jun 2024 17:31:37 +0300 Subject: [PATCH 06/61] chore: move TaskResultCallbackRequestOptions above Update function --- run_task_results_callback.go | 42 ++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/run_task_results_callback.go b/run_task_results_callback.go index d24b8d27d..635218778 100644 --- a/run_task_results_callback.go +++ b/run_task_results_callback.go @@ -22,27 +22,6 @@ type taskResultsCallback struct { client *Client } -// Update sends updates to TFC/E Run Task Callback URL -func (s *taskResultsCallback) Update(ctx context.Context, callbackURL string, accessToken string, options TaskResultCallbackRequestOptions) error { - if !validString(&callbackURL) { - return ErrInvalidCallbackURL - } - if !validString(&accessToken) { - return ErrInvalidAccessToken - } - if err := options.valid(); err != nil { - return err - } - req, err := s.client.NewRequest(http.MethodPatch, callbackURL, &options) - if err != nil { - return err - } - // The PATCH request must use the token supplied in the originating request (access_token) for authentication. - // https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#request-headers-1 - req.Header.Set("Authorization", "Bearer "+accessToken) - return req.Do(ctx, nil) -} - // TaskResultCallbackRequestOptions represents the TFC/E Task result callback request // https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#request-body-1 type TaskResultCallbackRequestOptions struct { @@ -71,6 +50,27 @@ type TaskResultTag struct { Level *string `json:"level,omitempty"` } +// Update sends updates to TFC/E Run Task Callback URL +func (s *taskResultsCallback) Update(ctx context.Context, callbackURL string, accessToken string, options TaskResultCallbackRequestOptions) error { + if !validString(&callbackURL) { + return ErrInvalidCallbackURL + } + if !validString(&accessToken) { + return ErrInvalidAccessToken + } + if err := options.valid(); err != nil { + return err + } + req, err := s.client.NewRequest(http.MethodPatch, callbackURL, &options) + if err != nil { + return err + } + // The PATCH request must use the token supplied in the originating request (access_token) for authentication. + // https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#request-headers-1 + req.Header.Set("Authorization", "Bearer "+accessToken) + return req.Do(ctx, nil) +} + func (o *TaskResultCallbackRequestOptions) valid() error { if !validStringID(String(string(o.Status))) || (o.Status != TaskFailed && o.Status != TaskPassed && o.Status != TaskRunning) { return ErrInvalidTaskResultsCallbackStatus From a29db9a9de43a53ec7d43995c868942e178384ca Mon Sep 17 00:00:00 2001 From: Evangelos Karvounis Date: Mon, 17 Jun 2024 17:53:12 +0300 Subject: [PATCH 07/61] chore: add unit tests for TaskResultCallbackRequestOptions --- run_task_results_callback_test.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 run_task_results_callback_test.go diff --git a/run_task_results_callback_test.go b/run_task_results_callback_test.go new file mode 100644 index 000000000..fb3da977b --- /dev/null +++ b/run_task_results_callback_test.go @@ -0,0 +1,28 @@ +package tfe + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestTaskResultsCallbackRequestOptions(t *testing.T) { + t.Run("with an empty status", func(t *testing.T) { + o := TaskResultCallbackRequestOptions{Status: ""} + err := o.valid() + assert.EqualError(t, err, ErrInvalidTaskResultsCallbackStatus.Error()) + }) + t.Run("without a valid Status", func(t *testing.T) { + for _, s := range []TaskResultStatus{TaskPending, TaskErrored, "foo"} { + o := TaskResultCallbackRequestOptions{Status: s} + err := o.valid() + assert.EqualError(t, err, ErrInvalidTaskResultsCallbackStatus.Error()) + } + }) + t.Run("with a valid Status option", func(t *testing.T) { + for _, s := range []TaskResultStatus{TaskFailed, TaskPassed, TaskRunning} { + o := TaskResultCallbackRequestOptions{Status: s} + err := o.valid() + assert.Nil(t, err) + } + }) +} From 8acbcede9cae2af0fde8124ce3d4a86c6392f9dc Mon Sep 17 00:00:00 2001 From: Evangelos Karvounis Date: Tue, 18 Jun 2024 17:42:52 +0300 Subject: [PATCH 08/61] chore: add unit test TestTaskResultsCallbackRequestOptions_Marshal and TestTaskResultsCallbackUpdate --- errors.go | 2 - run_task_results_callback.go | 4 +- run_task_results_callback_test.go | 69 ++++++++++++++++++++++++++----- 3 files changed, 61 insertions(+), 14 deletions(-) diff --git a/errors.go b/errors.go index f34517666..3a5992eed 100644 --- a/errors.go +++ b/errors.go @@ -225,8 +225,6 @@ var ( ErrInvalidAccessToken = errors.New("invalid value for access token") - ErrInvalidTaskResultsCallbackType = errors.New("invalid value for task result type") - ErrInvalidTaskResultsCallbackStatus = errors.New(fmt.Sprintf("invalid value for task result status. Must be either `%s`, `%s`, or `%s`", TaskFailed, TaskPassed, TaskRunning)) ) diff --git a/run_task_results_callback.go b/run_task_results_callback.go index 635218778..b859ab287 100644 --- a/run_task_results_callback.go +++ b/run_task_results_callback.go @@ -46,8 +46,8 @@ type TaskResultOutcome struct { // TaskResultTag can be used to enrich outcomes display list in TFC/E. // https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#severity-and-status-tags type TaskResultTag struct { - Label string `json:"label"` - Level *string `json:"level,omitempty"` + Label string `json:"label"` + Level string `json:"level,omitempty"` } // Update sends updates to TFC/E Run Task Callback URL diff --git a/run_task_results_callback_test.go b/run_task_results_callback_test.go index fb3da977b..9e1fd9761 100644 --- a/run_task_results_callback_test.go +++ b/run_task_results_callback_test.go @@ -1,28 +1,77 @@ package tfe import ( + "bytes" + "context" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "testing" ) -func TestTaskResultsCallbackRequestOptions(t *testing.T) { +// TestTaskResultsCallbackRequestOptions_Validate runs a series of tests that test whether various TaskResultCallbackRequestOptions objects can be considered valid or not +func TestTaskResultsCallbackRequestOptions_Validate(t *testing.T) { t.Run("with an empty status", func(t *testing.T) { - o := TaskResultCallbackRequestOptions{Status: ""} - err := o.valid() + opts := TaskResultCallbackRequestOptions{Status: ""} + err := opts.valid() assert.EqualError(t, err, ErrInvalidTaskResultsCallbackStatus.Error()) }) - t.Run("without a valid Status", func(t *testing.T) { + t.Run("without valid Status options", func(t *testing.T) { for _, s := range []TaskResultStatus{TaskPending, TaskErrored, "foo"} { - o := TaskResultCallbackRequestOptions{Status: s} - err := o.valid() + opts := TaskResultCallbackRequestOptions{Status: s} + err := opts.valid() assert.EqualError(t, err, ErrInvalidTaskResultsCallbackStatus.Error()) } }) - t.Run("with a valid Status option", func(t *testing.T) { + t.Run("with valid Status options", func(t *testing.T) { for _, s := range []TaskResultStatus{TaskFailed, TaskPassed, TaskRunning} { - o := TaskResultCallbackRequestOptions{Status: s} - err := o.valid() - assert.Nil(t, err) + opts := TaskResultCallbackRequestOptions{Status: s} + err := opts.valid() + require.NoError(t, err) } }) } + +// TestTaskResultsCallbackRequestOptions_Marshal tests whether you can properly serialise a TaskResultCallbackRequestOptions object +// You may find the expected body here: https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#request-body-1 +func TestTaskResultsCallbackRequestOptions_Marshal(t *testing.T) { + opts := TaskResultCallbackRequestOptions{ + Status: TaskPassed, + Message: "4 passed, 0 skipped, 0 failed", + URL: "https://external.service.dev/terraform-plan-checker/run-i3Df5to9ELvibKpQ", + Outcomes: []*TaskResultOutcome{ + { + OutcomeID: "PRTNR-CC-TF-127", + Description: "ST-2942:S3 Bucket will not enforce MFA login on delete requests", + Body: "# Resolution for issue ST-2942\n\n## Impact\n\nFollow instructions in the [AWS S3 docs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/MultiFactorAuthenticationDelete.html) to manually configure the MFA setting.\nā€”-- Payload truncated ā€”--", + URL: "https://external.service.dev/result/PRTNR-CC-TF-127", + Tags: map[string][]*TaskResultTag{ + "Status": {&TaskResultTag{Label: "Denied", Level: "error"}}, + "Severity": { + &TaskResultTag{Label: "High", Level: "error"}, + &TaskResultTag{Label: "Recoverable", Level: "info"}, + }, + "Cost Centre": {&TaskResultTag{Label: "IT-OPS"}}, + }, + }, + }, + } + require.NoError(t, opts.valid()) + reqBody, err := serializeRequestBody(&opts) + require.NoError(t, err) + expectedBody := `{"data":{"type":"task-results","attributes":{"message":"4 passed, 0 skipped, 0 failed","status":"passed","url":"https://external.service.dev/terraform-plan-checker/run-i3Df5to9ELvibKpQ"},"relationships":{"outcomes":{"data":[{"type":"task-result-outcomes","attributes":{"body":"# Resolution for issue ST-2942\n\n## Impact\n\nFollow instructions in the [AWS S3 docs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/MultiFactorAuthenticationDelete.html) to manually configure the MFA setting.\nā€”-- Payload truncated ā€”--","description":"ST-2942:S3 Bucket will not enforce MFA login on delete requests","outcome-id":"PRTNR-CC-TF-127","tags":{"Cost Centre":[{"label":"IT-OPS"}],"Severity":[{"label":"High","level":"error"},{"label":"Recoverable","level":"info"}],"Status":[{"label":"Denied","level":"error"}]},"url":"https://external.service.dev/result/PRTNR-CC-TF-127"}}]}}}} +` + assert.Equal(t, reqBody.(*bytes.Buffer).String(), expectedBody) +} + +func TestTaskResultsCallbackUpdate(t *testing.T) { + t.Run("with invalid callbackURL", func(t *testing.T) { + trc := taskResultsCallback{client: nil} + err := trc.Update(context.Background(), "", "", TaskResultCallbackRequestOptions{}) + assert.EqualError(t, err, ErrInvalidCallbackURL.Error()) + }) + t.Run("with invalid accessToken", func(t *testing.T) { + trc := taskResultsCallback{client: nil} + err := trc.Update(context.Background(), "https://app.terraform.io/foo", "", TaskResultCallbackRequestOptions{}) + assert.EqualError(t, err, ErrInvalidAccessToken.Error()) + }) +} From 57a3f09280cde79c9f38df1e137fa0fab497b117 Mon Sep 17 00:00:00 2001 From: Evangelos Karvounis Date: Tue, 18 Jun 2024 18:39:51 +0300 Subject: [PATCH 09/61] chore: add test for taskResultsCallback Update function --- helper_test.go | 23 +++++++++++++++++++++++ run_task_results_callback_test.go | 23 ++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/helper_test.go b/helper_test.go index 1a0cf57e4..4d3dd5cb3 100644 --- a/helper_test.go +++ b/helper_test.go @@ -18,6 +18,7 @@ import ( "io" "math/rand" "net/http" + "net/http/httptest" "os" "os/exec" "path/filepath" @@ -34,6 +35,8 @@ import ( const badIdentifier = "! / nope" //nolint const agentVersion = "1.3.0" +const testInitialClientToken = "insert-your-token-here" +const testTaskResultCallbackToken = "this-is-task-result-callback-token" var _testAccountDetails *TestAccountDetails @@ -2855,6 +2858,26 @@ func requireExactlyOneNotEmpty(t *testing.T, v ...any) { } } +func runTaskCallbackMockServer(t *testing.T) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + return + } + if r.Header.Get("Accept") != ContentTypeJSONAPI { + t.Fatalf("unexpected accept header: %q", r.Header.Get("Accept")) + } + if r.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", testTaskResultCallbackToken) { + t.Fatalf("unexpected authorization header: %q", r.Header.Get("Authorization")) + } + if r.Header.Get("Authorization") == fmt.Sprintf("Bearer %s", testInitialClientToken) { + t.Fatalf("authorization header is still the initial one: %q", r.Header.Get("Authorization")) + } + if r.Header.Get("User-Agent") != "go-tfe" { + t.Fatalf("unexpected user agent header: %q", r.Header.Get("User-Agent")) + } + })) +} + // Useless key but enough to pass validation in the API const testGpgArmor string = ` -----BEGIN PGP PUBLIC KEY BLOCK----- diff --git a/run_task_results_callback_test.go b/run_task_results_callback_test.go index 9e1fd9761..5bb11d139 100644 --- a/run_task_results_callback_test.go +++ b/run_task_results_callback_test.go @@ -63,7 +63,7 @@ func TestTaskResultsCallbackRequestOptions_Marshal(t *testing.T) { assert.Equal(t, reqBody.(*bytes.Buffer).String(), expectedBody) } -func TestTaskResultsCallbackUpdate(t *testing.T) { +func TestTaskResultsCallbackUpdate_Validate(t *testing.T) { t.Run("with invalid callbackURL", func(t *testing.T) { trc := taskResultsCallback{client: nil} err := trc.Update(context.Background(), "", "", TaskResultCallbackRequestOptions{}) @@ -75,3 +75,24 @@ func TestTaskResultsCallbackUpdate(t *testing.T) { assert.EqualError(t, err, ErrInvalidAccessToken.Error()) }) } + +func TestTaskResultsCallbackUpdate(t *testing.T) { + ts := runTaskCallbackMockServer(t) + defer ts.Close() + + client, err := NewClient(&Config{ + RetryServerErrors: true, + Token: testInitialClientToken, + Address: ts.URL, + }) + require.NoError(t, err) + trc := taskResultsCallback{ + client: client, + } + req := RunTaskRequest{ + AccessToken: testTaskResultCallbackToken, + TaskResultCallbackURL: ts.URL, + } + err = trc.Update(context.Background(), req.TaskResultCallbackURL, req.AccessToken, TaskResultCallbackRequestOptions{Status: TaskPassed}) + require.NoError(t, err) +} From 4e1579713fbbfea450a534ca1fe0c0e537c27520 Mon Sep 17 00:00:00 2001 From: Evangelos Karvounis Date: Tue, 25 Jun 2024 17:14:18 +0300 Subject: [PATCH 10/61] chore: improvements based on golangci-lint suggestions --- errors.go | 2 +- run_task_results_callback.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/errors.go b/errors.go index 3a5992eed..67d912d4a 100644 --- a/errors.go +++ b/errors.go @@ -225,7 +225,7 @@ var ( ErrInvalidAccessToken = errors.New("invalid value for access token") - ErrInvalidTaskResultsCallbackStatus = errors.New(fmt.Sprintf("invalid value for task result status. Must be either `%s`, `%s`, or `%s`", TaskFailed, TaskPassed, TaskRunning)) + ErrInvalidTaskResultsCallbackStatus = fmt.Errorf("invalid value for task result status. Must be either `%s`, `%s`, or `%s`", TaskFailed, TaskPassed, TaskRunning) ) var ( diff --git a/run_task_results_callback.go b/run_task_results_callback.go index b859ab287..db3e37ccf 100644 --- a/run_task_results_callback.go +++ b/run_task_results_callback.go @@ -14,7 +14,7 @@ var _ RunTasksCallback = (*taskResultsCallback)(nil) // https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration type RunTasksCallback interface { // Update sends updates to TFC/E Run Task Callback URL - Update(ctx context.Context, callbackURL string, accessToken string, options TaskResultCallbackRequestOptions) error + Update(ctx context.Context, callbackURL, accessToken string, options TaskResultCallbackRequestOptions) error } // taskResultsCallback implements RunTasksCallback. @@ -51,7 +51,7 @@ type TaskResultTag struct { } // Update sends updates to TFC/E Run Task Callback URL -func (s *taskResultsCallback) Update(ctx context.Context, callbackURL string, accessToken string, options TaskResultCallbackRequestOptions) error { +func (s *taskResultsCallback) Update(ctx context.Context, callbackURL, accessToken string, options TaskResultCallbackRequestOptions) error { if !validString(&callbackURL) { return ErrInvalidCallbackURL } From 2767bde98aa522637b9bd01584f6eec429322a21 Mon Sep 17 00:00:00 2001 From: "hashicorp-copywrite[bot]" <110428419+hashicorp-copywrite[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 16:12:14 +0000 Subject: [PATCH 11/61] [COMPLIANCE] Add Copyright and License Headers --- stack.go | 3 +++ stack_integration_test.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/stack.go b/stack.go index 3e881128b..707a592ab 100644 --- a/stack.go +++ b/stack.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package tfe import ( diff --git a/stack_integration_test.go b/stack_integration_test.go index 39ab596bd..f3b352944 100644 --- a/stack_integration_test.go +++ b/stack_integration_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package tfe import ( From 7d657ff136acdd14ffe0bf7f199ac78c97d3a2a4 Mon Sep 17 00:00:00 2001 From: Luces Huayhuaca <21225410+uturunku1@users.noreply.github.com> Date: Tue, 2 Jul 2024 15:20:55 -0700 Subject: [PATCH 12/61] prep for release 1.58.0 (#928) --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a338b4cef..06cbd2182 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ # UNRELEASED +# v1.58.0 + ## Enhancements -* Adds BETA support for `Stacks` resources, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users. [#920](https://github.com/hashicorp/go-tfe/pull/920) +* Adds BETA support for `Stacks` resources, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @brandonc. [#920](https://github.com/hashicorp/go-tfe/pull/920) # v1.57.0 From 8bfe446971f10c87bcd821aaaf390e57187ef8dc Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Wed, 3 Jul 2024 11:47:22 -0400 Subject: [PATCH 13/61] Rebased "main" onto a local branch From 514e19eea5a8e8607cef8ab52a56b0593ce3e0cf Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Wed, 3 Jul 2024 14:51:27 -0400 Subject: [PATCH 14/61] Rename interface to RunTasksIntegration --- ...ts_callback.go => run_tasks_integration.go | 16 ++++++------- ...k_test.go => run_tasks_integration_test.go | 23 ++++++++++--------- tfe.go | 4 ++-- 3 files changed, 22 insertions(+), 21 deletions(-) rename run_task_results_callback.go => run_tasks_integration.go (80%) rename run_task_results_callback_test.go => run_tasks_integration_test.go (83%) diff --git a/run_task_results_callback.go b/run_tasks_integration.go similarity index 80% rename from run_task_results_callback.go rename to run_tasks_integration.go index db3e37ccf..a2b0b9191 100644 --- a/run_task_results_callback.go +++ b/run_tasks_integration.go @@ -6,19 +6,19 @@ import ( ) // Compile-time proof of interface implementation. -var _ RunTasksCallback = (*taskResultsCallback)(nil) +var _ RunTasksIntegration = (*runTaskIntegration)(nil) -// RunTasksCallback describes all the Run Tasks Integration Callback API methods. +// RunTasksIntegration describes all the Run Tasks Integration Callback API methods. // // TFE API docs: // https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration -type RunTasksCallback interface { +type RunTasksIntegration interface { // Update sends updates to TFC/E Run Task Callback URL - Update(ctx context.Context, callbackURL, accessToken string, options TaskResultCallbackRequestOptions) error + Callback(ctx context.Context, callbackURL string, accessToken string, options TaskResultCallbackRequestOptions) error } -// taskResultsCallback implements RunTasksCallback. -type taskResultsCallback struct { +// taskResultsCallback implements RunTasksIntegration. +type runTaskIntegration struct { client *Client } @@ -51,7 +51,7 @@ type TaskResultTag struct { } // Update sends updates to TFC/E Run Task Callback URL -func (s *taskResultsCallback) Update(ctx context.Context, callbackURL, accessToken string, options TaskResultCallbackRequestOptions) error { +func (s *runTaskIntegration) Callback(ctx context.Context, callbackURL, accessToken string, options TaskResultCallbackRequestOptions) error { if !validString(&callbackURL) { return ErrInvalidCallbackURL } @@ -72,7 +72,7 @@ func (s *taskResultsCallback) Update(ctx context.Context, callbackURL, accessTok } func (o *TaskResultCallbackRequestOptions) valid() error { - if !validStringID(String(string(o.Status))) || (o.Status != TaskFailed && o.Status != TaskPassed && o.Status != TaskRunning) { + if o.Status != TaskFailed && o.Status != TaskPassed && o.Status != TaskRunning { return ErrInvalidTaskResultsCallbackStatus } return nil diff --git a/run_task_results_callback_test.go b/run_tasks_integration_test.go similarity index 83% rename from run_task_results_callback_test.go rename to run_tasks_integration_test.go index 5bb11d139..d3f606067 100644 --- a/run_task_results_callback_test.go +++ b/run_tasks_integration_test.go @@ -3,13 +3,14 @@ package tfe import ( "bytes" "context" + "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "testing" ) -// TestTaskResultsCallbackRequestOptions_Validate runs a series of tests that test whether various TaskResultCallbackRequestOptions objects can be considered valid or not -func TestTaskResultsCallbackRequestOptions_Validate(t *testing.T) { +// TestRunTasksIntegration_Validate runs a series of tests that test whether various TaskResultCallbackRequestOptions objects can be considered valid or not +func TestRunTasksIntegration_Validate(t *testing.T) { t.Run("with an empty status", func(t *testing.T) { opts := TaskResultCallbackRequestOptions{Status: ""} err := opts.valid() @@ -63,20 +64,20 @@ func TestTaskResultsCallbackRequestOptions_Marshal(t *testing.T) { assert.Equal(t, reqBody.(*bytes.Buffer).String(), expectedBody) } -func TestTaskResultsCallbackUpdate_Validate(t *testing.T) { +func TestRunTasksIntegration_ValidateCallback(t *testing.T) { t.Run("with invalid callbackURL", func(t *testing.T) { - trc := taskResultsCallback{client: nil} - err := trc.Update(context.Background(), "", "", TaskResultCallbackRequestOptions{}) + trc := runTaskIntegration{client: nil} + err := trc.Callback(context.Background(), "", "", TaskResultCallbackRequestOptions{}) assert.EqualError(t, err, ErrInvalidCallbackURL.Error()) }) t.Run("with invalid accessToken", func(t *testing.T) { - trc := taskResultsCallback{client: nil} - err := trc.Update(context.Background(), "https://app.terraform.io/foo", "", TaskResultCallbackRequestOptions{}) + trc := runTaskIntegration{client: nil} + err := trc.Callback(context.Background(), "https://app.terraform.io/foo", "", TaskResultCallbackRequestOptions{}) assert.EqualError(t, err, ErrInvalidAccessToken.Error()) }) } -func TestTaskResultsCallbackUpdate(t *testing.T) { +func TestRunTasksIntegration_Callback(t *testing.T) { ts := runTaskCallbackMockServer(t) defer ts.Close() @@ -86,13 +87,13 @@ func TestTaskResultsCallbackUpdate(t *testing.T) { Address: ts.URL, }) require.NoError(t, err) - trc := taskResultsCallback{ + trc := runTaskIntegration{ client: client, } req := RunTaskRequest{ AccessToken: testTaskResultCallbackToken, TaskResultCallbackURL: ts.URL, } - err = trc.Update(context.Background(), req.TaskResultCallbackURL, req.AccessToken, TaskResultCallbackRequestOptions{Status: TaskPassed}) + err = trc.Callback(context.Background(), req.TaskResultCallbackURL, req.AccessToken, TaskResultCallbackRequestOptions{Status: TaskPassed}) require.NoError(t, err) } diff --git a/tfe.go b/tfe.go index da10e6ef2..c35705312 100644 --- a/tfe.go +++ b/tfe.go @@ -158,7 +158,7 @@ type Client struct { Runs Runs RunEvents RunEvents RunTasks RunTasks - RunTasksCallback RunTasksCallback + RunTasksIntegration RunTasksIntegration RunTriggers RunTriggers SSHKeys SSHKeys Stacks Stacks @@ -460,7 +460,7 @@ func NewClient(cfg *Config) (*Client, error) { client.Runs = &runs{client: client} client.RunEvents = &runEvents{client: client} client.RunTasks = &runTasks{client: client} - client.RunTasksCallback = &taskResultsCallback{client: client} + client.RunTasksIntegration = &runTaskIntegration{client: client} client.RunTriggers = &runTriggers{client: client} client.SSHKeys = &sshKeys{client: client} client.Stacks = &stacks{client: client} From 1f5ad0c71ac6f0ecc10b1d7991c17cd315be04b5 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Wed, 3 Jul 2024 14:51:43 -0400 Subject: [PATCH 15/61] Remove extraneous token check --- run_task_request.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/run_task_request.go b/run_task_request.go index 84280a7d9..b903f9486 100644 --- a/run_task_request.go +++ b/run_task_request.go @@ -7,11 +7,6 @@ import ( "time" ) -const ( - // VerificationToken is a nonsense Terraform Cloud API token that should NEVER be valid. - VerificationToken = "test-token" -) - // RunTaskRequest is the payload object that TFC/E sends to the Run Task's URL. // https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#common-properties type RunTaskRequest struct { @@ -46,9 +41,3 @@ type RunTaskRequest struct { type RunTaskRequestCapabilitites struct { Outcomes bool `json:"outcomes"` } - -// IsEndpointValidation returns true if this is a Request from TFC/E to validate and register this API endpoint. -// Function copied from: https://github.com/hashicorp/terraform-run-task-scaffolding-go/blob/d7ed63b7d8eacf0897ab687d35d353386e4bd0ac/internal/sdk/api/structs.go#L55-L60 -func (r RunTaskRequest) IsEndpointValidation() bool { - return r.AccessToken == VerificationToken -} From e4f527e0e7e4fe2021dcdce16f943dcdae2c8bd2 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Wed, 3 Jul 2024 15:05:19 -0400 Subject: [PATCH 16/61] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06cbd2182..a37251af0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # UNRELEASED +## Features + +* Adds support for the Run Tasks Integration API by @karvounis-form3 [#929](https://github.com/hashicorp/go-tfe/pull/929) + # v1.58.0 ## Enhancements From e48db07ae6d64f8469cd0444c7aa493a94e59f0a Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Tue, 9 Jul 2024 14:41:00 -0400 Subject: [PATCH 17/61] Update CHANGELOG.md for v1.59.0 release (#932) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a37251af0..5a28e7df9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # UNRELEASED +# v1.59.0 + ## Features * Adds support for the Run Tasks Integration API by @karvounis-form3 [#929](https://github.com/hashicorp/go-tfe/pull/929) From 00d8d5c2f54622f7ded5d9e8ebe6fed2d62cfbbf Mon Sep 17 00:00:00 2001 From: Teresa Chu Date: Wed, 10 Jul 2024 13:39:41 -0400 Subject: [PATCH 18/61] add no-code-upgrade-avalable as an attribute on workspace struct. --- workspace.go | 1 + 1 file changed, 1 insertion(+) diff --git a/workspace.go b/workspace.go index 46a332dd6..4fa4984e1 100644 --- a/workspace.go +++ b/workspace.go @@ -173,6 +173,7 @@ type Workspace struct { Locked bool `jsonapi:"attr,locked"` MigrationEnvironment string `jsonapi:"attr,migration-environment"` Name string `jsonapi:"attr,name"` + NoCodeUpgradeAvailable bool `jsonapi:"attr,no-code-upgrade-available"` Operations bool `jsonapi:"attr,operations"` Permissions *WorkspacePermissions `jsonapi:"attr,permissions"` QueueAllRuns bool `jsonapi:"attr,queue-all-runs"` From f73f83308b91c36a663a947fdbd66cd111122776 Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Fri, 12 Jul 2024 16:48:39 -0600 Subject: [PATCH 19/61] More Stacks, StackPlans support --- stack.go | 77 ++++++++++++++++- stack_configuration.go | 33 ++++++++ stack_deployments.go | 31 +++++++ stack_integration_test.go | 147 +++++++++++++++++++++++++++++++++ stack_plan.go | 114 +++++++++++++++++++++++++ stack_plan_integration_test.go | 53 ++++++++++++ tfe.go | 6 ++ 7 files changed, 460 insertions(+), 1 deletion(-) create mode 100644 stack_configuration.go create mode 100644 stack_deployments.go create mode 100644 stack_plan.go create mode 100644 stack_plan_integration_test.go diff --git a/stack.go b/stack.go index 707a592ab..02c87d4f0 100644 --- a/stack.go +++ b/stack.go @@ -28,6 +28,9 @@ type Stacks interface { // Delete deletes a stack. Delete(ctx context.Context, stackID string) error + + // UpdateConfiguration updates the configuration of a stack, triggering stack preparation. + UpdateConfiguration(ctx context.Context, stackID string) (*Stack, error) } // stacks implements Stacks. @@ -61,6 +64,14 @@ type StackList struct { Items []*Stack } +type StackDiagnosticsList struct { + *Pagination + Items []*StackDiagnostics +} + +type StackDiagnostics struct { +} + // StackVCSRepo represents the version control system repository for a stack. type StackVCSRepo struct { Identifier string `jsonapi:"attr,identifier"` @@ -82,7 +93,56 @@ type Stack struct { UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` // Relationships - Project *Project `jsonapi:"relation,project"` + Project *Project `jsonapi:"relation,project"` + LatestStackConfiguration *StackConfiguration `jsonapi:"relation,latest-stack-configuration"` + StackDiagnostics *StackDiagnostics `jsonapi:"relation,stack-diagnostics"` +} + +type StackConfigurationStatusTimestamps struct { + QueuedAt *time.Time `jsonapi:"attr,queued-at,omitempty,rfc3339"` + CompletedAt *time.Time `jsonapi:"attr,completed-at,omitempty,rfc3339"` + PreparingAt *time.Time `jsonapi:"attr,preparing-at,omitempty,rfc3339"` + EnqueueingAt *time.Time `jsonapi:"attr,enqueueing-at,omitempty,rfc3339"` + CanceledAt *time.Time `jsonapi:"attr,canceled-at,omitempty,rfc3339"` + ErroredAt *time.Time `jsonapi:"attr,errored-at,omitempty,rfc3339"` +} + +type StackComponent struct { + Name string `json:"name"` + Correlator string `json:"correlator"` + Expanded bool `json:"expanded"` +} + +type StackConfiguration struct { + // Attributes + ID string `jsonapi:"primary,stack-configurations"` + Status string `jsonapi:"attr,status"` + StatusTimestamps *StackConfigurationStatusTimestamps `jsonapi:"attr,status-timestamps"` + SequenceNumber int `jsonapi:"attr,sequence-number"` + DeploymentNames []string `jsonapi:"attr,deployment-names"` + ConvergedDeployments []string `jsonapi:"attr,converged-deployments"` + Components []*StackComponent `jsonapi:"attr,components"` + ErrorMessage *string `jsonapi:"attr,error-message"` + EventStreamURL string `jsonapi:"attr,event-stream-url"` +} + +type StackDeployment struct { + // Attributes + ID string `jsonapi:"primary,stack-deployments"` + Name string `jsonapi:"attr,name"` + Status string `jsonapi:"attr,status"` + DeployedAt time.Time `jsonapi:"attr,deployed-at,iso8601"` + ErrorsCount int `jsonapi:"attr,errors-count"` + WarningsCount int `jsonapi:"attr,warnings-count"` + PausedCount int `jsonapi:"attr,paused-count"` + + // Relationships + CurrentStackState *StackState `jsonapi:"relation,current-stack-state"` +} + +type StackState struct { + // Attributes + ID string `jsonapi:"primary,stack-states"` } // StackListOptions represents the options for listing stacks. @@ -110,6 +170,21 @@ type StackUpdateOptions struct { VCSRepo *StackVCSRepo `jsonapi:"attr,vcs-repo,omitempty"` } +func (s stacks) UpdateConfiguration(ctx context.Context, stackID string) (*Stack, error) { + req, err := s.client.NewRequest("POST", fmt.Sprintf("stacks/%s/actions/update-configuration", url.PathEscape(stackID)), nil) + if err != nil { + return nil, err + } + + stack := &Stack{} + err = req.Do(ctx, stack) + if err != nil { + return nil, err + } + + return stack, nil +} + func (s stacks) List(ctx context.Context, organization string, options *StackListOptions) (*StackList, error) { if err := options.valid(); err != nil { return nil, err diff --git a/stack_configuration.go b/stack_configuration.go new file mode 100644 index 000000000..322d69074 --- /dev/null +++ b/stack_configuration.go @@ -0,0 +1,33 @@ +package tfe + +import ( + "context" + "fmt" + "net/url" +) + +type StackConfigurations interface { + // ReadConfiguration returns a stack configuration by its ID. + Read(ctx context.Context, ID string) (*StackConfiguration, error) +} + +type stackConfigurations struct { + client *Client +} + +var _ StackConfigurations = &stackConfigurations{} + +func (s stackConfigurations) Read(ctx context.Context, ID string) (*StackConfiguration, error) { + req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-configurations/%s", url.PathEscape(ID)), nil) + if err != nil { + return nil, err + } + + stackConfiguration := &StackConfiguration{} + err = req.Do(ctx, stackConfiguration) + if err != nil { + return nil, err + } + + return stackConfiguration, nil +} diff --git a/stack_deployments.go b/stack_deployments.go new file mode 100644 index 000000000..11f1fae7c --- /dev/null +++ b/stack_deployments.go @@ -0,0 +1,31 @@ +package tfe + +import ( + "context" + "fmt" + "net/url" +) + +type StackDeployments interface { + // Read returns a stack deployment by its name. + Read(ctx context.Context, stackID, deployment string) (*StackDeployment, error) +} + +type stackDeployments struct { + client *Client +} + +func (s stackDeployments) Read(ctx context.Context, stackID, deploymentName string) (*StackDeployment, error) { + req, err := s.client.NewRequest("GET", fmt.Sprintf("stacks/%s/stack-deployments/%s", url.PathEscape(stackID), url.PathEscape(deploymentName)), nil) + if err != nil { + return nil, err + } + + deployment := &StackDeployment{} + err = req.Do(ctx, deployment) + if err != nil { + return nil, err + } + + return deployment, nil +} diff --git a/stack_integration_test.go b/stack_integration_test.go index f3b352944..a8c832beb 100644 --- a/stack_integration_test.go +++ b/stack_integration_test.go @@ -6,6 +6,7 @@ package tfe import ( "context" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -166,6 +167,10 @@ func TestStackReadUpdateDelete(t *testing.T) { require.NoError(t, err) require.Equal(t, "updated description", stackUpdated.Description) + stackUpdatedConfig, err := client.Stacks.UpdateConfiguration(ctx, stack.ID) + require.NoError(t, err) + require.Equal(t, stack.Name, stackUpdatedConfig.Name) + err = client.Stacks.Delete(ctx, stack.ID) require.NoError(t, err) @@ -173,3 +178,145 @@ func TestStackReadUpdateDelete(t *testing.T) { require.ErrorIs(t, err, ErrResourceNotFound) require.Nil(t, stackReadAfterDelete) } + +func pollStackDeployments(t *testing.T, ctx context.Context, client *Client, stackID string) (stack *Stack) { + t.Helper() + + // pollStackDeployments will poll the given stack until it has deployments or the deadline is reached. + ctx, cancel := context.WithDeadline(ctx, time.Now().Add(5*time.Minute)) + defer cancel() + + deadline, _ := ctx.Deadline() + t.Logf("Polling stack %q for deployments with deadline of %s", stackID, deadline) + + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for finished := false; !finished; { + t.Log("...") + select { + case <-ctx.Done(): + t.Fatalf("Stack %q had no deployments at deadline", stackID) + case <-ticker.C: + var err error + stack, err = client.Stacks.Read(ctx, stackID) + if err != nil { + t.Fatalf("Failed to read stack %q: %s", stackID, err) + } + + t.Logf("Stack %q had %d deployments", stack.ID, len(stack.DeploymentNames)) + if len(stack.DeploymentNames) > 0 { + finished = true + } + } + } + + return +} + +func pollStackDeploymentStatus(t *testing.T, ctx context.Context, client *Client, stackID, deploymentName, status string) { + // pollStackDeployments will poll the given stack until it has deployments or the deadline is reached. + ctx, cancel := context.WithDeadline(ctx, time.Now().Add(5*time.Minute)) + defer cancel() + + deadline, _ := ctx.Deadline() + t.Logf("Polling stack %q for deployments with deadline of %s", stackID, deadline) + + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for finished := false; !finished; { + t.Log("...") + select { + case <-ctx.Done(): + t.Fatalf("Stack deployment %s/%s did not have status %q at deadline", stackID, deploymentName, status) + case <-ticker.C: + var err error + deployment, err := client.StackDeployments.Read(ctx, stackID, deploymentName) + if err != nil { + t.Fatalf("Failed to read stack %q: %s", stackID, err) + } + + t.Logf("Stack deployment %s/%s had status %q", stackID, deploymentName, deployment.Status) + if deployment.Status == status { + finished = true + } + } + } +} + +func pollStackConfigurationStatus(t *testing.T, ctx context.Context, client *Client, stackConfigID, status string) (stackConfig *StackConfiguration) { + // pollStackDeployments will poll the given stack until it has deployments or the deadline is reached. + ctx, cancel := context.WithDeadline(ctx, time.Now().Add(5*time.Minute)) + defer cancel() + + deadline, _ := ctx.Deadline() + t.Logf("Polling stack configuration %q for status %q with deadline of %s", stackConfigID, status, deadline) + + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + var err error + for finished := false; !finished; { + t.Log("...") + select { + case <-ctx.Done(): + t.Fatalf("Stack configuration %q did not have status %q at deadline", stackConfigID, status) + case <-ticker.C: + stackConfig, err = client.StackConfigurations.Read(ctx, stackConfigID) + if err != nil { + t.Fatalf("Failed to read stack configuration %q: %s", stackConfigID, err) + } + + t.Logf("Stack configuration %q had status %q", stackConfigID, stackConfig.Status) + if stackConfig.Status == status { + finished = true + } + } + } + + return +} + +func TestStackConverged(t *testing.T) { + skipUnlessBeta(t) + + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + + oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil) + t.Cleanup(cleanup) + + stack, err := client.Stacks.Create(ctx, StackCreateOptions{ + Name: "test-stack", + VCSRepo: &StackVCSRepo{ + Identifier: "brandonc/pet-nulls-stack", + OAuthTokenID: oauthClient.OAuthTokens[0].ID, + }, + Project: &Project{ + ID: orgTest.DefaultProject.ID, + }, + }) + + require.NoError(t, err) + require.NotNil(t, stack) + + stackUpdated, err := client.Stacks.UpdateConfiguration(ctx, stack.ID) + require.NoError(t, err) + require.NotNil(t, stackUpdated) + + deployments := []string{"production", "staging"} + + stack = pollStackDeployments(t, ctx, client, stackUpdated.ID) + require.ElementsMatch(t, deployments, stack.DeploymentNames) + require.NotNil(t, stack.LatestStackConfiguration) + + for _, deployment := range deployments { + pollStackDeploymentStatus(t, ctx, client, stack.ID, deployment, "paused") + } + + pollStackConfigurationStatus(t, ctx, client, stack.LatestStackConfiguration.ID, "converged") +} diff --git a/stack_plan.go b/stack_plan.go new file mode 100644 index 000000000..ecaaf7504 --- /dev/null +++ b/stack_plan.go @@ -0,0 +1,114 @@ +package tfe + +import ( + "context" + "fmt" + "net/url" + "time" +) + +// StackPlans describes all the stacks plans-related methods that the HCP Terraform API supports. +// NOTE WELL: This is a beta feature and is subject to change until noted otherwise in the +// release notes. +type StackPlans interface { + // Read returns a stack plan by its ID. + Read(ctx context.Context, stackPlanID string) (*StackPlan, error) + + // ListByConfiguration returns a list of stack plans for a given stack configuration. + ListByConfiguration(ctx context.Context, stackConfigurationID string, options *StackPlansListOptions) (*StackPlanList, error) +} + +type StackPlansStatusFilter string + +const ( + StackPlansStatusFilterCreated StackPlansStatusFilter = "created" + StackPlansStatusFilterRunning StackPlansStatusFilter = "running" + StackPlansStatusFilterPaused StackPlansStatusFilter = "paused" + StackPlansStatusFilterFinished StackPlansStatusFilter = "finished" + StackPlansStatusFilterDiscarded StackPlansStatusFilter = "discarded" + StackPlansStatusFilterErrored StackPlansStatusFilter = "errored" + StackPlansStatusFilterCanceled StackPlansStatusFilter = "canceled" +) + +type StackPlansListOptions struct { + ListOptions + + // Optional: A query string to filter plans by status. + Status StackPlansStatusFilter `url:"filter[status],omitempty"` + + // Optional: A query string to filter plans by deployment. + Deployment string `url:"filter[deployment],omitempty"` +} + +type StackPlanList struct { + *Pagination + Items []*StackPlan +} + +// stackPlans implements StackPlans. +type stackPlans struct { + client *Client +} + +var _ StackPlans = &stackPlans{} + +type StackPlanStatusTimestamps struct { + CreatedAt time.Time `jsonapi:"attr,created-at,rfc3339"` + RunningAt time.Time `jsonapi:"attr,running-at,rfc3339"` + PausedAt time.Time `jsonapi:"attr,paused-at,rfc3339"` + FinishedAt time.Time `jsonapi:"attr,finished-at,rfc3339"` +} + +type PlanChanges struct { + Add int `jsonapi:"attr,add"` + Total int `jsonapi:"attr,total"` + Change int `jsonapi:"attr,change"` + Import int `jsonapi:"attr,import"` + Remove int `jsonapi:"attr,remove"` +} + +// StackPlan represents a plan for a stack. +type StackPlan struct { + ID string `jsonapi:"primary,stack-plans"` + PlanMode string `jsonapi:"attr,plan-mode"` + PlanNumber string `jsonapi:"attr,plan-number"` + Status string `jsonapi:"attr,status"` + StatusTimestamps *StackPlanStatusTimestamps `jsonapi:"attr,status-timestamps"` + IsPlanned bool `jsonapi:"attr,is-planned"` + Changes *PlanChanges `jsonapi:"attr,changes"` + Deployment string `jsonapi:"attr,deployment"` + + // Relationships + StackConfiguration *StackConfiguration `jsonapi:"relation,stack-configuration"` + Stack *Stack `jsonapi:"relation,stack"` +} + +func (s stackPlans) Read(ctx context.Context, stackPlanID string) (*StackPlan, error) { + req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-plans/%s", url.PathEscape(stackPlanID)), nil) + if err != nil { + return nil, err + } + + sp := &StackPlan{} + err = req.Do(ctx, sp) + if err != nil { + return nil, err + } + + return sp, nil +} + +func (s stackPlans) ListByConfiguration(ctx context.Context, stackConfigurationID string, options *StackPlansListOptions) (*StackPlanList, error) { + req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-configurations/%s/stack-plans", url.PathEscape(stackConfigurationID)), options) + if err != nil { + return nil, err + } + + sl := &StackPlanList{} + err = req.Do(ctx, sl) + if err != nil { + return nil, err + } + + return sl, nil +} diff --git a/stack_plan_integration_test.go b/stack_plan_integration_test.go new file mode 100644 index 000000000..0bfe86332 --- /dev/null +++ b/stack_plan_integration_test.go @@ -0,0 +1,53 @@ +package tfe + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestStackPlanList(t *testing.T) { + skipUnlessBeta(t) + + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + + _, err := client.Projects.Create(ctx, orgTest.Name, ProjectCreateOptions{ + Name: "test-project-2", + }) + require.NoError(t, err) + + oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil) + t.Cleanup(cleanup) + + stack, err := client.Stacks.Create(ctx, StackCreateOptions{ + Name: "aa-test-stack", + VCSRepo: &StackVCSRepo{ + Identifier: "brandonc/pet-nulls-stack", + OAuthTokenID: oauthClient.OAuthTokens[0].ID, + }, + Project: &Project{ + ID: orgTest.DefaultProject.ID, + }, + }) + require.NoError(t, err) + + stackUpdated, err := client.Stacks.UpdateConfiguration(ctx, stack.ID) + require.NoError(t, err) + + stackUpdated = pollStackDeployments(t, ctx, client, stackUpdated.ID) + require.NotNil(t, stackUpdated.LatestStackConfiguration) + + planList, err := client.StackPlans.ListByConfiguration(ctx, stackUpdated.LatestStackConfiguration.ID, &StackPlansListOptions{}) + require.NoError(t, err) + + require.Len(t, planList.Items, 4) + + plan, err := client.StackPlans.Read(ctx, planList.Items[0].ID) + require.NoError(t, err) + require.NotNil(t, plan) +} diff --git a/tfe.go b/tfe.go index c35705312..b42ef4149 100644 --- a/tfe.go +++ b/tfe.go @@ -162,6 +162,9 @@ type Client struct { RunTriggers RunTriggers SSHKeys SSHKeys Stacks Stacks + StackConfigurations StackConfigurations + StackDeployments StackDeployments + StackPlans StackPlans StateVersionOutputs StateVersionOutputs StateVersions StateVersions TaskResults TaskResults @@ -464,6 +467,9 @@ func NewClient(cfg *Config) (*Client, error) { client.RunTriggers = &runTriggers{client: client} client.SSHKeys = &sshKeys{client: client} client.Stacks = &stacks{client: client} + client.StackConfigurations = &stackConfigurations{client: client} + client.StackDeployments = &stackDeployments{client: client} + client.StackPlans = &stackPlans{client: client} client.StateVersionOutputs = &stateVersionOutputs{client: client} client.StateVersions = &stateVersions{client: client} client.TaskResults = &taskResults{client: client} From 79e2f3725e6c52570d91464d38560779fadecf2e Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Fri, 12 Jul 2024 17:03:28 -0600 Subject: [PATCH 20/61] Adds additional godoc comments and CHANGELOG --- CHANGELOG.md | 2 ++ stack_configuration.go | 4 ++++ stack_deployments.go | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a28e7df9..db2500d2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # UNRELEASED +* Adds more BETA support for `Stacks` resources, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @brandonc. [#934](https://github.com/hashicorp/go-tfe/pull/934) + # v1.59.0 ## Features diff --git a/stack_configuration.go b/stack_configuration.go index 322d69074..91c7c2dcd 100644 --- a/stack_configuration.go +++ b/stack_configuration.go @@ -6,6 +6,10 @@ import ( "net/url" ) +// StackConfigurations describes all the stacks configurations-related methods that the +// HCP Terraform API supports. +// NOTE WELL: This is a beta feature and is subject to change until noted otherwise in the +// release notes. type StackConfigurations interface { // ReadConfiguration returns a stack configuration by its ID. Read(ctx context.Context, ID string) (*StackConfiguration, error) diff --git a/stack_deployments.go b/stack_deployments.go index 11f1fae7c..dd982b2a2 100644 --- a/stack_deployments.go +++ b/stack_deployments.go @@ -6,6 +6,10 @@ import ( "net/url" ) +// StackDeployments describes all the stacks deployments-related methods that the +// HCP Terraform API supports. +// NOTE WELL: This is a beta feature and is subject to change until noted otherwise in the +// release notes. type StackDeployments interface { // Read returns a stack deployment by its name. Read(ctx context.Context, stackID, deployment string) (*StackDeployment, error) From d780907af4baeb1551526eb35b28143b97a43ff8 Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Fri, 12 Jul 2024 17:14:35 -0600 Subject: [PATCH 21/61] Add Stack Plans actions --- stack_plan.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/stack_plan.go b/stack_plan.go index ecaaf7504..7c235a55a 100644 --- a/stack_plan.go +++ b/stack_plan.go @@ -16,6 +16,15 @@ type StackPlans interface { // ListByConfiguration returns a list of stack plans for a given stack configuration. ListByConfiguration(ctx context.Context, stackConfigurationID string, options *StackPlansListOptions) (*StackPlanList, error) + + // Approve approves a stack plan. + Approve(ctx context.Context, stackPlanID string) error + + // Cancel cancels a stack plan. + Cancel(ctx context.Context, stackPlanID string) error + + // Discard discards a stack plan. + Discard(ctx context.Context, stackPlanID string) error } type StackPlansStatusFilter string @@ -112,3 +121,30 @@ func (s stackPlans) ListByConfiguration(ctx context.Context, stackConfigurationI return sl, nil } + +func (s stackPlans) Approve(ctx context.Context, stackPlanID string) error { + req, err := s.client.NewRequest("POST", fmt.Sprintf("stack-plans/%s/approve", url.PathEscape(stackPlanID)), nil) + if err != nil { + return err + } + + return req.Do(ctx, nil) +} + +func (s stackPlans) Discard(ctx context.Context, stackPlanID string) error { + req, err := s.client.NewRequest("POST", fmt.Sprintf("stack-plans/%s/discard", url.PathEscape(stackPlanID)), nil) + if err != nil { + return err + } + + return req.Do(ctx, nil) +} + +func (s stackPlans) Cancel(ctx context.Context, stackPlanID string) error { + req, err := s.client.NewRequest("POST", fmt.Sprintf("stack-plans/%s/cancel", url.PathEscape(stackPlanID)), nil) + if err != nil { + return err + } + + return req.Do(ctx, nil) +} From ed5a42a98da9e87d75f20e21ef23659e839e277e Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Mon, 15 Jul 2024 10:08:48 -0600 Subject: [PATCH 22/61] lint fix --- stack_configuration.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stack_configuration.go b/stack_configuration.go index 91c7c2dcd..6a6c45e53 100644 --- a/stack_configuration.go +++ b/stack_configuration.go @@ -12,7 +12,7 @@ import ( // release notes. type StackConfigurations interface { // ReadConfiguration returns a stack configuration by its ID. - Read(ctx context.Context, ID string) (*StackConfiguration, error) + Read(ctx context.Context, id string) (*StackConfiguration, error) } type stackConfigurations struct { @@ -21,8 +21,8 @@ type stackConfigurations struct { var _ StackConfigurations = &stackConfigurations{} -func (s stackConfigurations) Read(ctx context.Context, ID string) (*StackConfiguration, error) { - req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-configurations/%s", url.PathEscape(ID)), nil) +func (s stackConfigurations) Read(ctx context.Context, id string) (*StackConfiguration, error) { + req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-configurations/%s", url.PathEscape(id)), nil) if err != nil { return nil, err } From 97ee49b54b7fd937dd76e1837bbb89303fca8293 Mon Sep 17 00:00:00 2001 From: "hashicorp-copywrite[bot]" <110428419+hashicorp-copywrite[bot]@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:12:06 +0000 Subject: [PATCH 23/61] [COMPLIANCE] Add Copyright and License Headers --- run_tasks_integration.go | 3 +++ run_tasks_integration_test.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/run_tasks_integration.go b/run_tasks_integration.go index a2b0b9191..fc150d9ae 100644 --- a/run_tasks_integration.go +++ b/run_tasks_integration.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package tfe import ( diff --git a/run_tasks_integration_test.go b/run_tasks_integration_test.go index d3f606067..b490de024 100644 --- a/run_tasks_integration_test.go +++ b/run_tasks_integration_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package tfe import ( From c9475a545fa254856b6d3200c5522137411c84f8 Mon Sep 17 00:00:00 2001 From: paladin-devops <83741749+paladin-devops@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:40:15 -0400 Subject: [PATCH 24/61] nc: Add method for creating no-code workspaces. The registry no-code module resource is updated with this commit to include an additional method which can be used to create workspaces using the registry no-code module. --- docs/CONTRIBUTING.md | 2 +- mocks/registry_no_code_module_mocks.go | 15 ++++++ registry_no_code_module.go | 66 ++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index eab28a649..4c9cafb51 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -20,7 +20,7 @@ There are instances where several new resources being added (i.e Workspace Run T After opening a PR, our CI system will perform a series of code checks, one of which is linting. Linting is not strictly required for a change to be merged, but it helps smooth the review process and catch common mistakes early. If you'd like to run the linters manually, follow these steps: -1. Ensure you have [installed golangci-lint](https://golangci-lint.run/usage/install/#local-installation) +1. Ensure you have [installed golangci-lint](https://golangci-lint.run/welcome/install/#local-installation) 2. Format your code by running `make fmt` 3. Run lint checks using `make lint` diff --git a/mocks/registry_no_code_module_mocks.go b/mocks/registry_no_code_module_mocks.go index b4b9f0db8..4aea68fbb 100644 --- a/mocks/registry_no_code_module_mocks.go +++ b/mocks/registry_no_code_module_mocks.go @@ -55,6 +55,21 @@ func (mr *MockRegistryNoCodeModulesMockRecorder) Create(ctx, organization, optio return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRegistryNoCodeModules)(nil).Create), ctx, organization, options) } +// CreateWorkspace mocks base method. +func (m *MockRegistryNoCodeModules) CreateWorkspace(ctx context.Context, noCodeModuleID string, options *tfe.RegistryNoCodeModuleCreateWorkspaceOptions) (*tfe.RegistryNoCodeModuleWorkspace, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateWorkspace", ctx, noCodeModuleID, options) + ret0, _ := ret[0].(*tfe.RegistryNoCodeModuleWorkspace) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateWorkspace indicates an expected call of CreateWorkspace. +func (mr *MockRegistryNoCodeModulesMockRecorder) CreateWorkspace(ctx, noCodeModuleID, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateWorkspace", reflect.TypeOf((*MockRegistryNoCodeModules)(nil).CreateWorkspace), ctx, noCodeModuleID, options) +} + // Delete mocks base method. func (m *MockRegistryNoCodeModules) Delete(ctx context.Context, ID string) error { m.ctrl.T.Helper() diff --git a/registry_no_code_module.go b/registry_no_code_module.go index 2f79cd6d5..0a7ea2665 100644 --- a/registry_no_code_module.go +++ b/registry_no_code_module.go @@ -33,6 +33,36 @@ type RegistryNoCodeModules interface { // Delete a registry no-code module // **Note: This API is still in BETA and subject to change.** Delete(ctx context.Context, ID string) error + + // CreateWorkspace creates a workspace using a no-code module. + CreateWorkspace(ctx context.Context, noCodeModuleID string, options *RegistryNoCodeModuleCreateWorkspaceOptions) (*RegistryNoCodeModuleWorkspace, error) +} + +type RegistryNoCodeModuleCreateWorkspaceOptions struct { + Type string `jsonapi:"primary,no-code-module-workspace"` + // Add more create options here + + // Name is the name of the workspace, which can only include letters, + // numbers, and _. This will be used as an identifier and must be unique in + // the organization. + Name *string `jsonapi:"attr,name,omitempty"` + + // Description is a description for the workspace. + Description *string `jsonapi:"attr,description,omitempty"` + + AutoApply *bool `jsonapi:"attr,auto-apply,omitempty"` + + // Project is the associated project with the workspace. If not provided, + // default project of the organization will be assigned to the workspace. + Project *Project `jsonapi:"relation,project,omitempty"` + + // Variables is the slice of variables to be configured for the no-code + // workspace. + Variables []*Variable `jsonapi:"relation,vars,omitempty"` +} + +type RegistryNoCodeModuleWorkspace struct { + Workspace } // registryNoCodeModules implements RegistryNoCodeModules. @@ -223,6 +253,31 @@ func (r *registryNoCodeModules) Delete(ctx context.Context, noCodeModuleID strin return req.Do(ctx, nil) } +// CreateWorkspace creates a no-code workspace using a no-code module. +func (r *registryNoCodeModules) CreateWorkspace( + ctx context.Context, + noCodeModuleID string, + options *RegistryNoCodeModuleCreateWorkspaceOptions, +) (*RegistryNoCodeModuleWorkspace, error) { + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("no-code-modules/%s/workspaces", url.QueryEscape(noCodeModuleID)) + req, err := r.client.NewRequest("POST", u, options) + if err != nil { + return nil, err + } + + w := &RegistryNoCodeModuleWorkspace{} + err = req.Do(ctx, w) + if err != nil { + return nil, err + } + + return w, nil +} + func (o RegistryNoCodeModuleCreateOptions) valid() error { if o.RegistryModule == nil || o.RegistryModule.ID == "" { return ErrRequiredRegistryModule @@ -246,3 +301,14 @@ func (o *RegistryNoCodeModuleUpdateOptions) valid() error { func (o *RegistryNoCodeModuleReadOptions) valid() error { return nil } + +func (o *RegistryNoCodeModuleCreateWorkspaceOptions) valid() error { + if !validString(o.Name) { + return ErrRequiredName + } + if !validStringID(o.Name) { + return ErrInvalidName + } + + return nil +} From 1af9761c45d2e1aaed9a287bd08450db5e60d421 Mon Sep 17 00:00:00 2001 From: paladin-devops <83741749+paladin-devops@users.noreply.github.com> Date: Mon, 15 Jul 2024 19:24:00 -0400 Subject: [PATCH 25/61] Add integration test for creating a workspace w/no-code. --- docs/TESTS.md | 1 + registry_no_code_module_integration_test.go | 81 +++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/docs/TESTS.md b/docs/TESTS.md index 0eb05893f..282378113 100644 --- a/docs/TESTS.md +++ b/docs/TESTS.md @@ -55,6 +55,7 @@ Tests are run against an actual backend so they require a valid backend address ```sh $ GITHUB_APP_INSTALLATION_ID=ghain-xxxx TFE_ADDRESS= https://tfe.local TFE_TOKEN=xxx GITHUB_POLICY_SET_IDENTIFIER=username/repository GITHUB_REGISTRY_MODULE_IDENTIFIER=username/repository go test -run "(GHA|GithubApp)" -v ./... ``` +8. `GITHUB_REGISTRY_NO_CODE_MODULE_IDENTIFIER` - Required for running tests for workspaces using no-code modules. ## 3. Make sure run queue settings are correct diff --git a/registry_no_code_module_integration_test.go b/registry_no_code_module_integration_test.go index 38b32cb4d..9b54ebf7b 100644 --- a/registry_no_code_module_integration_test.go +++ b/registry_no_code_module_integration_test.go @@ -5,7 +5,10 @@ package tfe import ( "context" + "fmt" + "os" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -298,3 +301,81 @@ func createNoCodeRegistryModule(t *testing.T, client *Client, orgName string, rm } } } + +func TestRegistryNoCodeModulesCreateWorkspace(t *testing.T) { + skipUnlessBeta(t) + client := testClient(t) + ctx := context.Background() + r := require.New(t) + + // create an org that will be deleted later. the wskp will live here + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + org, err := client.Organizations.Read(ctx, orgTest.Name) + r.NoError(err) + r.NotNil(org) + + githubIdentifier := os.Getenv("GITHUB_REGISTRY_NO_CODE_MODULE_IDENTIFIER") + if githubIdentifier == "" { + t.Skip("Export a valid GITHUB_REGISTRY_NO_CODE_MODULE_IDENTIFIER before running this test") + } + + token, cleanupToken := createOAuthToken(t, client, org) + defer cleanupToken() + + rmOpts := RegistryModuleCreateWithVCSConnectionOptions{ + VCSRepo: &RegistryModuleVCSRepoOptions{ + OrganizationName: String(org.Name), + Identifier: String(githubIdentifier), + Tags: Bool(true), + OAuthTokenID: String(token.ID), + DisplayIdentifier: String(githubIdentifier), + }, + } + + rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, rmOpts) + r.NoError(err) + + // 1. create the registry module + // 2. create the no-code module, with the registry module + // 3. use the ID to create the workspace + ncm, err := client.RegistryNoCodeModules.Create(ctx, org.Name, RegistryNoCodeModuleCreateOptions{ + RegistryModule: rm, + Enabled: Bool(true), + VariableOptions: nil, + }) + r.NoError(err) + r.NotNil(ncm) + + // We sleep for 10 seconds to let the module finish getting ready + time.Sleep(time.Second * 10) + + t.Run("test creating a workspace via a no-code module", func(t *testing.T) { + wn := fmt.Sprintf("foo-%s", randomString(t)) + _, err = client.RegistryNoCodeModules.CreateWorkspace( + ctx, + ncm.ID, + &RegistryNoCodeModuleCreateWorkspaceOptions{ + Name: String(wn), + }, + ) + r.NoError(err) + + w, err := client.Workspaces.Read(ctx, org.Name, wn) + r.NoError(err) + r.Equal(wn, w.Name) + }) + + t.Run("fail to create a workspace with a bad module ID", func(t *testing.T) { + wn := fmt.Sprintf("foo-%s", randomString(t)) + _, err = client.RegistryNoCodeModules.CreateWorkspace( + ctx, + "codeno-abc123XYZ", + &RegistryNoCodeModuleCreateWorkspaceOptions{ + Name: String(wn), + }, + ) + r.Error(err) + }) +} From f69f5e822133fa83d9dae9d5d7bbabf71c221653 Mon Sep 17 00:00:00 2001 From: paladin-devops <83741749+paladin-devops@users.noreply.github.com> Date: Tue, 16 Jul 2024 13:49:23 -0400 Subject: [PATCH 26/61] Add source name & URL to no-code wksp creation. --- registry_no_code_module.go | 6 ++++++ registry_no_code_module_integration_test.go | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/registry_no_code_module.go b/registry_no_code_module.go index 0a7ea2665..8a6b138bd 100644 --- a/registry_no_code_module.go +++ b/registry_no_code_module.go @@ -59,6 +59,12 @@ type RegistryNoCodeModuleCreateWorkspaceOptions struct { // Variables is the slice of variables to be configured for the no-code // workspace. Variables []*Variable `jsonapi:"relation,vars,omitempty"` + + // SourceName is the name of the source of the workspace. + SourceName *string `jsonapi:"attr,source-name"` + + // SourceUrl is the URL of the source of the workspace. + SourceURL *string `jsonapi:"attr,source-url"` } type RegistryNoCodeModuleWorkspace struct { diff --git a/registry_no_code_module_integration_test.go b/registry_no_code_module_integration_test.go index 9b54ebf7b..847c71bce 100644 --- a/registry_no_code_module_integration_test.go +++ b/registry_no_code_module_integration_test.go @@ -353,11 +353,15 @@ func TestRegistryNoCodeModulesCreateWorkspace(t *testing.T) { t.Run("test creating a workspace via a no-code module", func(t *testing.T) { wn := fmt.Sprintf("foo-%s", randomString(t)) + sn := "my-app" + su := "http://my-app.com" _, err = client.RegistryNoCodeModules.CreateWorkspace( ctx, ncm.ID, &RegistryNoCodeModuleCreateWorkspaceOptions{ - Name: String(wn), + Name: String(wn), + SourceName: String(sn), + SourceURL: String(su), }, ) r.NoError(err) @@ -365,6 +369,8 @@ func TestRegistryNoCodeModulesCreateWorkspace(t *testing.T) { w, err := client.Workspaces.Read(ctx, org.Name, wn) r.NoError(err) r.Equal(wn, w.Name) + r.Equal(sn, w.SourceName) + r.Equal(su, w.SourceURL) }) t.Run("fail to create a workspace with a bad module ID", func(t *testing.T) { From e8f7b3144ee48aabe3a327ed2c59e3972da73403 Mon Sep 17 00:00:00 2001 From: paladin-devops <83741749+paladin-devops@users.noreply.github.com> Date: Tue, 16 Jul 2024 14:24:48 -0400 Subject: [PATCH 27/61] Fix attribute requirements for creating no-code workspaces. Also fix validation of the no-code workspace name. --- registry_no_code_module.go | 11 ++++------- registry_no_code_module_integration_test.go | 4 ++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/registry_no_code_module.go b/registry_no_code_module.go index 8a6b138bd..9cf393ff0 100644 --- a/registry_no_code_module.go +++ b/registry_no_code_module.go @@ -45,7 +45,7 @@ type RegistryNoCodeModuleCreateWorkspaceOptions struct { // Name is the name of the workspace, which can only include letters, // numbers, and _. This will be used as an identifier and must be unique in // the organization. - Name *string `jsonapi:"attr,name,omitempty"` + Name string `jsonapi:"attr,name"` // Description is a description for the workspace. Description *string `jsonapi:"attr,description,omitempty"` @@ -61,10 +61,10 @@ type RegistryNoCodeModuleCreateWorkspaceOptions struct { Variables []*Variable `jsonapi:"relation,vars,omitempty"` // SourceName is the name of the source of the workspace. - SourceName *string `jsonapi:"attr,source-name"` + SourceName *string `jsonapi:"attr,source-name,omitempty"` // SourceUrl is the URL of the source of the workspace. - SourceURL *string `jsonapi:"attr,source-url"` + SourceURL *string `jsonapi:"attr,source-url,omitempty"` } type RegistryNoCodeModuleWorkspace struct { @@ -309,12 +309,9 @@ func (o *RegistryNoCodeModuleReadOptions) valid() error { } func (o *RegistryNoCodeModuleCreateWorkspaceOptions) valid() error { - if !validString(o.Name) { + if !validString(&o.Name) { return ErrRequiredName } - if !validStringID(o.Name) { - return ErrInvalidName - } return nil } diff --git a/registry_no_code_module_integration_test.go b/registry_no_code_module_integration_test.go index 847c71bce..e68fcfb24 100644 --- a/registry_no_code_module_integration_test.go +++ b/registry_no_code_module_integration_test.go @@ -359,7 +359,7 @@ func TestRegistryNoCodeModulesCreateWorkspace(t *testing.T) { ctx, ncm.ID, &RegistryNoCodeModuleCreateWorkspaceOptions{ - Name: String(wn), + Name: wn, SourceName: String(sn), SourceURL: String(su), }, @@ -379,7 +379,7 @@ func TestRegistryNoCodeModulesCreateWorkspace(t *testing.T) { ctx, "codeno-abc123XYZ", &RegistryNoCodeModuleCreateWorkspaceOptions{ - Name: String(wn), + Name: wn, }, ) r.Error(err) From 12402ebf3b883e97b16451bcaf82498022b322fb Mon Sep 17 00:00:00 2001 From: paladin-devops <83741749+paladin-devops@users.noreply.github.com> Date: Tue, 16 Jul 2024 14:57:18 -0400 Subject: [PATCH 28/61] Add no-code module identifier to GH action config. --- .github/actions/test-go-tfe/action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/test-go-tfe/action.yml b/.github/actions/test-go-tfe/action.yml index aed98d71d..c9119fa5e 100644 --- a/.github/actions/test-go-tfe/action.yml +++ b/.github/actions/test-go-tfe/action.yml @@ -95,6 +95,7 @@ runs: TFC_RUN_TASK_URL: "http://testing-mocks.tfe:22180/runtasks/pass" GITHUB_POLICY_SET_IDENTIFIER: "hashicorp/test-policy-set" GITHUB_REGISTRY_MODULE_IDENTIFIER: "hashicorp/terraform-random-module" + GITHUB_REGISTRY_NO_CODE_MODULE_IDENTIFIER: "hashicorp/terraform-random-no-code-module" OAUTH_CLIENT_GITHUB_TOKEN: "${{ inputs.oauth-client-github-token }}" GO111MODULE: "on" ENABLE_TFE: ${{ inputs.enterprise }} From 0654890049123cad68d3eb243b75ff477e4fa79b Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Tue, 16 Jul 2024 15:42:27 -0600 Subject: [PATCH 29/61] Remove references to stack-diagnostics --- stack.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/stack.go b/stack.go index 02c87d4f0..384937037 100644 --- a/stack.go +++ b/stack.go @@ -64,14 +64,6 @@ type StackList struct { Items []*Stack } -type StackDiagnosticsList struct { - *Pagination - Items []*StackDiagnostics -} - -type StackDiagnostics struct { -} - // StackVCSRepo represents the version control system repository for a stack. type StackVCSRepo struct { Identifier string `jsonapi:"attr,identifier"` @@ -95,7 +87,6 @@ type Stack struct { // Relationships Project *Project `jsonapi:"relation,project"` LatestStackConfiguration *StackConfiguration `jsonapi:"relation,latest-stack-configuration"` - StackDiagnostics *StackDiagnostics `jsonapi:"relation,stack-diagnostics"` } type StackConfigurationStatusTimestamps struct { From e69da9f1ce5ff53fc3e0dc583d12439a63c9e33e Mon Sep 17 00:00:00 2001 From: paladin-devops <83741749+paladin-devops@users.noreply.github.com> Date: Thu, 18 Jul 2024 11:38:36 -0400 Subject: [PATCH 30/61] nc: Add method for upgrading no-code workspaces. --- registry_no_code_module.go | 43 +++++++++++++++++++++ registry_no_code_module_integration_test.go | 2 + 2 files changed, 45 insertions(+) diff --git a/registry_no_code_module.go b/registry_no_code_module.go index 9cf393ff0..a39dc8350 100644 --- a/registry_no_code_module.go +++ b/registry_no_code_module.go @@ -36,6 +36,9 @@ type RegistryNoCodeModules interface { // CreateWorkspace creates a workspace using a no-code module. CreateWorkspace(ctx context.Context, noCodeModuleID string, options *RegistryNoCodeModuleCreateWorkspaceOptions) (*RegistryNoCodeModuleWorkspace, error) + + // UpgradeWorkspace initiates an upgrade of an existing no-code module workspace. + UpgradeWorkspace(ctx context.Context, noCodeModuleID string, workspaceID string, options *RegistryNoCodeModuleUpgradeWorkspaceOptions) (*RegistryNoCodeModuleWorkspace, error) } type RegistryNoCodeModuleCreateWorkspaceOptions struct { @@ -67,6 +70,14 @@ type RegistryNoCodeModuleCreateWorkspaceOptions struct { SourceURL *string `jsonapi:"attr,source-url,omitempty"` } +type RegistryNoCodeModuleUpgradeWorkspaceOptions struct { + Type string `jsonapi:"primary,no-code-module-workspace"` + + // Variables is the slice of variables to be configured for the no-code + // workspace. + Variables []*Variable `jsonapi:"relation,vars,omitempty"` +} + type RegistryNoCodeModuleWorkspace struct { Workspace } @@ -284,6 +295,34 @@ func (r *registryNoCodeModules) CreateWorkspace( return w, nil } +func (r *registryNoCodeModules) UpgradeWorkspace( + ctx context.Context, + noCodeModuleID string, + workspaceID string, + options *RegistryNoCodeModuleUpgradeWorkspaceOptions, +) (*RegistryNoCodeModuleWorkspace, error) { + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("no-code-modules/%s/workspaces/%s/upgrade", + url.QueryEscape(noCodeModuleID), + workspaceID, + ) + req, err := r.client.NewRequest("POST", u, options) + if err != nil { + return nil, err + } + + w := &RegistryNoCodeModuleWorkspace{} + err = req.Do(ctx, w) + if err != nil { + return nil, err + } + + return w, nil +} + func (o RegistryNoCodeModuleCreateOptions) valid() error { if o.RegistryModule == nil || o.RegistryModule.ID == "" { return ErrRequiredRegistryModule @@ -315,3 +354,7 @@ func (o *RegistryNoCodeModuleCreateWorkspaceOptions) valid() error { return nil } + +func (o *RegistryNoCodeModuleUpgradeWorkspaceOptions) valid() error { + return nil +} diff --git a/registry_no_code_module_integration_test.go b/registry_no_code_module_integration_test.go index e68fcfb24..0fe721af9 100644 --- a/registry_no_code_module_integration_test.go +++ b/registry_no_code_module_integration_test.go @@ -385,3 +385,5 @@ func TestRegistryNoCodeModulesCreateWorkspace(t *testing.T) { r.Error(err) }) } + +// TODO: Add workspace upgrade test From bad5fdbdefaed78ad74035e1337575827f176827 Mon Sep 17 00:00:00 2001 From: paladin-devops <83741749+paladin-devops@users.noreply.github.com> Date: Thu, 18 Jul 2024 17:20:43 -0400 Subject: [PATCH 31/61] nc: Add integration test for upgrading no-code workspaces. --- registry_no_code_module_integration_test.go | 115 +++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/registry_no_code_module_integration_test.go b/registry_no_code_module_integration_test.go index 0fe721af9..5b2c5f5d1 100644 --- a/registry_no_code_module_integration_test.go +++ b/registry_no_code_module_integration_test.go @@ -386,4 +386,117 @@ func TestRegistryNoCodeModulesCreateWorkspace(t *testing.T) { }) } -// TODO: Add workspace upgrade test +func TestRegistryNoCodeModuleWorkspaceUpgrade(t *testing.T) { + skipUnlessBeta(t) + + client := testClient(t) + ctx := context.Background() + r := require.New(t) + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + org, err := client.Organizations.Read(ctx, orgTest.Name) + r.NoError(err) + r.NotNil(org) + + githubIdentifier := os.Getenv("GITHUB_REGISTRY_NO_CODE_MODULE_IDENTIFIER") + if githubIdentifier == "" { + t.Skip("Export a valid GITHUB_REGISTRY_NO_CODE_MODULE_IDENTIFIER before running this test") + } + + token, cleanupToken := createOAuthToken(t, client, org) + defer cleanupToken() + + rmOpts := RegistryModuleCreateWithVCSConnectionOptions{ + VCSRepo: &RegistryModuleVCSRepoOptions{ + OrganizationName: String(org.Name), + Identifier: String(githubIdentifier), + Tags: Bool(true), + OAuthTokenID: String(token.ID), + DisplayIdentifier: String(githubIdentifier), + }, + InitialVersion: String("1.0.0"), + } + + // create the module + rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, rmOpts) + r.NoError(err) + + // create the no-code module + ncm, err := client.RegistryNoCodeModules.Create(ctx, org.Name, RegistryNoCodeModuleCreateOptions{ + RegistryModule: rm, + Enabled: Bool(true), + VariableOptions: nil, + }) + r.NoError(err) + r.NotNil(ncm) + + // We sleep for 10 seconds to let the module finish getting ready + time.Sleep(time.Second * 10) + + // update the module's pinned version to be 1.0.0 + // NOTE: This is done here as an update instead of at create time, because + // that results in the following error: + // Validation failed: Provided version pin is not equal to latest or provided + // string does not represent an existing version of the module. + uncm, err := client.RegistryNoCodeModules.Update(ctx, ncm.ID, RegistryNoCodeModuleUpdateOptions{ + RegistryModule: rm, + VersionPin: "1.0.0", + }) + r.NoError(err) + r.NotNil(uncm) + + // create a workspace, which will be attempted to be updated during the test + wn := fmt.Sprintf("foo-%s", randomString(t)) + sn := "my-app" + su := "http://my-app.com" + w, err := client.RegistryNoCodeModules.CreateWorkspace( + ctx, + uncm.ID, + &RegistryNoCodeModuleCreateWorkspaceOptions{ + Name: wn, + SourceName: String(sn), + SourceURL: String(su), + }, + ) + r.NoError(err) + r.NotNil(w) + + // update the module's pinned version + uncm, err = client.RegistryNoCodeModules.Update(ctx, ncm.ID, RegistryNoCodeModuleUpdateOptions{ + VersionPin: "1.0.1", + }) + r.NoError(err) + r.NotNil(uncm) + + t.Run("test upgrading a workspace via a no-code module", func(t *testing.T) { + _, err = client.RegistryNoCodeModules.UpgradeWorkspace( + ctx, + ncm.ID, + w.ID, + &RegistryNoCodeModuleUpgradeWorkspaceOptions{}, + ) + r.NoError(err) + }) + + t.Run("fail to upgrade workspace with invalid no-code module", func(t *testing.T) { + _, err = client.RegistryNoCodeModules.UpgradeWorkspace( + ctx, + ncm.ID+"-invalid", + w.ID, + &RegistryNoCodeModuleUpgradeWorkspaceOptions{}, + ) + r.Error(err) + }) + + t.Run("fail to upgrade workspace with invalid workspace ID", func(t *testing.T) { + _, err = client.RegistryNoCodeModules.UpgradeWorkspace( + ctx, + ncm.ID, + w.ID+"-invalid", + &RegistryNoCodeModuleUpgradeWorkspaceOptions{}, + ) + r.Error(err) + }) +} From 4972402585a1ad18bf98717097be4d882fc53735 Mon Sep 17 00:00:00 2001 From: paladin-devops <83741749+paladin-devops@users.noreply.github.com> Date: Thu, 18 Jul 2024 17:37:53 -0400 Subject: [PATCH 32/61] mock: Add no-code upgrade mocks. --- mocks/registry_no_code_module_mocks.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/mocks/registry_no_code_module_mocks.go b/mocks/registry_no_code_module_mocks.go index 4aea68fbb..b63b3bf4e 100644 --- a/mocks/registry_no_code_module_mocks.go +++ b/mocks/registry_no_code_module_mocks.go @@ -113,3 +113,18 @@ func (mr *MockRegistryNoCodeModulesMockRecorder) Update(ctx, noCodeModuleID, opt mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRegistryNoCodeModules)(nil).Update), ctx, noCodeModuleID, options) } + +// UpgradeWorkspace mocks base method. +func (m *MockRegistryNoCodeModules) UpgradeWorkspace(ctx context.Context, noCodeModuleID, workspaceID string, options *tfe.RegistryNoCodeModuleUpgradeWorkspaceOptions) (*tfe.RegistryNoCodeModuleWorkspace, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpgradeWorkspace", ctx, noCodeModuleID, workspaceID, options) + ret0, _ := ret[0].(*tfe.RegistryNoCodeModuleWorkspace) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpgradeWorkspace indicates an expected call of UpgradeWorkspace. +func (mr *MockRegistryNoCodeModulesMockRecorder) UpgradeWorkspace(ctx, noCodeModuleID, workspaceID, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpgradeWorkspace", reflect.TypeOf((*MockRegistryNoCodeModules)(nil).UpgradeWorkspace), ctx, noCodeModuleID, workspaceID, options) +} From 30cc1ac2f5a133d34c9fe7c55a3c9043036c2a01 Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Thu, 18 Jul 2024 16:27:26 -0600 Subject: [PATCH 33/61] adds Stack godocs --- stack.go | 13 ++++++++++++- stack_integration_test.go | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/stack.go b/stack.go index 384937037..0b547a99d 100644 --- a/stack.go +++ b/stack.go @@ -89,6 +89,7 @@ type Stack struct { LatestStackConfiguration *StackConfiguration `jsonapi:"relation,latest-stack-configuration"` } +// StackConfigurationStatusTimestamps represents the timestamps for a stack configuration type StackConfigurationStatusTimestamps struct { QueuedAt *time.Time `jsonapi:"attr,queued-at,omitempty,rfc3339"` CompletedAt *time.Time `jsonapi:"attr,completed-at,omitempty,rfc3339"` @@ -98,12 +99,14 @@ type StackConfigurationStatusTimestamps struct { ErroredAt *time.Time `jsonapi:"attr,errored-at,omitempty,rfc3339"` } +// StackComponent represents a stack component, specified by configuration type StackComponent struct { Name string `json:"name"` Correlator string `json:"correlator"` Expanded bool `json:"expanded"` } +// StackConfiguration represents a stack configuration snapshot type StackConfiguration struct { // Attributes ID string `jsonapi:"primary,stack-configurations"` @@ -117,6 +120,7 @@ type StackConfiguration struct { EventStreamURL string `jsonapi:"attr,event-stream-url"` } +// StackDeployment represents a stack deployment, specified by configuration type StackDeployment struct { // Attributes ID string `jsonapi:"primary,stack-deployments"` @@ -131,6 +135,7 @@ type StackDeployment struct { CurrentStackState *StackState `jsonapi:"relation,current-stack-state"` } +// StackState represents a stack state type StackState struct { // Attributes ID string `jsonapi:"primary,stack-states"` @@ -161,7 +166,8 @@ type StackUpdateOptions struct { VCSRepo *StackVCSRepo `jsonapi:"attr,vcs-repo,omitempty"` } -func (s stacks) UpdateConfiguration(ctx context.Context, stackID string) (*Stack, error) { +// UpdateConfiguration updates the configuration of a stack, triggering stack operations +func (s *stacks) UpdateConfiguration(ctx context.Context, stackID string) (*Stack, error) { req, err := s.client.NewRequest("POST", fmt.Sprintf("stacks/%s/actions/update-configuration", url.PathEscape(stackID)), nil) if err != nil { return nil, err @@ -176,6 +182,7 @@ func (s stacks) UpdateConfiguration(ctx context.Context, stackID string) (*Stack return stack, nil } +// List returns a list of stacks, optionally filtered by additional paameters. func (s stacks) List(ctx context.Context, organization string, options *StackListOptions) (*StackList, error) { if err := options.valid(); err != nil { return nil, err @@ -195,6 +202,7 @@ func (s stacks) List(ctx context.Context, organization string, options *StackLis return sl, nil } +// Read returns a stack by its ID. func (s stacks) Read(ctx context.Context, stackID string) (*Stack, error) { req, err := s.client.NewRequest("GET", fmt.Sprintf("stacks/%s", url.PathEscape(stackID)), nil) if err != nil { @@ -210,6 +218,7 @@ func (s stacks) Read(ctx context.Context, stackID string) (*Stack, error) { return stack, nil } +// Create creates a new stack. func (s stacks) Create(ctx context.Context, options StackCreateOptions) (*Stack, error) { if err := options.valid(); err != nil { return nil, err @@ -229,6 +238,7 @@ func (s stacks) Create(ctx context.Context, options StackCreateOptions) (*Stack, return stack, nil } +// Update updates a stack. func (s stacks) Update(ctx context.Context, stackID string, options StackUpdateOptions) (*Stack, error) { req, err := s.client.NewRequest("PATCH", fmt.Sprintf("stacks/%s", url.PathEscape(stackID)), &options) if err != nil { @@ -244,6 +254,7 @@ func (s stacks) Update(ctx context.Context, stackID string, options StackUpdateO return stack, nil } +// Delete deletes a stack. func (s stacks) Delete(ctx context.Context, stackID string) error { req, err := s.client.NewRequest("POST", fmt.Sprintf("stacks/%s/delete", url.PathEscape(stackID)), nil) if err != nil { diff --git a/stack_integration_test.go b/stack_integration_test.go index a8c832beb..9d97c8a19 100644 --- a/stack_integration_test.go +++ b/stack_integration_test.go @@ -234,7 +234,7 @@ func pollStackDeploymentStatus(t *testing.T, ctx context.Context, client *Client var err error deployment, err := client.StackDeployments.Read(ctx, stackID, deploymentName) if err != nil { - t.Fatalf("Failed to read stack %q: %s", stackID, err) + t.Fatalf("Failed to read stack deployment %s/%s: %s", stackID, deploymentName, err) } t.Logf("Stack deployment %s/%s had status %q", stackID, deploymentName, deployment.Status) From dee5a9cfda19483a7cdf7f6728b70d0e2340168b Mon Sep 17 00:00:00 2001 From: Luces Huayhuaca <21225410+uturunku1@users.noreply.github.com> Date: Tue, 23 Jul 2024 11:12:52 -0700 Subject: [PATCH 34/61] update changelog for release 1.60.0 (#938) --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db2500d2c..b4a956d5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # UNRELEASED +# v1.60.0 + +## Enhancements + * Adds more BETA support for `Stacks` resources, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @brandonc. [#934](https://github.com/hashicorp/go-tfe/pull/934) # v1.59.0 From cc8bfee1c2d6823284e7baf5a0ae762d78ef7a39 Mon Sep 17 00:00:00 2001 From: Barrett Clark Date: Tue, 23 Jul 2024 15:32:11 -0500 Subject: [PATCH 35/61] Updating CODEOWNERS (#882) Update CODEOWNERS to allow other internal employees to approve PRs. TF-CLI still owns approval on general repo things. --- .github/CODEOWNERS | 2 ++ CODEOWNERS | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 .github/CODEOWNERS delete mode 100644 CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..3471db2cb --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +.github/ @hashicorp/tf-cli +Makefile @hashicorp/tf-cli diff --git a/CODEOWNERS b/CODEOWNERS deleted file mode 100644 index 0ff5bf358..000000000 --- a/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @hashicorp/tf-cli From 80ed5a392473335431e860ae4a1e51faeafd2ee1 Mon Sep 17 00:00:00 2001 From: paladin-devops <83741749+paladin-devops@users.noreply.github.com> Date: Fri, 26 Jul 2024 15:12:32 -0400 Subject: [PATCH 36/61] doc: Update changelog. --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4a956d5b..b715d0826 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # UNRELEASED +# v1.61.0 + +## Enhancements + +* Adds support for creating no-code workspaces by @paladin-devops [#927](https://github.com/hashicorp/go-tfe/pull/927) + # v1.60.0 ## Enhancements From 1ab710767cfd5418bf2f758c7948240643ad0ea6 Mon Sep 17 00:00:00 2001 From: paladin-devops <83741749+paladin-devops@users.noreply.github.com> Date: Fri, 26 Jul 2024 15:29:53 -0400 Subject: [PATCH 37/61] doc: Update changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b715d0826..9851be67e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ## Enhancements * Adds support for creating no-code workspaces by @paladin-devops [#927](https://github.com/hashicorp/go-tfe/pull/927) +* Adds support for upgrading no-code workspaces by @paladin-devops [#935](https://github.com/hashicorp/go-tfe/pull/935) # v1.60.0 From 713c23586f29d80b7d611460aa51b77d68338b60 Mon Sep 17 00:00:00 2001 From: paladin-devops <83741749+paladin-devops@users.noreply.github.com> Date: Fri, 26 Jul 2024 15:30:56 -0400 Subject: [PATCH 38/61] doc: Document UpgradeWorkspace method for no-code modules. --- registry_no_code_module.go | 1 + 1 file changed, 1 insertion(+) diff --git a/registry_no_code_module.go b/registry_no_code_module.go index a39dc8350..2e785be4e 100644 --- a/registry_no_code_module.go +++ b/registry_no_code_module.go @@ -295,6 +295,7 @@ func (r *registryNoCodeModules) CreateWorkspace( return w, nil } +// UpgradeWorkspace initiates an upgrade of an existing no-code module workspace. func (r *registryNoCodeModules) UpgradeWorkspace( ctx context.Context, noCodeModuleID string, From 55ed9ac411dcfb20c5e7384ffbfaeaa1ce4e2218 Mon Sep 17 00:00:00 2001 From: paladin-devops <83741749+paladin-devops@users.noreply.github.com> Date: Fri, 26 Jul 2024 15:48:50 -0400 Subject: [PATCH 39/61] changelog: Remove unreleased version. --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b715d0826..6f814fc00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,5 @@ # UNRELEASED -# v1.61.0 - ## Enhancements * Adds support for creating no-code workspaces by @paladin-devops [#927](https://github.com/hashicorp/go-tfe/pull/927) From ea2a5a1755bce7df2ad5eafc81828d5532f78eda Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Thu, 25 Jul 2024 15:38:32 +0200 Subject: [PATCH 40/61] stack: add jsonschema endpoint I decided to not unmarshal this here since moving the type from inside terraform to here seems overkill. --- stack_configuration.go | 26 ++++++++++++++++++++++++++ stack_plan_integration_test.go | 4 ++++ 2 files changed, 30 insertions(+) diff --git a/stack_configuration.go b/stack_configuration.go index 6a6c45e53..f06170c08 100644 --- a/stack_configuration.go +++ b/stack_configuration.go @@ -1,6 +1,7 @@ package tfe import ( + "bytes" "context" "fmt" "net/url" @@ -13,6 +14,9 @@ import ( type StackConfigurations interface { // ReadConfiguration returns a stack configuration by its ID. Read(ctx context.Context, id string) (*StackConfiguration, error) + + // JSONSchemas returns a byte slice of the JSON schema for the stack configuration. + JSONSchemas(ctx context.Context, stackConfigurationID string) ([]byte, error) } type stackConfigurations struct { @@ -35,3 +39,25 @@ func (s stackConfigurations) Read(ctx context.Context, id string) (*StackConfigu return stackConfiguration, nil } + +/** +* Returns the JSON schema for the stack configuration as a byte slice. +* The return value needs to be unmarshalled into a struct to be useful. +* It is meant to be unmarshalled with terraform/internal/command/jsonproivder.Providers. + */ +func (s stackConfigurations) JSONSchemas(ctx context.Context, stackConfigurationID string) ([]byte, error) { + req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-configurations/%s/json-schemas", url.PathEscape(stackConfigurationID)), nil) + if err != nil { + return nil, err + } + + req.Header.Set("Accept", "application/json") + + var raw bytes.Buffer + err = req.Do(ctx, &raw) + if err != nil { + return nil, err + } + + return raw.Bytes(), nil +} diff --git a/stack_plan_integration_test.go b/stack_plan_integration_test.go index 0bfe86332..6ff59088d 100644 --- a/stack_plan_integration_test.go +++ b/stack_plan_integration_test.go @@ -50,4 +50,8 @@ func TestStackPlanList(t *testing.T) { plan, err := client.StackPlans.Read(ctx, planList.Items[0].ID) require.NoError(t, err) require.NotNil(t, plan) + + jsonSchema, err := client.StackConfigurations.JSONSchemas(ctx, stackUpdated.LatestStackConfiguration.ID) + require.NoError(t, err) + require.NotNil(t, jsonSchema) } From 56443018755bdac1601dcfca1b57cd0fa13b3c7f Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Thu, 25 Jul 2024 15:53:16 +0200 Subject: [PATCH 41/61] stack: add plan description endpoint --- stack_plan.go | 80 ++++++++++++++++++++++++++++++++++ stack_plan_integration_test.go | 4 ++ 2 files changed, 84 insertions(+) diff --git a/stack_plan.go b/stack_plan.go index 7c235a55a..96ef8077a 100644 --- a/stack_plan.go +++ b/stack_plan.go @@ -2,6 +2,7 @@ package tfe import ( "context" + "encoding/json" "fmt" "net/url" "time" @@ -25,6 +26,9 @@ type StackPlans interface { // Discard discards a stack plan. Discard(ctx context.Context, stackPlanID string) error + + // Discard returns the plan description for a stack plan. + PlanDescription(ctx context.Context, stackPlanID string) (*JSONChangeDesc, error) } type StackPlansStatusFilter string @@ -92,6 +96,67 @@ type StackPlan struct { Stack *Stack `jsonapi:"relation,stack"` } +// JSONChangeDesc represents a change description of a stack plan / apply operation. +type JSONChangeDesc struct { + FormatVersion uint64 `json:"terraform_stack_change_description"` + Interim bool `json:"interim,omitempty"` + Applyable bool `json:"applyable"` + PlanMode string `json:"plan_mode"` + Components []JSONComponent `json:"components"` + ResourceInstances []JSONResourceInstance `json:"resource_instances"` + DeferredResourceInstances []JSONResourceInstanceDeferral `json:"deferred_resource_instances"` + Outputs map[string]JSONOutput `json:"outputs"` +} + +type JSONComponent struct { + // FIXME: UI seems to want a "name" that is something more compact + // than the full address, but not sure exactly what that ought to + // be once we consider the possibility of embedded stacks and + // components with for_each set. For now we just return the + // full address pending further discussion. + Address string `json:"address"` + ComponentAddress string `json:"component_address"` + InstanceCorrelator string `json:"instance_correlator"` + ComponentCorrelator string `json:"component_correlator"` + Actions []ChangeAction `json:"actions"` + Complete bool `json:"complete"` +} + +type ChangeAction string + +type JSONResourceInstance struct { + ComponentInstanceCorrelator string `json:"component_instance_correlator"` + ComponentInstanceAddress string `json:"component_instance_address"` + Address string `json:"address"` + PreviousComponentInstanceAddress string `json:"previous_component_instance_address,omitempty"` + PreviousAddress string `json:"previous_address,omitempty"` + DeposedKey string `json:"deposed,omitempty"` + ResourceMode string `json:"mode,omitempty"` + ResourceType string `json:"type"` + ProviderAddr string `json:"provider_name"` + Change Change `json:"change"` +} + +type JSONResourceInstanceDeferral struct { + ResourceInstance JSONResourceInstance `json:"resource_instance"` + Deferred JSONDeferred `json:"deferred"` +} + +type JSONDeferred struct { + Reason string `json:"reason"` +} + +type JSONOutput struct { + Change json.RawMessage `json:"change"` +} + +type Change struct { + Actions []ChangeAction `json:"actions"` + After json.RawMessage `json:"after"` + Before json.RawMessage `json:"before"` + // TODO: Add after_sensitive, after_unknown, before_sensitive +} + func (s stackPlans) Read(ctx context.Context, stackPlanID string) (*StackPlan, error) { req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-plans/%s", url.PathEscape(stackPlanID)), nil) if err != nil { @@ -148,3 +213,18 @@ func (s stackPlans) Cancel(ctx context.Context, stackPlanID string) error { return req.Do(ctx, nil) } + +func (s stackPlans) PlanDescription(ctx context.Context, stackPlanID string) (*JSONChangeDesc, error) { + req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-plans/%s/plan-description", url.PathEscape(stackPlanID)), nil) + if err != nil { + return nil, err + } + + jd := &JSONChangeDesc{} + err = req.Do(ctx, jd) + if err != nil { + return nil, err + } + + return jd, nil +} diff --git a/stack_plan_integration_test.go b/stack_plan_integration_test.go index 6ff59088d..99a1241c4 100644 --- a/stack_plan_integration_test.go +++ b/stack_plan_integration_test.go @@ -54,4 +54,8 @@ func TestStackPlanList(t *testing.T) { jsonSchema, err := client.StackConfigurations.JSONSchemas(ctx, stackUpdated.LatestStackConfiguration.ID) require.NoError(t, err) require.NotNil(t, jsonSchema) + + planDesc, err := client.StackPlans.PlanDescription(ctx, planList.Items[0].ID) + require.NoError(t, err) + require.NotNil(t, planDesc) } From efc035626fc6761fcff66b5b8d246557cd51fa1c Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Thu, 25 Jul 2024 16:03:41 +0200 Subject: [PATCH 42/61] stacks: add stack plan operation --- stack_plan_integration_test.go | 4 ++ stack_plan_operation.go | 88 ++++++++++++++++++++++++++++++++++ tfe.go | 2 + 3 files changed, 94 insertions(+) create mode 100644 stack_plan_operation.go diff --git a/stack_plan_integration_test.go b/stack_plan_integration_test.go index 99a1241c4..f773c186b 100644 --- a/stack_plan_integration_test.go +++ b/stack_plan_integration_test.go @@ -58,4 +58,8 @@ func TestStackPlanList(t *testing.T) { planDesc, err := client.StackPlans.PlanDescription(ctx, planList.Items[0].ID) require.NoError(t, err) require.NotNil(t, planDesc) + + spo, err := client.StackPlanOperations.Read(ctx, stackUpdated.LatestStackConfiguration.ID) + require.NoError(t, err) + require.NotNil(t, spo) } diff --git a/stack_plan_operation.go b/stack_plan_operation.go new file mode 100644 index 000000000..cc77bba66 --- /dev/null +++ b/stack_plan_operation.go @@ -0,0 +1,88 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfe + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" +) + +// NOTE WELL: This is a beta feature and is subject to change until noted otherwise in the +// release notes. +type StackPlanOperations interface { + // Read returns a stack plan operation by its ID. + Read(ctx context.Context, stackPlanOperationID string) (*StackPlanOperation, error) + + // Get Stack Plans from Configuration Version + DownloadEventStream(ctx context.Context, stackPlanOperationID string) ([]byte, error) +} + +type stackPlanOperations struct { + client *Client +} + +var _ StackPlanOperations = &stackPlanOperations{} + +type StackPlanOperation struct { + ID string `jsonapi:"primary,stack-plan-operations"` + Type string `jsonapi:"attr,operation-type"` + Status string `jsonapi:"attr,status"` + EventStreamURL string `jsonapi:"attr,event-stream-url"` + Diagnostics string `jsonapi:"attr,diags"` + + // Relations + StackPlan *StackPlan `jsonapi:"relation,stack-plan"` +} + +func (s stackPlanOperations) Read(ctx context.Context, stackPlanOperationID string) (*StackPlanOperation, error) { + req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-plans-operations/%s", url.PathEscape(stackPlanOperationID)), nil) + if err != nil { + return nil, err + } + + ucs := &StackPlanOperation{} + err = req.Do(ctx, ucs) + if err != nil { + return nil, err + } + + return ucs, nil +} + +func (s stackPlanOperations) DownloadEventStream(ctx context.Context, eventStreamURL string) ([]byte, error) { + // Create a new request. + req, err := http.NewRequest("GET", eventStreamURL, nil) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + + // Attach the default headers. + for k, v := range s.client.headers { + req.Header[k] = v + } + + // Retrieve the next chunk. + resp, err := s.client.http.HTTPClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Basic response checking. + if err := checkResponseCode(resp); err != nil { + return nil, err + } + + // Read the retrieved chunk. + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return b, nil +} diff --git a/tfe.go b/tfe.go index b42ef4149..5109156ea 100644 --- a/tfe.go +++ b/tfe.go @@ -165,6 +165,7 @@ type Client struct { StackConfigurations StackConfigurations StackDeployments StackDeployments StackPlans StackPlans + StackPlanOperations StackPlanOperations StateVersionOutputs StateVersionOutputs StateVersions StateVersions TaskResults TaskResults @@ -470,6 +471,7 @@ func NewClient(cfg *Config) (*Client, error) { client.StackConfigurations = &stackConfigurations{client: client} client.StackDeployments = &stackDeployments{client: client} client.StackPlans = &stackPlans{client: client} + client.StackPlanOperations = &stackPlanOperations{client: client} client.StateVersionOutputs = &stateVersionOutputs{client: client} client.StateVersions = &stateVersions{client: client} client.TaskResults = &taskResults{client: client} From 06ace7688e15ccfaae4e91a55fafc10c4babc611 Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Mon, 29 Jul 2024 15:00:31 +0200 Subject: [PATCH 43/61] fix typo Co-authored-by: Luces Huayhuaca <21225410+uturunku1@users.noreply.github.com> --- stack_plan.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack_plan.go b/stack_plan.go index 96ef8077a..8a528ecc2 100644 --- a/stack_plan.go +++ b/stack_plan.go @@ -27,7 +27,7 @@ type StackPlans interface { // Discard discards a stack plan. Discard(ctx context.Context, stackPlanID string) error - // Discard returns the plan description for a stack plan. + // PlanDescription returns the plan description for a stack plan. PlanDescription(ctx context.Context, stackPlanID string) (*JSONChangeDesc, error) } From 4a486e23e97485b40d91ab0e00bdcbf649d240e2 Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Mon, 29 Jul 2024 16:13:35 +0200 Subject: [PATCH 44/61] add comments --- stack_plan.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/stack_plan.go b/stack_plan.go index 8a528ecc2..113191f90 100644 --- a/stack_plan.go +++ b/stack_plan.go @@ -65,6 +65,7 @@ type stackPlans struct { var _ StackPlans = &stackPlans{} +// StackPlanStatusTimestamps are the timestamps of the status changes for a stack type StackPlanStatusTimestamps struct { CreatedAt time.Time `jsonapi:"attr,created-at,rfc3339"` RunningAt time.Time `jsonapi:"attr,running-at,rfc3339"` @@ -72,6 +73,7 @@ type StackPlanStatusTimestamps struct { FinishedAt time.Time `jsonapi:"attr,finished-at,rfc3339"` } +// PlanChanges is the summary of the planned changes type PlanChanges struct { Add int `jsonapi:"attr,add"` Total int `jsonapi:"attr,total"` @@ -108,12 +110,8 @@ type JSONChangeDesc struct { Outputs map[string]JSONOutput `json:"outputs"` } +// JSONComponent represents a change description of a single component in a plan. type JSONComponent struct { - // FIXME: UI seems to want a "name" that is something more compact - // than the full address, but not sure exactly what that ought to - // be once we consider the possibility of embedded stacks and - // components with for_each set. For now we just return the - // full address pending further discussion. Address string `json:"address"` ComponentAddress string `json:"component_address"` InstanceCorrelator string `json:"instance_correlator"` @@ -122,8 +120,10 @@ type JSONComponent struct { Complete bool `json:"complete"` } +// ChangeAction are the actions a change can have: no-op, create, read, update, delte, forget. type ChangeAction string +// JSONResourceInstance is the change description of a single resource instance in a plan. type JSONResourceInstance struct { ComponentInstanceCorrelator string `json:"component_instance_correlator"` ComponentInstanceAddress string `json:"component_instance_address"` @@ -137,24 +137,27 @@ type JSONResourceInstance struct { Change Change `json:"change"` } +// JSONResourceInstanceDeferral is the change description of a single resource instance that is deferred. type JSONResourceInstanceDeferral struct { ResourceInstance JSONResourceInstance `json:"resource_instance"` Deferred JSONDeferred `json:"deferred"` } +// JSONDeferred contains the reason why a resource instance is deferred: instance_count_unknown, resource_config_unknown, provider_config_unknown, provider_config_unknown, or deferred_prereq. type JSONDeferred struct { Reason string `json:"reason"` } +// JSONOutput is the value of a single output in a plan. type JSONOutput struct { Change json.RawMessage `json:"change"` } +// Change represents the change of a resource instance in a plan. type Change struct { Actions []ChangeAction `json:"actions"` After json.RawMessage `json:"after"` Before json.RawMessage `json:"before"` - // TODO: Add after_sensitive, after_unknown, before_sensitive } func (s stackPlans) Read(ctx context.Context, stackPlanID string) (*StackPlan, error) { From d76f3bd1158fb0a1085047376a9f02dd2ac1e532 Mon Sep 17 00:00:00 2001 From: "hashicorp-copywrite[bot]" <110428419+hashicorp-copywrite[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:11:38 +0000 Subject: [PATCH 45/61] [COMPLIANCE] Add Copyright and License Headers --- stack_configuration.go | 3 +++ stack_deployments.go | 3 +++ stack_plan.go | 3 +++ stack_plan_integration_test.go | 3 +++ 4 files changed, 12 insertions(+) diff --git a/stack_configuration.go b/stack_configuration.go index 6a6c45e53..3a7e73da6 100644 --- a/stack_configuration.go +++ b/stack_configuration.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package tfe import ( diff --git a/stack_deployments.go b/stack_deployments.go index dd982b2a2..eb631cf02 100644 --- a/stack_deployments.go +++ b/stack_deployments.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package tfe import ( diff --git a/stack_plan.go b/stack_plan.go index 7c235a55a..365f90979 100644 --- a/stack_plan.go +++ b/stack_plan.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package tfe import ( diff --git a/stack_plan_integration_test.go b/stack_plan_integration_test.go index 0bfe86332..9b9fa0c57 100644 --- a/stack_plan_integration_test.go +++ b/stack_plan_integration_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package tfe import ( From 9c6dccf1643b2748e8cad65614ff39f25867622c Mon Sep 17 00:00:00 2001 From: paladin-devops <83741749+paladin-devops@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:37:18 -0400 Subject: [PATCH 46/61] [WAYP-2855] no-code: Enable execution mode option for creating no-code workspaces. --- registry_no_code_module.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/registry_no_code_module.go b/registry_no_code_module.go index 2e785be4e..7fe7ea7f1 100644 --- a/registry_no_code_module.go +++ b/registry_no_code_module.go @@ -43,7 +43,6 @@ type RegistryNoCodeModules interface { type RegistryNoCodeModuleCreateWorkspaceOptions struct { Type string `jsonapi:"primary,no-code-module-workspace"` - // Add more create options here // Name is the name of the workspace, which can only include letters, // numbers, and _. This will be used as an identifier and must be unique in @@ -68,6 +67,9 @@ type RegistryNoCodeModuleCreateWorkspaceOptions struct { // SourceUrl is the URL of the source of the workspace. SourceURL *string `jsonapi:"attr,source-url,omitempty"` + + // ExecutionMode is the execution mode of the workspace. + ExecutionMode *string `jsonapi:"attr,execution-mode,omitempty"` } type RegistryNoCodeModuleUpgradeWorkspaceOptions struct { From 46a166b85f490a480cc958b81fa1bc37a4d015e0 Mon Sep 17 00:00:00 2001 From: paladin-devops <83741749+paladin-devops@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:41:11 -0400 Subject: [PATCH 47/61] [WAYP-2855] no-code: Test execution mode on workspace creation. --- registry_no_code_module_integration_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/registry_no_code_module_integration_test.go b/registry_no_code_module_integration_test.go index 5b2c5f5d1..0a68d994a 100644 --- a/registry_no_code_module_integration_test.go +++ b/registry_no_code_module_integration_test.go @@ -359,9 +359,10 @@ func TestRegistryNoCodeModulesCreateWorkspace(t *testing.T) { ctx, ncm.ID, &RegistryNoCodeModuleCreateWorkspaceOptions{ - Name: wn, - SourceName: String(sn), - SourceURL: String(su), + Name: wn, + SourceName: String(sn), + SourceURL: String(su), + ExecutionMode: String("remote"), }, ) r.NoError(err) @@ -371,6 +372,7 @@ func TestRegistryNoCodeModulesCreateWorkspace(t *testing.T) { r.Equal(wn, w.Name) r.Equal(sn, w.SourceName) r.Equal(su, w.SourceURL) + r.Equal("remote", w.ExecutionMode) }) t.Run("fail to create a workspace with a bad module ID", func(t *testing.T) { From 1e2265213095640c9e72358070fc7945347a3d13 Mon Sep 17 00:00:00 2001 From: paladin-devops <83741749+paladin-devops@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:53:58 -0400 Subject: [PATCH 48/61] [WAYP-2855] no-code: Add agent pool ID param. --- registry_no_code_module.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/registry_no_code_module.go b/registry_no_code_module.go index 7fe7ea7f1..0d29d756a 100644 --- a/registry_no_code_module.go +++ b/registry_no_code_module.go @@ -70,6 +70,11 @@ type RegistryNoCodeModuleCreateWorkspaceOptions struct { // ExecutionMode is the execution mode of the workspace. ExecutionMode *string `jsonapi:"attr,execution-mode,omitempty"` + + // AgentPoolId is the ID of the agent pool to use for the workspace. + // This is required when execution mode is set to "agent". + // This must not be specified when execution mode is set to "remote". + AgentPoolID *string `jsonapi:"attr,agent-pool-id,omitempty"` } type RegistryNoCodeModuleUpgradeWorkspaceOptions struct { From 2cab9d956413b2ad6ac30615de6c84984e41d1c6 Mon Sep 17 00:00:00 2001 From: Julianna Tetreault Date: Tue, 25 Jun 2024 13:45:27 -0500 Subject: [PATCH 49/61] Add AllowMemberTokenManagement to Team --- CHANGELOG.md | 1 + team.go | 7 +++++++ team_integration_test.go | 14 +++++++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55984a6f3..3a364f921 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ # UNRELEASED +* Adds `AllowMemberTokenManagement` permission to `Team` by @juliannatetreault []() ## Enhancements diff --git a/team.go b/team.go index a432b657b..1f8fab37d 100644 --- a/team.go +++ b/team.go @@ -54,6 +54,7 @@ type Team struct { Permissions *TeamPermissions `jsonapi:"attr,permissions"` UserCount int `jsonapi:"attr,users-count"` SSOTeamID string `jsonapi:"attr,sso-team-id"` + AllowMemberTokenManagement bool `jsonapi:"attr,allow-member-token-management"` // Relations Users []*User `jsonapi:"relation,users"` @@ -127,6 +128,9 @@ type TeamCreateOptions struct { // The team's visibility ("secret", "organization") Visibility *string `jsonapi:"attr,visibility,omitempty"` + + // Used by Owners and users with "Manage Teams" permissions to control team tokens + AllowMemberTokenManagement *bool `jsonapi:"attr,allow-member-token-management,omitempty"` } // TeamUpdateOptions represents the options for updating a team. @@ -148,6 +152,9 @@ type TeamUpdateOptions struct { // Optional: The team's visibility ("secret", "organization") Visibility *string `jsonapi:"attr,visibility,omitempty"` + + // Optional: Used by Owners and users with "Manage Teams" permissions to control team tokens + AllowMemberTokenManagement *bool `jsonapi:"attr,allow-member-token-management,omitempty"` } // OrganizationAccessOptions represents the organization access options of a team. diff --git a/team_integration_test.go b/team_integration_test.go index 0c0d3652a..e0cb88e51 100644 --- a/team_integration_test.go +++ b/team_integration_test.go @@ -159,6 +159,7 @@ func TestTeamsRead(t *testing.T) { OrganizationAccess: &OrganizationAccessOptions{ ManagePolicies: Bool(true), }, + AllowMemberTokenManagement: Bool(true), } ssoTeam, err := client.Teams.Create(ctx, orgTest.Name, opts) require.NoError(t, err) @@ -188,6 +189,10 @@ func TestTeamsRead(t *testing.T) { assert.NotNil(t, ssoTeam.SSOTeamID) assert.Equal(t, *opts.SSOTeamID, ssoTeam.SSOTeamID) }) + + t.Run("allow member token management is returned", func(t *testing.T) { + assert.Equal(t, *opts.AllowMemberTokenManagement, tm.AllowMemberTokenManagement) + }) }) t.Run("when the team does not exist", func(t *testing.T) { @@ -224,6 +229,7 @@ func TestTeamsUpdate(t *testing.T) { ManageModules: Bool(false), }, Visibility: String("organization"), + AllowMemberTokenManagement: Bool(true), } tm, err := client.Teams.Update(ctx, tmTest.ID, options) @@ -241,6 +247,10 @@ func TestTeamsUpdate(t *testing.T) { *options.Visibility, item.Visibility, ) + assert.Equal(t, + *options.AllowMemberTokenManagement, + item.AllowMemberTokenManagement, + ) assert.Equal(t, *options.OrganizationAccess.ManagePolicies, item.OrganizationAccess.ManagePolicies, @@ -344,12 +354,14 @@ func TestTeam_Unmarshal(t *testing.T) { assert.Equal(t, team.OrganizationAccess.ReadProjects, true) assert.Equal(t, team.Permissions.CanDestroy, true) assert.Equal(t, team.Permissions.CanUpdateMembership, true) + assert.Equal(t, team.AllowMemberTokenManagement, true) } func TestTeamCreateOptions_Marshal(t *testing.T) { opts := TeamCreateOptions{ Name: String("team name"), Visibility: String("organization"), + AllowMemberTokenManagement: Bool(true), OrganizationAccess: &OrganizationAccessOptions{ ManagePolicies: Bool(true), }, @@ -362,7 +374,7 @@ func TestTeamCreateOptions_Marshal(t *testing.T) { bodyBytes, err := req.BodyBytes() require.NoError(t, err) - expectedBody := `{"data":{"type":"teams","attributes":{"name":"team name","organization-access":{"manage-policies":true},"visibility":"organization"}}} + expectedBody := `{"data":{"type":"teams","attributes":{"name":"team name","organization-access":{"manage-policies":true},"visibility":"organization","allow-member-token-management":true}}} ` assert.Equal(t, expectedBody, string(bodyBytes)) } From 29f81ea304182db33eead80c0d8206adbbccceda Mon Sep 17 00:00:00 2001 From: Julianna Tetreault Date: Thu, 27 Jun 2024 14:46:07 -0500 Subject: [PATCH 50/61] Update PR link and in-line code comments --- CHANGELOG.md | 2 +- team.go | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a364f921..7e1f68dda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ # UNRELEASED -* Adds `AllowMemberTokenManagement` permission to `Team` by @juliannatetreault []() +* Adds `AllowMemberTokenManagement` permission to `Team` by @juliannatetreault [#922](https://github.com/hashicorp/go-tfe/pull/922) ## Enhancements diff --git a/team.go b/team.go index 1f8fab37d..3586fc908 100644 --- a/team.go +++ b/team.go @@ -54,7 +54,8 @@ type Team struct { Permissions *TeamPermissions `jsonapi:"attr,permissions"` UserCount int `jsonapi:"attr,users-count"` SSOTeamID string `jsonapi:"attr,sso-team-id"` - AllowMemberTokenManagement bool `jsonapi:"attr,allow-member-token-management"` + // AllowMemberTokenManagement is false for TFE versions older than v202408 + AllowMemberTokenManagement bool `jsonapi:"attr,allow-member-token-management"` // Relations Users []*User `jsonapi:"relation,users"` @@ -129,8 +130,8 @@ type TeamCreateOptions struct { // The team's visibility ("secret", "organization") Visibility *string `jsonapi:"attr,visibility,omitempty"` - // Used by Owners and users with "Manage Teams" permissions to control team tokens - AllowMemberTokenManagement *bool `jsonapi:"attr,allow-member-token-management,omitempty"` + // Optional: Used by Owners and users with "Manage Teams" permissions to control whether team members can manage team tokens + AllowMemberTokenManagement *bool `jsonapi:"attr,allow-member-token-management,omitempty"` } // TeamUpdateOptions represents the options for updating a team. @@ -153,8 +154,8 @@ type TeamUpdateOptions struct { // Optional: The team's visibility ("secret", "organization") Visibility *string `jsonapi:"attr,visibility,omitempty"` - // Optional: Used by Owners and users with "Manage Teams" permissions to control team tokens - AllowMemberTokenManagement *bool `jsonapi:"attr,allow-member-token-management,omitempty"` + // Optional: Used by Owners and users with "Manage Teams" permissions to control whether team members can manage team tokens + AllowMemberTokenManagement *bool `jsonapi:"attr,allow-member-token-management,omitempty"` } // OrganizationAccessOptions represents the organization access options of a team. From 0f6078fcb06eb4b12322e5de2683b266c1c5b6c4 Mon Sep 17 00:00:00 2001 From: Mark DeCrane Date: Tue, 30 Jul 2024 14:17:41 -0400 Subject: [PATCH 51/61] Update CHANGElOG for 1.61.0 release --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55984a6f3..9851be67e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # UNRELEASED +# v1.61.0 + ## Enhancements * Adds support for creating no-code workspaces by @paladin-devops [#927](https://github.com/hashicorp/go-tfe/pull/927) From 037fcab10dff7f330c793cb820a6955ce5353a96 Mon Sep 17 00:00:00 2001 From: Julianna Tetreault Date: Tue, 30 Jul 2024 14:44:41 -0500 Subject: [PATCH 52/61] Fix lint issues --- team_integration_test.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/team_integration_test.go b/team_integration_test.go index e0cb88e51..1ac4560b3 100644 --- a/team_integration_test.go +++ b/team_integration_test.go @@ -190,9 +190,9 @@ func TestTeamsRead(t *testing.T) { assert.Equal(t, *opts.SSOTeamID, ssoTeam.SSOTeamID) }) - t.Run("allow member token management is returned", func(t *testing.T) { - assert.Equal(t, *opts.AllowMemberTokenManagement, tm.AllowMemberTokenManagement) - }) + t.Run("allow member token management is returned", func(t *testing.T) { + assert.Equal(t, *opts.AllowMemberTokenManagement, tm.AllowMemberTokenManagement) + }) }) t.Run("when the team does not exist", func(t *testing.T) { @@ -228,7 +228,7 @@ func TestTeamsUpdate(t *testing.T) { ManageProviders: Bool(true), ManageModules: Bool(false), }, - Visibility: String("organization"), + Visibility: String("organization"), AllowMemberTokenManagement: Bool(true), } @@ -247,10 +247,10 @@ func TestTeamsUpdate(t *testing.T) { *options.Visibility, item.Visibility, ) - assert.Equal(t, - *options.AllowMemberTokenManagement, - item.AllowMemberTokenManagement, - ) + assert.Equal(t, + *options.AllowMemberTokenManagement, + item.AllowMemberTokenManagement, + ) assert.Equal(t, *options.OrganizationAccess.ManagePolicies, item.OrganizationAccess.ManagePolicies, @@ -359,8 +359,8 @@ func TestTeam_Unmarshal(t *testing.T) { func TestTeamCreateOptions_Marshal(t *testing.T) { opts := TeamCreateOptions{ - Name: String("team name"), - Visibility: String("organization"), + Name: String("team name"), + Visibility: String("organization"), AllowMemberTokenManagement: Bool(true), OrganizationAccess: &OrganizationAccessOptions{ ManagePolicies: Bool(true), From 9c996d76f07c135bfe2be5ffdbe54f6e2b0fefdd Mon Sep 17 00:00:00 2001 From: Julianna Tetreault Date: Tue, 30 Jul 2024 16:05:00 -0500 Subject: [PATCH 53/61] Fix team_integration_text.go failures --- team_integration_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/team_integration_test.go b/team_integration_test.go index 1ac4560b3..91d530cae 100644 --- a/team_integration_test.go +++ b/team_integration_test.go @@ -354,7 +354,6 @@ func TestTeam_Unmarshal(t *testing.T) { assert.Equal(t, team.OrganizationAccess.ReadProjects, true) assert.Equal(t, team.Permissions.CanDestroy, true) assert.Equal(t, team.Permissions.CanUpdateMembership, true) - assert.Equal(t, team.AllowMemberTokenManagement, true) } func TestTeamCreateOptions_Marshal(t *testing.T) { @@ -374,7 +373,7 @@ func TestTeamCreateOptions_Marshal(t *testing.T) { bodyBytes, err := req.BodyBytes() require.NoError(t, err) - expectedBody := `{"data":{"type":"teams","attributes":{"name":"team name","organization-access":{"manage-policies":true},"visibility":"organization","allow-member-token-management":true}}} + expectedBody := `{"data":{"type":"teams","attributes":{"allow-member-token-management":true,"name":"team name","organization-access":{"manage-policies":true},"visibility":"organization"}}} ` assert.Equal(t, expectedBody, string(bodyBytes)) } From ed2be5be8ba1587e6162cd0374c5397218401008 Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Thu, 1 Aug 2024 13:11:09 +0200 Subject: [PATCH 54/61] stacks: add stack plan operations to stack plan --- stack_plan.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/stack_plan.go b/stack_plan.go index 88d7456b9..5ea2385e0 100644 --- a/stack_plan.go +++ b/stack_plan.go @@ -97,8 +97,9 @@ type StackPlan struct { Deployment string `jsonapi:"attr,deployment"` // Relationships - StackConfiguration *StackConfiguration `jsonapi:"relation,stack-configuration"` - Stack *Stack `jsonapi:"relation,stack"` + StackConfiguration *StackConfiguration `jsonapi:"relation,stack-configuration"` + Stack *Stack `jsonapi:"relation,stack"` + StackPlanOperations []*StackPlanOperation `jsonapi:"relation,stack-plan-operations"` } // JSONChangeDesc represents a change description of a stack plan / apply operation. From 98d7917125b0ed5a736bebba028298d37abfb112 Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Thu, 1 Aug 2024 13:32:48 +0200 Subject: [PATCH 55/61] stacks: add include options --- stack_plan.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/stack_plan.go b/stack_plan.go index 5ea2385e0..54183acae 100644 --- a/stack_plan.go +++ b/stack_plan.go @@ -46,6 +46,12 @@ const ( StackPlansStatusFilterCanceled StackPlansStatusFilter = "canceled" ) +type StackPlansIncludeOpt string + +const ( + StackPlansIncludeOperations StackPlansIncludeOpt = "stack_plan_operations" +) + type StackPlansListOptions struct { ListOptions @@ -54,6 +60,8 @@ type StackPlansListOptions struct { // Optional: A query string to filter plans by deployment. Deployment string `url:"filter[deployment],omitempty"` + + Include []StackPlansIncludeOpt `url:"include,omitempty"` } type StackPlanList struct { From 736c201d4fc9388655cf2179fe936adaee63ffcd Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Thu, 1 Aug 2024 16:50:56 +0200 Subject: [PATCH 56/61] stacks: use DoJSON for json response --- stack_plan.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack_plan.go b/stack_plan.go index 54183acae..2fd50b536 100644 --- a/stack_plan.go +++ b/stack_plan.go @@ -236,7 +236,7 @@ func (s stackPlans) PlanDescription(ctx context.Context, stackPlanID string) (*J } jd := &JSONChangeDesc{} - err = req.Do(ctx, jd) + err = req.DoJSON(ctx, jd) if err != nil { return nil, err } From 3d40746ead346ec48feb609f9e4b2f318080fcb3 Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Thu, 1 Aug 2024 10:07:22 -0600 Subject: [PATCH 57/61] Use json encoding for vcs-repo request params --- stack.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stack.go b/stack.go index 0b547a99d..402fa306b 100644 --- a/stack.go +++ b/stack.go @@ -66,10 +66,10 @@ type StackList struct { // StackVCSRepo represents the version control system repository for a stack. type StackVCSRepo struct { - Identifier string `jsonapi:"attr,identifier"` - Branch string `jsonapi:"attr,branch"` - GHAInstallationID string `jsonapi:"attr,github-app-installation-id"` - OAuthTokenID string `jsonapi:"attr,oauth-token-id"` + Identifier string `json:"identifier"` + Branch string `json:"branch,omitempty"` + GHAInstallationID string `json:"github-app-installation-id,omitempty"` + OAuthTokenID string `json:"oauth-token-id,omitempty"` } // Stack represents a stack. From 3b6d3a7b1af513b7a4ac17a1d462d99a886f63d3 Mon Sep 17 00:00:00 2001 From: CJ Horton Date: Thu, 1 Aug 2024 18:04:04 -0700 Subject: [PATCH 58/61] rename TBW-based timeout attributes --- admin_organization.go | 4 ++++ admin_organization_integration_test.go | 16 ++++++++++------ admin_setting_general.go | 16 ++++++++++------ admin_setting_general_integration_test.go | 14 ++++++++++++++ 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/admin_organization.go b/admin_organization.go index ccaeffd27..afcdf1d47 100644 --- a/admin_organization.go +++ b/admin_organization.go @@ -53,6 +53,8 @@ type AdminOrganization struct { SsoEnabled bool `jsonapi:"attr,sso-enabled"` TerraformBuildWorkerApplyTimeout string `jsonapi:"attr,terraform-build-worker-apply-timeout"` TerraformBuildWorkerPlanTimeout string `jsonapi:"attr,terraform-build-worker-plan-timeout"` + ApplyTimeout string `jsonapi:"attr,apply-timeout"` + PlanTimeout string `jsonapi:"attr,plan-timeout"` TerraformWorkerSudoEnabled bool `jsonapi:"attr,terraform-worker-sudo-enabled"` WorkspaceLimit *int `jsonapi:"attr,workspace-limit"` @@ -69,6 +71,8 @@ type AdminOrganizationUpdateOptions struct { IsDisabled *bool `jsonapi:"attr,is-disabled,omitempty"` TerraformBuildWorkerApplyTimeout *string `jsonapi:"attr,terraform-build-worker-apply-timeout,omitempty"` TerraformBuildWorkerPlanTimeout *string `jsonapi:"attr,terraform-build-worker-plan-timeout,omitempty"` + ApplyTimeout *string `jsonapi:"attr,apply-timeout,omitempty"` + PlanTimeout *string `jsonapi:"attr,plan-timeout,omitempty"` TerraformWorkerSudoEnabled bool `jsonapi:"attr,terraform-worker-sudo-enabled,omitempty"` WorkspaceLimit *int `jsonapi:"attr,workspace-limit,omitempty"` } diff --git a/admin_organization_integration_test.go b/admin_organization_integration_test.go index 15493e5f5..40a194111 100644 --- a/admin_organization_integration_test.go +++ b/admin_organization_integration_test.go @@ -222,8 +222,8 @@ func TestAdminOrganizations_Update(t *testing.T) { globalModuleSharing := false globalProviderSharing := false isDisabled := false - terraformBuildWorkerApplyTimeout := "24h" - terraformBuildWorkerPlanTimeout := "24h" + applyTimeout := "24h" + planTimeout := "24h" terraformWorkerSudoEnabled := true opts := AdminOrganizationUpdateOptions{ @@ -231,8 +231,10 @@ func TestAdminOrganizations_Update(t *testing.T) { GlobalModuleSharing: &globalModuleSharing, GlobalProviderSharing: &globalProviderSharing, IsDisabled: &isDisabled, - TerraformBuildWorkerApplyTimeout: &terraformBuildWorkerApplyTimeout, - TerraformBuildWorkerPlanTimeout: &terraformBuildWorkerPlanTimeout, + TerraformBuildWorkerApplyTimeout: &applyTimeout, + TerraformBuildWorkerPlanTimeout: &planTimeout, + ApplyTimeout: &applyTimeout, + PlanTimeout: &planTimeout, TerraformWorkerSudoEnabled: terraformWorkerSudoEnabled, } @@ -244,8 +246,10 @@ func TestAdminOrganizations_Update(t *testing.T) { assert.Equal(t, adminOrg.GlobalModuleSharing, &globalModuleSharing) assert.Equal(t, adminOrg.GlobalProviderSharing, &globalProviderSharing) assert.Equal(t, isDisabled, adminOrg.IsDisabled) - assert.Equal(t, terraformBuildWorkerApplyTimeout, adminOrg.TerraformBuildWorkerApplyTimeout) - assert.Equal(t, terraformBuildWorkerPlanTimeout, adminOrg.TerraformBuildWorkerPlanTimeout) + assert.Equal(t, applyTimeout, adminOrg.TerraformBuildWorkerApplyTimeout) + assert.Equal(t, planTimeout, adminOrg.TerraformBuildWorkerPlanTimeout) + assert.Equal(t, applyTimeout, adminOrg.ApplyTimeout) + assert.Equal(t, planTimeout, adminOrg.PlanTimeout) assert.Equal(t, terraformWorkerSudoEnabled, adminOrg.TerraformWorkerSudoEnabled) assert.Nil(t, adminOrg.WorkspaceLimit, "default workspace limit should be nil") diff --git a/admin_setting_general.go b/admin_setting_general.go index 3588ec417..9af4620f9 100644 --- a/admin_setting_general.go +++ b/admin_setting_general.go @@ -40,6 +40,8 @@ type AdminGeneralSetting struct { DefaultWorkspacesPerOrgCeiling int `jsonapi:"attr,default-workspaces-per-organization-ceiling"` TerraformBuildWorkerApplyTimeout string `jsonapi:"attr,terraform-build-worker-apply-timeout"` TerraformBuildWorkerPlanTimeout string `jsonapi:"attr,terraform-build-worker-plan-timeout"` + ApplyTimeout string `jsonapi:"attr,apply-timeout"` + PlanTimeout string `jsonapi:"attr,plan-timeout"` DefaultRemoteStateAccess bool `jsonapi:"attr,default-remote-state-access"` } @@ -47,12 +49,14 @@ type AdminGeneralSetting struct { // general settings. // https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/settings#request-body type AdminGeneralSettingsUpdateOptions struct { - LimitUserOrgCreation *bool `jsonapi:"attr,limit-user-organization-creation,omitempty"` - APIRateLimitingEnabled *bool `jsonapi:"attr,api-rate-limiting-enabled,omitempty"` - APIRateLimit *int `jsonapi:"attr,api-rate-limit,omitempty"` - SendPassingStatusUntriggeredPlans *bool `jsonapi:"attr,send-passing-statuses-for-untriggered-speculative-plans,omitempty"` - AllowSpeculativePlansOnPR *bool `jsonapi:"attr,allow-speculative-plans-on-pull-requests-from-forks,omitempty"` - DefaultRemoteStateAccess *bool `jsonapi:"attr,default-remote-state-access,omitempty"` + LimitUserOrgCreation *bool `jsonapi:"attr,limit-user-organization-creation,omitempty"` + APIRateLimitingEnabled *bool `jsonapi:"attr,api-rate-limiting-enabled,omitempty"` + APIRateLimit *int `jsonapi:"attr,api-rate-limit,omitempty"` + SendPassingStatusUntriggeredPlans *bool `jsonapi:"attr,send-passing-statuses-for-untriggered-speculative-plans,omitempty"` + AllowSpeculativePlansOnPR *bool `jsonapi:"attr,allow-speculative-plans-on-pull-requests-from-forks,omitempty"` + DefaultRemoteStateAccess *bool `jsonapi:"attr,default-remote-state-access,omitempty"` + ApplyTimeout *string `jsonapi:"attr,apply-timeout"` + PlanTimeout *string `jsonapi:"attr,plan-timeout"` } // Read returns the general settings. diff --git a/admin_setting_general_integration_test.go b/admin_setting_general_integration_test.go index 6cfbccfd1..3291ed842 100644 --- a/admin_setting_general_integration_test.go +++ b/admin_setting_general_integration_test.go @@ -34,6 +34,8 @@ func TestAdminSettings_General_Read(t *testing.T) { assert.NotNil(t, generalSettings.DefaultWorkspacesPerOrgCeiling) assert.NotNil(t, generalSettings.TerraformBuildWorkerApplyTimeout) assert.NotNil(t, generalSettings.TerraformBuildWorkerPlanTimeout) + assert.NotNil(t, generalSettings.ApplyTimeout) + assert.NotNil(t, generalSettings.PlanTimeout) assert.NotNil(t, generalSettings.DefaultRemoteStateAccess) } @@ -50,23 +52,31 @@ func TestAdminSettings_General_Update(t *testing.T) { origAPIRateLimitEnabled := generalSettings.APIRateLimitingEnabled origAPIRateLimit := generalSettings.APIRateLimit origDefaultRemoteState := generalSettings.DefaultRemoteStateAccess + origApplyTimeout := generalSettings.ApplyTimeout + origPlanTimeout := generalSettings.PlanTimeout limitOrgCreation := true apiRateLimitEnabled := true apiRateLimit := 50 defaultRemoteStateAccess := false + applyTimeout := "2h" + planTimeout := "30m" generalSettings, err = client.Admin.Settings.General.Update(ctx, AdminGeneralSettingsUpdateOptions{ LimitUserOrgCreation: Bool(limitOrgCreation), APIRateLimitingEnabled: Bool(apiRateLimitEnabled), APIRateLimit: Int(apiRateLimit), DefaultRemoteStateAccess: Bool(defaultRemoteStateAccess), + ApplyTimeout: &applyTimeout, + PlanTimeout: &planTimeout, }) require.NoError(t, err) assert.Equal(t, limitOrgCreation, generalSettings.LimitUserOrganizationCreation) assert.Equal(t, apiRateLimitEnabled, generalSettings.APIRateLimitingEnabled) assert.Equal(t, apiRateLimit, generalSettings.APIRateLimit) assert.Equal(t, defaultRemoteStateAccess, generalSettings.DefaultRemoteStateAccess) + assert.Equal(t, applyTimeout, generalSettings.ApplyTimeout) + assert.Equal(t, planTimeout, generalSettings.PlanTimeout) // Undo Updates, revert back to original generalSettings, err = client.Admin.Settings.General.Update(ctx, AdminGeneralSettingsUpdateOptions{ @@ -74,10 +84,14 @@ func TestAdminSettings_General_Update(t *testing.T) { APIRateLimitingEnabled: Bool(origAPIRateLimitEnabled), APIRateLimit: Int(origAPIRateLimit), DefaultRemoteStateAccess: Bool(origDefaultRemoteState), + ApplyTimeout: &origApplyTimeout, + PlanTimeout: &origPlanTimeout, }) require.NoError(t, err) assert.Equal(t, origLimitOrgCreation, generalSettings.LimitUserOrganizationCreation) assert.Equal(t, origAPIRateLimitEnabled, generalSettings.APIRateLimitingEnabled) assert.Equal(t, origAPIRateLimit, generalSettings.APIRateLimit) assert.Equal(t, origDefaultRemoteState, generalSettings.DefaultRemoteStateAccess) + assert.Equal(t, origApplyTimeout, generalSettings.ApplyTimeout) + assert.Equal(t, origPlanTimeout, generalSettings.PlanTimeout) } From 5b8d80588d164aed5408399e8cdf2628fe1ec912 Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Fri, 2 Aug 2024 10:38:41 +0200 Subject: [PATCH 59/61] stacks: add missing fields to change and resource instance --- stack_plan.go | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/stack_plan.go b/stack_plan.go index 2fd50b536..281b51e8e 100644 --- a/stack_plan.go +++ b/stack_plan.go @@ -137,16 +137,21 @@ type ChangeAction string // JSONResourceInstance is the change description of a single resource instance in a plan. type JSONResourceInstance struct { - ComponentInstanceCorrelator string `json:"component_instance_correlator"` - ComponentInstanceAddress string `json:"component_instance_address"` - Address string `json:"address"` - PreviousComponentInstanceAddress string `json:"previous_component_instance_address,omitempty"` - PreviousAddress string `json:"previous_address,omitempty"` - DeposedKey string `json:"deposed,omitempty"` - ResourceMode string `json:"mode,omitempty"` - ResourceType string `json:"type"` - ProviderAddr string `json:"provider_name"` - Change Change `json:"change"` + ComponentInstanceCorrelator string `json:"component_instance_correlator"` + ComponentInstanceAddress string `json:"component_instance_address"` + Address string `json:"address"` + PreviousComponentInstanceAddress string `json:"previous_component_instance_address,omitempty"` + PreviousAddress string `json:"previous_address,omitempty"` + DeposedKey string `json:"deposed,omitempty"` + ResourceMode string `json:"mode,omitempty"` + ResourceType string `json:"type"` + ProviderAddr string `json:"provider_name"` + Change Change `json:"change"` + ResourceName string `json:"resource_name"` + Index json.RawMessage `json:"index"` + IndexUnknown bool `json:"index_unknown"` + ModuleAddr string `json:"module_address"` + ActionReason string `json:"action_reason,omitempty"` } // JSONResourceInstanceDeferral is the change description of a single resource instance that is deferred. @@ -167,9 +172,22 @@ type JSONOutput struct { // Change represents the change of a resource instance in a plan. type Change struct { - Actions []ChangeAction `json:"actions"` - After json.RawMessage `json:"after"` - Before json.RawMessage `json:"before"` + Actions []ChangeAction `json:"actions"` + After json.RawMessage `json:"after"` + Before json.RawMessage `json:"before"` + AfterUnknown json.RawMessage `json:"after_unknown"` + BeforeSensitive json.RawMessage `json:"before_sensitive"` + AfterSensitive json.RawMessage `json:"after_sensitive"` + Importing *JsonImporting `json:"importing,omitempty"` + ReplacePaths json.RawMessage `json:"replace_paths,omitempty"` +} + +// JsonImporting represents the import status of a resource instance in a plan. +type JsonImporting struct { + // True within a deferred instance + Unknown bool `json:"unknown"` + ID string `json:"id"` + GeneratedConfig string `json:"generated_config"` } func (s stackPlans) Read(ctx context.Context, stackPlanID string) (*StackPlan, error) { From 5353a7a2473fac8690f5dbb0132cb4e9cfa6b6f5 Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Fri, 2 Aug 2024 16:00:12 +0200 Subject: [PATCH 60/61] stacks: fix naming of importing --- stack_plan.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stack_plan.go b/stack_plan.go index 281b51e8e..eed01f5f5 100644 --- a/stack_plan.go +++ b/stack_plan.go @@ -178,12 +178,12 @@ type Change struct { AfterUnknown json.RawMessage `json:"after_unknown"` BeforeSensitive json.RawMessage `json:"before_sensitive"` AfterSensitive json.RawMessage `json:"after_sensitive"` - Importing *JsonImporting `json:"importing,omitempty"` + Importing *JSONImporting `json:"importing,omitempty"` ReplacePaths json.RawMessage `json:"replace_paths,omitempty"` } -// JsonImporting represents the import status of a resource instance in a plan. -type JsonImporting struct { +// JSONImporting represents the import status of a resource instance in a plan. +type JSONImporting struct { // True within a deferred instance Unknown bool `json:"unknown"` ID string `json:"id"` From f79eff02e90bd82c0beba0e31ddc7cb71c2ce040 Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Mon, 5 Aug 2024 15:46:28 +0200 Subject: [PATCH 61/61] stacks: fix typo --- stack_plan_operation.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack_plan_operation.go b/stack_plan_operation.go index cc77bba66..095d9bacb 100644 --- a/stack_plan_operation.go +++ b/stack_plan_operation.go @@ -39,7 +39,7 @@ type StackPlanOperation struct { } func (s stackPlanOperations) Read(ctx context.Context, stackPlanOperationID string) (*StackPlanOperation, error) { - req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-plans-operations/%s", url.PathEscape(stackPlanOperationID)), nil) + req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-plan-operations/%s", url.PathEscape(stackPlanOperationID)), nil) if err != nil { return nil, err }