Skip to content

Commit

Permalink
Implement Katello lifecycle environments resource and data source (#149)
Browse files Browse the repository at this point in the history
Adds the "lifecycle environment" resources from Katello to Terraform, as
both resource and data source.

Tested on Katello 4.9: data source READ, resource CREATE/READ/UPDATE/DELETE.

The handling of API prefixes for Katello in client.go might conflict
with other PRs, this code block should be refactored.

An example shows how the first lifecycle environment in a path can be
created from the Library root environment.

Resolves #141

Signed-off-by: Dominik Pataky <pataky@mindful-security.eu>
  • Loading branch information
bitkeks authored Jan 26, 2024
1 parent f5bb8ad commit 0befc51
Show file tree
Hide file tree
Showing 9 changed files with 609 additions and 58 deletions.
36 changes: 36 additions & 0 deletions docs/data-sources/foreman_katello_lifecycle_environment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

# foreman_katello_lifecycle_environment


Lifecycle environments group hosts into logical stages, example dev/test/prod.


## Example Usage

```
# Autogenerated example with required keys
data "foreman_katello_lifecycle_environment" "example" {
name = "Library"
}
```


## Argument Reference

The following arguments are supported:

- `name` - (Required) Name of the lifecycle environment.


## Attributes Reference

The following attributes are exported:

- `description` - Description for the lifecycle environment
- `label` - Label for the lifecycle environment. Cannot be changed after creation. By default set to the name, with underscores as spaces replacement.
- `library` - Specifies if this environment is the special 'Library' root environment.
- `name` - Name of the lifecycle environment.
- `organization_id` -
- `prior_id` - ID of the prior lifecycle environment. Use '1' to refer to the built-in 'Library' root environment.
- `successor_id` -

42 changes: 42 additions & 0 deletions docs/resources/foreman_katello_lifecycle_environment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@

# foreman_katello_lifecycle_environment


Lifecycle environments group hosts into logical stages, example dev/test/prod.


## Example Usage

```
# Autogenerated example with required keys
resource "foreman_katello_lifecycle_environment" "example" {
name = "My new env"
organization_id = 1
prior_id = data.foreman_katello_lifecycle_environment.library.id
}
```


## Argument Reference

The following arguments are supported:

- `description` - (Optional) Description for the lifecycle environment
- `label` - (Optional, Force New) Label for the lifecycle environment. Cannot be changed after creation. By default set to the name, with underscores as spaces replacement.
- `name` - (Required) Name of the lifecycle environment.
- `organization_id` - (Required)
- `prior_id` - (Required) ID of the prior lifecycle environment. Use '1' to refer to the built-in 'Library' root environment.


## Attributes Reference

The following attributes are exported:

- `description` - Description for the lifecycle environment
- `label` - Label for the lifecycle environment. Cannot be changed after creation. By default set to the name, with underscores as spaces replacement.
- `library` - Specifies if this environment is the special 'Library' root environment.
- `name` - Name of the lifecycle environment.
- `organization_id` -
- `prior_id` - ID of the prior lifecycle environment. Use '1' to refer to the built-in 'Library' root environment.
- `successor_id` -

11 changes: 11 additions & 0 deletions examples/lifecycle_environment/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Get the root 'Library' environment as data source
data "foreman_katello_lifecycle_environment" "library" {
name = "Library"
}

// Then create a new lifecycle environment which uses the Library as the prior environment
resource "foreman_katello_lifecycle_environment" "newenv" {
name = "My new lifecycle env"
prior_id = data.foreman_katello_lifecycle_environment.library.id
organization_id = 1
}
2 changes: 2 additions & 0 deletions foreman/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ func (client *Client) NewRequestWithContext(ctx context.Context, method string,
// Check for katello endpoint
if strings.HasPrefix(endpoint, "katello") {
reqURL.Path = FOREMAN_KATELLO_API_URL_PREFIX + strings.TrimPrefix(endpoint, "katello")
} else if strings.HasPrefix(endpoint, "/katello/api") {
reqURL.Path = endpoint
} else if strings.HasPrefix(endpoint, "puppet") {
reqURL.Path = FOREMAN_PUPPET_API_URL_PREFIX + strings.TrimPrefix(endpoint, "puppet")
} else {
Expand Down
205 changes: 205 additions & 0 deletions foreman/api/katello_lifecycle_environments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package api

import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/terraform-coop/terraform-provider-foreman/foreman/utils"
"net/http"
)

const (
LifecycleEnvironmentEndpointPrefix = "/katello/api/environments"
LifecycleEnvironmentById = LifecycleEnvironmentEndpointPrefix + "/%d" // :id
LifecycleEnvironmentPathsByOrg = "/katello/api/organizations/%d/environments/paths" // :organization_id
)

type ContentViews struct {
Name string `json:"name"`
Id int `json:"id"`
}

type Organization struct {
Name string `json:"name"`
Label string `json:"label"`
Id int `json:"id"`
}

type LifecycleEnvironment struct {
ForemanObject

Label string `json:"label"`
Description string `json:"description"`
OrganizationId int `json:"organization_id"`
Organization Organization `json:"organization"`

// Is this LifecycleEnvironment a library?
Library bool `json:"library"`

// Container Image Registry related
RegistryNamePattern string `json:"registry_name_pattern"`
RegistryUnauthenticatedPull bool `json:"registry_unauthenticated_pull"`

Prior struct {
Name string `json:"name"`
Id int `json:"id"`
} `json:"prior"`

Successor struct {
Name string `json:"name"`
Id int `json:"id"`
} `json:"successor"`

Counts struct {
ContentHosts int `json:"content_hosts"`
ContentViews int `json:"content_views"`
} `json:"counts"`

ContentViews []ContentViews `json:"content_views"`
}

func (lce *LifecycleEnvironment) MarshalJSON() ([]byte, error) {
jsonMap := map[string]interface{}{
"id": lce.Id,
"name": lce.Name,
"description": lce.Description,
"organization_id": lce.OrganizationId,
"label": lce.Label,
"prior_id": lce.Prior.Id,
}
return json.Marshal(jsonMap)
}

func (c *Client) QueryLifecycleEnvironment(ctx context.Context, d *LifecycleEnvironment) (QueryResponse, error) {
utils.TraceFunctionCall()

queryResponse := QueryResponse{}

endpoint := LifecycleEnvironmentEndpointPrefix
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 []LifecycleEnvironment
resultsBytes, err := json.Marshal(queryResponse.Results)
if err != nil {
return queryResponse, err
}
err = json.Unmarshal(resultsBytes, &results)
if err != nil {
return queryResponse, err
}

// convert the search results from []ForemanImage to []interface
// and set the search results on the query
iArr := make([]interface{}, len(results))
for idx, val := range results {
iArr[idx] = val
}
queryResponse.Results = iArr

return queryResponse, nil
}

func (c *Client) CreateKatelloLifecycleEnvironment(ctx context.Context, lce *LifecycleEnvironment) (*LifecycleEnvironment, error) {
utils.TraceFunctionCall()

endpoint := LifecycleEnvironmentEndpointPrefix

jsonBytes, err := c.WrapJSONWithTaxonomy(nil, lce)
if err != nil {
return nil, err
}

req, err := c.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(jsonBytes))
if err != nil {
return nil, err
}

var createdLce LifecycleEnvironment
err = c.SendAndParse(req, &createdLce)
if err != nil {
return nil, err
}

utils.Debugf("createdLce: %+v", createdLce)

return &createdLce, nil
}

func (c *Client) ReadKatelloLifecycleEnvironment(ctx context.Context, d *LifecycleEnvironment) (*LifecycleEnvironment, error) {
utils.TraceFunctionCall()

reqEndpoint := fmt.Sprintf(LifecycleEnvironmentById, d.Id)
var lce LifecycleEnvironment

req, err := c.NewRequestWithContext(ctx, http.MethodGet, reqEndpoint, nil)
if err != nil {
return nil, err
}

err = c.SendAndParse(req, &lce)
if err != nil {
return nil, err
}

utils.Debugf("read LifecycleEnv: %+v", lce)

return &lce, nil
}

func (c *Client) UpdateKatelloLifecycleEnvironment(ctx context.Context, lce *LifecycleEnvironment) (*LifecycleEnvironment, error) {
utils.TraceFunctionCall()

endpoint := fmt.Sprintf(LifecycleEnvironmentById, lce.Id)

jsonBytes, err := c.WrapJSONWithTaxonomy(nil, lce)
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 updatedLce LifecycleEnvironment
err = c.SendAndParse(req, &updatedLce)
if err != nil {
return nil, err
}

utils.Debugf("updatedLce: %+v", updatedLce)

return &updatedLce, nil
}

func (c *Client) DeleteKatelloLifecycleEnvironment(ctx context.Context, id int) error {
utils.TraceFunctionCall()

endpoint := fmt.Sprintf(LifecycleEnvironmentById, id)

req, err := c.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil)
if err != nil {
return err
}

return c.SendAndParse(req, nil)
}
65 changes: 65 additions & 0 deletions foreman/data_source_foreman_katello_lifecycle_environment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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 dataSourceForemanKatelloLifecycleEnvironment() *schema.Resource {
r := resourceForemanKatelloLifecycleEnvironment()
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 lifecycle environment. %s \"Library\"", autodoc.MetaExample),
}

return &schema.Resource{
ReadContext: dataSourceForemanKatelloLifecycleRead,
Schema: ds,
}
}

func dataSourceForemanKatelloLifecycleRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
utils.TraceFunctionCall()

client := meta.(*api.Client)
lce := buildForemanKatelloLifecycleEnvironment(d)

utils.Debugf("lifecycle env: %+v", lce)

queryResponse, err := client.QueryLifecycleEnvironment(ctx, lce)
if err != nil {
return diag.FromErr(err)
}

if queryResponse.Subtotal == 0 {
return diag.Errorf("data source lifecycle_environment returned no results")
} else if queryResponse.Subtotal > 1 {
return diag.Errorf("data source lifecycle_environment returned more than 1 result")
}

if queryLce, ok := queryResponse.Results[0].(api.LifecycleEnvironment); !ok {
return diag.Errorf(
"data source results contain unexpected type. Expected "+
"[api.LifecycleEnvironment], got [%T]",
queryResponse.Results[0],
)
} else {
lce = &queryLce
}

utils.Debugf("lifecycle env: %+v", lce)

setResourceDataFromForemanKatelloLifecycleEnvironment(d, lce)

return nil
}
Loading

0 comments on commit 0befc51

Please sign in to comment.