Skip to content

Commit

Permalink
Add labels to Agent Configuration (#945)
Browse files Browse the repository at this point in the history
  • Loading branch information
oliveromahony authored Jan 17, 2025
1 parent db82ebd commit 406a927
Show file tree
Hide file tree
Showing 11 changed files with 553 additions and 5 deletions.
2 changes: 1 addition & 1 deletion api/grpc/mpi/v1/command.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion api/grpc/mpi/v1/common.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion api/grpc/mpi/v1/files.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions api/grpc/mpi/v1/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) F5, Inc.
//
// This source code is licensed under the Apache License, Version 2.0 license found in the
// LICENSE file in the root directory of this source tree.

package v1

import (
"google.golang.org/protobuf/types/known/structpb"
)

// ConvertToStructs converts a map[string]any into a slice of *structpb.Struct.
// Each key-value pair in the input map is converted into a *structpb.Struct,
// where the key is used as the field name, and the value is added to the Struct.
//
// Parameters:
// - input: A map[string]any containing key-value pairs to be converted.
//
// Returns:
// - []*structpb.Struct: A slice of *structpb.Struct, where each map entry is converted into a struct.
// - error: An error if any value in the input map cannot be converted into a *structpb.Struct.
//
// Example:
//
// input := map[string]any{
// "key1": "value1",
// "key2": 123,
// "key3": true,
// }
// structs, err := ConvertToStructs(input)
// // structs will contain a slice of *structpb.Struct
// // err will be nil if all conversions succeed.
func ConvertToStructs(input map[string]any) ([]*structpb.Struct, error) {
structs := []*structpb.Struct{}
for key, value := range input {
// Convert each value in the map to *structpb.Struct
structValue, err := structpb.NewStruct(map[string]any{
key: value,
})
if err != nil {
return structs, err
}
structs = append(structs, structValue)
}

return structs, nil
}
72 changes: 72 additions & 0 deletions api/grpc/mpi/v1/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) F5, Inc.
//
// This source code is licensed under the Apache License, Version 2.0 license found in the
// LICENSE file in the root directory of this source tree.

package v1

import (
"testing"

"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/structpb"
)

func TestConvertToStructs(t *testing.T) {
tests := []struct {
name string
input map[string]any
expected []*structpb.Struct
wantErr bool
}{
{
name: "Test 1: Valid input with simple key-value pairs",
input: map[string]any{
"key1": "value1",
"key2": 123,
"key3": true,
},
expected: []*structpb.Struct{
{
Fields: map[string]*structpb.Value{
"key1": structpb.NewStringValue("value1"),
},
},
{
Fields: map[string]*structpb.Value{
"key2": structpb.NewNumberValue(123),
},
},
{
Fields: map[string]*structpb.Value{
"key3": structpb.NewBoolValue(true),
},
},
},
wantErr: false,
},
{
name: "Test 2: Empty input map",
input: make(map[string]any),
expected: []*structpb.Struct{},
wantErr: false,
},
{
name: "Test 3: Invalid input type",
input: map[string]any{
"key1": func() {}, // Unsupported type
},
expected: []*structpb.Struct{},
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ConvertToStructs(tt.input)

assert.Equal(t, tt.expected, got)
assert.Equal(t, tt.wantErr, err != nil)
})
}
}
109 changes: 109 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ package config

import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"os"
"path/filepath"
"slices"
"strconv"
"strings"

selfsignedcerts "github.com/nginx/agent/v3/pkg/tls"
Expand All @@ -26,6 +28,7 @@ const (
ConfigFileName = "nginx-agent.conf"
EnvPrefix = "NGINX_AGENT"
KeyDelimiter = "_"
KeyValueNumber = 2
)

var viperInstance = viper.NewWithOptions(viper.KeyDelimiter(KeyDelimiter))
Expand Down Expand Up @@ -102,6 +105,7 @@ func ResolveConfig() (*Config, error) {
Command: resolveCommand(),
Watchers: resolveWatchers(),
Features: viperInstance.GetStringSlice(FeaturesKey),
Labels: resolveLabels(),
}

slog.Debug("Agent config", "config", config)
Expand Down Expand Up @@ -181,6 +185,7 @@ func registerFlags() {
"A comma-separated list of features enabled for the agent.",
)

registerCommonFlags(fs)
registerCommandFlags(fs)
registerCollectorFlags(fs)
registerClientFlags(fs)
Expand All @@ -198,6 +203,14 @@ func registerFlags() {
})
}

func registerCommonFlags(fs *flag.FlagSet) {
fs.StringToString(
LabelsRootKey,
DefaultLabels(),
"A list of labels associated with these instances",
)
}

func registerClientFlags(fs *flag.FlagSet) {
// HTTP Flags
fs.Duration(
Expand Down Expand Up @@ -456,6 +469,102 @@ func resolveLog() *Log {
}
}

func resolveLabels() map[string]interface{} {
input := viperInstance.GetStringMapString(LabelsRootKey)

// Parsing the environment variable for labels needs to be done differently
// by parsing the environment variable as a string.
envLabels := resolveEnvironmentVariableLabels()

if len(envLabels) > 0 {
input = envLabels
}

result := make(map[string]interface{})

for key, value := range input {
trimmedKey := strings.TrimSpace(key)
trimmedValue := strings.TrimSpace(value)

switch {
case trimmedValue == "" || trimmedValue == "nil": // Handle empty values as nil
result[trimmedKey] = nil

case parseInt(trimmedValue) != nil: // Integer
result[trimmedKey] = parseInt(trimmedValue)

case parseFloat(trimmedValue) != nil: // Float
result[trimmedKey] = parseFloat(trimmedValue)

case parseBool(trimmedValue) != nil: // Boolean
result[trimmedKey] = parseBool(trimmedValue)

case parseJSON(trimmedValue) != nil: // JSON object/array
result[trimmedKey] = parseJSON(trimmedValue)

default: // String
result[trimmedKey] = trimmedValue
}
}

slog.Info("Configured labels", "labels", result)

return result
}

func resolveEnvironmentVariableLabels() map[string]string {
envLabels := make(map[string]string)
envInput := viperInstance.GetString(LabelsRootKey)

labels := strings.Split(envInput, ",")
if len(labels) > 0 && labels[0] != "" {
for _, label := range labels {
splitLabel := strings.Split(label, "=")
if len(splitLabel) == KeyValueNumber {
envLabels[splitLabel[0]] = splitLabel[1]
} else {
slog.Warn("Unable to parse label: " + label)
}
}
}

return envLabels
}

// Parsing helper functions return the parsed value or nil if parsing fails
func parseInt(value string) interface{} {
if intValue, err := strconv.Atoi(value); err == nil {
return intValue
}

return nil
}

func parseFloat(value string) interface{} {
if floatValue, err := strconv.ParseFloat(value, 64); err == nil {
return floatValue
}

return nil
}

func parseBool(value string) interface{} {
if boolValue, err := strconv.ParseBool(value); err == nil {
return boolValue
}

return nil
}

func parseJSON(value string) interface{} {
var jsonValue interface{}
if err := json.Unmarshal([]byte(value), &jsonValue); err == nil {
return jsonValue
}

return nil
}

func resolveDataPlaneConfig() *DataPlaneConfig {
return &DataPlaneConfig{
Nginx: &NginxDataPlaneConfig{
Expand Down
Loading

0 comments on commit 406a927

Please sign in to comment.