From 41345d304f99ed5b51af085668f3954cb9acbe33 Mon Sep 17 00:00:00 2001 From: Emanuele Date: Mon, 24 Jul 2023 16:03:29 +0200 Subject: [PATCH] feat (team service account) Manage Team Based Service Account as Terraform resource (#386) * add support for team service account * add documentation * fix typo in test * add test for secure * add sensitive flag to api_key * fix error description * replace schema strings with constants * rename from GetTeamServiceAccountById -> GetTeamServiceAccountByID * replace strings with costants also in utility methods --- sysdig/common.go | 6 + sysdig/internal/client/v2/model.go | 11 + sysdig/internal/client/v2/sysdig.go | 1 + .../client/v2/team_service_account.go | 123 +++++++++++ sysdig/provider.go | 1 + .../resource_sysdig_team_service_account.go | 198 ++++++++++++++++++ ...source_sysdig_team_service_account_test.go | 97 +++++++++ website/docs/r/team_service_account.md | 67 ++++++ 8 files changed, 504 insertions(+) create mode 100644 sysdig/internal/client/v2/team_service_account.go create mode 100644 sysdig/resource_sysdig_team_service_account.go create mode 100644 sysdig/resource_sysdig_team_service_account_test.go create mode 100644 website/docs/r/team_service_account.md diff --git a/sysdig/common.go b/sysdig/common.go index 7a75fff9..5cf1bba3 100644 --- a/sysdig/common.go +++ b/sysdig/common.go @@ -2,6 +2,7 @@ package sysdig const ( SchemaIDKey = "id" + SchemaTeamIDKey = "team_id" SchemaPoliciesKey = "policies" SchemaPolicyIDsKey = "policy_ids" SchemaNameKey = "name" @@ -14,7 +15,9 @@ const ( SchemaAuthorKey = "author" SchemaLastModifiedBy = "last_modified_by" SchemaLastUpdated = "last_updated" + SchemaExpirationDateKey = "expiration_date" SchemaPublishedDateKey = "published_date" + SchemaCreatedDateKey = "date_created" SchemaMinKubeVersionKey = "min_kube_version" SchemaMaxKubeVersionKey = "max_kube_version" SchemaIsCustomKey = "is_custom" @@ -26,5 +29,8 @@ const ( SchemaScopeKey = "scope" SchemaScopesKey = "scopes" SchemaTargetTypeKey = "target_type" + SchemaRoleKey = "role" + SchemaSystemRoleKey = "system_role" SchemaRulesKey = "rules" + SchemaApiKeyKey = "api_key" ) diff --git a/sysdig/internal/client/v2/model.go b/sysdig/internal/client/v2/model.go index a8539ede..e7f7301f 100644 --- a/sysdig/internal/client/v2/model.go +++ b/sysdig/internal/client/v2/model.go @@ -38,6 +38,17 @@ type UserRoles struct { Admin bool `json:"admin,omitempty"` } +type TeamServiceAccount struct { + ID int `json:"id,omitempty"` + Name string `json:"name"` + SystemRole string `json:"systemRole"` + TeamId int `json:"teamId"` + TeamRole string `json:"teamRole"` + DateCreated int64 `json:"dateCreated,omitempty"` + ExpirationDate int64 `json:"expirationDate"` + ApiKey string `json:"apiKey,omitempty"` +} + type EntryPoint struct { Module string `json:"module"` Selection string `json:"selection,omitempty"` diff --git a/sysdig/internal/client/v2/sysdig.go b/sysdig/internal/client/v2/sysdig.go index b19b2007..2e35e9e7 100644 --- a/sysdig/internal/client/v2/sysdig.go +++ b/sysdig/internal/client/v2/sysdig.go @@ -20,6 +20,7 @@ type SysdigCommon interface { Common GroupMappingInterface GroupMappingConfigInterface + TeamServiceAccountInterface } type SysdigMonitor interface { diff --git a/sysdig/internal/client/v2/team_service_account.go b/sysdig/internal/client/v2/team_service_account.go new file mode 100644 index 00000000..e7f9a832 --- /dev/null +++ b/sysdig/internal/client/v2/team_service_account.go @@ -0,0 +1,123 @@ +package v2 + +import ( + "context" + "errors" + "fmt" + "net/http" +) + +var TeamServiceAccountNotFound = errors.New("team service account not found") + +const ( + ServiceAccountsPath = "%s/api/serviceaccounts/team" + ServiceAccountPath = "%s/api/serviceaccounts/team/%d" + ServiceAccountDeletePath = "%s/api/serviceaccounts/team/%d/delete" +) + +type TeamServiceAccountInterface interface { + Base + GetTeamServiceAccountByID(ctx context.Context, id int) (*TeamServiceAccount, error) + CreateTeamServiceAccount(ctx context.Context, account *TeamServiceAccount) (*TeamServiceAccount, error) + UpdateTeamServiceAccount(ctx context.Context, account *TeamServiceAccount, id int) (*TeamServiceAccount, error) + DeleteTeamServiceAccount(ctx context.Context, id int) error +} + +func (client *Client) GetTeamServiceAccountByID(ctx context.Context, id int) (*TeamServiceAccount, error) { + response, err := client.requester.Request(ctx, http.MethodGet, client.GetTeamServiceAccountURL(id), nil) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + if response.StatusCode == http.StatusNotFound { + return nil, TeamServiceAccountNotFound + } + return nil, client.ErrorFromResponse(response) + } + + teamServiceAccount, err := Unmarshal[TeamServiceAccount](response.Body) + if err != nil { + return nil, err + } + return &teamServiceAccount, nil +} + +func (client *Client) CreateTeamServiceAccount(ctx context.Context, account *TeamServiceAccount) (*TeamServiceAccount, error) { + payload, err := Marshal(account) + if err != nil { + return nil, err + } + + response, err := client.requester.Request(ctx, http.MethodPost, client.CreateTeamServiceAccountURL(), payload) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, client.ErrorFromResponse(response) + } + + created, err := Unmarshal[TeamServiceAccount](response.Body) + if err != nil { + return nil, err + } + + return &created, nil +} + +func (client *Client) UpdateTeamServiceAccount(ctx context.Context, account *TeamServiceAccount, id int) (*TeamServiceAccount, error) { + payload, err := Marshal(account) + if err != nil { + return nil, err + } + + response, err := client.requester.Request(ctx, http.MethodPut, client.UpdateTeamServiceAccountURL(id), payload) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, client.ErrorFromResponse(response) + } + + updated, err := Unmarshal[TeamServiceAccount](response.Body) + if err != nil { + return nil, err + } + + return &updated, nil +} + +func (client *Client) DeleteTeamServiceAccount(ctx context.Context, id int) error { + response, err := client.requester.Request(ctx, http.MethodDelete, client.DeleteTeamServiceAccountURL(id), nil) + if err != nil { + return err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusNoContent && response.StatusCode != http.StatusOK && response.StatusCode != http.StatusNotFound { + return client.ErrorFromResponse(response) + } + + return nil +} + +func (client *Client) GetTeamServiceAccountURL(id int) string { + return fmt.Sprintf(ServiceAccountPath, client.config.url, id) +} + +func (client *Client) CreateTeamServiceAccountURL() string { + return fmt.Sprintf(ServiceAccountsPath, client.config.url) +} + +func (client *Client) UpdateTeamServiceAccountURL(id int) string { + return fmt.Sprintf(ServiceAccountPath, client.config.url, id) +} + +func (client *Client) DeleteTeamServiceAccountURL(id int) string { + return fmt.Sprintf(ServiceAccountDeletePath, client.config.url, id) +} diff --git a/sysdig/provider.go b/sysdig/provider.go index 43cc8ba3..9f0b18f9 100644 --- a/sysdig/provider.go +++ b/sysdig/provider.go @@ -102,6 +102,7 @@ func Provider() *schema.Provider { "sysdig_user": resourceSysdigUser(), "sysdig_group_mapping": resourceSysdigGroupMapping(), "sysdig_group_mapping_config": resourceSysdigGroupMappingConfig(), + "sysdig_team_service_account": resourceSysdigTeamServiceAccount(), "sysdig_secure_custom_policy": resourceSysdigSecureCustomPolicy(), "sysdig_secure_managed_policy": resourceSysdigSecureManagedPolicy(), diff --git a/sysdig/resource_sysdig_team_service_account.go b/sysdig/resource_sysdig_team_service_account.go new file mode 100644 index 00000000..c340d8dc --- /dev/null +++ b/sysdig/resource_sysdig_team_service_account.go @@ -0,0 +1,198 @@ +package sysdig + +import ( + "context" + v2 "github.com/draios/terraform-provider-sysdig/sysdig/internal/client/v2" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "strconv" + "time" +) + +func resourceSysdigTeamServiceAccount() *schema.Resource { + timeout := 5 * time.Minute + return &schema.Resource{ + ReadContext: resourceSysdigTeamServiceAccountRead, + CreateContext: resourceSysdigTeamServiceAccountCreate, + UpdateContext: resourceSysdigTeamServiceAccountUpdate, + DeleteContext: resourceSysdigTeamServiceAccountDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(timeout), + Update: schema.DefaultTimeout(timeout), + Read: schema.DefaultTimeout(timeout), + Delete: schema.DefaultTimeout(timeout), + }, + Schema: map[string]*schema.Schema{ + SchemaNameKey: { + Type: schema.TypeString, + Required: true, + }, + SchemaRoleKey: { + Type: schema.TypeString, + Optional: true, + Default: "ROLE_TEAM_READ", + }, + SchemaExpirationDateKey: { + Type: schema.TypeInt, + Required: true, + }, + SchemaTeamIDKey: { + Type: schema.TypeInt, + Required: true, + }, + SchemaSystemRoleKey: { + Type: schema.TypeString, + Optional: true, + Default: "ROLE_SERVICE_ACCOUNT", + }, + SchemaCreatedDateKey: { + Type: schema.TypeInt, + Computed: true, + }, + SchemaApiKeyKey: { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + }, + } +} + +func resourceSysdigTeamServiceAccountRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client, err := m.(SysdigClients).sysdigCommonClientV2() + if err != nil { + diag.FromErr(err) + } + + id, err := strconv.Atoi(d.Id()) + if err != nil { + diag.FromErr(err) + } + + teamServiceAccount, err := client.GetTeamServiceAccountByID(ctx, id) + if err != nil { + if err == v2.TeamServiceAccountNotFound { + d.SetId("") + return nil + } + return diag.FromErr(err) + } + + err = teamServiceAccountToResourceData(teamServiceAccount, d) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceSysdigTeamServiceAccountCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var err error + + client, err := m.(SysdigClients).sysdigCommonClientV2() + if err != nil { + return diag.FromErr(err) + } + + teamServiceAccount := teamServiceAccountFromResourceData(d) + teamServiceAccount, err = client.CreateTeamServiceAccount(ctx, teamServiceAccount) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(strconv.Itoa(teamServiceAccount.ID)) + + resourceSysdigTeamServiceAccountRead(ctx, d, m) + + return nil +} + +func resourceSysdigTeamServiceAccountUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var err error + + client, err := m.(SysdigClients).sysdigCommonClientV2() + if err != nil { + return diag.FromErr(err) + } + + teamServiceAccount := teamServiceAccountFromResourceData(d) + id, err := strconv.Atoi(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + teamServiceAccount.ID = id + _, err = client.UpdateTeamServiceAccount(ctx, teamServiceAccount, id) + if err != nil { + return diag.FromErr(err) + } + + resourceSysdigTeamServiceAccountRead(ctx, d, m) + + return nil +} + +func resourceSysdigTeamServiceAccountDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client, err := m.(SysdigClients).sysdigCommonClientV2() + if err != nil { + return diag.FromErr(err) + } + + id, err := strconv.Atoi(d.Id()) + if err != nil { + return diag.FromErr(err) + } + err = client.DeleteTeamServiceAccount(ctx, id) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func teamServiceAccountFromResourceData(d *schema.ResourceData) *v2.TeamServiceAccount { + return &v2.TeamServiceAccount{ + Name: d.Get(SchemaNameKey).(string), + TeamRole: d.Get(SchemaRoleKey).(string), + ExpirationDate: int64(d.Get(SchemaExpirationDateKey).(int) * 1000), + TeamId: d.Get(SchemaTeamIDKey).(int), + SystemRole: d.Get(SchemaSystemRoleKey).(string), + ApiKey: d.Get(SchemaApiKeyKey).(string), + } +} + +func teamServiceAccountToResourceData(teamServiceAccount *v2.TeamServiceAccount, d *schema.ResourceData) error { + err := d.Set(SchemaNameKey, teamServiceAccount.Name) + if err != nil { + return err + } + err = d.Set(SchemaRoleKey, teamServiceAccount.TeamRole) + if err != nil { + return err + } + err = d.Set(SchemaExpirationDateKey, teamServiceAccount.ExpirationDate/1000) + if err != nil { + return err + } + err = d.Set(SchemaTeamIDKey, teamServiceAccount.TeamId) + if err != nil { + return err + } + err = d.Set(SchemaSystemRoleKey, teamServiceAccount.SystemRole) + if err != nil { + return err + } + err = d.Set(SchemaCreatedDateKey, teamServiceAccount.DateCreated) + if err != nil { + return err + } + err = d.Set(SchemaApiKeyKey, teamServiceAccount.ApiKey) + if err != nil { + return err + } + + return nil +} diff --git a/sysdig/resource_sysdig_team_service_account_test.go b/sysdig/resource_sysdig_team_service_account_test.go new file mode 100644 index 00000000..13b1fff6 --- /dev/null +++ b/sysdig/resource_sysdig_team_service_account_test.go @@ -0,0 +1,97 @@ +//go:build tf_acc_sysdig_monitor || tf_acc_sysdig_secure + +package sysdig_test + +import ( + "fmt" + "github.com/draios/terraform-provider-sysdig/sysdig" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "testing" +) + +func TestAccTeamServiceAccount(t *testing.T) { + monitorsvc := randomText(10) + securesvc := randomText(10) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: preCheckAnyEnv(t, SysdigMonitorApiTokenEnv, SysdigSecureApiTokenEnv), + ProviderFactories: map[string]func() (*schema.Provider, error){ + "sysdig": func() (*schema.Provider, error) { + return sysdig.Provider(), nil + }, + }, + ExternalProviders: map[string]resource.ExternalProvider{ + "time": {Source: "hashicorp/time"}, + }, + Steps: []resource.TestStep{ + { + Config: teamServiceAccountMonitorTeam(monitorsvc), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("sysdig_team_service_account.service-account-monitor", + "name", + monitorsvc, + ), + resource.TestCheckResourceAttr("sysdig_team_service_account.service-account-monitor", + "role", + "ROLE_TEAM_READ", + ), + ), + }, + { + Config: teamServiceAccountSecureTeam(securesvc), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("sysdig_team_service_account.service-account-secure", + "name", + securesvc, + ), + resource.TestCheckResourceAttr("sysdig_team_service_account.service-account-secure", + "role", + "ROLE_TEAM_READ", + ), + ), + }, + }, + }) +} + +func teamServiceAccountMonitorTeam(name string) string { + return fmt.Sprintf(` +resource "time_static" "example" { + rfc3339 = "2099-01-01T00:00:00Z" +} + +resource "sysdig_monitor_team" "sample" { + name = "monitor-sample-%s" + + entrypoint { + type = "Explore" + } +} + +resource "sysdig_team_service_account" "service-account-monitor" { + name = "%s" + expiration_date = time_static.example.unix + team_id = sysdig_monitor_team.sample.id +} +`, name, name) +} + +func teamServiceAccountSecureTeam(name string) string { + return fmt.Sprintf(` +resource "time_static" "example" { + rfc3339 = "2099-01-01T00:00:00Z" +} + +resource "sysdig_secure_team" "sample" { + name = "secure-sample-%s" + all_zones = "true" +} + +resource "sysdig_team_service_account" "service-account-secure" { + name = "%s" + expiration_date = time_static.example.unix + team_id = sysdig_secure_team.sample.id +} +`, name, name) +} diff --git a/website/docs/r/team_service_account.md b/website/docs/r/team_service_account.md new file mode 100644 index 00000000..bed4c835 --- /dev/null +++ b/website/docs/r/team_service_account.md @@ -0,0 +1,67 @@ +--- +subcategory: "Sysdig Platform" +layout: "sysdig" +page_title: "Sysdig: sysdig_team_service_account" +description: |- + Creates a team service account in Sysdig. +--- + +# Resource: sysdig_team_service_account + +Creates a team service account in Sysdig. + +-> **Note:** Sysdig Terraform Provider is under rapid development at this point. If you experience any issue or discrepancy while using it, please make sure you have the latest version. If the issue persists, or you have a Feature Request to support an additional set of resources, please open a [new issue](https://github.com/sysdiglabs/terraform-provider-sysdig/issues/new) in the GitHub repository. + +## Example Usage + +```terraform +resource "time_static" "example" { + rfc3339 = "2025-01-01T00:00:00Z" +} + +resource "sysdig_monitor_team" "devops" { + name = "Monitoring DevOps team" + + entrypoint { + type = "Explore" + } +} + +resource "sysdig_team_service_account" "service-account" { + name = "read only" + role = "ROLE_TEAM_READ" + expiration_date = time_static.example.unix + team_id = sysdig_monitor_teamt.sample.id +} + +``` + +## Argument Reference + +* `name` - (Required) The team service account name. + +* `role` - (Required) The role that is assigned to the service account. It can be a standard role or a custom team role ID. + +* `expiration_date` (Required) The service account expiration date. + +* `team_id` - (Required) The team where the service account belongs to. + +* `system_role`- The service account system role. The only value supported is `ROLE_SERVICE_ACCOUNT` + + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `date_created` - The team service account creation date + +* `api_key` - The api key to be using in API calls + +## Import + +Sysdig team service account can be imported using the ID, e.g. + +``` +$ terraform import sysdig_team_service_account.my_team_service_account 10 +``` +