diff --git a/codefresh/cfclient/api_key.go b/codefresh/cfclient/api_key.go index 3a41dc5b..4bd54d91 100644 --- a/codefresh/cfclient/api_key.go +++ b/codefresh/cfclient/api_key.go @@ -245,6 +245,95 @@ func (client *Client) GetApiKeysList() ([]ApiKey, error) { return apiKeys, nil } +func (client *Client) GetAPIKeyServiceUser(keyID string, serviceUserId string) (*ApiKey, error) { + + opts := RequestOptions{ + Path: fmt.Sprintf("/auth/key/service-user/%s/%s", serviceUserId, keyID), + Method: "GET", + } + + resp, err := client.RequestAPI(&opts) + + if err != nil { + return nil, err + } + + var apiKey ApiKey + + err = DecodeResponseInto(resp, &apiKey) + if err != nil { + return nil, err + } + + return &apiKey, nil +} + +func (client *Client) DeleteAPIKeyServiceUser(keyID string, serviceUserId string) error { + + opts := RequestOptions{ + Path: fmt.Sprintf("/auth/key/service-user/%s/%s", serviceUserId, keyID), + Method: "DELETE", + } + + resp, err := client.RequestAPI(&opts) + if err != nil { + fmt.Println(string(resp)) + return err + } + + return nil +} + +func (client *Client) UpdateAPIKeyServiceUser(key *ApiKey, serviceUserId string) error { + + keyID := key.ID + if keyID == "" { + return errors.New("[ERROR] Key ID is empty") + } + + body, err := EncodeToJSON(key) + if err != nil { + return err + } + + opts := RequestOptions{ + Path: fmt.Sprintf("/auth/key/service-user/%s/%s", serviceUserId, keyID), + Method: "PATCH", + Body: body, + } + + resp, err := client.RequestAPI(&opts) + + if err != nil { + fmt.Println(string(resp)) + return err + } + + return nil +} + +func (client *Client) CreateApiKeyServiceUser(serviceUserId string, apiKey *ApiKey) (string, error) { + + body, err := EncodeToJSON(apiKey) + if err != nil { + return "", err + } + + opts := RequestOptions{ + Path: fmt.Sprintf("/auth/key/service-user/%s", serviceUserId), + Method: "POST", + Body: body, + } + + resp, err := client.RequestAPI(&opts) + + if err != nil { + return "", err + } + + return string(resp), nil +} + func (client *Client) createRandomUser(accountId string) (string, error) { // add user userPrefix := acctest.RandString(10) diff --git a/codefresh/cfclient/service_user.go b/codefresh/cfclient/service_user.go new file mode 100644 index 00000000..624c12b2 --- /dev/null +++ b/codefresh/cfclient/service_user.go @@ -0,0 +1,174 @@ +package cfclient + +import ( + "fmt" + "golang.org/x/exp/slices" +) + +type ServiceUserTeam struct { + ID string `json:"_id,omitempty"` +} + +type ServiceUser struct { + ID string `json:"_id,omitempty"` + Name string `json:"userName,omitempty"` + Teams []ServiceUserTeam `json:"teams,omitempty"` + Roles []string `json:"roles,omitempty"` +} + +type ServiceUserCreateUpdate struct { + ID string `json:"_id,omitempty"` + Name string `json:"userName,omitempty"` + TeamIDs []string `json:"teamIds,omitempty"` + AssignAdminRole bool `json:"assignAdminRole,omitempty"` +} + +// GetID implement CodefreshObject interface +func (serviceuser *ServiceUser) GetID() string { + return serviceuser.ID +} + +func (serviceuser *ServiceUser) HasAdminRole() bool { + return slices.Contains(serviceuser.Roles, "Admin") +} + +func (client *Client) GetServiceUserList() ([]ServiceUser, error) { + fullPath := "/service-users" + opts := RequestOptions{ + Path: fullPath, + Method: "GET", + } + + resp, err := client.RequestAPI(&opts) + + if err != nil { + return nil, err + } + + var serviceusers []ServiceUser + + err = DecodeResponseInto(resp, &serviceusers) + if err != nil { + return nil, err + } + + return serviceusers, nil +} + +func (client *Client) GetServiceUserByName(name string) (*ServiceUser, error) { + + serviceusers, err := client.GetServiceUserList() + if err != nil { + return nil, err + } + + for _, serviceuser := range serviceusers { + if serviceuser.Name == name { + return &serviceuser, nil + } + } + + return nil, nil +} + +func (client *Client) GetServiceUserByID(id string) (*ServiceUser, error) { + + fullPath := fmt.Sprintf("/service-users/%s", id) + opts := RequestOptions{ + Path: fullPath, + Method: "GET", + } + + resp, err := client.RequestAPI(&opts) + + if err != nil { + return nil, err + } + + var serviceuser ServiceUser + + err = DecodeResponseInto(resp, &serviceuser) + if err != nil { + return nil, err + } + + return &serviceuser, nil +} + +func (client *Client) CreateServiceUser(serviceUserCreateUpdate *ServiceUserCreateUpdate) (*ServiceUser, error) { + + fullPath := "/service-users" + body, err := EncodeToJSON(serviceUserCreateUpdate) + + if err != nil { + return nil, err + } + + opts := RequestOptions{ + Path: fullPath, + Method: "POST", + Body: body, + } + + resp, err := client.RequestAPI(&opts) + + if err != nil { + return nil, err + } + + var serviceuser ServiceUser + + err = DecodeResponseInto(resp, &serviceuser) + if err != nil { + return nil, err + } + + return &serviceuser, nil +} + +func (client *Client) UpdateServiceUser(serviceUserCreateUpdate *ServiceUserCreateUpdate) (*ServiceUser, error) { + + fullPath := fmt.Sprintf("/service-users/%s", serviceUserCreateUpdate.ID) + body, err := EncodeToJSON(serviceUserCreateUpdate) + + if err != nil { + return nil, err + } + + opts := RequestOptions{ + Path: fullPath, + Method: "PATCH", + Body: body, + } + + resp, err := client.RequestAPI(&opts) + + if err != nil { + return nil, err + } + + var serviceuser ServiceUser + + err = DecodeResponseInto(resp, &serviceuser) + if err != nil { + return nil, err + } + + return &serviceuser, nil +} + +func (client *Client) DeleteServiceUser(id string) error { + fullPath := fmt.Sprintf("/service-users/%s", id) + opts := RequestOptions{ + Path: fullPath, + Method: "DELETE", + } + + _, err := client.RequestAPI(&opts) + + if err != nil { + return err + } + + return nil +} diff --git a/codefresh/data_service_account.go b/codefresh/data_service_account.go new file mode 100644 index 00000000..b9480e46 --- /dev/null +++ b/codefresh/data_service_account.go @@ -0,0 +1,85 @@ +package codefresh + +import ( + "fmt" + + cfClient "github.com/codefresh-io/terraform-provider-codefresh/codefresh/cfclient" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceServiceAccount() *schema.Resource { + return &schema.Resource{ + Description: "This data source retrieves a Codefresh service account by its ID or name.", + Read: dataSourceServiceAccountRead, + Schema: map[string]*schema.Schema{ + "_id": { + Type: schema.TypeString, + Optional: true, + }, + "name": { + Description: "Service account name", + Type: schema.TypeString, + Optional: true, + AtLeastOneOf: []string{"_id", "name"}, + }, + "assign_admin_role": { + Description: "Whether or not account admin role is assigned to the service account", + Type: schema.TypeBool, + Optional: true, + }, + "assigned_teams": { + Description: "A list of team IDs the service account is be assigned to", + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func dataSourceServiceAccountRead(d *schema.ResourceData, meta interface{}) error { + + client := meta.(*cfClient.Client) + var serviceAccount *cfClient.ServiceUser + var err error + + if _id, _idOk := d.GetOk("_id"); _idOk { + serviceAccount, err = client.GetServiceUserByID(_id.(string)) + } else if name, nameOk := d.GetOk("name"); nameOk { + serviceAccount, err = client.GetServiceUserByName(name.(string)) + } + + if err != nil { + return err + } + + if serviceAccount == nil { + return fmt.Errorf("data.codefresh_service_account - cannot find service account") + } + + return mapDataServiceAccountToResource(serviceAccount, d) + +} + +func mapDataServiceAccountToResource(serviceAccount *cfClient.ServiceUser, d *schema.ResourceData) error { + + if serviceAccount == nil || serviceAccount.ID == "" { + return fmt.Errorf("data.codefresh_service_account - failed to mapDataServiceAccountToResource") + } + + d.SetId(serviceAccount.ID) + d.Set("name", serviceAccount.Name) + d.Set("assign_admin_role", serviceAccount.HasAdminRole()) + + teamIds := []string{} + + for _, team := range serviceAccount.Teams { + teamIds = append(teamIds, team.ID) + } + + d.Set("assigned_teams", teamIds) + + return nil +} diff --git a/codefresh/provider.go b/codefresh/provider.go index fafc1fa1..39d2fbf8 100644 --- a/codefresh/provider.go +++ b/codefresh/provider.go @@ -54,6 +54,7 @@ func Provider() *schema.Provider { "codefresh_project": dataSourceProject(), "codefresh_account_gitops_settings": dataSourceAccountGitopsSettings(), "codefresh_current_account_user": dataSourceCurrentAccountUser(), + "codefresh_service_account": dataSourceServiceAccount(), }, ResourcesMap: map[string]*schema.Resource{ "codefresh_account": resourceAccount(), @@ -74,6 +75,7 @@ func Provider() *schema.Provider { "codefresh_idp": resourceIdp(), "codefresh_account_idp": resourceAccountIdp(), "codefresh_account_gitops_settings": resourceAccountGitopsSettings(), + "codefresh_service_account": resourceServiceAccount(), }, ConfigureFunc: configureProvider, } diff --git a/codefresh/resource_api_key.go b/codefresh/resource_api_key.go index 6a8e45f2..e94449c7 100644 --- a/codefresh/resource_api_key.go +++ b/codefresh/resource_api_key.go @@ -13,8 +13,9 @@ import ( func resourceApiKey() *schema.Resource { return &schema.Resource{ Description: ` - Manages an API Key tied to an Account and a User. - Requires a Codefresh admin token and applies only to Codefresh on-premises installations. + Manages an API Key tied to a user within an account or a service account within the current account. + On the Codefresh SaaS platfrom this resource is only usable for service accounts. + Management of API keys for users in other accounts requires admin priveleges and hence can only be done on Codefresh on-premises installations. `, Create: resourceApiKeyCreate, Read: resourceApiKeyRead, @@ -30,14 +31,26 @@ func resourceApiKey() *schema.Resource { Required: true, }, "account_id": { - Description: "The ID of account in which the API key will be created.", - Type: schema.TypeString, - Required: true, + Description: "The ID of account in which the API key will be created. Required if user_id is set.", + Type: schema.TypeString, + Optional: true, + RequiredWith: []string{"user_id", "account_id"}, + ForceNew: true, }, "user_id": { - Description: "The ID of a user within the referenced `account_id` that will own the API key.", - Type: schema.TypeString, - Required: true, + Description: "The ID of a user within the referenced `account_id` that will own the API key. Requires a Codefresh admin token and can be used only in Codefresh on-premises installations.", + Type: schema.TypeString, + Optional: true, + ExactlyOneOf: []string{"user_id", "service_account_id"}, + RequiredWith: []string{"user_id", "account_id"}, + ForceNew: true, + }, + "service_account_id": { + Description: "The ID of the service account to create the API key for.", + Type: schema.TypeString, + Optional: true, + ExactlyOneOf: []string{"user_id", "service_account_id"}, + ForceNew: true, }, "token": { Description: "The resulting API key.", @@ -79,12 +92,22 @@ A list of access scopes for the API key. The possible values: func resourceApiKeyCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*cfclient.Client) - apiKey := *mapResourceToApiKey(d) - accountID := d.Get("account_id").(string) - userID := d.Get("user_id").(string) - resp, err := client.CreateApiKey(userID, accountID, &apiKey) + var ( + resp string + err error + ) + + if serviceAccountId := d.Get("service_account_id").(string); serviceAccountId != "" { + resp, err = client.CreateApiKeyServiceUser(serviceAccountId, &apiKey) + } else { + accountID := d.Get("account_id").(string) + userID := d.Get("user_id").(string) + + resp, err = client.CreateApiKey(userID, accountID, &apiKey) + } + if err != nil { fmt.Println(string(resp)) return err @@ -117,7 +140,17 @@ func resourceApiKeyRead(d *schema.ResourceData, meta interface{}) error { return errors.New("[ERROR] Can't read API Key. Token is empty.") } - apiKey, err := client.GetAPIKey(keyID) + var ( + apiKey *cfclient.ApiKey + err error + ) + + if serviceAccountId := d.Get("service_account_id").(string); serviceAccountId != "" { + apiKey, err = client.GetAPIKeyServiceUser(keyID, serviceAccountId) + } else { + apiKey, err = client.GetAPIKey(keyID) + } + if err != nil { return err } @@ -140,7 +173,15 @@ func resourceApiKeyUpdate(d *schema.ResourceData, meta interface{}) error { return errors.New("[ERROR] Can't read API Key. Token is empty.") } - err := client.UpdateAPIKey(&apiKey) + var err error + + if serviceAccountId := d.Get("service_account_id").(string); serviceAccountId != "" { + err = client.UpdateAPIKeyServiceUser(&apiKey, serviceAccountId) + } else { + err = client.UpdateAPIKey(&apiKey) + + } + if err != nil { return err } @@ -156,7 +197,13 @@ func resourceApiKeyDelete(d *schema.ResourceData, meta interface{}) error { return errors.New("[ERROR] Can't read API Key. Token is empty.") } - err := client.DeleteAPIKey(d.Id()) + var err error + if serviceAccountId := d.Get("service_account_id").(string); serviceAccountId != "" { + err = client.DeleteAPIKeyServiceUser(d.Id(), serviceAccountId) + } else { + err = client.DeleteAPIKey(d.Id()) + } + if err != nil { return err } diff --git a/codefresh/resource_api_key_test.go b/codefresh/resource_api_key_test.go new file mode 100644 index 00000000..68f40e56 --- /dev/null +++ b/codefresh/resource_api_key_test.go @@ -0,0 +1,133 @@ +package codefresh + +import ( + "fmt" + "testing" + + "github.com/codefresh-io/terraform-provider-codefresh/codefresh/cfclient" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +var apiKeyNamePrefix = "TerraformAccTest_" + +func TestAccCodefreshAPIKey_ServiceUser(t *testing.T) { + name := apiKeyNamePrefix + acctest.RandString(10) + + resourceName := "codefresh_api_key.test_apikey" + serviceAccountResourceName := "codefresh_service_account.test_apikey" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCodefreshServiceUserAndAPIKeyDestroyed, + Steps: []resource.TestStep{ + { + Config: testAccCodefreshAPIKeyServiceAccount(name, name), + Check: resource.ComposeTestCheckFunc( + testAccCheckCodefreshServiceUserAPIKeyExists(resourceName, serviceAccountResourceName), + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "scopes.0", "agent"), + ), + }, + { + ResourceName: resourceName, + RefreshState: true, + }, + }, + }) +} + +func testAccCheckCodefreshServiceUserAPIKeyExists(apiKeyResource string, serviceUserResource string) resource.TestCheckFunc { + return func(state *terraform.State) error { + serviceUserState, ok := state.RootModule().Resources[serviceUserResource] + + if !ok { + return fmt.Errorf("Not found: %s", serviceUserResource) + } + + if serviceUserState.Primary.ID == "" { + return fmt.Errorf("No Record ID is set") + } + + apiKeyState, ok := state.RootModule().Resources[apiKeyResource] + + if !ok { + return fmt.Errorf("Not found: %s", apiKeyResource) + } + + if apiKeyState.Primary.ID == "" { + return fmt.Errorf("No Record ID is set for team") + } + + serviceUserID := serviceUserState.Primary.ID + apiKeyID := apiKeyState.Primary.ID + + apiClient := testAccProvider.Meta().(*cfclient.Client) + _, err := apiClient.GetAPIKeyServiceUser(apiKeyID, serviceUserID) + + if err != nil { + return fmt.Errorf("error fetching service user api key for resource %s. %s", apiKeyID, err) + } + + return nil + } +} + +func testAccCheckCodefreshServiceUserAndAPIKeyDestroyed(s *terraform.State) error { + apiClient := testAccProvider.Meta().(*cfclient.Client) + + for _, rs := range s.RootModule().Resources { + + if rs.Type != "codefresh_service_account" && rs.Type != "codefresh_api_key" { + continue + } + + var ( + serviceAccountId string + apiKeyId string + ) + + if rs.Type == "codefresh_service_account" { + serviceAccountId = rs.Primary.ID + _, err := apiClient.GetServiceUserByID(serviceAccountId) + + if err == nil { + return fmt.Errorf("Alert service account still exists") + } + } + + if rs.Type == "codefresh_api_key" { + apiKeyId = rs.Primary.ID + _, err := apiClient.GetAPIKeyServiceUser(apiKeyId, serviceAccountId) + + if err == nil { + return fmt.Errorf("Alert api key still exists") + } + } + } + + return nil +} + +func testAccCodefreshAPIKeyServiceAccount(apiKeyName string, serviceUserName string) string { + return fmt.Sprintf(` +resource "codefresh_service_account" "test_apikey" { + name = "%s" +} + +resource "codefresh_api_key" "test_apikey" { + service_account_id = codefresh_service_account.test_apikey.id + name = "%s" + scopes = [ + "agent", + "agents", + "audit", + "api-keys" + ] +} + + +`, serviceUserName, apiKeyName) +} diff --git a/codefresh/resource_service_account.go b/codefresh/resource_service_account.go new file mode 100644 index 00000000..91d94ed2 --- /dev/null +++ b/codefresh/resource_service_account.go @@ -0,0 +1,151 @@ +package codefresh + +import ( + "fmt" + + "github.com/codefresh-io/terraform-provider-codefresh/codefresh/cfclient" + "github.com/codefresh-io/terraform-provider-codefresh/codefresh/internal/datautil" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceServiceAccount() *schema.Resource { + return &schema.Resource{ + Description: "A service account is an identity that provides automated processes, applications, and services with the necessary permissions to interact securely with the Codefresh platform", + Create: resourceServiceAccountCreate, + Read: resourceServiceAccountRead, + Update: resourceServiceAccountUpdate, + Delete: resourceServiceAccountDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "name": { + Description: "Service account display name", + Type: schema.TypeString, + Required: true, + }, + "assign_admin_role": { + Description: "Whether or not to assign account admin role to the service account", + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "assigned_teams": { + Description: "A list of team IDs the service account is be assigned to", + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func resourceServiceAccountCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cfclient.Client) + + newSerivceAccount := *mapResourceToServiceAccount(d) + + resp, err := client.CreateServiceUser(&newSerivceAccount) + if err != nil { + return err + } + + d.SetId(resp.ID) + + return nil +} + +func resourceServiceAccountRead(d *schema.ResourceData, meta interface{}) error { + + client := meta.(*cfclient.Client) + + serviceAccountID := d.Id() + + if serviceAccountID == "" { + d.SetId("") + return nil + } + + serviceAccount, err := client.GetServiceUserByID(serviceAccountID) + + if err != nil { + return err + } + + err = mapServiceAccountToResource(serviceAccount, d) + if err != nil { + return err + } + + return nil +} + +func resourceServiceAccountUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cfclient.Client) + + updateServiceAccount := *mapResourceToServiceAccount(d) + + _, err := client.UpdateServiceUser(&updateServiceAccount) + + if err != nil { + return err + } + + return nil +} + +func resourceServiceAccountDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cfclient.Client) + + err := client.DeleteServiceUser(d.Id()) + + if err != nil { + return err + } + + return nil +} + +func mapServiceAccountToResource(serviceAccount *cfclient.ServiceUser, d *schema.ResourceData) error { + + if serviceAccount == nil { + return fmt.Errorf("mapServiceAccountToResource - cannot find service account") + } + err := d.Set("name", serviceAccount.Name) + + if err != nil { + return err + } + + teamIds := []string{} + + for _, team := range serviceAccount.Teams { + teamIds = append(teamIds, team.ID) + } + + err = d.Set("assigned_teams", teamIds) + + if err != nil { + return err + } + + err = d.Set("assign_admin_role", serviceAccount.HasAdminRole()) + + if err != nil { + return err + } + + return nil +} + +func mapResourceToServiceAccount(d *schema.ResourceData) *cfclient.ServiceUserCreateUpdate { + + return &cfclient.ServiceUserCreateUpdate{ + ID: d.Id(), + Name: d.Get("name").(string), + TeamIDs: datautil.ConvertStringArr(d.Get("assigned_teams").(*schema.Set).List()), + AssignAdminRole: d.Get("assign_admin_role").(bool), + } +} diff --git a/codefresh/resource_service_account_test.go b/codefresh/resource_service_account_test.go new file mode 100644 index 00000000..c03db0ad --- /dev/null +++ b/codefresh/resource_service_account_test.go @@ -0,0 +1,154 @@ +package codefresh + +import ( + "fmt" + "regexp" + "testing" + + "github.com/codefresh-io/terraform-provider-codefresh/codefresh/cfclient" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +var serviceUserNamePrefix = "TerraformAccTest_" + +func TestAccCodefreshServiceUser_WithTeamAssignment(t *testing.T) { + name := serviceUserNamePrefix + acctest.RandString(10) + + resourceName := "codefresh_service_account.test_serviceaccount" + teamResourceName := "codefresh_team.test_serviceaccount" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCodefreshServiceUserDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCodefreshServiceUserTeam(name, name, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckCodefreshServiceUserExists(resourceName), + testAccCheckCodefreshServiceUserAssignedToTeam(resourceName, teamResourceName), + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "assign_admin_role", "false"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckCodefreshServiceUserExists(resource string) resource.TestCheckFunc { + return func(state *terraform.State) error { + rs, ok := state.RootModule().Resources[resource] + if !ok { + return fmt.Errorf("Not found: %s", resource) + } + if rs.Primary.ID == "" { + return fmt.Errorf("No Record ID is set") + } + + serviceUserID := rs.Primary.ID + + apiClient := testAccProvider.Meta().(*cfclient.Client) + _, err := apiClient.GetServiceUserByID(serviceUserID) + + if err != nil { + return fmt.Errorf("error fetching serviceUser with resource %s. %s", resource, err) + } + return nil + } +} + +func testAccCheckCodefreshServiceUserAssignedToTeam(serviceUserResource string, teamResource string) resource.TestCheckFunc { + return func(state *terraform.State) error { + serviceUserState, ok := state.RootModule().Resources[serviceUserResource] + + if !ok { + return fmt.Errorf("Not found: %s", serviceUserResource) + } + + if serviceUserState.Primary.ID == "" { + return fmt.Errorf("No Record ID is set") + } + + teamState, ok := state.RootModule().Resources[teamResource] + + if !ok { + return fmt.Errorf("Not found: %s", teamResource) + } + + if teamState.Primary.ID == "" { + return fmt.Errorf("No Record ID is set for team") + } + + serviceUserID := serviceUserState.Primary.ID + teamID := teamState.Primary.ID + + apiClient := testAccProvider.Meta().(*cfclient.Client) + serviceUser, err := apiClient.GetServiceUserByID(serviceUserID) + + if err != nil { + return fmt.Errorf("error fetching serviceUser with resource %s. %s", serviceUserID, err) + } + + isTeamAssigned := false + + for _, team := range serviceUser.Teams { + if team.ID == teamID { + isTeamAssigned = true + break + } + } + + if !isTeamAssigned { + return fmt.Errorf("service user %s is not assigned to team %s", serviceUserID, teamID) + } + + return nil + } +} + +func testAccCheckCodefreshServiceUserDestroy(s *terraform.State) error { + apiClient := testAccProvider.Meta().(*cfclient.Client) + + for _, rs := range s.RootModule().Resources { + + if rs.Type != "codefresh_service_account" { + continue + } + + _, err := apiClient.GetServiceUserByID(rs.Primary.ID) + + if err == nil { + return fmt.Errorf("Alert still exists") + } + notFoundErr := "does not exist" + expectedErr := regexp.MustCompile(notFoundErr) + if !expectedErr.Match([]byte(err.Error())) { + return fmt.Errorf("expected %s, got %s", notFoundErr, err) + } + + } + + return nil +} + +func testAccCodefreshServiceUserTeam(serviceUserName string, teamName string, assignAdminRole bool) string { + return fmt.Sprintf(` +resource "codefresh_team" "test_serviceaccount" { + name = "%s" +} + +resource "codefresh_service_account" "test_serviceaccount" { + name = "%s" + assigned_teams = [codefresh_team.test_serviceaccount.id] + assign_admin_role = %t + +} +`, serviceUserName, teamName, assignAdminRole) +} diff --git a/docs/data-sources/current_account_user.md b/docs/data-sources/current_account_user.md new file mode 100644 index 00000000..64d8b268 --- /dev/null +++ b/docs/data-sources/current_account_user.md @@ -0,0 +1,27 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "codefresh_current_account_user Data Source - terraform-provider-codefresh" +subcategory: "" +description: |- + Returns a user the current Codefresh account by name or email. +--- + +# codefresh_current_account_user (Data Source) + +Returns a user the current Codefresh account by name or email. + + + + +## Schema + +### Optional + +- `email` (String) +- `name` (String) + +### Read-Only + +- `id` (String) The ID of this resource. + + diff --git a/docs/data-sources/service_account.md b/docs/data-sources/service_account.md new file mode 100644 index 00000000..7f3d625e --- /dev/null +++ b/docs/data-sources/service_account.md @@ -0,0 +1,29 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "codefresh_service_account Data Source - terraform-provider-codefresh" +subcategory: "" +description: |- + This data source retrieves a Codefresh service account by its ID or name. +--- + +# codefresh_service_account (Data Source) + +This data source retrieves a Codefresh service account by its ID or name. + + + + +## Schema + +### Optional + +- `_id` (String) +- `assign_admin_role` (Boolean) Whether or not account admin role is assigned to the service account +- `assigned_teams` (Set of String) A list of team IDs the service account is be assigned to +- `name` (String) Service account name + +### Read-Only + +- `id` (String) The ID of this resource. + + diff --git a/docs/resources/api_key.md b/docs/resources/api_key.md index 276cb1e1..2e9b8e21 100644 --- a/docs/resources/api_key.md +++ b/docs/resources/api_key.md @@ -2,21 +2,60 @@ page_title: "codefresh_api_key Resource - terraform-provider-codefresh" subcategory: "" description: |- - Manages an API Key tied to an Account and a User. - Requires a Codefresh admin token and applies only to Codefresh on-premises installations. + Manages an API Key tied to a user within an account or a service account within the current account. + On the Codefresh SaaS platfrom this resource is only usable for service accounts. + Management of API keys for users in other accounts requires admin priveleges and hence can only be done on Codefresh on-premises installations. --- # codefresh_api_key (Resource) -Manages an API Key tied to an Account and a User. - Requires a Codefresh admin token and applies only to Codefresh on-premises installations. - -terraform-provider-codefresh itself uses an API key, passed as provider's attribute, but it's possible to use that API Key to generate a new one. -This resource requires Codefresh system admin permissions, hence is relevant for on-prem deployments of Codefresh only. +Manages an API Key tied to a user within an account or a service account within the current account. + On the Codefresh SaaS platfrom this resource is only usable for service accounts. + Management of API keys for users in other accounts requires admin priveleges and hence can only be done on Codefresh on-premises installations. +terraform-provider-codefresh itself uses an API key, passed as provider's attribute, but it's possible to use that API Key to generate a new one. ## Example usage +### With service accounts + +```hcl +provider "codefresh" { + api_url = "my API URL" + token = "my init API token" +} + +resource "codefresh_service_account" "example" { + name = "example-service-account" +} + +resource "codefresh_api_key" "example" { + service_account_id = codefresh_service_account.example.id + name = "example-token" + scopes = [ + "project" + ] +} + +provider "codefresh" { + alias = "project_creator_sa" + api_url = "my API URL" + token = codefresh_api_key.example.token +} + +resource "codefresh_project" "example" { + + provider = codefresh.project_creator_sa + + name = "myproject" + + tags = [ + "team_1" + ] +} +``` + +### With user and account combination (on-premise only) ```hcl provider "codefresh" { api_url = "my API URL" @@ -79,12 +118,11 @@ resource "codefresh_team" "team_1" { ### Required -- `account_id` (String) The ID of account in which the API key will be created. - `name` (String) The display name for the API key. -- `user_id` (String) The ID of a user within the referenced `account_id` that will own the API key. ### Optional +- `account_id` (String) The ID of account in which the API key will be created. Required if user_id is set. - `scopes` (Set of String) A list of access scopes for the API key. The possible values: * agent * agents @@ -104,8 +142,10 @@ resource "codefresh_team" "team_1" { * step-types * view * workflow +- `service_account_id` (String) The ID of the service account to create the API key for. +- `user_id` (String) The ID of a user within the referenced `account_id` that will own the API key. Requires a Codefresh admin token and can be used only in Codefresh on-premises installations. ### Read-Only - `id` (String) The ID of this resource. -- `token` (String, Sensitive) The resulting API key. \ No newline at end of file +- `token` (String, Sensitive) The resulting API key. diff --git a/docs/resources/service_account.md b/docs/resources/service_account.md new file mode 100644 index 00000000..e9b72763 --- /dev/null +++ b/docs/resources/service_account.md @@ -0,0 +1,50 @@ +--- +page_title: "codefresh_service_account Resource - terraform-provider-codefresh" +subcategory: "" +description: |- + A service account is an identity that provides automated processes, applications, and services with the necessary permissions to interact securely with the Codefresh platform +--- + +# codefresh_service_account (Resource) + +A service account is an identity that provides automated processes, applications, and services with the necessary permissions to interact securely with the Codefresh platform + +For more information about service accounts in Codefresh see [official documentation](https://codefresh.io/docs/docs/administration/account-user-management/service-accounts). + +It is also possible to generate API tokens for service accounts, see the documentation for `codefresh_api_key` resource for usage example. + +## Example Usage + +```hcl +data "codefresh_team" "serviceaccounts" { + name = "service-accounts" +} + +resource "codefresh_service_account" "example" { + name = "tf-test1" + assign_admin_role = true + assigned_teams = [data.codefresh_team.serviceaccounts.id] +} +``` + + +## Schema + +### Required + +- `name` (String) Service account display name + +### Optional + +- `assign_admin_role` (Boolean) Whether or not to assign account admin role to the service account +- `assigned_teams` (Set of String) A list of team IDs the service account is be assigned to + +### Read-Only + +- `id` (String) The ID of this resource. + +## Import + +```sh +terraform import codefresh_service_account.test xxxxxxxxxxxxxxxxxxx +``` diff --git a/templates/resources/api_key.md.tmpl b/templates/resources/api_key.md.tmpl index 8c618e54..219f2591 100644 --- a/templates/resources/api_key.md.tmpl +++ b/templates/resources/api_key.md.tmpl @@ -9,12 +9,49 @@ description: |- {{ .Description | trimspace }} -{{ .ProviderName }} itself uses an API key, passed as provider's attribute, but it's possible to use that API Key to generate a new one. -This resource requires Codefresh system admin permissions, hence is relevant for on-prem deployments of Codefresh only. - +{{ .ProviderName }} itself uses an API key, passed as provider's attribute, but it's possible to use that API Key to generate a new one. ## Example usage +### With service accounts + +```hcl +provider "codefresh" { + api_url = "my API URL" + token = "my init API token" +} + +resource "codefresh_service_account" "example" { + name = "example-service-account" +} + +resource "codefresh_api_key" "example" { + service_account_id = codefresh_service_account.example.id + name = "example-token" + scopes = [ + "project" + ] +} + +provider "codefresh" { + alias = "project_creator_sa" + api_url = "my API URL" + token = codefresh_api_key.example.token +} + +resource "codefresh_project" "example" { + + provider = codefresh.project_creator_sa + + name = "myproject" + + tags = [ + "team_1" + ] +} +``` + +### With user and account combination (on-premise only) ```hcl provider "codefresh" { api_url = "my API URL" @@ -72,4 +109,4 @@ resource "codefresh_team" "team_1" { } ``` -{{ .SchemaMarkdown | trimspace }} \ No newline at end of file +{{ .SchemaMarkdown | trimspace }} diff --git a/templates/resources/service_account.md.tmpl b/templates/resources/service_account.md.tmpl new file mode 100644 index 00000000..9bf0c625 --- /dev/null +++ b/templates/resources/service_account.md.tmpl @@ -0,0 +1,36 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +For more information about service accounts in Codefresh see [official documentation](https://codefresh.io/docs/docs/administration/account-user-management/service-accounts). + +It is also possible to generate API tokens for service accounts, see the documentation for `codefresh_api_key` resource for usage example. + +## Example Usage + +```hcl +data "codefresh_team" "serviceaccounts" { + name = "service-accounts" +} + +resource "codefresh_service_account" "example" { + name = "tf-test1" + assign_admin_role = true + assigned_teams = [data.codefresh_team.serviceaccounts.id] +} +``` + +{{ .SchemaMarkdown | trimspace }} + +## Import + +```sh +terraform import codefresh_service_account.test xxxxxxxxxxxxxxxxxxx +```