diff --git a/client/immutable_tag_rule.go b/client/immutable_tag_rule.go new file mode 100644 index 0000000..05511bf --- /dev/null +++ b/client/immutable_tag_rule.go @@ -0,0 +1,68 @@ +package client + +import ( + "log" + "strconv" + "strings" + + "github.com/BESTSELLER/terraform-provider-harbor/models" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func GetImmutableTagRuleBody(d *schema.ResourceData) *models.ImmutableTagRule { + tagId := 0 + if d.Id() != "" { + lastSlashIndex := strings.LastIndex(d.Id(), "/") + tagId, _ = strconv.Atoi(d.Id()[lastSlashIndex+1:]) + } + + tags := []models.ImmutableTagRuleTagSelectors{} + tag := models.ImmutableTagRuleTagSelectors{ + Kind: "doublestar", + } + + if d.Get("tag_matching").(string) != "" { + tag.Decoration = "matches" + tag.Pattern = d.Get("tag_matching").(string) + d.Set("tag_excluding", nil) + } + if d.Get("tag_excluding").(string) != "" { + tag.Decoration = "excludes" + tag.Pattern = d.Get("tag_excluding").(string) + d.Set("tag_matching", nil) + } + tags = append(tags, tag) + + scopeSelectorsRepository := models.ScopeSelectors{ + Repository: []models.Repository{}, + } + + repo := models.Repository{ + Kind: "doublestar", + } + + if d.Get("repo_matching").(string) != "" { + repo.Decoration = "repoMatches" + repo.Pattern = d.Get("repo_matching").(string) + d.Set("repo_excluding", nil) + } + if d.Get("repo_excluding").(string) != "" { + repo.Decoration = "repoExcludes" + repo.Pattern = d.Get("repo_excluding").(string) + d.Set("repo_matching", nil) + } + scopeSelectorsRepository.Repository = append(scopeSelectorsRepository.Repository, repo) + + body := models.ImmutableTagRule{ + Id: tagId, + Disabled: d.Get("disabled").(bool), + ScopeSelectors: scopeSelectorsRepository, + ImmutableTagRuleTagSelectors: tags, + Action: "immutable", + Template: "immutable_template", + } + + log.Printf("[DEBUG] %+v\n ", body) + + return &body +} diff --git a/docs/index.md b/docs/index.md index 9185013..29c5aa6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,6 +6,7 @@ The Harbor provider is used to configure an instance of Harbor. The provider nee * [Resource: harbor_config_system](resources/config_system.md) * [Resource: harbor_config_email](resources/config_email.md) * [Resource: harbor_garbage_collection](resources/garbage_collection.md) +* [Resource: harbor_immutable_tag_rule](resources/immutable_tag_rule.md) * [Resource: harbor_interrogation_services](resources/interrogation_services.md) * [Resource: harbor_label](resources/label.md) * [Resource: harbor_project_member_group](resources/project_member_group.md) @@ -29,7 +30,7 @@ provider "harbor" { ## Argument Reference The following arguments are supported: -* **url** - (Required) The url of the harbor +* **url** - (Required) The url of the harbor * **username** - (Required) The username to be used to access harbor * **password** - (Required) The password to be used to access harbor * **insecure** - (Optional) Choose to igorne certificate errors diff --git a/docs/resources/immutable_tag_rule.md b/docs/resources/immutable_tag_rule.md new file mode 100644 index 0000000..c48a825 --- /dev/null +++ b/docs/resources/immutable_tag_rule.md @@ -0,0 +1,32 @@ +# Resource: harbor_immutable_tag_rule + +## Example Usage + +```hcl +resource "harbor_project" "main" { + name = "acctest" +} + +resource "harbor_immutable_tag_rule" "main" { + project_id = harbor_project.main.id + repo_matching = "**" + tag_excluding = "latest" +} +``` + +## Argument Reference +The following arguments are supported: + +* `disabled`- (Optional) Specify if the rule is disable or not. Defaults to `false` +* `repo_matching`- (Optional) For the repositories matching. +* `repo_excluding` - (Optional) For the repositories excuding. +* `tag_matching`- (Optional) For the tag matching. +* `tag_excluding` - (Optional) For the tag excuding. +* `scope` - (Required) The project id of which you would like to apply this policy. + +## Import +Harbor immutable tag rule can be imported using the `project and immutabletagrule ids` eg, + +` +terraform import harbor_immutable_tag_rule.main /projects/4/immutabletagrules/25 +` diff --git a/models/immutable_tag_rule.go b/models/immutable_tag_rule.go new file mode 100644 index 0000000..b742ba1 --- /dev/null +++ b/models/immutable_tag_rule.go @@ -0,0 +1,18 @@ +package models + +var PathImmutableTagRules = "/immutabletagrules" + +type ImmutableTagRule struct { + Id int `json:"id,omitempty"` + Disabled bool `json:"disabled"` + ScopeSelectors ScopeSelectors `json:"scope_selectors"` + ImmutableTagRuleTagSelectors []ImmutableTagRuleTagSelectors `json:"tag_selectors"` + Action string `json:"action"` + Template string `json:"template"` +} + +type ImmutableTagRuleTagSelectors struct { + Kind string `json:"kind"` + Decoration string `json:"decoration"` + Pattern string `json:"pattern"` +} diff --git a/provider/provider.go b/provider/provider.go index 6e5a447..1fbd421 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -55,6 +55,7 @@ func Provider() *schema.Provider { "harbor_retention_policy": resourceRetention(), "harbor_garbage_collection": resourceGC(), "harbor_label": resourceLabel(), + "harbor_immutable_tag_rule": resourceImmutableTagRule(), }, DataSourcesMap: map[string]*schema.Resource{ "harbor_project": dataProject(), diff --git a/provider/resource_immutable_tag_rule.go b/provider/resource_immutable_tag_rule.go new file mode 100644 index 0000000..8b36645 --- /dev/null +++ b/provider/resource_immutable_tag_rule.go @@ -0,0 +1,147 @@ +package provider + +import ( + "encoding/json" + "fmt" + "log" + "strconv" + "strings" + + "github.com/BESTSELLER/terraform-provider-harbor/client" + "github.com/BESTSELLER/terraform-provider-harbor/models" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceImmutableTagRule() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "disabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "repo_matching": { + Type: schema.TypeString, + Optional: true, + // ConflictsWith: []string{".repo_excluding"}, + }, + "repo_excluding": { + Type: schema.TypeString, + Optional: true, + // ConflictsWith: []string{".repo_matching"}, + }, + "tag_matching": { + Type: schema.TypeString, + Optional: true, + // ConflictsWith: []string{".tag_excluding"}, + }, + "tag_excluding": { + Type: schema.TypeString, + Optional: true, + // ConflictsWith: []string{".tag_matching"}, + }, + }, + Create: resourceImmutableTagRuleCreate, + Read: resourceImmutableTagRuleRead, + Update: resourceImmutableTagRuleUpdate, + Delete: resourceImmutableTagRuleDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + } +} + +func resourceImmutableTagRuleCreate(d *schema.ResourceData, m interface{}) error { + apiClient := m.(*client.Client) + + projectid := checkProjectid(d.Get("project_id").(string)) + path := projectid + models.PathImmutableTagRules + + body := client.GetImmutableTagRuleBody(d) + id := "" + + _, headers, err := apiClient.SendRequest("POST", path, body, 201) + if err != nil { + return err + } + + id, err = client.GetID(headers) + d.SetId(id) + return resourceImmutableTagRuleRead(d, m) +} + +func resourceImmutableTagRuleRead(d *schema.ResourceData, m interface{}) error { + apiClient := m.(*client.Client) + + lastSlashIndex := strings.LastIndex(d.Id(), "/") + projectImmutableTagRulePath := d.Id()[0:lastSlashIndex] + tagId, err := strconv.Atoi(d.Id()[lastSlashIndex+1:]) + if err != nil { + return err + } + log.Printf("[DEBUG] Path to immutable tag rules: %+v\n", projectImmutableTagRulePath) + + resp, _, err := apiClient.SendRequest("GET", projectImmutableTagRulePath, nil, 200) + if err != nil { + return fmt.Errorf("Resource not found %s", projectImmutableTagRulePath) + } + + var immutableTagRuleModels []models.ImmutableTagRule + err = json.Unmarshal([]byte(resp), &immutableTagRuleModels) + if err != nil { + return err + } + for _, rule := range immutableTagRuleModels { + if rule.Id == tagId { + log.Printf("[DEBUG] found tag id %d", tagId) + d.Set("disabled", rule.Disabled) + d.Set("project_id", strings.ReplaceAll(projectImmutableTagRulePath, models.PathImmutableTagRules, "")) + + switch rule.ImmutableTagRuleTagSelectors[0].Decoration { + case "matches": + d.Set("tag_matching", rule.ImmutableTagRuleTagSelectors[0].Pattern) + case "excludes": + d.Set("tag_excluding", rule.ImmutableTagRuleTagSelectors[0].Pattern) + } + + switch rule.ScopeSelectors.Repository[0].Decoration { + case "repoMatches": + d.Set("repo_matching", rule.ScopeSelectors.Repository[0].Pattern) + case "excludes": + d.Set("repo_excluding", rule.ScopeSelectors.Repository[0].Pattern) + } + + return nil + } + } + + return fmt.Errorf("Resource not found %s", d.Id()) +} + +func resourceImmutableTagRuleUpdate(d *schema.ResourceData, m interface{}) error { + apiClient := m.(*client.Client) + body := client.GetImmutableTagRuleBody(d) + id := d.Id() + log.Printf("[DEBUG] Update Id: %+v\n", id) + _, _, err := apiClient.SendRequest("PUT", id, body, 200) + if err != nil { + return err + } + + return resourceImmutableTagRuleRead(d, m) +} + +func resourceImmutableTagRuleDelete(d *schema.ResourceData, m interface{}) error { + apiClient := m.(*client.Client) + + _, _, err := apiClient.SendRequest("DELETE", d.Id(), nil, 200) + if err != nil { + return err + } + return nil +}