Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,4 @@ The following environment variables can be configured in the rest-dynamic-contro
| REST_CONTROLLER_MIN_ERROR_RETRY_INTERVAL | The minimum interval between retries when an error occurs. This should be less than max-error-retry-interval. | `1s` |
| REST_CONTROLLER_MAX_ERROR_RETRIES | How many times to retry the processing of a resource when an error occurs before giving up and dropping the resource. | `5` |
| REST_CONTROLLER_METRICS_SERVER_PORT | The port where the metrics server will be listening. If not set, the metrics server is disabled. | |
| REST_CONTROLLER_IDENTIFIER_MATCH_POLICY | Policy to match identifier fields when checking if a remote resource exists. Possible values are "AND" (all fields must match) and "OR" (at least one field must match). | `OR` |
52 changes: 30 additions & 22 deletions internal/controllers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package restResources
import (
"fmt"
"math"
"strings"

"github.com/krateoplatformops/rest-dynamic-controller/internal/tools/comparison"
getter "github.com/krateoplatformops/rest-dynamic-controller/internal/tools/definitiongetter"
Expand Down Expand Up @@ -35,8 +36,9 @@ func isCRUpdated(mg *unstructured.Unstructured, rm map[string]interface{}) (comp
return comparison.CompareExisting(m, rm)
}

// populateStatusFields populates the status fields in the mg object with the values from the body
// It checks both the `identifiers` and `additionalStatusFields` defined in the resource
// populateStatusFields populates the status fields in the mg object with the values from the response body of the API call.
// It supports dot notation for nested fields and performs necessary type conversions.
// It uses the identifiers and additionalStatusFields from the clientInfo to determine which fields to populate.
func populateStatusFields(clientInfo *getter.Info, mg *unstructured.Unstructured, body map[string]interface{}) error {
// Handle nil inputs
if mg == nil {
Expand All @@ -49,34 +51,40 @@ func populateStatusFields(clientInfo *getter.Info, mg *unstructured.Unstructured
return nil // Nothing to populate, but not an error
}

// Combine identifiers and additionalStatusFields into one list.
allFields := append(clientInfo.Resource.Identifiers, clientInfo.Resource.AdditionalStatusFields...)
// Early return if no fields to populate
if len(clientInfo.Resource.Identifiers) == 0 && len(clientInfo.Resource.AdditionalStatusFields) == 0 {
if len(allFields) == 0 {
return nil
}

// Create a set of all fields we need to look for
fieldsToPopulate := make(map[string]struct{})
for _, fieldName := range allFields {
//log.Printf("Managing field: %s", fieldName)
// Split the field name by '.' to handle nested paths.
path := strings.Split(fieldName, ".")
//log.Printf("Field path split: %v", path)

// Add identifiers to the set
for _, identifier := range clientInfo.Resource.Identifiers {
fieldsToPopulate[identifier] = struct{}{}
}

// Add additionalStatusFields to the set
for _, additionalField := range clientInfo.Resource.AdditionalStatusFields {
fieldsToPopulate[additionalField] = struct{}{}
}
// Extract the raw value from the response body without copying.
value, found, err := unstructured.NestedFieldNoCopy(body, path...)
if err != nil || !found {
// An error here means the path was invalid or not found.
// We can safely continue to the next field.
//log.Printf("Field '%s' not found in response body or error occurred: %v", fieldName, err)
continue
}
//log.Printf("Extracted value for field '%s': %v", fieldName, value)

// Single pass through the body map
for k, v := range body {
if _, exists := fieldsToPopulate[k]; exists {
// Convert the value to a format that unstructured can handle
convertedValue := deepCopyJSONValue(v)
// Perform deep copy and type conversions (e.g., float64 to int64).
convertedValue := deepCopyJSONValue(value)
//log.Printf("Converted value for field '%s': %v", fieldName, convertedValue)

if err := unstructured.SetNestedField(mg.Object, convertedValue, "status", k); err != nil {
return fmt.Errorf("setting nested field '%s' in status: %w", k, err)
}
// The destination path in the status should mirror the source path.
statusPath := append([]string{"status"}, path...)
//log.Printf("Setting value for field '%s' at status path: %v", fieldName, statusPath)
if err := unstructured.SetNestedField(mg.Object, convertedValue, statusPath...); err != nil {
return fmt.Errorf("setting nested field '%s' in status: %w", fieldName, err)
}
//log.Printf("Successfully set field '%s' with value: %v at path: %v", fieldName, convertedValue, statusPath)
}

return nil
Expand Down
274 changes: 228 additions & 46 deletions internal/controllers/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,232 @@ func TestPopulateStatusFields(t *testing.T) {
},
},
},
{
name: "nested identifiers and additional status fields",
clientInfo: &getter.Info{
Resource: getter.Resource{
Identifiers: []string{"metadata.uid"},
AdditionalStatusFields: []string{"spec.host", "status.phase"},
},
},
mg: &unstructured.Unstructured{
Object: map[string]interface{}{},
},
body: map[string]interface{}{
"metadata": map[string]interface{}{
"uid": "xyz-123",
},
"spec": map[string]interface{}{
"host": "example.com",
},
"status": map[string]interface{}{
"phase": "Running",
},
},
wantErr: false,
expected: map[string]interface{}{
"status": map[string]interface{}{
"metadata": map[string]interface{}{
"uid": "xyz-123",
},
"spec": map[string]interface{}{
"host": "example.com",
},
"status": map[string]interface{}{
"phase": "Running",
},
},
},
},
{
name: "mixed top-level and nested fields",
clientInfo: &getter.Info{
Resource: getter.Resource{
Identifiers: []string{"id"},
AdditionalStatusFields: []string{"metadata.name"},
},
},
mg: &unstructured.Unstructured{
Object: map[string]interface{}{},
},
body: map[string]interface{}{
"id": "abc-456",
"metadata": map[string]interface{}{
"name": "my-resource",
},
},
wantErr: false,
expected: map[string]interface{}{
"status": map[string]interface{}{
"id": "abc-456",
"metadata": map[string]interface{}{
"name": "my-resource",
},
},
},
},
{
name: "nested field not found in body",
clientInfo: &getter.Info{
Resource: getter.Resource{
AdditionalStatusFields: []string{"spec.nonexistent"},
},
},
mg: &unstructured.Unstructured{
Object: map[string]interface{}{},
},
body: map[string]interface{}{
"spec": map[string]interface{}{
"host": "example.com",
},
},
wantErr: false,
expected: map[string]interface{}{},
},
{
name: "complex nested object",
clientInfo: &getter.Info{
Resource: getter.Resource{
AdditionalStatusFields: []string{"data.config.spec"},
},
},
mg: &unstructured.Unstructured{
Object: map[string]interface{}{},
},
body: map[string]interface{}{
"data": map[string]interface{}{
"config": map[string]interface{}{
"spec": map[string]interface{}{
"key": "value",
"num": float64(123),
},
},
},
},
wantErr: false,
expected: map[string]interface{}{
"status": map[string]interface{}{
"data": map[string]interface{}{
"config": map[string]interface{}{
"spec": map[string]interface{}{
"key": "value",
"num": int64(123),
},
},
},
},
},
},
{
name: "slice of strings",
clientInfo: &getter.Info{
Resource: getter.Resource{
AdditionalStatusFields: []string{"spec.tags"},
},
},
mg: &unstructured.Unstructured{
Object: map[string]interface{}{},
},
body: map[string]interface{}{
"spec": map[string]interface{}{
"tags": []interface{}{"tag1", "tag2"},
},
},
wantErr: false,
expected: map[string]interface{}{
"status": map[string]interface{}{
"spec": map[string]interface{}{
"tags": []interface{}{"tag1", "tag2"},
},
},
},
},
{
name: "slice of objects with mixed numeric types",
clientInfo: &getter.Info{
Resource: getter.Resource{
AdditionalStatusFields: []string{"spec.ports"},
},
},
mg: &unstructured.Unstructured{
Object: map[string]interface{}{},
},
body: map[string]interface{}{
"spec": map[string]interface{}{
"ports": []interface{}{
map[string]interface{}{"name": "http", "port": 80},
map[string]interface{}{"name": "https", "port": float32(443)},
},
},
},
wantErr: false,
expected: map[string]interface{}{
"status": map[string]interface{}{
"spec": map[string]interface{}{
"ports": []interface{}{
map[string]interface{}{"name": "http", "port": int64(80)},
map[string]interface{}{"name": "https", "port": int64(443)},
},
},
},
},
},
{
name: "object with nil value",
clientInfo: &getter.Info{
Resource: getter.Resource{
AdditionalStatusFields: []string{"config"},
},
},
mg: &unstructured.Unstructured{
Object: map[string]interface{}{},
},
body: map[string]interface{}{
"config": map[string]interface{}{
"settingA": "valueA",
"settingB": nil,
},
},
wantErr: false,
expected: map[string]interface{}{
"status": map[string]interface{}{
"config": map[string]interface{}{
"settingA": "valueA",
"settingB": nil,
},
},
},
},
{
name: "slice of objects with mixed numeric types",
clientInfo: &getter.Info{
Resource: getter.Resource{
AdditionalStatusFields: []string{"spec.ports"},
},
},
mg: &unstructured.Unstructured{
Object: map[string]interface{}{},
},
body: map[string]interface{}{
"spec": map[string]interface{}{
"ports": []interface{}{
map[string]interface{}{"name": "http", "port": 80},
map[string]interface{}{"name": "https", "port": float32(443)},
},
},
},
wantErr: false,
expected: map[string]interface{}{
"status": map[string]interface{}{
"spec": map[string]interface{}{
"ports": []interface{}{
map[string]interface{}{"name": "http", "port": int64(80)},
map[string]interface{}{"name": "https", "port": int64(443)},
},
},
},
},
},
}

for _, tt := range tests {
Expand All @@ -542,52 +768,8 @@ func TestPopulateStatusFields(t *testing.T) {
}

if !tt.wantErr {
// Validate the results
if len(tt.expected) == 0 {
// No status should be created or should be empty
status, exists, _ := unstructured.NestedMap(tt.mg.Object, "status")
if exists && len(status) > 0 {
// Check if there were pre-existing status fields ("existing" field in status)
hasPreExisting := false
for _, test := range tests {
if test.name == tt.name {
if statusObj, ok := test.mg.Object["status"]; ok {
if statusMap, ok := statusObj.(map[string]interface{}); ok && len(statusMap) > 0 {
hasPreExisting = true
break
}
}
}
}
if !hasPreExisting {
t.Errorf("populateStatusFields() unexpected status field created: %v while expected is empty", status)
}
}
} else {
// Validate expected status fields
status, exists, _ := unstructured.NestedMap(tt.mg.Object, "status")
if !exists {
t.Errorf("populateStatusFields() status field not created while length of expected is %d", len(tt.expected))
return
}

expectedStatus := tt.expected["status"].(map[string]interface{})

// Check that all expected fields are present with correct values
for k, expectedVal := range expectedStatus {
if actualVal, ok := status[k]; !ok {
t.Errorf("populateStatusFields() status.%s not found", k)
} else if actualVal != expectedVal {
t.Errorf("populateStatusFields() status.%s = %v, want %v", k, actualVal, expectedVal)
}
}

// For tests with existing status, ensure they're preserved
if tt.name == "existing status fields should be preserved" {
if status["existing"] != "existingValue" {
t.Errorf("populateStatusFields() existing status field not preserved")
}
}
if diff := cmp.Diff(tt.expected, tt.mg.Object); diff != "" {
t.Errorf("populateStatusFields() mismatch (-want +got):\n%s", diff)
}
}
})
Expand Down
Loading