Skip to content

Commit 5b594b5

Browse files
committed
Merge branch 'v3' into add-nginx-plus-api-actions
2 parents 0e03b60 + 406a927 commit 5b594b5

File tree

14 files changed

+763
-181
lines changed

14 files changed

+763
-181
lines changed

api/grpc/mpi/v1/files.pb.go

Lines changed: 188 additions & 178 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/grpc/mpi/v1/files.pb.validate.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/grpc/mpi/v1/files.proto

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ message File {
9292
}
9393
// Optional action
9494
optional FileAction action = 2;
95+
// Unmanaged files will not be modified
96+
bool unmanaged = 3;
9597
}
9698

9799
// Represents the get file request
@@ -290,4 +292,4 @@ message AttributeTypeAndValue {
290292

291293
// The value associated with the attribute.
292294
string value = 2 [(buf.validate.field).string.min_len = 1];
293-
}
295+
}

api/grpc/mpi/v1/helpers.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (c) F5, Inc.
2+
//
3+
// This source code is licensed under the Apache License, Version 2.0 license found in the
4+
// LICENSE file in the root directory of this source tree.
5+
6+
package v1
7+
8+
import (
9+
"google.golang.org/protobuf/types/known/structpb"
10+
)
11+
12+
// ConvertToStructs converts a map[string]any into a slice of *structpb.Struct.
13+
// Each key-value pair in the input map is converted into a *structpb.Struct,
14+
// where the key is used as the field name, and the value is added to the Struct.
15+
//
16+
// Parameters:
17+
// - input: A map[string]any containing key-value pairs to be converted.
18+
//
19+
// Returns:
20+
// - []*structpb.Struct: A slice of *structpb.Struct, where each map entry is converted into a struct.
21+
// - error: An error if any value in the input map cannot be converted into a *structpb.Struct.
22+
//
23+
// Example:
24+
//
25+
// input := map[string]any{
26+
// "key1": "value1",
27+
// "key2": 123,
28+
// "key3": true,
29+
// }
30+
// structs, err := ConvertToStructs(input)
31+
// // structs will contain a slice of *structpb.Struct
32+
// // err will be nil if all conversions succeed.
33+
func ConvertToStructs(input map[string]any) ([]*structpb.Struct, error) {
34+
structs := []*structpb.Struct{}
35+
for key, value := range input {
36+
// Convert each value in the map to *structpb.Struct
37+
structValue, err := structpb.NewStruct(map[string]any{
38+
key: value,
39+
})
40+
if err != nil {
41+
return structs, err
42+
}
43+
structs = append(structs, structValue)
44+
}
45+
46+
return structs, nil
47+
}

api/grpc/mpi/v1/helpers_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright (c) F5, Inc.
2+
//
3+
// This source code is licensed under the Apache License, Version 2.0 license found in the
4+
// LICENSE file in the root directory of this source tree.
5+
6+
package v1
7+
8+
import (
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
"google.golang.org/protobuf/types/known/structpb"
13+
)
14+
15+
func TestConvertToStructs(t *testing.T) {
16+
tests := []struct {
17+
name string
18+
input map[string]any
19+
expected []*structpb.Struct
20+
wantErr bool
21+
}{
22+
{
23+
name: "Test 1: Valid input with simple key-value pairs",
24+
input: map[string]any{
25+
"key1": "value1",
26+
"key2": 123,
27+
"key3": true,
28+
},
29+
expected: []*structpb.Struct{
30+
{
31+
Fields: map[string]*structpb.Value{
32+
"key1": structpb.NewStringValue("value1"),
33+
},
34+
},
35+
{
36+
Fields: map[string]*structpb.Value{
37+
"key2": structpb.NewNumberValue(123),
38+
},
39+
},
40+
{
41+
Fields: map[string]*structpb.Value{
42+
"key3": structpb.NewBoolValue(true),
43+
},
44+
},
45+
},
46+
wantErr: false,
47+
},
48+
{
49+
name: "Test 2: Empty input map",
50+
input: make(map[string]any),
51+
expected: []*structpb.Struct{},
52+
wantErr: false,
53+
},
54+
{
55+
name: "Test 3: Invalid input type",
56+
input: map[string]any{
57+
"key1": func() {}, // Unsupported type
58+
},
59+
expected: []*structpb.Struct{},
60+
wantErr: true,
61+
},
62+
}
63+
64+
for _, tt := range tests {
65+
t.Run(tt.name, func(t *testing.T) {
66+
got, err := ConvertToStructs(tt.input)
67+
68+
assert.Equal(t, tt.expected, got)
69+
assert.Equal(t, tt.wantErr, err != nil)
70+
})
71+
}
72+
}

