From 3ffdb13054f3ee0992238c0d5188fce746e3c33b Mon Sep 17 00:00:00 2001 From: Dominik Pataky <33180520+bitkeks@users.noreply.github.com> Date: Mon, 28 Aug 2023 08:15:46 +0000 Subject: [PATCH] Implement compute profiles (#134) * First draft of computeprofile resource Implements the resource and refactors the data source. The compute_attributes JSON is more tricky to handle, needs more testing and validation. * Extend compute_profiles with compute_attributes Implements creation and deletion of compute profiles and their own compute attributes. The most difficult part is the separate creation of compute attributes, and the combined parsing to create a single struct from both API sources. Terraform understands multiple formats: strings (jsonencode..), lists, maps etc. This implementation uses maps in the schema. See examples/compute_profiles for reference. * compute profiles: add resourceForemanComputeprofileRead; fix list init * Compute profile: add custom JSON marshaller to ForemanComputeAttribute Adds a custom marshalling func to ForemanComputeAttribute to handle the JSONified attributes (e.g. boot_order and interfaces_attributes). Also adds a more complex example, see examples/compute_profile/ * Comp Profile: remove "name" attr from compute attributes Signed-off-by: Dominik Pataky * Compute profiles: fix parsing and marshalling of compute attributes The compute attributes are more difficult to handle, since they are very dynamically configurable in the Terraform manifests. This commit improves JSON marshalling and unmarshalling and type handling. Especially the correct setting of id and name of compute attributes was reworked, and tested. Signed-off-by: Dominik Pataky * Implement update func for compute profiles Handles updates from Terraform for compute profiles and their respective compute attributes. Signed-off-by: Dominik Pataky --------- Signed-off-by: Dominik Pataky --- docs/data-sources/foreman_computeprofile.md | 3 +- docs/resources/foreman_computeprofile.md | 31 +++ examples/compute_profile/complex_vmware.tf | 24 ++ examples/compute_profile/main.tf | 15 ++ foreman/api/computeprofile.go | 227 +++++++++++++++++- foreman/data_source_foreman_computeprofile.go | 54 ++--- foreman/provider.go | 1 + foreman/resource_foreman_computeprofile.go | 224 +++++++++++++++++ mkdocs.yml | 1 + 9 files changed, 540 insertions(+), 40 deletions(-) create mode 100755 docs/resources/foreman_computeprofile.md create mode 100644 examples/compute_profile/complex_vmware.tf create mode 100644 examples/compute_profile/main.tf create mode 100644 foreman/resource_foreman_computeprofile.go diff --git a/docs/data-sources/foreman_computeprofile.md b/docs/data-sources/foreman_computeprofile.md index 679b0f5e..2b5b7cc6 100644 --- a/docs/data-sources/foreman_computeprofile.md +++ b/docs/data-sources/foreman_computeprofile.md @@ -2,7 +2,7 @@ # foreman_computeprofile - +Foreman representation of a compute profile. ## Example Usage @@ -26,5 +26,6 @@ The following arguments are supported: The following attributes are exported: +- `compute_attributes` - List of compute attributes - `name` - Compute profile name. diff --git a/docs/resources/foreman_computeprofile.md b/docs/resources/foreman_computeprofile.md new file mode 100755 index 00000000..e2c08949 --- /dev/null +++ b/docs/resources/foreman_computeprofile.md @@ -0,0 +1,31 @@ + +# foreman_computeprofile + + +Foreman representation of a compute profile. + + +## Example Usage + +``` +# Autogenerated example with required keys +resource "foreman_computeprofile" "example" { +} +``` + + +## Argument Reference + +The following arguments are supported: + +- `compute_attributes` - (Required) List of compute attributes +- `name` - (Required) Name of the compute profile + + +## Attributes Reference + +The following attributes are exported: + +- `compute_attributes` - List of compute attributes +- `name` - Name of the compute profile + diff --git a/examples/compute_profile/complex_vmware.tf b/examples/compute_profile/complex_vmware.tf new file mode 100644 index 00000000..90334b38 --- /dev/null +++ b/examples/compute_profile/complex_vmware.tf @@ -0,0 +1,24 @@ +resource "foreman_computeprofile" "vmware_webserver" { + name = "VMware Webserver" + + compute_attributes { + name = "Webserver middle (2 CPUs and 16GB memory)" + compute_resource_id = data.foreman_computeresource.vmware.id + + vm_attrs = { + cpus = 2 + corespersocket = 1 + memory_mb = 16384 + firmware = "bios" + resource_pool = "pool1" + guest_id = "ubuntu64Guest" + + boot_order = jsonencode([ "disk", "network" ]) + + interfaces_attributes = jsonencode({ + 0: { type: "VirtualE1000", network: "dmz-net" }, + 1: { type: "VirtualE1000", network: "webserver-net" } } + ) + } + } +} diff --git a/examples/compute_profile/main.tf b/examples/compute_profile/main.tf new file mode 100644 index 00000000..afed0e39 --- /dev/null +++ b/examples/compute_profile/main.tf @@ -0,0 +1,15 @@ +data "foreman_computeresource" "vmware" { + name = "VMware Cluster ABC" +} + +resource "foreman_computeprofile" "Small VM" { + name = "Small VM" + + compute_attributes { + compute_resource_id = data.foreman_computeresource.vmware.id + vm_attrs = { + cpus = 2 + memory_mb = 4096 + } + } +} \ No newline at end of file diff --git a/foreman/api/computeprofile.go b/foreman/api/computeprofile.go index 7d488bbb..dddb847d 100644 --- a/foreman/api/computeprofile.go +++ b/foreman/api/computeprofile.go @@ -1,10 +1,12 @@ package api import ( + "bytes" "context" "encoding/json" "fmt" "net/http" + "strconv" "github.com/HanseMerkur/terraform-provider-utils/log" ) @@ -18,8 +20,78 @@ const ( // ----------------------------------------------------------------------------- type ForemanComputeProfile struct { - // Inherits the base object's attributes ForemanObject + ComputeAttributes []*ForemanComputeAttribute `json:"compute_attributes,omitempty"` +} + +type ForemanComputeAttribute struct { + ForemanObject + ComputeResourceId int `json:"compute_resource_id"` + VMAttrs map[string]interface{} `json:"vm_attrs,omitempty"` +} + +// Implement custom Marshal function for ForemanComputeAttribute to convert +// the internal vm_attrs map from all-string to their matching types. +func (ca *ForemanComputeAttribute) MarshalJSON() ([]byte, error) { + fca := map[string]interface{}{ + "id": ca.Id, + "name": ca.Name, + "compute_resource_id": ca.ComputeResourceId, + "vm_attrs": nil, + } + + attrs := map[string]interface{}{} + + // Since we allow all types of input in the VMAttrs JSON, + // all types must be handled for conversion + + for k, v := range ca.VMAttrs { + // log.Debugf("v %s %T: %+v", k, v, v) + + switch v := v.(type) { + + case int: + attrs[k] = strconv.Itoa(v) + + case float32: + attrs[k] = strconv.FormatFloat(float64(v), 'f', -1, 32) + + case float64: + attrs[k] = strconv.FormatFloat(v, 'f', -1, 64) + + case bool: + attrs[k] = strconv.FormatBool(v) + + case nil: + attrs[k] = nil + + case string: + var res interface{} + umErr := json.Unmarshal([]byte(v), &res) + if umErr != nil { + // Most likely a "true" string, that cannot be unmarshalled + // Example err: "invalid character 'x' looking for beginning of value" + attrs[k] = v + } else { + // Conversion from JSON string to internal type worked, use it + attrs[k] = res + } + + case map[string]interface{}, []interface{}: + // JSON array or object passed in, simply convert it to a string + by, err := json.Marshal(v) + if err != nil { + return nil, err + } + attrs[k] = string(by) + + default: + log.Errorf("v had a type that was not handled: %T", v) + } + } + + fca["vm_attrs"] = attrs + return json.Marshal(fca) } // ----------------------------------------------------------------------------- @@ -51,6 +123,10 @@ func (c *Client) ReadComputeProfile(ctx context.Context, id int) (*ForemanComput log.Debugf("readComputeProfile: [%+v]", readComputeProfile) + for i := 0; i < len(readComputeProfile.ComputeAttributes); i++ { + log.Debugf("compute_attribute: [%+v]", readComputeProfile.ComputeAttributes[i]) + } + return &readComputeProfile, nil } @@ -113,3 +189,152 @@ func (c *Client) QueryComputeProfile(ctx context.Context, t *ForemanComputeProfi return queryResponse, nil } + +func (c *Client) CreateComputeprofile(ctx context.Context, d *ForemanComputeProfile) (*ForemanComputeProfile, error) { + log.Tracef("foreman/api/computeprofile.go#Create") + + reqEndpoint := ComputeProfileEndpointPrefix + + // Copy the original obj and then remove ComputeAttributes + compProfileClean := new(ForemanComputeProfile) + compProfileClean.ForemanObject = d.ForemanObject + compProfileClean.ComputeAttributes = nil + + cprofJSONBytes, jsonEncErr := c.WrapJSONWithTaxonomy("compute_profile", compProfileClean) + if jsonEncErr != nil { + return nil, jsonEncErr + } + + log.Debugf("cprofJSONBytes: [%s]", cprofJSONBytes) + + req, reqErr := c.NewRequestWithContext( + ctx, + http.MethodPost, + reqEndpoint, + bytes.NewBuffer(cprofJSONBytes), + ) + if reqErr != nil { + return nil, reqErr + } + + var createdComputeprofile ForemanComputeProfile + sendErr := c.SendAndParse(req, &createdComputeprofile) + if sendErr != nil { + return nil, sendErr + } + + // Add the compute attributes as well + for i := 0; i < len(d.ComputeAttributes); i++ { + compattrsEndpoint := fmt.Sprintf("%s/%d/compute_resources/%d/compute_attributes", + ComputeProfileEndpointPrefix, + createdComputeprofile.Id, + d.ComputeAttributes[i].ComputeResourceId) + + log.Debugf("d.ComputeAttributes[i]: %+v", d.ComputeAttributes[i]) + + by, err := c.WrapJSONWithTaxonomy("compute_attribute", d.ComputeAttributes[i]) + if err != nil { + return nil, err + } + log.Debugf("%s", by) + req, reqErr = c.NewRequestWithContext( + ctx, http.MethodPost, compattrsEndpoint, bytes.NewBuffer(by), + ) + if reqErr != nil { + return nil, reqErr + } + var createdComputeAttribute ForemanComputeAttribute + sendErr = c.SendAndParse(req, &createdComputeAttribute) + if sendErr != nil { + return nil, sendErr + } + createdComputeprofile.ComputeAttributes = append(createdComputeprofile.ComputeAttributes, &createdComputeAttribute) + } + + log.Debugf("createdComputeprofile: [%+v]", createdComputeprofile) + + return &createdComputeprofile, nil +} + +func (c *Client) UpdateComputeProfile(ctx context.Context, d *ForemanComputeProfile) (*ForemanComputeProfile, error) { + log.Tracef("foreman/api/computeprofile.go#Update") + + reqEndpoint := fmt.Sprintf("/%s/%d", ComputeProfileEndpointPrefix, d.Id) + + jsonBytes, jsonEncErr := c.WrapJSONWithTaxonomy("compute_profile", d) + if jsonEncErr != nil { + return nil, jsonEncErr + } + + log.Debugf("jsonBytes: [%s]", jsonBytes) + + req, reqErr := c.NewRequestWithContext( + ctx, + http.MethodPut, + reqEndpoint, + bytes.NewBuffer(jsonBytes), + ) + if reqErr != nil { + return nil, reqErr + } + + var updatedComputeProfile ForemanComputeProfile + sendErr := c.SendAndParse(req, &updatedComputeProfile) + if sendErr != nil { + return nil, sendErr + } + + // Handle updates for the compute attributes of this compute profile + updatedComputeAttributes := []*ForemanComputeAttribute{} + for i := 0; i < len(d.ComputeAttributes); i++ { + elem := d.ComputeAttributes[i] + updateEndpoint := fmt.Sprintf("%s/%d/compute_resources/%d/compute_attributes/%d", + ComputeProfileEndpointPrefix, + updatedComputeProfile.Id, + elem.ComputeResourceId, + elem.Id) + + log.Debugf("d.ComputeAttributes[i]: %+v", elem) + + by, err := c.WrapJSONWithTaxonomy("compute_attribute", elem) + if err != nil { + return nil, err + } + log.Debugf("by: %s", by) + + req, reqErr = c.NewRequestWithContext( + ctx, + http.MethodPut, + updateEndpoint, + bytes.NewBuffer(by), + ) + if reqErr != nil { + return nil, reqErr + } + + var updatedComputeAttribute ForemanComputeAttribute + sendErr = c.SendAndParse(req, &updatedComputeAttribute) + if sendErr != nil { + return nil, sendErr + } + updatedComputeAttributes = append(updatedComputeAttributes, &updatedComputeAttribute) + } + + updatedComputeProfile.ComputeAttributes = updatedComputeAttributes + + log.Debugf("updatedComputeprofile: [%+v]", updatedComputeProfile) + + return &updatedComputeProfile, nil +} + +func (c *Client) DeleteComputeProfile(ctx context.Context, id int) error { + log.Tracef("foreman/api/computeprofile.go#Delete") + + reqEndpoint := fmt.Sprintf("/%s/%d", ComputeProfileEndpointPrefix, id) + req, reqErr := c.NewRequestWithContext(ctx, http.MethodDelete, reqEndpoint, nil) + if reqErr != nil { + return reqErr + } + + return c.SendAndParse(req, nil) +} diff --git a/foreman/data_source_foreman_computeprofile.go b/foreman/data_source_foreman_computeprofile.go index 1038e0e0..71a6b507 100644 --- a/foreman/data_source_foreman_computeprofile.go +++ b/foreman/data_source_foreman_computeprofile.go @@ -3,9 +3,9 @@ package foreman import ( "context" "fmt" - "strconv" "github.com/HanseMerkur/terraform-provider-utils/autodoc" + "github.com/HanseMerkur/terraform-provider-utils/helper" "github.com/HanseMerkur/terraform-provider-utils/log" "github.com/terraform-coop/terraform-provider-foreman/foreman/api" @@ -14,53 +14,31 @@ import ( ) func dataSourceForemanComputeProfile() *schema.Resource { - return &schema.Resource{ + r := resourceForemanComputeProfile() + ds := helper.DataSourceSchemaFromResourceSchema(r.Schema) + + ds["name"] = &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf( + "Compute profile name."+ + "%s \"2-Medium\"", + autodoc.MetaExample, + ), + } + return &schema.Resource{ ReadContext: dataSourceForemanComputeProfileRead, - - Schema: map[string]*schema.Schema{ - - "name": { - Type: schema.TypeString, - Required: true, - Description: fmt.Sprintf( - "Compute profile name."+ - "%s \"2-Medium\"", - autodoc.MetaExample, - ), - }, - }, + Schema: ds, } } -// ----------------------------------------------------------------------------- -// Conversion Helpers -// ----------------------------------------------------------------------------- - -// buildForemanComputeProfile constructs a ForemanComputeProfile reference from a -// resource data reference. The struct's members are populated from the data -// populated in the resource data. Missing members will be left to the zero -// value for that member's type. -func buildForemanComputeProfile(d *schema.ResourceData) *api.ForemanComputeProfile { - t := api.ForemanComputeProfile{} - obj := buildForemanObject(d) - t.ForemanObject = *obj - return &t -} - -// setResourceDataFromForemanComputeProfile sets a ResourceData's attributes from -// the attributes of the supplied ForemanComputeProfile reference -func setResourceDataFromForemanComputeProfile(d *schema.ResourceData, fk *api.ForemanComputeProfile) { - d.SetId(strconv.Itoa(fk.Id)) - d.Set("name", fk.Name) -} - // ----------------------------------------------------------------------------- // Resource CRUD Operations // ----------------------------------------------------------------------------- func dataSourceForemanComputeProfileRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - log.Tracef("data_source_foreman_architecture.go#Read") + log.Tracef("data_source_foreman_computeprofile.go#Read") client := meta.(*api.Client) t := buildForemanComputeProfile(d) diff --git a/foreman/provider.go b/foreman/provider.go index 28c3638d..cd200ff7 100644 --- a/foreman/provider.go +++ b/foreman/provider.go @@ -201,6 +201,7 @@ func Provider() *schema.Provider { "foreman_user": resourceForemanUser(), "foreman_usergroup": resourceForemanUsergroup(), "foreman_override_value": resourceForemanOverrideValue(), + "foreman_computeprofile": resourceForemanComputeProfile(), }, DataSourcesMap: map[string]*schema.Resource{ diff --git a/foreman/resource_foreman_computeprofile.go b/foreman/resource_foreman_computeprofile.go new file mode 100644 index 00000000..a974b468 --- /dev/null +++ b/foreman/resource_foreman_computeprofile.go @@ -0,0 +1,224 @@ +package foreman + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + + "github.com/HanseMerkur/terraform-provider-utils/autodoc" + "github.com/HanseMerkur/terraform-provider-utils/log" + "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" +) + +func resourceForemanComputeProfile() *schema.Resource { + return &schema.Resource{ + + CreateContext: resourceForemanComputeprofileCreate, + ReadContext: resourceForemanComputeprofileRead, + UpdateContext: resourceForemanComputeprofileUpdate, + DeleteContext: resourceForemanComputeprofileDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + autodoc.MetaAttribute: { + Type: schema.TypeBool, + Computed: true, + Description: fmt.Sprintf( + "%s Foreman representation of a compute profile.", + autodoc.MetaSummary, + ), + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the compute profile", + }, + "compute_attributes": { + Type: schema.TypeList, + Required: true, + Description: "List of compute attributes", + Elem: resourceForemanComputeAttribute(), + }, + }, + } +} + +func resourceForemanComputeAttribute() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeInt, + Computed: true, + Description: "ID of the compute_attribute", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "Auto-generated name of the compute attribute", + }, + "compute_resource_id": { + Type: schema.TypeInt, + Required: true, + Description: "ID of the compute resource", + }, + "vm_attrs": { + Type: schema.TypeMap, + Required: false, + Optional: true, + Computed: true, + Description: "VM attributes as JSON", + }, + }, + } +} + +// buildForemanComputeProfile constructs a ForemanComputeProfile reference from a +// resource data reference. The struct's members are populated from the data +// populated in the resource data. Missing members will be left to the zero +// value for that member's type. +func buildForemanComputeProfile(d *schema.ResourceData) *api.ForemanComputeProfile { + log.Tracef("foreman/resource_foreman_computeprofile.go#buildForemanComputeProfile") + + t := api.ForemanComputeProfile{} + obj := buildForemanObject(d) + t.ForemanObject = *obj + + caList := d.Get("compute_attributes").([]interface{}) + var compattrObjList []*api.ForemanComputeAttribute + + for i := 0; i < len(caList); i++ { + ca := caList[i].(map[string]interface{}) + caObj := api.ForemanComputeAttribute{} + + data, err := json.Marshal(ca) + if err != nil { + return nil + } + + err = json.Unmarshal(data, &caObj) + if err != nil { + log.Warningf("Error during json.Unmarshal: %s", err) + return nil + } + + log.Debugf("buildForemanComputeProfile caObj: [%+v]", caObj) + + compattrObjList = append(compattrObjList, &caObj) + } + + t.ComputeAttributes = compattrObjList + return &t +} + +// setResourceDataFromForemanComputeProfile sets a ResourceData's attributes from +// the attributes of the supplied ForemanComputeProfile reference +func setResourceDataFromForemanComputeProfile(d *schema.ResourceData, fk *api.ForemanComputeProfile) { + log.Tracef("foreman/resource_foreman_computeprofile.go#setResourceDataFromForemanComputeProfile") + + d.SetId(strconv.Itoa(fk.Id)) + + err := d.Set("name", fk.Name) + if err != nil { + log.Errorf("Error in d.Set: %s", err) + } + + var caList []map[string]interface{} + + for i := 0; i < len(fk.ComputeAttributes); i++ { + elem := fk.ComputeAttributes[i] + log.Debugf("elem: %+v", elem) + + data, err := json.Marshal(&elem) + if err != nil { + log.Errorf("Error in json.Marshal: %s", err) + } + + var unmarshElem map[string]interface{} + err = json.Unmarshal(data, &unmarshElem) + if err != nil { + log.Errorf("Error in json.Unmarshal: %s", err) + } + + log.Debugf("unmarshElem: %+v", unmarshElem) + caList = append(caList, unmarshElem) + } + + err = d.Set("compute_attributes", caList) + if err != nil { + log.Errorf("Error in setting compute_attributes: %s", err) + } +} + +func resourceForemanComputeprofileCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + log.Tracef("foreman/resource_foreman_computeprofile.go#resourceForemanComputeprofileCreate") + + client := meta.(*api.Client) + p := buildForemanComputeProfile(d) + + createdComputeprofile, createErr := client.CreateComputeprofile(ctx, p) + if createErr != nil { + return diag.FromErr(createErr) + } + + log.Debugf("Created ForemanComputeprofile [%+v]", createdComputeprofile) + + setResourceDataFromForemanComputeProfile(d, createdComputeprofile) + + return nil +} + +func resourceForemanComputeprofileRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + log.Tracef("foreman/resource_foreman_computeprofile.go#resourceForemanComputeprofileRead") + + client := meta.(*api.Client) + p := buildForemanComputeProfile(d) + + cp, err := client.ReadComputeProfile(ctx, p.Id) + if err != nil { + return diag.FromErr(err) + } + + log.Debugf("Read compute_profile: %+v", cp) + + setResourceDataFromForemanComputeProfile(d, cp) + + return nil +} + +func resourceForemanComputeprofileUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + log.Tracef("foreman/resource_foreman_computeprofile.go#resourceForemanComputeprofileUpdate") + + client := meta.(*api.Client) + p := buildForemanComputeProfile(d) + + cp, err := client.UpdateComputeProfile(ctx, p) + if err != nil { + return diag.FromErr(err) + } + + log.Debugf("Update compute_profile: %+v", cp) + + setResourceDataFromForemanComputeProfile(d, cp) + + return nil +} + +func resourceForemanComputeprofileDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + log.Tracef("foreman/resource_foreman_computeprofile.go#resourceForemanComputeprofileDelete") + + client := meta.(*api.Client) + p := buildForemanComputeProfile(d) + + err := client.DeleteComputeProfile(ctx, p.Id) + if err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/mkdocs.yml b/mkdocs.yml index 19f473f6..0b425be0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -53,6 +53,7 @@ nav: - 'foreman_usergroup': 'data-sources/foreman_usergroup.md' - Resources: - 'foreman_architecture': 'resources/foreman_architecture.md' + - 'foreman_computeprofile': 'resources/foreman_computeprofile.md' - 'foreman_computeresource': 'resources/foreman_computeresource.md' - 'foreman_defaulttemplate': 'resources/foreman_defaulttemplate.md' - 'foreman_domain': 'resources/foreman_domain.md'