From aabd3559f5745a58a1c57f55877cf66fc9648ae7 Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Fri, 11 Oct 2024 16:38:26 +0900 Subject: [PATCH 01/25] Add user management functionality --- internal/client/user.go | 133 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 internal/client/user.go diff --git a/internal/client/user.go b/internal/client/user.go new file mode 100644 index 00000000..d2a30d7f --- /dev/null +++ b/internal/client/user.go @@ -0,0 +1,133 @@ +package client + +import ( + "fmt" + "net/http" + "net/url" +) + +const BasePath = "/api/users" + +type User struct { + ID int64 `json:"id"` + Email string `json:"email"` + Role string `json:"role"` + CanUseBasicService bool `json:"can_use_basic_service"` + CanUseAuditLog bool `json:"can_use_audit_log"` + IsRestrictedConnectionModify bool `json:"is_restricted_connection_modify"` + LastSignInAt string `json:"last_sign_in_at"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +// List of Users + +type ListUsersInput struct { + limit *int + cursor *string +} + +func (input *ListUsersInput) SetLimit(limit int) { + input.limit = &limit +} + +func (input *ListUsersInput) SetCursor(cursor string) { + input.cursor = &cursor +} + +type ListUsersOutput struct { + Items []User + NextCursor string +} + +const MaxListUsersLimit = 100 + +func (client *TroccoClient) ListUsers(input *ListUsersInput) (*ListUsersOutput, error) { + params := url.Values{} + if input != nil && input.limit != nil { + if *input.limit < 1 || *input.limit > MaxListUsersLimit { + return nil, fmt.Errorf("limit must be between 1 and %d", MaxListUsersLimit) + } + params.Add("limit", fmt.Sprintf("%d", *input.limit)) + } + if input != nil && input.cursor != nil { + params.Add("cursor", *input.cursor) + } + path := fmt.Sprintf(BasePath+"?%s", params.Encode()) + output := new(ListUsersOutput) + err := client.do(http.MethodGet, path, nil, output) + if err != nil { + return nil, err + } + return output, nil +} + +// Get a User + +type GetUserOutput struct { + User User +} + +func (client *TroccoClient) GetUser(id int64) (*GetUserOutput, error) { + path := fmt.Sprintf(BasePath+"/%d", id) + output := new(GetUserOutput) + err := client.do(http.MethodGet, path, nil, output) + if err != nil { + return nil, err + } + return output, nil +} + +// Create a User + +type CreateUserInput struct { + Email string `json:"email"` + Password string `json:"password"` + Role string `json:"role"` + CanUseBasicService bool `json:"can_use_basic_service"` + CanUseAuditLog bool `json:"can_use_audit_log"` + IsRestrictedConnectionModify bool `json:"is_restricted_connection_modify"` +} + +type CreateUserOutput struct { + User User +} + +func (client *TroccoClient) CreateUser(input *CreateUserInput) (*CreateUserOutput, error) { + output := new(CreateUserOutput) + err := client.do(http.MethodPost, BasePath, input, output) + if err != nil { + return nil, err + } + return output, nil +} + +// Update a User + +type UpdateUserInput struct { + Role string `json:"role"` + CanUseBasicService bool `json:"can_use_basic_service"` + CanUseAuditLog bool `json:"can_use_audit_log"` + IsRestrictedConnectionModify bool `json:"is_restricted_connection_modify"` +} + +type UpdateUserOutput struct { + User User +} + +func (client *TroccoClient) UpdateUser(id int64, input *UpdateUserInput) (*UpdateUserOutput, error) { + path := fmt.Sprintf(BasePath+"/%d", id) + output := new(UpdateUserOutput) + err := client.do(http.MethodPatch, path, input, output) + if err != nil { + return nil, err + } + return output, nil +} + +// Delete a User + +func (client *TroccoClient) DeleteUser(id int64) error { + path := fmt.Sprintf(BasePath+"/%d", id) + return client.do(http.MethodDelete, path, nil, nil) +} From 8f6570a862c77066fa010a19e41a48fc4292d9f1 Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Wed, 16 Oct 2024 20:01:32 +0900 Subject: [PATCH 02/25] Add test cases for listing users with limit and cursor --- internal/client/api_test_helper.go | 30 +++++ internal/client/datamart_definition_test.go | 27 ----- internal/client/user.go | 4 +- internal/client/user_test.go | 117 ++++++++++++++++++++ 4 files changed, 149 insertions(+), 29 deletions(-) create mode 100644 internal/client/api_test_helper.go create mode 100644 internal/client/user_test.go diff --git a/internal/client/api_test_helper.go b/internal/client/api_test_helper.go new file mode 100644 index 00000000..3fb64c88 --- /dev/null +++ b/internal/client/api_test_helper.go @@ -0,0 +1,30 @@ +package client + +import ( + "reflect" + "testing" +) + +type Case struct { + name string + value interface{} + expected interface{} +} + +func testCases(t *testing.T, cases []Case) { + for _, c := range cases { + value := c.value + if c.expected == nil { + if !reflect.ValueOf(value).IsNil() { + t.Errorf("Expected %s to be nil, got %v", c.name, value) + } + continue + } + if reflect.ValueOf(value).Kind() == reflect.Ptr { + value = reflect.ValueOf(value).Elem().Interface() + } + if c.expected != value { + t.Errorf("Expected %s to be %v, got %v", c.name, c.expected, value) + } + } +} diff --git a/internal/client/datamart_definition_test.go b/internal/client/datamart_definition_test.go index 028bd1c4..9f6db0ec 100644 --- a/internal/client/datamart_definition_test.go +++ b/internal/client/datamart_definition_test.go @@ -4,36 +4,9 @@ import ( "io" "net/http" "net/http/httptest" - "reflect" "testing" ) -// Helpers - -type Case struct { - name string - value interface{} - expected interface{} -} - -func testCases(t *testing.T, cases []Case) { - for _, c := range cases { - value := c.value - if c.expected == nil { - if !reflect.ValueOf(value).IsNil() { - t.Errorf("Expected %s to be nil, got %v", c.name, value) - } - continue - } - if reflect.ValueOf(value).Kind() == reflect.Ptr { - value = reflect.ValueOf(value).Elem().Interface() - } - if c.expected != value { - t.Errorf("Expected %s to be %v, got %v", c.name, c.expected, value) - } - } -} - // ListDatamartDefinitions func TestListDatamartDefinitions(t *testing.T) { diff --git a/internal/client/user.go b/internal/client/user.go index d2a30d7f..71bdc76d 100644 --- a/internal/client/user.go +++ b/internal/client/user.go @@ -36,8 +36,8 @@ func (input *ListUsersInput) SetCursor(cursor string) { } type ListUsersOutput struct { - Items []User - NextCursor string + Items []User `json:"items"` + NextCursor *string `json:"next_cursor"` } const MaxListUsersLimit = 100 diff --git a/internal/client/user_test.go b/internal/client/user_test.go new file mode 100644 index 00000000..d4e51ec3 --- /dev/null +++ b/internal/client/user_test.go @@ -0,0 +1,117 @@ +package client + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +// ListUsers + +func TestListUsers(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cases := []Case{ + {"path", r.URL.Path, "/api/users"}, + {"method", r.Method, http.MethodGet}, + } + testCases(t, cases) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + resp := ` + { + "items": [ + { + "id": 1, + "email": "test1@example.com", + "role": "admin", + "can_use_audit_log": true, + "is_restricted_connection_modify": false, + "last_sign_in_at": "2024-07-29T19:00:00.000+09:00", + "created_at": "2024-07-29T19:00:00.000+09:00", + "updated_at": "2024-07-29T20:00:00.000+09:00" + }, + { + "id": 2, + "email": "test2@example.com", + "role": "member", + "can_use_audit_log": false, + "is_restricted_connection_modify": true, + "last_sign_in_at": "2024-07-29T19:00:00.000+09:00", + "created_at": "2024-07-29T21:00:00.000+09:00", + "updated_at": "2024-07-29T22:00:00.000+09:00" + } + ] + } + ` + _, err := w.Write([]byte(resp)) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + })) + defer server.Close() + + client := NewDevTroccoClient("1234567890", server.URL) + output, err := client.ListUsers(nil) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + if len(output.Items) != 2 { + t.Errorf("Expected output.Items to have 2 items, got %d", len(output.Items)) + } + cases := []Case{ + {"first item's ID", output.Items[0].ID, int64(1)}, + {"first item's email", output.Items[0].Email, "test1@example.com"}, + {"first item's role", output.Items[0].Role, "admin"}, + {"first item's can_use_audit_log", output.Items[0].CanUseAuditLog, true}, + {"first item's is_restricted_connection_modify", output.Items[0].IsRestrictedConnectionModify, false}, + {"first item's last_sign_in_at", output.Items[0].LastSignInAt, "2024-07-29T19:00:00.000+09:00"}, + {"first item's created_at", output.Items[0].CreatedAt, "2024-07-29T19:00:00.000+09:00"}, + {"first item's updated_at", output.Items[0].UpdatedAt, "2024-07-29T20:00:00.000+09:00"}, + {"second item's ID", output.Items[1].ID, int64(2)}, + {"second item's email", output.Items[1].Email, "test2@example.com"}, + {"second item's role", output.Items[1].Role, "member"}, + {"second item's can_use_audit_log", output.Items[1].CanUseAuditLog, false}, + {"second item's is_restricted_connection_modify", output.Items[1].IsRestrictedConnectionModify, true}, + {"second item's last_sign_in_at", output.Items[1].LastSignInAt, "2024-07-29T19:00:00.000+09:00"}, + {"second item's created_at", output.Items[1].CreatedAt, "2024-07-29T21:00:00.000+09:00"}, + {"second item's updated_at", output.Items[1].UpdatedAt, "2024-07-29T22:00:00.000+09:00"}, + } + testCases(t, cases) +} + +func TestListUsersLimitAndCursor(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cases := []Case{ + {"query parameter limit", r.URL.Query().Get("limit"), "1"}, + {"query parameter cursor", r.URL.Query().Get("cursor"), "test_prev_cursor"}, + } + testCases(t, cases) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + resp := ` + { + "items": [], + "next_cursor": "test_next_cursor" + } + ` + _, err := w.Write([]byte(resp)) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + })) + defer server.Close() + + client := NewDevTroccoClient("1234567890", server.URL) + input := ListUsersInput{} + input.SetLimit(1) + input.SetCursor("test_prev_cursor") + output, err := client.ListUsers(&input) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + cases := []Case{ + {"next_cursor", *output.NextCursor, "test_next_cursor"}, + } + testCases(t, cases) +} From 75d73f9e1bb625ad38f5616c5b2e1b6bd6558e1c Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Fri, 18 Oct 2024 17:04:20 +0900 Subject: [PATCH 03/25] Add GetUser method and related structs for TroccoClient --- internal/client/user.go | 30 +++++++++++++++++-- internal/client/user_test.go | 48 ++++++++++++++++++++++++++++++ internal/provider/user_resource.go | 1 + 3 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 internal/provider/user_resource.go diff --git a/internal/client/user.go b/internal/client/user.go index 71bdc76d..e3d62dee 100644 --- a/internal/client/user.go +++ b/internal/client/user.go @@ -65,7 +65,15 @@ func (client *TroccoClient) ListUsers(input *ListUsersInput) (*ListUsersOutput, // Get a User type GetUserOutput struct { - User User + ID int64 `json:"id"` + Email string `json:"email"` + Role string `json:"role"` + CanUseBasicService bool `json:"can_use_basic_service"` + CanUseAuditLog bool `json:"can_use_audit_log"` + IsRestrictedConnectionModify bool `json:"is_restricted_connection_modify"` + LastSignInAt string `json:"last_sign_in_at"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` } func (client *TroccoClient) GetUser(id int64) (*GetUserOutput, error) { @@ -90,7 +98,15 @@ type CreateUserInput struct { } type CreateUserOutput struct { - User User + ID int64 `json:"id"` + Email string `json:"email"` + Role string `json:"role"` + CanUseBasicService bool `json:"can_use_basic_service"` + CanUseAuditLog bool `json:"can_use_audit_log"` + IsRestrictedConnectionModify bool `json:"is_restricted_connection_modify"` + LastSignInAt string `json:"last_sign_in_at"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` } func (client *TroccoClient) CreateUser(input *CreateUserInput) (*CreateUserOutput, error) { @@ -112,7 +128,15 @@ type UpdateUserInput struct { } type UpdateUserOutput struct { - User User + ID int64 `json:"id"` + Email string `json:"email"` + Role string `json:"role"` + CanUseBasicService bool `json:"can_use_basic_service"` + CanUseAuditLog bool `json:"can_use_audit_log"` + IsRestrictedConnectionModify bool `json:"is_restricted_connection_modify"` + LastSignInAt string `json:"last_sign_in_at"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` } func (client *TroccoClient) UpdateUser(id int64, input *UpdateUserInput) (*UpdateUserOutput, error) { diff --git a/internal/client/user_test.go b/internal/client/user_test.go index d4e51ec3..5e68d11a 100644 --- a/internal/client/user_test.go +++ b/internal/client/user_test.go @@ -115,3 +115,51 @@ func TestListUsersLimitAndCursor(t *testing.T) { } testCases(t, cases) } + +// GetUser + +func TestGetUser(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cases := []Case{ + {"path", r.URL.Path, "/api/users/1"}, + {"method", r.Method, http.MethodGet}, + } + testCases(t, cases) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + resp := ` + { + "id": 1, + "email": "test1@example.com", + "role": "admin", + "can_use_audit_log": true, + "is_restricted_connection_modify": false, + "last_sign_in_at": "2024-07-29T19:00:00.000+09:00", + "created_at": "2024-07-29T19:00:00.000+09:00", + "updated_at": "2024-07-29T20:00:00.000+09:00" + } + ` + _, err := w.Write([]byte(resp)) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + })) + defer server.Close() + + client := NewDevTroccoClient("1234567890", server.URL) + output, err := client.GetUser(1) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + cases := []Case{ + {"ID", output.ID, int64(1)}, + {"email", output.Email, "test1@example.com"}, + {"role", output.Role, "admin"}, + {"can_use_audit_log", output.CanUseAuditLog, true}, + {"is_restricted_connection_modify", output.IsRestrictedConnectionModify, false}, + {"last_sign_in_at", output.LastSignInAt, "2024-07-29T19:00:00.000+09:00"}, + {"created_at", output.CreatedAt, "2024-07-29T19:00:00.000+09:00"}, + {"updated_at", output.UpdatedAt, "2024-07-29T20:00:00.000+09:00"}, + } + testCases(t, cases) +} diff --git a/internal/provider/user_resource.go b/internal/provider/user_resource.go new file mode 100644 index 00000000..4f504f66 --- /dev/null +++ b/internal/provider/user_resource.go @@ -0,0 +1 @@ +package provider From b476896b66501fef823c44e93d30f930a7a1c6b3 Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Mon, 21 Oct 2024 17:36:45 +0900 Subject: [PATCH 04/25] Remove CanUseBasicService from User struct and related functions --- internal/client/user.go | 6 ---- internal/client/user_test.go | 54 ++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/internal/client/user.go b/internal/client/user.go index e3d62dee..c5caebe9 100644 --- a/internal/client/user.go +++ b/internal/client/user.go @@ -12,7 +12,6 @@ type User struct { ID int64 `json:"id"` Email string `json:"email"` Role string `json:"role"` - CanUseBasicService bool `json:"can_use_basic_service"` CanUseAuditLog bool `json:"can_use_audit_log"` IsRestrictedConnectionModify bool `json:"is_restricted_connection_modify"` LastSignInAt string `json:"last_sign_in_at"` @@ -68,7 +67,6 @@ type GetUserOutput struct { ID int64 `json:"id"` Email string `json:"email"` Role string `json:"role"` - CanUseBasicService bool `json:"can_use_basic_service"` CanUseAuditLog bool `json:"can_use_audit_log"` IsRestrictedConnectionModify bool `json:"is_restricted_connection_modify"` LastSignInAt string `json:"last_sign_in_at"` @@ -92,7 +90,6 @@ type CreateUserInput struct { Email string `json:"email"` Password string `json:"password"` Role string `json:"role"` - CanUseBasicService bool `json:"can_use_basic_service"` CanUseAuditLog bool `json:"can_use_audit_log"` IsRestrictedConnectionModify bool `json:"is_restricted_connection_modify"` } @@ -101,7 +98,6 @@ type CreateUserOutput struct { ID int64 `json:"id"` Email string `json:"email"` Role string `json:"role"` - CanUseBasicService bool `json:"can_use_basic_service"` CanUseAuditLog bool `json:"can_use_audit_log"` IsRestrictedConnectionModify bool `json:"is_restricted_connection_modify"` LastSignInAt string `json:"last_sign_in_at"` @@ -122,7 +118,6 @@ func (client *TroccoClient) CreateUser(input *CreateUserInput) (*CreateUserOutpu type UpdateUserInput struct { Role string `json:"role"` - CanUseBasicService bool `json:"can_use_basic_service"` CanUseAuditLog bool `json:"can_use_audit_log"` IsRestrictedConnectionModify bool `json:"is_restricted_connection_modify"` } @@ -131,7 +126,6 @@ type UpdateUserOutput struct { ID int64 `json:"id"` Email string `json:"email"` Role string `json:"role"` - CanUseBasicService bool `json:"can_use_basic_service"` CanUseAuditLog bool `json:"can_use_audit_log"` IsRestrictedConnectionModify bool `json:"is_restricted_connection_modify"` LastSignInAt string `json:"last_sign_in_at"` diff --git a/internal/client/user_test.go b/internal/client/user_test.go index 5e68d11a..fadffa41 100644 --- a/internal/client/user_test.go +++ b/internal/client/user_test.go @@ -163,3 +163,57 @@ func TestGetUser(t *testing.T) { } testCases(t, cases) } + +// CreateUser + +func TestCreateUser(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cases := []Case{ + {"path", r.URL.Path, "/api/users"}, + {"method", r.Method, http.MethodPost}, + } + testCases(t, cases) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + resp := ` + { + "id": 1, + "email": "test@example.com", + "role": "admin", + "can_use_audit_log": true, + "is_restricted_connection_modify": false + "last_sign_in_at": "2024-07-29T19:00:00.000+09:00", + "created_at": "2024-07-29T19:00:00.000+09:00", + "updated_at": "2024-07-29T20:00:00.000+09:00" + } + ` + _, err := w.Write([]byte(resp)) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + })) + defer server.Close() + + client := NewDevTroccoClient("1234567890", server.URL) + input := CreateUserInput{ + Email: "test@example.com", + Role: "admin", + CanUseAuditLog: true, + IsRestrictedConnectionModify: false, + } + output, err := client.CreateUser(&input) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + cases := []Case{ + {"ID", output.ID, int64(1)}, + {"email", output.Email, "test@exmaple.com"}, + {"role", output.Role, "admin"}, + {"can_use_audit_log", output.CanUseAuditLog, true}, + {"is_restricted_connection_modify", output.IsRestrictedConnectionModify, false}, + {"last_sign_in_at", output.LastSignInAt, "2024-07-29T19:00:00.000+09:00"}, + {"created_at", output.CreatedAt, "2024-07-29T19:00:00.000+09:00"}, + {"updated_at", output.UpdatedAt, "2024-07-29T20:00:00.000+09:00"}, + } + testCases(t, cases) +} From b8752b8ff5f1fb666b417579c627e9d6a4a98afb Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Tue, 22 Oct 2024 12:14:19 +0900 Subject: [PATCH 05/25] Add test cases for UpdateUser and DeleteUser functions --- internal/client/user_test.go | 81 ++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/internal/client/user_test.go b/internal/client/user_test.go index fadffa41..4b2cb233 100644 --- a/internal/client/user_test.go +++ b/internal/client/user_test.go @@ -181,11 +181,11 @@ func TestCreateUser(t *testing.T) { "email": "test@example.com", "role": "admin", "can_use_audit_log": true, - "is_restricted_connection_modify": false + "is_restricted_connection_modify": false, "last_sign_in_at": "2024-07-29T19:00:00.000+09:00", "created_at": "2024-07-29T19:00:00.000+09:00", "updated_at": "2024-07-29T20:00:00.000+09:00" - } + } ` _, err := w.Write([]byte(resp)) if err != nil { @@ -207,7 +207,7 @@ func TestCreateUser(t *testing.T) { } cases := []Case{ {"ID", output.ID, int64(1)}, - {"email", output.Email, "test@exmaple.com"}, + {"email", output.Email, "test@example.com"}, {"role", output.Role, "admin"}, {"can_use_audit_log", output.CanUseAuditLog, true}, {"is_restricted_connection_modify", output.IsRestrictedConnectionModify, false}, @@ -217,3 +217,78 @@ func TestCreateUser(t *testing.T) { } testCases(t, cases) } + +// UpdateUser + +func TestUpdateUser(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cases := []Case{ + {"path", r.URL.Path, "/api/users/1"}, + {"method", r.Method, http.MethodPatch}, + } + testCases(t, cases) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + resp := ` + { + "id": 1, + "email": "test@example.com", + "role": "admin", + "can_use_audit_log": true, + "is_restricted_connection_modify": false, + "last_sign_in_at": "2024-07-29T19:00:00.000+09:00", + "created_at": "2024-07-29T19:00:00.000+09:00", + "updated_at": "2024-07-29T20:00:00.000+09:00" + } + ` + _, err := w.Write([]byte(resp)) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + })) + defer server.Close() + + client := NewDevTroccoClient("1234567890", server.URL) + input := UpdateUserInput{ + Role: "admin", + CanUseAuditLog: true, + IsRestrictedConnectionModify: false, + } + output, err := client.UpdateUser(1, &input) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + cases := []Case{ + {"ID", output.ID, int64(1)}, + {"email", output.Email, "test@example.com"}, + {"role", output.Role, "admin"}, + {"can_use_audit_log", output.CanUseAuditLog, true}, + {"is_restricted_connection_modify", output.IsRestrictedConnectionModify, false}, + {"last_sign_in_at", output.LastSignInAt, "2024-07-29T19:00:00.000+09:00"}, + {"created_at", output.CreatedAt, "2024-07-29T19:00:00.000+09:00"}, + {"updated_at", output.UpdatedAt, "2024-07-29T20:00:00.000+09:00"}, + } + testCases(t, cases) + +} + +// DeleteUser + +func TestDeleteUser(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cases := []Case{ + {"path", r.URL.Path, "/api/users/1"}, + {"method", r.Method, http.MethodDelete}, + } + testCases(t, cases) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + client := NewDevTroccoClient("1234567890", server.URL) + err := client.DeleteUser(1) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } +} From bb81abe243d9d39fa79acc7c27cdc3e908f04d1c Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Tue, 22 Oct 2024 18:09:33 +0900 Subject: [PATCH 06/25] Add UserResource and Implement Create and Read --- internal/provider/provider.go | 1 + internal/provider/user_resource.go | 160 +++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 798828ac..a4f643f2 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -132,6 +132,7 @@ func (p *TroccoProvider) Configure(ctx context.Context, req provider.ConfigureRe func (p *TroccoProvider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ newBigqueryDatamartDefinitionResource, + NewUserResource, } } diff --git a/internal/provider/user_resource.go b/internal/provider/user_resource.go index 4f504f66..27874f61 100644 --- a/internal/provider/user_resource.go +++ b/internal/provider/user_resource.go @@ -1 +1,161 @@ package provider + +import ( + "context" + "fmt" + "terraform-provider-trocco/internal/client" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &userResource{} + _ resource.ResourceWithConfigure = &userResource{} +) + +func NewUserResource() resource.Resource { + return &userResource{} +} + +type userResource struct { + client *client.TroccoClient +} + +type userResourceModel struct { + ID types.Int64 `tfsdk:"id"` + Email types.String `tfsdk:"email"` + Password types.String `tfsdk:"password"` + Role types.String `tfsdk:"role"` + CanUseAuditLog types.Bool `tfsdk:"can_use_audit_log"` + IsRestrictedConnectionModify types.Bool `tfsdk:"is_restricted_connection_modify"` +} + +func (r *userResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_user" +} + +func (r *userResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.TroccoClient) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client +} + +func (r *userResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Provides a TROCCO user resource.", + Attributes: map[string]schema.Attribute{ + "id": schema.Int64Attribute{ + Computed: true, + MarkdownDescription: "The ID of the user.", + }, + "email": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The email of the user.", + }, + "password": schema.StringAttribute{ + Required: true, + Sensitive: true, + MarkdownDescription: "The password of the user.", + }, + "role": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The role of the user.", + }, + "can_use_audit_log": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether the user can use the audit log.", + }, + "is_restricted_connection_modify": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether the user is restricted to modify connections.", + }, + }, + } +} + +func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan userResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + input := client.CreateUserInput{ + Email: plan.Email.ValueString(), + Password: plan.Password.ValueString(), + Role: plan.Role.ValueString(), + } + if !plan.CanUseAuditLog.IsNull() { + input.CanUseAuditLog = plan.CanUseAuditLog.ValueBool() + } + if !plan.IsRestrictedConnectionModify.IsNull() { + input.IsRestrictedConnectionModify = plan.IsRestrictedConnectionModify.ValueBool() + } + + user, err := r.client.CreateUser(&input) + if err != nil { + resp.Diagnostics.AddError( + "Creating user", + fmt.Sprintf("Unable to create user, got error: %s", err), + ) + return + } + + data := userResourceModel{ + ID: types.Int64Value(user.ID), + Email: types.StringValue(user.Email), + Role: types.StringValue(user.Role), + CanUseAuditLog: types.BoolValue(user.CanUseAuditLog), + IsRestrictedConnectionModify: types.BoolValue(user.IsRestrictedConnectionModify), + } + + resp.Diagnostics.Append(resp.State.Set(ctx, data)...) +} + +func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state userResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + user, err := r.client.GetUser(state.ID.ValueInt64()) + if err != nil { + resp.Diagnostics.AddError( + "Reading user", + fmt.Sprintf("Unable to read user, got error: %s", err), + ) + return + } + + data := userResourceModel{ + ID: types.Int64Value(user.ID), + Email: types.StringValue(user.Email), + Role: types.StringValue(user.Role), + CanUseAuditLog: types.BoolValue(user.CanUseAuditLog), + IsRestrictedConnectionModify: types.BoolValue(user.IsRestrictedConnectionModify), + } + + resp.Diagnostics.Append(resp.State.Set(ctx, data)...) +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} From 4bd2ef483ccc66f6e9b047d0f2b1725f52fd8ba8 Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Tue, 22 Oct 2024 20:13:58 +0900 Subject: [PATCH 07/25] fix typo --- internal/provider/bigquery_datamart_definition_resource.go | 4 ++-- internal/provider/provider.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/provider/bigquery_datamart_definition_resource.go b/internal/provider/bigquery_datamart_definition_resource.go index 37e84312..5ecf9b6e 100644 --- a/internal/provider/bigquery_datamart_definition_resource.go +++ b/internal/provider/bigquery_datamart_definition_resource.go @@ -22,7 +22,7 @@ import ( var _ resource.Resource = &bigqueryDatamartDefinitionResource{} var _ resource.ResourceWithImportState = &bigqueryDatamartDefinitionResource{} -func newBigqueryDatamartDefinitionResource() resource.Resource { +func NewBigqueryDatamartDefinitionResource() resource.Resource { return &bigqueryDatamartDefinitionResource{} } @@ -797,7 +797,7 @@ func (r *bigqueryDatamartDefinitionResource) Update(ctx context.Context, req res model, err := parseToBigqueryDatamartDefinitionModel(data.DatamartDefinition) if err != nil { resp.Diagnostics.AddError( - "Parseing datamart definition", + "Parsing datamart definition", fmt.Sprintf("Unable to parse datamart definition (id: %d), got error: %s", state.ID.ValueInt64(), err), ) return diff --git a/internal/provider/provider.go b/internal/provider/provider.go index a4f643f2..95ec446c 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -131,7 +131,7 @@ func (p *TroccoProvider) Configure(ctx context.Context, req provider.ConfigureRe func (p *TroccoProvider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ - newBigqueryDatamartDefinitionResource, + NewBigqueryDatamartDefinitionResource, NewUserResource, } } From 8cee53a3c16bab39b4a33110f51f3bea404064bd Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Tue, 22 Oct 2024 20:15:55 +0900 Subject: [PATCH 08/25] remove comment --- internal/provider/user_resource.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/provider/user_resource.go b/internal/provider/user_resource.go index 27874f61..4a3a63ab 100644 --- a/internal/provider/user_resource.go +++ b/internal/provider/user_resource.go @@ -152,10 +152,8 @@ func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp resp.Diagnostics.Append(resp.State.Set(ctx, data)...) } -// Update updates the resource and sets the updated Terraform state on success. func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { } -// Delete deletes the resource and removes the Terraform state on success. func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { } From 26ac362da4bbcb27a134f5dea34a0f59433fe26c Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Fri, 25 Oct 2024 14:16:14 +0900 Subject: [PATCH 09/25] implement Create and Read --- internal/provider/user_resource.go | 35 +++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/internal/provider/user_resource.go b/internal/provider/user_resource.go index 4a3a63ab..a146665b 100644 --- a/internal/provider/user_resource.go +++ b/internal/provider/user_resource.go @@ -5,8 +5,14 @@ import ( "fmt" "terraform-provider-trocco/internal/client" + //"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + //"github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -58,16 +64,25 @@ func (r *userResource) Schema(ctx context.Context, req resource.SchemaRequest, r MarkdownDescription: "Provides a TROCCO user resource.", Attributes: map[string]schema.Attribute{ "id": schema.Int64Attribute{ - Computed: true, + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, MarkdownDescription: "The ID of the user.", }, "email": schema.StringAttribute{ Required: true, MarkdownDescription: "The email of the user.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, "password": schema.StringAttribute{ - Required: true, - Sensitive: true, + Optional: true, + Sensitive: true, + //Validators: []validator.String{ + // stringvalidator.RequiredDuringCreate(), // TODO: require if only creating + //}, MarkdownDescription: "The password of the user.", }, "role": schema.StringAttribute{ @@ -75,11 +90,19 @@ func (r *userResource) Schema(ctx context.Context, req resource.SchemaRequest, r MarkdownDescription: "The role of the user.", }, "can_use_audit_log": schema.BoolAttribute{ - Optional: true, + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, MarkdownDescription: "Whether the user can use the audit log.", }, "is_restricted_connection_modify": schema.BoolAttribute{ - Optional: true, + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, MarkdownDescription: "Whether the user is restricted to modify connections.", }, }, @@ -116,6 +139,7 @@ func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, r data := userResourceModel{ ID: types.Int64Value(user.ID), + Password: types.StringValue(plan.Password.ValueString()), Email: types.StringValue(user.Email), Role: types.StringValue(user.Role), CanUseAuditLog: types.BoolValue(user.CanUseAuditLog), @@ -143,6 +167,7 @@ func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp data := userResourceModel{ ID: types.Int64Value(user.ID), + Password: types.StringValue(state.Password.ValueString()), Email: types.StringValue(user.Email), Role: types.StringValue(user.Role), CanUseAuditLog: types.BoolValue(user.CanUseAuditLog), From 0f4af9e5c36277eeb91c84c220a33fd6863399ac Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Fri, 25 Oct 2024 14:28:27 +0900 Subject: [PATCH 10/25] implement Update and Delete --- internal/provider/user_resource.go | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/internal/provider/user_resource.go b/internal/provider/user_resource.go index a146665b..afcb8aef 100644 --- a/internal/provider/user_resource.go +++ b/internal/provider/user_resource.go @@ -178,7 +178,53 @@ func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp } func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state userResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + input := client.UpdateUserInput{ + Role: plan.Role.ValueString(), + CanUseAuditLog: plan.CanUseAuditLog.ValueBool(), + IsRestrictedConnectionModify: plan.IsRestrictedConnectionModify.ValueBool(), + } + + user, err := r.client.UpdateUser(state.ID.ValueInt64(), &input) + if err != nil { + resp.Diagnostics.AddError( + "Updating user", + fmt.Sprintf("Unable to update user, got error: %s", err), + ) + return + } + + data := userResourceModel{ + ID: types.Int64Value(user.ID), + Password: types.StringValue(state.Password.ValueString()), + Email: types.StringValue(user.Email), + Role: types.StringValue(user.Role), + CanUseAuditLog: types.BoolValue(user.CanUseAuditLog), + IsRestrictedConnectionModify: types.BoolValue(user.IsRestrictedConnectionModify), + } + + resp.Diagnostics.Append(resp.State.Set(ctx, data)...) } func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state userResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + err := r.client.DeleteUser(state.ID.ValueInt64()) + if err != nil { + resp.Diagnostics.AddError( + "Deleting user", + fmt.Sprintf("Unable to delete user, got error: %s", err), + ) + return + } } From b63efd1f33ce65f7ea29c2abf168e1e219c5908c Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Fri, 25 Oct 2024 19:24:55 +0900 Subject: [PATCH 11/25] add planmodifiers --- internal/provider/ignore_changes_modifier.go | 31 ++++++++++++++++ .../require_on_create_plan_modifier.go | 37 +++++++++++++++++++ internal/provider/user_resource.go | 10 ++--- 3 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 internal/provider/ignore_changes_modifier.go create mode 100644 internal/provider/require_on_create_plan_modifier.go diff --git a/internal/provider/ignore_changes_modifier.go b/internal/provider/ignore_changes_modifier.go new file mode 100644 index 00000000..6a0e204d --- /dev/null +++ b/internal/provider/ignore_changes_modifier.go @@ -0,0 +1,31 @@ +package provider + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +var _ planmodifier.String = &IgnoreChangesPlanModifier{} + +// IgnoreChangesPlanModifier is a plan modifier that ignores changes attribute after creation. +type IgnoreChangesPlanModifier struct{} + +func (m IgnoreChangesPlanModifier) Description(ctx context.Context) string { + return "Ignore changes to attribute after creation." +} + +func (m IgnoreChangesPlanModifier) MarkdownDescription(ctx context.Context) string { + return m.Description(ctx) +} + +func (m IgnoreChangesPlanModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + // return if the resource is being created + if req.State.Raw.IsNull() { + return + } + + if req.PlanValue.IsUnknown() || req.StateValue.IsUnknown() || req.PlanValue == req.StateValue { + return + } + resp.PlanValue = req.StateValue +} diff --git a/internal/provider/require_on_create_plan_modifier.go b/internal/provider/require_on_create_plan_modifier.go new file mode 100644 index 00000000..75ecf05f --- /dev/null +++ b/internal/provider/require_on_create_plan_modifier.go @@ -0,0 +1,37 @@ +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +var _ planmodifier.String = &RequiredOnCreatePlanModifier{} + +// RequiredOnCreatePlanModifier is a plan modifier that ensures that a field is only required on resource creation. +type RequiredOnCreatePlanModifier struct { + AttributeName string +} + +func (m RequiredOnCreatePlanModifier) Description(ctx context.Context) string { + return "This field is required on resource creation." +} + +func (m RequiredOnCreatePlanModifier) MarkdownDescription(ctx context.Context) string { + return m.Description(ctx) +} + +func (m RequiredOnCreatePlanModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + // Check if the resource is being created. + if !req.State.Raw.IsNull() { + return + } + if req.ConfigValue.IsNull() { + resp.Diagnostics.AddAttributeError( + path.Root(m.AttributeName), + "Missing Required Attribute", + "The attribute '"+m.AttributeName+"' is required on resource creation.", + ) + } +} diff --git a/internal/provider/user_resource.go b/internal/provider/user_resource.go index afcb8aef..98e7fe47 100644 --- a/internal/provider/user_resource.go +++ b/internal/provider/user_resource.go @@ -5,14 +5,12 @@ import ( "fmt" "terraform-provider-trocco/internal/client" - //"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - //"github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -79,10 +77,12 @@ func (r *userResource) Schema(ctx context.Context, req resource.SchemaRequest, r }, "password": schema.StringAttribute{ Optional: true, + Computed: true, Sensitive: true, - //Validators: []validator.String{ - // stringvalidator.RequiredDuringCreate(), // TODO: require if only creating - //}, + PlanModifiers: []planmodifier.String{ + &IgnoreChangesPlanModifier{}, + &RequiredOnCreatePlanModifier{"password"}, + }, MarkdownDescription: "The password of the user.", }, "role": schema.StringAttribute{ From 7ddf150603aa44601519c8366f1a2557d465eba9 Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Mon, 28 Oct 2024 13:57:05 +0900 Subject: [PATCH 12/25] add validators --- internal/provider/user_resource.go | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/internal/provider/user_resource.go b/internal/provider/user_resource.go index 98e7fe47..7def42d0 100644 --- a/internal/provider/user_resource.go +++ b/internal/provider/user_resource.go @@ -3,14 +3,17 @@ package provider import ( "context" "fmt" + "regexp" "terraform-provider-trocco/internal/client" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -69,11 +72,17 @@ func (r *userResource) Schema(ctx context.Context, req resource.SchemaRequest, r MarkdownDescription: "The ID of the user.", }, "email": schema.StringAttribute{ - Required: true, - MarkdownDescription: "The email of the user.", + Required: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile(`^[^@\s]+@[^@\s]+$`), + "invalid email address", + ), + }, + MarkdownDescription: "The email of the user.", }, "password": schema.StringAttribute{ Optional: true, @@ -83,10 +92,21 @@ func (r *userResource) Schema(ctx context.Context, req resource.SchemaRequest, r &IgnoreChangesPlanModifier{}, &RequiredOnCreatePlanModifier{"password"}, }, + Validators: []validator.String{ + // see: https://documents.trocco.io/docs/password-policy + stringvalidator.LengthBetween(8, 128), + stringvalidator.All( + stringvalidator.RegexMatches(regexp.MustCompile(`[a-zA-Z]`), "must contain at least one letter"), + stringvalidator.RegexMatches(regexp.MustCompile(`[0-9]`), "must contain at least one number"), + ), + }, MarkdownDescription: "The password of the user.", }, "role": schema.StringAttribute{ - Required: true, + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("admin", "member"), + }, MarkdownDescription: "The role of the user.", }, "can_use_audit_log": schema.BoolAttribute{ From a68a1a025d007d9746e0f72673eb41c51eadd7c7 Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Tue, 29 Oct 2024 11:37:33 +0900 Subject: [PATCH 13/25] add test WIP --- go.mod | 29 ++++++++++--- go.sum | 54 +++++++++++++++++++++++++ internal/provider/provider_test.go | 20 +++++++++ internal/provider/user_resource_test.go | 37 +++++++++++++++++ 4 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 internal/provider/provider_test.go create mode 100644 internal/provider/user_resource_test.go diff --git a/go.mod b/go.mod index 5f00b7a0..e1b532bb 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,22 @@ require ( github.com/hashicorp/terraform-plugin-go v0.23.0 ) +require ( + github.com/agext/levenshtein v1.2.2 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect + github.com/hashicorp/hcl/v2 v2.21.0 // indirect + github.com/hashicorp/logutils v1.0.0 // indirect + github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + google.golang.org/appengine v1.6.8 // indirect +) + require ( github.com/BurntSushi/toml v1.2.1 // indirect github.com/Kunde21/markdownfmt/v3 v3.1.0 // indirect @@ -33,10 +49,11 @@ require ( github.com/hashicorp/go-plugin v1.6.0 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/go-version v1.7.0 // indirect - github.com/hashicorp/hc-install v0.7.0 // indirect + github.com/hashicorp/hc-install v0.8.0 // indirect github.com/hashicorp/terraform-exec v0.21.0 // indirect github.com/hashicorp/terraform-json v0.22.1 // indirect github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect + github.com/hashicorp/terraform-plugin-testing v1.10.0 github.com/hashicorp/terraform-registry-address v0.2.3 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect @@ -56,14 +73,14 @@ require ( github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yuin/goldmark v1.7.1 // indirect github.com/yuin/goldmark-meta v1.1.0 // indirect - github.com/zclconf/go-cty v1.14.4 // indirect + github.com/zclconf/go-cty v1.15.0 // indirect go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect - golang.org/x/crypto v0.25.0 // indirect + golang.org/x/crypto v0.26.0 // indirect golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819 // indirect - golang.org/x/mod v0.17.0 // indirect + golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.25.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect google.golang.org/grpc v1.63.2 // indirect google.golang.org/protobuf v1.34.0 // indirect diff --git a/go.sum b/go.sum index 3d64f204..817e8120 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,9 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg= github.com/ProtonMail/go-crypto v1.1.0-alpha.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= +github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= @@ -46,8 +49,13 @@ github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZt github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -63,6 +71,8 @@ github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuD github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI= +github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= @@ -70,6 +80,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= +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-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 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= @@ -77,6 +89,12 @@ github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKe github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hc-install v0.7.0 h1:Uu9edVqjKQxxuD28mR5TikkKDd/p55S8vzPC1659aBk= github.com/hashicorp/hc-install v0.7.0/go.mod h1:ELmmzZlGnEcqoUMKUuykHaPCIR1sYLYX+KSggWSKZuA= +github.com/hashicorp/hc-install v0.8.0 h1:LdpZeXkZYMQhoKPCecJHlKvUkQFixN/nvyR1CdfOLjI= +github.com/hashicorp/hc-install v0.8.0/go.mod h1:+MwJYjDfCruSD/udvBmRB22Nlkwwkwf5sAB6uTIhSaU= +github.com/hashicorp/hcl/v2 v2.21.0 h1:lve4q/o/2rqwYOgUg3y3V2YPyD1/zkCLGjIV74Jit14= +github.com/hashicorp/hcl/v2 v2.21.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= +github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ= github.com/hashicorp/terraform-exec v0.21.0/go.mod h1:1PPeMYou+KDUSSeRE9szMZ/oHf4fYUmB923Wzbq1ICg= github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7orfb5Ltvec= @@ -91,6 +109,10 @@ github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/12 github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 h1:kJiWGx2kiQVo97Y5IOGR4EMcZ8DtMswHhUuFibsCQQE= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0/go.mod h1:sl/UoabMc37HA6ICVMmGO+/0wofkVIRxf+BMb/dnoIg= +github.com/hashicorp/terraform-plugin-testing v1.10.0 h1:2+tmRNhvnfE4Bs8rB6v58S/VpqzGC6RCh9Y8ujdn+aw= +github.com/hashicorp/terraform-plugin-testing v1.10.0/go.mod h1:iWRW3+loP33WMch2P/TEyCxxct/ZEcCGMquSLSCVsrc= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= @@ -108,8 +130,11 @@ github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgf github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -128,6 +153,10 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= @@ -158,6 +187,9 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= +github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= @@ -171,6 +203,8 @@ github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUei github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ= +github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= go.abhg.dev/goldmark/frontmatter v0.2.0 h1:P8kPG0YkL12+aYk2yU3xHv4tcXzeVnN+gU0tJ5JnxRw= go.abhg.dev/goldmark/frontmatter v0.2.0/go.mod h1:XqrEkZuM57djk7zrlRUB02x8I5J0px76YjkOzhB4YlU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -178,21 +212,29 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819 h1:EDuYyU/MkFXllv9QF9819VlI9a4tzGuCbhG0ExK9o1U= golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -208,25 +250,37 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go new file mode 100644 index 00000000..53e31170 --- /dev/null +++ b/internal/provider/provider_test.go @@ -0,0 +1,20 @@ +package provider + +import ( + "fmt" + "os" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +var ( + providerConfig = fmt.Sprintf(` + provider "trocco" { + dev_base_url = "%s" + } + `, os.Getenv("TROCCO_TEST_URL")) + testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ + "trocco": providerserver.NewProtocol6WithError(New("test")()), + } +) diff --git a/internal/provider/user_resource_test.go b/internal/provider/user_resource_test.go new file mode 100644 index 00000000..0bd0d22f --- /dev/null +++ b/internal/provider/user_resource_test.go @@ -0,0 +1,37 @@ +package provider + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccUserResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: providerConfig + ` + resource "trocco_user" "test" { + email = "test@example.com" + password = "3XRambMkp-Hw" + role = "admin" + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + + resource.TestCheckResourceAttr("trocco_user.test", "email", "test@example.com"), + ), + }, + // ImportState testing + { + ResourceName: "trocco_user.test", + ImportState: true, + ImportStateVerify: true, + //ImportStateVerifyIgnore: []string{"last_updated"}, + }, + // Update and Read testing + }, + }) +} From 0951abc872da2e224e6889330d25b1072f9398d6 Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Tue, 29 Oct 2024 12:18:36 +0900 Subject: [PATCH 14/25] implement Import --- internal/provider/user_resource.go | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/internal/provider/user_resource.go b/internal/provider/user_resource.go index 7def42d0..c40f6060 100644 --- a/internal/provider/user_resource.go +++ b/internal/provider/user_resource.go @@ -4,9 +4,11 @@ import ( "context" "fmt" "regexp" + "strconv" "terraform-provider-trocco/internal/client" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" @@ -18,8 +20,9 @@ import ( ) var ( - _ resource.Resource = &userResource{} - _ resource.ResourceWithConfigure = &userResource{} + _ resource.Resource = &userResource{} + _ resource.ResourceWithConfigure = &userResource{} + _ resource.ResourceWithImportState = &userResource{} ) func NewUserResource() resource.Resource { @@ -105,7 +108,7 @@ func (r *userResource) Schema(ctx context.Context, req resource.SchemaRequest, r "role": schema.StringAttribute{ Required: true, Validators: []validator.String{ - stringvalidator.OneOf("admin", "member"), + stringvalidator.OneOf("super_admin", "admin", "member"), }, MarkdownDescription: "The role of the user.", }, @@ -248,3 +251,16 @@ func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, r return } } + +func (r *userResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + id, err := strconv.ParseInt(req.ID, 10, 64) + if err != nil { + resp.Diagnostics.AddError( + "Importing user", + fmt.Sprintf("Unable to parse id, got error: %s", err), + ) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) +} From 4a11c8e1ae5eb8fab83193443b2debd46a8459db Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Tue, 29 Oct 2024 15:54:28 +0900 Subject: [PATCH 15/25] add user_resouce acceptance test --- internal/provider/user_resource_test.go | 113 ++++++++++++++++++++++-- 1 file changed, 108 insertions(+), 5 deletions(-) diff --git a/internal/provider/user_resource_test.go b/internal/provider/user_resource_test.go index 0bd0d22f..e1f2eb75 100644 --- a/internal/provider/user_resource_test.go +++ b/internal/provider/user_resource_test.go @@ -1,6 +1,7 @@ package provider import ( + "regexp" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -20,18 +21,120 @@ func TestAccUserResource(t *testing.T) { } `, Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("trocco_user.test", "email", "test@example.com"), + resource.TestCheckResourceAttr("trocco_user.test", "role", "admin"), + resource.TestCheckResourceAttr("trocco_user.test", "can_use_audit_log", "false"), + resource.TestCheckResourceAttr("trocco_user.test", "is_restricted_connection_modify", "false"), + resource.TestCheckResourceAttrSet("trocco_user.test", "id"), ), }, // ImportState testing { - ResourceName: "trocco_user.test", - ImportState: true, - ImportStateVerify: true, - //ImportStateVerifyIgnore: []string{"last_updated"}, + ResourceName: "trocco_user.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"password"}, }, // Update and Read testing + { + Config: providerConfig + ` + resource "trocco_user" "test" { + email = "test@example.com" + role = "member" + can_use_audit_log = true + is_restricted_connection_modify = true + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("trocco_user.test", "email", "test@example.com"), + resource.TestCheckResourceAttr("trocco_user.test", "role", "member"), + resource.TestCheckResourceAttr("trocco_user.test", "can_use_audit_log", "true"), + resource.TestCheckResourceAttr("trocco_user.test", "is_restricted_connection_modify", "true"), + ), + }, + }, + }) +} + +func TestAccUserResourceInvalidEmail(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: providerConfig + ` + resource "trocco_user" "test" { + email = "test" + password = "3XRambMkp-Hw" + role = "admin" + } + `, + ExpectError: regexp.MustCompile(`invalid email address`), + }, + }, + }) +} + +func TestAccUserResourceInvalidPassword(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: providerConfig + ` + resource "trocco_user" "test" { + email = "test@example.com" + password = "abc123" + role = "admin" + } + `, + ExpectError: regexp.MustCompile(`password string length`), + }, + { + Config: providerConfig + ` + resource "trocco_user" "test" { + email = "test@example.com" + password = "1111111111111" + role = "admin" + } + `, + ExpectError: regexp.MustCompile(`must contain at least one letter`), + }, + { + Config: providerConfig + ` + resource "trocco_user" "test" { + email = "test@example.com" + password = "aaaaaaaaaaaaa" + role = "admin" + } + `, + ExpectError: regexp.MustCompile(`must contain at least one number`), + }, + { + Config: providerConfig + ` + resource "trocco_user" "test" { + email = "test@example.com" + role = "admin" + } + `, + ExpectError: regexp.MustCompile(`Missing Required Attribute`), + }, + }, + }) +} + +func TestAccUserResourceInvalidRole(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: providerConfig + ` + resource "trocco_user" "test" { + email = "test@example.com" + password = "abc123" + role = "invalid role" + } + `, + ExpectError: regexp.MustCompile(`role value must be one of`), + }, }, }) } From 8dd60f4f7e155df7331640933c005813034c4a3c Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Wed, 30 Oct 2024 11:59:59 +0900 Subject: [PATCH 16/25] disable acceptance tests --- .github/workflows/test.yml | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3ff5645b..bee3ac00 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -60,16 +60,17 @@ jobs: needs: build runs-on: ubuntu-latest timeout-minutes: 15 - strategy: - fail-fast: false - matrix: - # list whatever Terraform versions here you would like to support - terraform: - - "1.0.*" - - "1.1.*" - - "1.2.*" - - "1.3.*" - - "1.4.*" + # enable strategy when run acceptance tests + # strategy: + # fail-fast: false + # matrix: + # # list whatever Terraform versions here you would like to support + # terraform: + # - "1.0.*" + # - "1.1.*" + # - "1.2.*" + # - "1.3.*" + # - "1.4.*" steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 @@ -81,7 +82,5 @@ jobs: terraform_version: ${{ matrix.terraform }} terraform_wrapper: false - run: go mod download - - env: - TF_ACC: "1" - run: go test -v -cover ./internal/* + - run: go test -v -cover ./internal/* timeout-minutes: 10 From 79fba3a5815770c3034cea2f0b197783c0af190d Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Wed, 30 Oct 2024 13:26:00 +0900 Subject: [PATCH 17/25] fix github actions --- .github/workflows/test.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bee3ac00..4311fa91 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -54,7 +54,6 @@ jobs: git diff --compact-summary --exit-code || \ (echo; echo "Unexpected difference in directories after code generation. Run 'go generate ./...' command and commit."; exit 1) - # Run acceptance tests in a matrix with Terraform CLI versions test: name: Terraform Provider Acceptance Tests needs: build @@ -78,9 +77,9 @@ jobs: go-version-file: "go.mod" cache: true - uses: hashicorp/setup-terraform@651471c36a6092792c552e8b1bef71e592b462d8 # v3.1.1 - with: - terraform_version: ${{ matrix.terraform }} - terraform_wrapper: false + # with: + # terraform_version: ${{ matrix.terraform }} + # terraform_wrapper: false - run: go mod download - run: go test -v -cover ./internal/* timeout-minutes: 10 From 458fe4999d695f00df131f37a23037f8df29593c Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Tue, 5 Nov 2024 14:36:45 +0900 Subject: [PATCH 18/25] Add trocco_user resource and example usage --- docs/resources/user.md | 31 +++++++++++++++++++++++++++++ examples/resources/user/import.sh | 1 + examples/resources/user/resource.tf | 7 +++++++ 3 files changed, 39 insertions(+) create mode 100644 docs/resources/user.md create mode 100644 examples/resources/user/import.sh create mode 100644 examples/resources/user/resource.tf diff --git a/docs/resources/user.md b/docs/resources/user.md new file mode 100644 index 00000000..09124084 --- /dev/null +++ b/docs/resources/user.md @@ -0,0 +1,31 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "trocco_user Resource - trocco" +subcategory: "" +description: |- + Provides a TROCCO user resource. +--- + +# trocco_user (Resource) + +Provides a TROCCO user resource. + + + + +## Schema + +### Required + +- `email` (String) The email of the user. +- `role` (String) The role of the user. + +### Optional + +- `can_use_audit_log` (Boolean) Whether the user can use the audit log. +- `is_restricted_connection_modify` (Boolean) Whether the user is restricted to modify connections. +- `password` (String, Sensitive) The password of the user. + +### Read-Only + +- `id` (Number) The ID of the user. diff --git a/examples/resources/user/import.sh b/examples/resources/user/import.sh new file mode 100644 index 00000000..54431991 --- /dev/null +++ b/examples/resources/user/import.sh @@ -0,0 +1 @@ +terraform import trocco_user.example diff --git a/examples/resources/user/resource.tf b/examples/resources/user/resource.tf new file mode 100644 index 00000000..0bc23c8e --- /dev/null +++ b/examples/resources/user/resource.tf @@ -0,0 +1,7 @@ +resource "trocco_user" "example" { + email = "trocco@example.com" + password = "Jb1p4f1uuC" + role = "member" + can_use_audit_log = false + is_restricted_connection_modify = false +} From 0ba8f0c2d829bda6ce30a5b58924b55634c53eb1 Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Tue, 5 Nov 2024 17:28:28 +0900 Subject: [PATCH 19/25] generate documents --- docs/resources/user.md | 24 ++++++++++++++++--- .../resources/{user => trocco_user}/import.sh | 0 .../{user => trocco_user}/resource.tf | 0 internal/provider/user_resource.go | 4 ++-- 4 files changed, 23 insertions(+), 5 deletions(-) rename examples/resources/{user => trocco_user}/import.sh (100%) rename examples/resources/{user => trocco_user}/resource.tf (100%) diff --git a/docs/resources/user.md b/docs/resources/user.md index 09124084..2b69e129 100644 --- a/docs/resources/user.md +++ b/docs/resources/user.md @@ -10,7 +10,17 @@ description: |- Provides a TROCCO user resource. - +## Example Usage + +```terraform +resource "trocco_user" "example" { + email = "trocco@example.com" + password = "Jb1p4f1uuC" + role = "member" + can_use_audit_log = false + is_restricted_connection_modify = false +} +``` ## Schema @@ -18,14 +28,22 @@ Provides a TROCCO user resource. ### Required - `email` (String) The email of the user. -- `role` (String) The role of the user. +- `role` (String) The role of the user. Valid value is `super_admin`, `admin`, or `member`. ### Optional - `can_use_audit_log` (Boolean) Whether the user can use the audit log. - `is_restricted_connection_modify` (Boolean) Whether the user is restricted to modify connections. -- `password` (String, Sensitive) The password of the user. +- `password` (String, Sensitive) The password of the user. It must be at least 8 characters long and contain at least one letter and one number. It is required when creating a new user but optional during updates. ### Read-Only - `id` (Number) The ID of the user. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import trocco_user.example +``` diff --git a/examples/resources/user/import.sh b/examples/resources/trocco_user/import.sh similarity index 100% rename from examples/resources/user/import.sh rename to examples/resources/trocco_user/import.sh diff --git a/examples/resources/user/resource.tf b/examples/resources/trocco_user/resource.tf similarity index 100% rename from examples/resources/user/resource.tf rename to examples/resources/trocco_user/resource.tf diff --git a/internal/provider/user_resource.go b/internal/provider/user_resource.go index c40f6060..926174f3 100644 --- a/internal/provider/user_resource.go +++ b/internal/provider/user_resource.go @@ -103,14 +103,14 @@ func (r *userResource) Schema(ctx context.Context, req resource.SchemaRequest, r stringvalidator.RegexMatches(regexp.MustCompile(`[0-9]`), "must contain at least one number"), ), }, - MarkdownDescription: "The password of the user.", + MarkdownDescription: "The password of the user. It must be at least 8 characters long and contain at least one letter and one number. It is required when creating a new user but optional during updates.", }, "role": schema.StringAttribute{ Required: true, Validators: []validator.String{ stringvalidator.OneOf("super_admin", "admin", "member"), }, - MarkdownDescription: "The role of the user.", + MarkdownDescription: "The role of the user. Valid value is `super_admin`, `admin`, or `member`.", }, "can_use_audit_log": schema.BoolAttribute{ Optional: true, From 6d9566860639a52beafca0a070c0f565799439b3 Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Thu, 14 Nov 2024 12:29:36 +0900 Subject: [PATCH 20/25] Add NullableBool and NullableString types --- go.mod | 1 + go.sum | 2 ++ internal/client/type.go | 12 ++++++++++++ internal/client/user.go | 16 ++++++++-------- internal/client/user_test.go | 12 +++++++----- 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index e1b532bb..e39a2a26 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/samber/lo v1.47.0 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect diff --git a/go.sum b/go.sum index 817e8120..7077a8f1 100644 --- a/go.sum +++ b/go.sum @@ -170,6 +170,8 @@ github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXq github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= diff --git a/internal/client/type.go b/internal/client/type.go index 6defaf61..22535d76 100644 --- a/internal/client/type.go +++ b/internal/client/type.go @@ -25,3 +25,15 @@ func (n NullableString) MarshalJSON() ([]byte, error) { } return json.Marshal(n.Value) } + +type NullableBool struct { + Value bool + Valid bool +} + +func (n NullableBool) MarshalJSON() ([]byte, error) { + if !n.Valid { + return []byte("null"), nil + } + return json.Marshal(n.Value) +} diff --git a/internal/client/user.go b/internal/client/user.go index c5caebe9..d50e8144 100644 --- a/internal/client/user.go +++ b/internal/client/user.go @@ -87,11 +87,11 @@ func (client *TroccoClient) GetUser(id int64) (*GetUserOutput, error) { // Create a User type CreateUserInput struct { - Email string `json:"email"` - Password string `json:"password"` - Role string `json:"role"` - CanUseAuditLog bool `json:"can_use_audit_log"` - IsRestrictedConnectionModify bool `json:"is_restricted_connection_modify"` + Email string `json:"email"` + Password string `json:"password"` + Role string `json:"role"` + CanUseAuditLog *NullableBool `json:"can_use_audit_log"` + IsRestrictedConnectionModify *NullableBool `json:"is_restricted_connection_modify"` } type CreateUserOutput struct { @@ -117,9 +117,9 @@ func (client *TroccoClient) CreateUser(input *CreateUserInput) (*CreateUserOutpu // Update a User type UpdateUserInput struct { - Role string `json:"role"` - CanUseAuditLog bool `json:"can_use_audit_log"` - IsRestrictedConnectionModify bool `json:"is_restricted_connection_modify"` + Role *NullableString `json:"role"` + CanUseAuditLog *NullableBool `json:"can_use_audit_log"` + IsRestrictedConnectionModify *NullableBool `json:"is_restricted_connection_modify"` } type UpdateUserOutput struct { diff --git a/internal/client/user_test.go b/internal/client/user_test.go index 4b2cb233..a89ab38a 100644 --- a/internal/client/user_test.go +++ b/internal/client/user_test.go @@ -4,6 +4,8 @@ import ( "net/http" "net/http/httptest" "testing" + + "github.com/samber/lo" ) // ListUsers @@ -198,8 +200,8 @@ func TestCreateUser(t *testing.T) { input := CreateUserInput{ Email: "test@example.com", Role: "admin", - CanUseAuditLog: true, - IsRestrictedConnectionModify: false, + CanUseAuditLog: lo.ToPtr(NullableBool{Value: true, Valid: true}), + IsRestrictedConnectionModify: lo.ToPtr(NullableBool{Value: false, Valid: true}), } output, err := client.CreateUser(&input) if err != nil { @@ -250,9 +252,9 @@ func TestUpdateUser(t *testing.T) { client := NewDevTroccoClient("1234567890", server.URL) input := UpdateUserInput{ - Role: "admin", - CanUseAuditLog: true, - IsRestrictedConnectionModify: false, + Role: lo.ToPtr(NullableString{Value: "admin", Valid: true}), + CanUseAuditLog: lo.ToPtr(NullableBool{Value: true, Valid: true}), + IsRestrictedConnectionModify: lo.ToPtr(NullableBool{Value: false, Valid: true}), } output, err := client.UpdateUser(1, &input) if err != nil { From 01ab84bb5cda418d1b865a1667c2c7008cfa1f84 Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Thu, 14 Nov 2024 15:02:39 +0900 Subject: [PATCH 21/25] Add newNullableFromTerraformBool in provider --- internal/provider/types.go | 46 ++++++++++++++++++++++++++++++ internal/provider/user_resource.go | 20 ++++++------- 2 files changed, 54 insertions(+), 12 deletions(-) create mode 100644 internal/provider/types.go diff --git a/internal/provider/types.go b/internal/provider/types.go new file mode 100644 index 00000000..17e64526 --- /dev/null +++ b/internal/provider/types.go @@ -0,0 +1,46 @@ +package provider + +import ( + "terraform-provider-trocco/internal/client" + + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// NewNullableFromTerraformString create a client.NullableString from a types.String. +func newNullableFromTerraformString(v types.String) *client.NullableString { + return &client.NullableString{Valid: !v.IsNull(), Value: v.ValueString()} +} + +func ExampleNewNullableFromTerraformInt64() { + newNullableFromTerraformInt64(types.Int64Null()) + // Output: i1.Valid = false + + newNullableFromTerraformInt64(types.Int64Value(42)) + // Output: i2.Valid = true, i2.Value = 42 +} + +// NewNullableFromTerraformInt64 create a client.NullableInt64 from a types.Int64. +func newNullableFromTerraformInt64(v types.Int64) *client.NullableInt64 { + return &client.NullableInt64{Valid: !v.IsNull(), Value: v.ValueInt64()} +} + +func ExampleNewNullableFromTerraformString() { + newNullableFromTerraformString(types.StringNull()) + // Output: s1.Valid = false + + newNullableFromTerraformString(types.StringValue("example")) + // Output: s2.Valid = true, s2.Value = "example" +} + +// NewNullableFromTerraformBool create a client.NullableBool from a types.Bool. +func newNullableFromTerraformBool(v types.Bool) *client.NullableBool { + return &client.NullableBool{Valid: !v.IsNull(), Value: v.ValueBool()} +} + +func ExampleNewNullableFromTerraformBool() { + newNullableFromTerraformBool(types.BoolNull()) + // Output: b1.Valid = false + + newNullableFromTerraformBool(types.BoolValue(true)) + // Output: b2.Valid = true, b2.Value = true +} diff --git a/internal/provider/user_resource.go b/internal/provider/user_resource.go index 926174f3..c91a8541 100644 --- a/internal/provider/user_resource.go +++ b/internal/provider/user_resource.go @@ -140,15 +140,11 @@ func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, r } input := client.CreateUserInput{ - Email: plan.Email.ValueString(), - Password: plan.Password.ValueString(), - Role: plan.Role.ValueString(), - } - if !plan.CanUseAuditLog.IsNull() { - input.CanUseAuditLog = plan.CanUseAuditLog.ValueBool() - } - if !plan.IsRestrictedConnectionModify.IsNull() { - input.IsRestrictedConnectionModify = plan.IsRestrictedConnectionModify.ValueBool() + Email: plan.Email.ValueString(), + Password: plan.Password.ValueString(), + Role: plan.Role.ValueString(), + CanUseAuditLog: newNullableFromTerraformBool(plan.CanUseAuditLog), + IsRestrictedConnectionModify: newNullableFromTerraformBool(plan.IsRestrictedConnectionModify), } user, err := r.client.CreateUser(&input) @@ -209,9 +205,9 @@ func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, r } input := client.UpdateUserInput{ - Role: plan.Role.ValueString(), - CanUseAuditLog: plan.CanUseAuditLog.ValueBool(), - IsRestrictedConnectionModify: plan.IsRestrictedConnectionModify.ValueBool(), + Role: newNullableFromTerraformString(plan.Role), + CanUseAuditLog: newNullableFromTerraformBool(plan.CanUseAuditLog), + IsRestrictedConnectionModify: newNullableFromTerraformBool(plan.IsRestrictedConnectionModify), } user, err := r.client.UpdateUser(state.ID.ValueInt64(), &input) From 13a866747564a046c69f4cfeb8b5f13a4ec2db80 Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Thu, 14 Nov 2024 15:08:48 +0900 Subject: [PATCH 22/25] Refactor NewNullableFromTerraform functions for consistency --- internal/provider/types.go | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/internal/provider/types.go b/internal/provider/types.go index 17e64526..5efe76b6 100644 --- a/internal/provider/types.go +++ b/internal/provider/types.go @@ -6,32 +6,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -// NewNullableFromTerraformString create a client.NullableString from a types.String. -func newNullableFromTerraformString(v types.String) *client.NullableString { - return &client.NullableString{Valid: !v.IsNull(), Value: v.ValueString()} -} - -func ExampleNewNullableFromTerraformInt64() { - newNullableFromTerraformInt64(types.Int64Null()) - // Output: i1.Valid = false - - newNullableFromTerraformInt64(types.Int64Value(42)) - // Output: i2.Valid = true, i2.Value = 42 -} - -// NewNullableFromTerraformInt64 create a client.NullableInt64 from a types.Int64. -func newNullableFromTerraformInt64(v types.Int64) *client.NullableInt64 { - return &client.NullableInt64{Valid: !v.IsNull(), Value: v.ValueInt64()} -} - -func ExampleNewNullableFromTerraformString() { - newNullableFromTerraformString(types.StringNull()) - // Output: s1.Valid = false - - newNullableFromTerraformString(types.StringValue("example")) - // Output: s2.Valid = true, s2.Value = "example" -} - // NewNullableFromTerraformBool create a client.NullableBool from a types.Bool. func newNullableFromTerraformBool(v types.Bool) *client.NullableBool { return &client.NullableBool{Valid: !v.IsNull(), Value: v.ValueBool()} From ca0c533e914e2628906d7da61cfd61b9d616fbee Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Thu, 14 Nov 2024 15:13:11 +0900 Subject: [PATCH 23/25] Fix provider/types.go --- internal/provider/types.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/internal/provider/types.go b/internal/provider/types.go index 5efe76b6..c036bcaa 100644 --- a/internal/provider/types.go +++ b/internal/provider/types.go @@ -6,15 +6,12 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) +// NewNullableFromTerraformString create a client.NullableString from a types.String. +func newNullableFromTerraformString(v types.String) *client.NullableString { + return &client.NullableString{Valid: !v.IsNull(), Value: v.ValueString()} +} + // NewNullableFromTerraformBool create a client.NullableBool from a types.Bool. func newNullableFromTerraformBool(v types.Bool) *client.NullableBool { return &client.NullableBool{Valid: !v.IsNull(), Value: v.ValueBool()} } - -func ExampleNewNullableFromTerraformBool() { - newNullableFromTerraformBool(types.BoolNull()) - // Output: b1.Valid = false - - newNullableFromTerraformBool(types.BoolValue(true)) - // Output: b2.Valid = true, b2.Value = true -} From fa8a37af3561035c5b36325f317b026d73249f8f Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Mon, 18 Nov 2024 13:53:53 +0900 Subject: [PATCH 24/25] Update user input fields with bool type --- internal/client/user.go | 16 ++++++++-------- internal/client/user_test.go | 10 +++++----- internal/provider/types.go | 17 ----------------- internal/provider/user_resource.go | 27 ++++++++++++++++++--------- 4 files changed, 31 insertions(+), 39 deletions(-) delete mode 100644 internal/provider/types.go diff --git a/internal/client/user.go b/internal/client/user.go index d50e8144..dd4a09b5 100644 --- a/internal/client/user.go +++ b/internal/client/user.go @@ -87,11 +87,11 @@ func (client *TroccoClient) GetUser(id int64) (*GetUserOutput, error) { // Create a User type CreateUserInput struct { - Email string `json:"email"` - Password string `json:"password"` - Role string `json:"role"` - CanUseAuditLog *NullableBool `json:"can_use_audit_log"` - IsRestrictedConnectionModify *NullableBool `json:"is_restricted_connection_modify"` + Email string `json:"email"` + Password string `json:"password"` + Role string `json:"role"` + CanUseAuditLog *bool `json:"can_use_audit_log"` + IsRestrictedConnectionModify *bool `json:"is_restricted_connection_modify"` } type CreateUserOutput struct { @@ -117,9 +117,9 @@ func (client *TroccoClient) CreateUser(input *CreateUserInput) (*CreateUserOutpu // Update a User type UpdateUserInput struct { - Role *NullableString `json:"role"` - CanUseAuditLog *NullableBool `json:"can_use_audit_log"` - IsRestrictedConnectionModify *NullableBool `json:"is_restricted_connection_modify"` + Role *string `json:"role"` + CanUseAuditLog *bool `json:"can_use_audit_log"` + IsRestrictedConnectionModify *bool `json:"is_restricted_connection_modify"` } type UpdateUserOutput struct { diff --git a/internal/client/user_test.go b/internal/client/user_test.go index a89ab38a..7897469c 100644 --- a/internal/client/user_test.go +++ b/internal/client/user_test.go @@ -200,8 +200,8 @@ func TestCreateUser(t *testing.T) { input := CreateUserInput{ Email: "test@example.com", Role: "admin", - CanUseAuditLog: lo.ToPtr(NullableBool{Value: true, Valid: true}), - IsRestrictedConnectionModify: lo.ToPtr(NullableBool{Value: false, Valid: true}), + CanUseAuditLog: lo.ToPtr(true), + IsRestrictedConnectionModify: lo.ToPtr(false), } output, err := client.CreateUser(&input) if err != nil { @@ -252,9 +252,9 @@ func TestUpdateUser(t *testing.T) { client := NewDevTroccoClient("1234567890", server.URL) input := UpdateUserInput{ - Role: lo.ToPtr(NullableString{Value: "admin", Valid: true}), - CanUseAuditLog: lo.ToPtr(NullableBool{Value: true, Valid: true}), - IsRestrictedConnectionModify: lo.ToPtr(NullableBool{Value: false, Valid: true}), + Role: lo.ToPtr("admin"), + CanUseAuditLog: lo.ToPtr(true), + IsRestrictedConnectionModify: lo.ToPtr(false), } output, err := client.UpdateUser(1, &input) if err != nil { diff --git a/internal/provider/types.go b/internal/provider/types.go deleted file mode 100644 index c036bcaa..00000000 --- a/internal/provider/types.go +++ /dev/null @@ -1,17 +0,0 @@ -package provider - -import ( - "terraform-provider-trocco/internal/client" - - "github.com/hashicorp/terraform-plugin-framework/types" -) - -// NewNullableFromTerraformString create a client.NullableString from a types.String. -func newNullableFromTerraformString(v types.String) *client.NullableString { - return &client.NullableString{Valid: !v.IsNull(), Value: v.ValueString()} -} - -// NewNullableFromTerraformBool create a client.NullableBool from a types.Bool. -func newNullableFromTerraformBool(v types.Bool) *client.NullableBool { - return &client.NullableBool{Valid: !v.IsNull(), Value: v.ValueBool()} -} diff --git a/internal/provider/user_resource.go b/internal/provider/user_resource.go index c91a8541..d2c98c02 100644 --- a/internal/provider/user_resource.go +++ b/internal/provider/user_resource.go @@ -140,11 +140,15 @@ func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, r } input := client.CreateUserInput{ - Email: plan.Email.ValueString(), - Password: plan.Password.ValueString(), - Role: plan.Role.ValueString(), - CanUseAuditLog: newNullableFromTerraformBool(plan.CanUseAuditLog), - IsRestrictedConnectionModify: newNullableFromTerraformBool(plan.IsRestrictedConnectionModify), + Email: plan.Email.ValueString(), + Password: plan.Password.ValueString(), + Role: plan.Role.ValueString(), + } + if !plan.CanUseAuditLog.IsNull() { + input.CanUseAuditLog = plan.CanUseAuditLog.ValueBoolPointer() + } + if !plan.IsRestrictedConnectionModify.IsNull() { + input.IsRestrictedConnectionModify = plan.IsRestrictedConnectionModify.ValueBoolPointer() } user, err := r.client.CreateUser(&input) @@ -204,10 +208,15 @@ func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, r return } - input := client.UpdateUserInput{ - Role: newNullableFromTerraformString(plan.Role), - CanUseAuditLog: newNullableFromTerraformBool(plan.CanUseAuditLog), - IsRestrictedConnectionModify: newNullableFromTerraformBool(plan.IsRestrictedConnectionModify), + input := client.UpdateUserInput{} + if !plan.Role.IsNull() { + input.Role = plan.Role.ValueStringPointer() + } + if !plan.CanUseAuditLog.IsNull() { + input.CanUseAuditLog = plan.CanUseAuditLog.ValueBoolPointer() + } + if !plan.IsRestrictedConnectionModify.IsNull() { + input.IsRestrictedConnectionModify = plan.IsRestrictedConnectionModify.ValueBoolPointer() } user, err := r.client.UpdateUser(state.ID.ValueInt64(), &input) From 9eed13e690f3611e1d81726ce53ec4337d8d4788 Mon Sep 17 00:00:00 2001 From: yosuke-oka Date: Mon, 18 Nov 2024 14:43:56 +0900 Subject: [PATCH 25/25] Update test job name to 'Tests'. --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4311fa91..f4b5ab51 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,7 +55,7 @@ jobs: (echo; echo "Unexpected difference in directories after code generation. Run 'go generate ./...' command and commit."; exit 1) test: - name: Terraform Provider Acceptance Tests + name: Tests needs: build runs-on: ubuntu-latest timeout-minutes: 15