diff --git a/.tutone.yml b/.tutone.yml index 9769f37fe..35195539d 100644 --- a/.tutone.yml +++ b/.tutone.yml @@ -44,6 +44,8 @@ packages: max_query_field_depth: 3 - name: accountManagementUpdateAccount max_query_field_depth: 3 + - name: accountManagementCancelAccount + max_query_field_depth: 3 - name: accounts path: pkg/accounts @@ -1242,8 +1244,8 @@ packages: max_query_field_depth: 2 # - name: organizationCreateSharedAccount # max_query_field_depth: 2 - # - name: organizationRevokeSharedAccount - # max_query_field_depth: 2 + - name: organizationRevokeSharedAccount + max_query_field_depth: 2 - name: organizationUpdateSharedAccount max_query_field_depth: 2 types: diff --git a/pkg/accountmanagement/accountmanagement_api.go b/pkg/accountmanagement/accountmanagement_api.go index 09bef5ab1..1259a6585 100644 --- a/pkg/accountmanagement/accountmanagement_api.go +++ b/pkg/accountmanagement/accountmanagement_api.go @@ -7,6 +7,48 @@ import ( "github.com/newrelic/newrelic-client-go/v2/pkg/errors" ) +// Cancels an account. +func (a *Accountmanagement) AccountManagementCancelAccount( + iD int, +) (*AccountManagementManagedAccount, error) { + return a.AccountManagementCancelAccountWithContext(context.Background(), + iD, + ) +} + +// Cancels an account. +func (a *Accountmanagement) AccountManagementCancelAccountWithContext( + ctx context.Context, + iD int, +) (*AccountManagementManagedAccount, error) { + + resp := AccountManagementCancelAccountQueryResponse{} + vars := map[string]interface{}{ + "id": iD, + } + + if err := a.client.NerdGraphQueryWithContext(ctx, AccountManagementCancelAccountMutation, vars, &resp); err != nil { + return nil, err + } + + return &resp.AccountManagementManagedAccount, nil +} + +type AccountManagementCancelAccountQueryResponse struct { + AccountManagementManagedAccount AccountManagementManagedAccount `json:"AccountManagementCancelAccount"` +} + +const AccountManagementCancelAccountMutation = `mutation( + $id: Int!, +) { accountManagementCancelAccount( + id: $id, +) { + id + isCanceled + name + regionCode +} }` + // Creates an organization-scoped account. func (a *Accountmanagement) AccountManagementCreateAccount( managedAccount AccountManagementCreateInput, @@ -45,6 +87,7 @@ const AccountManagementCreateAccountMutation = `mutation( ) { managedAccount { id + isCanceled name regionCode } @@ -93,6 +136,10 @@ const AccountManagementUpdateAccountMutation = `mutation( } } }` +// NOTE: Tutone changes made to GetManagedAccounts and dependent functions and queries have been reverted +// owing to a few limitations. In order to use the recently added attribute "isCanceled" with GetManagedAccounts, +// please see the functions in accountmanagement_api_.go. + // Admin-level info about the accounts in an organization. func (a *Accountmanagement) GetManagedAccounts() (*[]AccountManagementManagedAccount, error) { return a.GetManagedAccountsWithContext(context.Background()) @@ -119,6 +166,7 @@ func (a *Accountmanagement) GetManagedAccountsWithContext( const getManagedAccountsQuery = `query { actor { organization { accountManagement { managedAccounts { id + isCanceled name regionCode } } } } }` diff --git a/pkg/accountmanagement/accountmanagement_api_.go b/pkg/accountmanagement/accountmanagement_api_.go new file mode 100644 index 000000000..7c5995f9d --- /dev/null +++ b/pkg/accountmanagement/accountmanagement_api_.go @@ -0,0 +1,73 @@ +// Code **NOT** generated by tutone +package accountmanagement + +import ( + "context" + + "github.com/newrelic/newrelic-client-go/v2/pkg/errors" +) + +// -------------------------------------------------------------------- +// NOTE: (for the maintainers and users of newrelic-client-go) +// -------------------------------------------------------------------- +// The function `GetManagedAccountsWithAdditionalArguments`, function `GetManagedAccountsWithAdditionalArgumentsWithContext` and the query `getManagedAccountsWithAdditionalArgumentsQuery` in this file +// are "modified" versions of the function `GetManagedAccounts`, the function `GetManagedAccountsWithContext` and the query `getManagedAccountsQuery` respectively, +// originally defined in accountmanagement_api.go. +// +// These manual modifications had to be written owing to the introduction of a new input (and output) field to the `getManagedAccountsQuery`, "isCanceled", which +// is causing conflicts with the Tutone generated code; see the PR linked to this change for more details on the exact limitations. +// +// Owing to this, in order to facilitate using these functions with the isCanceled attribute, modified versions of the functions and mutations have been added to +// this file. This would also allow us to ensure the older counterparts of these functions defined in accountmanagement_api.go inflict no breaking changes onto upstream +// services, such as the New Relic Terraform Provider. While we shall aim to bring this under Tutone's scope, please use the functions in this file to use the "isCanceled" +// attribute recently added to this query. +// +// TL;DR The functions in this file are NOT Tutone generated; we would eventually need to move functionalities in these duplicated functions into the original ones +// in accountmanagement_api.go . + +// Admin-level info about the accounts in an organization. +func (a *Accountmanagement) GetManagedAccountsWithAdditionalArguments( + isCanceled *bool, +) (*[]AccountManagementManagedAccount, error) { + return a.GetManagedAccountsWithAdditionalArgumentsWithContext(context.Background(), + isCanceled, + ) +} + +// Admin-level info about the accounts in an organization. +func (a *Accountmanagement) GetManagedAccountsWithAdditionalArgumentsWithContext( + ctx context.Context, + isCanceled *bool, +) (*[]AccountManagementManagedAccount, error) { + + resp := managedAccountsResponse{} + vars := map[string]interface{}{ + "isCanceled": &isCanceled, + } + + if err := a.client.NerdGraphQueryWithContext(ctx, getManagedAccountsWithAdditionalArgumentsQuery, vars, &resp); err != nil { + return nil, err + } + + if len(resp.Actor.Organization.AccountManagement.ManagedAccounts) == 0 { + return nil, errors.NewNotFound("") + } + + return &resp.Actor.Organization.AccountManagement.ManagedAccounts, nil +} + +const getManagedAccountsWithAdditionalArgumentsQuery = `query ($isCanceled: Boolean) { + actor { + organization { + accountManagement { + managedAccounts(isCanceled: $isCanceled) { + id + isCanceled + name + regionCode + } + } + } + } +} +` diff --git a/pkg/accountmanagement/accountmanagement_integration_test.go b/pkg/accountmanagement/accountmanagement_integration_test.go index 5fed396a6..9dba1aef6 100644 --- a/pkg/accountmanagement/accountmanagement_integration_test.go +++ b/pkg/accountmanagement/accountmanagement_integration_test.go @@ -1,7 +1,9 @@ package accountmanagement import ( + "log" "testing" + "time" "github.com/stretchr/testify/require" @@ -64,3 +66,138 @@ func TestIntegrationUpdateAccountError(t *testing.T) { require.Nil(t, actual) require.NotNil(t, err) } + +func TestIntegrationAccountManagement_CreateUpdateCancelAccount(t *testing.T) { + t.Parallel() + accountManagementClient := newAccountManagementTestClient(t) + + // Create Account + name := "client-go-test-account-" + mock.RandSeq(5) + createAccountInput := AccountManagementCreateInput{ + Name: name, + RegionCode: "us01", + } + createAccountResponse, err := accountManagementClient.AccountManagementCreateAccount(createAccountInput) + + require.Nil(t, err) + require.NotNil(t, createAccountResponse.ManagedAccount.ID) + require.Equal(t, createAccountInput.RegionCode, createAccountResponse.ManagedAccount.RegionCode) + require.Equal(t, createAccountInput.Name, createAccountResponse.ManagedAccount.Name) + time.Sleep(time.Second * 2) + + // Update Account + updateAccountInput := AccountManagementUpdateInput{ + ID: createAccountResponse.ManagedAccount.ID, + Name: name + "-updated", + } + updateAccountResponse, err := accountManagementClient.AccountManagementUpdateAccount(updateAccountInput) + + require.Nil(t, err) + require.NotNil(t, updateAccountResponse.ManagedAccount.ID) + require.Equal(t, updateAccountResponse.ManagedAccount.ID, createAccountResponse.ManagedAccount.ID) + require.Equal(t, updateAccountInput.Name, updateAccountResponse.ManagedAccount.Name) + time.Sleep(time.Second * 3) + + // Get Account + getAccountResponse, err := accountManagementClient.GetManagedAccounts() + + require.Nil(t, err) + require.NotNil(t, getAccountResponse) + foundAccountInGetResponse := false + + for _, account := range *getAccountResponse { + if account.ID == updateAccountResponse.ManagedAccount.ID { + foundAccountInGetResponse = true + break + } + } + + require.True(t, foundAccountInGetResponse) + + // Cancel Account + cancelAccountResponse, err := accountManagementClient.AccountManagementCancelAccount(createAccountResponse.ManagedAccount.ID) + + require.Nil(t, err) + require.NotNil(t, cancelAccountResponse) + time.Sleep(time.Second * 2) + + // Get Account to Confirm Account Cancellation based on the value of `isCanceled` + isCancelled := true + getAccountResponse, err = accountManagementClient.GetManagedAccountsWithAdditionalArguments(&isCancelled) + + require.Nil(t, err) + require.NotNil(t, getAccountResponse) + foundAccountInGetResponse = false + + for _, account := range *getAccountResponse { + if account.ID == updateAccountResponse.ManagedAccount.ID { + foundAccountInGetResponse = true + require.True(t, account.IsCanceled) + break + } + } + + require.True(t, foundAccountInGetResponse) + +} + +func TestIntegrationGetManagedAccounts(t *testing.T) { + t.Parallel() + accountManagementClient := newAccountManagementTestClient(t) + + actual, _ := accountManagementClient.GetManagedAccounts() + + log.Println(actual) + require.NotNil(t, actual) + require.NotZero(t, len(*actual)) +} + +func TestIntegrationGetManagedAccountsModified_CanceledAccounts(t *testing.T) { + t.Parallel() + accountManagementClient := newAccountManagementTestClient(t) + + cancelled := true + actual, _ := accountManagementClient.GetManagedAccountsWithAdditionalArguments(&cancelled) + log.Println(actual) + require.NotNil(t, actual) + require.NotZero(t, len(*actual)) +} + +func TestIntegrationGetManagedAccountsModified_NonCanceledAccounts(t *testing.T) { + t.Parallel() + accountManagementClient := newAccountManagementTestClient(t) + + cancelled := false + actual, _ := accountManagementClient.GetManagedAccountsWithAdditionalArguments(&cancelled) + log.Println(actual) + require.NotNil(t, actual) + require.NotZero(t, len(*actual)) +} + +func TestIntegrationGetManagedAccountsModified_AllCancellationStatuses(t *testing.T) { + t.Parallel() + accountManagementClient := newAccountManagementTestClient(t) + + actual, _ := accountManagementClient.GetManagedAccountsWithAdditionalArguments(nil) + + require.NotNil(t, actual) + require.NotZero(t, len(*actual)) + + foundCancelledAccount := false + foundUncancelledAccount := false + + for _, acct := range *actual { + if foundUncancelledAccount == true && foundCancelledAccount == true { + break + } + if acct.IsCanceled == true { + foundCancelledAccount = true + } + if acct.IsCanceled == false { + foundUncancelledAccount = true + } + } + + require.True(t, foundCancelledAccount) + require.True(t, foundUncancelledAccount) +} diff --git a/pkg/accountmanagement/accountmanagement_unit_test.go b/pkg/accountmanagement/accountmanagement_unit_test.go index 6c62aafb3..e0cdd2b2e 100644 --- a/pkg/accountmanagement/accountmanagement_unit_test.go +++ b/pkg/accountmanagement/accountmanagement_unit_test.go @@ -26,6 +26,14 @@ var ( "regionCode": "us01" } } + }` + testCancelAccountResponseJSON = `{ + "accountManagementCancelAccount": { + "id": 3833407, + "isCanceled": true, + "name": "test sub account", + "regionCode": "us01" + } }` ) @@ -78,3 +86,22 @@ func TestCreateAccount(t *testing.T) { assert.NotNil(t, actual) assert.Equal(t, expected, actual) } + +func TestCancelAccount(t *testing.T) { + t.Parallel() + respJSON := fmt.Sprintf(`{ "data":%s }`, testCancelAccountResponseJSON) + accountManagement := newMockResponse(t, respJSON, http.StatusCreated) + + expected := &AccountManagementManagedAccount{ + Name: "test sub account", + RegionCode: "us01", + ID: 3833407, + IsCanceled: true, + } + + actual, err := accountManagement.AccountManagementCancelAccount(3833407) + + assert.NoError(t, err) + assert.NotNil(t, actual) + assert.Equal(t, expected, actual) +} diff --git a/pkg/accountmanagement/types.go b/pkg/accountmanagement/types.go index eb38b21c7..824ea366b 100644 --- a/pkg/accountmanagement/types.go +++ b/pkg/accountmanagement/types.go @@ -5,6 +5,8 @@ package accountmanagement type AccountManagementCreateInput struct { // The name of the account. Name string `json:"name"` + // The id of the managed organization where the account will be created. + OrganizationId int `json:"organizationId,omitempty"` // The data center region for the account RegionCode string `json:"regionCode,omitempty"` } @@ -19,6 +21,8 @@ type AccountManagementCreateResponse struct { type AccountManagementManagedAccount struct { // The account ID. ID int `json:"id"` + // True if account is canceled + IsCanceled bool `json:"isCanceled"` // The name of the account. Name string `json:"name"` // The data center region for the account (US or EU). @@ -57,6 +61,8 @@ type Organization struct { AccountManagement AccountManagementOrganizationStitchedFields `json:"accountManagement,omitempty"` // The customer id for the organization. CustomerId string `json:"customerId,omitempty"` + // The ID of the organization. + ID int `json:"id,omitempty"` // The name of the organization. Name string `json:"name,omitempty"` // The telemetry id for the organization @@ -66,3 +72,10 @@ type Organization struct { type managedAccountsResponse struct { Actor Actor `json:"actor"` } + +// ID - The `ID` scalar type represents a unique identifier, often used to +// refetch an object or as key for a cache. The ID type appears in a JSON +// response as a String; however, it is not intended to be human-readable. +// When expected as an input type, any string (such as `"4"`) or integer +// (such as `4`) input value will be accepted as an ID. +type ID string diff --git a/pkg/organization/organization_api.go b/pkg/organization/organization_api.go index c1c98cfd9..8a729b349 100644 --- a/pkg/organization/organization_api.go +++ b/pkg/organization/organization_api.go @@ -65,6 +65,54 @@ const OrganizationCreateMutation = `mutation( jobId } }` +// The shared account to revoke +func (a *Organization) OrganizationRevokeSharedAccount( + sharedAccount OrganizationRevokeSharedAccountInput, +) (*OrganizationRevokeSharedAccountResponse, error) { + return a.OrganizationRevokeSharedAccountWithContext(context.Background(), + sharedAccount, + ) +} + +// The shared account to revoke +func (a *Organization) OrganizationRevokeSharedAccountWithContext( + ctx context.Context, + sharedAccount OrganizationRevokeSharedAccountInput, +) (*OrganizationRevokeSharedAccountResponse, error) { + + resp := OrganizationRevokeSharedAccountQueryResponse{} + vars := map[string]interface{}{ + "sharedAccount": sharedAccount, + } + + if err := a.client.NerdGraphQueryWithContext(ctx, OrganizationRevokeSharedAccountMutation, vars, &resp); err != nil { + return nil, err + } + + return &resp.OrganizationRevokeSharedAccountResponse, nil +} + +type OrganizationRevokeSharedAccountQueryResponse struct { + OrganizationRevokeSharedAccountResponse OrganizationRevokeSharedAccountResponse `json:"OrganizationRevokeSharedAccount"` +} + +const OrganizationRevokeSharedAccountMutation = `mutation( + $sharedAccount: OrganizationRevokeSharedAccountInput!, +) { organizationRevokeSharedAccount( + sharedAccount: $sharedAccount, +) { + sharedAccount { + accountId + id + limitingRoleId + name + sourceOrganizationId + sourceOrganizationName + targetOrganizationId + targetOrganizationName + } +} }` + // The organization to update func (a *Organization) OrganizationUpdate( organization OrganizationUpdateInput, diff --git a/pkg/organization/organization_integration_test.go b/pkg/organization/organization_integration_test.go index a7f64a087..7c149b58d 100644 --- a/pkg/organization/organization_integration_test.go +++ b/pkg/organization/organization_integration_test.go @@ -120,3 +120,23 @@ func TestIntegrationOrganizationUpdate_AccessDeniedError(t *testing.T) { require.Error(t, err) require.True(t, matchOrganizationUnauthorizedErrorRegex(err.Error())) } + +func TestIntegrationOrganizationRevokeSharedAccount_Error(t *testing.T) { + t.Parallel() + _, err := mock.GetTestAccountID() + if err != nil { + t.Skipf("%s", err) + } + + client := newIntegrationTestClient(t) + + response, _ := client.OrganizationRevokeSharedAccount( + OrganizationRevokeSharedAccountInput{ID: fmt.Sprint(unitTestMockAccountOneId)}, + ) + + // current API behaviour appears to be returning zero against the Shared Account ID if the operation is unsuccessful + require.Zero(t, response.SharedAccount.AccountID) + + //require.Error(t, err) + //require.True(t, matchOrganizationUnauthorizedErrorRegex(err.Error())) +} diff --git a/pkg/organization/organization_unit_test.go b/pkg/organization/organization_unit_test.go index 4308de62b..4e297cc1b 100644 --- a/pkg/organization/organization_unit_test.go +++ b/pkg/organization/organization_unit_test.go @@ -42,6 +42,21 @@ var ( } } }` + + testRevokeSharedAccountResponseJSON = `{ + "data": { + "organizationRevokeSharedAccount": { + "sharedAccount": { + "accountId": ` + fmt.Sprint(unitTestMockAccountOneId) + `, + "id": "` + unitTestMockOrganizationOneId + `", + "limitingRoleId": ` + fmt.Sprint(unitTestMockLimitingRoleId) + `, + "name": "` + unitTestMockOrganizationOneName + `", + "sourceOrganizationId": "` + unitTestMockOrganizationOneId + `", + "sourceOrganizationName": "` + unitTestMockOrganizationOneName + `" + } + } + } + }` ) func TestUnitCreateOrganization(t *testing.T) { @@ -120,3 +135,30 @@ func TestUnitOrganizationUpdateSharedAccount(t *testing.T) { assert.NotNil(t, actual) assert.Equal(t, expected, actual) } + +func TestUnitOrganizationRevokeSharedAccount(t *testing.T) { + t.Parallel() + + organization := newMockResponse(t, testRevokeSharedAccountResponseJSON, http.StatusOK) + + expected := &OrganizationRevokeSharedAccountResponse{ + SharedAccount: OrganizationSharedAccount{ + AccountID: unitTestMockAccountOneId, + ID: unitTestMockOrganizationOneId, + LimitingRoleId: unitTestMockLimitingRoleId, + Name: unitTestMockOrganizationOneName, + SourceOrganizationId: unitTestMockOrganizationOneId, + SourceOrganizationName: unitTestMockOrganizationOneName, + }, + } + + actual, err := organization.OrganizationRevokeSharedAccount( + OrganizationRevokeSharedAccountInput{ + ID: fmt.Sprint(unitTestMockAccountOneId), + }, + ) + + assert.NoError(t, err) + assert.NotNil(t, actual) + assert.Equal(t, expected, actual) +} diff --git a/pkg/organization/types.go b/pkg/organization/types.go index 4231d3185..367133d21 100644 --- a/pkg/organization/types.go +++ b/pkg/organization/types.go @@ -85,6 +85,18 @@ type OrganizationNewManagedAccountInput struct { RegionCode OrganizationRegionCodeEnum `json:"regionCode,omitempty"` } +// OrganizationRevokeSharedAccountInput - Attributes for revoking an account share. +type OrganizationRevokeSharedAccountInput struct { + // The id of the account share to be revoked + ID string `json:"id"` +} + +// OrganizationRevokeSharedAccountResponse - The object that's returned from successfully revoking a shared account. +type OrganizationRevokeSharedAccountResponse struct { + // Information about the revoked shared account. + SharedAccount OrganizationSharedAccount `json:"sharedAccount,omitempty"` +} + // OrganizationSharedAccount - The attributes of an account share. type OrganizationSharedAccount struct { // The ID of the account being shared.