From 90bbf747e1ffb106c54ac06239e941d1493cabc8 Mon Sep 17 00:00:00 2001 From: Dominik Pataky <33180520+bitkeks@users.noreply.github.com> Date: Mon, 29 Jan 2024 20:08:54 +0100 Subject: [PATCH] Katello content_views: add CV and CCV (#150) Implements Katello (Composite) Content Views. Tested with Foreman 3.7 / Katello 4.9 for Creation/Update/Deletion. Refs #140 Signed-off-by: Dominik Pataky --- .../foreman_katello_content_view.md | 39 ++ .../resources/foreman_katello_content_view.md | 50 +++ examples/content_view/main.tf | 27 ++ foreman/api/katello_content_views.go | 416 ++++++++++++++++++ ...ta_source_foreman_katello_content_views.go | 72 +++ foreman/provider.go | 4 +- .../resource_foreman_katello_content_views.go | 360 +++++++++++++++ 7 files changed, 967 insertions(+), 1 deletion(-) create mode 100755 docs/data-sources/foreman_katello_content_view.md create mode 100755 docs/resources/foreman_katello_content_view.md create mode 100644 examples/content_view/main.tf create mode 100644 foreman/api/katello_content_views.go create mode 100644 foreman/data_source_foreman_katello_content_views.go create mode 100644 foreman/resource_foreman_katello_content_views.go diff --git a/docs/data-sources/foreman_katello_content_view.md b/docs/data-sources/foreman_katello_content_view.md new file mode 100755 index 00000000..d4c00c5e --- /dev/null +++ b/docs/data-sources/foreman_katello_content_view.md @@ -0,0 +1,39 @@ + +# foreman_katello_content_view + + +(Composite) Content Views create an abstract view on a collection of repositories and allow versioning of these views. Additional fine tuning can be done with package filters. + + +## Example Usage + +``` +# Autogenerated example with required keys +data "foreman_katello_content_view" "example" { + name = "my content view" +} +``` + + +## Argument Reference + +The following arguments are supported: + +- `name` - (Required) Name of the content view. + + +## Attributes Reference + +The following attributes are exported: + +- `auto_publish` - Relevant for Composite Content Views: 'Automatically publish a new version of the composite content view whenever one of its content views is published. Autopublish will only happen for component views that use the 'Always use latest version' option.' +- `component_ids` - Relevant for CCVs: list of CV IDs. +- `composite` - Is this Content View a Composite CV? +- `description` - Description for the (composite) content view +- `filter` - Content view filters and their rules. Currently read-only, to be used as data source +- `label` - Label for the (composite) content view. Cannot be changed after creation. By default set to the name, with underscores as spaces replacement. +- `name` - Name of the content view. +- `organization_id` - +- `repository_ids` - List of repository IDs. +- `solve_dependencies` - Relevant for Content Views: 'This will solve RPM and module stream dependencies on every publish of this content view. Dependency solving significantly increases publish time (publishes can take over three times as long) and filters will be ignored when adding packages to solve dependencies. Also, certain scenarios involving errata may still cause dependency errors.' + diff --git a/docs/resources/foreman_katello_content_view.md b/docs/resources/foreman_katello_content_view.md new file mode 100755 index 00000000..0fa90cc5 --- /dev/null +++ b/docs/resources/foreman_katello_content_view.md @@ -0,0 +1,50 @@ + +# foreman_katello_content_view + + +(Composite) Content Views create an abstract view on a collection of repositories and allow versioning of these views. Additional fine tuning can be done with package filters. + + +## Example Usage + +``` +# Autogenerated example with required keys +resource "foreman_katello_content_view" "example" { + component_ids = [1, 4] + composite = false + name = "My new CV" + repository_ids = [1, 4, 5] +} +``` + + +## Argument Reference + +The following arguments are supported: + +- `auto_publish` - (Optional) Relevant for Composite Content Views: 'Automatically publish a new version of the composite content view whenever one of its content views is published. Autopublish will only happen for component views that use the 'Always use latest version' option.' +- `component_ids` - (Optional) Relevant for CCVs: list of CV IDs. +- `composite` - (Optional) Is this Content View a Composite CV? +- `description` - (Optional) Description for the (composite) content view +- `label` - (Optional, Force New) Label for the (composite) content view. Cannot be changed after creation. By default set to the name, with underscores as spaces replacement. +- `name` - (Required) Name of the (composite) content view. +- `organization_id` - (Optional) +- `repository_ids` - (Optional) List of repository IDs. +- `solve_dependencies` - (Optional) Relevant for Content Views: 'This will solve RPM and module stream dependencies on every publish of this content view. Dependency solving significantly increases publish time (publishes can take over three times as long) and filters will be ignored when adding packages to solve dependencies. Also, certain scenarios involving errata may still cause dependency errors.' + + +## Attributes Reference + +The following attributes are exported: + +- `auto_publish` - Relevant for Composite Content Views: 'Automatically publish a new version of the composite content view whenever one of its content views is published. Autopublish will only happen for component views that use the 'Always use latest version' option.' +- `component_ids` - Relevant for CCVs: list of CV IDs. +- `composite` - Is this Content View a Composite CV? +- `description` - Description for the (composite) content view +- `filter` - Content view filters and their rules. Currently read-only, to be used as data source +- `label` - Label for the (composite) content view. Cannot be changed after creation. By default set to the name, with underscores as spaces replacement. +- `name` - Name of the (composite) content view. +- `organization_id` - +- `repository_ids` - List of repository IDs. +- `solve_dependencies` - Relevant for Content Views: 'This will solve RPM and module stream dependencies on every publish of this content view. Dependency solving significantly increases publish time (publishes can take over three times as long) and filters will be ignored when adding packages to solve dependencies. Also, certain scenarios involving errata may still cause dependency errors.' + diff --git a/examples/content_view/main.tf b/examples/content_view/main.tf new file mode 100644 index 00000000..1ebcc9a2 --- /dev/null +++ b/examples/content_view/main.tf @@ -0,0 +1,27 @@ +// Simple Content View data source +data "foreman_katello_content_view" "ubuntu2204" { + name = "Ubuntu 22.04" +} + +// Query a repository to use its ID in the Content View +data "foreman_katello_repository" "ubuntu2204" { + name = "Ubuntu 22.04" +} + +// Create a new Content View with one repository and one filter +resource "foreman_katello_content_view" "test_cv_write" { + name = "Test CV for Ubuntu Sec" + repository_ids = [data.foreman_katello_repository.ubuntu2204.id] + composite = false + + filter { + name = "my filter 1" + type = "deb" + inclusion = true + description = "Filters all packages except those with name 'testfilter-*'" + + rule { + name = "testfilter-*" + } + } +} diff --git a/foreman/api/katello_content_views.go b/foreman/api/katello_content_views.go new file mode 100644 index 00000000..72f4ccac --- /dev/null +++ b/foreman/api/katello_content_views.go @@ -0,0 +1,416 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "github.com/terraform-coop/terraform-provider-foreman/foreman/utils" + "net/http" +) + +const ( + ContentViewEndpointPrefix = "/katello/api/content_views" + ContentViewById = ContentViewEndpointPrefix + "/%d" // :id + ContentViewsByOrg = "/katello/api/organizations/%d/content_views" // :organization_id + ContentViewFilters = "/katello/api/content_views/%d/filters" // :content_view_id + ContentViewFilterRules = "/katello/api/content_view_filters/%d/rules" // :content_view_filter_id +) + +// A ContentView contains repositories, filters etc. to manage specific views on the Katello contents. +type ContentView struct { + ForemanObject + + ContentHostCount int `json:"content_host_count"` + Composite bool `json:"composite"` + ComponentIds []int `json:"component_ids"` + Default bool `json:"default"` + VersionCount int `json:"version_count"` + LatestVersion string `json:"latest_version"` + LatestVersionId int `json:"latest_version_id"` + AutoPublish bool `json:"auto_publish"` + SolveDependencies bool `json:"solve_dependencies"` + ImportOnly bool `json:"import_only"` + GeneratedFor string `json:"generated_for"` + RelatedCvCount int `json:"related_cv_count"` + RelatedCompositeCvs []interface{} `json:"related_composite_cvs"` + NeedsPublish bool `json:"needs_publish"` + Filtered bool `json:"filtered"` + + Label string `json:"label"` + Description string `json:"description"` + + OrganizationId int `json:"organization_id"` + Organization struct { + Name string `json:"name"` + Label string `json:"label"` + Id int `json:"id"` + } `json:"organization"` + + LastTask struct { + Id string `json:"id"` + StartedAt string `json:"started_at"` + Result string `json:"result"` + LastSyncWords string `json:"last_sync_words"` + } `json:"last_task"` + + LatestVersionEnvironments []struct { + Id int `json:"id"` + Name string `json:"name"` + Label string `json:"label"` + } `json:"latest_version_environments"` + + RepositoryIds []int `json:"repository_ids"` + Repositories []struct { + Id int `json:"id"` + Name string `json:"name"` + Label string `json:"label"` + ContentType string `json:"content_type"` + } `json:"repositories"` + + Versions []struct { + Id int `json:"id"` + Version string `json:"version"` + Published string `json:"published"` + EnvironmentIds []int `json:"environment_ids"` + FiltersApplied bool `json:"filters_applied"` + } `json:"versions"` + + Components []interface{} `json:"components"` + ContentViewComponents []interface{} `json:"content_view_components"` + ActivationKeys []interface{} `json:"activation_keys"` + Hosts []interface{} `json:"hosts"` + NextVersion string `json:"next_version"` + LastPublished string `json:"last_published"` + + Environments []struct { + Id int `json:"id"` + Label string `json:"label"` + Name string `json:"name"` + ActivationKeys []interface{} `json:"activation_keys"` + Hosts []interface{} `json:"hosts"` + Permissions struct { + Readable bool `json:"readable"` + } `json:"permissions"` + } `json:"environments"` + + DuplicateRepositoriesToPublish []interface{} `json:"duplicate_repositories_to_publish"` + Errors interface{} `json:"errors"` + + // Filters are not part of this struct in upstream, but we couple the objects in the provider + Filters []ContentViewFilter +} + +func (cv *ContentView) MarshalJSON() ([]byte, error) { + jsonMap := map[string]interface{}{ + "id": cv.Id, + "name": cv.Name, + "description": cv.Description, + "organization_id": cv.OrganizationId, + "label": cv.Label, + "composite": cv.Composite, + "auto_publish": cv.AutoPublish, // for CCV + "solve_dependencies": cv.SolveDependencies, // for CV + "filtered": cv.Filtered, + "repository_ids": cv.RepositoryIds, + "component_ids": cv.ComponentIds, + } + + return json.Marshal(jsonMap) +} + +// ContentViewFilter is part of a ContentView and filters the presented content according to its rules. +type ContentViewFilter struct { + ForemanObject + + Inclusion bool `json:"inclusion"` + Description string `json:"description"` + + ContentView interface{} `json:"content_view"` + Repositories []interface{} `json:"repositories"` + Type string `json:"type"` + Rules []ContentViewFilterRule `json:"rules"` +} + +type ContentViewFilterRule struct { + ForemanObject + + ContentViewFilterId int `json:"content_view_filter_id"` + Architecture string `json:"architecture"` +} + +func (cvf *ContentViewFilter) MarshalJSON() ([]byte, error) { + jsonMap := map[string]interface{}{ + "id": cvf.Id, + "name": cvf.Name, + "type": cvf.Type, + "inclusion": cvf.Inclusion, + "description": cvf.Description, + "rules": cvf.Rules, + } + + return json.Marshal(jsonMap) +} + +func (c *Client) QueryContentView(ctx context.Context, d *ContentView) (QueryResponse, error) { + utils.TraceFunctionCall() + + queryResponse := QueryResponse{} + + endpoint := ContentViewEndpointPrefix + req, err := c.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return queryResponse, err + } + + // dynamically build the query based on the attributes + reqQuery := req.URL.Query() + name := `"` + d.Name + `"` + reqQuery.Set("search", "name="+name) + + req.URL.RawQuery = reqQuery.Encode() + err = c.SendAndParse(req, &queryResponse) + if err != nil { + return queryResponse, err + } + + utils.Debugf("queryResponse: %+v", queryResponse) + + var results []ContentView + resultsBytes, err := json.Marshal(queryResponse.Results) + if err != nil { + return queryResponse, err + } + err = json.Unmarshal(resultsBytes, &results) + if err != nil { + return queryResponse, err + } + + iArr := make([]interface{}, len(results)) + for idx, val := range results { + iArr[idx] = val + } + queryResponse.Results = iArr + + return queryResponse, nil +} + +// QueryContentViewFilters returns the filters including their rules +func (c *Client) QueryContentViewFilters(ctx context.Context, cvId int) (QueryResponse, error) { + utils.TraceFunctionCall() + queryResponse := QueryResponse{} + + endpoint := fmt.Sprintf(ContentViewFilters, cvId) + req, err := c.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return queryResponse, err + } + + err = c.SendAndParse(req, &queryResponse) + if err != nil { + return queryResponse, err + } + + utils.Debugf("queryResponse: %+v", queryResponse) + + var results []ContentViewFilter + resultsBytes, err := json.Marshal(queryResponse.Results) + if err != nil { + return queryResponse, err + } + + err = json.Unmarshal(resultsBytes, &results) + if err != nil { + return queryResponse, err + } + + iArr := make([]interface{}, len(results)) + for idx, val := range results { + iArr[idx] = val + } + queryResponse.Results = iArr + + return queryResponse, nil +} + +func (c *Client) CreateKatelloContentView(ctx context.Context, cv *ContentView) (*ContentView, error) { + utils.TraceFunctionCall() + + endpoint := ContentViewEndpointPrefix + + jsonBytes, err := c.WrapJSONWithTaxonomy(nil, cv) + if err != nil { + return nil, err + } + + req, err := c.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(jsonBytes)) + if err != nil { + return nil, err + } + + var createdCv ContentView + err = c.SendAndParse(req, &createdCv) + if err != nil { + return nil, err + } + + cvfs, err := c.CreateKatelloContentViewFilters(ctx, createdCv.Id, &cv.Filters) + if err != nil { + return nil, err + } + createdCv.Filters = *cvfs + + utils.Debugf("createdCv: %+v", createdCv) + + return &createdCv, nil +} + +func (c *Client) CreateKatelloContentViewFilters(ctx context.Context, cvId int, cvf *[]ContentViewFilter) (*[]ContentViewFilter, error) { + utils.TraceFunctionCall() + + endpoint := fmt.Sprintf(ContentViewFilters, cvId) + + jsonBytes, err := c.WrapJSONWithTaxonomy(nil, cvf) + if err != nil { + return nil, err + } + + req, err := c.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(jsonBytes)) + if err != nil { + return nil, err + } + + var createdCvf []ContentViewFilter + err = c.SendAndParse(req, &createdCvf) + if err != nil { + return nil, err + } + + utils.Debugf("createdCvf: %+v", createdCvf) + + return &createdCvf, nil +} + +func (c *Client) ReadKatelloContentView(ctx context.Context, d *ContentView) (*ContentView, error) { + utils.TraceFunctionCall() + + reqEndpoint := fmt.Sprintf(ContentViewById, d.Id) + var cv ContentView + + req, err := c.NewRequestWithContext(ctx, http.MethodGet, reqEndpoint, nil) + if err != nil { + return nil, err + } + + err = c.SendAndParse(req, &cv) + if err != nil { + return nil, err + } + + cvfs, err := c.ReadContentViewFilters(ctx, cv.Id) + if err != nil { + return nil, err + } + cv.Filters = *cvfs + + utils.Debugf("read content_view: %+v", cv) + + return &cv, nil +} + +func (c *Client) ReadContentViewFilters(ctx context.Context, cvId int) (*[]ContentViewFilter, error) { + utils.TraceFunctionCall() + + reqEndpoint := fmt.Sprintf(ContentViewFilters, cvId) + var cvf []ContentViewFilter + + req, err := c.NewRequestWithContext(ctx, http.MethodGet, reqEndpoint, nil) + if err != nil { + return nil, err + } + + err = c.SendAndParse(req, &cvf) + if err != nil { + return nil, err + } + + utils.Debugf("read content_view filter: %+v", cvf) + + return &cvf, nil +} + +func (c *Client) UpdateKatelloContentView(ctx context.Context, cv *ContentView) (*ContentView, error) { + utils.TraceFunctionCall() + + endpoint := fmt.Sprintf(ContentViewById, cv.Id) + + jsonBytes, err := c.WrapJSONWithTaxonomy(nil, cv) + if err != nil { + return nil, err + } + + utils.Debugf("jsonBytes: %s", jsonBytes) + + req, err := c.NewRequestWithContext(ctx, http.MethodPut, endpoint, bytes.NewBuffer(jsonBytes)) + if err != nil { + return nil, err + } + + var updatedCv ContentView + err = c.SendAndParse(req, &updatedCv) + if err != nil { + return nil, err + } + + cvfs, err := c.UpdateKatelloContentViewFilters(ctx, updatedCv.Id, &cv.Filters) + if err != nil { + return nil, err + } + updatedCv.Filters = *cvfs + + utils.Debugf("updatedCv: %+v", updatedCv) + + return &updatedCv, nil +} + +func (c *Client) UpdateKatelloContentViewFilters(ctx context.Context, cvId int, cvf *[]ContentViewFilter) (*[]ContentViewFilter, error) { + utils.TraceFunctionCall() + + endpoint := fmt.Sprintf(ContentViewFilters, cvId) + + jsonBytes, err := c.WrapJSONWithTaxonomy(nil, cvf) + if err != nil { + return nil, err + } + + utils.Debugf("jsonBytes: %s", jsonBytes) + + req, err := c.NewRequestWithContext(ctx, http.MethodPut, endpoint, bytes.NewBuffer(jsonBytes)) + if err != nil { + return nil, err + } + + var updatedCvf []ContentViewFilter + err = c.SendAndParse(req, &updatedCvf) + if err != nil { + return nil, err + } + + utils.Debugf("updatedCvf: %+v", updatedCvf) + + return &updatedCvf, nil +} + +// DeleteKatelloContentView also deletes all Filters and Rules +func (c *Client) DeleteKatelloContentView(ctx context.Context, id int) error { + utils.TraceFunctionCall() + + endpoint := fmt.Sprintf(ContentViewById, id) + + req, err := c.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return err + } + + return c.SendAndParse(req, nil) +} diff --git a/foreman/data_source_foreman_katello_content_views.go b/foreman/data_source_foreman_katello_content_views.go new file mode 100644 index 00000000..623d0467 --- /dev/null +++ b/foreman/data_source_foreman_katello_content_views.go @@ -0,0 +1,72 @@ +package foreman + +import ( + "context" + "fmt" + "github.com/HanseMerkur/terraform-provider-utils/autodoc" + "github.com/HanseMerkur/terraform-provider-utils/helper" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-coop/terraform-provider-foreman/foreman/api" + "github.com/terraform-coop/terraform-provider-foreman/foreman/utils" +) + +func dataSourceForemanKatelloContentView() *schema.Resource { + r := resourceForemanKatelloContentView() + ds := helper.DataSourceSchemaFromResourceSchema(r.Schema) + + // define searchable attributes for the data source + ds["name"] = &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf("Name of the content view. %s \"my content view\"", autodoc.MetaExample), + } + + return &schema.Resource{ + ReadContext: dataSourceForemanKatelloContentViewRead, + Schema: ds, + } +} + +func dataSourceForemanKatelloContentViewRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + utils.TraceFunctionCall() + + client := meta.(*api.Client) + cv := buildForemanKatelloContentView(d) + + utils.Debugf("cv: %+v", cv) + + queryResponse, err := client.QueryContentView(ctx, cv) + if err != nil { + return diag.FromErr(err) + } + + if queryResponse.Subtotal == 0 { + return diag.Errorf("data source content_view returned no results") + } else if queryResponse.Subtotal > 1 { + return diag.Errorf("data source content_view returned more than 1 result") + } + + if queryCv, ok := queryResponse.Results[0].(api.ContentView); !ok { + return diag.Errorf( + "data source results contain unexpected type. Expected "+ + "[api.ContentView], got [%T]", + queryResponse.Results[0], + ) + } else { + cv = &queryCv + } + + filtersResult, err := client.QueryContentViewFilters(ctx, cv.Id) + for _, item := range filtersResult.Results { + asserted := item.(api.ContentViewFilter) + cv.Filters = append(cv.Filters, asserted) + utils.Debugf("%+v", asserted) + } + + utils.Debugf("cv: %+v", cv) + + setResourceDataFromForemanKatelloContentView(d, cv) + + return nil +} diff --git a/foreman/provider.go b/foreman/provider.go index 75617280..9e0ee9fc 100644 --- a/foreman/provider.go +++ b/foreman/provider.go @@ -175,7 +175,7 @@ func Provider() *schema.Provider { }, }, - ResourcesMap: map[string]*schema.Resource{ +ResourcesMap: map[string]*schema.Resource{ "foreman_architecture": resourceForemanArchitecture(), "foreman_host": resourceForemanHost(), "foreman_hostgroup": resourceForemanHostgroup(), @@ -198,6 +198,7 @@ func Provider() *schema.Provider { "foreman_katello_lifecycle_environment": resourceForemanKatelloLifecycleEnvironment(), "foreman_katello_product": resourceForemanKatelloProduct(), "foreman_katello_repository": resourceForemanKatelloRepository(), + "foreman_katello_content_view": resourceForemanKatelloContentView(), "foreman_katello_sync_plan": resourceForemanKatelloSyncPlan(), "foreman_user": resourceForemanUser(), "foreman_usergroup": resourceForemanUsergroup(), @@ -233,6 +234,7 @@ func Provider() *schema.Provider { "foreman_katello_lifecycle_environment": dataSourceForemanKatelloLifecycleEnvironment(), "foreman_katello_product": dataSourceForemanKatelloProduct(), "foreman_katello_repository": dataSourceForemanKatelloRepository(), + "foreman_katello_content_view": dataSourceForemanKatelloContentView(), "foreman_katello_sync_plan": dataSourceForemanKatelloSyncPlan(), "foreman_user": dataSourceForemanUser(), "foreman_usergroup": dataSourceForemanUsergroup(), diff --git a/foreman/resource_foreman_katello_content_views.go b/foreman/resource_foreman_katello_content_views.go new file mode 100644 index 00000000..3e340b95 --- /dev/null +++ b/foreman/resource_foreman_katello_content_views.go @@ -0,0 +1,360 @@ +package foreman + +import ( + "context" + "fmt" + "github.com/HanseMerkur/terraform-provider-utils/autodoc" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/terraform-coop/terraform-provider-foreman/foreman/api" + "github.com/terraform-coop/terraform-provider-foreman/foreman/utils" + "strconv" +) + +func resourceForemanKatelloContentView() *schema.Resource { + return &schema.Resource{ + + CreateContext: resourceForemanKatelloContentViewCreate, + ReadContext: resourceForemanKatelloContentViewRead, + UpdateContext: resourceForemanKatelloContentViewUpdate, + DeleteContext: resourceForemanKatelloContentViewDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + autodoc.MetaAttribute: { + Type: schema.TypeBool, + Computed: true, + Description: fmt.Sprintf( + "%s (Composite) Content Views create an abstract view on a collection of repositories and "+ + "allow versioning of these views. Additional fine tuning can be done with package filters.", + autodoc.MetaSummary, + ), + }, + + "name": { + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf("Name of the (composite) content view. %s \"My new CV\"", autodoc.MetaExample), + }, + + "description": { + Type: schema.TypeString, + Optional: true, + Description: "Description for the (composite) content view", + }, + + "label": { + Type: schema.TypeString, + Optional: true, + Computed: true, // Created from name if not passed in + ForceNew: true, + Description: fmt.Sprintf( + "Label for the (composite) content view. Cannot be changed after creation. "+ + "By default set to the name, with underscores as spaces replacement. %s", + autodoc.MetaExample, + ), + }, + + "organization_id": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + + "composite": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: fmt.Sprintf("Is this Content View a Composite CV? %s false", autodoc.MetaExample), + }, + + "solve_dependencies": { + Type: schema.TypeBool, + Default: false, + Optional: true, + Description: "Relevant for Content Views: 'This will solve RPM and module stream dependencies on " + + "every publish of this content " + + "view. Dependency solving significantly increases publish time (publishes can take over three " + + "times as long) and filters will be ignored when adding packages to solve dependencies. Also, " + + "certain scenarios involving errata may still cause dependency errors.'", + }, + + "auto_publish": { + Type: schema.TypeBool, + Default: false, + Optional: true, + Description: "Relevant for Composite Content Views: 'Automatically publish a new version of the " + + "composite content view whenever one of its content views is published. Autopublish will only " + + "happen for component views that use the 'Always use latest version' option.'", + }, + + "repository_ids": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, + Optional: true, + Description: fmt.Sprintf("List of repository IDs. %s [1, 4, 5]", autodoc.MetaExample), + }, + + "component_ids": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, + Optional: true, + Description: fmt.Sprintf("Relevant for CCVs: list of CV IDs. %s [1, 4]", autodoc.MetaExample), + }, + + "filter": { + Type: schema.TypeSet, + Required: false, + Optional: false, + Computed: true, + Description: "Content view filters and their rules. Currently read-only, to be used as data source", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + + "type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + "deb", + "rpm", + "package_group", + "erratum", + "erratum_id", + "erratum_date", + "docker", + "modulemd", + }, false), + Description: "Type of this filter, e.g. DEB or RPM", + }, + + "inclusion": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "specifies if content should be included or excluded, " + + "default: inclusion=false", + }, + + "description": { + Type: schema.TypeString, + Optional: true, + }, + + "rule": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "architecture": { + Type: schema.TypeString, + Optional: true, + }, + + "name": { + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf("Filter pattern of this filter %s apt*", + autodoc.MetaExample), + }, + }, + }, + }, + + //original_packages bool + //original_module_streams bool + //repository_ids []interface + }, + }, + }, + }, + } +} + +func buildForemanKatelloContentView(d *schema.ResourceData) *api.ContentView { + utils.TraceFunctionCall() + + cv := api.ContentView{} + cv.ForemanObject = *buildForemanObject(d) + + cv.Description = d.Get("description").(string) + cv.Label = d.Get("label").(string) + cv.OrganizationId = d.Get("organization_id").(int) + cv.Composite = d.Get("composite").(bool) + cv.AutoPublish = d.Get("auto_publish").(bool) + cv.SolveDependencies = d.Get("solve_dependencies").(bool) + + if filtered, ok := d.GetOk("filtered"); ok { + cv.Filtered = filtered.(bool) + } + + // repository_ids and component_ids are defined as "TypeList" which can + // be any type according to Terraform docs. So we need to cast to interface and then to int. + + if repoIds, ok := d.GetOk("repository_ids"); ok { + casted := repoIds.([]interface{}) + var ids []int + for _, item := range casted { + ids = append(ids, item.(int)) + } + cv.RepositoryIds = ids + } + + if componentIds, ok := d.GetOk("component_ids"); ok { + casted := componentIds.([]interface{}) + var ids []int + for _, item := range casted { + ids = append(ids, item.(int)) + } + cv.ComponentIds = ids + } + + // Handle list of ContentViewFilters + if filters, ok := d.GetOk("filter"); ok { + var cvfs []api.ContentViewFilter + + for _, cvfsResData := range filters.([]schema.ResourceData) { + var cvf api.ContentViewFilter + + cvf.Name = cvfsResData.Get("name").(string) + cvf.Type = cvfsResData.Get("type").(string) + cvf.Inclusion = cvfsResData.Get("inclusion").(bool) + cvf.Description = cvfsResData.Get("description").(string) + + if rules, ok := cvfsResData.GetOk("rule"); ok { + var cvfrs []api.ContentViewFilterRule + + for _, rulesResData := range rules.([]schema.ResourceData) { + var cvfr api.ContentViewFilterRule + + cvfr.Name = rulesResData.Get("name").(string) + cvfr.Architecture = rulesResData.Get("architecture").(string) + + cvfrs = append(cvfrs, cvfr) + } + + cvf.Rules = cvfrs + } + + cvfs = append(cvfs, cvf) + } + + cv.Filters = cvfs + } + + return &cv +} + +func setResourceDataFromForemanKatelloContentView(d *schema.ResourceData, cv *api.ContentView) { + utils.TraceFunctionCall() + + d.SetId(strconv.Itoa(cv.Id)) + d.Set("name", cv.Name) + d.Set("description", cv.Description) + d.Set("label", cv.Label) + d.Set("organization_id", cv.OrganizationId) + d.Set("composite", cv.Composite) + d.Set("auto_publish", cv.AutoPublish) + d.Set("solve_dependencies", cv.SolveDependencies) + d.Set("filtered", cv.Filtered) + d.Set("repository_ids", cv.RepositoryIds) + d.Set("component_ids", cv.ComponentIds) + + // Handle ContentViewFilters and their ContentViewFilterRules + var filterSet []map[string]interface{} + for _, item := range cv.Filters { + newFilter := map[string]interface{}{ + "name": item.Name, + "type": item.Type, + "inclusion": item.Inclusion, + "description": item.Description, + "rule": nil, + } + + var ruleSet []map[string]interface{} + for _, item2 := range item.Rules { + newRule := map[string]interface{}{ + "name": item2.Name, + "architecture": item2.Architecture, + } + ruleSet = append(ruleSet, newRule) + } + + newFilter["rule"] = ruleSet + + filterSet = append(filterSet, newFilter) + } + d.Set("filter", filterSet) +} + +func resourceForemanKatelloContentViewCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + utils.TraceFunctionCall() + + client := meta.(*api.Client) + cv := buildForemanKatelloContentView(d) + utils.Debugf("cv: %+v", cv) + + createdCv, err := client.CreateKatelloContentView(ctx, cv) + if err != nil { + return diag.FromErr(err) + } + utils.Debugf("createdCv: %+v", createdCv) + + setResourceDataFromForemanKatelloContentView(d, createdCv) + return nil +} + +func resourceForemanKatelloContentViewRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + utils.TraceFunctionCall() + + client := meta.(*api.Client) + cv := buildForemanKatelloContentView(d) + + readCv, err := client.ReadKatelloContentView(ctx, cv) + if err != nil { + return diag.FromErr(api.CheckDeleted(d, err)) + } + utils.Debugf("readCv: %+v", readCv) + + setResourceDataFromForemanKatelloContentView(d, readCv) + return nil +} + +func resourceForemanKatelloContentViewUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + utils.TraceFunctionCall() + + client := meta.(*api.Client) + cv := buildForemanKatelloContentView(d) + utils.Debugf("cv: [%+v]", cv) + + updatedCv, err := client.UpdateKatelloContentView(ctx, cv) + if err != nil { + return diag.FromErr(err) + } + utils.Debugf("updatedCv: %+v", updatedCv) + + setResourceDataFromForemanKatelloContentView(d, updatedCv) + return nil +} + +func resourceForemanKatelloContentViewDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + utils.TraceFunctionCall() + + client := meta.(*api.Client) + cv := buildForemanKatelloContentView(d) + + utils.Debugf("cv to be deleted: %+v", cv) + + return diag.FromErr(api.CheckDeleted(d, client.DeleteKatelloContentView(ctx, cv.Id))) +}