docs/proto/protos.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ Represents meta data about a file
301301
| ----- | ---- | ----- | ----------- |
302302
| file_meta | [FileMeta](#mpi-v1-FileMeta) | | Meta information about the file, the name (including path) and hash |
303303
| action | [File.FileAction](#mpi-v1-File-FileAction) | optional | Optional action |
304+
| unmanaged | [bool](#bool) | | Unmanaged files will not be modified |
304305

305306

306307

internal/config/config.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ package config
77

88
import (
99
"context"
10+
"encoding/json"
1011
"errors"
1112
"fmt"
1213
"log/slog"
1314
"os"
1415
"path/filepath"
1516
"slices"
17+
"strconv"
1618
"strings"
1719

1820
selfsignedcerts "github.com/nginx/agent/v3/pkg/tls"
@@ -26,6 +28,7 @@ const (
2628
ConfigFileName = "nginx-agent.conf"
2729
EnvPrefix = "NGINX_AGENT"
2830
KeyDelimiter = "_"
31+
KeyValueNumber = 2
2932
)
3033

3134
var viperInstance = viper.NewWithOptions(viper.KeyDelimiter(KeyDelimiter))
@@ -102,6 +105,7 @@ func ResolveConfig() (*Config, error) {
102105
Command: resolveCommand(),
103106
Watchers: resolveWatchers(),
104107
Features: viperInstance.GetStringSlice(FeaturesKey),
108+
Labels: resolveLabels(),
105109
}
106110

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

188+
registerCommonFlags(fs)
184189
registerCommandFlags(fs)
185190
registerCollectorFlags(fs)
186191
registerClientFlags(fs)
@@ -198,6 +203,14 @@ func registerFlags() {
198203
})
199204
}
200205

206+
func registerCommonFlags(fs *flag.FlagSet) {
207+
fs.StringToString(
208+
LabelsRootKey,
209+
DefaultLabels(),
210+
"A list of labels associated with these instances",
211+
)
212+
}
213+
201214
func registerClientFlags(fs *flag.FlagSet) {
202215
// HTTP Flags
203216
fs.Duration(
@@ -456,6 +469,102 @@ func resolveLog() *Log {
456469
}
457470
}
458471

472+
func resolveLabels() map[string]interface{} {
473+
input := viperInstance.GetStringMapString(LabelsRootKey)
474+
475+
// Parsing the environment variable for labels needs to be done differently
476+
// by parsing the environment variable as a string.
477+
envLabels := resolveEnvironmentVariableLabels()
478+
479+
if len(envLabels) > 0 {
480+
input = envLabels
481+
}
482+
483+
result := make(map[string]interface{})
484+
485+
for key, value := range input {
486+
trimmedKey := strings.TrimSpace(key)
487+
trimmedValue := strings.TrimSpace(value)
488+
489+
switch {
490+
case trimmedValue == "" || trimmedValue == "nil": // Handle empty values as nil
491+
result[trimmedKey] = nil
492+
493+
case parseInt(trimmedValue) != nil: // Integer
494+
result[trimmedKey] = parseInt(trimmedValue)
495+
496+
case parseFloat(trimmedValue) != nil: // Float
497+
result[trimmedKey] = parseFloat(trimmedValue)
498+
499+
case parseBool(trimmedValue) != nil: // Boolean
500+
result[trimmedKey] = parseBool(trimmedValue)
501+
502+
case parseJSON(trimmedValue) != nil: // JSON object/array
503+
result[trimmedKey] = parseJSON(trimmedValue)
504+
505+
default: // String
506+
result[trimmedKey] = trimmedValue
507+
}
508+
}
509+
510+
slog.Info("Configured labels", "labels", result)
511+
512+
return result
513+
}
514+
515+
func resolveEnvironmentVariableLabels() map[string]string {
516+
envLabels := make(map[string]string)
517+
envInput := viperInstance.GetString(LabelsRootKey)
518+
519+
labels := strings.Split(envInput, ",")
520+
if len(labels) > 0 && labels[0] != "" {
521+
for _, label := range labels {
522+
splitLabel := strings.Split(label, "=")
523+
if len(splitLabel) == KeyValueNumber {
524+
envLabels[splitLabel[0]] = splitLabel[1]
525+
} else {
526+
slog.Warn("Unable to parse label: " + label)
527+
}
528+
}
529+
}
530+
531+
return envLabels
532+
}
533+
534+
// Parsing helper functions return the parsed value or nil if parsing fails
535+
func parseInt(value string) interface{} {
536+
if intValue, err := strconv.Atoi(value); err == nil {
537+
return intValue
538+
}
539+
540+
return nil
541+
}
542+
543+
func parseFloat(value string) interface{} {
544+
if floatValue, err := strconv.ParseFloat(value, 64); err == nil {
545+
return floatValue
546+
}
547+
548+
return nil
549+
}
550+
551+
func parseBool(value string) interface{} {
552+
if boolValue, err := strconv.ParseBool(value); err == nil {
553+
return boolValue
554+
}
555+
556+
return nil
557+
}
558+
559+
func parseJSON(value string) interface{} {
560+
var jsonValue interface{}
561+
if err := json.Unmarshal([]byte(value), &jsonValue); err == nil {
562+
return jsonValue
563+
}
564+
565+
return nil
566+
}
567+
459568
func resolveDataPlaneConfig() *DataPlaneConfig {
460569
return &DataPlaneConfig{
461570
Nginx: &NginxDataPlaneConfig{

0 commit comments

Comments
 (0)