diff --git a/client/replication.go b/client/replication.go old mode 100644 new mode 100755 index 3ce3466..6ce6f5e --- a/client/replication.go +++ b/client/replication.go @@ -16,7 +16,7 @@ func GetReplicationBody(d *schema.ResourceData) models.ReplicationBody { Name: d.Get("name").(string), Override: d.Get("override").(bool), Enabled: d.Get("enabled").(bool), - Deletion: d.Get("deletion").(bool), + Deletion: d.Get("deletion").(bool), DestNamespace: d.Get("dest_namespace").(string), } diff --git a/client/robot_account.go b/client/robot_account.go old mode 100644 new mode 100755 index f4e9e90..36870b6 --- a/client/robot_account.go +++ b/client/robot_account.go @@ -1,37 +1,37 @@ package client import ( - "strings" - "github.com/BESTSELLER/terraform-provider-harbor/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -func RobotBody(d *schema.ResourceData, projectid string) models.RobotBody { - resource := strings.Replace(projectid, "s", "", +1) - +func RobotBody(d *schema.ResourceData) models.RobotBody { body := models.RobotBody{ Name: d.Get("name").(string), Description: d.Get("description").(string), + Disable: d.Get("disable").(bool), + Duration: d.Get("duration").(int), + Level: d.Get("level").(string), } - robotAccess := models.RobotBodyAccess{} + permissions := d.Get("permissions").(*schema.Set).List() + for _, p := range permissions { - access := d.Get("actions").([]interface{}) - for _, v := range access { + permission := models.RobotBodyPermission{ + Kind: p.(map[string]interface{})["kind"].(string), + Namespace: p.(map[string]interface{})["namespace"].(string), + } - switch v.(string) { - case "push", "pull": - robotAccess.Action = v.(string) - robotAccess.Resource = resource + "/repository" - case "read": - robotAccess.Action = v.(string) - robotAccess.Resource = resource + "/helm-chart" - case "create": - robotAccess.Action = v.(string) - robotAccess.Resource = resource + "/helm-chart-version" + for _, a := range p.(map[string]interface{})["access"].(*schema.Set).List() { + access := models.RobotBodyAccess{ + Action: a.(map[string]interface{})["action"].(string), + Resource: a.(map[string]interface{})["resource"].(string), + Effect: a.(map[string]interface{})["effect"].(string), + } + permission.Access = append(permission.Access, access) } - body.Access = append(body.Access, robotAccess) + + body.Permissions = append(body.Permissions, permission) } return body diff --git a/docs/resources/robot_account.md b/docs/resources/robot_account.md index 191ec04..faa03b7 100644 --- a/docs/resources/robot_account.md +++ b/docs/resources/robot_account.md @@ -1,39 +1,148 @@ # Resource: harbor_robot_account +Harbor supports different levels of robot accounts. Currently `system` and `project` level robot accounts are supported. + ## Example Usage + +### System Level +Introduced in harbor 2.2.0, system level robot accounts can have basically [all available permissions](https://github.com/goharbor/harbor/blob/master/src/common/rbac/const.go) in harbor and are not dependent on a single project. + ```hcl resource "harbor_project" "main" { name = "main" } -resource "harbor_robot_account" "account" { - name = "${harbor_project.main.name}" - description = "Robot account used to push images to harbor" - project_id = harbor_project.main.id - actions = ["push"] +resource "harbor_robot_account" "system" { + name = "example-system" + description = "system level robot account" + level = "system" + permissions { + access { + action = "create" + resource = "labels" + } + kind = "system" + namespace = "/" + } + permissions { + access { + action = "push" + resource = "repository" + } + access { + action = "read" + resource = "helm-chart" + } + access { + action = "read" + resource = "helm-chart-version" + } + kind = "project" + namespace = harbor_project.main.name + } + permissions { + access { + action = "pull" + resource = "repository" + } + kind = "project" + namespace = "*" + } } ``` +The above example, creates a system level robot account with permissions to +- permission to create labels on system level +- pull repository across all projects +- push repository to project "my-project-name" +- read helm-chart and helm-chart-version in project "my-project-name" + +### Project Level + +Other than system level robot accounts, project level robot accounts can interact on project level only. +The [available permissions](https://github.com/goharbor/harbor/blob/master/src/common/rbac/const.go) are mostly the same as for system level robots. + + +```hcl +resource "harbor_project" "main" { + name = "main" +} + +resource "harbor_robot_account" "project" { + name = "example-project" + description = "project level robot account" + level = "project" + permissions { + access { + action = "pull" + resource = "repository" + } + access { + action = "push" + resource = "repository" + } + kind = "project" + namespace = harbor_project.main.name + } +} +``` + +The above example creates a project level robot account with permissions to +- pull repository on project "main" +- push repository on project "main" + + ## Argument Reference The following arguments are supported: -* **name** - (Required) The of the project that will be created in harbor. +* **name** - (string, required) The of the project that will be created in harbor. + +* **level** - (string, required) Level of the robot account, currently either `system` or `project`. + +* **description** - (string, optional) The description of the robot account will be displayed in harbor. + +* **duration** - (int, optional) By default, the robot account will not expire. Set it to the amount of days until the account should expire. + +* **disable** - (bool, optional) Disables the robot account when set to `true`. + +* **permissions** - (block, required) [Permissions](#permissions-arguments) to be applied to the robot account. + ``` + permissions { + access { + action = "action" + resource = "resource" + effect = "effect" + } + access { + ... + } + kind = "project" + namespace = harbor_project.main.name + } + permissions { + ... + } + ``` + **Note, that for `project` level accounts, only one `permission` block is allowed!** + +### Permissions Arguments +* **access** - (block, required) Define one or multiple [access blocks](#access-arguments). + +* **kind** - (string, required) Either `system` or `project`. + +* **namespace** - (string, required) namespace is the name of your project. + For kind `system` permissions, always use `/` as namespace. + Use `*` to match all projects. -* **description** - (Optional) The description of the robot account will be displayed in harbor. +### Access Arguments +* **action** - (string, required) Eg. `push`, `pull`, `read`, etc. Check [available actions](https://github.com/goharbor/harbor/blob/master/src/common/rbac/const.go). -* **project_id** - (Required) The project id of the project that the robot account will be associated with. +* **resource** - (string, required) Eg. `repository`, `helm-chart`, `labels`, etc. Check [available resources](https://github.com/goharbor/harbor/blob/master/src/common/rbac/const.go). -* **actions** - (Optional) A list of actions that the robot account will be able to perform on the project.  - You to have set `["pull"]` as minimal requirement, if `["push"]` is set you don't need to set pull. Other combinations can be `["push","create","read"]` or `["push","read"]` or `["pull","read"]` - ``` - pull = permission to pull from docker registry - push = permission to push to docker registry - create = permission to created helm charts - read = permission to read helm charts - ``` +* **effect** - (string, optional) Either `allow` or `deny`. Defaults to `allow`. ## Attributes Reference In addition to all argument, the following attributes are exported: -* **token** - The token of the robot account. \ No newline at end of file +* **secret** - The secret of the robot account used for authentication \ No newline at end of file diff --git a/models/robot_account.go b/models/robot_account.go old mode 100644 new mode 100755 index a7ca2b3..a04b731 --- a/models/robot_account.go +++ b/models/robot_account.go @@ -1,21 +1,29 @@ package models -type RobotBody struct { - Access []RobotBodyAccess `json:"access,omitempty"` - Name string `json:"name,omitempty"` - ExpiresAt int `json:"expires_at,omitempty"` - Description string `json:"description,omitempty"` +type RobotBodyPermission struct { + Access []RobotBodyAccess `json:"access,omitempty"` + Kind string `json:"kind,omitempty"` + Namespace string `json:"namespace,omitempty"` } type RobotBodyAccess struct { Action string `json:"action,omitempty"` Resource string `json:"resource,omitempty"` + Effect string `json:"effect,omitempty"` +} +type RobotBody struct { + ID int `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Level string `json:"level,omitempty"` + Description string `json:"description,omitempty"` + Secret string `json:"secret,omitempty"` + Duration int `json:"duration,omitempty"` + Disable bool `json:"disable,omitempty"` + Permissions []RobotBodyPermission `json:"permissions,omitempty"` } -type RobotBodyRepones struct { - ID int `json:"id"` - Name string `json:"name"` - Token string `json:"token"` - Description string `json:"description"` - ProjectID int `json:"project_id"` - ExpiresAt int `json:"expires_at"` - Disabled bool `json:"disabled"` +type RobotBodyResponse struct { + ID int `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Secret string `json:"secret,omitempty"` + ExpiresAt int `json:"expires_at,omitempty"` + CreationTime string `json:"creation_time,omitempty"` } diff --git a/provider/resource_robot_account.go b/provider/resource_robot_account.go old mode 100644 new mode 100755 index 345594e..c4ae80a --- a/provider/resource_robot_account.go +++ b/provider/resource_robot_account.go @@ -14,12 +14,16 @@ import ( func resourceRobotAccount() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ + "robot_id": { + Type: schema.TypeString, + Computed: true, + }, "name": { Type: schema.TypeString, Required: true, ForceNew: true, }, - "project_id": { + "level": { Type: schema.TypeString, Required: true, ForceNew: true, @@ -29,23 +33,58 @@ func resourceRobotAccount() *schema.Resource { Optional: true, Default: nil, }, - "actions": { - Type: schema.TypeList, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, + "disable": { + Type: schema.TypeBool, Optional: true, - // Default: ["pull"], - ForceNew: true, + Default: false, + }, + "duration": { + Type: schema.TypeInt, + Optional: true, + Default: -1, }, - "token": { + "secret": { Type: schema.TypeString, Computed: true, Sensitive: true, }, - "robot_id": { - Type: schema.TypeString, - Computed: true, + "permissions": { + Type: schema.TypeSet, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "access": { + Type: schema.TypeSet, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "action": { + Type: schema.TypeString, + Required: true, + }, + "resource": { + Type: schema.TypeString, + Required: true, + }, + "effect": { + Type: schema.TypeString, + Optional: true, + Default: "allow", + }, + }, + }, + Required: true, + }, + "kind": { + Type: schema.TypeString, + Required: true, + }, + "namespace": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + Required: true, + ForceNew: true, }, }, Create: resourceRobotAccountCreate, @@ -70,17 +109,14 @@ func checkProjectid(id string) (projecid string) { func resourceRobotAccountCreate(d *schema.ResourceData, m interface{}) error { apiClient := m.(*client.Client) - projectid := d.Get("project_id").(string) - url := projectid + "/robots" - - body := client.RobotBody(d, projectid) + body := client.RobotBody(d) - resp, headers, err := apiClient.SendRequest("POST", url, body, 201) + resp, headers, err := apiClient.SendRequest("POST", "/robots", body, 201) if err != nil { return err } - var jsonData models.RobotBodyRepones + var jsonData models.RobotBodyResponse err = json.Unmarshal([]byte(resp), &jsonData) if err != nil { return err @@ -92,31 +128,43 @@ func resourceRobotAccountCreate(d *schema.ResourceData, m interface{}) error { } d.SetId(id) - d.Set("token", jsonData.Token) + d.Set("secret", jsonData.Secret) return resourceRobotAccountRead(d, m) } func resourceRobotAccountRead(d *schema.ResourceData, m interface{}) error { apiClient := m.(*client.Client) - resp, _, err := apiClient.SendRequest("GET", d.Id(), nil, 200) + robot, err := getRobot(d, apiClient) if err != nil { return err } - var jsonData models.RobotBodyRepones - err = json.Unmarshal([]byte(resp), &jsonData) - if err != nil { - return fmt.Errorf("Resource not found %s", d.Id()) - } - - d.Set("robot_id", strconv.Itoa(jsonData.ID)) - d.Set("description", jsonData.Description) + d.Set("robot_id", strconv.Itoa(robot.ID)) return nil } func resourceRobotAccountUpdate(d *schema.ResourceData, m interface{}) error { + apiClient := m.(*client.Client) + + body := client.RobotBody(d) + + // if name not changed, use robot account name from api, otherwise it would always trigger a recreation, + // since harbor does internally attach the robot account prefix to it´s names + if false == d.HasChange("name") { + robot, err := getRobot(d, apiClient) + if err != nil { + return err + } + body.Name = robot.Name + } + + _, _, err := apiClient.SendRequest("PUT", d.Id(), body, 200) + if err != nil { + return err + } + return resourceRobotAccountRead(d, m) } @@ -128,3 +176,16 @@ func resourceRobotAccountDelete(d *schema.ResourceData, m interface{}) error { } return nil } + +func getRobot(d *schema.ResourceData, apiClient *client.Client) (models.RobotBody, error) { + resp, _, err := apiClient.SendRequest("GET", d.Id(), nil, 200) + if err != nil { + return models.RobotBody{}, err + } + var jsonData models.RobotBody + err = json.Unmarshal([]byte(resp), &jsonData) + if err != nil { + return models.RobotBody{}, fmt.Errorf("Resource not found %s", d.Id()) + } + return jsonData, nil +} diff --git a/provider/resource_robot_account_test.go b/provider/resource_robot_account_test.go index b023b35..a8b9183 100644 --- a/provider/resource_robot_account_test.go +++ b/provider/resource_robot_account_test.go @@ -11,40 +11,40 @@ import ( const harborRobotAccount = "harbor_robot_account.main" -func TestAccRobotBasic(t *testing.T) { +func TestAccRobotSystem(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckRobotDestroy, Steps: []resource.TestStep{ { - Config: testAccCheckRobotBasic(), + Config: testAccCheckRobotSystem(), Check: resource.ComposeTestCheckFunc( testAccCheckResourceExists("harbor_project.main"), testAccCheckResourceExists(harborRobotAccount), resource.TestCheckResourceAttr( - harborRobotAccount, "name", "test_robot_account"), + harborRobotAccount, "name", "test_robot_system"), ), }, }, }) } -func TestAccRobotMultipleAction(t *testing.T) { +func TestAccRobotProject(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckRobotDestroy, Steps: []resource.TestStep{ { - Config: testAccCheckRobotMultipleAction(), + Config: testAccCheckRobotProject(), Check: resource.ComposeTestCheckFunc( testAccCheckResourceExists("harbor_project.main"), testAccCheckResourceExists(harborRobotAccount), resource.TestCheckResourceAttr( - harborRobotAccount, "name", "test_robot_account"), + harborRobotAccount, "name", "test_robot_project"), ), }, }, @@ -72,37 +72,74 @@ func testAccCheckRobotDestroy(s *terraform.State) error { return nil } -func testAccCheckRobotBasic() string { +func testAccCheckRobotSystem() string { return fmt.Sprintf(` - resource "harbor_robot_account" "main" { - name = "test_robot_account" - description = "Robot account to be used to pull images" - project_id = harbor_project.main.id - actions = ["pull"] - + name = "test_robot_system" + description = "system level robot account" + level = "system" + permissions { + access { + action = "create" + resource = "labels" + } + kind = "system" + namespace = "/" } - - resource "harbor_project" "main" { - name = "test_basic" + permissions { + access { + action = "push" + resource = "repository" + } + access { + action = "read" + resource = "helm-chart" + } + access { + action = "read" + resource = "helm-chart-version" + } + kind = "project" + namespace = harbor_project.main.name + } + permissions { + access { + action = "pull" + resource = "repository" + } + kind = "project" + namespace = "*" } - + } + + resource "harbor_project" "main" { + name = "test_basic" + } `) } -func testAccCheckRobotMultipleAction() string { +func testAccCheckRobotProject() string { return fmt.Sprintf(` - resource "harbor_robot_account" "main" { - name = "test_robot_account" - description = "Robot account to be used to push images" - project_id = harbor_project.main.id - actions = ["push","read","create"] + name = "test_robot_project" + description = "project level robot account" + level = "project" + permissions { + access { + action = "pull" + resource = "repository" + } + access { + action = "push" + resource = "repository" + } + kind = "project" + namespace = harbor_project.main.name } + } - resource "harbor_project" "main" { - name = "test_basic" - } - + resource "harbor_project" "main" { + name = "test_basic" + } `) }