diff --git a/docs/guides/knowingProblems.md b/docs/guides/knowingProblems.md new file mode 100644 index 0000000..e14debb --- /dev/null +++ b/docs/guides/knowingProblems.md @@ -0,0 +1,32 @@ +# Swagger MissMatches + +Page for Colleting Swagger ApiClient Issues. + +## `#/definitions/ReplicationPolicy` + +```json +"filters": [ + { + "type": "name", + "value": "test" + }, + ... + { + "type": "label", + "value": [ + "testlabel-acc-classic" + ] + } + ... + ] +... +``` + +```swagger +"filters": [ + { + "type": "string", + "value": "string" + } + ], +```` diff --git a/docs/resources/harbor_registry.md b/docs/resources/harbor_registry.md index f412e7e..216b60a 100644 --- a/docs/resources/harbor_registry.md +++ b/docs/resources/harbor_registry.md @@ -19,7 +19,7 @@ resource "harbor_registry" "helmhub" { The following arguments are supported: -* `name` - (Required) The of the project that will be created in harbor. +* `name` - (Required) of the project that will be created in harbor. * `url` - (Required) The registry remote endpoint, like `https://hub.docker.com`. diff --git a/docs/resources/harbor_replication.md b/docs/resources/harbor_replication.md new file mode 100644 index 0000000..7ea9972 --- /dev/null +++ b/docs/resources/harbor_replication.md @@ -0,0 +1,80 @@ +# Resource: harbor_replication + +Harbor Doc: [configuring-replication](https://goharbor.io/docs/2.0.0/administration/configuring-replication/) + + +## Example Usage + +```hcl +data "harbor_project" "project_replica" { + name = "main" +} + +data "harbor_registry" "registry_helm_hub" { + name = "helmhub" +} + +data "harbor_registry" "registry_docker_hub" { + name = "dockerhub" +} + +resource "harbor_replication" "pull_based_helm" { + name = "acc-helm-prometheus-operator-test" + description = "Prometheus Operator Replica" + source_registry_id = data.harbor_registry.registry_replica_helm_hub.id + source_registry_filter_name = "stable/prometheus-operator" + source_registry_filter_tag = "**" + destination_namespace = data.harbor_project.project_replica.name +} + +resource "harbor_replication_pull" "push_based_docker" { + name = "docker-push" + description = "Push Docker" + destination_registry_id = data.harbor_registry.registry_docker_hub.id + destination_namespace = "notexisting" + source_registry_filter_name = "${data.harbor_project.project_replica.name}/vscode-devcontainers/k8s-operator" + source_registry_filter_tag = "**" +} + +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) of the replication that will be created in harbor. + +* `description` - (Optional) of replication that will be displayed in harbor. + +* `source_registry_id` - (Optional) Used for pull the resources from the remote registry to the local Harbor. + +* `source_registry_filter_name` - (Optional) Filter the name of the resource. Leave empty or use '\*\*' to match all. 'library/\*\*' only matches resources under 'library'. + +* `source_registry_filter_tag` - (Optional) Filter the tag/version part of the resources. Leave empty or use '\*\*' to match all. '1.0*' only matches the tags that starts with '1.0'. + +* `destination_namespace` - (Optional) Destination namespace Specify the destination namespace. If empty, the resources will be put under the same namespace as the source. + +* `destination_registry_id` - (Optional) The target Registry ID, used only for `push-based` replications. + +* `trigger_mode` - (Optional) Can be `manual`,`scheduled` and for push-based addition `event_based`, Default: `manual` + +* `trigger_cron` - (Optional) Used cron for `scheduled` trigger mode, like `* * 5 * * *` + +* `override` - (Optional) Specify whether to override the resources at the destination if a resource with the same name exists. Default: `false` + +* `enabled` - (Optional) + + +## Attributes Reference + +In addition to all argument, the following attributes are exported: + +* `id` - The id of the registry policy with harbor. + +## Import + +Harbor Projects can be imported using the `harbor_replication`, e.g. + +```sh +terraform import harbor_replication.helmhub_prometheus 1 +``` diff --git a/harbor/provider.go b/harbor/provider.go index cd097cb..cab3686 100644 --- a/harbor/provider.go +++ b/harbor/provider.go @@ -54,6 +54,7 @@ func Provider() terraform.ResourceProvider { "harbor_robot_account": resourceRobotAccount(), "harbor_tasks": resourceTasks(), "harbor_label": resourceLabel(), + "harbor_replication": resourceReplication(), }, DataSourcesMap: map[string]*schema.Resource{ "harbor_project": dataSourceProject(), diff --git a/harbor/resource_replication.go b/harbor/resource_replication.go new file mode 100644 index 0000000..73d79aa --- /dev/null +++ b/harbor/resource_replication.go @@ -0,0 +1,275 @@ +package harbor + +import ( + "fmt" + "strconv" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/nolte/terraform-provider-harbor/gen/harborctl/client" + "github.com/nolte/terraform-provider-harbor/gen/harborctl/client/products" + "github.com/nolte/terraform-provider-harbor/gen/harborctl/models" +) + +//nolint:funlen +func resourceReplication() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name from the Replication Policy", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: "Will be displayed in harbor", + }, + "source_registry_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: `Pull the resources from the remote registry to the local Harbor.`, + }, + "source_registry_filter_name": { + Type: schema.TypeString, + Optional: true, + Description: ` + Filter the name of the resource. + Leave empty or use '**' to match all. 'library/**' only matches resources under 'library'. + For more patterns, please refer to the user guide. + `, + }, + "source_registry_filter_tag": { + Type: schema.TypeString, + Optional: true, + Description: ` + Filter the tag/version part of the resources. + Leave empty or use '**' to match all. '1.0*' only matches the tags that starts with '1.0'. + For more patterns, please refer to the user guide. + `, + }, + // not supported for the moment swagger client problems + // "source_registry_filter_labels": { + // Type: schema.TypeList, + // Optional: true, + // Elem: &schema.Schema{Type: schema.TypeString}, + // }, + "destination_namespace": { + Type: schema.TypeString, + Optional: true, + Description: `Destination namespace Specify the destination namespace. + If empty, the resources will be put under the same namespace as the source.`, + }, + "destination_registry_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: "The Id from the destination registry used for push-based replication policies", + }, + "trigger_mode": { + Type: schema.TypeString, + Optional: true, + Default: "manual", + Description: "Can be manual,scheduled and for push-based addition event_based", + }, + "trigger_cron": { + Type: schema.TypeString, + Optional: true, + Default: "", + }, + "override": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: `Specify whether to override the resources at the destination + if a resource with the same name exists.`, + }, + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + }, + Create: resourceReplicationCreate, + Read: resourceReplicationRead, + Update: resourceReplicationUpdate, + Delete: resourceReplicationDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + } +} + +func buildReplicationPolicy(d *schema.ResourceData) *models.ReplicationPolicy { + filters := make([]*models.ReplicationFilter, 2) + filters[0] = &models.ReplicationFilter{ + Type: "name", + Value: d.Get("source_registry_filter_name").(string), + } + filters[1] = &models.ReplicationFilter{ + Type: "tag", + Value: d.Get("source_registry_filter_tag").(string), + } + + return &models.ReplicationPolicy{ + Name: d.Get("name").(string), + Description: d.Get("description").(string), + DestNamespace: d.Get("destination_namespace").(string), + DestRegistry: &models.Registry{ + ID: int64(d.Get("destination_registry_id").(int)), + }, + SrcRegistry: &models.Registry{ + ID: int64(d.Get("source_registry_id").(int)), + }, + Filters: filters, + Enabled: d.Get("enabled").(bool), + Override: d.Get("override").(bool), + Trigger: &models.ReplicationTrigger{ + Type: d.Get("trigger_mode").(string), + TriggerSettings: &models.TriggerSettings{ + Cron: d.Get("trigger_cron").(string), + }, + }, + } +} + +func resourceReplicationCreate(d *schema.ResourceData, m interface{}) error { + apiClient := m.(*client.Harbor) + + params := products.NewPostReplicationPoliciesParams().WithPolicy(buildReplicationPolicy(d)) + + _, err := apiClient.Products.PostReplicationPolicies(params, nil) + if err != nil { + return err + } + + registry, err := findReplicationByName(d, m) + + if err != nil { + return err + } + + d.SetId(strconv.Itoa(int(registry.ID))) + + return resourceReplicationRead(d, m) +} + +func findReplicationByName(d *schema.ResourceData, m interface{}) (*models.ReplicationPolicy, error) { + apiClient := m.(*client.Harbor) + + if name, ok := d.GetOk("name"); ok { + registryName := name.(string) + query := products.NewGetReplicationPoliciesParams().WithName(®istryName) + + resp, err := apiClient.Products.GetReplicationPolicies(query, nil) + if err != nil { + d.SetId("") + return &models.ReplicationPolicy{}, err + } + + if len(resp.Payload) < 1 { + return &models.ReplicationPolicy{}, fmt.Errorf("no Replication found with name %v", registryName) + } else if resp.Payload[0].Name != registryName { + return &models.ReplicationPolicy{}, + fmt.Errorf("response Name %v not match with Expected Name %v", resp.Payload[0].Name, registryName) + } + + return resp.Payload[0], nil + } + + return &models.ReplicationPolicy{}, fmt.Errorf("fail to lookup Replication by Name") +} + +func resourceReplicationRead(d *schema.ResourceData, m interface{}) error { + apiClient := m.(*client.Harbor) + + if registryID, err := strconv.ParseInt(d.Id(), 10, 64); err == nil { + params := products.NewGetReplicationPoliciesIDParams().WithID(registryID) + + resp, err := apiClient.Products.GetReplicationPoliciesID(params, nil) + if err != nil { + return err + } + + if err = setReplicationSchema(d, resp.Payload); err != nil { + return err + } + + return nil + } + + return fmt.Errorf("replication Id not a Integer currently: '%s'", d.Id()) +} + +func resourceReplicationUpdate(d *schema.ResourceData, m interface{}) error { + apiClient := m.(*client.Harbor) + + if registryID, err := strconv.ParseInt(d.Id(), 10, 64); err == nil { + params := products.NewPutReplicationPoliciesIDParams().WithPolicy(buildReplicationPolicy(d)).WithID(registryID) + if _, err := apiClient.Products.PutReplicationPoliciesID(params, nil); err != nil { + return err + } + + return resourceReplicationRead(d, m) + } + + return fmt.Errorf("replication Id not a Integer") +} + +func resourceReplicationDelete(d *schema.ResourceData, m interface{}) error { + apiClient := m.(*client.Harbor) + + if registryID, err := strconv.ParseInt(d.Id(), 10, 64); err == nil { + params := products.NewDeleteReplicationPoliciesIDParams().WithID(registryID) + + if _, err := apiClient.Products.DeleteReplicationPoliciesID(params, nil); err != nil { + return err + } + + return nil + } + + return fmt.Errorf("replication Id not a Integer") +} + +func setReplicationSchema(d *schema.ResourceData, registry *models.ReplicationPolicy) error { + d.SetId(strconv.Itoa(int(registry.ID))) + + if err := d.Set("description", registry.Description); err != nil { + return err + } + + if err := d.Set("name", registry.Name); err != nil { + return err + } + + if err := d.Set("destination_namespace", registry.DestNamespace); err != nil { + return err + } + + if err := d.Set("trigger_mode", registry.Trigger.Type); err != nil { + return err + } + + if err := d.Set("trigger_cron", registry.Trigger.TriggerSettings.Cron); err != nil { + return err + } + + if err := d.Set("enabled", registry.Enabled); err != nil { + return err + } + + if err := d.Set("override", registry.Override); err != nil { + return err + } + + if err := d.Set("source_registry_id", registry.SrcRegistry.ID); err != nil { + return err + } + + if err := d.Set("destination_registry_id", registry.DestRegistry.ID); err != nil { + return err + } + + return nil +} diff --git a/harbor/resource_replication_test.go b/harbor/resource_replication_test.go new file mode 100644 index 0000000..c82c971 --- /dev/null +++ b/harbor/resource_replication_test.go @@ -0,0 +1,105 @@ +package harbor_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/nolte/terraform-provider-harbor/gen/harborctl/models" +) + +func init() { + resource.AddTestSweepers("resource_harbor_replication", &resource.Sweeper{ + Name: "harbor_replication", + }) +} + +func TestAccHarborReplication_Basic(t *testing.T) { + var project models.Project + + var registry models.Registry + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccHarborPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccHarborCheckReplicationPullResourceConfig(), + Check: resource.ComposeTestCheckFunc( + testAccHarborCheckProjectExists("harbor_project.project_replica", &project), + testAccHarborCheckRegistryExists("harbor_registry.registry_replica_helm_hub", ®istry), + resource.TestCheckResourceAttr( + "harbor_replication.pull_helm_chart", "name", "acc-helm-prometheus-operator-test"), + resource.TestCheckResourceAttr( + "harbor_replication.pull_helm_chart", "source_registry_filter_name", "stable/prometheus-operator"), + resource.TestCheckResourceAttr( + "harbor_replication.pull_helm_chart", "description", "Prometheus Operator Replica"), + resource.TestCheckResourceAttr( + "harbor_replication.pull_helm_chart", "destination_namespace", "acc-project-replica-test"), + ), + }, { + Config: testAccHarborCheckReplicationPushResourceConfig(), + Check: resource.ComposeTestCheckFunc( + testAccHarborCheckRegistryExists("harbor_registry.registry_replica_push_helm_hub", ®istry), + resource.TestCheckResourceAttr( + "harbor_replication.push_helm_chart", "name", "acc-push-test"), + resource.TestCheckResourceAttr( + "harbor_replication.push_helm_chart", "source_registry_filter_name", "stable/prometheus-operator"), + resource.TestCheckResourceAttr( + "harbor_replication.push_helm_chart", "description", "Push Replica"), + resource.TestCheckResourceAttr( + "harbor_replication.push_helm_chart", "destination_namespace", "notexistingtest"), + ), + }, + }, + }) +} + +func testAccHarborCheckReplicationPullResourceConfig() string { + return ` +resource "harbor_project" "project_replica" { + name = "acc-project-replica-test" + public = false + vulnerability_scanning = false +} +resource "harbor_registry" "registry_replica_helm_hub" { + name = "acc-registry-replica-test" + url = "https://hub.helm.sh" + type = "helm-hub" + description = "Helm Hub Registry" + insecure = false +} +resource "harbor_replication" "pull_helm_chart" { + name = "acc-helm-prometheus-operator-test" + description = "Prometheus Operator Replica" + source_registry_id = harbor_registry.registry_replica_helm_hub.id + source_registry_filter_name = "stable/prometheus-operator" + source_registry_filter_tag = "**" + destination_namespace = harbor_project.project_replica.name +} +` +} + +func testAccHarborCheckReplicationPushResourceConfig() string { + return ` +resource "harbor_registry" "registry_replica_push_helm_hub" { + name = "acc-registry-push-replica-test" + url = "https://hub.helm.sh" + type = "helm-hub" + description = "Helm Hub Registry" + insecure = false +} +resource "harbor_label" "main_push" { + name = "global-acc-push-golang" + description = "Golang Acc Test Label" + color = "#61717D" +} +resource "harbor_replication" "push_helm_chart" { + name = "acc-push-test" + description = "Push Replica" + source_registry_filter_name = "stable/prometheus-operator" + source_registry_filter_tag = "**" + destination_registry_id = harbor_registry.registry_replica_push_helm_hub.id + destination_namespace = "notexistingtest" +} +` +} diff --git a/mkdocs.yml b/mkdocs.yml index 8dcab72..f6d4688 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,17 +5,18 @@ repo_url: https://github.com/nolte/terraform-provider-harbor nav: - Getting Started: index.md - Data Sources: + - harbor_label: data_sources/harbor_label.md - harbor_project: data_sources/harbor_project.md - harbor_registry: data_sources/harbor_registry.md - - harbor_label: data_sources/harbor_label.md - Resources: - harbor_config_system: resources/harbor_config_system.md - harbor_configuration: resources/harbor_configuration.md + - harbor_label: resources/harbor_label.md - harbor_project: resources/harbor_project.md - harbor_registry: resources/harbor_registry.md + - harbor_replication: resources/harbor_replication.md - harbor_robot_account: resources/harbor_robot_account.md - harbor_tasks: resources/harbor_tasks.md - - harbor_label: resources/harbor_label.md - Guides: - Development: guides/development.md - Local E2E Tests: guides/kind.md diff --git a/scripts/test/tf-acception-test/main.tf b/scripts/test/tf-acception-test/main.tf index cbdb881..a687fcb 100644 --- a/scripts/test/tf-acception-test/main.tf +++ b/scripts/test/tf-acception-test/main.tf @@ -28,6 +28,9 @@ resource "harbor_robot_account" "master_robot" { actions = ["docker_read", "docker_write", "helm_read", "helm_write"] } + + + output "harbor_robot_account_token" { value = harbor_robot_account.master_robot.token } @@ -48,6 +51,7 @@ resource "harbor_registry" "helmhub" { description = "Helm Hub Registry" insecure = false } + # resource "harbor_label" "main" { name = "testlabel-acc-classic" @@ -63,3 +67,23 @@ resource "harbor_label" "project_label" { scope = "p" project_id = harbor_project.main.id } +### + + +resource "harbor_replication_pull" "pull_helm_chart" { + name = "helm-prometheus-operator-acc-classic" + description = "Prometheus Operator Replica ACC Classic" + source_registry_id = harbor_registry.helmhub.id + source_registry_filter_name = "stable/prometheus-operator" + source_registry_filter_tag = "**" + destination_namespace = harbor_project.main.name +} + +resource "harbor_replication_pull" "push_helm_chart" { + name = "docker-push-acc-classic" + description = "Push Docker Replica ACC Classic" + destination_registry_id = harbor_registry.dockerhub.id + source_registry_filter_name = "${harbor_project.main.name}/vscode-devcontainers/k8s-operator" + source_registry_filter_tag = "**" + destination_namespace = "notexisting" +}