Skip to content

Commit cf6521a

Browse files
authored
Add VersionCheck and Metadata to Agent labels (nginx#7737)
1 parent ab29ccc commit cf6521a

File tree

7 files changed

+342
-15
lines changed

7 files changed

+342
-15
lines changed

cmd/nginx-ingress/main.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/nginx/kubernetes-ingress/internal/k8s"
2727
"github.com/nginx/kubernetes-ingress/internal/k8s/secrets"
2828
license_reporting "github.com/nginx/kubernetes-ingress/internal/license_reporting"
29+
"github.com/nginx/kubernetes-ingress/internal/metadata"
2930
"github.com/nginx/kubernetes-ingress/internal/metrics"
3031
"github.com/nginx/kubernetes-ingress/internal/metrics/collectors"
3132
"github.com/nginx/kubernetes-ingress/internal/nginx"
@@ -128,7 +129,13 @@ func main() {
128129
licenseReporter = license_reporting.NewLicenseReporter(kubeClient, eventRecorder, pod)
129130
}
130131

131-
nginxManager, useFakeNginxManager := createNginxManager(ctx, managerCollector, licenseReporter)
132+
var deploymentMetadata *metadata.Metadata
133+
134+
if *agent {
135+
deploymentMetadata = metadata.NewMetadataReporter(kubeClient, pod, version)
136+
}
137+
138+
nginxManager, useFakeNginxManager := createNginxManager(ctx, managerCollector, licenseReporter, deploymentMetadata)
132139

133140
nginxVersion := getNginxVersionInfo(ctx, nginxManager)
134141

@@ -562,14 +569,14 @@ func createTemplateExecutors(ctx context.Context) (*version1.TemplateExecutor, *
562569
return templateExecutor, templateExecutorV2
563570
}
564571

565-
func createNginxManager(ctx context.Context, managerCollector collectors.ManagerCollector, licenseReporter *license_reporting.LicenseReporter) (nginx.Manager, bool) {
572+
func createNginxManager(ctx context.Context, managerCollector collectors.ManagerCollector, licenseReporter *license_reporting.LicenseReporter, deploymentMetadata *metadata.Metadata) (nginx.Manager, bool) {
566573
useFakeNginxManager := *proxyURL != ""
567574
var nginxManager nginx.Manager
568575
if useFakeNginxManager {
569576
nginxManager = nginx.NewFakeManager("/etc/nginx")
570577
} else {
571578
timeout := time.Duration(*nginxReloadTimeout) * time.Millisecond
572-
nginxManager = nginx.NewLocalManager(ctx, "/etc/nginx/", *nginxDebug, managerCollector, licenseReporter, timeout, *nginxPlus)
579+
nginxManager = nginx.NewLocalManager(ctx, "/etc/nginx/", *nginxDebug, managerCollector, licenseReporter, deploymentMetadata, timeout, *nginxPlus)
573580
}
574581
return nginxManager, useFakeNginxManager
575582
}

internal/common_cluster_info/common_cluster_info.go

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"k8s.io/client-go/kubernetes"
1010
)
1111

12-
// This file contains functions for data used in both product telemetry and license reporting
12+
// This file contains functions for data used in product telemetry, metadata and license reporting
1313

1414
// GetNodeCount returns the number of nodes in the cluster
1515
func GetNodeCount(ctx context.Context, client kubernetes.Interface) (int, error) {
@@ -43,10 +43,6 @@ func GetInstallationID(ctx context.Context, client kubernetes.Interface, podNSNa
4343
return "", err
4444
}
4545
podOwner := pod.GetOwnerReferences()
46-
if len(podOwner) != 1 {
47-
return "", fmt.Errorf("expected pod owner reference to be 1, got %d", len(podOwner))
48-
}
49-
5046
switch podOwner[0].Kind {
5147
case "ReplicaSet":
5248
rs, err := client.AppsV1().ReplicaSets(podNSName.Namespace).Get(ctx, podOwner[0].Name, metav1.GetOptions{})
@@ -61,6 +57,33 @@ func GetInstallationID(ctx context.Context, client kubernetes.Interface, podNSNa
6157
case "DaemonSet":
6258
return string(podOwner[0].UID), nil
6359
default:
64-
return "", fmt.Errorf("expected pod owner reference to be ReplicaSet or DeamonSet, got %s", podOwner[0].Kind)
60+
return string(podOwner[0].UID), nil
61+
}
62+
}
63+
64+
// GetDeploymentName returns the name of the Deployment
65+
func GetDeploymentName(ctx context.Context, client kubernetes.Interface, podNSName types.NamespacedName) (string, error) {
66+
pod, err := client.CoreV1().Pods(podNSName.Namespace).Get(ctx, podNSName.Name, metav1.GetOptions{})
67+
if err != nil {
68+
return "", err
69+
}
70+
owners := pod.GetOwnerReferences()
71+
owner := owners[0]
72+
switch owner.Kind {
73+
case "ReplicaSet":
74+
replicaSet, err := client.AppsV1().ReplicaSets(podNSName.Namespace).Get(ctx, owner.Name, metav1.GetOptions{})
75+
if err != nil {
76+
return "", err
77+
}
78+
for _, replicaSetOwner := range replicaSet.GetOwnerReferences() {
79+
if replicaSetOwner.Kind == "Deployment" {
80+
return replicaSetOwner.Name, nil
81+
}
82+
}
83+
return "", fmt.Errorf("replicaset %s has no owner", replicaSet.Name)
84+
case "DaemonSet":
85+
return owner.Name, nil
86+
default:
87+
return owner.Name, nil
6588
}
6689
}

internal/metadata/metadata.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package metadata
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
8+
clusterInfo "github.com/nginx/kubernetes-ingress/internal/common_cluster_info"
9+
api_v1 "k8s.io/api/core/v1"
10+
"k8s.io/apimachinery/pkg/types"
11+
"k8s.io/client-go/kubernetes"
12+
)
13+
14+
// Labels contains the metadata information needed for reporting to Agent
15+
type Labels struct {
16+
ProductName string `json:"product_name"`
17+
ProductVersion string `json:"product_version"`
18+
ClusterID string `json:"cluster_id"`
19+
DeploymentName string `json:"deployment_name"`
20+
DeploymentID string `json:"deployment_id"`
21+
DeploymentNamespace string `json:"deployment_namespace"`
22+
}
23+
24+
func newMetadataInfo(deploymentNamespace, clusterID, deploymentID, productVersion, deploymentName string) *Labels {
25+
return &Labels{
26+
ProductName: "nic",
27+
ProductVersion: productVersion,
28+
ClusterID: clusterID,
29+
DeploymentID: deploymentID,
30+
DeploymentName: deploymentName,
31+
DeploymentNamespace: deploymentNamespace,
32+
}
33+
}
34+
35+
// Metadata contains required information for metadata reporting
36+
type Metadata struct {
37+
K8sClientReader kubernetes.Interface
38+
PodNSName types.NamespacedName
39+
Pod *api_v1.Pod
40+
NICVersion string
41+
}
42+
43+
// NewMetadataReporter creates a new MetadataConfig
44+
func NewMetadataReporter(client kubernetes.Interface, pod *api_v1.Pod, version string) *Metadata {
45+
return &Metadata{
46+
K8sClientReader: client,
47+
PodNSName: types.NamespacedName{Namespace: os.Getenv("POD_NAMESPACE"), Name: os.Getenv("POD_NAME")},
48+
Pod: pod,
49+
NICVersion: version,
50+
}
51+
}
52+
53+
// CollectAndWrite collects the metadata information and returns a Labels struct
54+
func (md *Metadata) CollectAndWrite(ctx context.Context) (*Labels, error) {
55+
deploymentNamespace := md.PodNSName.Namespace
56+
clusterID, err := clusterInfo.GetClusterID(ctx, md.K8sClientReader)
57+
if err != nil {
58+
return nil, fmt.Errorf("error collecting ClusterID: %w", err)
59+
}
60+
deploymentID, err := clusterInfo.GetInstallationID(ctx, md.K8sClientReader, md.PodNSName)
61+
if err != nil {
62+
return nil, fmt.Errorf("error collecting InstallationID: %w", err)
63+
}
64+
deploymentName, err := clusterInfo.GetDeploymentName(ctx, md.K8sClientReader, md.PodNSName)
65+
if err != nil {
66+
return nil, fmt.Errorf("error collecting DeploymentName: %w", err)
67+
}
68+
info := newMetadataInfo(deploymentNamespace, clusterID, deploymentID, md.NICVersion, deploymentName)
69+
return info, nil
70+
}

internal/metadata/metadata_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package metadata
2+
3+
import (
4+
"context"
5+
"os"
6+
"testing"
7+
8+
api_v1 "k8s.io/api/core/v1"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/apimachinery/pkg/types"
11+
"k8s.io/client-go/kubernetes/fake"
12+
)
13+
14+
func TestNewMetadataInfo(t *testing.T) {
15+
info := newMetadataInfo("nginx-ingress", "e3a5e702-65a7-a55f-753d78cd7ff7", "555-1222-4414test-11223355", "5.0.0", "my-release")
16+
if info.ProductName != "nic" {
17+
t.Errorf("ProductName = %q, want %q", info.ProductName, "nic")
18+
}
19+
if info.DeploymentNamespace != "nginx-ingress" {
20+
t.Errorf("DeploymentNamespace = %q, want %q", info.DeploymentNamespace, "nginx-ingress")
21+
}
22+
if info.ClusterID != "e3a5e702-65a7-a55f-753d78cd7ff7" {
23+
t.Errorf("ClusterID = %q, want %q", info.ClusterID, "e3a5e702-65a7-a55f-753d78cd7ff7")
24+
}
25+
if info.DeploymentID != "555-1222-4414test-11223355" {
26+
t.Errorf("DeploymentID = %q, want %q", info.DeploymentID, "555-1222-4414test-11223355")
27+
}
28+
if info.ProductVersion != "5.0.0" {
29+
t.Errorf("ProductVersion = %q, want %q", info.ProductVersion, "5.0.0")
30+
}
31+
if info.DeploymentName != "my-release" {
32+
t.Errorf("DeploymentName = %q, want %q", info.DeploymentName, "my-release")
33+
}
34+
}
35+
36+
func TestCollectAndWrite(t *testing.T) {
37+
pod := &api_v1.Pod{
38+
ObjectMeta: metav1.ObjectMeta{
39+
Name: "test-pod",
40+
Namespace: "test-namespace",
41+
OwnerReferences: []metav1.OwnerReference{
42+
{
43+
APIVersion: "apps/v1",
44+
Kind: "DaemonSet",
45+
Name: "test-pod",
46+
UID: types.UID("install-123"),
47+
},
48+
},
49+
},
50+
}
51+
52+
if err := os.Setenv("POD_NAMESPACE", pod.Namespace); err != nil {
53+
t.Errorf("unable to set POD_NAMESPACE: %v", err)
54+
}
55+
if err := os.Setenv("POD_NAME", pod.Name); err != nil {
56+
t.Errorf("unable to set POD_NAME: %v", err)
57+
}
58+
59+
client := fake.NewSimpleClientset(
60+
&api_v1.Namespace{
61+
ObjectMeta: metav1.ObjectMeta{
62+
Name: "kube-system",
63+
UID: types.UID("123-abc-456-def"),
64+
},
65+
},
66+
pod,
67+
)
68+
69+
reporter := NewMetadataReporter(client, pod, "5.0.0")
70+
if reporter == nil {
71+
t.Fatal("expected reporter to be non-nil")
72+
}
73+
74+
info, err := reporter.CollectAndWrite(context.TODO())
75+
if err != nil {
76+
t.Fatalf("CollectAndWrite() error = %v", err)
77+
}
78+
if got, want := info.ProductName, "nic"; got != want {
79+
t.Errorf("ProductName = %q, want %q", got, want)
80+
}
81+
}
82+
83+
func TestNewMetadataReporter(t *testing.T) {
84+
reporter := NewMetadataReporter(
85+
fake.NewSimpleClientset(),
86+
&api_v1.Pod{},
87+
"5.0.0",
88+
)
89+
if reporter == nil {
90+
t.Fatal("Expected NewMetadataReporter to return non-nil")
91+
}
92+
}

internal/nginx/manager.go

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import (
1414
"strings"
1515
"time"
1616

17+
"github.com/nginx/kubernetes-ingress/internal/metadata"
18+
1719
license_reporting "github.com/nginx/kubernetes-ingress/internal/license_reporting"
1820
nl "github.com/nginx/kubernetes-ingress/internal/logger"
1921
"github.com/nginx/kubernetes-ingress/internal/metrics/collectors"
@@ -51,8 +53,9 @@ const (
5153
)
5254

5355
var (
54-
ossre = regexp.MustCompile(`(?P<name>\S+)/(?P<version>\S+)`)
55-
plusre = regexp.MustCompile(`(?P<name>\S+)/(?P<version>\S+).\((?P<plus>\S+plus\S+)\)`)
56+
ossre = regexp.MustCompile(`(?P<name>\S+)/(?P<version>\S+)`)
57+
plusre = regexp.MustCompile(`(?P<name>\S+)/(?P<version>\S+).\((?P<plus>\S+plus\S+)\)`)
58+
agentre = regexp.MustCompile(`^v(?P<major>\d+)\.?(?P<minor>\d+)?\.?(?P<patch>\d+)?(-.+)?$`)
5659
)
5760

5861
// ServerConfig holds the config data for an upstream server in NGINX Plus.
@@ -99,7 +102,7 @@ type Manager interface {
99102
DeleteKeyValStateFiles(virtualServerName string)
100103
}
101104

102-
// LocalManager updates NGINX configuration, starts, reloads and quits NGINX, updates License Reporting file
105+
// LocalManager updates NGINX configuration, starts, reloads and quits NGINX, updates License Reporting and the Deployment Metadata file
103106
// updates NGINX Plus upstream servers. It assumes that NGINX is running in the same container.
104107
type LocalManager struct {
105108
confdPath string
@@ -119,6 +122,7 @@ type LocalManager struct {
119122
metricsCollector collectors.ManagerCollector
120123
licenseReporter *license_reporting.LicenseReporter
121124
licenseReporterCancel context.CancelFunc
125+
deploymentMetadata *metadata.Metadata
122126
appProtectPluginPid int
123127
appProtectDosAgentPid int
124128
agentPid int
@@ -127,7 +131,7 @@ type LocalManager struct {
127131
}
128132

129133
// NewLocalManager creates a LocalManager.
130-
func NewLocalManager(ctx context.Context, confPath string, debug bool, mc collectors.ManagerCollector, lr *license_reporting.LicenseReporter, timeout time.Duration, nginxPlus bool) *LocalManager {
134+
func NewLocalManager(ctx context.Context, confPath string, debug bool, mc collectors.ManagerCollector, lr *license_reporting.LicenseReporter, metadata *metadata.Metadata, timeout time.Duration, nginxPlus bool) *LocalManager {
131135
l := nl.LoggerFromContext(ctx)
132136
verifyConfigGenerator, err := newVerifyConfigGenerator()
133137
if err != nil {
@@ -149,6 +153,7 @@ func NewLocalManager(ctx context.Context, confPath string, debug bool, mc collec
149153
verifyClient: newVerifyClient(timeout),
150154
metricsCollector: mc,
151155
licenseReporter: lr,
156+
deploymentMetadata: metadata,
152157
nginxPlus: nginxPlus,
153158
logger: l,
154159
}
@@ -607,10 +612,34 @@ func (lm *LocalManager) AppProtectDosAgentStart(apdaDone chan error, debug bool,
607612

608613
// AgentStart starts the AppProtect plugin and sets AppProtect log level.
609614
func (lm *LocalManager) AgentStart(agentDone chan error, instanceGroup string) {
615+
ctx := nl.ContextWithLogger(context.Background(), lm.logger)
610616
nl.Debugf(lm.logger, "Starting Agent")
611617
args := []string{}
612-
if len(instanceGroup) > 0 {
613-
args = append(args, "--instance-group", instanceGroup)
618+
nl.Debug(lm.logger, lm.AgentVersion())
619+
major, _, _, err := ExtractAgentVersionValues(lm.AgentVersion())
620+
if err != nil {
621+
nl.Fatalf(lm.logger, "Failed to extract Agent version: %v", err)
622+
}
623+
if major <= 2 {
624+
if len(instanceGroup) > 0 {
625+
args = append(args, "--instance-group", instanceGroup)
626+
}
627+
}
628+
if major >= 3 {
629+
metadataInfo, err := lm.deploymentMetadata.CollectAndWrite(ctx)
630+
if err != nil {
631+
nl.Fatalf(lm.logger, "Failed to start NGINX Agent: %v", err)
632+
}
633+
labels := []string{
634+
fmt.Sprintf("product_name=%s", metadataInfo.ProductName),
635+
fmt.Sprintf("product_version=%s", metadataInfo.ProductVersion),
636+
fmt.Sprintf("cluster_id=%s", metadataInfo.ClusterID),
637+
fmt.Sprintf("deployment_name=%s", metadataInfo.DeploymentName),
638+
fmt.Sprintf("deployment_id=%s", metadataInfo.DeploymentID),
639+
fmt.Sprintf("deployment_namespace=%s", metadataInfo.DeploymentNamespace),
640+
}
641+
metadataLabels := "--labels=" + strings.Join(labels, ",")
642+
args = append(args, metadataLabels)
614643
}
615644
cmd := exec.Command(agentPath, args...) // #nosec G204
616645

internal/nginx/version.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,30 @@ func extractPlusVersionValues(input string) (int, int, error) {
9999

100100
return rValue, pValue, nil
101101
}
102+
103+
// ExtractAgentVersionValues splits the agent version string into major, minor, and patch values.
104+
func ExtractAgentVersionValues(input string) (int, int, int, error) {
105+
var major, minor, patch int
106+
matches := agentre.FindStringSubmatch(input)
107+
108+
if len(matches) == 0 {
109+
return 0, 0, 0, fmt.Errorf("no matches found in the input string")
110+
}
111+
112+
major, err := strconv.Atoi(matches[1])
113+
if err != nil {
114+
return 0, 0, 0, fmt.Errorf("failed to convert major version to integer: %w", err)
115+
}
116+
117+
minor, err = strconv.Atoi(matches[2])
118+
if err != nil {
119+
return major, 0, 0, fmt.Errorf("failed to convert minor version to integer: %w", err)
120+
}
121+
122+
patch, err = strconv.Atoi(matches[3])
123+
if err != nil {
124+
return major, minor, 0, fmt.Errorf("failed to convert patch version to integer: %w", err)
125+
}
126+
127+
return major, minor, patch, nil
128+
}

0 commit comments

Comments
 (0)