From 87e4befbfeb01f83624c9ee9e689591299b1aabe Mon Sep 17 00:00:00 2001 From: Josh Freda Date: Thu, 5 Dec 2024 11:34:42 -0600 Subject: [PATCH 01/41] Add support for team notification configurations --- helper_test.go | 48 ++ notification_configuration.go | 1 + team_notification_configuration.go | 327 +++++++++++++ ...fication_configuration_integration_test.go | 457 ++++++++++++++++++ tfe.go | 130 ++--- 5 files changed, 899 insertions(+), 64 deletions(-) create mode 100644 team_notification_configuration.go create mode 100644 team_notification_configuration_integration_test.go diff --git a/helper_test.go b/helper_test.go index 7d8b508ec..9e82d54d4 100644 --- a/helper_test.go +++ b/helper_test.go @@ -615,6 +615,54 @@ func createNotificationConfiguration(t *testing.T, client *Client, w *Workspace, } } +func createTeamNotificationConfiguration(t *testing.T, client *Client, team *Team, options *TeamNotificationConfigurationCreateOptions) (*TeamNotificationConfiguration, func()) { + var tCleanup func() + + if team == nil { + team, tCleanup = createTeam(t, client, nil) + } + + // Team notification configurations do not actually require a run task, but we'll + // reuse this as a URL that returns a 200. + runTaskURL := os.Getenv("TFC_RUN_TASK_URL") + if runTaskURL == "" { + t.Error("You must set TFC_RUN_TASK_URL for run task related tests.") + } + + if options == nil { + options = &TeamNotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), + Enabled: Bool(false), + Name: String(randomString(t)), + Token: String(randomString(t)), + URL: String(runTaskURL), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + } + } + + ctx := context.Background() + nc, err := client.TeamNotificationConfigurations.Create( + ctx, + team.ID, + *options, + ) + if err != nil { + t.Fatal(err) + } + + return nc, func() { + if err := client.TeamNotificationConfigurations.Delete(ctx, nc.ID); err != nil { + t.Errorf("Error destroying team notification configuration! WARNING: Dangling\n"+ + "resources may exist! The full error is shown below.\n\n"+ + "TeamNotificationConfiguration: %s\nError: %s", nc.ID, err) + } + + if tCleanup != nil { + tCleanup() + } + } +} + func createPolicySetParameter(t *testing.T, client *Client, ps *PolicySet) (*PolicySetParameter, func()) { var psCleanup func() diff --git a/notification_configuration.go b/notification_configuration.go index aeac2f04e..ece867477 100644 --- a/notification_configuration.go +++ b/notification_configuration.go @@ -59,6 +59,7 @@ const ( NotificationTriggerAssessmentCheckFailed NotificationTriggerType = "assessment:check_failure" NotificationTriggerWorkspaceAutoDestroyReminder NotificationTriggerType = "workspace:auto_destroy_reminder" NotificationTriggerWorkspaceAutoDestroyRunResults NotificationTriggerType = "workspace:auto_destroy_run_results" + NotificationTriggerChangeRequestCreated NotificationTriggerType = "change_request:created" ) // NotificationDestinationType represents the destination type of the diff --git a/team_notification_configuration.go b/team_notification_configuration.go new file mode 100644 index 000000000..68b86319f --- /dev/null +++ b/team_notification_configuration.go @@ -0,0 +1,327 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfe + +import ( + "context" + "fmt" + "net/url" + "time" +) + +// Compile-time proof of interface implementation. +var _ TeamNotificationConfigurations = (*teamNotificationConfigurations)(nil) + +// TeamNotificationConfigurations describes all the Team Notification Configuration +// related methods that the Terraform Enterprise API supports. +// +// TFE API docs: +// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/notification-configurations#team-notification-configuration +type TeamNotificationConfigurations interface { + // List all the notification configurations within a team. + List(ctx context.Context, teamID string, options *TeamNotificationConfigurationListOptions) (*TeamNotificationConfigurationList, error) + + // Create a new team notification configuration with the given options. + Create(ctx context.Context, teamID string, options TeamNotificationConfigurationCreateOptions) (*TeamNotificationConfiguration, error) + + // Read a notification configuration by its ID. + Read(ctx context.Context, teamNotificationConfigurationID string) (*TeamNotificationConfiguration, error) + + // Update an existing team notification configuration. + Update(ctx context.Context, teamNotificationConfigurationID string, options TeamNotificationConfigurationUpdateOptions) (*TeamNotificationConfiguration, error) + + // Delete a team notification configuration by its ID. + Delete(ctx context.Context, teamNotificationConfigurationID string) error + + // Verify a team notification configuration by its ID. + Verify(ctx context.Context, teamNotificationConfigurationID string) (*TeamNotificationConfiguration, error) +} + +// teamNotificationConfigurations implements TeamNotificationConfigurations. +type teamNotificationConfigurations struct { + client *Client +} + +// TeamNotificationConfigurationList represents a list of team notification +// configurations. +type TeamNotificationConfigurationList struct { + *Pagination + Items []*TeamNotificationConfiguration +} + +// TeamNotificationConfiguration represents a team notification configuration. +type TeamNotificationConfiguration struct { + ID string `jsonapi:"primary,notification-configurations"` + CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` + DeliveryResponses []*DeliveryResponse `jsonapi:"attr,delivery-responses"` + DestinationType NotificationDestinationType `jsonapi:"attr,destination-type"` + Enabled bool `jsonapi:"attr,enabled"` + Name string `jsonapi:"attr,name"` + Token string `jsonapi:"attr,token"` + Triggers []string `jsonapi:"attr,triggers"` + UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` + URL string `jsonapi:"attr,url"` + + // EmailAddresses is only available for TFE users. It is not available in HCP Terraform. + EmailAddresses []string `jsonapi:"attr,email-addresses"` + + // Relations + Subscribable *Team `jsonapi:"relation,subscribable"` + EmailUsers []*User `jsonapi:"relation,users"` +} + +// TeamNotificationConfigurationListOptions represents the options for listing +// notification configurations. +type TeamNotificationConfigurationListOptions struct { + ListOptions +} + +// TeamNotificationConfigurationCreateOptions represents the options for +// creating a new team notification configuration. +type TeamNotificationConfigurationCreateOptions struct { + // Type is a public field utilized by JSON:API to + // set the resource type via the field tag. + // It is not a user-defined value and does not need to be set. + // https://jsonapi.org/format/#crud-creating + Type string `jsonapi:"primary,notification-configurations"` + + // Required: The destination type of the team notification configuration + DestinationType *NotificationDestinationType `jsonapi:"attr,destination-type"` + + // Required: Whether the team notification configuration should be enabled or not + Enabled *bool `jsonapi:"attr,enabled"` + + // Required: The name of the team notification configuration + Name *string `jsonapi:"attr,name"` + + // Optional: The token of the team notification configuration + Token *string `jsonapi:"attr,token,omitempty"` + + // Optional: The list of events that will trigger team notifications + Triggers []NotificationTriggerType `jsonapi:"attr,triggers,omitempty"` + + // Optional: The URL of the team notification configuration + URL *string `jsonapi:"attr,url,omitempty"` + + // Optional: The list of email addresses that will receive team notification emails. + // EmailAddresses is only available for TFE users. It is not available in HCP Terraform. + EmailAddresses []string `jsonapi:"attr,email-addresses,omitempty"` + + // Optional: The list of users belonging to the organization that will receive + // team notification emails. + EmailUsers []*User `jsonapi:"relation,users,omitempty"` +} + +// TeamNotificationConfigurationUpdateOptions represents the options for +// updating a existing team notification configuration. +type TeamNotificationConfigurationUpdateOptions struct { + // Type is a public field utilized by JSON:API to + // set the resource type via the field tag. + // It is not a user-defined value and does not need to be set. + // https://jsonapi.org/format/#crud-creating + Type string `jsonapi:"primary,notification-configurations"` + + // Optional: Whether the team notification configuration should be enabled or not + Enabled *bool `jsonapi:"attr,enabled,omitempty"` + + // Optional: The name of the team notification configuration + Name *string `jsonapi:"attr,name,omitempty"` + + // Optional: The token of the team notification configuration + Token *string `jsonapi:"attr,token,omitempty"` + + // Optional: The list of events that will trigger team notifications + Triggers []NotificationTriggerType `jsonapi:"attr,triggers,omitempty"` + + // Optional: The URL of the team notification configuration + URL *string `jsonapi:"attr,url,omitempty"` + + // Optional: The list of email addresses that will receive team notification emails. + // EmailAddresses is only available for TFE users. It is not available in HCP Terraform. + EmailAddresses []string `jsonapi:"attr,email-addresses,omitempty"` + + // Optional: The list of users belonging to the organization that will receive + // team notification emails. + EmailUsers []*User `jsonapi:"relation,users,omitempty"` +} + +// List all the notification configurations associated with a team. +func (s *teamNotificationConfigurations) List(ctx context.Context, teamID string, options *TeamNotificationConfigurationListOptions) (*TeamNotificationConfigurationList, error) { + if !validStringID(&teamID) { + return nil, ErrInvalidTeamID + } + + u := fmt.Sprintf("teams/%s/notification-configurations", url.PathEscape(teamID)) + req, err := s.client.NewRequest("GET", u, options) + if err != nil { + return nil, err + } + + ncl := &TeamNotificationConfigurationList{} + err = req.Do(ctx, ncl) + if err != nil { + return nil, err + } + + return ncl, nil +} + +// Create a team notification configuration with the given options. +func (s *teamNotificationConfigurations) Create(ctx context.Context, teamID string, options TeamNotificationConfigurationCreateOptions) (*TeamNotificationConfiguration, error) { + if !validStringID(&teamID) { + return nil, ErrInvalidTeamID + } + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("teams/%s/notification-configurations", url.PathEscape(teamID)) + req, err := s.client.NewRequest("POST", u, &options) + if err != nil { + return nil, err + } + + nc := &TeamNotificationConfiguration{} + err = req.Do(ctx, nc) + if err != nil { + return nil, err + } + + return nc, nil +} + +// Read a team notification configuration by its ID. +func (s *teamNotificationConfigurations) Read(ctx context.Context, teamNotificationConfigurationID string) (*TeamNotificationConfiguration, error) { + if !validStringID(&teamNotificationConfigurationID) { + return nil, ErrInvalidNotificationConfigID + } + + u := fmt.Sprintf("notification-configurations/%s", url.PathEscape(teamNotificationConfigurationID)) + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, err + } + + nc := &TeamNotificationConfiguration{} + err = req.Do(ctx, nc) + if err != nil { + return nil, err + } + + return nc, nil +} + +// Updates a team notification configuration with the given options. +func (s *teamNotificationConfigurations) Update(ctx context.Context, teamNotificationConfigurationID string, options TeamNotificationConfigurationUpdateOptions) (*TeamNotificationConfiguration, error) { + if !validStringID(&teamNotificationConfigurationID) { + return nil, ErrInvalidNotificationConfigID + } + + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("notification-configurations/%s", url.PathEscape(teamNotificationConfigurationID)) + req, err := s.client.NewRequest("PATCH", u, &options) + if err != nil { + return nil, err + } + + nc := &TeamNotificationConfiguration{} + err = req.Do(ctx, nc) + if err != nil { + return nil, err + } + + return nc, nil +} + +// Delete a team notification configuration by its ID. +func (s *teamNotificationConfigurations) Delete(ctx context.Context, teamNotificationConfigurationID string) error { + if !validStringID(&teamNotificationConfigurationID) { + return ErrInvalidNotificationConfigID + } + + u := fmt.Sprintf("notification-configurations/%s", url.PathEscape(teamNotificationConfigurationID)) + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return err + } + + return req.Do(ctx, nil) +} + +// Verify a team notification configuration by delivering a verification payload +// to the configured URL. +func (s *teamNotificationConfigurations) Verify(ctx context.Context, teamNotificationConfigurationID string) (*TeamNotificationConfiguration, error) { + if !validStringID(&teamNotificationConfigurationID) { + return nil, ErrInvalidNotificationConfigID + } + + u := fmt.Sprintf( + "notification-configurations/%s/actions/verify", url.PathEscape(teamNotificationConfigurationID)) + req, err := s.client.NewRequest("POST", u, nil) + if err != nil { + return nil, err + } + + nc := &TeamNotificationConfiguration{} + err = req.Do(ctx, nc) + if err != nil { + return nil, err + } + + return nc, nil +} + +func (o TeamNotificationConfigurationCreateOptions) valid() error { + if o.DestinationType == nil { + return ErrRequiredDestinationType + } + if o.Enabled == nil { + return ErrRequiredEnabled + } + if !validString(o.Name) { + return ErrRequiredName + } + + if !validTeamNotificationTriggerType(o.Triggers) { + return ErrInvalidNotificationTrigger + } + + if *o.DestinationType == NotificationDestinationTypeGeneric || + *o.DestinationType == NotificationDestinationTypeSlack || + *o.DestinationType == NotificationDestinationTypeMicrosoftTeams { + if o.URL == nil { + return ErrRequiredURL + } + } + return nil +} + +func (o TeamNotificationConfigurationUpdateOptions) valid() error { + if o.Name != nil && !validString(o.Name) { + return ErrRequiredName + } + + if !validTeamNotificationTriggerType(o.Triggers) { + return ErrInvalidNotificationTrigger + } + + return nil +} + +func validTeamNotificationTriggerType(triggers []NotificationTriggerType) bool { + for _, t := range triggers { + switch t { + case + NotificationTriggerChangeRequestCreated: + continue + default: + return false + } + } + + return true +} diff --git a/team_notification_configuration_integration_test.go b/team_notification_configuration_integration_test.go new file mode 100644 index 000000000..d78e0fe67 --- /dev/null +++ b/team_notification_configuration_integration_test.go @@ -0,0 +1,457 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfe + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTeamNotificationConfigurationList(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + + newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + + tmTest, tmTestCleanup := createTeam(t, client, orgTest) + t.Cleanup(tmTestCleanup) + require.NotNil(t, tmTest) + + ncTest1, ncTestCleanup1 := createTeamNotificationConfiguration(t, client, tmTest, nil) + t.Cleanup(ncTestCleanup1) + ncTest2, ncTestCleanup2 := createTeamNotificationConfiguration(t, client, tmTest, nil) + t.Cleanup(ncTestCleanup2) + + t.Run("with a valid team", func(t *testing.T) { + ncl, err := client.TeamNotificationConfigurations.List( + ctx, + tmTest.ID, + nil, + ) + require.NoError(t, err) + assert.Contains(t, ncl.Items, ncTest1) + assert.Contains(t, ncl.Items, ncTest2) + + t.Skip("paging not supported yet in API") + assert.Equal(t, 1, ncl.CurrentPage) + assert.Equal(t, 2, ncl.TotalCount) + }) + + t.Run("with list options", func(t *testing.T) { + t.Skip("paging not supported yet in API") + // Request a page number which is out of range. The result should + // be successful, but return no results if the paging options are + // properly passed along. + ncl, err := client.TeamNotificationConfigurations.List( + ctx, + tmTest.ID, + &TeamNotificationConfigurationListOptions{ + ListOptions: ListOptions{ + PageNumber: 999, + PageSize: 100, + }, + }, + ) + require.NoError(t, err) + assert.Empty(t, ncl.Items) + assert.Equal(t, 999, ncl.CurrentPage) + assert.Equal(t, 2, ncl.TotalCount) + }) + + t.Run("without a valid team", func(t *testing.T) { + ncl, err := client.TeamNotificationConfigurations.List( + ctx, + badIdentifier, + nil, + ) + assert.Nil(t, ncl) + assert.EqualError(t, err, ErrInvalidTeamID.Error()) + }) +} + +func TestTeamNotificationConfigurationCreate(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + + newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + + tmTest, tmTestCleanup := createTeam(t, client, orgTest) + t.Cleanup(tmTestCleanup) + + // Create user to use when testing email destination type + orgMemberTest, orgMemberTestCleanup := createOrganizationMembership(t, client, orgTest) + t.Cleanup(orgMemberTestCleanup) + + // Add user to team + options := TeamMemberAddOptions{ + OrganizationMembershipIDs: []string{orgMemberTest.ID}, + } + err := client.TeamMembers.Add(ctx, tmTest.ID, options) + require.NoError(t, err) + + t.Run("with all required values", func(t *testing.T) { + options := TeamNotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), + Enabled: Bool(false), + Name: String(randomString(t)), + Token: String(randomString(t)), + URL: String("http://example.com"), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + } + + _, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) + require.NoError(t, err) + }) + + t.Run("without a required value", func(t *testing.T) { + options := TeamNotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), + Enabled: Bool(false), + Token: String(randomString(t)), + URL: String("http://example.com"), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + } + + nc, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) + assert.Nil(t, nc) + assert.EqualError(t, err, ErrRequiredName.Error()) + }) + + t.Run("without a required value URL when destination type is generic", func(t *testing.T) { + options := TeamNotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), + Enabled: Bool(false), + Name: String(randomString(t)), + Token: String(randomString(t)), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + } + + nc, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) + assert.Nil(t, nc) + assert.Equal(t, err, ErrRequiredURL) + }) + + t.Run("without a required value URL when destination type is slack", func(t *testing.T) { + options := TeamNotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeSlack), + Enabled: Bool(false), + Name: String(randomString(t)), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + } + + nc, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) + assert.Nil(t, nc) + assert.Equal(t, err, ErrRequiredURL) + }) + + t.Run("without a required value URL when destination type is MS Teams", func(t *testing.T) { + options := TeamNotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeMicrosoftTeams), + Enabled: Bool(false), + Name: String(randomString(t)), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + } + + nc, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) + assert.Nil(t, nc) + assert.Equal(t, err, ErrRequiredURL) + }) + + t.Run("without a valid team", func(t *testing.T) { + nc, err := client.TeamNotificationConfigurations.Create(ctx, badIdentifier, TeamNotificationConfigurationCreateOptions{}) + assert.Nil(t, nc) + assert.EqualError(t, err, ErrInvalidTeamID.Error()) + }) + + t.Run("with an invalid notification trigger", func(t *testing.T) { + options := TeamNotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), + Enabled: Bool(false), + Name: String(randomString(t)), + Token: String(randomString(t)), + URL: String("http://example.com"), + Triggers: []NotificationTriggerType{"the beacons of gondor are lit"}, + } + + nc, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) + assert.Nil(t, nc) + assert.EqualError(t, err, ErrInvalidNotificationTrigger.Error()) + }) + + t.Run("with email users when destination type is email", func(t *testing.T) { + options := TeamNotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeEmail), + Enabled: Bool(false), + Name: String(randomString(t)), + EmailUsers: []*User{orgMemberTest.User}, + } + + _, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) + require.NoError(t, err) + }) + + t.Run("without email users when destination type is email", func(t *testing.T) { + options := TeamNotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeEmail), + Enabled: Bool(false), + Name: String(randomString(t)), + } + + _, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) + require.NoError(t, err) + }) +} + +func TestTeamNotificationConfigurationsCreate_byType(t *testing.T) { + t.Parallel() + + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + + newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + + tmTest, tmTestCleanup := createTeam(t, client, orgTest) + t.Cleanup(tmTestCleanup) + + testCases := []NotificationTriggerType{ + NotificationTriggerChangeRequestCreated, + } + + for _, trigger := range testCases { + trigger := trigger + message := fmt.Sprintf("with trigger %s and all required values", trigger) + + t.Run(message, func(t *testing.T) { + t.Parallel() + options := TeamNotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), + Enabled: Bool(false), + Name: String(randomString(t)), + Token: String(randomString(t)), + URL: String("http://example.com"), + Triggers: []NotificationTriggerType{trigger}, + } + + _, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) + require.NoError(t, err) + }) + } +} + +func TestTeamNotificationConfigurationRead(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + + newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + + tmTest, tmTestCleanup := createTeam(t, client, orgTest) + t.Cleanup(tmTestCleanup) + + ncTest, ncTestCleanup := createTeamNotificationConfiguration(t, client, tmTest, nil) + t.Cleanup(ncTestCleanup) + + t.Run("with a valid ID", func(t *testing.T) { + nc, err := client.TeamNotificationConfigurations.Read(ctx, ncTest.ID) + require.NoError(t, err) + assert.Equal(t, ncTest.ID, nc.ID) + }) + + t.Run("when the notification configuration does not exist", func(t *testing.T) { + _, err := client.TeamNotificationConfigurations.Read(ctx, "nonexisting") + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the notification configuration ID is invalid", func(t *testing.T) { + _, err := client.TeamNotificationConfigurations.Read(ctx, badIdentifier) + assert.Equal(t, err, ErrInvalidNotificationConfigID) + }) +} + +func TestTeamNotificationConfigurationUpdate(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + + newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + + tmTest, tmTestCleanup := createTeam(t, client, orgTest) + t.Cleanup(tmTestCleanup) + + ncTest, ncTestCleanup := createTeamNotificationConfiguration(t, client, tmTest, nil) + t.Cleanup(ncTestCleanup) + + // Create users to use when testing email destination type + orgMemberTest1, orgMemberTest1Cleanup := createOrganizationMembership(t, client, orgTest) + defer orgMemberTest1Cleanup() + orgMemberTest2, orgMemberTest2Cleanup := createOrganizationMembership(t, client, orgTest) + defer orgMemberTest2Cleanup() + + orgMemberTest1.User = &User{ID: orgMemberTest1.User.ID} + orgMemberTest2.User = &User{ID: orgMemberTest2.User.ID} + + // Add users to team + for _, orgMember := range []*OrganizationMembership{orgMemberTest1, orgMemberTest2} { + options := TeamMemberAddOptions{ + OrganizationMembershipIDs: []string{orgMember.ID}, + } + err := client.TeamMembers.Add(ctx, tmTest.ID, options) + require.NoError(t, err) + } + + options := &TeamNotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeEmail), + Enabled: Bool(false), + Name: String(randomString(t)), + EmailUsers: []*User{orgMemberTest1.User}, + } + ncEmailTest, ncEmailTestCleanup := createTeamNotificationConfiguration(t, client, tmTest, options) + t.Cleanup(ncEmailTestCleanup) + + t.Run("with options", func(t *testing.T) { + options := TeamNotificationConfigurationUpdateOptions{ + Enabled: Bool(true), + Name: String("newName"), + } + + nc, err := client.TeamNotificationConfigurations.Update(ctx, ncTest.ID, options) + require.NoError(t, err) + assert.Equal(t, nc.Enabled, true) + assert.Equal(t, nc.Name, "newName") + }) + + t.Run("with invalid notification trigger", func(t *testing.T) { + options := TeamNotificationConfigurationUpdateOptions{ + Triggers: []NotificationTriggerType{"fly you fools!"}, + } + + nc, err := client.TeamNotificationConfigurations.Update(ctx, ncTest.ID, options) + assert.Nil(t, nc) + assert.EqualError(t, err, ErrInvalidNotificationTrigger.Error()) + }) + + t.Run("with email users when destination type is email", func(t *testing.T) { + options := TeamNotificationConfigurationUpdateOptions{ + Enabled: Bool(true), + Name: String("newName"), + EmailUsers: []*User{orgMemberTest1.User, orgMemberTest2.User}, + } + + nc, err := client.TeamNotificationConfigurations.Update(ctx, ncEmailTest.ID, options) + require.NoError(t, err) + assert.Equal(t, nc.Enabled, true) + assert.Equal(t, nc.Name, "newName") + assert.Contains(t, nc.EmailUsers, orgMemberTest1.User) + assert.Contains(t, nc.EmailUsers, orgMemberTest2.User) + }) + + t.Run("without email users when destination type is email", func(t *testing.T) { + options := TeamNotificationConfigurationUpdateOptions{ + Enabled: Bool(true), + Name: String("newName"), + } + + nc, err := client.TeamNotificationConfigurations.Update(ctx, ncEmailTest.ID, options) + require.NoError(t, err) + assert.Equal(t, nc.Enabled, true) + assert.Equal(t, nc.Name, "newName") + assert.Empty(t, nc.EmailUsers) + }) + + t.Run("without options", func(t *testing.T) { + _, err := client.TeamNotificationConfigurations.Update(ctx, ncTest.ID, TeamNotificationConfigurationUpdateOptions{}) + require.NoError(t, err) + }) + + t.Run("when the notification configuration does not exist", func(t *testing.T) { + _, err := client.TeamNotificationConfigurations.Update(ctx, "nonexisting", TeamNotificationConfigurationUpdateOptions{}) + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the notification configuration ID is invalid", func(t *testing.T) { + _, err := client.TeamNotificationConfigurations.Update(ctx, badIdentifier, TeamNotificationConfigurationUpdateOptions{}) + assert.Equal(t, err, ErrInvalidNotificationConfigID) + }) +} + +func TestTeamNotificationConfigurationDelete(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + + newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + + tmTest, tmTestCleanup := createTeam(t, client, orgTest) + t.Cleanup(tmTestCleanup) + + ncTest, _ := createTeamNotificationConfiguration(t, client, tmTest, nil) + + t.Run("with a valid ID", func(t *testing.T) { + err := client.TeamNotificationConfigurations.Delete(ctx, ncTest.ID) + require.NoError(t, err) + + _, err = client.TeamNotificationConfigurations.Read(ctx, ncTest.ID) + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the notification configuration does not exist", func(t *testing.T) { + err := client.TeamNotificationConfigurations.Delete(ctx, "nonexisting") + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the notification configuration ID is invalid", func(t *testing.T) { + err := client.TeamNotificationConfigurations.Delete(ctx, badIdentifier) + assert.Equal(t, err, ErrInvalidNotificationConfigID) + }) +} + +func TestTeamNotificationConfigurationVerify(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + + newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + + tmTest, tmTestCleanup := createTeam(t, client, orgTest) + t.Cleanup(tmTestCleanup) + + ncTest, ncTestCleanup := createTeamNotificationConfiguration(t, client, tmTest, nil) + t.Cleanup(ncTestCleanup) + + t.Run("with a valid ID", func(t *testing.T) { + _, err := client.TeamNotificationConfigurations.Verify(ctx, ncTest.ID) + require.NoError(t, err) + }) + + t.Run("when the notification configuration does not exists", func(t *testing.T) { + _, err := client.TeamNotificationConfigurations.Verify(ctx, "nonexisting") + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the notification configuration ID is invalid", func(t *testing.T) { + _, err := client.TeamNotificationConfigurations.Verify(ctx, badIdentifier) + assert.Equal(t, err, ErrInvalidNotificationConfigID) + }) +} diff --git a/tfe.go b/tfe.go index a4f40be3e..c229c26fe 100644 --- a/tfe.go +++ b/tfe.go @@ -123,70 +123,71 @@ type Client struct { remoteTFEVersion string appName string - Admin Admin - Agents Agents - AgentPools AgentPools - AgentTokens AgentTokens - Applies Applies - AuditTrails AuditTrails - Comments Comments - ConfigurationVersions ConfigurationVersions - CostEstimates CostEstimates - GHAInstallations GHAInstallations - GPGKeys GPGKeys - NotificationConfigurations NotificationConfigurations - OAuthClients OAuthClients - OAuthTokens OAuthTokens - Organizations Organizations - OrganizationMemberships OrganizationMemberships - OrganizationTags OrganizationTags - OrganizationTokens OrganizationTokens - Plans Plans - PlanExports PlanExports - Policies Policies - PolicyChecks PolicyChecks - PolicyEvaluations PolicyEvaluations - PolicySetOutcomes PolicySetOutcomes - PolicySetParameters PolicySetParameters - PolicySetVersions PolicySetVersions - PolicySets PolicySets - RegistryModules RegistryModules - RegistryNoCodeModules RegistryNoCodeModules - RegistryProviders RegistryProviders - RegistryProviderPlatforms RegistryProviderPlatforms - RegistryProviderVersions RegistryProviderVersions - Runs Runs - RunEvents RunEvents - RunTasks RunTasks - RunTasksIntegration RunTasksIntegration - RunTriggers RunTriggers - SSHKeys SSHKeys - Stacks Stacks - StackConfigurations StackConfigurations - StackDeployments StackDeployments - StackPlans StackPlans - StackPlanOperations StackPlanOperations - StackSources StackSources - StateVersionOutputs StateVersionOutputs - StateVersions StateVersions - TaskResults TaskResults - TaskStages TaskStages - Teams Teams - TeamAccess TeamAccesses - TeamMembers TeamMembers - TeamProjectAccess TeamProjectAccesses - TeamTokens TeamTokens - TestRuns TestRuns - TestVariables TestVariables - Users Users - UserTokens UserTokens - Variables Variables - VariableSets VariableSets - VariableSetVariables VariableSetVariables - Workspaces Workspaces - WorkspaceResources WorkspaceResources - WorkspaceRunTasks WorkspaceRunTasks - Projects Projects + Admin Admin + Agents Agents + AgentPools AgentPools + AgentTokens AgentTokens + Applies Applies + AuditTrails AuditTrails + Comments Comments + ConfigurationVersions ConfigurationVersions + CostEstimates CostEstimates + GHAInstallations GHAInstallations + GPGKeys GPGKeys + NotificationConfigurations NotificationConfigurations + OAuthClients OAuthClients + OAuthTokens OAuthTokens + Organizations Organizations + OrganizationMemberships OrganizationMemberships + OrganizationTags OrganizationTags + OrganizationTokens OrganizationTokens + Plans Plans + PlanExports PlanExports + Policies Policies + PolicyChecks PolicyChecks + PolicyEvaluations PolicyEvaluations + PolicySetOutcomes PolicySetOutcomes + PolicySetParameters PolicySetParameters + PolicySetVersions PolicySetVersions + PolicySets PolicySets + RegistryModules RegistryModules + RegistryNoCodeModules RegistryNoCodeModules + RegistryProviders RegistryProviders + RegistryProviderPlatforms RegistryProviderPlatforms + RegistryProviderVersions RegistryProviderVersions + Runs Runs + RunEvents RunEvents + RunTasks RunTasks + RunTasksIntegration RunTasksIntegration + RunTriggers RunTriggers + SSHKeys SSHKeys + Stacks Stacks + StackConfigurations StackConfigurations + StackDeployments StackDeployments + StackPlans StackPlans + StackPlanOperations StackPlanOperations + StackSources StackSources + StateVersionOutputs StateVersionOutputs + StateVersions StateVersions + TaskResults TaskResults + TaskStages TaskStages + Teams Teams + TeamAccess TeamAccesses + TeamMembers TeamMembers + TeamNotificationConfigurations TeamNotificationConfigurations + TeamProjectAccess TeamProjectAccesses + TeamTokens TeamTokens + TestRuns TestRuns + TestVariables TestVariables + Users Users + UserTokens UserTokens + Variables Variables + VariableSets VariableSets + VariableSetVariables VariableSetVariables + Workspaces Workspaces + WorkspaceResources WorkspaceResources + WorkspaceRunTasks WorkspaceRunTasks + Projects Projects Meta Meta } @@ -501,6 +502,7 @@ func NewClient(cfg *Config) (*Client, error) { client.TaskStages = &taskStages{client: client} client.TeamAccess = &teamAccesses{client: client} client.TeamMembers = &teamMembers{client: client} + client.TeamNotificationConfigurations = &teamNotificationConfigurations{client: client} client.TeamProjectAccess = &teamProjectAccesses{client: client} client.Teams = &teams{client: client} client.TeamTokens = &teamTokens{client: client} From 07477b9b757f6e13ec344b00a559197edef3b36d Mon Sep 17 00:00:00 2001 From: Josh Freda Date: Tue, 10 Dec 2024 18:42:56 -0600 Subject: [PATCH 02/41] Add skipUnlessBeta to team notification configuration tests until feature is GA --- team_notification_configuration_integration_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/team_notification_configuration_integration_test.go b/team_notification_configuration_integration_test.go index d78e0fe67..328c6cbb6 100644 --- a/team_notification_configuration_integration_test.go +++ b/team_notification_configuration_integration_test.go @@ -13,6 +13,7 @@ import ( ) func TestTeamNotificationConfigurationList(t *testing.T) { + skipUnlessBeta(t) client := testClient(t) ctx := context.Background() @@ -78,6 +79,7 @@ func TestTeamNotificationConfigurationList(t *testing.T) { } func TestTeamNotificationConfigurationCreate(t *testing.T) { + skipUnlessBeta(t) client := testClient(t) ctx := context.Background() @@ -214,6 +216,7 @@ func TestTeamNotificationConfigurationCreate(t *testing.T) { } func TestTeamNotificationConfigurationsCreate_byType(t *testing.T) { + skipUnlessBeta(t) t.Parallel() client := testClient(t) @@ -253,6 +256,7 @@ func TestTeamNotificationConfigurationsCreate_byType(t *testing.T) { } func TestTeamNotificationConfigurationRead(t *testing.T) { + skipUnlessBeta(t) client := testClient(t) ctx := context.Background() @@ -285,6 +289,7 @@ func TestTeamNotificationConfigurationRead(t *testing.T) { } func TestTeamNotificationConfigurationUpdate(t *testing.T) { + skipUnlessBeta(t) client := testClient(t) ctx := context.Background() @@ -393,6 +398,7 @@ func TestTeamNotificationConfigurationUpdate(t *testing.T) { } func TestTeamNotificationConfigurationDelete(t *testing.T) { + skipUnlessBeta(t) client := testClient(t) ctx := context.Background() @@ -426,6 +432,7 @@ func TestTeamNotificationConfigurationDelete(t *testing.T) { } func TestTeamNotificationConfigurationVerify(t *testing.T) { + skipUnlessBeta(t) client := testClient(t) ctx := context.Background() From 79d08528b1198ff5903dd6007b1b5f24be7df27f Mon Sep 17 00:00:00 2001 From: Netra Mali Date: Mon, 16 Dec 2024 11:11:17 -0500 Subject: [PATCH 03/41] add variable sets permission to team project --- team_project_access.go | 19 +++++++++++++++---- team_project_access_integration_test.go | 13 +++++++++---- type_helpers.go | 5 +++++ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/team_project_access.go b/team_project_access.go index a08d0992f..aaf99085a 100644 --- a/team_project_access.go +++ b/team_project_access.go @@ -69,8 +69,9 @@ type TeamProjectAccess struct { // ProjectPermissions represents the team's permissions on its project type TeamProjectAccessProjectPermissions struct { - ProjectSettingsPermission ProjectSettingsPermissionType `jsonapi:"attr,settings"` - ProjectTeamsPermission ProjectTeamsPermissionType `jsonapi:"attr,teams"` + ProjectSettingsPermission ProjectSettingsPermissionType `jsonapi:"attr,settings"` + ProjectTeamsPermission ProjectTeamsPermissionType `jsonapi:"attr,teams"` + ProjectVariableSetsPermission ProjectVariableSetsPermissionType `jsonapi:"attr,variable-sets"` } // WorkspacePermissions represents the team's permission on all workspaces in its project @@ -104,6 +105,15 @@ const ( ProjectTeamsPermissionManage ProjectTeamsPermissionType = "manage" ) +// ProjectVariableSetsPermissionType represents the permission type to a project's variable sets +type ProjectVariableSetsPermissionType string + +const ( + ProjectVariableSetsPermissionNone ProjectVariableSetsPermissionType = "none" + ProjectVariableSetsPermissionRead ProjectVariableSetsPermissionType = "read" + ProjectVariableSetsPermissionWrite ProjectVariableSetsPermissionType = "write" +) + // WorkspaceRunsPermissionType represents the permissiontype to project workspaces' runs type WorkspaceRunsPermissionType string @@ -141,8 +151,9 @@ const ( ) type TeamProjectAccessProjectPermissionsOptions struct { - Settings *ProjectSettingsPermissionType `json:"settings,omitempty"` - Teams *ProjectTeamsPermissionType `json:"teams,omitempty"` + Settings *ProjectSettingsPermissionType `json:"settings,omitempty"` + Teams *ProjectTeamsPermissionType `json:"teams,omitempty"` + VariableSets *ProjectVariableSetsPermissionType `json:"variable-sets,omitempty"` } type TeamProjectAccessWorkspacePermissionsOptions struct { diff --git a/team_project_access_integration_test.go b/team_project_access_integration_test.go index 831c1f1c6..934af232c 100644 --- a/team_project_access_integration_test.go +++ b/team_project_access_integration_test.go @@ -171,8 +171,9 @@ func TestTeamProjectAccessesAdd(t *testing.T) { Team: tmTest, Project: pTest, ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{ - Settings: ProjectSettingsPermission(ProjectSettingsPermissionUpdate), - Teams: ProjectTeamsPermission(ProjectTeamsPermissionManage), + Settings: ProjectSettingsPermission(ProjectSettingsPermissionUpdate), + Teams: ProjectTeamsPermission(ProjectTeamsPermissionManage), + VariableSets: ProjectVariableSetsPermission(ProjectVariableSetsPermissionWrite), }, WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{ Runs: WorkspaceRunsPermission(WorkspaceRunsPermissionApply), @@ -209,6 +210,7 @@ func TestTeamProjectAccessesAdd(t *testing.T) { assert.Equal(t, options.Access, item.Access) assert.Equal(t, *options.ProjectAccess.Settings, item.ProjectAccess.ProjectSettingsPermission) assert.Equal(t, *options.ProjectAccess.Teams, item.ProjectAccess.ProjectTeamsPermission) + assert.Equal(t, *options.ProjectAccess.VariableSets, item.ProjectAccess.ProjectVariableSetsPermission) assert.Equal(t, *options.WorkspaceAccess.Runs, item.WorkspaceAccess.WorkspaceRunsPermission) assert.Equal(t, *options.WorkspaceAccess.SentinelMocks, item.WorkspaceAccess.WorkspaceSentinelMocksPermission) assert.Equal(t, *options.WorkspaceAccess.StateVersions, item.WorkspaceAccess.WorkspaceStateVersionsPermission) @@ -355,8 +357,9 @@ func TestTeamProjectAccessesUpdate(t *testing.T) { options := TeamProjectAccessUpdateOptions{ Access: ProjectAccess(TeamProjectAccessCustom), ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{ - Settings: ProjectSettingsPermission(ProjectSettingsPermissionUpdate), - Teams: ProjectTeamsPermission(ProjectTeamsPermissionManage), + Settings: ProjectSettingsPermission(ProjectSettingsPermissionUpdate), + Teams: ProjectTeamsPermission(ProjectTeamsPermissionManage), + VariableSets: ProjectVariableSetsPermission(ProjectVariableSetsPermissionWrite), }, WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{ Runs: WorkspaceRunsPermission(WorkspaceRunsPermissionPlan), @@ -378,6 +381,7 @@ func TestTeamProjectAccessesUpdate(t *testing.T) { assert.Equal(t, tpa.Access, TeamProjectAccessCustom) assert.Equal(t, *options.ProjectAccess.Teams, tpa.ProjectAccess.ProjectTeamsPermission) assert.Equal(t, *options.ProjectAccess.Settings, tpa.ProjectAccess.ProjectSettingsPermission) + assert.Equal(t, *options.ProjectAccess.VariableSets, tpa.ProjectAccess.ProjectVariableSetsPermission) assert.Equal(t, *options.WorkspaceAccess.Runs, tpa.WorkspaceAccess.WorkspaceRunsPermission) assert.Equal(t, *options.WorkspaceAccess.SentinelMocks, tpa.WorkspaceAccess.WorkspaceSentinelMocksPermission) assert.Equal(t, *options.WorkspaceAccess.StateVersions, tpa.WorkspaceAccess.WorkspaceStateVersionsPermission) @@ -422,6 +426,7 @@ func TestTeamProjectAccessesUpdate(t *testing.T) { assert.Equal(t, false, tpa.WorkspaceAccess.WorkspaceCreatePermission) // assert that other attributes remain the same assert.Equal(t, tpaCustomTest.ProjectAccess.ProjectSettingsPermission, tpa.ProjectAccess.ProjectSettingsPermission) + assert.Equal(t, tpaCustomTest.ProjectAccess.ProjectVariableSetsPermission, tpa.ProjectAccess.ProjectVariableSetsPermission) assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceLockingPermission, tpa.WorkspaceAccess.WorkspaceLockingPermission) assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceMovePermission, tpa.WorkspaceAccess.WorkspaceMovePermission) assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceDeletePermission, tpa.WorkspaceAccess.WorkspaceDeletePermission) diff --git a/type_helpers.go b/type_helpers.go index 1ca99d2e5..24252f273 100644 --- a/type_helpers.go +++ b/type_helpers.go @@ -29,6 +29,11 @@ func ProjectTeamsPermission(v ProjectTeamsPermissionType) *ProjectTeamsPermissio return &v } +// ProjectVariableSetsPermission returns a pointer to the given team access project type. +func ProjectVariableSetsPermission(v ProjectVariableSetsPermissionType) *ProjectVariableSetsPermissionType { + return &v +} + // WorkspaceRunsPermission returns a pointer to the given team access project type. func WorkspaceRunsPermission(v WorkspaceRunsPermissionType) *WorkspaceRunsPermissionType { return &v From b5ede4b1f43e41d1036ba4f6a7b5363d2e2cca90 Mon Sep 17 00:00:00 2001 From: Taylor Chaparro Date: Mon, 16 Dec 2024 11:47:32 -0800 Subject: [PATCH 04/41] use plus feature set --- subscription_updater_test.go | 13 +++++++++++++ ..._notification_configuration_integration_test.go | 14 +++++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/subscription_updater_test.go b/subscription_updater_test.go index 25554fc1f..8b677ee17 100644 --- a/subscription_updater_test.go +++ b/subscription_updater_test.go @@ -72,6 +72,19 @@ func (b *organizationSubscriptionUpdater) WithTrialPlan() *organizationSubscript return b } +func (b *organizationSubscriptionUpdater) WithPlusPlan() *organizationSubscriptionUpdater { + b.planName = "Plus" + + start := time.Now() + ceiling := 1 + managedResourcesLimit := 1000 + + b.updateOpts.ContractStartAt = &start + b.updateOpts.RunsCeiling = &ceiling + b.updateOpts.ContractManagedResourcesLimit = &managedResourcesLimit + return b +} + func (b *organizationSubscriptionUpdater) WithPlusEntitlementPlan() *organizationSubscriptionUpdater { b.planName = "Plus (entitlement)" diff --git a/team_notification_configuration_integration_test.go b/team_notification_configuration_integration_test.go index 328c6cbb6..289da16e6 100644 --- a/team_notification_configuration_integration_test.go +++ b/team_notification_configuration_integration_test.go @@ -20,7 +20,7 @@ func TestTeamNotificationConfigurationList(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) t.Cleanup(orgTestCleanup) - newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + newSubscriptionUpdater(orgTest).WithPlusPlan().Update(t) tmTest, tmTestCleanup := createTeam(t, client, orgTest) t.Cleanup(tmTestCleanup) @@ -86,7 +86,7 @@ func TestTeamNotificationConfigurationCreate(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) t.Cleanup(orgTestCleanup) - newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + newSubscriptionUpdater(orgTest).WithPlusPlan().Update(t) tmTest, tmTestCleanup := createTeam(t, client, orgTest) t.Cleanup(tmTestCleanup) @@ -225,7 +225,7 @@ func TestTeamNotificationConfigurationsCreate_byType(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) t.Cleanup(orgTestCleanup) - newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + newSubscriptionUpdater(orgTest).WithPlusPlan().Update(t) tmTest, tmTestCleanup := createTeam(t, client, orgTest) t.Cleanup(tmTestCleanup) @@ -263,7 +263,7 @@ func TestTeamNotificationConfigurationRead(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) t.Cleanup(orgTestCleanup) - newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + newSubscriptionUpdater(orgTest).WithPlusPlan().Update(t) tmTest, tmTestCleanup := createTeam(t, client, orgTest) t.Cleanup(tmTestCleanup) @@ -296,7 +296,7 @@ func TestTeamNotificationConfigurationUpdate(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) t.Cleanup(orgTestCleanup) - newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + newSubscriptionUpdater(orgTest).WithPlusPlan().Update(t) tmTest, tmTestCleanup := createTeam(t, client, orgTest) t.Cleanup(tmTestCleanup) @@ -405,7 +405,7 @@ func TestTeamNotificationConfigurationDelete(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) t.Cleanup(orgTestCleanup) - newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + newSubscriptionUpdater(orgTest).WithPlusPlan().Update(t) tmTest, tmTestCleanup := createTeam(t, client, orgTest) t.Cleanup(tmTestCleanup) @@ -439,7 +439,7 @@ func TestTeamNotificationConfigurationVerify(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) t.Cleanup(orgTestCleanup) - newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + newSubscriptionUpdater(orgTest).WithPlusPlan().Update(t) tmTest, tmTestCleanup := createTeam(t, client, orgTest) t.Cleanup(tmTestCleanup) From 98975f447700482a79e4d7d177cd406ff8132f1d Mon Sep 17 00:00:00 2001 From: Simon Huang Date: Thu, 19 Dec 2024 12:09:50 -0500 Subject: [PATCH 05/41] Add project level auto destroy setting (#1011) * Add project level settings for auto destroy setting * Add missing example change * fix fmt * Update changelog * Fix typo * Run go fmt * Add project moving workspace scoped tests * Run go fmt again * Fix potential subscription error * Add workspace level indicator of project inheritance * Add new subscription updater * Remove some moving tests that have dependent logic * Update API for go-tfe * Add fmt changes * update the main example * Update all to use business plan * Add skip unless beta tags to certain tests * Run fmt * Update CHANGELOG.md Co-authored-by: Sebastian Rivera --------- Co-authored-by: Sebastian Rivera --- CHANGELOG.md | 3 ++ examples/projects/main.go | 57 +++++++++++++++++++++++++++++++++ examples/workspaces/main.go | 14 +++++---- project.go | 14 +++++++++ projects_integration_test.go | 59 +++++++++++++++++++++++++++++++++++ workspace.go | 7 +++++ workspace_integration_test.go | 44 +++++++++++++++----------- 7 files changed, 174 insertions(+), 24 deletions(-) create mode 100644 examples/projects/main.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d112c9d6..0c5d6f95d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Unreleased +## Enhancements +* Add support for project level auto destroy settings @simonxmh [#1011](https://github.com/hashicorp/go-tfe/pull/1011) + # v1.71.0 ## Enhancements diff --git a/examples/projects/main.go b/examples/projects/main.go new file mode 100644 index 000000000..b952f1bb5 --- /dev/null +++ b/examples/projects/main.go @@ -0,0 +1,57 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package main + +import ( + "context" + "log" + + tfe "github.com/hashicorp/go-tfe" + + "github.com/hashicorp/jsonapi" +) + +func main() { + config := &tfe.Config{ + Token: "insert-your-token-here", + RetryServerErrors: true, + } + + client, err := tfe.NewClient(config) + if err != nil { + log.Fatal(err) + } + + // Create a context + ctx := context.Background() + + // Create a new project + p, err := client.Projects.Create(ctx, "org-test", tfe.ProjectCreateOptions{ + Name: "my-app-tst", + }) + if err != nil { + log.Fatal(err) + } + + // Update the project auto destroy activity duration + p, err = client.Projects.Update(ctx, p.ID, tfe.ProjectUpdateOptions{ + AutoDestroyActivityDuration: jsonapi.NewNullableAttrWithValue("3d"), + }) + if err != nil { + log.Fatal(err) + } + + // Disable auto destroy + p, err = client.Projects.Update(ctx, p.ID, tfe.ProjectUpdateOptions{ + AutoDestroyActivityDuration: jsonapi.NewNullNullableAttr[string](), + }) + if err != nil { + log.Fatal(err) + } + + err = client.Projects.Delete(ctx, p.ID) + if err != nil { + log.Fatal(err) + } +} diff --git a/examples/workspaces/main.go b/examples/workspaces/main.go index 5d460acac..b324e1dba 100644 --- a/examples/workspaces/main.go +++ b/examples/workspaces/main.go @@ -27,8 +27,9 @@ func main() { // Create a new workspace w, err := client.Workspaces.Create(ctx, "org-name", tfe.WorkspaceCreateOptions{ - Name: tfe.String("my-app-tst"), - AutoDestroyAt: tfe.NullableTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)), + Name: tfe.String("my-app-tst"), + AutoDestroyAt: tfe.NullableTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)), + InheritsProjectAutoDestroy: tfe.Bool(false), }) if err != nil { log.Fatal(err) @@ -36,10 +37,11 @@ func main() { // Update the workspace w, err = client.Workspaces.Update(ctx, "org-name", w.Name, tfe.WorkspaceUpdateOptions{ - AutoApply: tfe.Bool(false), - TerraformVersion: tfe.String("0.11.1"), - WorkingDirectory: tfe.String("my-app/infra"), - AutoDestroyAt: tfe.NullableTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)), + AutoApply: tfe.Bool(false), + TerraformVersion: tfe.String("0.11.1"), + WorkingDirectory: tfe.String("my-app/infra"), + AutoDestroyAt: tfe.NullableTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)), + InheritsProjectAutoDestroy: tfe.Bool(false), }) if err != nil { log.Fatal(err) diff --git a/project.go b/project.go index 89437a328..faf1da8c5 100644 --- a/project.go +++ b/project.go @@ -7,6 +7,8 @@ import ( "context" "fmt" "net/url" + + "github.com/hashicorp/jsonapi" ) // Compile-time proof of interface implementation. @@ -63,6 +65,8 @@ type Project struct { Description string `jsonapi:"attr,description"` + AutoDestroyActivityDuration jsonapi.NullableAttr[string] `jsonapi:"attr,auto-destroy-activity-duration,omitempty"` + // Relations Organization *Organization `jsonapi:"relation,organization"` } @@ -100,6 +104,11 @@ type ProjectCreateOptions struct { // Associated TagBindings of the project. TagBindings []*TagBinding `jsonapi:"relation,tag-bindings,omitempty"` + + // Optional: For all workspaces in the project, the period of time to wait + // after workspace activity to trigger a destroy run. The format should roughly + // match a Go duration string limited to days and hours, e.g. "24h" or "1d". + AutoDestroyActivityDuration jsonapi.NullableAttr[string] `jsonapi:"attr,auto-destroy-activity-duration,omitempty"` } // ProjectUpdateOptions represents the options for updating a project @@ -119,6 +128,11 @@ type ProjectUpdateOptions struct { // Associated TagBindings of the project. Note that this will replace // all existing tag bindings. TagBindings []*TagBinding `jsonapi:"relation,tag-bindings,omitempty"` + + // Optional: For all workspaces in the project, the period of time to wait + // after workspace activity to trigger a destroy run. The format should roughly + // match a Go duration string limited to days and hours, e.g. "24h" or "1d". + AutoDestroyActivityDuration jsonapi.NullableAttr[string] `jsonapi:"attr,auto-destroy-activity-duration,omitempty"` } // ProjectAddTagBindingsOptions represents the options for adding tag bindings diff --git a/projects_integration_test.go b/projects_integration_test.go index 962719282..b643aad9e 100644 --- a/projects_integration_test.go +++ b/projects_integration_test.go @@ -10,6 +10,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/hashicorp/jsonapi" ) func TestProjectsList(t *testing.T) { @@ -150,6 +152,8 @@ func TestProjectsCreate(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) defer orgTestCleanup() + newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t) + t.Run("with valid options", func(t *testing.T) { options := ProjectCreateOptions{ Name: "foo", @@ -193,6 +197,17 @@ func TestProjectsCreate(t *testing.T) { assert.Nil(t, w) assert.EqualError(t, err, ErrInvalidOrg.Error()) }) + + t.Run("when options has an invalid auto destroy activity duration", func(t *testing.T) { + skipUnlessBeta(t) + + w, err := client.Projects.Create(ctx, orgTest.Name, ProjectCreateOptions{ + Name: "foo", + AutoDestroyActivityDuration: jsonapi.NewNullableAttrWithValue("20m"), + }) + assert.Nil(t, w) + assert.Contains(t, err.Error(), "invalid attribute\n\nAuto destroy activity duration has an incorrect format, we expect up to 4 numeric digits and 1 unit ('d' or 'h')") + }) } func TestProjectsUpdate(t *testing.T) { @@ -284,6 +299,21 @@ func TestProjectsUpdate(t *testing.T) { assert.Nil(t, w) assert.EqualError(t, err, ErrInvalidProjectID.Error()) }) + + t.Run("without a valid projects auto destroy activity duration", func(t *testing.T) { + skipUnlessBeta(t) + + newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t) + + kBefore, kTestCleanup := createProject(t, client, orgTest) + defer kTestCleanup() + + w, err := client.Projects.Update(ctx, kBefore.ID, ProjectUpdateOptions{ + AutoDestroyActivityDuration: jsonapi.NewNullableAttrWithValue("bar"), + }) + assert.Nil(t, w) + assert.Contains(t, err.Error(), "invalid attribute\n\nAuto destroy activity duration has an incorrect format, we expect up to 4 numeric digits and 1 unit ('d' or 'h')") + }) } func TestProjectsAddTagBindings(t *testing.T) { @@ -378,3 +408,32 @@ func TestProjectsDelete(t *testing.T) { assert.EqualError(t, err, ErrInvalidProjectID.Error()) }) } + +func TestProjectsAutoDestroy(t *testing.T) { + skipUnlessBeta(t) + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t) + + t.Run("when creating workspace in project with autodestroy", func(t *testing.T) { + options := ProjectCreateOptions{ + Name: "foo", + Description: String("qux"), + AutoDestroyActivityDuration: jsonapi.NewNullableAttrWithValue("3d"), + } + + p, err := client.Projects.Create(ctx, orgTest.Name, options) + require.NoError(t, err) + + w, _ := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{ + Name: String(randomString(t)), + Project: p, + }) + + assert.Equal(t, p.AutoDestroyActivityDuration, w.AutoDestroyActivityDuration) + }) +} diff --git a/workspace.go b/workspace.go index 1a3990dbb..40ee48d71 100644 --- a/workspace.go +++ b/workspace.go @@ -186,6 +186,7 @@ type Workspace struct { ExecutionMode string `jsonapi:"attr,execution-mode"` FileTriggersEnabled bool `jsonapi:"attr,file-triggers-enabled"` GlobalRemoteState bool `jsonapi:"attr,global-remote-state"` + InheritsProjectAutoDestroy bool `jsonapi:"attr,inherits-project-auto-destroy"` Locked bool `jsonapi:"attr,locked"` MigrationEnvironment string `jsonapi:"attr,migration-environment"` Name string `jsonapi:"attr,name"` @@ -393,6 +394,9 @@ type WorkspaceCreateOptions struct { // should roughly match a Go duration string limited to days and hours, e.g. "24h" or "1d". AutoDestroyActivityDuration jsonapi.NullableAttr[string] `jsonapi:"attr,auto-destroy-activity-duration,omitempty"` + // Optional: Whether the workspace inherits auto destroy settings from the project + InheritsProjectAutoDestroy *bool `jsonapi:"attr,inherits-project-auto-destroy,omitempty"` + // Optional: A description for the workspace. Description *string `jsonapi:"attr,description,omitempty"` @@ -550,6 +554,9 @@ type WorkspaceUpdateOptions struct { // should roughly match a Go duration string limited to days and hours, e.g. "24h" or "1d". AutoDestroyActivityDuration jsonapi.NullableAttr[string] `jsonapi:"attr,auto-destroy-activity-duration,omitempty"` + // Optional: Whether the workspace inherits auto destroy settings from the project + InheritsProjectAutoDestroy *bool `jsonapi:"attr,inherits-project-auto-destroy,omitempty"` + // Optional: A new name for the workspace, which can only include letters, numbers, -, // and _. This will be used as an identifier and must be unique in the // organization. Warning: Changing a workspace's name changes its URL in the diff --git a/workspace_integration_test.go b/workspace_integration_test.go index 1505716a6..28e34c61c 100644 --- a/workspace_integration_test.go +++ b/workspace_integration_test.go @@ -2961,7 +2961,7 @@ func TestWorkspacesAutoDestroy(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) t.Cleanup(orgTestCleanup) - upgradeOrganizationSubscription(t, client, orgTest) + newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t) autoDestroyAt := NullableTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)) wTest, wCleanup := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{ @@ -2999,31 +2999,39 @@ func TestWorkspacesAutoDestroy(t *testing.T) { } func TestWorkspacesAutoDestroyDuration(t *testing.T) { + skipUnlessBeta(t) + client := testClient(t) ctx := context.Background() orgTest, orgTestCleanup := createOrganization(t, client) t.Cleanup(orgTestCleanup) - upgradeOrganizationSubscription(t, client, orgTest) + newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t) - duration := jsonapi.NewNullableAttrWithValue("14d") - nilDuration := jsonapi.NewNullNullableAttr[string]() - nilAutoDestroy := jsonapi.NewNullNullableAttr[time.Time]() - wTest, wCleanup := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{ - Name: String(randomString(t)), - AutoDestroyActivityDuration: duration, - }) - t.Cleanup(wCleanup) + t.Run("when creating a new workspace with standalone auto destroy settings", func(t *testing.T) { + duration := jsonapi.NewNullableAttrWithValue("14d") + nilDuration := jsonapi.NewNullNullableAttr[string]() + nilAutoDestroy := jsonapi.NewNullNullableAttr[time.Time]() + wTest, wCleanup := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{ + Name: String(randomString(t)), + AutoDestroyActivityDuration: duration, + InheritsProjectAutoDestroy: Bool(false), + }) + t.Cleanup(wCleanup) - require.Equal(t, duration, wTest.AutoDestroyActivityDuration) - require.NotEqual(t, nilAutoDestroy, wTest.AutoDestroyAt) + require.Equal(t, duration, wTest.AutoDestroyActivityDuration) + require.NotEqual(t, nilAutoDestroy, wTest.AutoDestroyAt) + require.Equal(t, wTest.InheritsProjectAutoDestroy, false) - w, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, WorkspaceUpdateOptions{ - AutoDestroyActivityDuration: nilDuration, - }) + w, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, WorkspaceUpdateOptions{ + AutoDestroyActivityDuration: nilDuration, + InheritsProjectAutoDestroy: Bool(false), + }) - require.NoError(t, err) - require.False(t, w.AutoDestroyActivityDuration.IsSpecified()) - require.False(t, w.AutoDestroyAt.IsSpecified()) + require.NoError(t, err) + require.False(t, w.AutoDestroyActivityDuration.IsSpecified()) + require.False(t, w.AutoDestroyAt.IsSpecified()) + require.Equal(t, wTest.InheritsProjectAutoDestroy, false) + }) } From 8724bc7a1da61f23da53fbdac257e79f61618bba Mon Sep 17 00:00:00 2001 From: natalie-todd Date: Fri, 20 Dec 2024 10:09:12 -0800 Subject: [PATCH 06/41] [TF-18527] Add Archs to AdminTerraformVersionCreateOptions --- admin_terraform_version.go | 71 ++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/admin_terraform_version.go b/admin_terraform_version.go index 6ae0de9d9..057fa75a0 100644 --- a/admin_terraform_version.go +++ b/admin_terraform_version.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "net/url" + "reflect" "time" ) @@ -42,17 +43,25 @@ type adminTerraformVersions struct { // AdminTerraformVersion represents a Terraform Version type AdminTerraformVersion struct { - ID string `jsonapi:"primary,terraform-versions"` - Version string `jsonapi:"attr,version"` - URL string `jsonapi:"attr,url"` - Sha string `jsonapi:"attr,sha"` - Deprecated bool `jsonapi:"attr,deprecated"` - DeprecatedReason *string `jsonapi:"attr,deprecated-reason,omitempty"` - Official bool `jsonapi:"attr,official"` - Enabled bool `jsonapi:"attr,enabled"` - Beta bool `jsonapi:"attr,beta"` - Usage int `jsonapi:"attr,usage"` - CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` + ID string `jsonapi:"primary,terraform-versions"` + Version string `jsonapi:"attr,version"` + URL string `jsonapi:"attr,url"` + Sha string `jsonapi:"attr,sha"` + Deprecated bool `jsonapi:"attr,deprecated"` + DeprecatedReason *string `jsonapi:"attr,deprecated-reason,omitempty"` + Official bool `jsonapi:"attr,official"` + Enabled bool `jsonapi:"attr,enabled"` + Beta bool `jsonapi:"attr,beta"` + Usage int `jsonapi:"attr,usage"` + CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` + Archs []*ToolVersionArchitecture `jsonapi:"attr,archs"` +} + +type ToolVersionArchitecture struct { + URL string `jsonapi:"attr,url"` + Sha string `jsonapi:"attr,sha"` + OS string `jsonapi:"attr,os"` + Arch string `jsonapi:"attr,arch"` } // AdminTerraformVersionsListOptions represents the options for listing @@ -70,29 +79,31 @@ type AdminTerraformVersionsListOptions struct { // AdminTerraformVersionCreateOptions for creating a terraform version. // https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/terraform-versions#request-body type AdminTerraformVersionCreateOptions struct { - Type string `jsonapi:"primary,terraform-versions"` - Version *string `jsonapi:"attr,version"` // Required - URL *string `jsonapi:"attr,url"` // Required - Sha *string `jsonapi:"attr,sha"` // Required - Official *bool `jsonapi:"attr,official,omitempty"` - Deprecated *bool `jsonapi:"attr,deprecated,omitempty"` - DeprecatedReason *string `jsonapi:"attr,deprecated-reason,omitempty"` - Enabled *bool `jsonapi:"attr,enabled,omitempty"` - Beta *bool `jsonapi:"attr,beta,omitempty"` + Type string `jsonapi:"primary,terraform-versions"` + Version *string `jsonapi:"attr,version"` // Required + URL *string `jsonapi:"attr,url"` // Required + Sha *string `jsonapi:"attr,sha"` // Required + Official *bool `jsonapi:"attr,official,omitempty"` + Deprecated *bool `jsonapi:"attr,deprecated,omitempty"` + DeprecatedReason *string `jsonapi:"attr,deprecated-reason,omitempty"` + Enabled *bool `jsonapi:"attr,enabled,omitempty"` + Beta *bool `jsonapi:"attr,beta,omitempty"` + Archs []*ToolVersionArchitecture `jsonapi:"attr,archs"` } // AdminTerraformVersionUpdateOptions for updating terraform version. // https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/terraform-versions#request-body type AdminTerraformVersionUpdateOptions struct { - Type string `jsonapi:"primary,terraform-versions"` - Version *string `jsonapi:"attr,version,omitempty"` - URL *string `jsonapi:"attr,url,omitempty"` - Sha *string `jsonapi:"attr,sha,omitempty"` - Official *bool `jsonapi:"attr,official,omitempty"` - Deprecated *bool `jsonapi:"attr,deprecated,omitempty"` - DeprecatedReason *string `jsonapi:"attr,deprecated-reason,omitempty"` - Enabled *bool `jsonapi:"attr,enabled,omitempty"` - Beta *bool `jsonapi:"attr,beta,omitempty"` + Type string `jsonapi:"primary,terraform-versions"` + Version *string `jsonapi:"attr,version,omitempty"` + URL *string `jsonapi:"attr,url,omitempty"` + Sha *string `jsonapi:"attr,sha,omitempty"` + Official *bool `jsonapi:"attr,official,omitempty"` + Deprecated *bool `jsonapi:"attr,deprecated,omitempty"` + DeprecatedReason *string `jsonapi:"attr,deprecated-reason,omitempty"` + Enabled *bool `jsonapi:"attr,enabled,omitempty"` + Beta *bool `jsonapi:"attr,beta,omitempty"` + Archs []*ToolVersionArchitecture `jsonapi:"attr,archs"` } // AdminTerraformVersionsList represents a list of terraform versions. @@ -194,7 +205,7 @@ func (a *adminTerraformVersions) Delete(ctx context.Context, id string) error { } func (o AdminTerraformVersionCreateOptions) valid() error { - if (o == AdminTerraformVersionCreateOptions{}) { + if (reflect.DeepEqual(o, AdminTerraformVersionCreateOptions{})) { return ErrRequiredTFVerCreateOps } if !validString(o.Version) { From 57017b9a619b8bb7ee4010d6a4d678d72696ed87 Mon Sep 17 00:00:00 2001 From: Taylor Chaparro Date: Mon, 23 Dec 2024 15:27:07 -0800 Subject: [PATCH 07/41] move team notifications to notification configuration resources, use patched jsonapi --- errors.go | 2 + go.mod | 2 + go.sum | 4 +- helper_test.go | 23 +- notification_configuration.go | 68 ++- ...fication_configuration_integration_test.go | 419 +++++++++++++++- subscription_updater_test.go | 13 - team_notification_configuration.go | 327 ------------ ...fication_configuration_integration_test.go | 464 ------------------ tfe.go | 130 +++-- 10 files changed, 553 insertions(+), 899 deletions(-) delete mode 100644 team_notification_configuration.go delete mode 100644 team_notification_configuration_integration_test.go diff --git a/errors.go b/errors.go index 935f5b361..d1f027fca 100644 --- a/errors.go +++ b/errors.go @@ -145,6 +145,8 @@ var ( ErrInvalidNotificationConfigID = errors.New("invalid value for notification configuration ID") + ErrInvalidNotificationConfigSubscribableChoice = errors.New("invalid value for notification configuration subscribable choice") + ErrInvalidMembership = errors.New("invalid value for membership") ErrInvalidMembershipIDs = errors.New("invalid value for organization membership ids") diff --git a/go.mod b/go.mod index 22ad6cda3..116fc9581 100644 --- a/go.mod +++ b/go.mod @@ -22,3 +22,5 @@ require ( golang.org/x/sys v0.25.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/hashicorp/jsonapi => github.com/notchairmk/jsonapi v0.0.0-20241223221631-b0c6a5b7edd8 diff --git a/go.sum b/go.sum index 1293b8565..89e2b6bcc 100644 --- a/go.sum +++ b/go.sum @@ -16,10 +16,10 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/jsonapi v1.3.1 h1:GtPvnmcWgYwCuDGvYT5VZBHcUyFdq9lSyCzDjn1DdPo= -github.com/hashicorp/jsonapi v1.3.1/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/notchairmk/jsonapi v0.0.0-20241223221631-b0c6a5b7edd8 h1:Nll3UptyKamtMP60oCHnRKI3l/kgadZHKQ6/uLYPyVM= +github.com/notchairmk/jsonapi v0.0.0-20241223221631-b0c6a5b7edd8/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/helper_test.go b/helper_test.go index 9e82d54d4..6d7062426 100644 --- a/helper_test.go +++ b/helper_test.go @@ -615,7 +615,7 @@ func createNotificationConfiguration(t *testing.T, client *Client, w *Workspace, } } -func createTeamNotificationConfiguration(t *testing.T, client *Client, team *Team, options *TeamNotificationConfigurationCreateOptions) (*TeamNotificationConfiguration, func()) { +func createTeamNotificationConfiguration(t *testing.T, client *Client, team *Team, options *NotificationConfigurationCreateOptions) (*NotificationConfiguration, func()) { var tCleanup func() if team == nil { @@ -630,18 +630,19 @@ func createTeamNotificationConfiguration(t *testing.T, client *Client, team *Tea } if options == nil { - options = &TeamNotificationConfigurationCreateOptions{ - DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), - Enabled: Bool(false), - Name: String(randomString(t)), - Token: String(randomString(t)), - URL: String(runTaskURL), - Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + options = &NotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), + Enabled: Bool(false), + Name: String(randomString(t)), + Token: String(randomString(t)), + URL: String(runTaskURL), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: team}, } } ctx := context.Background() - nc, err := client.TeamNotificationConfigurations.Create( + nc, err := client.NotificationConfigurations.Create( ctx, team.ID, *options, @@ -651,10 +652,10 @@ func createTeamNotificationConfiguration(t *testing.T, client *Client, team *Tea } return nc, func() { - if err := client.TeamNotificationConfigurations.Delete(ctx, nc.ID); err != nil { + if err := client.NotificationConfigurations.Delete(ctx, nc.ID); err != nil { t.Errorf("Error destroying team notification configuration! WARNING: Dangling\n"+ "resources may exist! The full error is shown below.\n\n"+ - "TeamNotificationConfiguration: %s\nError: %s", nc.ID, err) + "NotificationConfiguration: %s\nError: %s", nc.ID, err) } if tCleanup != nil { diff --git a/notification_configuration.go b/notification_configuration.go index ece867477..bd74a2d0b 100644 --- a/notification_configuration.go +++ b/notification_configuration.go @@ -81,6 +81,14 @@ type NotificationConfigurationList struct { Items []*NotificationConfiguration } +// NotificationConfigurationSubscribableChoice is a choice type struct that represents the possible values +// within a polymorphic relation. If a value is available, exactly one field +// will be non-nil. +type NotificationConfigurationSubscribableChoice struct { + Team *Team + Workspace *Workspace +} + // NotificationConfiguration represents a Notification Configuration. type NotificationConfiguration struct { ID string `jsonapi:"primary,notification-configurations"` @@ -98,8 +106,11 @@ type NotificationConfiguration struct { EmailAddresses []string `jsonapi:"attr,email-addresses"` // Relations - Subscribable *Workspace `jsonapi:"relation,subscribable"` - EmailUsers []*User `jsonapi:"relation,users"` + // DEPRECATED. The subscribable field is polymorphic. Use NotificationConfigurationSubscribableChoice instead. + Subscribable *Workspace `jsonapi:"relation,subscribable"` + SubscribableChoice *NotificationConfigurationSubscribableChoice `jsonapi:"polyrelation,subscribable"` + + EmailUsers []*User `jsonapi:"relation,users"` } // DeliveryResponse represents a notification configuration delivery response. @@ -116,6 +127,8 @@ type DeliveryResponse struct { // notification configurations. type NotificationConfigurationListOptions struct { ListOptions + + SubscribableChoice *NotificationConfigurationSubscribableChoice `jsonapi:"polyrelation,subscribable"` } // NotificationConfigurationCreateOptions represents the options for @@ -151,6 +164,9 @@ type NotificationConfigurationCreateOptions struct { // Optional: The list of users belonging to the organization that will receive notification emails. EmailUsers []*User `jsonapi:"relation,users,omitempty"` + + // Required: The workspace or team that the notification configuration is associated with. + SubscribableChoice *NotificationConfigurationSubscribableChoice `jsonapi:"polyrelation,subscribable"` } // NotificationConfigurationUpdateOptions represents the options for @@ -186,12 +202,22 @@ type NotificationConfigurationUpdateOptions struct { } // List all the notification configurations associated with a workspace. -func (s *notificationConfigurations) List(ctx context.Context, workspaceID string, options *NotificationConfigurationListOptions) (*NotificationConfigurationList, error) { - if !validStringID(&workspaceID) { - return nil, ErrInvalidWorkspaceID +func (s *notificationConfigurations) List(ctx context.Context, subscribableID string, options *NotificationConfigurationListOptions) (*NotificationConfigurationList, error) { + var u string + if options == nil || options.SubscribableChoice == nil || options.SubscribableChoice.Workspace != nil { + if !validStringID(&subscribableID) { + return nil, ErrInvalidWorkspaceID + } + u = fmt.Sprintf("workspaces/%s/notification-configurations", url.PathEscape(subscribableID)) + } else if options.SubscribableChoice.Team != nil { + if !validStringID(&subscribableID) { + return nil, ErrInvalidTeamID + } + u = fmt.Sprintf("teams/%s/notification-configurations", url.PathEscape(subscribableID)) + } else { + return nil, ErrInvalidNotificationConfigSubscribableChoice } - u := fmt.Sprintf("workspaces/%s/notification-configurations", url.PathEscape(workspaceID)) req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err @@ -207,22 +233,39 @@ func (s *notificationConfigurations) List(ctx context.Context, workspaceID strin } // Create a notification configuration with the given options. -func (s *notificationConfigurations) Create(ctx context.Context, workspaceID string, options NotificationConfigurationCreateOptions) (*NotificationConfiguration, error) { - if !validStringID(&workspaceID) { - return nil, ErrInvalidWorkspaceID - } +func (s *notificationConfigurations) Create(ctx context.Context, subscribableID string, options NotificationConfigurationCreateOptions) (*NotificationConfiguration, error) { if err := options.valid(); err != nil { return nil, err } - u := fmt.Sprintf("workspaces/%s/notification-configurations", url.PathEscape(workspaceID)) + var u string + var subscribableChoice *NotificationConfigurationSubscribableChoice + if options.SubscribableChoice == nil || options.SubscribableChoice.Workspace != nil { + if !validStringID(&subscribableID) { + return nil, ErrInvalidWorkspaceID + } + + u = fmt.Sprintf("workspaces/%s/notification-configurations", url.PathEscape(subscribableID)) + subscribableChoice = &NotificationConfigurationSubscribableChoice{Workspace: &Workspace{ID: subscribableID}} + } else if options.SubscribableChoice != nil && options.SubscribableChoice.Team != nil { + if !validStringID(&subscribableID) { + return nil, ErrInvalidTeamID + } + + u = fmt.Sprintf("teams/%s/notification-configurations", url.PathEscape(subscribableID)) + subscribableChoice = &NotificationConfigurationSubscribableChoice{Team: &Team{ID: subscribableID}} + } else { + return nil, ErrInvalidNotificationConfigSubscribableChoice + } + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } - nc := &NotificationConfiguration{} + nc := &NotificationConfiguration{SubscribableChoice: subscribableChoice} err = req.Do(ctx, nc) + if err != nil { return nil, err } @@ -364,6 +407,7 @@ func validNotificationTriggerType(triggers []NotificationTriggerType) bool { NotificationTriggerAssessmentFailed, NotificationTriggerWorkspaceAutoDestroyReminder, NotificationTriggerWorkspaceAutoDestroyRunResults, + NotificationTriggerChangeRequestCreated, NotificationTriggerAssessmentCheckFailed: continue default: diff --git a/notification_configuration_integration_test.go b/notification_configuration_integration_test.go index 927c08212..d81ede21d 100644 --- a/notification_configuration_integration_test.go +++ b/notification_configuration_integration_test.go @@ -34,13 +34,16 @@ func TestNotificationConfigurationList(t *testing.T) { assert.Contains(t, ncl.Items, ncTest1) assert.Contains(t, ncl.Items, ncTest2) - t.Skip("paging not supported yet in API") - assert.Equal(t, 1, ncl.CurrentPage) - assert.Equal(t, 2, ncl.TotalCount) + assert.Equal(t, 0, ncl.CurrentPage) + assert.Equal(t, 0, ncl.TotalCount) + + assert.NotNil(t, ncl.Items[0].Subscribable) + assert.NotEmpty(t, ncl.Items[0].Subscribable) + assert.NotNil(t, ncl.Items[0].SubscribableChoice.Workspace) + assert.NotEmpty(t, ncl.Items[0].SubscribableChoice.Workspace) }) t.Run("with list options", func(t *testing.T) { - t.Skip("paging not supported yet in API") // Request a page number which is out of range. The result should // be successful, but return no results if the paging options are // properly passed along. @@ -71,6 +74,55 @@ func TestNotificationConfigurationList(t *testing.T) { }) } +func TestNotificationConfigurationList_forTeams(t *testing.T) { + skipUnlessBeta(t) + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + + newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + + tmTest, tmTestCleanup := createTeam(t, client, orgTest) + t.Cleanup(tmTestCleanup) + require.NotNil(t, tmTest) + + ncTest1, ncTestCleanup1 := createTeamNotificationConfiguration(t, client, tmTest, nil) + t.Cleanup(ncTestCleanup1) + ncTest2, ncTestCleanup2 := createTeamNotificationConfiguration(t, client, tmTest, nil) + t.Cleanup(ncTestCleanup2) + + t.Run("with a valid team", func(t *testing.T) { + ncl, err := client.NotificationConfigurations.List( + ctx, + tmTest.ID, + &NotificationConfigurationListOptions{ + SubscribableChoice: &NotificationConfigurationSubscribableChoice{ + Team: tmTest, + }, + }, + ) + require.NoError(t, err) + assert.Contains(t, ncl.Items, ncTest1) + assert.Contains(t, ncl.Items, ncTest2) + }) + + t.Run("without a valid team", func(t *testing.T) { + ncl, err := client.NotificationConfigurations.List( + ctx, + badIdentifier, + &NotificationConfigurationListOptions{ + SubscribableChoice: &NotificationConfigurationSubscribableChoice{ + Team: tmTest, + }, + }, + ) + assert.Nil(t, ncl) + assert.EqualError(t, err, ErrInvalidTeamID.Error()) + }) +} + func TestNotificationConfigurationCreate(t *testing.T) { client := testClient(t) ctx := context.Background() @@ -255,6 +307,156 @@ func TestNotificationConfigurationsCreate_byType(t *testing.T) { } } +func TestNotificationConfigurationCreate_forTeams(t *testing.T) { + skipUnlessBeta(t) + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + + newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + + tmTest, tmTestCleanup := createTeam(t, client, orgTest) + t.Cleanup(tmTestCleanup) + + // Create user to use when testing email destination type + orgMemberTest, orgMemberTestCleanup := createOrganizationMembership(t, client, orgTest) + t.Cleanup(orgMemberTestCleanup) + + // Add user to team + options := TeamMemberAddOptions{ + OrganizationMembershipIDs: []string{orgMemberTest.ID}, + } + err := client.TeamMembers.Add(ctx, tmTest.ID, options) + require.NoError(t, err) + + t.Run("with all required values", func(t *testing.T) { + options := NotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), + Enabled: Bool(false), + Name: String(randomString(t)), + Token: String(randomString(t)), + URL: String("http://example.com"), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest}, + } + nc, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options) + + require.NoError(t, err) + require.NotNil(t, nc) + }) + + t.Run("without a required value", func(t *testing.T) { + options := NotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), + Enabled: Bool(false), + Token: String(randomString(t)), + URL: String("http://example.com"), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest}, + } + nc, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options) + + assert.Nil(t, nc) + assert.EqualError(t, err, ErrRequiredName.Error()) + }) + + t.Run("without a required value URL when destination type is generic", func(t *testing.T) { + options := NotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), + Enabled: Bool(false), + Name: String(randomString(t)), + Token: String(randomString(t)), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest}, + } + + nc, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options) + assert.Nil(t, nc) + assert.Equal(t, err, ErrRequiredURL) + }) + + t.Run("without a required value URL when destination type is slack", func(t *testing.T) { + options := NotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeSlack), + Enabled: Bool(false), + Name: String(randomString(t)), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest}, + } + + nc, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options) + assert.Nil(t, nc) + assert.Equal(t, err, ErrRequiredURL) + }) + + t.Run("without a required value URL when destination type is MS Teams", func(t *testing.T) { + options := NotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeMicrosoftTeams), + Enabled: Bool(false), + Name: String(randomString(t)), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest}, + } + + nc, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options) + assert.Nil(t, nc) + assert.Equal(t, err, ErrRequiredURL) + }) + + t.Run("without a valid team", func(t *testing.T) { + nc, err := client.NotificationConfigurations.Create(ctx, badIdentifier, NotificationConfigurationCreateOptions{ + SubscribableChoice: &NotificationConfigurationSubscribableChoice{ + Team: tmTest, + }, + }) + assert.Nil(t, nc) + assert.EqualError(t, err, ErrInvalidTeamID.Error()) + }) + + t.Run("with an invalid notification trigger", func(t *testing.T) { + options := NotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), + Enabled: Bool(false), + Name: String(randomString(t)), + Token: String(randomString(t)), + URL: String("http://example.com"), + Triggers: []NotificationTriggerType{"the beacons of gondor are lit"}, + SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest}, + } + + nc, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options) + assert.Nil(t, nc) + assert.EqualError(t, err, ErrInvalidNotificationTrigger.Error()) + }) + + t.Run("with email users when destination type is email", func(t *testing.T) { + options := NotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeEmail), + Enabled: Bool(false), + Name: String(randomString(t)), + EmailUsers: []*User{orgMemberTest.User}, + SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest}, + } + + _, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options) + require.NoError(t, err) + }) + + t.Run("without email users when destination type is email", func(t *testing.T) { + options := NotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeEmail), + Enabled: Bool(false), + Name: String(randomString(t)), + SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest}, + } + + _, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options) + require.NoError(t, err) + }) +} + func TestNotificationConfigurationRead(t *testing.T) { client := testClient(t) ctx := context.Background() @@ -279,6 +481,149 @@ func TestNotificationConfigurationRead(t *testing.T) { }) } +func TestNotificationConfigurationRead_forTeams(t *testing.T) { + skipUnlessBeta(t) + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + + newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + + tmTest, tmTestCleanup := createTeam(t, client, orgTest) + t.Cleanup(tmTestCleanup) + + ncTest, ncTestCleanup := createTeamNotificationConfiguration(t, client, tmTest, nil) + t.Cleanup(ncTestCleanup) + + t.Run("with a valid ID", func(t *testing.T) { + nc, err := client.NotificationConfigurations.Read(ctx, ncTest.ID) + require.NoError(t, err) + assert.Equal(t, ncTest.ID, nc.ID) + }) + + t.Run("when the notification configuration does not exist", func(t *testing.T) { + _, err := client.NotificationConfigurations.Read(ctx, "nonexisting") + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the notification configuration ID is invalid", func(t *testing.T) { + _, err := client.NotificationConfigurations.Read(ctx, badIdentifier) + assert.Equal(t, err, ErrInvalidNotificationConfigID) + }) +} + +func TestNotificationConfigurationUpdate_forTeams(t *testing.T) { + skipUnlessBeta(t) + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + + newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + + tmTest, tmTestCleanup := createTeam(t, client, orgTest) + t.Cleanup(tmTestCleanup) + + ncTest, ncTestCleanup := createTeamNotificationConfiguration(t, client, tmTest, nil) + t.Cleanup(ncTestCleanup) + + // Create users to use when testing email destination type + orgMemberTest1, orgMemberTest1Cleanup := createOrganizationMembership(t, client, orgTest) + defer orgMemberTest1Cleanup() + orgMemberTest2, orgMemberTest2Cleanup := createOrganizationMembership(t, client, orgTest) + defer orgMemberTest2Cleanup() + + orgMemberTest1.User = &User{ID: orgMemberTest1.User.ID} + orgMemberTest2.User = &User{ID: orgMemberTest2.User.ID} + + // Add users to team + for _, orgMember := range []*OrganizationMembership{orgMemberTest1, orgMemberTest2} { + options := TeamMemberAddOptions{ + OrganizationMembershipIDs: []string{orgMember.ID}, + } + err := client.TeamMembers.Add(ctx, tmTest.ID, options) + require.NoError(t, err) + } + + options := &NotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeEmail), + Enabled: Bool(false), + Name: String(randomString(t)), + EmailUsers: []*User{orgMemberTest1.User}, + SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest}, + } + ncEmailTest, ncEmailTestCleanup := createTeamNotificationConfiguration(t, client, tmTest, options) + t.Cleanup(ncEmailTestCleanup) + + t.Run("with options", func(t *testing.T) { + options := NotificationConfigurationUpdateOptions{ + Enabled: Bool(true), + Name: String("newName"), + } + + nc, err := client.NotificationConfigurations.Update(ctx, ncTest.ID, options) + require.NoError(t, err) + assert.Equal(t, nc.Enabled, true) + assert.Equal(t, nc.Name, "newName") + }) + + t.Run("with invalid notification trigger", func(t *testing.T) { + options := NotificationConfigurationUpdateOptions{ + Triggers: []NotificationTriggerType{"fly you fools!"}, + } + + nc, err := client.NotificationConfigurations.Update(ctx, ncTest.ID, options) + assert.Nil(t, nc) + assert.EqualError(t, err, ErrInvalidNotificationTrigger.Error()) + }) + + t.Run("with email users when destination type is email", func(t *testing.T) { + options := NotificationConfigurationUpdateOptions{ + Enabled: Bool(true), + Name: String("newName"), + EmailUsers: []*User{orgMemberTest1.User, orgMemberTest2.User}, + } + + nc, err := client.NotificationConfigurations.Update(ctx, ncEmailTest.ID, options) + require.NoError(t, err) + assert.Equal(t, nc.Enabled, true) + assert.Equal(t, nc.Name, "newName") + assert.Contains(t, nc.EmailUsers, orgMemberTest1.User) + assert.Contains(t, nc.EmailUsers, orgMemberTest2.User) + }) + + t.Run("without email users when destination type is email", func(t *testing.T) { + options := NotificationConfigurationUpdateOptions{ + Enabled: Bool(true), + Name: String("newName"), + } + + nc, err := client.NotificationConfigurations.Update(ctx, ncEmailTest.ID, options) + require.NoError(t, err) + assert.Equal(t, nc.Enabled, true) + assert.Equal(t, nc.Name, "newName") + assert.Empty(t, nc.EmailUsers) + }) + + t.Run("without options", func(t *testing.T) { + _, err := client.NotificationConfigurations.Update(ctx, ncTest.ID, NotificationConfigurationUpdateOptions{}) + require.NoError(t, err) + }) + + t.Run("when the notification configuration does not exist", func(t *testing.T) { + _, err := client.NotificationConfigurations.Update(ctx, "nonexisting", NotificationConfigurationUpdateOptions{}) + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the notification configuration ID is invalid", func(t *testing.T) { + _, err := client.NotificationConfigurations.Update(ctx, badIdentifier, NotificationConfigurationUpdateOptions{}) + assert.Equal(t, err, ErrInvalidNotificationConfigID) + }) +} + func TestNotificationConfigurationUpdate(t *testing.T) { client := testClient(t) ctx := context.Background() @@ -404,6 +749,40 @@ func TestNotificationConfigurationDelete(t *testing.T) { }) } +func TestNotificationConfigurationDelete_forTeams(t *testing.T) { + skipUnlessBeta(t) + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + + newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + + tmTest, tmTestCleanup := createTeam(t, client, orgTest) + t.Cleanup(tmTestCleanup) + + ncTest, _ := createTeamNotificationConfiguration(t, client, tmTest, nil) + + t.Run("with a valid ID", func(t *testing.T) { + err := client.NotificationConfigurations.Delete(ctx, ncTest.ID) + require.NoError(t, err) + + _, err = client.NotificationConfigurations.Read(ctx, ncTest.ID) + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the notification configuration does not exist", func(t *testing.T) { + err := client.NotificationConfigurations.Delete(ctx, "nonexisting") + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the notification configuration ID is invalid", func(t *testing.T) { + err := client.NotificationConfigurations.Delete(ctx, badIdentifier) + assert.Equal(t, err, ErrInvalidNotificationConfigID) + }) +} + func TestNotificationConfigurationVerify(t *testing.T) { client := testClient(t) ctx := context.Background() @@ -426,3 +805,35 @@ func TestNotificationConfigurationVerify(t *testing.T) { assert.Equal(t, err, ErrInvalidNotificationConfigID) }) } + +func TestNotificationConfigurationVerify_forTeams(t *testing.T) { + skipUnlessBeta(t) + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + + newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + + tmTest, tmTestCleanup := createTeam(t, client, orgTest) + t.Cleanup(tmTestCleanup) + + ncTest, ncTestCleanup := createTeamNotificationConfiguration(t, client, tmTest, nil) + t.Cleanup(ncTestCleanup) + + t.Run("with a valid ID", func(t *testing.T) { + _, err := client.NotificationConfigurations.Verify(ctx, ncTest.ID) + require.NoError(t, err) + }) + + t.Run("when the notification configuration does not exists", func(t *testing.T) { + _, err := client.NotificationConfigurations.Verify(ctx, "nonexisting") + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the notification configuration ID is invalid", func(t *testing.T) { + _, err := client.NotificationConfigurations.Verify(ctx, badIdentifier) + assert.Equal(t, err, ErrInvalidNotificationConfigID) + }) +} diff --git a/subscription_updater_test.go b/subscription_updater_test.go index 8b677ee17..25554fc1f 100644 --- a/subscription_updater_test.go +++ b/subscription_updater_test.go @@ -72,19 +72,6 @@ func (b *organizationSubscriptionUpdater) WithTrialPlan() *organizationSubscript return b } -func (b *organizationSubscriptionUpdater) WithPlusPlan() *organizationSubscriptionUpdater { - b.planName = "Plus" - - start := time.Now() - ceiling := 1 - managedResourcesLimit := 1000 - - b.updateOpts.ContractStartAt = &start - b.updateOpts.RunsCeiling = &ceiling - b.updateOpts.ContractManagedResourcesLimit = &managedResourcesLimit - return b -} - func (b *organizationSubscriptionUpdater) WithPlusEntitlementPlan() *organizationSubscriptionUpdater { b.planName = "Plus (entitlement)" diff --git a/team_notification_configuration.go b/team_notification_configuration.go deleted file mode 100644 index 68b86319f..000000000 --- a/team_notification_configuration.go +++ /dev/null @@ -1,327 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package tfe - -import ( - "context" - "fmt" - "net/url" - "time" -) - -// Compile-time proof of interface implementation. -var _ TeamNotificationConfigurations = (*teamNotificationConfigurations)(nil) - -// TeamNotificationConfigurations describes all the Team Notification Configuration -// related methods that the Terraform Enterprise API supports. -// -// TFE API docs: -// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/notification-configurations#team-notification-configuration -type TeamNotificationConfigurations interface { - // List all the notification configurations within a team. - List(ctx context.Context, teamID string, options *TeamNotificationConfigurationListOptions) (*TeamNotificationConfigurationList, error) - - // Create a new team notification configuration with the given options. - Create(ctx context.Context, teamID string, options TeamNotificationConfigurationCreateOptions) (*TeamNotificationConfiguration, error) - - // Read a notification configuration by its ID. - Read(ctx context.Context, teamNotificationConfigurationID string) (*TeamNotificationConfiguration, error) - - // Update an existing team notification configuration. - Update(ctx context.Context, teamNotificationConfigurationID string, options TeamNotificationConfigurationUpdateOptions) (*TeamNotificationConfiguration, error) - - // Delete a team notification configuration by its ID. - Delete(ctx context.Context, teamNotificationConfigurationID string) error - - // Verify a team notification configuration by its ID. - Verify(ctx context.Context, teamNotificationConfigurationID string) (*TeamNotificationConfiguration, error) -} - -// teamNotificationConfigurations implements TeamNotificationConfigurations. -type teamNotificationConfigurations struct { - client *Client -} - -// TeamNotificationConfigurationList represents a list of team notification -// configurations. -type TeamNotificationConfigurationList struct { - *Pagination - Items []*TeamNotificationConfiguration -} - -// TeamNotificationConfiguration represents a team notification configuration. -type TeamNotificationConfiguration struct { - ID string `jsonapi:"primary,notification-configurations"` - CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` - DeliveryResponses []*DeliveryResponse `jsonapi:"attr,delivery-responses"` - DestinationType NotificationDestinationType `jsonapi:"attr,destination-type"` - Enabled bool `jsonapi:"attr,enabled"` - Name string `jsonapi:"attr,name"` - Token string `jsonapi:"attr,token"` - Triggers []string `jsonapi:"attr,triggers"` - UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` - URL string `jsonapi:"attr,url"` - - // EmailAddresses is only available for TFE users. It is not available in HCP Terraform. - EmailAddresses []string `jsonapi:"attr,email-addresses"` - - // Relations - Subscribable *Team `jsonapi:"relation,subscribable"` - EmailUsers []*User `jsonapi:"relation,users"` -} - -// TeamNotificationConfigurationListOptions represents the options for listing -// notification configurations. -type TeamNotificationConfigurationListOptions struct { - ListOptions -} - -// TeamNotificationConfigurationCreateOptions represents the options for -// creating a new team notification configuration. -type TeamNotificationConfigurationCreateOptions struct { - // Type is a public field utilized by JSON:API to - // set the resource type via the field tag. - // It is not a user-defined value and does not need to be set. - // https://jsonapi.org/format/#crud-creating - Type string `jsonapi:"primary,notification-configurations"` - - // Required: The destination type of the team notification configuration - DestinationType *NotificationDestinationType `jsonapi:"attr,destination-type"` - - // Required: Whether the team notification configuration should be enabled or not - Enabled *bool `jsonapi:"attr,enabled"` - - // Required: The name of the team notification configuration - Name *string `jsonapi:"attr,name"` - - // Optional: The token of the team notification configuration - Token *string `jsonapi:"attr,token,omitempty"` - - // Optional: The list of events that will trigger team notifications - Triggers []NotificationTriggerType `jsonapi:"attr,triggers,omitempty"` - - // Optional: The URL of the team notification configuration - URL *string `jsonapi:"attr,url,omitempty"` - - // Optional: The list of email addresses that will receive team notification emails. - // EmailAddresses is only available for TFE users. It is not available in HCP Terraform. - EmailAddresses []string `jsonapi:"attr,email-addresses,omitempty"` - - // Optional: The list of users belonging to the organization that will receive - // team notification emails. - EmailUsers []*User `jsonapi:"relation,users,omitempty"` -} - -// TeamNotificationConfigurationUpdateOptions represents the options for -// updating a existing team notification configuration. -type TeamNotificationConfigurationUpdateOptions struct { - // Type is a public field utilized by JSON:API to - // set the resource type via the field tag. - // It is not a user-defined value and does not need to be set. - // https://jsonapi.org/format/#crud-creating - Type string `jsonapi:"primary,notification-configurations"` - - // Optional: Whether the team notification configuration should be enabled or not - Enabled *bool `jsonapi:"attr,enabled,omitempty"` - - // Optional: The name of the team notification configuration - Name *string `jsonapi:"attr,name,omitempty"` - - // Optional: The token of the team notification configuration - Token *string `jsonapi:"attr,token,omitempty"` - - // Optional: The list of events that will trigger team notifications - Triggers []NotificationTriggerType `jsonapi:"attr,triggers,omitempty"` - - // Optional: The URL of the team notification configuration - URL *string `jsonapi:"attr,url,omitempty"` - - // Optional: The list of email addresses that will receive team notification emails. - // EmailAddresses is only available for TFE users. It is not available in HCP Terraform. - EmailAddresses []string `jsonapi:"attr,email-addresses,omitempty"` - - // Optional: The list of users belonging to the organization that will receive - // team notification emails. - EmailUsers []*User `jsonapi:"relation,users,omitempty"` -} - -// List all the notification configurations associated with a team. -func (s *teamNotificationConfigurations) List(ctx context.Context, teamID string, options *TeamNotificationConfigurationListOptions) (*TeamNotificationConfigurationList, error) { - if !validStringID(&teamID) { - return nil, ErrInvalidTeamID - } - - u := fmt.Sprintf("teams/%s/notification-configurations", url.PathEscape(teamID)) - req, err := s.client.NewRequest("GET", u, options) - if err != nil { - return nil, err - } - - ncl := &TeamNotificationConfigurationList{} - err = req.Do(ctx, ncl) - if err != nil { - return nil, err - } - - return ncl, nil -} - -// Create a team notification configuration with the given options. -func (s *teamNotificationConfigurations) Create(ctx context.Context, teamID string, options TeamNotificationConfigurationCreateOptions) (*TeamNotificationConfiguration, error) { - if !validStringID(&teamID) { - return nil, ErrInvalidTeamID - } - if err := options.valid(); err != nil { - return nil, err - } - - u := fmt.Sprintf("teams/%s/notification-configurations", url.PathEscape(teamID)) - req, err := s.client.NewRequest("POST", u, &options) - if err != nil { - return nil, err - } - - nc := &TeamNotificationConfiguration{} - err = req.Do(ctx, nc) - if err != nil { - return nil, err - } - - return nc, nil -} - -// Read a team notification configuration by its ID. -func (s *teamNotificationConfigurations) Read(ctx context.Context, teamNotificationConfigurationID string) (*TeamNotificationConfiguration, error) { - if !validStringID(&teamNotificationConfigurationID) { - return nil, ErrInvalidNotificationConfigID - } - - u := fmt.Sprintf("notification-configurations/%s", url.PathEscape(teamNotificationConfigurationID)) - req, err := s.client.NewRequest("GET", u, nil) - if err != nil { - return nil, err - } - - nc := &TeamNotificationConfiguration{} - err = req.Do(ctx, nc) - if err != nil { - return nil, err - } - - return nc, nil -} - -// Updates a team notification configuration with the given options. -func (s *teamNotificationConfigurations) Update(ctx context.Context, teamNotificationConfigurationID string, options TeamNotificationConfigurationUpdateOptions) (*TeamNotificationConfiguration, error) { - if !validStringID(&teamNotificationConfigurationID) { - return nil, ErrInvalidNotificationConfigID - } - - if err := options.valid(); err != nil { - return nil, err - } - - u := fmt.Sprintf("notification-configurations/%s", url.PathEscape(teamNotificationConfigurationID)) - req, err := s.client.NewRequest("PATCH", u, &options) - if err != nil { - return nil, err - } - - nc := &TeamNotificationConfiguration{} - err = req.Do(ctx, nc) - if err != nil { - return nil, err - } - - return nc, nil -} - -// Delete a team notification configuration by its ID. -func (s *teamNotificationConfigurations) Delete(ctx context.Context, teamNotificationConfigurationID string) error { - if !validStringID(&teamNotificationConfigurationID) { - return ErrInvalidNotificationConfigID - } - - u := fmt.Sprintf("notification-configurations/%s", url.PathEscape(teamNotificationConfigurationID)) - req, err := s.client.NewRequest("DELETE", u, nil) - if err != nil { - return err - } - - return req.Do(ctx, nil) -} - -// Verify a team notification configuration by delivering a verification payload -// to the configured URL. -func (s *teamNotificationConfigurations) Verify(ctx context.Context, teamNotificationConfigurationID string) (*TeamNotificationConfiguration, error) { - if !validStringID(&teamNotificationConfigurationID) { - return nil, ErrInvalidNotificationConfigID - } - - u := fmt.Sprintf( - "notification-configurations/%s/actions/verify", url.PathEscape(teamNotificationConfigurationID)) - req, err := s.client.NewRequest("POST", u, nil) - if err != nil { - return nil, err - } - - nc := &TeamNotificationConfiguration{} - err = req.Do(ctx, nc) - if err != nil { - return nil, err - } - - return nc, nil -} - -func (o TeamNotificationConfigurationCreateOptions) valid() error { - if o.DestinationType == nil { - return ErrRequiredDestinationType - } - if o.Enabled == nil { - return ErrRequiredEnabled - } - if !validString(o.Name) { - return ErrRequiredName - } - - if !validTeamNotificationTriggerType(o.Triggers) { - return ErrInvalidNotificationTrigger - } - - if *o.DestinationType == NotificationDestinationTypeGeneric || - *o.DestinationType == NotificationDestinationTypeSlack || - *o.DestinationType == NotificationDestinationTypeMicrosoftTeams { - if o.URL == nil { - return ErrRequiredURL - } - } - return nil -} - -func (o TeamNotificationConfigurationUpdateOptions) valid() error { - if o.Name != nil && !validString(o.Name) { - return ErrRequiredName - } - - if !validTeamNotificationTriggerType(o.Triggers) { - return ErrInvalidNotificationTrigger - } - - return nil -} - -func validTeamNotificationTriggerType(triggers []NotificationTriggerType) bool { - for _, t := range triggers { - switch t { - case - NotificationTriggerChangeRequestCreated: - continue - default: - return false - } - } - - return true -} diff --git a/team_notification_configuration_integration_test.go b/team_notification_configuration_integration_test.go deleted file mode 100644 index 289da16e6..000000000 --- a/team_notification_configuration_integration_test.go +++ /dev/null @@ -1,464 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package tfe - -import ( - "context" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestTeamNotificationConfigurationList(t *testing.T) { - skipUnlessBeta(t) - client := testClient(t) - ctx := context.Background() - - orgTest, orgTestCleanup := createOrganization(t, client) - t.Cleanup(orgTestCleanup) - - newSubscriptionUpdater(orgTest).WithPlusPlan().Update(t) - - tmTest, tmTestCleanup := createTeam(t, client, orgTest) - t.Cleanup(tmTestCleanup) - require.NotNil(t, tmTest) - - ncTest1, ncTestCleanup1 := createTeamNotificationConfiguration(t, client, tmTest, nil) - t.Cleanup(ncTestCleanup1) - ncTest2, ncTestCleanup2 := createTeamNotificationConfiguration(t, client, tmTest, nil) - t.Cleanup(ncTestCleanup2) - - t.Run("with a valid team", func(t *testing.T) { - ncl, err := client.TeamNotificationConfigurations.List( - ctx, - tmTest.ID, - nil, - ) - require.NoError(t, err) - assert.Contains(t, ncl.Items, ncTest1) - assert.Contains(t, ncl.Items, ncTest2) - - t.Skip("paging not supported yet in API") - assert.Equal(t, 1, ncl.CurrentPage) - assert.Equal(t, 2, ncl.TotalCount) - }) - - t.Run("with list options", func(t *testing.T) { - t.Skip("paging not supported yet in API") - // Request a page number which is out of range. The result should - // be successful, but return no results if the paging options are - // properly passed along. - ncl, err := client.TeamNotificationConfigurations.List( - ctx, - tmTest.ID, - &TeamNotificationConfigurationListOptions{ - ListOptions: ListOptions{ - PageNumber: 999, - PageSize: 100, - }, - }, - ) - require.NoError(t, err) - assert.Empty(t, ncl.Items) - assert.Equal(t, 999, ncl.CurrentPage) - assert.Equal(t, 2, ncl.TotalCount) - }) - - t.Run("without a valid team", func(t *testing.T) { - ncl, err := client.TeamNotificationConfigurations.List( - ctx, - badIdentifier, - nil, - ) - assert.Nil(t, ncl) - assert.EqualError(t, err, ErrInvalidTeamID.Error()) - }) -} - -func TestTeamNotificationConfigurationCreate(t *testing.T) { - skipUnlessBeta(t) - client := testClient(t) - ctx := context.Background() - - orgTest, orgTestCleanup := createOrganization(t, client) - t.Cleanup(orgTestCleanup) - - newSubscriptionUpdater(orgTest).WithPlusPlan().Update(t) - - tmTest, tmTestCleanup := createTeam(t, client, orgTest) - t.Cleanup(tmTestCleanup) - - // Create user to use when testing email destination type - orgMemberTest, orgMemberTestCleanup := createOrganizationMembership(t, client, orgTest) - t.Cleanup(orgMemberTestCleanup) - - // Add user to team - options := TeamMemberAddOptions{ - OrganizationMembershipIDs: []string{orgMemberTest.ID}, - } - err := client.TeamMembers.Add(ctx, tmTest.ID, options) - require.NoError(t, err) - - t.Run("with all required values", func(t *testing.T) { - options := TeamNotificationConfigurationCreateOptions{ - DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), - Enabled: Bool(false), - Name: String(randomString(t)), - Token: String(randomString(t)), - URL: String("http://example.com"), - Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, - } - - _, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) - require.NoError(t, err) - }) - - t.Run("without a required value", func(t *testing.T) { - options := TeamNotificationConfigurationCreateOptions{ - DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), - Enabled: Bool(false), - Token: String(randomString(t)), - URL: String("http://example.com"), - Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, - } - - nc, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) - assert.Nil(t, nc) - assert.EqualError(t, err, ErrRequiredName.Error()) - }) - - t.Run("without a required value URL when destination type is generic", func(t *testing.T) { - options := TeamNotificationConfigurationCreateOptions{ - DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), - Enabled: Bool(false), - Name: String(randomString(t)), - Token: String(randomString(t)), - Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, - } - - nc, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) - assert.Nil(t, nc) - assert.Equal(t, err, ErrRequiredURL) - }) - - t.Run("without a required value URL when destination type is slack", func(t *testing.T) { - options := TeamNotificationConfigurationCreateOptions{ - DestinationType: NotificationDestination(NotificationDestinationTypeSlack), - Enabled: Bool(false), - Name: String(randomString(t)), - Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, - } - - nc, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) - assert.Nil(t, nc) - assert.Equal(t, err, ErrRequiredURL) - }) - - t.Run("without a required value URL when destination type is MS Teams", func(t *testing.T) { - options := TeamNotificationConfigurationCreateOptions{ - DestinationType: NotificationDestination(NotificationDestinationTypeMicrosoftTeams), - Enabled: Bool(false), - Name: String(randomString(t)), - Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, - } - - nc, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) - assert.Nil(t, nc) - assert.Equal(t, err, ErrRequiredURL) - }) - - t.Run("without a valid team", func(t *testing.T) { - nc, err := client.TeamNotificationConfigurations.Create(ctx, badIdentifier, TeamNotificationConfigurationCreateOptions{}) - assert.Nil(t, nc) - assert.EqualError(t, err, ErrInvalidTeamID.Error()) - }) - - t.Run("with an invalid notification trigger", func(t *testing.T) { - options := TeamNotificationConfigurationCreateOptions{ - DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), - Enabled: Bool(false), - Name: String(randomString(t)), - Token: String(randomString(t)), - URL: String("http://example.com"), - Triggers: []NotificationTriggerType{"the beacons of gondor are lit"}, - } - - nc, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) - assert.Nil(t, nc) - assert.EqualError(t, err, ErrInvalidNotificationTrigger.Error()) - }) - - t.Run("with email users when destination type is email", func(t *testing.T) { - options := TeamNotificationConfigurationCreateOptions{ - DestinationType: NotificationDestination(NotificationDestinationTypeEmail), - Enabled: Bool(false), - Name: String(randomString(t)), - EmailUsers: []*User{orgMemberTest.User}, - } - - _, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) - require.NoError(t, err) - }) - - t.Run("without email users when destination type is email", func(t *testing.T) { - options := TeamNotificationConfigurationCreateOptions{ - DestinationType: NotificationDestination(NotificationDestinationTypeEmail), - Enabled: Bool(false), - Name: String(randomString(t)), - } - - _, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) - require.NoError(t, err) - }) -} - -func TestTeamNotificationConfigurationsCreate_byType(t *testing.T) { - skipUnlessBeta(t) - t.Parallel() - - client := testClient(t) - ctx := context.Background() - - orgTest, orgTestCleanup := createOrganization(t, client) - t.Cleanup(orgTestCleanup) - - newSubscriptionUpdater(orgTest).WithPlusPlan().Update(t) - - tmTest, tmTestCleanup := createTeam(t, client, orgTest) - t.Cleanup(tmTestCleanup) - - testCases := []NotificationTriggerType{ - NotificationTriggerChangeRequestCreated, - } - - for _, trigger := range testCases { - trigger := trigger - message := fmt.Sprintf("with trigger %s and all required values", trigger) - - t.Run(message, func(t *testing.T) { - t.Parallel() - options := TeamNotificationConfigurationCreateOptions{ - DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), - Enabled: Bool(false), - Name: String(randomString(t)), - Token: String(randomString(t)), - URL: String("http://example.com"), - Triggers: []NotificationTriggerType{trigger}, - } - - _, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) - require.NoError(t, err) - }) - } -} - -func TestTeamNotificationConfigurationRead(t *testing.T) { - skipUnlessBeta(t) - client := testClient(t) - ctx := context.Background() - - orgTest, orgTestCleanup := createOrganization(t, client) - t.Cleanup(orgTestCleanup) - - newSubscriptionUpdater(orgTest).WithPlusPlan().Update(t) - - tmTest, tmTestCleanup := createTeam(t, client, orgTest) - t.Cleanup(tmTestCleanup) - - ncTest, ncTestCleanup := createTeamNotificationConfiguration(t, client, tmTest, nil) - t.Cleanup(ncTestCleanup) - - t.Run("with a valid ID", func(t *testing.T) { - nc, err := client.TeamNotificationConfigurations.Read(ctx, ncTest.ID) - require.NoError(t, err) - assert.Equal(t, ncTest.ID, nc.ID) - }) - - t.Run("when the notification configuration does not exist", func(t *testing.T) { - _, err := client.TeamNotificationConfigurations.Read(ctx, "nonexisting") - assert.Equal(t, err, ErrResourceNotFound) - }) - - t.Run("when the notification configuration ID is invalid", func(t *testing.T) { - _, err := client.TeamNotificationConfigurations.Read(ctx, badIdentifier) - assert.Equal(t, err, ErrInvalidNotificationConfigID) - }) -} - -func TestTeamNotificationConfigurationUpdate(t *testing.T) { - skipUnlessBeta(t) - client := testClient(t) - ctx := context.Background() - - orgTest, orgTestCleanup := createOrganization(t, client) - t.Cleanup(orgTestCleanup) - - newSubscriptionUpdater(orgTest).WithPlusPlan().Update(t) - - tmTest, tmTestCleanup := createTeam(t, client, orgTest) - t.Cleanup(tmTestCleanup) - - ncTest, ncTestCleanup := createTeamNotificationConfiguration(t, client, tmTest, nil) - t.Cleanup(ncTestCleanup) - - // Create users to use when testing email destination type - orgMemberTest1, orgMemberTest1Cleanup := createOrganizationMembership(t, client, orgTest) - defer orgMemberTest1Cleanup() - orgMemberTest2, orgMemberTest2Cleanup := createOrganizationMembership(t, client, orgTest) - defer orgMemberTest2Cleanup() - - orgMemberTest1.User = &User{ID: orgMemberTest1.User.ID} - orgMemberTest2.User = &User{ID: orgMemberTest2.User.ID} - - // Add users to team - for _, orgMember := range []*OrganizationMembership{orgMemberTest1, orgMemberTest2} { - options := TeamMemberAddOptions{ - OrganizationMembershipIDs: []string{orgMember.ID}, - } - err := client.TeamMembers.Add(ctx, tmTest.ID, options) - require.NoError(t, err) - } - - options := &TeamNotificationConfigurationCreateOptions{ - DestinationType: NotificationDestination(NotificationDestinationTypeEmail), - Enabled: Bool(false), - Name: String(randomString(t)), - EmailUsers: []*User{orgMemberTest1.User}, - } - ncEmailTest, ncEmailTestCleanup := createTeamNotificationConfiguration(t, client, tmTest, options) - t.Cleanup(ncEmailTestCleanup) - - t.Run("with options", func(t *testing.T) { - options := TeamNotificationConfigurationUpdateOptions{ - Enabled: Bool(true), - Name: String("newName"), - } - - nc, err := client.TeamNotificationConfigurations.Update(ctx, ncTest.ID, options) - require.NoError(t, err) - assert.Equal(t, nc.Enabled, true) - assert.Equal(t, nc.Name, "newName") - }) - - t.Run("with invalid notification trigger", func(t *testing.T) { - options := TeamNotificationConfigurationUpdateOptions{ - Triggers: []NotificationTriggerType{"fly you fools!"}, - } - - nc, err := client.TeamNotificationConfigurations.Update(ctx, ncTest.ID, options) - assert.Nil(t, nc) - assert.EqualError(t, err, ErrInvalidNotificationTrigger.Error()) - }) - - t.Run("with email users when destination type is email", func(t *testing.T) { - options := TeamNotificationConfigurationUpdateOptions{ - Enabled: Bool(true), - Name: String("newName"), - EmailUsers: []*User{orgMemberTest1.User, orgMemberTest2.User}, - } - - nc, err := client.TeamNotificationConfigurations.Update(ctx, ncEmailTest.ID, options) - require.NoError(t, err) - assert.Equal(t, nc.Enabled, true) - assert.Equal(t, nc.Name, "newName") - assert.Contains(t, nc.EmailUsers, orgMemberTest1.User) - assert.Contains(t, nc.EmailUsers, orgMemberTest2.User) - }) - - t.Run("without email users when destination type is email", func(t *testing.T) { - options := TeamNotificationConfigurationUpdateOptions{ - Enabled: Bool(true), - Name: String("newName"), - } - - nc, err := client.TeamNotificationConfigurations.Update(ctx, ncEmailTest.ID, options) - require.NoError(t, err) - assert.Equal(t, nc.Enabled, true) - assert.Equal(t, nc.Name, "newName") - assert.Empty(t, nc.EmailUsers) - }) - - t.Run("without options", func(t *testing.T) { - _, err := client.TeamNotificationConfigurations.Update(ctx, ncTest.ID, TeamNotificationConfigurationUpdateOptions{}) - require.NoError(t, err) - }) - - t.Run("when the notification configuration does not exist", func(t *testing.T) { - _, err := client.TeamNotificationConfigurations.Update(ctx, "nonexisting", TeamNotificationConfigurationUpdateOptions{}) - assert.Equal(t, err, ErrResourceNotFound) - }) - - t.Run("when the notification configuration ID is invalid", func(t *testing.T) { - _, err := client.TeamNotificationConfigurations.Update(ctx, badIdentifier, TeamNotificationConfigurationUpdateOptions{}) - assert.Equal(t, err, ErrInvalidNotificationConfigID) - }) -} - -func TestTeamNotificationConfigurationDelete(t *testing.T) { - skipUnlessBeta(t) - client := testClient(t) - ctx := context.Background() - - orgTest, orgTestCleanup := createOrganization(t, client) - t.Cleanup(orgTestCleanup) - - newSubscriptionUpdater(orgTest).WithPlusPlan().Update(t) - - tmTest, tmTestCleanup := createTeam(t, client, orgTest) - t.Cleanup(tmTestCleanup) - - ncTest, _ := createTeamNotificationConfiguration(t, client, tmTest, nil) - - t.Run("with a valid ID", func(t *testing.T) { - err := client.TeamNotificationConfigurations.Delete(ctx, ncTest.ID) - require.NoError(t, err) - - _, err = client.TeamNotificationConfigurations.Read(ctx, ncTest.ID) - assert.Equal(t, err, ErrResourceNotFound) - }) - - t.Run("when the notification configuration does not exist", func(t *testing.T) { - err := client.TeamNotificationConfigurations.Delete(ctx, "nonexisting") - assert.Equal(t, err, ErrResourceNotFound) - }) - - t.Run("when the notification configuration ID is invalid", func(t *testing.T) { - err := client.TeamNotificationConfigurations.Delete(ctx, badIdentifier) - assert.Equal(t, err, ErrInvalidNotificationConfigID) - }) -} - -func TestTeamNotificationConfigurationVerify(t *testing.T) { - skipUnlessBeta(t) - client := testClient(t) - ctx := context.Background() - - orgTest, orgTestCleanup := createOrganization(t, client) - t.Cleanup(orgTestCleanup) - - newSubscriptionUpdater(orgTest).WithPlusPlan().Update(t) - - tmTest, tmTestCleanup := createTeam(t, client, orgTest) - t.Cleanup(tmTestCleanup) - - ncTest, ncTestCleanup := createTeamNotificationConfiguration(t, client, tmTest, nil) - t.Cleanup(ncTestCleanup) - - t.Run("with a valid ID", func(t *testing.T) { - _, err := client.TeamNotificationConfigurations.Verify(ctx, ncTest.ID) - require.NoError(t, err) - }) - - t.Run("when the notification configuration does not exists", func(t *testing.T) { - _, err := client.TeamNotificationConfigurations.Verify(ctx, "nonexisting") - assert.Equal(t, err, ErrResourceNotFound) - }) - - t.Run("when the notification configuration ID is invalid", func(t *testing.T) { - _, err := client.TeamNotificationConfigurations.Verify(ctx, badIdentifier) - assert.Equal(t, err, ErrInvalidNotificationConfigID) - }) -} diff --git a/tfe.go b/tfe.go index c229c26fe..a4f40be3e 100644 --- a/tfe.go +++ b/tfe.go @@ -123,71 +123,70 @@ type Client struct { remoteTFEVersion string appName string - Admin Admin - Agents Agents - AgentPools AgentPools - AgentTokens AgentTokens - Applies Applies - AuditTrails AuditTrails - Comments Comments - ConfigurationVersions ConfigurationVersions - CostEstimates CostEstimates - GHAInstallations GHAInstallations - GPGKeys GPGKeys - NotificationConfigurations NotificationConfigurations - OAuthClients OAuthClients - OAuthTokens OAuthTokens - Organizations Organizations - OrganizationMemberships OrganizationMemberships - OrganizationTags OrganizationTags - OrganizationTokens OrganizationTokens - Plans Plans - PlanExports PlanExports - Policies Policies - PolicyChecks PolicyChecks - PolicyEvaluations PolicyEvaluations - PolicySetOutcomes PolicySetOutcomes - PolicySetParameters PolicySetParameters - PolicySetVersions PolicySetVersions - PolicySets PolicySets - RegistryModules RegistryModules - RegistryNoCodeModules RegistryNoCodeModules - RegistryProviders RegistryProviders - RegistryProviderPlatforms RegistryProviderPlatforms - RegistryProviderVersions RegistryProviderVersions - Runs Runs - RunEvents RunEvents - RunTasks RunTasks - RunTasksIntegration RunTasksIntegration - RunTriggers RunTriggers - SSHKeys SSHKeys - Stacks Stacks - StackConfigurations StackConfigurations - StackDeployments StackDeployments - StackPlans StackPlans - StackPlanOperations StackPlanOperations - StackSources StackSources - StateVersionOutputs StateVersionOutputs - StateVersions StateVersions - TaskResults TaskResults - TaskStages TaskStages - Teams Teams - TeamAccess TeamAccesses - TeamMembers TeamMembers - TeamNotificationConfigurations TeamNotificationConfigurations - TeamProjectAccess TeamProjectAccesses - TeamTokens TeamTokens - TestRuns TestRuns - TestVariables TestVariables - Users Users - UserTokens UserTokens - Variables Variables - VariableSets VariableSets - VariableSetVariables VariableSetVariables - Workspaces Workspaces - WorkspaceResources WorkspaceResources - WorkspaceRunTasks WorkspaceRunTasks - Projects Projects + Admin Admin + Agents Agents + AgentPools AgentPools + AgentTokens AgentTokens + Applies Applies + AuditTrails AuditTrails + Comments Comments + ConfigurationVersions ConfigurationVersions + CostEstimates CostEstimates + GHAInstallations GHAInstallations + GPGKeys GPGKeys + NotificationConfigurations NotificationConfigurations + OAuthClients OAuthClients + OAuthTokens OAuthTokens + Organizations Organizations + OrganizationMemberships OrganizationMemberships + OrganizationTags OrganizationTags + OrganizationTokens OrganizationTokens + Plans Plans + PlanExports PlanExports + Policies Policies + PolicyChecks PolicyChecks + PolicyEvaluations PolicyEvaluations + PolicySetOutcomes PolicySetOutcomes + PolicySetParameters PolicySetParameters + PolicySetVersions PolicySetVersions + PolicySets PolicySets + RegistryModules RegistryModules + RegistryNoCodeModules RegistryNoCodeModules + RegistryProviders RegistryProviders + RegistryProviderPlatforms RegistryProviderPlatforms + RegistryProviderVersions RegistryProviderVersions + Runs Runs + RunEvents RunEvents + RunTasks RunTasks + RunTasksIntegration RunTasksIntegration + RunTriggers RunTriggers + SSHKeys SSHKeys + Stacks Stacks + StackConfigurations StackConfigurations + StackDeployments StackDeployments + StackPlans StackPlans + StackPlanOperations StackPlanOperations + StackSources StackSources + StateVersionOutputs StateVersionOutputs + StateVersions StateVersions + TaskResults TaskResults + TaskStages TaskStages + Teams Teams + TeamAccess TeamAccesses + TeamMembers TeamMembers + TeamProjectAccess TeamProjectAccesses + TeamTokens TeamTokens + TestRuns TestRuns + TestVariables TestVariables + Users Users + UserTokens UserTokens + Variables Variables + VariableSets VariableSets + VariableSetVariables VariableSetVariables + Workspaces Workspaces + WorkspaceResources WorkspaceResources + WorkspaceRunTasks WorkspaceRunTasks + Projects Projects Meta Meta } @@ -502,7 +501,6 @@ func NewClient(cfg *Config) (*Client, error) { client.TaskStages = &taskStages{client: client} client.TeamAccess = &teamAccesses{client: client} client.TeamMembers = &teamMembers{client: client} - client.TeamNotificationConfigurations = &teamNotificationConfigurations{client: client} client.TeamProjectAccess = &teamProjectAccesses{client: client} client.Teams = &teams{client: client} client.TeamTokens = &teamTokens{client: client} From b3dc95db92c41a3d1521087d8b971ea5fa186290 Mon Sep 17 00:00:00 2001 From: natalie-todd Date: Tue, 31 Dec 2024 10:59:24 -0800 Subject: [PATCH 08/41] Add CHANGELOG entry, omitempty, and remove Archs from structs not used by terraform releases --- CHANGELOG.md | 4 ++++ admin_terraform_version.go | 44 ++++++++++++++++++-------------------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d112c9d6..9d7b142c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Unreleased +## Enhancements + +* Add `Archs` field to `AdminTerraformVersionCreateOptions` by @natalie-todd [#1022](https://github.com/hashicorp/go-tfe/pull/1022) + # v1.71.0 ## Enhancements diff --git a/admin_terraform_version.go b/admin_terraform_version.go index 057fa75a0..3ea396848 100644 --- a/admin_terraform_version.go +++ b/admin_terraform_version.go @@ -43,18 +43,17 @@ type adminTerraformVersions struct { // AdminTerraformVersion represents a Terraform Version type AdminTerraformVersion struct { - ID string `jsonapi:"primary,terraform-versions"` - Version string `jsonapi:"attr,version"` - URL string `jsonapi:"attr,url"` - Sha string `jsonapi:"attr,sha"` - Deprecated bool `jsonapi:"attr,deprecated"` - DeprecatedReason *string `jsonapi:"attr,deprecated-reason,omitempty"` - Official bool `jsonapi:"attr,official"` - Enabled bool `jsonapi:"attr,enabled"` - Beta bool `jsonapi:"attr,beta"` - Usage int `jsonapi:"attr,usage"` - CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` - Archs []*ToolVersionArchitecture `jsonapi:"attr,archs"` + ID string `jsonapi:"primary,terraform-versions"` + Version string `jsonapi:"attr,version"` + URL string `jsonapi:"attr,url"` + Sha string `jsonapi:"attr,sha"` + Deprecated bool `jsonapi:"attr,deprecated"` + DeprecatedReason *string `jsonapi:"attr,deprecated-reason,omitempty"` + Official bool `jsonapi:"attr,official"` + Enabled bool `jsonapi:"attr,enabled"` + Beta bool `jsonapi:"attr,beta"` + Usage int `jsonapi:"attr,usage"` + CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` } type ToolVersionArchitecture struct { @@ -88,22 +87,21 @@ type AdminTerraformVersionCreateOptions struct { DeprecatedReason *string `jsonapi:"attr,deprecated-reason,omitempty"` Enabled *bool `jsonapi:"attr,enabled,omitempty"` Beta *bool `jsonapi:"attr,beta,omitempty"` - Archs []*ToolVersionArchitecture `jsonapi:"attr,archs"` + Archs []*ToolVersionArchitecture `jsonapi:"attr,archs,omitempty"` } // AdminTerraformVersionUpdateOptions for updating terraform version. // https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/terraform-versions#request-body type AdminTerraformVersionUpdateOptions struct { - Type string `jsonapi:"primary,terraform-versions"` - Version *string `jsonapi:"attr,version,omitempty"` - URL *string `jsonapi:"attr,url,omitempty"` - Sha *string `jsonapi:"attr,sha,omitempty"` - Official *bool `jsonapi:"attr,official,omitempty"` - Deprecated *bool `jsonapi:"attr,deprecated,omitempty"` - DeprecatedReason *string `jsonapi:"attr,deprecated-reason,omitempty"` - Enabled *bool `jsonapi:"attr,enabled,omitempty"` - Beta *bool `jsonapi:"attr,beta,omitempty"` - Archs []*ToolVersionArchitecture `jsonapi:"attr,archs"` + Type string `jsonapi:"primary,terraform-versions"` + Version *string `jsonapi:"attr,version,omitempty"` + URL *string `jsonapi:"attr,url,omitempty"` + Sha *string `jsonapi:"attr,sha,omitempty"` + Official *bool `jsonapi:"attr,official,omitempty"` + Deprecated *bool `jsonapi:"attr,deprecated,omitempty"` + DeprecatedReason *string `jsonapi:"attr,deprecated-reason,omitempty"` + Enabled *bool `jsonapi:"attr,enabled,omitempty"` + Beta *bool `jsonapi:"attr,beta,omitempty"` } // AdminTerraformVersionsList represents a list of terraform versions. From b8504626a96a427494d1f345efc6dad6a0437e13 Mon Sep 17 00:00:00 2001 From: natalie-todd Date: Tue, 31 Dec 2024 11:36:29 -0800 Subject: [PATCH 09/41] Add Archs to tests --- admin_terraform_version_integration_test.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/admin_terraform_version_integration_test.go b/admin_terraform_version_integration_test.go index bec375a5e..252162a7b 100644 --- a/admin_terraform_version_integration_test.go +++ b/admin_terraform_version_integration_test.go @@ -103,15 +103,22 @@ func TestAdminTerraformVersions_CreateDelete(t *testing.T) { version := genSafeRandomTerraformVersion() t.Run("with valid options", func(t *testing.T) { + sha := String(genSha(t)) opts := AdminTerraformVersionCreateOptions{ Version: String(version), URL: String("https://www.hashicorp.com"), - Sha: String(genSha(t)), + Sha: sha, Deprecated: Bool(true), DeprecatedReason: String("Test Reason"), Official: Bool(false), Enabled: Bool(false), Beta: Bool(false), + Archs: []*ToolVersionArchitecture{{ + URL: "https://www.hashicorp.com", + Sha: *sha, + OS: "linux", + Arch: "amd64", + }}, } tfv, err := client.Admin.TerraformVersions.Create(ctx, opts) require.NoError(t, err) @@ -170,6 +177,7 @@ func TestAdminTerraformVersions_ReadUpdate(t *testing.T) { t.Run("reads and updates", func(t *testing.T) { version := genSafeRandomTerraformVersion() + sha := String(genSha(t)) opts := AdminTerraformVersionCreateOptions{ Version: String(version), URL: String("https://www.hashicorp.com"), @@ -179,6 +187,12 @@ func TestAdminTerraformVersions_ReadUpdate(t *testing.T) { DeprecatedReason: String("Test Reason"), Enabled: Bool(false), Beta: Bool(false), + Archs: []*ToolVersionArchitecture{{ + URL: "https://www.hashicorp.com", + Sha: *sha, + OS: "linux", + Arch: "amd64", + }}, } tfv, err := client.Admin.TerraformVersions.Create(ctx, opts) require.NoError(t, err) From bd9a54b976d01e13737bd336d5cc08d14a03e141 Mon Sep 17 00:00:00 2001 From: Taylor Chaparro Date: Thu, 2 Jan 2025 12:15:20 -0800 Subject: [PATCH 10/41] update NC interface signatures, omit deprecated subscribable, backfill subscribable --- notification_configuration.go | 46 +++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/notification_configuration.go b/notification_configuration.go index bd74a2d0b..64e5b5b3b 100644 --- a/notification_configuration.go +++ b/notification_configuration.go @@ -20,10 +20,10 @@ var _ NotificationConfigurations = (*notificationConfigurations)(nil) // https://developer.hashicorp.com/terraform/cloud-docs/api-docs/notification-configurations type NotificationConfigurations interface { // List all the notification configurations within a workspace. - List(ctx context.Context, workspaceID string, options *NotificationConfigurationListOptions) (*NotificationConfigurationList, error) + List(ctx context.Context, subscribableID string, options *NotificationConfigurationListOptions) (*NotificationConfigurationList, error) // Create a new notification configuration with the given options. - Create(ctx context.Context, workspaceID string, options NotificationConfigurationCreateOptions) (*NotificationConfiguration, error) + Create(ctx context.Context, subscribableID string, options NotificationConfigurationCreateOptions) (*NotificationConfiguration, error) // Read a notification configuration by its ID. Read(ctx context.Context, notificationConfigurationID string) (*NotificationConfiguration, error) @@ -107,7 +107,7 @@ type NotificationConfiguration struct { // Relations // DEPRECATED. The subscribable field is polymorphic. Use NotificationConfigurationSubscribableChoice instead. - Subscribable *Workspace `jsonapi:"relation,subscribable"` + Subscribable *Workspace `jsonapi:"relation,subscribable,omitempty"` SubscribableChoice *NotificationConfigurationSubscribableChoice `jsonapi:"polyrelation,subscribable"` EmailUsers []*User `jsonapi:"relation,users"` @@ -128,7 +128,7 @@ type DeliveryResponse struct { type NotificationConfigurationListOptions struct { ListOptions - SubscribableChoice *NotificationConfigurationSubscribableChoice `jsonapi:"polyrelation,subscribable"` + SubscribableChoice *NotificationConfigurationSubscribableChoice } // NotificationConfigurationCreateOptions represents the options for @@ -204,18 +204,28 @@ type NotificationConfigurationUpdateOptions struct { // List all the notification configurations associated with a workspace. func (s *notificationConfigurations) List(ctx context.Context, subscribableID string, options *NotificationConfigurationListOptions) (*NotificationConfigurationList, error) { var u string - if options == nil || options.SubscribableChoice == nil || options.SubscribableChoice.Workspace != nil { - if !validStringID(&subscribableID) { - return nil, ErrInvalidWorkspaceID + if options == nil { + options = &NotificationConfigurationListOptions{ + SubscribableChoice: &NotificationConfigurationSubscribableChoice{ + Workspace: &Workspace{ID: subscribableID}, + }, } - u = fmt.Sprintf("workspaces/%s/notification-configurations", url.PathEscape(subscribableID)) - } else if options.SubscribableChoice.Team != nil { + } else if options.SubscribableChoice == nil { + options.SubscribableChoice = &NotificationConfigurationSubscribableChoice{ + Workspace: &Workspace{ID: subscribableID}, + } + } + + if options.SubscribableChoice.Team != nil { if !validStringID(&subscribableID) { return nil, ErrInvalidTeamID } u = fmt.Sprintf("teams/%s/notification-configurations", url.PathEscape(subscribableID)) } else { - return nil, ErrInvalidNotificationConfigSubscribableChoice + if !validStringID(&subscribableID) { + return nil, ErrInvalidWorkspaceID + } + u = fmt.Sprintf("workspaces/%s/notification-configurations", url.PathEscape(subscribableID)) } req, err := s.client.NewRequest("GET", u, options) @@ -229,6 +239,10 @@ func (s *notificationConfigurations) List(ctx context.Context, subscribableID st return nil, err } + for i := range ncl.Items { + backfillDeprecatedSubscribable(ncl.Items[i]) + } + return ncl, nil } @@ -270,6 +284,8 @@ func (s *notificationConfigurations) Create(ctx context.Context, subscribableID return nil, err } + backfillDeprecatedSubscribable(nc) + return nc, nil } @@ -394,6 +410,16 @@ func (o NotificationConfigurationUpdateOptions) valid() error { return nil } +func backfillDeprecatedSubscribable(notification *NotificationConfiguration) { + if notification.Subscribable != nil || notification.SubscribableChoice == nil { + return + } + + if notification.SubscribableChoice.Workspace != nil { + notification.Subscribable = notification.SubscribableChoice.Workspace + } +} + func validNotificationTriggerType(triggers []NotificationTriggerType) bool { for _, t := range triggers { switch t { From 22a04151b45d1c336de26f9a61105c1e3cc070c5 Mon Sep 17 00:00:00 2001 From: Taylor Chaparro Date: Thu, 2 Jan 2025 12:22:08 -0800 Subject: [PATCH 11/41] update notification configuration mock signatures --- mocks/notification_configuration_mocks.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/mocks/notification_configuration_mocks.go b/mocks/notification_configuration_mocks.go index c34212e83..5fb999a40 100644 --- a/mocks/notification_configuration_mocks.go +++ b/mocks/notification_configuration_mocks.go @@ -41,18 +41,18 @@ func (m *MockNotificationConfigurations) EXPECT() *MockNotificationConfiguration } // Create mocks base method. -func (m *MockNotificationConfigurations) Create(ctx context.Context, workspaceID string, options tfe.NotificationConfigurationCreateOptions) (*tfe.NotificationConfiguration, error) { +func (m *MockNotificationConfigurations) Create(ctx context.Context, subscribableID string, options tfe.NotificationConfigurationCreateOptions) (*tfe.NotificationConfiguration, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Create", ctx, workspaceID, options) + ret := m.ctrl.Call(m, "Create", ctx, subscribableID, options) ret0, _ := ret[0].(*tfe.NotificationConfiguration) ret1, _ := ret[1].(error) return ret0, ret1 } // Create indicates an expected call of Create. -func (mr *MockNotificationConfigurationsMockRecorder) Create(ctx, workspaceID, options any) *gomock.Call { +func (mr *MockNotificationConfigurationsMockRecorder) Create(ctx, subscribableID, options any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockNotificationConfigurations)(nil).Create), ctx, workspaceID, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockNotificationConfigurations)(nil).Create), ctx, subscribableID, options) } // Delete mocks base method. @@ -70,18 +70,18 @@ func (mr *MockNotificationConfigurationsMockRecorder) Delete(ctx, notificationCo } // List mocks base method. -func (m *MockNotificationConfigurations) List(ctx context.Context, workspaceID string, options *tfe.NotificationConfigurationListOptions) (*tfe.NotificationConfigurationList, error) { +func (m *MockNotificationConfigurations) List(ctx context.Context, subscribableID string, options *tfe.NotificationConfigurationListOptions) (*tfe.NotificationConfigurationList, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "List", ctx, workspaceID, options) + ret := m.ctrl.Call(m, "List", ctx, subscribableID, options) ret0, _ := ret[0].(*tfe.NotificationConfigurationList) ret1, _ := ret[1].(error) return ret0, ret1 } // List indicates an expected call of List. -func (mr *MockNotificationConfigurationsMockRecorder) List(ctx, workspaceID, options any) *gomock.Call { +func (mr *MockNotificationConfigurationsMockRecorder) List(ctx, subscribableID, options any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockNotificationConfigurations)(nil).List), ctx, workspaceID, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockNotificationConfigurations)(nil).List), ctx, subscribableID, options) } // Read mocks base method. From e4e84f526739b7ee042afc1504a83418a96f2ef8 Mon Sep 17 00:00:00 2001 From: Taylor Chaparro Date: Thu, 2 Jan 2025 13:18:06 -0800 Subject: [PATCH 12/41] fix tests, update run triggers to handle deprecated in the same way --- notification_configuration.go | 4 ++++ notification_configuration_integration_test.go | 2 +- run_trigger.go | 18 +++++++++++++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/notification_configuration.go b/notification_configuration.go index 64e5b5b3b..d0a5706de 100644 --- a/notification_configuration.go +++ b/notification_configuration.go @@ -307,6 +307,8 @@ func (s *notificationConfigurations) Read(ctx context.Context, notificationConfi return nil, err } + backfillDeprecatedSubscribable(nc) + return nc, nil } @@ -332,6 +334,8 @@ func (s *notificationConfigurations) Update(ctx context.Context, notificationCon return nil, err } + backfillDeprecatedSubscribable(nc) + return nc, nil } diff --git a/notification_configuration_integration_test.go b/notification_configuration_integration_test.go index d81ede21d..88f30fa64 100644 --- a/notification_configuration_integration_test.go +++ b/notification_configuration_integration_test.go @@ -70,7 +70,7 @@ func TestNotificationConfigurationList(t *testing.T) { nil, ) assert.Nil(t, ncl) - assert.EqualError(t, err, ErrInvalidWorkspaceID.Error()) + assert.EqualError(t, err, ErrRequiredDestinationType.Error()) }) } diff --git a/run_trigger.go b/run_trigger.go index e002271fa..f59671935 100644 --- a/run_trigger.go +++ b/run_trigger.go @@ -57,7 +57,7 @@ type RunTrigger struct { SourceableName string `jsonapi:"attr,sourceable-name"` WorkspaceName string `jsonapi:"attr,workspace-name"` // DEPRECATED. The sourceable field is polymorphic. Use SourceableChoice instead. - Sourceable *Workspace `jsonapi:"relation,sourceable"` + Sourceable *Workspace `jsonapi:"relation,sourceable,omitempty"` SourceableChoice *SourceableChoice `jsonapi:"polyrelation,sourceable"` Workspace *Workspace `jsonapi:"relation,workspace"` } @@ -121,6 +121,10 @@ func (s *runTriggers) List(ctx context.Context, workspaceID string, options *Run return nil, err } + for i := range rtl.Items { + backfillDeprecatedSourceable(rtl.Items[i]) + } + return rtl, nil } @@ -145,6 +149,8 @@ func (s *runTriggers) Create(ctx context.Context, workspaceID string, options Ru return nil, err } + backfillDeprecatedSourceable(rt) + return rt, nil } @@ -166,6 +172,8 @@ func (s *runTriggers) Read(ctx context.Context, runTriggerID string) (*RunTrigge return nil, err } + backfillDeprecatedSourceable(rt) + return rt, nil } @@ -203,6 +211,14 @@ func (o *RunTriggerListOptions) valid() error { return nil } +func backfillDeprecatedSourceable(runTrigger *RunTrigger) { + if runTrigger.Sourceable != nil || runTrigger.SourceableChoice == nil { + return + } + + runTrigger.Sourceable = runTrigger.SourceableChoice.Workspace +} + func validateRunTriggerFilterParam(filterParam RunTriggerFilterOp, includeParams []RunTriggerIncludeOpt) error { switch filterParam { case RunTriggerOutbound, RunTriggerInbound: From fc1e263d45ac960b8ffe0628c4633823fcb7f728 Mon Sep 17 00:00:00 2001 From: Taylor Chaparro Date: Thu, 2 Jan 2025 15:00:26 -0800 Subject: [PATCH 13/41] move create validation to valid receiver --- errors.go | 2 -- notification_configuration.go | 36 +++++++++---------- ...fication_configuration_integration_test.go | 2 +- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/errors.go b/errors.go index d1f027fca..935f5b361 100644 --- a/errors.go +++ b/errors.go @@ -145,8 +145,6 @@ var ( ErrInvalidNotificationConfigID = errors.New("invalid value for notification configuration ID") - ErrInvalidNotificationConfigSubscribableChoice = errors.New("invalid value for notification configuration subscribable choice") - ErrInvalidMembership = errors.New("invalid value for membership") ErrInvalidMembershipIDs = errors.New("invalid value for organization membership ids") diff --git a/notification_configuration.go b/notification_configuration.go index d0a5706de..87a7c60b1 100644 --- a/notification_configuration.go +++ b/notification_configuration.go @@ -248,28 +248,18 @@ func (s *notificationConfigurations) List(ctx context.Context, subscribableID st // Create a notification configuration with the given options. func (s *notificationConfigurations) Create(ctx context.Context, subscribableID string, options NotificationConfigurationCreateOptions) (*NotificationConfiguration, error) { - if err := options.valid(); err != nil { - return nil, err - } - var u string var subscribableChoice *NotificationConfigurationSubscribableChoice - if options.SubscribableChoice == nil || options.SubscribableChoice.Workspace != nil { - if !validStringID(&subscribableID) { - return nil, ErrInvalidWorkspaceID - } - + if options.SubscribableChoice == nil || options.SubscribableChoice.Team == nil { u = fmt.Sprintf("workspaces/%s/notification-configurations", url.PathEscape(subscribableID)) - subscribableChoice = &NotificationConfigurationSubscribableChoice{Workspace: &Workspace{ID: subscribableID}} - } else if options.SubscribableChoice != nil && options.SubscribableChoice.Team != nil { - if !validStringID(&subscribableID) { - return nil, ErrInvalidTeamID - } - - u = fmt.Sprintf("teams/%s/notification-configurations", url.PathEscape(subscribableID)) - subscribableChoice = &NotificationConfigurationSubscribableChoice{Team: &Team{ID: subscribableID}} + options.SubscribableChoice = &NotificationConfigurationSubscribableChoice{Workspace: &Workspace{ID: subscribableID}} } else { - return nil, ErrInvalidNotificationConfigSubscribableChoice + u = fmt.Sprintf("teams/%s/notification-configurations", url.PathEscape(subscribableID)) + options.SubscribableChoice = &NotificationConfigurationSubscribableChoice{Team: &Team{ID: subscribableID}} + } + + if err := options.valid(); err != nil { + return nil, err } req, err := s.client.NewRequest("POST", u, &options) @@ -378,6 +368,16 @@ func (s *notificationConfigurations) Verify(ctx context.Context, notificationCon } func (o NotificationConfigurationCreateOptions) valid() error { + if o.SubscribableChoice == nil || o.SubscribableChoice.Workspace != nil { + if !validStringID(&o.SubscribableChoice.Workspace.ID) { + return ErrInvalidWorkspaceID + } + } else { + if !validStringID(&o.SubscribableChoice.Team.ID) { + return ErrInvalidTeamID + } + } + if o.DestinationType == nil { return ErrRequiredDestinationType } diff --git a/notification_configuration_integration_test.go b/notification_configuration_integration_test.go index 88f30fa64..d81ede21d 100644 --- a/notification_configuration_integration_test.go +++ b/notification_configuration_integration_test.go @@ -70,7 +70,7 @@ func TestNotificationConfigurationList(t *testing.T) { nil, ) assert.Nil(t, ncl) - assert.EqualError(t, err, ErrRequiredDestinationType.Error()) + assert.EqualError(t, err, ErrInvalidWorkspaceID.Error()) }) } From 56eeb74ec01f2c9c29487ce23f2696f34b6d32bd Mon Sep 17 00:00:00 2001 From: natalie-todd Date: Fri, 3 Jan 2025 11:37:47 -0800 Subject: [PATCH 14/41] Add validation enforcing at least one valid arch or valid URL and SHA and update changelog --- CHANGELOG.md | 4 ++-- admin_terraform_version.go | 25 ++++++++++++++++----- admin_terraform_version_integration_test.go | 8 +++---- errors.go | 2 ++ 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 279cc97a7..7aa40a053 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,8 @@ ## Enhancements * Add support for project level auto destroy settings @simonxmh [#1011](https://github.com/hashicorp/go-tfe/pull/1011) -* Add `Archs` field to `AdminTerraformVersionCreateOptions` by @natalie-todd [#1022](https://github.com/hashicorp/go-tfe/pull/1022) - +* Add BETA support for Linux arm64 agents, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users @natalie-todd [#1022](https://github.com/hashicorp/go-tfe/pull/1022) + # v1.71.0 ## Enhancements diff --git a/admin_terraform_version.go b/admin_terraform_version.go index 3ea396848..78d75ce14 100644 --- a/admin_terraform_version.go +++ b/admin_terraform_version.go @@ -14,6 +14,12 @@ import ( // Compile-time proof of interface implementation. var _ AdminTerraformVersions = (*adminTerraformVersions)(nil) +const ( + linux = "linux" + amd64 = "amd64" + arm64 = "arm64" +) + // AdminTerraformVersions describes all the admin terraform versions related methods that // the Terraform Enterprise API supports. // Note that admin terraform versions are only available in Terraform Enterprise. @@ -209,12 +215,19 @@ func (o AdminTerraformVersionCreateOptions) valid() error { if !validString(o.Version) { return ErrRequiredVersion } - if !validString(o.URL) { - return ErrRequiredURL + if !o.validArch() && (!validString(o.URL) || !validString(o.Sha)) { + return ErrRequiredArchOrURLAndSha } - if !validString(o.Sha) { - return ErrRequiredSha - } - return nil } + +func (o AdminTerraformVersionCreateOptions) validArch() bool { + var valid bool + for _, a := range o.Archs { + valid = validString(&a.URL) && validString(&a.Sha) && a.OS == linux && (a.Arch == amd64 || a.Arch == arm64) + if valid { + break + } + } + return valid +} diff --git a/admin_terraform_version_integration_test.go b/admin_terraform_version_integration_test.go index 252162a7b..39e64006b 100644 --- a/admin_terraform_version_integration_test.go +++ b/admin_terraform_version_integration_test.go @@ -116,8 +116,8 @@ func TestAdminTerraformVersions_CreateDelete(t *testing.T) { Archs: []*ToolVersionArchitecture{{ URL: "https://www.hashicorp.com", Sha: *sha, - OS: "linux", - Arch: "amd64", + OS: linux, + Arch: amd64, }}, } tfv, err := client.Admin.TerraformVersions.Create(ctx, opts) @@ -190,8 +190,8 @@ func TestAdminTerraformVersions_ReadUpdate(t *testing.T) { Archs: []*ToolVersionArchitecture{{ URL: "https://www.hashicorp.com", Sha: *sha, - OS: "linux", - Arch: "amd64", + OS: linux, + Arch: amd64, }}, } tfv, err := client.Admin.TerraformVersions.Create(ctx, opts) diff --git a/errors.go b/errors.go index 935f5b361..fdb68e53f 100644 --- a/errors.go +++ b/errors.go @@ -274,6 +274,8 @@ var ( ErrRequiredURL = errors.New("url is required") + ErrRequiredArchOrURLAndSha = errors.New("valid arch or url and sha is required") + ErrRequiredAPIURL = errors.New("API URL is required") ErrRequiredHTTPURL = errors.New("HTTP URL is required") From 0c41524ea9f76d539e239891b82272bc57c88bad Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Thu, 26 Dec 2024 13:12:20 -0500 Subject: [PATCH 15/41] Add DeleteAllTagBindings to projects --- project.go | 27 +++++++++++++++++++++++++++ projects_integration_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/project.go b/project.go index faf1da8c5..433549f3d 100644 --- a/project.go +++ b/project.go @@ -44,6 +44,9 @@ type Projects interface { // AddTagBindings adds or modifies the value of existing tag binding keys for a project. AddTagBindings(ctx context.Context, projectID string, options ProjectAddTagBindingsOptions) ([]*TagBinding, error) + + // DeleteAllTagBindings removes all existing tag bindings for a project. + DeleteAllTagBindings(ctx context.Context, projectID string) error } // projects implements Projects @@ -326,6 +329,30 @@ func (s *projects) Delete(ctx context.Context, projectID string) error { return req.Do(ctx, nil) } +// Delete all tag bindings associated with a project. +func (s *projects) DeleteAllTagBindings(ctx context.Context, projectID string) error { + if !validStringID(&projectID) { + return ErrInvalidProjectID + } + + type aliasOpts struct { + Type string `jsonapi:"primary,projects"` + TagBindings []*TagBinding `jsonapi:"relation,tag-bindings"` + } + + opts := &aliasOpts{ + TagBindings: []*TagBinding{}, + } + + u := fmt.Sprintf("projects/%s", url.PathEscape(projectID)) + req, err := s.client.NewRequest("PATCH", u, opts) + if err != nil { + return err + } + + return req.Do(ctx, nil) +} + func (o ProjectCreateOptions) valid() error { if !validString(&o.Name) { return ErrRequiredName diff --git a/projects_integration_test.go b/projects_integration_test.go index b643aad9e..b84a9e869 100644 --- a/projects_integration_test.go +++ b/projects_integration_test.go @@ -380,6 +380,33 @@ func TestProjectsAddTagBindings(t *testing.T) { }) } +func TestProjects_DeleteAllTagBindings(t *testing.T) { + skipUnlessBeta(t) + + client := testClient(t) + ctx := context.Background() + + pTest, wCleanup := createProject(t, client, nil) + t.Cleanup(wCleanup) + + tagBindings := []*TagBinding{ + {Key: "foo", Value: "bar"}, + {Key: "baz", Value: "qux"}, + } + + _, err := client.Projects.AddTagBindings(ctx, pTest.ID, ProjectAddTagBindingsOptions{ + TagBindings: tagBindings, + }) + require.NoError(t, err) + + err = client.Projects.DeleteAllTagBindings(ctx, pTest.ID) + require.NoError(t, err) + + bindings, err := client.Projects.ListTagBindings(ctx, pTest.ID) + require.NoError(t, err) + require.Empty(t, bindings) +} + func TestProjectsDelete(t *testing.T) { client := testClient(t) ctx := context.Background() From eb3b7b9ea38d0b4086a6402f1538391504c7a034 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Thu, 26 Dec 2024 13:22:42 -0500 Subject: [PATCH 16/41] Add DeleteAllTagBindings to workspaces --- workspace.go | 29 +++++++++++++++++++++++++++++ workspace_integration_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/workspace.go b/workspace.go index 40ee48d71..be1004686 100644 --- a/workspace.go +++ b/workspace.go @@ -141,6 +141,9 @@ type Workspaces interface { // AddTagBindings adds or modifies the value of existing tag binding keys for a workspace. AddTagBindings(ctx context.Context, workspaceID string, options WorkspaceAddTagBindingsOptions) ([]*TagBinding, error) + + // DeleteAllTagBindings removes all tag bindings for a workspace. + DeleteAllTagBindings(ctx context.Context, workspaceID string) error } // workspaces implements Workspaces. @@ -829,6 +832,32 @@ func (s *workspaces) AddTagBindings(ctx context.Context, workspaceID string, opt return response.Items, err } +// DeleteAllTagBindings removes all tag bindings associated with a workspace. +// This method will not remove any inherited tag bindings, which must be +// explicitly removed from the parent project. +func (s *workspaces) DeleteAllTagBindings(ctx context.Context, workspaceID string) error { + if !validStringID(&workspaceID) { + return ErrInvalidWorkspaceID + } + + type aliasOpts struct { + Type string `jsonapi:"primary,workspaces"` + TagBindings []*TagBinding `jsonapi:"relation,tag-bindings"` + } + + opts := &aliasOpts{ + TagBindings: []*TagBinding{}, + } + + u := fmt.Sprintf("workspaces/%s", url.PathEscape(workspaceID)) + req, err := s.client.NewRequest("PATCH", u, opts) + if err != nil { + return err + } + + return req.Do(ctx, nil) +} + // Create is used to create a new workspace. func (s *workspaces) Create(ctx context.Context, organization string, options WorkspaceCreateOptions) (*Workspace, error) { if !validStringID(&organization) { diff --git a/workspace_integration_test.go b/workspace_integration_test.go index 28e34c61c..04b28c543 100644 --- a/workspace_integration_test.go +++ b/workspace_integration_test.go @@ -1237,6 +1237,33 @@ func TestWorkspacesAddTagBindings(t *testing.T) { }) } +func TestWorkspaces_DeleteAllTagBindings(t *testing.T) { + skipUnlessBeta(t) + + client := testClient(t) + ctx := context.Background() + + wTest, wCleanup := createWorkspace(t, client, nil) + t.Cleanup(wCleanup) + + tagBindings := []*TagBinding{ + {Key: "foo", Value: "bar"}, + {Key: "baz", Value: "qux"}, + } + + _, err := client.Workspaces.AddTagBindings(ctx, wTest.ID, WorkspaceAddTagBindingsOptions{ + TagBindings: tagBindings, + }) + require.NoError(t, err) + + err = client.Workspaces.DeleteAllTagBindings(ctx, wTest.ID) + require.NoError(t, err) + + bindings, err := client.Workspaces.ListTagBindings(ctx, wTest.ID) + require.NoError(t, err) + require.Empty(t, bindings) +} + func TestWorkspacesUpdate(t *testing.T) { client := testClient(t) ctx := context.Background() From 6303726c182dc747bbd1a05f1df197bc8d7e2fe8 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Thu, 26 Dec 2024 13:30:34 -0500 Subject: [PATCH 17/41] Update changelog entry --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aa40a053..2490c133a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ * Add support for project level auto destroy settings @simonxmh [#1011](https://github.com/hashicorp/go-tfe/pull/1011) * Add BETA support for Linux arm64 agents, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users @natalie-todd [#1022](https://github.com/hashicorp/go-tfe/pull/1022) - +* Adds support to delete all tag bindings on either a project or workspace by @sebasslash [#1023](https://github.com/hashicorp/go-tfe/pull/1023) + # v1.71.0 ## Enhancements From e1ea9cbf58a5cd6c9805996ce8c4492c2e45f56a Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Thu, 26 Dec 2024 13:48:53 -0500 Subject: [PATCH 18/41] Run generate_mocks script --- mocks/project_mocks.go | 14 ++++++++++++++ mocks/workspace_mocks.go | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/mocks/project_mocks.go b/mocks/project_mocks.go index 405c8afd7..9f2ed5c8c 100644 --- a/mocks/project_mocks.go +++ b/mocks/project_mocks.go @@ -84,6 +84,20 @@ func (mr *MockProjectsMockRecorder) Delete(ctx, projectID any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockProjects)(nil).Delete), ctx, projectID) } +// DeleteAllTagBindings mocks base method. +func (m *MockProjects) DeleteAllTagBindings(ctx context.Context, projectID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteAllTagBindings", ctx, projectID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteAllTagBindings indicates an expected call of DeleteAllTagBindings. +func (mr *MockProjectsMockRecorder) DeleteAllTagBindings(ctx, projectID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAllTagBindings", reflect.TypeOf((*MockProjects)(nil).DeleteAllTagBindings), ctx, projectID) +} + // List mocks base method. func (m *MockProjects) List(ctx context.Context, organization string, options *tfe.ProjectListOptions) (*tfe.ProjectList, error) { m.ctrl.T.Helper() diff --git a/mocks/workspace_mocks.go b/mocks/workspace_mocks.go index f9d89571d..537ad370d 100644 --- a/mocks/workspace_mocks.go +++ b/mocks/workspace_mocks.go @@ -128,6 +128,20 @@ func (mr *MockWorkspacesMockRecorder) Delete(ctx, organization, workspace any) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockWorkspaces)(nil).Delete), ctx, organization, workspace) } +// DeleteAllTagBindings mocks base method. +func (m *MockWorkspaces) DeleteAllTagBindings(ctx context.Context, workspaceID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteAllTagBindings", ctx, workspaceID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteAllTagBindings indicates an expected call of DeleteAllTagBindings. +func (mr *MockWorkspacesMockRecorder) DeleteAllTagBindings(ctx, workspaceID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAllTagBindings", reflect.TypeOf((*MockWorkspaces)(nil).DeleteAllTagBindings), ctx, workspaceID) +} + // DeleteByID mocks base method. func (m *MockWorkspaces) DeleteByID(ctx context.Context, workspaceID string) error { m.ctrl.T.Helper() From 68a78aa8815cf82ddee53ea8ca581257aeaf6323 Mon Sep 17 00:00:00 2001 From: NodyHub Date: Tue, 7 Jan 2025 10:18:36 +0100 Subject: [PATCH 19/41] upgraded github.com/hashicorp/go-slug v0.16.0 => v0.16.3 --- CHANGELOG.md | 4 ++++ go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2490c133a..96579c9b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ * Add BETA support for Linux arm64 agents, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users @natalie-todd [#1022](https://github.com/hashicorp/go-tfe/pull/1022) * Adds support to delete all tag bindings on either a project or workspace by @sebasslash [#1023](https://github.com/hashicorp/go-tfe/pull/1023) +## Bug Fixes + +* Update dependency `github.com/hashicorp/go-slug` `v0.16.0` => `v0.16.3` to integrate latest changes. + # v1.71.0 ## Enhancements diff --git a/go.mod b/go.mod index 22ad6cda3..2cf4359ab 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/google/go-querystring v1.1.0 github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-retryablehttp v0.7.7 - github.com/hashicorp/go-slug v0.16.0 + github.com/hashicorp/go-slug v0.16.3 github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/jsonapi v1.3.1 diff --git a/go.sum b/go.sum index 1293b8565..c5f2fd3f1 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/S github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= -github.com/hashicorp/go-slug v0.16.0 h1:S/ko9fms1gf6305ktJNUKGxFmscZ+yWvAtsas0SYUyA= -github.com/hashicorp/go-slug v0.16.0/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ= +github.com/hashicorp/go-slug v0.16.3 h1:pe0PMwz2UWN1168QksdW/d7u057itB2gY568iF0E2Ns= +github.com/hashicorp/go-slug v0.16.3/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= From 106dfd21bb47247cc1b6b0fe64d3a10b21fdd728 Mon Sep 17 00:00:00 2001 From: NodyHub Date: Tue, 7 Jan 2025 16:03:02 +0100 Subject: [PATCH 20/41] remove deprecated feature --- configuration_version_integration_test.go | 1 - example_test.go | 10 ++++------ examples/configuration_versions/main.go | 5 ++--- examples/registry_modules/main.go | 5 ++--- registry_module_integration_test.go | 1 - 5 files changed, 8 insertions(+), 14 deletions(-) diff --git a/configuration_version_integration_test.go b/configuration_version_integration_test.go index ea517c640..2ded10a93 100644 --- a/configuration_version_integration_test.go +++ b/configuration_version_integration_test.go @@ -311,7 +311,6 @@ func TestConfigurationVersionsUploadTarGzip(t *testing.T) { packer, err := slug.NewPacker( slug.DereferenceSymlinks(), slug.ApplyTerraformIgnore(), - slug.AllowSymlinkTarget("/target/symlink/path/foo"), ) require.NoError(t, err) diff --git a/example_test.go b/example_test.go index 863e65c4d..9c49bd241 100644 --- a/example_test.go +++ b/example_test.go @@ -90,9 +90,8 @@ func ExampleConfigurationVersions_UploadTarGzip() { } packer, err := slug.NewPacker( - slug.DereferenceSymlinks(), // dereferences symlinks - slug.ApplyTerraformIgnore(), // ignores paths specified in .terraformignore - slug.AllowSymlinkTarget("/some/path"), // allow certain symlink target paths + slug.DereferenceSymlinks(), // dereferences symlinks + slug.ApplyTerraformIgnore(), // ignores paths specified in .terraformignore ) if err != nil { log.Fatal(err) @@ -131,9 +130,8 @@ func ExampleRegistryModules_UploadTarGzip() { } packer, err := slug.NewPacker( - slug.DereferenceSymlinks(), // dereferences symlinks - slug.ApplyTerraformIgnore(), // ignores paths specified in .terraformignore - slug.AllowSymlinkTarget("/some/path"), // allow certain symlink target paths + slug.DereferenceSymlinks(), // dereferences symlinks + slug.ApplyTerraformIgnore(), // ignores paths specified in .terraformignore ) if err != nil { log.Fatal(err) diff --git a/examples/configuration_versions/main.go b/examples/configuration_versions/main.go index 3aa3d08c9..65ba9731f 100644 --- a/examples/configuration_versions/main.go +++ b/examples/configuration_versions/main.go @@ -22,9 +22,8 @@ func main() { } packer, err := slug.NewPacker( - slug.DereferenceSymlinks(), // dereferences symlinks - slug.ApplyTerraformIgnore(), // ignores paths specified in .terraformignore - slug.AllowSymlinkTarget("/some/path"), // allow certain symlink target paths + slug.DereferenceSymlinks(), // dereferences symlinks + slug.ApplyTerraformIgnore(), // ignores paths specified in .terraformignore ) if err != nil { log.Fatal(err) diff --git a/examples/registry_modules/main.go b/examples/registry_modules/main.go index 2965aba31..bc424ec6b 100644 --- a/examples/registry_modules/main.go +++ b/examples/registry_modules/main.go @@ -22,9 +22,8 @@ func main() { } packer, err := slug.NewPacker( - slug.DereferenceSymlinks(), // dereferences symlinks - slug.ApplyTerraformIgnore(), // ignores paths specified in .terraformignore - slug.AllowSymlinkTarget("/some/path"), // allow certain symlink target paths + slug.DereferenceSymlinks(), // dereferences symlinks + slug.ApplyTerraformIgnore(), // ignores paths specified in .terraformignore ) if err != nil { log.Fatal(err) diff --git a/registry_module_integration_test.go b/registry_module_integration_test.go index 805921fb0..35292604b 100644 --- a/registry_module_integration_test.go +++ b/registry_module_integration_test.go @@ -1763,7 +1763,6 @@ func TestRegistryModulesUploadTarGzip(t *testing.T) { packer, err := slug.NewPacker( slug.DereferenceSymlinks(), slug.ApplyTerraformIgnore(), - slug.AllowSymlinkTarget("/target/symlink/path/foo"), ) require.NoError(t, err) From cec7ba39741fb819976d77cd4d8f36560a29e643 Mon Sep 17 00:00:00 2001 From: Luces Huayhuaca <21225410+uturunku1@users.noreply.github.com> Date: Tue, 7 Jan 2025 12:06:06 -0800 Subject: [PATCH 21/41] Update CHANGELOG.md (#1026) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2490c133a..99f2a1e53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +# v1.72.0 + ## Enhancements * Add support for project level auto destroy settings @simonxmh [#1011](https://github.com/hashicorp/go-tfe/pull/1011) From f215e0778c8161dda52be4f8531d0bebc0545077 Mon Sep 17 00:00:00 2001 From: Taylor Chaparro <33099255+notchairmk@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:22:00 -0800 Subject: [PATCH 22/41] chore(deps): bump jsonapi, update RunTrigger use deprecated relation (#1027) Prerequisite for #1016. Bumps the jsonapi version to address a bug with interactions between `polyrelation` and deprecated `relation`. Updates run triggers to backfill the deprecated Sourceable field in the RunTrigger struct. --- go.mod | 2 +- go.sum | 4 ++-- run_trigger.go | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 22ad6cda3..e69323802 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/hashicorp/go-slug v0.16.0 github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-version v1.7.0 - github.com/hashicorp/jsonapi v1.3.1 + github.com/hashicorp/jsonapi v1.3.2 github.com/stretchr/testify v1.9.0 go.uber.org/mock v0.4.0 golang.org/x/sync v0.8.0 diff --git a/go.sum b/go.sum index 1293b8565..da2eab7f2 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +16,8 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/jsonapi v1.3.1 h1:GtPvnmcWgYwCuDGvYT5VZBHcUyFdq9lSyCzDjn1DdPo= -github.com/hashicorp/jsonapi v1.3.1/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= +github.com/hashicorp/jsonapi v1.3.2 h1:gP3fX2ZT7qXi+PbwieptzkspIohO2kCSiBUvUTBAbMs= +github.com/hashicorp/jsonapi v1.3.2/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/run_trigger.go b/run_trigger.go index e002271fa..930528bac 100644 --- a/run_trigger.go +++ b/run_trigger.go @@ -121,6 +121,10 @@ func (s *runTriggers) List(ctx context.Context, workspaceID string, options *Run return nil, err } + for i := range rtl.Items { + backfillDeprecatedSourceable(rtl.Items[i]) + } + return rtl, nil } @@ -145,6 +149,8 @@ func (s *runTriggers) Create(ctx context.Context, workspaceID string, options Ru return nil, err } + backfillDeprecatedSourceable(rt) + return rt, nil } @@ -166,6 +172,8 @@ func (s *runTriggers) Read(ctx context.Context, runTriggerID string) (*RunTrigge return nil, err } + backfillDeprecatedSourceable(rt) + return rt, nil } @@ -203,6 +211,14 @@ func (o *RunTriggerListOptions) valid() error { return nil } +func backfillDeprecatedSourceable(runTrigger *RunTrigger) { + if runTrigger.Sourceable != nil || runTrigger.SourceableChoice == nil { + return + } + + runTrigger.Sourceable = runTrigger.SourceableChoice.Workspace +} + func validateRunTriggerFilterParam(filterParam RunTriggerFilterOp, includeParams []RunTriggerIncludeOpt) error { switch filterParam { case RunTriggerOutbound, RunTriggerInbound: From 518ceb022e0360754e0763e288652709a1739ba0 Mon Sep 17 00:00:00 2001 From: Taylor Chaparro Date: Tue, 7 Jan 2025 13:26:14 -0800 Subject: [PATCH 23/41] run go mod tidy --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index 6391cb009..da2eab7f2 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,6 @@ github.com/hashicorp/jsonapi v1.3.2 h1:gP3fX2ZT7qXi+PbwieptzkspIohO2kCSiBUvUTBAb github.com/hashicorp/jsonapi v1.3.2/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/notchairmk/jsonapi v0.0.0-20241223221631-b0c6a5b7edd8 h1:Nll3UptyKamtMP60oCHnRKI3l/kgadZHKQ6/uLYPyVM= -github.com/notchairmk/jsonapi v0.0.0-20241223221631-b0c6a5b7edd8/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= From bb7b35f77f9af0822f7be10ac752c6967e37b50a Mon Sep 17 00:00:00 2001 From: Taylor Chaparro Date: Wed, 8 Jan 2025 10:24:02 -0800 Subject: [PATCH 24/41] omitempty when creating notification subscribable choice --- notification_configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notification_configuration.go b/notification_configuration.go index 87a7c60b1..ad6c8dfae 100644 --- a/notification_configuration.go +++ b/notification_configuration.go @@ -166,7 +166,7 @@ type NotificationConfigurationCreateOptions struct { EmailUsers []*User `jsonapi:"relation,users,omitempty"` // Required: The workspace or team that the notification configuration is associated with. - SubscribableChoice *NotificationConfigurationSubscribableChoice `jsonapi:"polyrelation,subscribable"` + SubscribableChoice *NotificationConfigurationSubscribableChoice `jsonapi:"polyrelation,subscribable,omitempty"` } // NotificationConfigurationUpdateOptions represents the options for From 2b26a238b9f4a71c1d9ff186be7b0b6c739858a2 Mon Sep 17 00:00:00 2001 From: Taylor Chaparro <33099255+notchairmk@users.noreply.github.com> Date: Wed, 8 Jan 2025 13:53:12 -0800 Subject: [PATCH 25/41] v1.73.0 Changelog (#1028) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99f2a1e53..aafd1951f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Unreleased +# v1.73.0 + +## Enhancements + +* Add support for team notification configurations @notchairmk [#1016](https://github.com/hashicorp/go-tfe/pull/1016) + # v1.72.0 ## Enhancements From 6baf42d649c28f401a317af352ecfd6ea087ca13 Mon Sep 17 00:00:00 2001 From: NodyHub Date: Thu, 9 Jan 2025 09:57:31 +0100 Subject: [PATCH 26/41] adjusted targeted release in the changelog --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d1ceec61..c007bb5b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ * Add support for team notification configurations @notchairmk [#1016](https://github.com/hashicorp/go-tfe/pull/1016) +## Bug Fixes + +* Update dependency `github.com/hashicorp/go-slug` `v0.16.0` => `v0.16.3` to integrate latest changes. + # v1.72.0 ## Enhancements @@ -14,10 +18,6 @@ * Add BETA support for Linux arm64 agents, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users @natalie-todd [#1022](https://github.com/hashicorp/go-tfe/pull/1022) * Adds support to delete all tag bindings on either a project or workspace by @sebasslash [#1023](https://github.com/hashicorp/go-tfe/pull/1023) -## Bug Fixes - -* Update dependency `github.com/hashicorp/go-slug` `v0.16.0` => `v0.16.3` to integrate latest changes. - # v1.71.0 ## Enhancements From d43e66705dae69a8165510e2e6f8095e0433d6c9 Mon Sep 17 00:00:00 2001 From: natalie-todd Date: Wed, 8 Jan 2025 13:28:40 -0800 Subject: [PATCH 27/41] Use json instead of jsonapi for Archs --- CHANGELOG.md | 4 ++++ admin_terraform_version.go | 9 ++++---- admin_terraform_version_integration_test.go | 25 ++++++++++++--------- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aafd1951f..f0498491c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ * Add support for team notification configurations @notchairmk [#1016](https://github.com/hashicorp/go-tfe/pull/1016) +## Bug Fixes + +* Fixes a bug in BETA support for Linux arm64 agents, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users @natalie-todd [#1022](https://github.com/hashicorp/go-tfe/pull/1022) + # v1.72.0 ## Enhancements diff --git a/admin_terraform_version.go b/admin_terraform_version.go index 78d75ce14..e1d00561f 100644 --- a/admin_terraform_version.go +++ b/admin_terraform_version.go @@ -63,10 +63,10 @@ type AdminTerraformVersion struct { } type ToolVersionArchitecture struct { - URL string `jsonapi:"attr,url"` - Sha string `jsonapi:"attr,sha"` - OS string `jsonapi:"attr,os"` - Arch string `jsonapi:"attr,arch"` + URL string `json:"url"` + Sha string `json:"sha"` + OS string `json:"os"` + Arch string `json:"arch"` } // AdminTerraformVersionsListOptions represents the options for listing @@ -168,7 +168,6 @@ func (a *adminTerraformVersions) Create(ctx context.Context, options AdminTerraf if err != nil { return nil, err } - return tfv, nil } diff --git a/admin_terraform_version_integration_test.go b/admin_terraform_version_integration_test.go index 39e64006b..dfd79baca 100644 --- a/admin_terraform_version_integration_test.go +++ b/admin_terraform_version_integration_test.go @@ -100,25 +100,28 @@ func TestAdminTerraformVersions_CreateDelete(t *testing.T) { client := testClient(t) ctx := context.Background() - version := genSafeRandomTerraformVersion() t.Run("with valid options", func(t *testing.T) { - sha := String(genSha(t)) opts := AdminTerraformVersionCreateOptions{ - Version: String(version), - URL: String("https://www.hashicorp.com"), - Sha: sha, + Version: String(genSafeRandomTerraformVersion()), Deprecated: Bool(true), DeprecatedReason: String("Test Reason"), Official: Bool(false), Enabled: Bool(false), Beta: Bool(false), - Archs: []*ToolVersionArchitecture{{ - URL: "https://www.hashicorp.com", - Sha: *sha, - OS: linux, - Arch: amd64, - }}, + Archs: []*ToolVersionArchitecture{ + { + URL: "https://www.hashicorp.com", + Sha: *String(genSha(t)), + OS: linux, + Arch: amd64, + }, + { + URL: "https://www.hashicorp.com", + Sha: *String(genSha(t)), + OS: linux, + Arch: arm64, + }}, } tfv, err := client.Admin.TerraformVersions.Create(ctx, opts) require.NoError(t, err) From 53fa83f8256ffe5259610b459ece724d309d757f Mon Sep 17 00:00:00 2001 From: natalie-todd Date: Thu, 9 Jan 2025 15:12:05 -0800 Subject: [PATCH 28/41] Add test for valid options, url, and sha --- admin_terraform_version_integration_test.go | 31 ++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/admin_terraform_version_integration_test.go b/admin_terraform_version_integration_test.go index dfd79baca..defffeb70 100644 --- a/admin_terraform_version_integration_test.go +++ b/admin_terraform_version_integration_test.go @@ -101,7 +101,7 @@ func TestAdminTerraformVersions_CreateDelete(t *testing.T) { client := testClient(t) ctx := context.Background() - t.Run("with valid options", func(t *testing.T) { + t.Run("with valid options and archs", func(t *testing.T) { opts := AdminTerraformVersionCreateOptions{ Version: String(genSafeRandomTerraformVersion()), Deprecated: Bool(true), @@ -141,6 +141,35 @@ func TestAdminTerraformVersions_CreateDelete(t *testing.T) { assert.Equal(t, *opts.Beta, tfv.Beta) }) + t.Run("with valid options, url, and sha", func(t *testing.T) { + opts := AdminTerraformVersionCreateOptions{ + Version: String(genSafeRandomTerraformVersion()), + URL: String("https://www.hashicorp.com"), + Sha: String(genSha(t)), + Deprecated: Bool(true), + DeprecatedReason: String("Test Reason"), + Official: Bool(false), + Enabled: Bool(false), + Beta: Bool(false), + } + tfv, err := client.Admin.TerraformVersions.Create(ctx, opts) + require.NoError(t, err) + + defer func() { + deleteErr := client.Admin.TerraformVersions.Delete(ctx, tfv.ID) + require.NoError(t, deleteErr) + }() + + assert.Equal(t, *opts.Version, tfv.Version) + assert.Equal(t, *opts.URL, tfv.URL) + assert.Equal(t, *opts.Sha, tfv.Sha) + assert.Equal(t, *opts.Official, tfv.Official) + assert.Equal(t, *opts.Deprecated, tfv.Deprecated) + assert.Equal(t, *opts.DeprecatedReason, *tfv.DeprecatedReason) + assert.Equal(t, *opts.Enabled, tfv.Enabled) + assert.Equal(t, *opts.Beta, tfv.Beta) + }) + t.Run("with only required options", func(t *testing.T) { version := genSafeRandomTerraformVersion() opts := AdminTerraformVersionCreateOptions{ From 55d5f2fabb530615f1952dcb91892b03e5150081 Mon Sep 17 00:00:00 2001 From: Netra Mali Date: Fri, 10 Jan 2025 10:27:38 -0500 Subject: [PATCH 29/41] add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c007bb5b2..c2c2e60c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +* Add support for adding custom project permission for variable sets `ProjectVariableSetsPermission` by @netramali [21879](https://github.com/hashicorp/atlas/pull/21879) + # v1.73.0 ## Enhancements From 687244f56fd00a5bbcf8d2778562dc86aa7aec63 Mon Sep 17 00:00:00 2001 From: Netra Mali Date: Fri, 10 Jan 2025 10:28:05 -0500 Subject: [PATCH 30/41] revert test temp --- team_project_access_integration_test.go | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/team_project_access_integration_test.go b/team_project_access_integration_test.go index 934af232c..831c1f1c6 100644 --- a/team_project_access_integration_test.go +++ b/team_project_access_integration_test.go @@ -171,9 +171,8 @@ func TestTeamProjectAccessesAdd(t *testing.T) { Team: tmTest, Project: pTest, ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{ - Settings: ProjectSettingsPermission(ProjectSettingsPermissionUpdate), - Teams: ProjectTeamsPermission(ProjectTeamsPermissionManage), - VariableSets: ProjectVariableSetsPermission(ProjectVariableSetsPermissionWrite), + Settings: ProjectSettingsPermission(ProjectSettingsPermissionUpdate), + Teams: ProjectTeamsPermission(ProjectTeamsPermissionManage), }, WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{ Runs: WorkspaceRunsPermission(WorkspaceRunsPermissionApply), @@ -210,7 +209,6 @@ func TestTeamProjectAccessesAdd(t *testing.T) { assert.Equal(t, options.Access, item.Access) assert.Equal(t, *options.ProjectAccess.Settings, item.ProjectAccess.ProjectSettingsPermission) assert.Equal(t, *options.ProjectAccess.Teams, item.ProjectAccess.ProjectTeamsPermission) - assert.Equal(t, *options.ProjectAccess.VariableSets, item.ProjectAccess.ProjectVariableSetsPermission) assert.Equal(t, *options.WorkspaceAccess.Runs, item.WorkspaceAccess.WorkspaceRunsPermission) assert.Equal(t, *options.WorkspaceAccess.SentinelMocks, item.WorkspaceAccess.WorkspaceSentinelMocksPermission) assert.Equal(t, *options.WorkspaceAccess.StateVersions, item.WorkspaceAccess.WorkspaceStateVersionsPermission) @@ -357,9 +355,8 @@ func TestTeamProjectAccessesUpdate(t *testing.T) { options := TeamProjectAccessUpdateOptions{ Access: ProjectAccess(TeamProjectAccessCustom), ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{ - Settings: ProjectSettingsPermission(ProjectSettingsPermissionUpdate), - Teams: ProjectTeamsPermission(ProjectTeamsPermissionManage), - VariableSets: ProjectVariableSetsPermission(ProjectVariableSetsPermissionWrite), + Settings: ProjectSettingsPermission(ProjectSettingsPermissionUpdate), + Teams: ProjectTeamsPermission(ProjectTeamsPermissionManage), }, WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{ Runs: WorkspaceRunsPermission(WorkspaceRunsPermissionPlan), @@ -381,7 +378,6 @@ func TestTeamProjectAccessesUpdate(t *testing.T) { assert.Equal(t, tpa.Access, TeamProjectAccessCustom) assert.Equal(t, *options.ProjectAccess.Teams, tpa.ProjectAccess.ProjectTeamsPermission) assert.Equal(t, *options.ProjectAccess.Settings, tpa.ProjectAccess.ProjectSettingsPermission) - assert.Equal(t, *options.ProjectAccess.VariableSets, tpa.ProjectAccess.ProjectVariableSetsPermission) assert.Equal(t, *options.WorkspaceAccess.Runs, tpa.WorkspaceAccess.WorkspaceRunsPermission) assert.Equal(t, *options.WorkspaceAccess.SentinelMocks, tpa.WorkspaceAccess.WorkspaceSentinelMocksPermission) assert.Equal(t, *options.WorkspaceAccess.StateVersions, tpa.WorkspaceAccess.WorkspaceStateVersionsPermission) @@ -426,7 +422,6 @@ func TestTeamProjectAccessesUpdate(t *testing.T) { assert.Equal(t, false, tpa.WorkspaceAccess.WorkspaceCreatePermission) // assert that other attributes remain the same assert.Equal(t, tpaCustomTest.ProjectAccess.ProjectSettingsPermission, tpa.ProjectAccess.ProjectSettingsPermission) - assert.Equal(t, tpaCustomTest.ProjectAccess.ProjectVariableSetsPermission, tpa.ProjectAccess.ProjectVariableSetsPermission) assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceLockingPermission, tpa.WorkspaceAccess.WorkspaceLockingPermission) assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceMovePermission, tpa.WorkspaceAccess.WorkspaceMovePermission) assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceDeletePermission, tpa.WorkspaceAccess.WorkspaceDeletePermission) From 372b311975bcb133d7b0601e451bfe0a36dacac8 Mon Sep 17 00:00:00 2001 From: Netra Mali Date: Fri, 10 Jan 2025 11:20:17 -0500 Subject: [PATCH 31/41] test added --- team_project_access_integration_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/team_project_access_integration_test.go b/team_project_access_integration_test.go index 831c1f1c6..6b3973760 100644 --- a/team_project_access_integration_test.go +++ b/team_project_access_integration_test.go @@ -171,8 +171,9 @@ func TestTeamProjectAccessesAdd(t *testing.T) { Team: tmTest, Project: pTest, ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{ - Settings: ProjectSettingsPermission(ProjectSettingsPermissionUpdate), - Teams: ProjectTeamsPermission(ProjectTeamsPermissionManage), + Settings: ProjectSettingsPermission(ProjectSettingsPermissionUpdate), + Teams: ProjectTeamsPermission(ProjectTeamsPermissionManage), + VariableSets: ProjectVariableSetsPermission(ProjectVariableSetsPermissionWrite), }, WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{ Runs: WorkspaceRunsPermission(WorkspaceRunsPermissionApply), @@ -209,6 +210,7 @@ func TestTeamProjectAccessesAdd(t *testing.T) { assert.Equal(t, options.Access, item.Access) assert.Equal(t, *options.ProjectAccess.Settings, item.ProjectAccess.ProjectSettingsPermission) assert.Equal(t, *options.ProjectAccess.Teams, item.ProjectAccess.ProjectTeamsPermission) + assert.Equal(t, *options.ProjectAccess.VariableSets, item.ProjectAccess.ProjectVariableSetsPermission) assert.Equal(t, *options.WorkspaceAccess.Runs, item.WorkspaceAccess.WorkspaceRunsPermission) assert.Equal(t, *options.WorkspaceAccess.SentinelMocks, item.WorkspaceAccess.WorkspaceSentinelMocksPermission) assert.Equal(t, *options.WorkspaceAccess.StateVersions, item.WorkspaceAccess.WorkspaceStateVersionsPermission) From 09fe349415b80ff5053b0799f3d3d80cada953b6 Mon Sep 17 00:00:00 2001 From: Netra Mali Date: Fri, 10 Jan 2025 11:27:00 -0500 Subject: [PATCH 32/41] test added --- team_project_access_integration_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/team_project_access_integration_test.go b/team_project_access_integration_test.go index 6b3973760..8d1c4e1a0 100644 --- a/team_project_access_integration_test.go +++ b/team_project_access_integration_test.go @@ -357,8 +357,9 @@ func TestTeamProjectAccessesUpdate(t *testing.T) { options := TeamProjectAccessUpdateOptions{ Access: ProjectAccess(TeamProjectAccessCustom), ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{ - Settings: ProjectSettingsPermission(ProjectSettingsPermissionUpdate), - Teams: ProjectTeamsPermission(ProjectTeamsPermissionManage), + Settings: ProjectSettingsPermission(ProjectSettingsPermissionUpdate), + Teams: ProjectTeamsPermission(ProjectTeamsPermissionManage), + VariableSets: ProjectVariableSetsPermission(ProjectVariableSetsPermissionRead), }, WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{ Runs: WorkspaceRunsPermission(WorkspaceRunsPermissionPlan), @@ -380,6 +381,7 @@ func TestTeamProjectAccessesUpdate(t *testing.T) { assert.Equal(t, tpa.Access, TeamProjectAccessCustom) assert.Equal(t, *options.ProjectAccess.Teams, tpa.ProjectAccess.ProjectTeamsPermission) assert.Equal(t, *options.ProjectAccess.Settings, tpa.ProjectAccess.ProjectSettingsPermission) + assert.Equal(t, *options.ProjectAccess.VariableSets, tpa.ProjectAccess.ProjectVariableSetsPermission) assert.Equal(t, *options.WorkspaceAccess.Runs, tpa.WorkspaceAccess.WorkspaceRunsPermission) assert.Equal(t, *options.WorkspaceAccess.SentinelMocks, tpa.WorkspaceAccess.WorkspaceSentinelMocksPermission) assert.Equal(t, *options.WorkspaceAccess.StateVersions, tpa.WorkspaceAccess.WorkspaceStateVersionsPermission) From a61628780313bfa7718364e8b9a2ccaab637ec6f Mon Sep 17 00:00:00 2001 From: Netra Mali Date: Fri, 10 Jan 2025 11:31:35 -0500 Subject: [PATCH 33/41] beta --- CHANGELOG.md | 2 +- team_project_access.go | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2c2e60c2..f5b578b7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Unreleased -* Add support for adding custom project permission for variable sets `ProjectVariableSetsPermission` by @netramali [21879](https://github.com/hashicorp/atlas/pull/21879) +* Add BETA support for adding custom project permission for variable sets `ProjectVariableSetsPermission` by @netramali [21879](https://github.com/hashicorp/atlas/pull/21879) # v1.73.0 diff --git a/team_project_access.go b/team_project_access.go index aaf99085a..36e9cda8d 100644 --- a/team_project_access.go +++ b/team_project_access.go @@ -69,8 +69,10 @@ type TeamProjectAccess struct { // ProjectPermissions represents the team's permissions on its project type TeamProjectAccessProjectPermissions struct { - ProjectSettingsPermission ProjectSettingsPermissionType `jsonapi:"attr,settings"` - ProjectTeamsPermission ProjectTeamsPermissionType `jsonapi:"attr,teams"` + ProjectSettingsPermission ProjectSettingsPermissionType `jsonapi:"attr,settings"` + ProjectTeamsPermission ProjectTeamsPermissionType `jsonapi:"attr,teams"` + // ProjectVariableSetsPermission represents read, manage, and no access custom permission for project-level variable sets + // This relation is considered BETA, SUBJECT TO CHANGE, and likely unavailable to most users. ProjectVariableSetsPermission ProjectVariableSetsPermissionType `jsonapi:"attr,variable-sets"` } @@ -106,6 +108,7 @@ const ( ) // ProjectVariableSetsPermissionType represents the permission type to a project's variable sets +// This relation is considered BETA, SUBJECT TO CHANGE, and likely unavailable to most users. type ProjectVariableSetsPermissionType string const ( @@ -151,8 +154,9 @@ const ( ) type TeamProjectAccessProjectPermissionsOptions struct { - Settings *ProjectSettingsPermissionType `json:"settings,omitempty"` - Teams *ProjectTeamsPermissionType `json:"teams,omitempty"` + Settings *ProjectSettingsPermissionType `json:"settings,omitempty"` + Teams *ProjectTeamsPermissionType `json:"teams,omitempty"` + // This relation is considered BETA, SUBJECT TO CHANGE, and likely unavailable to most users. VariableSets *ProjectVariableSetsPermissionType `json:"variable-sets,omitempty"` } From 1f49cd3112384ade93360f08c07a587fa7b9cf3f Mon Sep 17 00:00:00 2001 From: Netra Mali Date: Fri, 10 Jan 2025 12:19:24 -0500 Subject: [PATCH 34/41] beta test flag --- team_project_access_integration_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/team_project_access_integration_test.go b/team_project_access_integration_test.go index 8d1c4e1a0..31cdbdacb 100644 --- a/team_project_access_integration_test.go +++ b/team_project_access_integration_test.go @@ -166,6 +166,7 @@ func TestTeamProjectAccessesAdd(t *testing.T) { }) t.Run("with valid options for all custom TeamProject permissions", func(t *testing.T) { + skipUnlessBeta(t) options := TeamProjectAccessAddOptions{ Access: *ProjectAccess(TeamProjectAccessCustom), Team: tmTest, @@ -354,6 +355,7 @@ func TestTeamProjectAccessesUpdate(t *testing.T) { }) t.Run("with valid custom permissions attributes for all permissions", func(t *testing.T) { + skipUnlessBeta(t) options := TeamProjectAccessUpdateOptions{ Access: ProjectAccess(TeamProjectAccessCustom), ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{ From f42957d73c7e7bc0309ddf50cf150b8e6ec0dcdf Mon Sep 17 00:00:00 2001 From: natalie-todd Date: Fri, 10 Jan 2025 10:02:27 -0800 Subject: [PATCH 35/41] Rename ToolVersionArchitecture to ToolVersionArchitectureOptions for convention --- admin_terraform_version.go | 22 ++++++++++----------- admin_terraform_version_integration_test.go | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/admin_terraform_version.go b/admin_terraform_version.go index e1d00561f..01a69f84b 100644 --- a/admin_terraform_version.go +++ b/admin_terraform_version.go @@ -62,7 +62,7 @@ type AdminTerraformVersion struct { CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` } -type ToolVersionArchitecture struct { +type ToolVersionArchitectureOptions struct { URL string `json:"url"` Sha string `json:"sha"` OS string `json:"os"` @@ -84,16 +84,16 @@ type AdminTerraformVersionsListOptions struct { // AdminTerraformVersionCreateOptions for creating a terraform version. // https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/terraform-versions#request-body type AdminTerraformVersionCreateOptions struct { - Type string `jsonapi:"primary,terraform-versions"` - Version *string `jsonapi:"attr,version"` // Required - URL *string `jsonapi:"attr,url"` // Required - Sha *string `jsonapi:"attr,sha"` // Required - Official *bool `jsonapi:"attr,official,omitempty"` - Deprecated *bool `jsonapi:"attr,deprecated,omitempty"` - DeprecatedReason *string `jsonapi:"attr,deprecated-reason,omitempty"` - Enabled *bool `jsonapi:"attr,enabled,omitempty"` - Beta *bool `jsonapi:"attr,beta,omitempty"` - Archs []*ToolVersionArchitecture `jsonapi:"attr,archs,omitempty"` + Type string `jsonapi:"primary,terraform-versions"` + Version *string `jsonapi:"attr,version"` // Required + URL *string `jsonapi:"attr,url"` // Required + Sha *string `jsonapi:"attr,sha"` // Required + Official *bool `jsonapi:"attr,official,omitempty"` + Deprecated *bool `jsonapi:"attr,deprecated,omitempty"` + DeprecatedReason *string `jsonapi:"attr,deprecated-reason,omitempty"` + Enabled *bool `jsonapi:"attr,enabled,omitempty"` + Beta *bool `jsonapi:"attr,beta,omitempty"` + Archs []*ToolVersionArchitectureOptions `jsonapi:"attr,archs,omitempty"` } // AdminTerraformVersionUpdateOptions for updating terraform version. diff --git a/admin_terraform_version_integration_test.go b/admin_terraform_version_integration_test.go index defffeb70..67898e256 100644 --- a/admin_terraform_version_integration_test.go +++ b/admin_terraform_version_integration_test.go @@ -109,7 +109,7 @@ func TestAdminTerraformVersions_CreateDelete(t *testing.T) { Official: Bool(false), Enabled: Bool(false), Beta: Bool(false), - Archs: []*ToolVersionArchitecture{ + Archs: []*ToolVersionArchitectureOptions{ { URL: "https://www.hashicorp.com", Sha: *String(genSha(t)), @@ -219,7 +219,7 @@ func TestAdminTerraformVersions_ReadUpdate(t *testing.T) { DeprecatedReason: String("Test Reason"), Enabled: Bool(false), Beta: Bool(false), - Archs: []*ToolVersionArchitecture{{ + Archs: []*ToolVersionArchitectureOptions{{ URL: "https://www.hashicorp.com", Sha: *sha, OS: linux, From c339dc65705dadb3c248de25b0cddd489c7f7dfb Mon Sep 17 00:00:00 2001 From: Netra Mali Date: Fri, 10 Jan 2025 14:04:31 -0500 Subject: [PATCH 36/41] beta test --- team_project_access_integration_test.go | 96 ++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 10 deletions(-) diff --git a/team_project_access_integration_test.go b/team_project_access_integration_test.go index 31cdbdacb..888e1d2c7 100644 --- a/team_project_access_integration_test.go +++ b/team_project_access_integration_test.go @@ -166,15 +166,13 @@ func TestTeamProjectAccessesAdd(t *testing.T) { }) t.Run("with valid options for all custom TeamProject permissions", func(t *testing.T) { - skipUnlessBeta(t) options := TeamProjectAccessAddOptions{ Access: *ProjectAccess(TeamProjectAccessCustom), Team: tmTest, Project: pTest, ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{ - Settings: ProjectSettingsPermission(ProjectSettingsPermissionUpdate), - Teams: ProjectTeamsPermission(ProjectTeamsPermissionManage), - VariableSets: ProjectVariableSetsPermission(ProjectVariableSetsPermissionWrite), + Settings: ProjectSettingsPermission(ProjectSettingsPermissionUpdate), + Teams: ProjectTeamsPermission(ProjectTeamsPermissionManage), }, WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{ Runs: WorkspaceRunsPermission(WorkspaceRunsPermissionApply), @@ -211,7 +209,6 @@ func TestTeamProjectAccessesAdd(t *testing.T) { assert.Equal(t, options.Access, item.Access) assert.Equal(t, *options.ProjectAccess.Settings, item.ProjectAccess.ProjectSettingsPermission) assert.Equal(t, *options.ProjectAccess.Teams, item.ProjectAccess.ProjectTeamsPermission) - assert.Equal(t, *options.ProjectAccess.VariableSets, item.ProjectAccess.ProjectVariableSetsPermission) assert.Equal(t, *options.WorkspaceAccess.Runs, item.WorkspaceAccess.WorkspaceRunsPermission) assert.Equal(t, *options.WorkspaceAccess.SentinelMocks, item.WorkspaceAccess.WorkspaceSentinelMocksPermission) assert.Equal(t, *options.WorkspaceAccess.StateVersions, item.WorkspaceAccess.WorkspaceStateVersionsPermission) @@ -224,6 +221,45 @@ func TestTeamProjectAccessesAdd(t *testing.T) { } }) + t.Run("with valid options for custom variable sets permissions", func(t *testing.T) { + skipUnlessBeta(t) + options := TeamProjectAccessAddOptions{ + Access: *ProjectAccess(TeamProjectAccessCustom), + Team: tmTest, + Project: pTest, + ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{ + VariableSets: ProjectVariableSetsPermission(ProjectVariableSetsPermissionWrite), + }, + WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{ + Runs: WorkspaceRunsPermission(WorkspaceRunsPermissionApply), + }, + } + + tpa, err := client.TeamProjectAccess.Add(ctx, options) + t.Cleanup(func() { + err := client.TeamProjectAccess.Remove(ctx, tpa.ID) + if err != nil { + t.Logf("error removing team access (%s): %s", tpa.ID, err) + } + }) + + require.NoError(t, err) + + // Get a refreshed view from the API. + refreshed, err := client.TeamProjectAccess.Read(ctx, tpa.ID) + require.NoError(t, err) + + for _, item := range []*TeamProjectAccess{ + tpa, + refreshed, + } { + assert.NotEmpty(t, item.ID) + assert.Equal(t, options.Access, item.Access) + assert.Equal(t, *options.ProjectAccess.VariableSets, item.ProjectAccess.ProjectVariableSetsPermission) + assert.Equal(t, *options.WorkspaceAccess.Runs, item.WorkspaceAccess.WorkspaceRunsPermission) + } + }) + t.Run("with valid options for some custom TeamProject permissions", func(t *testing.T) { options := TeamProjectAccessAddOptions{ Access: *ProjectAccess(TeamProjectAccessCustom), @@ -355,13 +391,11 @@ func TestTeamProjectAccessesUpdate(t *testing.T) { }) t.Run("with valid custom permissions attributes for all permissions", func(t *testing.T) { - skipUnlessBeta(t) options := TeamProjectAccessUpdateOptions{ Access: ProjectAccess(TeamProjectAccessCustom), ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{ - Settings: ProjectSettingsPermission(ProjectSettingsPermissionUpdate), - Teams: ProjectTeamsPermission(ProjectTeamsPermissionManage), - VariableSets: ProjectVariableSetsPermission(ProjectVariableSetsPermissionRead), + Settings: ProjectSettingsPermission(ProjectSettingsPermissionUpdate), + Teams: ProjectTeamsPermission(ProjectTeamsPermissionManage), }, WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{ Runs: WorkspaceRunsPermission(WorkspaceRunsPermissionPlan), @@ -383,7 +417,6 @@ func TestTeamProjectAccessesUpdate(t *testing.T) { assert.Equal(t, tpa.Access, TeamProjectAccessCustom) assert.Equal(t, *options.ProjectAccess.Teams, tpa.ProjectAccess.ProjectTeamsPermission) assert.Equal(t, *options.ProjectAccess.Settings, tpa.ProjectAccess.ProjectSettingsPermission) - assert.Equal(t, *options.ProjectAccess.VariableSets, tpa.ProjectAccess.ProjectVariableSetsPermission) assert.Equal(t, *options.WorkspaceAccess.Runs, tpa.WorkspaceAccess.WorkspaceRunsPermission) assert.Equal(t, *options.WorkspaceAccess.SentinelMocks, tpa.WorkspaceAccess.WorkspaceSentinelMocksPermission) assert.Equal(t, *options.WorkspaceAccess.StateVersions, tpa.WorkspaceAccess.WorkspaceStateVersionsPermission) @@ -395,6 +428,48 @@ func TestTeamProjectAccessesUpdate(t *testing.T) { assert.Equal(t, true, tpa.WorkspaceAccess.WorkspaceRunTasksPermission) }) + t.Run("with valid custom permissions attributes for variable sets permissions", func(t *testing.T) { + skipUnlessBeta(t) + // create tpaCustomTest to verify unupdated attributes stay the same for custom permissions + // because going from admin to read to custom changes the values of all custom permissions + tm2Test, tm2TestCleanup := createTeam(t, client, orgTest) + defer tm2TestCleanup() + + TpaOptions := TeamProjectAccessAddOptions{ + Access: *ProjectAccess(TeamProjectAccessCustom), + Team: tm2Test, + Project: pTest, + } + + tpaCustomTest, err := client.TeamProjectAccess.Add(ctx, TpaOptions) + require.NoError(t, err) + + options := TeamProjectAccessUpdateOptions{ + Access: ProjectAccess(TeamProjectAccessCustom), + ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{ + VariableSets: ProjectVariableSetsPermission(ProjectVariableSetsPermissionRead), + }, + WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{ + Create: Bool(false), + }, + } + + tpa, err := client.TeamProjectAccess.Update(ctx, tpaCustomTest.ID, options) + require.NoError(t, err) + require.NotNil(t, options.ProjectAccess) + require.NotNil(t, options.WorkspaceAccess) + assert.Equal(t, *options.ProjectAccess.VariableSets, tpa.ProjectAccess.ProjectVariableSetsPermission) + assert.Equal(t, false, tpa.WorkspaceAccess.WorkspaceCreatePermission) + // assert that other attributes remain the same + assert.Equal(t, tpaCustomTest.ProjectAccess.ProjectSettingsPermission, tpa.ProjectAccess.ProjectSettingsPermission) + assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceLockingPermission, tpa.WorkspaceAccess.WorkspaceLockingPermission) + assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceMovePermission, tpa.WorkspaceAccess.WorkspaceMovePermission) + assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceDeletePermission, tpa.WorkspaceAccess.WorkspaceDeletePermission) + assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceRunsPermission, tpa.WorkspaceAccess.WorkspaceRunsPermission) + assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceSentinelMocksPermission, tpa.WorkspaceAccess.WorkspaceSentinelMocksPermission) + assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceStateVersionsPermission, tpa.WorkspaceAccess.WorkspaceStateVersionsPermission) + }) + t.Run("with valid custom permissions attributes for some permissions", func(t *testing.T) { // create tpaCustomTest to verify unupdated attributes stay the same for custom permissions // because going from admin to read to custom changes the values of all custom permissions @@ -435,6 +510,7 @@ func TestTeamProjectAccessesUpdate(t *testing.T) { assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceSentinelMocksPermission, tpa.WorkspaceAccess.WorkspaceSentinelMocksPermission) assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceStateVersionsPermission, tpa.WorkspaceAccess.WorkspaceStateVersionsPermission) }) + t.Run("with invalid custom permissions attributes", func(t *testing.T) { options := TeamProjectAccessUpdateOptions{ Access: ProjectAccess(TeamProjectAccessCustom), From ee7dcad3d2cf139aa1bb4c6fe798ef755d281e60 Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Fri, 10 Jan 2025 14:44:31 -0700 Subject: [PATCH 37/41] Update CHANGELOG.md (#1031) * Update CHANGELOG.md * Update CHANGELOG.md Unreleased line got deleted by mistake. --------- Co-authored-by: Luces Huayhuaca <21225410+uturunku1@users.noreply.github.com> --- CHANGELOG.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8bd6bab7..31020ba97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,18 @@ # Unreleased +# v1.73.1 + +## Bug fixes + +* Includes a critical security update in an upstream depdendency `hashicorp/go-slug` @NodyHub [#1025](https://github.com/hashicorp/go-tfe/pull/1025) +* Fix bug in BETA support for Linux arm64 agents, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users @natalie-todd [#1022](https://github.com/hashicorp/go-tfe/pull/1022) + # v1.73.0 ## Enhancements * Add support for team notification configurations @notchairmk [#1016](https://github.com/hashicorp/go-tfe/pull/1016) -## Bug Fixes - -* Update dependency `github.com/hashicorp/go-slug` `v0.16.0` => `v0.16.3` to integrate latest changes. -* Fix bug in BETA support for Linux arm64 agents, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users @natalie-todd [#1022](https://github.com/hashicorp/go-tfe/pull/1022) - # v1.72.0 ## Enhancements From 6aa126799818b73d119d2d8533e5fef5eba0f587 Mon Sep 17 00:00:00 2001 From: Luces Huayhuaca <21225410+uturunku1@users.noreply.github.com> Date: Mon, 13 Jan 2025 10:24:04 -0800 Subject: [PATCH 38/41] remove API coverage section from readme file. It hasn't been used or updated since it was added (#1033) --- README.md | 72 ------------------------------------------------------- 1 file changed, 72 deletions(-) diff --git a/README.md b/README.md index 0c0deac6c..a102cc1c5 100644 --- a/README.md +++ b/README.md @@ -113,78 +113,6 @@ if err != nil { For complete usage of the API client, see the [full package docs](https://pkg.go.dev/github.com/hashicorp/go-tfe). -## API Coverage - -This API client covers most of the existing HCP Terraform API calls and is updated regularly to add new or missing endpoints. - -- [x] Account -- [x] Agents -- [x] Agent Pools -- [x] Agent Tokens -- [x] Applies -- [x] Audit Trails -- [x] Changelog -- [x] Comments -- [x] Configuration Versions -- [x] Cost Estimation -- [ ] Feature Sets -- [ ] Invoices -- [x] IP Ranges -- [x] Notification Configurations -- [x] OAuth Clients -- [x] OAuth Tokens -- [x] Organizations -- [x] Organization Memberships -- [x] Organization Tags -- [x] Organization Tokens -- [x] Plan Exports -- [x] Plans -- [x] Policies -- [x] Policy Checks -- [x] Policy Sets -- [x] Policy Set Parameters -- [x] Private Registry - - [x] Modules - - [x] No-Code Modules - - [x] Providers - - [x] Provider Versions and Platforms - - [x] GPG Keys -- [x] Projects -- [x] Runs -- [x] Run Events -- [x] Run Tasks -- [x] Run Tasks Integration -- [x] Run Triggers -- [x] SSH Keys -- [x] Stability Policy -- [x] State Versions -- [x] State Version Outputs -- [ ] Subscriptions -- [x] Team Access -- [x] Team Membership -- [x] Team Tokens -- [x] Teams -- [x] Test Runs -- [x] User Tokens -- [x] Users -- [x] Variable Sets -- [x] Variables -- [ ] VCS Events -- [x] Workspaces -- [x] Workspace-Specific Variables -- [x] Workspace Resources -- [x] Admin - - [x] Module Sharing - - [x] Organizations - - [x] Runs - - [x] Settings - - [x] Terraform Versions - - [x] OPA Versions - - [x] Sentinel Versions - - [x] Users - - [x] Workspaces - - ## Examples See the [examples directory](https://github.com/hashicorp/go-tfe/tree/main/examples). From 41f4c7afe4bbc4b4c95ba977b1173281e69b07cd Mon Sep 17 00:00:00 2001 From: Michael Yocca <19916665+mjyocca@users.noreply.github.com> Date: Mon, 13 Jan 2025 12:31:30 -0800 Subject: [PATCH 39/41] fix(stacks/vcs): StackVCSRepo nested-struct incorrectly serializing attributes (#1029) hashicorp/jsonapi and by extension google/jsonapi packages have issues serializing nested-structs that are not relationship, but attr. Converting to json struct tag directive for the time being --- stack.go | 20 ++++++++++++++------ stack_integration_test.go | 10 +++++----- stack_plan_integration_test.go | 2 +- stack_source_integration_test.go | 2 +- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/stack.go b/stack.go index d45956450..7cb95bd8b 100644 --- a/stack.go +++ b/stack.go @@ -75,6 +75,14 @@ type StackVCSRepo struct { OAuthTokenID string `jsonapi:"attr,oauth-token-id,omitempty"` } +// StackVCSRepoOptions +type StackVCSRepoOptions struct { + 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. type Stack struct { ID string `jsonapi:"primary,stacks"` @@ -172,11 +180,11 @@ type StackReadOptions struct { // StackCreateOptions represents the options for creating a stack. The project // relation is required. type StackCreateOptions struct { - Type string `jsonapi:"primary,stacks"` - Name string `jsonapi:"attr,name"` - Description *string `jsonapi:"attr,description,omitempty"` - VCSRepo *StackVCSRepo `jsonapi:"attr,vcs-repo"` - Project *Project `jsonapi:"relation,project"` + Type string `jsonapi:"primary,stacks"` + Name string `jsonapi:"attr,name"` + Description *string `jsonapi:"attr,description,omitempty"` + VCSRepo *StackVCSRepoOptions `jsonapi:"attr,vcs-repo"` + Project *Project `jsonapi:"relation,project"` } // StackUpdateOptions represents the options for updating a stack. @@ -326,7 +334,7 @@ func (s StackCreateOptions) valid() error { return s.VCSRepo.valid() } -func (s StackVCSRepo) valid() error { +func (s StackVCSRepoOptions) valid() error { if s.Identifier == "" { return ErrRequiredVCSRepo } diff --git a/stack_integration_test.go b/stack_integration_test.go index 437120103..d8d82b554 100644 --- a/stack_integration_test.go +++ b/stack_integration_test.go @@ -31,7 +31,7 @@ func TestStackCreateAndList(t *testing.T) { stack1, err := client.Stacks.Create(ctx, StackCreateOptions{ Name: "aa-test-stack", - VCSRepo: &StackVCSRepo{ + VCSRepo: &StackVCSRepoOptions{ Identifier: "hashicorp-guides/pet-nulls-stack", OAuthTokenID: oauthClient.OAuthTokens[0].ID, }, @@ -45,7 +45,7 @@ func TestStackCreateAndList(t *testing.T) { stack2, err := client.Stacks.Create(ctx, StackCreateOptions{ Name: "zz-test-stack", - VCSRepo: &StackVCSRepo{ + VCSRepo: &StackVCSRepoOptions{ Identifier: "hashicorp-guides/pet-nulls-stack", OAuthTokenID: oauthClient.OAuthTokens[0].ID, }, @@ -143,7 +143,7 @@ func TestStackReadUpdateDelete(t *testing.T) { stack, err := client.Stacks.Create(ctx, StackCreateOptions{ Name: "test-stack", - VCSRepo: &StackVCSRepo{ + VCSRepo: &StackVCSRepoOptions{ Identifier: "brandonc/pet-nulls-stack", OAuthTokenID: oauthClient.OAuthTokens[0].ID, Branch: "main", @@ -200,7 +200,7 @@ func TestStackReadUpdateForceDelete(t *testing.T) { stack, err := client.Stacks.Create(ctx, StackCreateOptions{ Name: "test-stack", - VCSRepo: &StackVCSRepo{ + VCSRepo: &StackVCSRepoOptions{ Identifier: "brandonc/pet-nulls-stack", OAuthTokenID: oauthClient.OAuthTokens[0].ID, Branch: "main", @@ -356,7 +356,7 @@ func TestStackConverged(t *testing.T) { stack, err := client.Stacks.Create(ctx, StackCreateOptions{ Name: "test-stack", - VCSRepo: &StackVCSRepo{ + VCSRepo: &StackVCSRepoOptions{ Identifier: "brandonc/pet-nulls-stack", OAuthTokenID: oauthClient.OAuthTokens[0].ID, }, diff --git a/stack_plan_integration_test.go b/stack_plan_integration_test.go index ea5bbe834..29ae34a8a 100644 --- a/stack_plan_integration_test.go +++ b/stack_plan_integration_test.go @@ -29,7 +29,7 @@ func TestStackPlanList(t *testing.T) { stack, err := client.Stacks.Create(ctx, StackCreateOptions{ Name: "aa-test-stack", - VCSRepo: &StackVCSRepo{ + VCSRepo: &StackVCSRepoOptions{ Identifier: "brandonc/pet-nulls-stack", OAuthTokenID: oauthClient.OAuthTokens[0].ID, }, diff --git a/stack_source_integration_test.go b/stack_source_integration_test.go index fafaaa617..ab00fc8f2 100644 --- a/stack_source_integration_test.go +++ b/stack_source_integration_test.go @@ -26,7 +26,7 @@ func TestStackSourceCreateUploadAndRead(t *testing.T) { stack, err := client.Stacks.Create(ctx, StackCreateOptions{ Project: orgTest.DefaultProject, Name: "test-stack", - VCSRepo: &StackVCSRepo{ + VCSRepo: &StackVCSRepoOptions{ Identifier: "hashicorp-guides/pet-nulls-stack", OAuthTokenID: oauthClient.OAuthTokens[0].ID, }, From 5691516594ef91f3f1620a8a75702e76669a87f4 Mon Sep 17 00:00:00 2001 From: Netra Mali Date: Mon, 13 Jan 2025 17:32:40 -0500 Subject: [PATCH 40/41] additional test --- team_project_access_integration_test.go | 55 +++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/team_project_access_integration_test.go b/team_project_access_integration_test.go index 888e1d2c7..979da26aa 100644 --- a/team_project_access_integration_test.go +++ b/team_project_access_integration_test.go @@ -165,6 +165,61 @@ func TestTeamProjectAccessesAdd(t *testing.T) { } }) + t.Run("with no project access options for custom TeamProject permissions", func(t *testing.T) { + skipUnlessBeta(t) + options := TeamProjectAccessAddOptions{ + Access: *ProjectAccess(TeamProjectAccessCustom), + Team: tmTest, + Project: pTest, + ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{}, + WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{ + Runs: WorkspaceRunsPermission(WorkspaceRunsPermissionApply), + SentinelMocks: WorkspaceSentinelMocksPermission(WorkspaceSentinelMocksPermissionRead), + StateVersions: WorkspaceStateVersionsPermission(WorkspaceStateVersionsPermissionWrite), + Variables: WorkspaceVariablesPermission(WorkspaceVariablesPermissionWrite), + Create: Bool(true), + Locking: Bool(true), + Move: Bool(true), + Delete: Bool(false), + RunTasks: Bool(false), + }, + } + + tpa, err := client.TeamProjectAccess.Add(ctx, options) + defer func() { + err := client.TeamProjectAccess.Remove(ctx, tpa.ID) + if err != nil { + t.Logf("error removing team access (%s): %s", tpa.ID, err) + } + }() + + require.NoError(t, err) + + // Get a refreshed view from the API. + refreshed, err := client.TeamProjectAccess.Read(ctx, tpa.ID) + require.NoError(t, err) + + for _, item := range []*TeamProjectAccess{ + tpa, + refreshed, + } { + assert.NotEmpty(t, item.ID) + assert.Equal(t, options.Access, item.Access) + assert.Equal(t, *options.ProjectAccess.Settings, item.ProjectAccess.ProjectSettingsPermission) + assert.Equal(t, *options.ProjectAccess.Teams, item.ProjectAccess.ProjectTeamsPermission) + assert.Equal(t, *options.ProjectAccess.VariableSets, item.ProjectAccess.ProjectVariableSetsPermission) + assert.Equal(t, *options.WorkspaceAccess.Runs, item.WorkspaceAccess.WorkspaceRunsPermission) + assert.Equal(t, *options.WorkspaceAccess.SentinelMocks, item.WorkspaceAccess.WorkspaceSentinelMocksPermission) + assert.Equal(t, *options.WorkspaceAccess.StateVersions, item.WorkspaceAccess.WorkspaceStateVersionsPermission) + assert.Equal(t, *options.WorkspaceAccess.Variables, item.WorkspaceAccess.WorkspaceVariablesPermission) + assert.Equal(t, item.WorkspaceAccess.WorkspaceCreatePermission, true) + assert.Equal(t, item.WorkspaceAccess.WorkspaceLockingPermission, true) + assert.Equal(t, item.WorkspaceAccess.WorkspaceMovePermission, true) + assert.Equal(t, item.WorkspaceAccess.WorkspaceDeletePermission, false) + assert.Equal(t, item.WorkspaceAccess.WorkspaceRunTasksPermission, false) + } + }) + t.Run("with valid options for all custom TeamProject permissions", func(t *testing.T) { options := TeamProjectAccessAddOptions{ Access: *ProjectAccess(TeamProjectAccessCustom), From b7c71ca448c13d6b76804a445249ef8bfb880f2a Mon Sep 17 00:00:00 2001 From: Netra Mali Date: Tue, 14 Jan 2025 11:29:46 -0500 Subject: [PATCH 41/41] test fix --- team_project_access_integration_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/team_project_access_integration_test.go b/team_project_access_integration_test.go index 979da26aa..9770f6357 100644 --- a/team_project_access_integration_test.go +++ b/team_project_access_integration_test.go @@ -205,9 +205,6 @@ func TestTeamProjectAccessesAdd(t *testing.T) { } { assert.NotEmpty(t, item.ID) assert.Equal(t, options.Access, item.Access) - assert.Equal(t, *options.ProjectAccess.Settings, item.ProjectAccess.ProjectSettingsPermission) - assert.Equal(t, *options.ProjectAccess.Teams, item.ProjectAccess.ProjectTeamsPermission) - assert.Equal(t, *options.ProjectAccess.VariableSets, item.ProjectAccess.ProjectVariableSetsPermission) assert.Equal(t, *options.WorkspaceAccess.Runs, item.WorkspaceAccess.WorkspaceRunsPermission) assert.Equal(t, *options.WorkspaceAccess.SentinelMocks, item.WorkspaceAccess.WorkspaceSentinelMocksPermission) assert.Equal(t, *options.WorkspaceAccess.StateVersions, item.WorkspaceAccess.WorkspaceStateVersionsPermission)