From e84021ea226191c2005d28e9d0242803b421245c Mon Sep 17 00:00:00 2001 From: Ashley Davis Date: Thu, 26 Mar 2026 13:14:47 +0000 Subject: [PATCH] add helm chart for NGTS-capable agent This has been tested to the point of being able to upload data to the backend through NGTS. I haven't been able to fully test that the uploaded data appears in the UI, but the upload itself is successful. Signed-off-by: Ashley Davis --- deploy/charts/discovery-agent/.helmignore | 23 + deploy/charts/discovery-agent/Chart.yaml | 17 + deploy/charts/discovery-agent/README.md | 391 ++++++++++++++ .../discovery-agent/templates/NOTES.txt | 9 + .../discovery-agent/templates/_helpers.tpl | 120 +++++ .../discovery-agent/templates/configmap.yaml | 75 +++ .../discovery-agent/templates/deployment.yaml | 137 +++++ .../templates/poddisruptionbudget.yaml | 23 + .../discovery-agent/templates/podmonitor.yaml | 40 ++ .../discovery-agent/templates/rbac.yaml | 142 +++++ .../templates/serviceaccount.yaml | 13 + .../discovery-agent/values.linter.exceptions | 0 .../charts/discovery-agent/values.schema.json | 489 ++++++++++++++++++ deploy/charts/discovery-agent/values.yaml | 270 ++++++++++ hack/ngts/test-e2e.sh | 111 ++++ make/00_mod.mk | 1 + make/02_mod.mk | 1 + make/ngts/00_mod.mk | 30 ++ make/ngts/02_mod.mk | 63 +++ pkg/agent/config.go | 11 +- pkg/agent/config_test.go | 4 +- pkg/client/client_ngts.go | 48 +- pkg/client/client_ngts_test.go | 63 ++- 23 files changed, 2069 insertions(+), 12 deletions(-) create mode 100644 deploy/charts/discovery-agent/.helmignore create mode 100644 deploy/charts/discovery-agent/Chart.yaml create mode 100644 deploy/charts/discovery-agent/README.md create mode 100644 deploy/charts/discovery-agent/templates/NOTES.txt create mode 100644 deploy/charts/discovery-agent/templates/_helpers.tpl create mode 100644 deploy/charts/discovery-agent/templates/configmap.yaml create mode 100644 deploy/charts/discovery-agent/templates/deployment.yaml create mode 100644 deploy/charts/discovery-agent/templates/poddisruptionbudget.yaml create mode 100644 deploy/charts/discovery-agent/templates/podmonitor.yaml create mode 100644 deploy/charts/discovery-agent/templates/rbac.yaml create mode 100644 deploy/charts/discovery-agent/templates/serviceaccount.yaml create mode 100644 deploy/charts/discovery-agent/values.linter.exceptions create mode 100644 deploy/charts/discovery-agent/values.schema.json create mode 100644 deploy/charts/discovery-agent/values.yaml create mode 100755 hack/ngts/test-e2e.sh create mode 100644 make/ngts/00_mod.mk create mode 100644 make/ngts/02_mod.mk diff --git a/deploy/charts/discovery-agent/.helmignore b/deploy/charts/discovery-agent/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/deploy/charts/discovery-agent/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/deploy/charts/discovery-agent/Chart.yaml b/deploy/charts/discovery-agent/Chart.yaml new file mode 100644 index 00000000..376b8ec8 --- /dev/null +++ b/deploy/charts/discovery-agent/Chart.yaml @@ -0,0 +1,17 @@ +apiVersion: v2 +name: discovery-agent +description: |- + The discovery-agent connects your Kubernetes or Openshift cluster to NGTS for discovery and monitoring. + +maintainers: + - name: Palo Alto Networks + url: https://www.paloaltonetworks.com + +sources: + - https://github.com/jetstack/jetstack-secure + +# These versions are meant to be overridden by `make helm-chart`. No `v` prefix +# for the `version` because Helm doesn't support auto-determining the latest +# version for OCI Helm charts that use a `v` prefix. +version: 0.0.0 +appVersion: "v0.0.0" diff --git a/deploy/charts/discovery-agent/README.md b/deploy/charts/discovery-agent/README.md new file mode 100644 index 00000000..3f3330f5 --- /dev/null +++ b/deploy/charts/discovery-agent/README.md @@ -0,0 +1,391 @@ +# discovery-agent + +The Discovery Agent connects your Kubernetes or OpenShift cluster to Palo Alto NGTS. + +## Values + + + +#### **replicaCount** ~ `number` +> Default value: +> ```yaml +> 1 +> ``` + +This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +#### **imageRegistry** ~ `string` +> Default value: +> ```yaml +> quay.io +> ``` + +The container registry used for discovery-agent images by default. This can include path prefixes (e.g. "artifactory.example.com/docker"). + +#### **imageNamespace** ~ `string` +> Default value: +> ```yaml +> jetstack +> ``` + +The repository namespace used for discovery-agent images by default. +Examples: +- jetstack +- custom-namespace + +#### **image.registry** ~ `string` + +Deprecated: per-component registry prefix. + +If set, this value is *prepended* to the image repository that the chart would otherwise render. This applies both when `image.repository` is set and when the repository is computed from +`imageRegistry` + `imageNamespace` + `image.name`. + +This can produce "double registry" style references such as +`legacy.example.io/quay.io/jetstack/...`. Prefer using the global +`imageRegistry`/`imageNamespace` values. + +#### **image.repository** ~ `string` +> Default value: +> ```yaml +> "" +> ``` + +Full repository override (takes precedence over `imageRegistry`, `imageNamespace`, and `image.name`). +Example: quay.io/jetstack/discovery-agent + +#### **image.name** ~ `string` +> Default value: +> ```yaml +> discovery-agent +> ``` + +The image name for the Discovery Agent. +This is used (together with `imageRegistry` and `imageNamespace`) to construct the full image reference. + +#### **image.pullPolicy** ~ `string` +> Default value: +> ```yaml +> IfNotPresent +> ``` + +This sets the pull policy for images. +#### **image.tag** ~ `string` +> Default value: +> ```yaml +> "" +> ``` + +Override the image tag to deploy by setting this variable. If no value is set, the chart's appVersion is used. +#### **image.digest** ~ `string` +> Default value: +> ```yaml +> "" +> ``` + +Override the image digest to deploy by setting this variable. If set together with `image.tag`, the rendered image will include both tag and digest. +#### **imagePullSecrets** ~ `array` +> Default value: +> ```yaml +> [] +> ``` + +This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +#### **nameOverride** ~ `string` +> Default value: +> ```yaml +> "" +> ``` + +This is to override the chart name. +#### **fullnameOverride** ~ `string` +> Default value: +> ```yaml +> "" +> ``` +#### **serviceAccount.create** ~ `bool` +> Default value: +> ```yaml +> true +> ``` + +Specifies whether a service account should be created +#### **serviceAccount.automount** ~ `bool` +> Default value: +> ```yaml +> true +> ``` + +Automatically mount a ServiceAccount's API credentials? +#### **serviceAccount.annotations** ~ `object` +> Default value: +> ```yaml +> {} +> ``` + +Annotations to add to the service account +#### **serviceAccount.name** ~ `string` +> Default value: +> ```yaml +> "" +> ``` + +The name of the service account to use. +If not set and create is true, a name is generated using the fullname template +#### **podAnnotations** ~ `object` +> Default value: +> ```yaml +> {} +> ``` + +This is for setting Kubernetes Annotations to a Pod. For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +#### **podLabels** ~ `object` +> Default value: +> ```yaml +> {} +> ``` + +This is for setting Kubernetes Labels to a Pod. +For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +#### **podSecurityContext** ~ `object` +> Default value: +> ```yaml +> {} +> ``` +#### **securityContext** ~ `object` +> Default value: +> ```yaml +> allowPrivilegeEscalation: false +> capabilities: +> drop: +> - ALL +> readOnlyRootFilesystem: true +> runAsNonRoot: true +> seccompProfile: +> type: RuntimeDefault +> ``` + +Add Container specific SecurityContext settings to the container. Takes precedence over `podSecurityContext` when set. See https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-capabilities-for-a-container + +#### **resources** ~ `object` +> Default value: +> ```yaml +> {} +> ``` +#### **volumes** ~ `array` +> Default value: +> ```yaml +> [] +> ``` + +Additional volumes on the output Deployment definition. +#### **volumeMounts** ~ `array` +> Default value: +> ```yaml +> [] +> ``` + +Additional volumeMounts on the output Deployment definition. +#### **nodeSelector** ~ `object` +> Default value: +> ```yaml +> {} +> ``` +#### **tolerations** ~ `array` +> Default value: +> ```yaml +> [] +> ``` +#### **affinity** ~ `object` +> Default value: +> ```yaml +> {} +> ``` +#### **http_proxy** ~ `string` + +Configures the HTTP_PROXY environment variable where a HTTP proxy is required. + +#### **https_proxy** ~ `string` + +Configures the HTTPS_PROXY environment variable where a HTTP proxy is required. + +#### **no_proxy** ~ `string` + +Configures the NO_PROXY environment variable where a HTTP proxy is required, but certain domains should be excluded. + +#### **podDisruptionBudget** ~ `object` +> Default value: +> ```yaml +> enabled: false +> ``` + +Configure a PodDisruptionBudget for the agent's Deployment. If running with multiple replicas, consider setting podDisruptionBudget.enabled to true. + +#### **config.period** ~ `string` +> Default value: +> ```yaml +> 0h1m0s +> ``` + +Push data every 12 hours unless changed. +#### **config.excludeAnnotationKeysRegex** ~ `array` +> Default value: +> ```yaml +> [] +> ``` + +You can configure the agent to exclude some annotations or labels from being pushed. All Kubernetes objects are affected. The objects are still pushed, but the specified annotations and labels are removed before being pushed. + +Dots is the only character that needs to be escaped in the regex. Use either double quotes with escaped single quotes or unquoted strings for the regex to avoid YAML parsing issues with `\.`. + +Example: excludeAnnotationKeysRegex: ['^kapp\.k14s\.io/original.*'] +#### **config.excludeLabelKeysRegex** ~ `array` +> Default value: +> ```yaml +> [] +> ``` +#### **config.clusterName** ~ `string` +> Default value: +> ```yaml +> "" +> ``` + +A human readable name for the cluster where the agent is deployed (required). + +This cluster name will be associated with the data that the agent uploads to the NGTS service. +#### **config.clusterDescription** ~ `string` +> Default value: +> ```yaml +> "" +> ``` + +A short description of the cluster where the agent is deployed (optional). + +This description will be associated with the data that the agent uploads to the NGTS service. The description may include contact information such as the email address of the cluster administrator. +#### **config.clientID** ~ `string` +> Default value: +> ```yaml +> "" +> ``` + +Deprecated: Client ID for the configured service account. The client ID should be provided in the "clientID" field of the authentication secret (see config.secretName). + +#### **config.tsgID** ~ `string` +> Default value: +> ```yaml +> "" +> ``` + +The TSG (Tenant Security Group) ID for your NGTS account (required). NB: TSG IDs are numeric, but must be provided as strings (in double quotes). + + +#### **config.secretName** ~ `string` +> Default value: +> ```yaml +> discovery-agent-credentials +> ``` + +The name of the Secret containing the NGTS service account credentials. +The Secret must contain the following key: +- privatekey.pem: PEM-encoded private key for the service account +The Secret may contain the following key: +- clientID: Service account client ID (config.clientID must be set if not present) + +#### **extraArgs** ~ `array` +> Default value: +> ```yaml +> [] +> ``` + +```yaml +extraArgs: +- --logging-format=json +- --log-level=6 # To enable HTTP request logging +``` +#### **pprof.enabled** ~ `bool` +> Default value: +> ```yaml +> false +> ``` + +Enable profiling with the pprof endpoint +#### **metrics.enabled** ~ `bool` +> Default value: +> ```yaml +> true +> ``` + +Enable the metrics server. +If false, the metrics server will be disabled and the other metrics fields below will be ignored. +#### **metrics.podmonitor.enabled** ~ `bool` +> Default value: +> ```yaml +> false +> ``` + +Create a PodMonitor to add the metrics to Prometheus, if you are using Prometheus Operator. See https://prometheus-operator.dev/docs/operator/api/#monitoring.coreos.com/v1.PodMonitor +#### **metrics.podmonitor.namespace** ~ `string` + +The namespace that the pod monitor should live in. +Defaults to the discovery-agent namespace. + +#### **metrics.podmonitor.prometheusInstance** ~ `string` +> Default value: +> ```yaml +> default +> ``` + +Specifies the `prometheus` label on the created PodMonitor. This is used when different Prometheus instances have label selectors matching different PodMonitors. +#### **metrics.podmonitor.interval** ~ `string` +> Default value: +> ```yaml +> 60s +> ``` + +The interval to scrape metrics. +#### **metrics.podmonitor.scrapeTimeout** ~ `string` +> Default value: +> ```yaml +> 30s +> ``` + +The timeout before a metrics scrape fails. +#### **metrics.podmonitor.labels** ~ `object` +> Default value: +> ```yaml +> {} +> ``` + +Additional labels to add to the PodMonitor. +#### **metrics.podmonitor.annotations** ~ `object` +> Default value: +> ```yaml +> {} +> ``` + +Additional annotations to add to the PodMonitor. +#### **metrics.podmonitor.honorLabels** ~ `bool` +> Default value: +> ```yaml +> false +> ``` + +Keep labels from scraped data, overriding server-side labels. +#### **metrics.podmonitor.endpointAdditionalProperties** ~ `object` +> Default value: +> ```yaml +> {} +> ``` + +EndpointAdditionalProperties allows setting additional properties on the endpoint such as relabelings, metricRelabelings etc. + +For example: + +```yaml +endpointAdditionalProperties: + relabelings: + - action: replace + sourceLabels: + - __meta_kubernetes_pod_node_name + targetLabel: instance +``` + + diff --git a/deploy/charts/discovery-agent/templates/NOTES.txt b/deploy/charts/discovery-agent/templates/NOTES.txt new file mode 100644 index 00000000..92cce232 --- /dev/null +++ b/deploy/charts/discovery-agent/templates/NOTES.txt @@ -0,0 +1,9 @@ +CHART NAME: {{ .Chart.Name }} +CHART VERSION: {{ .Chart.Version }} +APP VERSION: {{ .Chart.AppVersion }} + +- Check the application is running: +> kubectl get pods -n {{ .Release.Namespace }} -l app.kubernetes.io/instance={{ .Release.Name }} + +- Check the application logs for successful connection to NGTS: +> kubectl logs -n {{ .Release.Namespace }} -l app.kubernetes.io/instance={{ .Release.Name }} diff --git a/deploy/charts/discovery-agent/templates/_helpers.tpl b/deploy/charts/discovery-agent/templates/_helpers.tpl new file mode 100644 index 00000000..9ee2ecf3 --- /dev/null +++ b/deploy/charts/discovery-agent/templates/_helpers.tpl @@ -0,0 +1,120 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "discovery-agent.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "discovery-agent.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "discovery-agent.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "discovery-agent.labels" -}} +helm.sh/chart: {{ include "discovery-agent.chart" . }} +{{ include "discovery-agent.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "discovery-agent.selectorLabels" -}} +app.kubernetes.io/name: {{ include "discovery-agent.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "discovery-agent.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "discovery-agent.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Util function for generating the image URL based on the provided options. +IMPORTANT: This function is standardized across all charts in the cert-manager GH organization. +Any changes to this function should also be made in cert-manager, trust-manager, approver-policy, ... +See https://github.com/cert-manager/cert-manager/issues/6329 for a list of linked PRs. +*/}} +{{- define "image" -}} +{{- /* +Calling convention: +- (tuple ) +We intentionally pass imageRegistry/imageNamespace as explicit arguments rather than reading +from `.Values` inside this helper, because `helm-tool lint` does not reliably track `.Values.*` +usage through tuple/variable indirection. +*/ -}} +{{- if ne (len .) 4 -}} + {{- fail (printf "ERROR: template \"image\" expects (tuple ), got %d arguments" (len .)) -}} +{{- end -}} +{{- $image := index . 0 -}} +{{- $imageRegistry := index . 1 | default "" -}} +{{- $imageNamespace := index . 2 | default "" -}} +{{- $defaultReference := index . 3 -}} +{{- $repository := "" -}} +{{- if $image.repository -}} + {{- $repository = $image.repository -}} + {{- /* + Backwards compatibility: if image.registry is set, additionally prefix the repository with this registry. + */ -}} + {{- if $image.registry -}} + {{- $repository = printf "%s/%s" $image.registry $repository -}} + {{- end -}} +{{- else -}} + {{- $name := required "ERROR: image.name must be set when image.repository is empty" $image.name -}} + {{- $repository = $name -}} + {{- if $imageNamespace -}} + {{- $repository = printf "%s/%s" $imageNamespace $repository -}} + {{- end -}} + {{- if $imageRegistry -}} + {{- $repository = printf "%s/%s" $imageRegistry $repository -}} + {{- end -}} + {{- /* + Backwards compatibility: if image.registry is set, additionally prefix the repository with this registry. + */ -}} + {{- if $image.registry -}} + {{- $repository = printf "%s/%s" $image.registry $repository -}} + {{- end -}} +{{- end -}} +{{- $repository -}} +{{- if and $image.tag $image.digest -}} + {{- printf ":%s@%s" $image.tag $image.digest -}} +{{- else if $image.tag -}} + {{- printf ":%s" $image.tag -}} +{{- else if $image.digest -}} + {{- printf "@%s" $image.digest -}} +{{- else -}} + {{- printf "%s" $defaultReference -}} +{{- end -}} +{{- end }} diff --git a/deploy/charts/discovery-agent/templates/configmap.yaml b/deploy/charts/discovery-agent/templates/configmap.yaml new file mode 100644 index 00000000..65c0a9ce --- /dev/null +++ b/deploy/charts/discovery-agent/templates/configmap.yaml @@ -0,0 +1,75 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "discovery-agent.fullname" . }}-config + namespace: {{ .Release.Namespace }} + labels: + {{- include "discovery-agent.labels" . | nindent 4 }} +data: + config.yaml: |- + cluster_name: {{ required "config.clusterName is required" .Values.config.clusterName | quote }} + cluster_description: {{ .Values.config.clusterDescription | quote }} + period: {{ .Values.config.period | quote }} + {{- with .Values.config.excludeAnnotationKeysRegex }} + exclude-annotation-keys-regex: + {{- . | toYaml | nindent 6 }} + {{- end }} + {{- with .Values.config.excludeLabelKeysRegex }} + exclude-label-keys-regex: + {{- . | toYaml | nindent 6 }} + {{- end }} + data-gatherers: + - kind: k8s-discovery + name: ngts/discovery + - kind: k8s-dynamic + name: ngts/secrets + config: + resource-type: + version: v1 + resource: secrets + field-selectors: + - type!=kubernetes.io/dockercfg + - type!=kubernetes.io/dockerconfigjson + - type!=bootstrap.kubernetes.io/token + - type!=helm.sh/release.v1 + - kind: k8s-dynamic + name: ngts/jobs + config: + resource-type: + version: v1 + group: batch + resource: jobs + - kind: k8s-dynamic + name: ngts/cronjobs + config: + resource-type: + version: v1 + group: batch + resource: cronjobs + - kind: k8s-dynamic + name: ngts/deployments + config: + resource-type: + version: v1 + group: apps + resource: deployments + - kind: k8s-dynamic + name: ngts/statefulsets + config: + resource-type: + version: v1 + group: apps + resource: statefulsets + - kind: k8s-dynamic + name: ngts/daemonsets + config: + resource-type: + version: v1 + group: apps + resource: daemonsets + - kind: k8s-dynamic + name: ngts/pods + config: + resource-type: + version: v1 + resource: pods diff --git a/deploy/charts/discovery-agent/templates/deployment.yaml b/deploy/charts/discovery-agent/templates/deployment.yaml new file mode 100644 index 00000000..3425de01 --- /dev/null +++ b/deploy/charts/discovery-agent/templates/deployment.yaml @@ -0,0 +1,137 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "discovery-agent.fullname" . }} + labels: + {{- include "discovery-agent.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "discovery-agent.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "discovery-agent.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "discovery-agent.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: agent + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ template "image" (tuple .Values.image .Values.imageRegistry .Values.imageNamespace (printf ":%s" .Chart.AppVersion)) }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_UID + valueFrom: + fieldRef: + fieldPath: metadata.uid + - name: POD_NODE + valueFrom: + fieldRef: + fieldPath: spec.nodeName + {{- with .Values.http_proxy }} + - name: HTTP_PROXY + value: {{ . }} + {{- end }} + {{- with .Values.https_proxy }} + - name: HTTPS_PROXY + value: {{ . }} + {{- end }} + {{- with .Values.no_proxy }} + - name: NO_PROXY + value: {{ . }} + {{- end }} + args: + - "agent" + - "-c" + - "/etc/discovery-agent/config.yaml" + - --ngts + - --tsg-id + - {{ required "config.tsgID is required" .Values.config.tsgID | toString | quote }} + {{- with .Values.config.serverURL }} + - --ngts-server-url + - {{ . | quote }} + {{- end }} + {{- if .Values.config.clientID }} + - --client-id + - {{ .Values.config.clientID }} + {{- end }} + - --private-key-path + - /etc/discovery-agent/credentials/privatekey.pem + - --logging-format=json + {{- if .Values.metrics.enabled }} + - --enable-metrics + {{- end }} + {{- if .Values.pprof.enabled }} + - --enable-pprof + {{- end }} + {{- range .Values.extraArgs }} + - {{ . | quote }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: config + mountPath: "/etc/discovery-agent" + readOnly: true + - name: credentials + mountPath: "/etc/discovery-agent/credentials" + readOnly: true + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: agent-api + containerPort: 8081 + volumes: + - name: config + configMap: + name: {{ include "discovery-agent.fullname" . }}-config + optional: false + - name: credentials + secret: + secretName: {{ .Values.config.secretName }} + optional: false + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/deploy/charts/discovery-agent/templates/poddisruptionbudget.yaml b/deploy/charts/discovery-agent/templates/poddisruptionbudget.yaml new file mode 100644 index 00000000..0ccdc945 --- /dev/null +++ b/deploy/charts/discovery-agent/templates/poddisruptionbudget.yaml @@ -0,0 +1,23 @@ +{{- if .Values.podDisruptionBudget.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "discovery-agent.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "discovery-agent.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "discovery-agent.selectorLabels" . | nindent 6 }} + + {{- if not (or (hasKey .Values.podDisruptionBudget "minAvailable") (hasKey .Values.podDisruptionBudget "maxUnavailable")) }} + minAvailable: 1 # Default value because minAvailable and maxUnavailable are not set + {{- end }} + {{- if hasKey .Values.podDisruptionBudget "minAvailable" }} + minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} + {{- end }} + {{- if hasKey .Values.podDisruptionBudget "maxUnavailable" }} + maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }} + {{- end }} +{{- end }} diff --git a/deploy/charts/discovery-agent/templates/podmonitor.yaml b/deploy/charts/discovery-agent/templates/podmonitor.yaml new file mode 100644 index 00000000..d48da1fd --- /dev/null +++ b/deploy/charts/discovery-agent/templates/podmonitor.yaml @@ -0,0 +1,40 @@ +{{- if and .Values.metrics.enabled .Values.metrics.podmonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: {{ include "discovery-agent.fullname" . }} +{{- if .Values.metrics.podmonitor.namespace }} + namespace: {{ .Values.metrics.podmonitor.namespace }} +{{- else }} + namespace: {{ .Release.Namespace | quote }} +{{- end }} + labels: + {{- include "discovery-agent.labels" . | nindent 4 }} + prometheus: {{ .Values.metrics.podmonitor.prometheusInstance }} + {{- with .Values.metrics.podmonitor.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +{{- with .Values.metrics.podmonitor.annotations }} + annotations: + {{- toYaml . | nindent 4 }} +{{- end }} +spec: + jobLabel: {{ include "discovery-agent.fullname" . }} + selector: + matchLabels: + {{- include "discovery-agent.selectorLabels" . | nindent 6 }} +{{- if .Values.metrics.podmonitor.namespace }} + namespaceSelector: + matchNames: + - {{ .Release.Namespace | quote }} +{{- end }} + podMetricsEndpoints: + - port: agent-api + path: /metrics + interval: {{ .Values.metrics.podmonitor.interval }} + scrapeTimeout: {{ .Values.metrics.podmonitor.scrapeTimeout }} + honorLabels: {{ .Values.metrics.podmonitor.honorLabels }} + {{- with .Values.metrics.podmonitor.endpointAdditionalProperties }} + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/deploy/charts/discovery-agent/templates/rbac.yaml b/deploy/charts/discovery-agent/templates/rbac.yaml new file mode 100644 index 00000000..6d9d204d --- /dev/null +++ b/deploy/charts/discovery-agent/templates/rbac.yaml @@ -0,0 +1,142 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "discovery-agent.fullname" . }}-event-emitted + labels: + {{- include "discovery-agent.labels" . | nindent 4 }} +rules: + - apiGroups: [""] + resources: ["events"] + verbs: ["create"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "discovery-agent.fullname" . }}-event-emitted + labels: + {{- include "discovery-agent.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "discovery-agent.fullname" . }}-event-emitted +subjects: + - kind: ServiceAccount + name: {{ include "discovery-agent.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "discovery-agent.fullname" . }}-cluster-viewer + labels: + {{- include "discovery-agent.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: view +subjects: + - kind: ServiceAccount + name: {{ include "discovery-agent.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "discovery-agent.fullname" . }}-secret-reader + labels: + {{- include "discovery-agent.labels" . | nindent 4 }} +rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "discovery-agent.fullname" . }}-secret-reader + labels: + {{- include "discovery-agent.labels" . | nindent 4 }} +roleRef: + kind: ClusterRole + name: {{ include "discovery-agent.fullname" . }}-secret-reader + apiGroup: rbac.authorization.k8s.io +subjects: + - kind: ServiceAccount + name: {{ include "discovery-agent.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "discovery-agent.fullname" . }}-rbac-reader + labels: + {{- include "discovery-agent.labels" . | nindent 4 }} +rules: + - apiGroups: ["rbac.authorization.k8s.io"] + resources: + - roles + - clusterroles + - rolebindings + - clusterrolebindings + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "discovery-agent.fullname" . }}-rbac-reader + labels: + {{- include "discovery-agent.labels" . | nindent 4 }} +roleRef: + kind: ClusterRole + name: {{ include "discovery-agent.fullname" . }}-rbac-reader + apiGroup: rbac.authorization.k8s.io +subjects: + - kind: ServiceAccount + name: {{ include "discovery-agent.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "discovery-agent.fullname" . }}-oidc-discovery + labels: + {{- include "discovery-agent.labels" . | nindent 4 }} +roleRef: + kind: ClusterRole + name: system:service-account-issuer-discovery + apiGroup: rbac.authorization.k8s.io +subjects: + - kind: ServiceAccount + name: {{ include "discovery-agent.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "discovery-agent.fullname" . }}-eso-reader + labels: + {{- include "discovery-agent.labels" . | nindent 4 }} +rules: + - apiGroups: ["external-secrets.io"] + resources: + - externalsecrets + - clusterexternalsecrets + - secretstores + - clustersecretstores + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "discovery-agent.fullname" . }}-eso-reader + labels: + {{- include "discovery-agent.labels" . | nindent 4 }} +roleRef: + kind: ClusterRole + name: {{ include "discovery-agent.fullname" . }}-eso-reader + apiGroup: rbac.authorization.k8s.io +subjects: + - kind: ServiceAccount + name: {{ include "discovery-agent.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/deploy/charts/discovery-agent/templates/serviceaccount.yaml b/deploy/charts/discovery-agent/templates/serviceaccount.yaml new file mode 100644 index 00000000..9281d7fc --- /dev/null +++ b/deploy/charts/discovery-agent/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "discovery-agent.serviceAccountName" . }} + labels: + {{- include "discovery-agent.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/deploy/charts/discovery-agent/values.linter.exceptions b/deploy/charts/discovery-agent/values.linter.exceptions new file mode 100644 index 00000000..e69de29b diff --git a/deploy/charts/discovery-agent/values.schema.json b/deploy/charts/discovery-agent/values.schema.json new file mode 100644 index 00000000..404b9d08 --- /dev/null +++ b/deploy/charts/discovery-agent/values.schema.json @@ -0,0 +1,489 @@ +{ + "$defs": { + "helm-values": { + "additionalProperties": false, + "properties": { + "affinity": { + "$ref": "#/$defs/helm-values.affinity" + }, + "config": { + "$ref": "#/$defs/helm-values.config" + }, + "extraArgs": { + "$ref": "#/$defs/helm-values.extraArgs" + }, + "fullnameOverride": { + "$ref": "#/$defs/helm-values.fullnameOverride" + }, + "global": { + "$ref": "#/$defs/helm-values.global" + }, + "http_proxy": { + "$ref": "#/$defs/helm-values.http_proxy" + }, + "https_proxy": { + "$ref": "#/$defs/helm-values.https_proxy" + }, + "image": { + "$ref": "#/$defs/helm-values.image" + }, + "imageNamespace": { + "$ref": "#/$defs/helm-values.imageNamespace" + }, + "imagePullSecrets": { + "$ref": "#/$defs/helm-values.imagePullSecrets" + }, + "imageRegistry": { + "$ref": "#/$defs/helm-values.imageRegistry" + }, + "metrics": { + "$ref": "#/$defs/helm-values.metrics" + }, + "nameOverride": { + "$ref": "#/$defs/helm-values.nameOverride" + }, + "no_proxy": { + "$ref": "#/$defs/helm-values.no_proxy" + }, + "nodeSelector": { + "$ref": "#/$defs/helm-values.nodeSelector" + }, + "podAnnotations": { + "$ref": "#/$defs/helm-values.podAnnotations" + }, + "podDisruptionBudget": { + "$ref": "#/$defs/helm-values.podDisruptionBudget" + }, + "podLabels": { + "$ref": "#/$defs/helm-values.podLabels" + }, + "podSecurityContext": { + "$ref": "#/$defs/helm-values.podSecurityContext" + }, + "pprof": { + "$ref": "#/$defs/helm-values.pprof" + }, + "replicaCount": { + "$ref": "#/$defs/helm-values.replicaCount" + }, + "resources": { + "$ref": "#/$defs/helm-values.resources" + }, + "securityContext": { + "$ref": "#/$defs/helm-values.securityContext" + }, + "serviceAccount": { + "$ref": "#/$defs/helm-values.serviceAccount" + }, + "tolerations": { + "$ref": "#/$defs/helm-values.tolerations" + }, + "volumeMounts": { + "$ref": "#/$defs/helm-values.volumeMounts" + }, + "volumes": { + "$ref": "#/$defs/helm-values.volumes" + } + }, + "type": "object" + }, + "helm-values.affinity": { + "default": {}, + "type": "object" + }, + "helm-values.config": { + "additionalProperties": false, + "properties": { + "clientID": { + "$ref": "#/$defs/helm-values.config.clientID" + }, + "clusterDescription": { + "$ref": "#/$defs/helm-values.config.clusterDescription" + }, + "clusterName": { + "$ref": "#/$defs/helm-values.config.clusterName" + }, + "excludeAnnotationKeysRegex": { + "$ref": "#/$defs/helm-values.config.excludeAnnotationKeysRegex" + }, + "excludeLabelKeysRegex": { + "$ref": "#/$defs/helm-values.config.excludeLabelKeysRegex" + }, + "period": { + "$ref": "#/$defs/helm-values.config.period" + }, + "secretName": { + "$ref": "#/$defs/helm-values.config.secretName" + }, + "serverURL": { + "$ref": "#/$defs/helm-values.config.serverURL" + }, + "tsgID": { + "$ref": "#/$defs/helm-values.config.tsgID" + } + }, + "type": "object" + }, + "helm-values.config.clientID": { + "default": "", + "description": "Deprecated: Client ID for the configured service account. The client ID should be provided in the \"clientID\" field of the authentication secret (see config.secretName).", + "type": "string" + }, + "helm-values.config.clusterDescription": { + "default": "", + "description": "A short description of the cluster where the agent is deployed (optional).\n\nThis description will be associated with the data that the agent uploads to the NGTS service. The description may include contact information such as the email address of the cluster administrator.", + "type": "string" + }, + "helm-values.config.clusterName": { + "default": "", + "description": "A human readable name for the cluster where the agent is deployed (required).\n\nThis cluster name will be associated with the data that the agent uploads to the NGTS service.", + "type": "string" + }, + "helm-values.config.excludeAnnotationKeysRegex": { + "default": [], + "description": "You can configure the agent to exclude some annotations or labels from being pushed. All Kubernetes objects are affected. The objects are still pushed, but the specified annotations and labels are removed before being pushed.\n\nDots is the only character that needs to be escaped in the regex. Use either double quotes with escaped single quotes or unquoted strings for the regex to avoid YAML parsing issues with `\\.`.\n\nExample: excludeAnnotationKeysRegex: ['^kapp\\.k14s\\.io/original.*']", + "items": {}, + "type": "array" + }, + "helm-values.config.excludeLabelKeysRegex": { + "default": [], + "items": {}, + "type": "array" + }, + "helm-values.config.period": { + "default": "0h1m0s", + "description": "Push data every 12 hours unless changed.", + "type": "string" + }, + "helm-values.config.secretName": { + "default": "discovery-agent-credentials", + "description": "The name of the Secret containing the NGTS service account credentials.\nThe Secret must contain the following key:\n- privatekey.pem: PEM-encoded private key for the service account\nThe Secret may contain the following key:\n- clientID: Service account client ID (config.clientID must be set if not present)", + "type": "string" + }, + "helm-values.config.serverURL": { + "default": "", + "description": "The NGTS server URL (optional).\nIf not set, a production NGTS server URL will be created based on the tsg ID\nExample: https://ngts.example.com", + "type": "string" + }, + "helm-values.config.tsgID": { + "default": "", + "description": "The TSG (Tenant Security Group) ID for your NGTS account (required). NB: TSG IDs are numeric, but must be provided as strings (in double quotes).", + "type": "string" + }, + "helm-values.extraArgs": { + "default": [], + "description": "extraArgs:\n- --logging-format=json\n- --log-level=6 # To enable HTTP request logging", + "items": {}, + "type": "array" + }, + "helm-values.fullnameOverride": { + "default": "", + "type": "string" + }, + "helm-values.global": { + "description": "Global values shared across all (sub)charts" + }, + "helm-values.http_proxy": { + "description": "Configures the HTTP_PROXY environment variable where a HTTP proxy is required.", + "type": "string" + }, + "helm-values.https_proxy": { + "description": "Configures the HTTPS_PROXY environment variable where a HTTP proxy is required.", + "type": "string" + }, + "helm-values.image": { + "additionalProperties": false, + "properties": { + "digest": { + "$ref": "#/$defs/helm-values.image.digest" + }, + "name": { + "$ref": "#/$defs/helm-values.image.name" + }, + "pullPolicy": { + "$ref": "#/$defs/helm-values.image.pullPolicy" + }, + "registry": { + "$ref": "#/$defs/helm-values.image.registry" + }, + "repository": { + "$ref": "#/$defs/helm-values.image.repository" + }, + "tag": { + "$ref": "#/$defs/helm-values.image.tag" + } + }, + "type": "object" + }, + "helm-values.image.digest": { + "default": "", + "description": "Override the image digest to deploy by setting this variable. If set together with `image.tag`, the rendered image will include both tag and digest.", + "type": "string" + }, + "helm-values.image.name": { + "default": "discovery-agent", + "description": "The image name for the Discovery Agent.\nThis is used (together with `imageRegistry` and `imageNamespace`) to construct the full image reference.", + "type": "string" + }, + "helm-values.image.pullPolicy": { + "default": "IfNotPresent", + "description": "This sets the pull policy for images.", + "type": "string" + }, + "helm-values.image.registry": { + "description": "Deprecated: per-component registry prefix.\n\nIf set, this value is *prepended* to the image repository that the chart would otherwise render. This applies both when `image.repository` is set and when the repository is computed from\n`imageRegistry` + `imageNamespace` + `image.name`.\n\nThis can produce \"double registry\" style references such as\n`legacy.example.io/quay.io/jetstack/...`. Prefer using the global\n`imageRegistry`/`imageNamespace` values.", + "type": "string" + }, + "helm-values.image.repository": { + "default": "", + "description": "Full repository override (takes precedence over `imageRegistry`, `imageNamespace`, and `image.name`).\nExample: quay.io/jetstack/discovery-agent", + "type": "string" + }, + "helm-values.image.tag": { + "default": "", + "description": "Override the image tag to deploy by setting this variable. If no value is set, the chart's appVersion is used.", + "type": "string" + }, + "helm-values.imageNamespace": { + "default": "jetstack", + "description": "The repository namespace used for discovery-agent images by default.\nExamples:\n- jetstack\n- custom-namespace", + "type": "string" + }, + "helm-values.imagePullSecrets": { + "default": [], + "description": "This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/", + "items": {}, + "type": "array" + }, + "helm-values.imageRegistry": { + "default": "quay.io", + "description": "The container registry used for discovery-agent images by default. This can include path prefixes (e.g. \"artifactory.example.com/docker\").", + "type": "string" + }, + "helm-values.metrics": { + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/$defs/helm-values.metrics.enabled" + }, + "podmonitor": { + "$ref": "#/$defs/helm-values.metrics.podmonitor" + } + }, + "type": "object" + }, + "helm-values.metrics.enabled": { + "default": true, + "description": "Enable the metrics server.\nIf false, the metrics server will be disabled and the other metrics fields below will be ignored.", + "type": "boolean" + }, + "helm-values.metrics.podmonitor": { + "additionalProperties": false, + "properties": { + "annotations": { + "$ref": "#/$defs/helm-values.metrics.podmonitor.annotations" + }, + "enabled": { + "$ref": "#/$defs/helm-values.metrics.podmonitor.enabled" + }, + "endpointAdditionalProperties": { + "$ref": "#/$defs/helm-values.metrics.podmonitor.endpointAdditionalProperties" + }, + "honorLabels": { + "$ref": "#/$defs/helm-values.metrics.podmonitor.honorLabels" + }, + "interval": { + "$ref": "#/$defs/helm-values.metrics.podmonitor.interval" + }, + "labels": { + "$ref": "#/$defs/helm-values.metrics.podmonitor.labels" + }, + "namespace": { + "$ref": "#/$defs/helm-values.metrics.podmonitor.namespace" + }, + "prometheusInstance": { + "$ref": "#/$defs/helm-values.metrics.podmonitor.prometheusInstance" + }, + "scrapeTimeout": { + "$ref": "#/$defs/helm-values.metrics.podmonitor.scrapeTimeout" + } + }, + "type": "object" + }, + "helm-values.metrics.podmonitor.annotations": { + "default": {}, + "description": "Additional annotations to add to the PodMonitor.", + "type": "object" + }, + "helm-values.metrics.podmonitor.enabled": { + "default": false, + "description": "Create a PodMonitor to add the metrics to Prometheus, if you are using Prometheus Operator. See https://prometheus-operator.dev/docs/operator/api/#monitoring.coreos.com/v1.PodMonitor", + "type": "boolean" + }, + "helm-values.metrics.podmonitor.endpointAdditionalProperties": { + "default": {}, + "description": "EndpointAdditionalProperties allows setting additional properties on the endpoint such as relabelings, metricRelabelings etc.\n\nFor example:\nendpointAdditionalProperties:\n relabelings:\n - action: replace\n sourceLabels:\n - __meta_kubernetes_pod_node_name\n targetLabel: instance", + "type": "object" + }, + "helm-values.metrics.podmonitor.honorLabels": { + "default": false, + "description": "Keep labels from scraped data, overriding server-side labels.", + "type": "boolean" + }, + "helm-values.metrics.podmonitor.interval": { + "default": "60s", + "description": "The interval to scrape metrics.", + "type": "string" + }, + "helm-values.metrics.podmonitor.labels": { + "default": {}, + "description": "Additional labels to add to the PodMonitor.", + "type": "object" + }, + "helm-values.metrics.podmonitor.namespace": { + "description": "The namespace that the pod monitor should live in.\nDefaults to the discovery-agent namespace.", + "type": "string" + }, + "helm-values.metrics.podmonitor.prometheusInstance": { + "default": "default", + "description": "Specifies the `prometheus` label on the created PodMonitor. This is used when different Prometheus instances have label selectors matching different PodMonitors.", + "type": "string" + }, + "helm-values.metrics.podmonitor.scrapeTimeout": { + "default": "30s", + "description": "The timeout before a metrics scrape fails.", + "type": "string" + }, + "helm-values.nameOverride": { + "default": "", + "description": "This is to override the chart name.", + "type": "string" + }, + "helm-values.no_proxy": { + "description": "Configures the NO_PROXY environment variable where a HTTP proxy is required, but certain domains should be excluded.", + "type": "string" + }, + "helm-values.nodeSelector": { + "default": {}, + "type": "object" + }, + "helm-values.podAnnotations": { + "default": {}, + "description": "This is for setting Kubernetes Annotations to a Pod. For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/", + "type": "object" + }, + "helm-values.podDisruptionBudget": { + "default": { + "enabled": false + }, + "description": "Configure a PodDisruptionBudget for the agent's Deployment. If running with multiple replicas, consider setting podDisruptionBudget.enabled to true.", + "type": "object" + }, + "helm-values.podLabels": { + "default": {}, + "description": "This is for setting Kubernetes Labels to a Pod.\nFor more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/", + "type": "object" + }, + "helm-values.podSecurityContext": { + "default": {}, + "type": "object" + }, + "helm-values.pprof": { + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/$defs/helm-values.pprof.enabled" + } + }, + "type": "object" + }, + "helm-values.pprof.enabled": { + "default": false, + "description": "Enable profiling with the pprof endpoint", + "type": "boolean" + }, + "helm-values.replicaCount": { + "default": 1, + "description": "This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/", + "type": "number" + }, + "helm-values.resources": { + "default": {}, + "type": "object" + }, + "helm-values.securityContext": { + "default": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": [ + "ALL" + ] + }, + "readOnlyRootFilesystem": true, + "runAsNonRoot": true, + "seccompProfile": { + "type": "RuntimeDefault" + } + }, + "description": "Add Container specific SecurityContext settings to the container. Takes precedence over `podSecurityContext` when set. See https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-capabilities-for-a-container", + "type": "object" + }, + "helm-values.serviceAccount": { + "additionalProperties": false, + "properties": { + "annotations": { + "$ref": "#/$defs/helm-values.serviceAccount.annotations" + }, + "automount": { + "$ref": "#/$defs/helm-values.serviceAccount.automount" + }, + "create": { + "$ref": "#/$defs/helm-values.serviceAccount.create" + }, + "name": { + "$ref": "#/$defs/helm-values.serviceAccount.name" + } + }, + "type": "object" + }, + "helm-values.serviceAccount.annotations": { + "default": {}, + "description": "Annotations to add to the service account", + "type": "object" + }, + "helm-values.serviceAccount.automount": { + "default": true, + "description": "Automatically mount a ServiceAccount's API credentials?", + "type": "boolean" + }, + "helm-values.serviceAccount.create": { + "default": true, + "description": "Specifies whether a service account should be created", + "type": "boolean" + }, + "helm-values.serviceAccount.name": { + "default": "", + "description": "The name of the service account to use.\nIf not set and create is true, a name is generated using the fullname template", + "type": "string" + }, + "helm-values.tolerations": { + "default": [], + "items": {}, + "type": "array" + }, + "helm-values.volumeMounts": { + "default": [], + "description": "Additional volumeMounts on the output Deployment definition.", + "items": {}, + "type": "array" + }, + "helm-values.volumes": { + "default": [], + "description": "Additional volumes on the output Deployment definition.", + "items": {}, + "type": "array" + } + }, + "$ref": "#/$defs/helm-values", + "$schema": "http://json-schema.org/draft-07/schema#" +} diff --git a/deploy/charts/discovery-agent/values.yaml b/deploy/charts/discovery-agent/values.yaml new file mode 100644 index 00000000..8440f787 --- /dev/null +++ b/deploy/charts/discovery-agent/values.yaml @@ -0,0 +1,270 @@ +# Default values for discovery-agent. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +# The container registry used for discovery-agent images by default. +# This can include path prefixes (e.g. "artifactory.example.com/docker"). +# +docs:property +imageRegistry: "quay.io" + +# The repository namespace used for discovery-agent images by default. +# Examples: +# - jetstack +# - custom-namespace +# +docs:property +imageNamespace: "jetstack" + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + # Deprecated: per-component registry prefix. + # + # If set, this value is *prepended* to the image repository that the chart would otherwise render. + # This applies both when `image.repository` is set and when the repository is computed from + # `imageRegistry` + `imageNamespace` + `image.name`. + # + # This can produce "double registry" style references such as + # `legacy.example.io/quay.io/jetstack/...`. Prefer using the global + # `imageRegistry`/`imageNamespace` values. + # +docs:property + # registry: quay.io + + # Full repository override (takes precedence over `imageRegistry`, `imageNamespace`, + # and `image.name`). + # Example: quay.io/jetstack/discovery-agent + # +docs:property + repository: "" + + # The image name for the Discovery Agent. + # This is used (together with `imageRegistry` and `imageNamespace`) to construct the full + # image reference. + # +docs:property + name: discovery-agent + + # This sets the pull policy for images. + pullPolicy: IfNotPresent + + # Override the image tag to deploy by setting this variable. + # If no value is set, the chart's appVersion is used. + tag: "" + + # Override the image digest to deploy by setting this variable. + # If set together with `image.tag`, the rendered image will include both tag and digest. + digest: "" + +# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: {} +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +# Add Container specific SecurityContext settings to the container. Takes +# precedence over `podSecurityContext` when set. See +# https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-capabilities-for-a-container +# +docs:property +securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + allowPrivilegeEscalation: false + seccompProfile: { type: RuntimeDefault } + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +# Configures the HTTP_PROXY environment variable where a HTTP proxy is required. +# +docs:property +# http_proxy: "http://proxy:8080" + +# Configures the HTTPS_PROXY environment variable where a HTTP proxy is required. +# +docs:property +# https_proxy: "https://proxy:8080" + +# Configures the NO_PROXY environment variable where a HTTP proxy is required, +# but certain domains should be excluded. +# +docs:property +# no_proxy: 127.0.0.1,localhost + +# Configure a PodDisruptionBudget for the agent's Deployment. If running with multiple +# replicas, consider setting podDisruptionBudget.enabled to true. +# +docs:property +podDisruptionBudget: + # Enable or disable the PodDisruptionBudget resource, which helps prevent downtime + # during voluntary disruptions such as during a Node upgrade. + enabled: false + + # Configure the minimum available pods for disruptions. Can either be set to + # an integer (e.g. 1) or a percentage value (e.g. 25%). + # Cannot be used if `maxUnavailable` is set. + # +docs:property + # minAvailable: 1 + + # Configure the maximum unavailable pods for disruptions. Can either be set to + # an integer (e.g. 1) or a percentage value (e.g. 25%). + # Cannot be used if `minAvailable` is set. + # +docs:property + # maxUnavailable: 1 + +# Configuration for the agent +config: + # Push data every 12 hours unless changed. + period: "0h1m0s" + + # You can configure the agent to exclude some annotations or + # labels from being pushed. All Kubernetes objects + # are affected. The objects are still pushed, but the specified annotations + # and labels are removed before being pushed. + # + # Dots is the only character that needs to be escaped in the regex. Use either + # double quotes with escaped single quotes or unquoted strings for the regex + # to avoid YAML parsing issues with `\.`. + # + # Example: excludeAnnotationKeysRegex: ['^kapp\.k14s\.io/original.*'] + excludeAnnotationKeysRegex: [] + excludeLabelKeysRegex: [] + + # A human readable name for the cluster where the agent is deployed (required). + # + # This cluster name will be associated with the data that the agent uploads to + # the NGTS service. + clusterName: "" + + # A short description of the cluster where the agent is deployed (optional). + # + # This description will be associated with the data that the agent uploads to + # the NGTS service. The description may include contact + # information such as the email address of the cluster administrator. + clusterDescription: "" + + # Deprecated: Client ID for the configured service account. + # The client ID should be provided in the "clientID" field of the authentication secret (see config.secretName). + # +docs:property + clientID: "" + + # The TSG (Tenant Security Group) ID for your NGTS account (required). + # NB: TSG IDs are numeric, but must be provided as strings (in double quotes). + # +docs:property + # +docs:type=string + tsgID: "" + + # The name of the Secret containing the NGTS service account credentials. + # The Secret must contain the following key: + # - privatekey.pem: PEM-encoded private key for the service account + # The Secret may contain the following key: + # - clientID: Service account client ID (config.clientID must be set if not present) + # +docs:property + secretName: discovery-agent-credentials + + # The NGTS server URL (optional). + # If not set, a production NGTS server URL will be created based on the tsg ID + # Example: https://ngts.example.com + # +docs:hidden + serverURL: "" + +# extraArgs: +# - --logging-format=json +# - --log-level=6 # To enable HTTP request logging +extraArgs: [] + +pprof: + # Enable profiling with the pprof endpoint + enabled: false + +metrics: + # Enable the metrics server. + # If false, the metrics server will be disabled and the other metrics fields below will be ignored. + enabled: true + podmonitor: + # Create a PodMonitor to add the metrics to Prometheus, if you are using Prometheus Operator. + # See https://prometheus-operator.dev/docs/operator/api/#monitoring.coreos.com/v1.PodMonitor + enabled: false + + # The namespace that the pod monitor should live in. + # Defaults to the discovery-agent namespace. + # +docs:property + # namespace: ngts + + # Specifies the `prometheus` label on the created PodMonitor. + # This is used when different Prometheus instances have label selectors + # matching different PodMonitors. + prometheusInstance: default + + # The interval to scrape metrics. + interval: 60s + + # The timeout before a metrics scrape fails. + scrapeTimeout: 30s + + # Additional labels to add to the PodMonitor. + labels: {} + + # Additional annotations to add to the PodMonitor. + annotations: {} + + # Keep labels from scraped data, overriding server-side labels. + honorLabels: false + + # EndpointAdditionalProperties allows setting additional properties on the endpoint such as relabelings, metricRelabelings etc. + # + # For example: + # endpointAdditionalProperties: + # relabelings: + # - action: replace + # sourceLabels: + # - __meta_kubernetes_pod_node_name + # targetLabel: instance + endpointAdditionalProperties: {} diff --git a/hack/ngts/test-e2e.sh b/hack/ngts/test-e2e.sh new file mode 100755 index 00000000..99043033 --- /dev/null +++ b/hack/ngts/test-e2e.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +# +# Build and deploy the discovery-agent Helm chart for NGTS. +# Wait for the agent to log a message indicating successful data upload. +# +# Prerequisites: +# * kubectl: https://kubernetes.io/docs/tasks/tools/#kubectl +# * kind: https://kind.sigs.k8s.io/docs/user/quick-start/ +# * helm: https://helm.sh/docs/intro/install/ +# * jq: https://jqlang.github.io/jq/download/ +# * make: https://www.gnu.org/software/make/ +# +# You can run `make ngts-test-e2e` which will automatically download all +# prerequisites and then run this script. + +set -o nounset +set -o errexit +set -o pipefail + +# NGTS API configuration +: ${NGTS_CLIENT_ID?} +: ${NGTS_PRIVATE_KEY?} +: ${NGTS_TSG_ID?} + +# The base URL of the OCI registry used for Docker images and Helm charts +# E.g. ttl.sh/7e6ca67c-96dc-4dea-9437-80b0f3a69fb1 +: ${OCI_BASE?} + +# The Kubernetes namespace to install into +: ${NAMESPACE:=ngts} + +# Set to true to use an existing cluster, otherwise a new kind cluster will be created. +# Note: the cluster will not be deleted after the test completes. +: ${USE_EXISTING_CLUSTER:=false} + +script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) +root_dir=$(cd "${script_dir}/../.." && pwd) +export TERM=dumb + +tmp_dir="$(mktemp -d /tmp/jetstack-secure.XXXXX)" +trap 'rm -rf "${tmp_dir}"' EXIT + +pushd "${tmp_dir}" +> release.env +make -C "$root_dir" ngts-release \ + GITHUB_OUTPUT="${tmp_dir}/release.env" \ + OCI_SIGN_ON_PUSH=false \ + oci_platforms="" \ + NGTS_OCI_BASE="${OCI_BASE}" +cat release.env +source release.env + +if [[ "$USE_EXISTING_CLUSTER" != true ]]; then + kind create cluster || true +fi + +kubectl create ns "$NAMESPACE" || true + +kubectl delete secret agent-credentials --namespace "$NAMESPACE" --ignore-not-found +kubectl create secret generic agent-credentials \ + --namespace "$NAMESPACE" \ + --from-literal=CLIENT_ID=$NGTS_CLIENT_ID \ + --from-literal=PRIVATE_KEY="$NGTS_PRIVATE_KEY" + +# Create a sample secret in the cluster +kubectl create secret generic e2e-sample-secret-$(date '+%s') \ + --namespace default \ + --from-literal=username=${RANDOM} + +# We use a non-existent tag and omit the `--version` flag, to work around a Helm +# v4 bug. See: https://github.com/helm/helm/issues/31600 +helm upgrade agent "oci://${NGTS_CHART}:NON_EXISTENT_TAG@${NGTS_CHART_DIGEST}" \ + --install \ + --wait \ + --create-namespace \ + --namespace "$NAMESPACE" \ + --set-json extraArgs='["--log-level=6"]' \ + --set pprof.enabled=true \ + --set fullnameOverride=discovery-agent \ + --set "imageRegistry=${OCI_BASE}" \ + --set "imageNamespace=" \ + --set "image.digest=${NGTS_IMAGE_DIGEST}" \ + --set config.clusterName="e2e-test-cluster" \ + --set config.clusterDescription="A temporary cluster for E2E testing." \ + --set config.period=60s \ + --set ngts.tsgId="${NGTS_TSG_ID}" \ + --set-json "podLabels={\"discovery-agent.ngts/test-id\": \"${RANDOM}\"}" + +kubectl rollout status deployments/discovery-agent --namespace "${NAMESPACE}" + +# Wait 60s for log message indicating success. +# Parse logs as JSON using jq to ensure logs are all JSON formatted. +timeout 60 jq -n \ + 'inputs | if .msg | test("Data sent successfully") then . | halt_error(0) else . end' \ + <(kubectl logs deployments/discovery-agent --namespace "${NAMESPACE}" --follow) + +# Query the Prometheus metrics endpoint to ensure it's working. +kubectl get pod \ + --namespace ngts \ + --selector app.kubernetes.io/name=discovery-agent \ + --output jsonpath={.items[*].metadata.name} \ + | xargs -I{} kubectl get --raw /api/v1/namespaces/ngts/pods/{}:8081/proxy/metrics \ + | grep '^process_' + +# Query the pprof endpoint to ensure it's working. +kubectl get pod \ + --namespace ngts \ + --selector app.kubernetes.io/name=discovery-agent \ + --output jsonpath={.items[*].metadata.name} \ + | xargs -I{} kubectl get --raw /api/v1/namespaces/ngts/pods/{}:8081/proxy/debug/pprof/cmdline \ + | xargs -0 diff --git a/make/00_mod.mk b/make/00_mod.mk index 8a605394..f9d6069b 100644 --- a/make/00_mod.mk +++ b/make/00_mod.mk @@ -69,3 +69,4 @@ go_header_file := /dev/null include make/extra_tools.mk include make/ark/00_mod.mk +include make/ngts/00_mod.mk diff --git a/make/02_mod.mk b/make/02_mod.mk index 6973f66c..88381b3a 100644 --- a/make/02_mod.mk +++ b/make/02_mod.mk @@ -1,5 +1,6 @@ include make/test-unit.mk include make/ark/02_mod.mk +include make/ngts/02_mod.mk GITHUB_OUTPUT ?= /dev/stderr .PHONY: release diff --git a/make/ngts/00_mod.mk b/make/ngts/00_mod.mk new file mode 100644 index 00000000..67c1935b --- /dev/null +++ b/make/ngts/00_mod.mk @@ -0,0 +1,30 @@ +build_names += ngts +go_ngts_main_dir := ./cmd/ark +go_ngts_mod_dir := . +go_ngts_ldflags := \ + -X $(gomodule_name)/pkg/version.PreflightVersion=$(VERSION) \ + -X $(gomodule_name)/pkg/version.Commit=$(GITCOMMIT) \ + -X $(gomodule_name)/pkg/version.BuildDate=$(shell date "+%F-%T-%Z") + +oci_ngts_base_image_flavor := static +oci_ngts_image_name := quay.io/jetstack/discovery-agent +oci_ngts_image_tag := $(VERSION) +oci_ngts_image_name_development := jetstack.local/discovery-agent + +# Annotations are the standardised set of annotations we set on every component we publish +oci_ngts_build_args := \ + --image-annotation="org.opencontainers.image.source"="https://github.com/jetstack/jetstack-secure" \ + --image-annotation="org.opencontainers.image.vendor"="Palo Alto Networks" \ + --image-annotation="org.opencontainers.image.licenses"="Apache-2.0" \ + --image-annotation="org.opencontainers.image.authors"="Palo Alto Networks" \ + --image-annotation="org.opencontainers.image.title"="Discovery Agent for NGTS" \ + --image-annotation="org.opencontainers.image.description"="Gathers machine identity data from Kubernetes clusters for NGTS." \ + --image-annotation="org.opencontainers.image.url"="https://www.paloaltonetworks.com/" \ + --image-annotation="org.opencontainers.image.documentation"="https://docs.paloaltonetworks.com/" \ + --image-annotation="org.opencontainers.image.version"="$(VERSION)" \ + --image-annotation="org.opencontainers.image.revision"="$(GITCOMMIT)" + + +define ngts_helm_values_mutation_function +echo "no mutations defined for this chart" +endef diff --git a/make/ngts/02_mod.mk b/make/ngts/02_mod.mk new file mode 100644 index 00000000..c501d8d0 --- /dev/null +++ b/make/ngts/02_mod.mk @@ -0,0 +1,63 @@ +# Makefile targets for NGTS Discovery Agent + +# The base OCI repository for all NGTS Discovery Agent artifacts +NGTS_OCI_BASE ?= quay.io/jetstack + +# The OCI repository (without tag) for the NGTS Discovery Agent Docker image +# Can be overridden when calling `make ngts-release` to push to a different repository. +NGTS_IMAGE ?= $(NGTS_OCI_BASE)/discovery-agent + +# The OCI repository (without tag) for the NGTS Discovery Agent Helm chart +# Can be overridden when calling `make ngts-release` to push to a different repository. +NGTS_CHART ?= $(NGTS_OCI_BASE)/charts/discovery-agent + +# Used to output variables when running in GitHub Actions +GITHUB_OUTPUT ?= /dev/stderr + +.PHONY: ngts-release +## Publish all release artifacts (image + helm chart) +## @category NGTS Discovery Agent +ngts-release: oci_ngts_image_digest_path := $(bin_dir)/scratch/image/oci-layout-ngts.digests +ngts-release: helm_digest_path := $(bin_dir)/scratch/helm/discovery-agent-$(helm_chart_version).digests +ngts-release: + $(MAKE) oci-push-ngts helm-chart-oci-push \ + oci_ngts_image_name="$(NGTS_IMAGE)" \ + helm_image_name="$(NGTS_IMAGE)" \ + helm_image_tag="$(oci_ngts_image_tag)" \ + helm_chart_source_dir=deploy/charts/discovery-agent \ + helm_chart_image_name="$(NGTS_CHART)" + + @echo "NGTS_IMAGE=$(NGTS_IMAGE)" >> "$(GITHUB_OUTPUT)" + @echo "NGTS_IMAGE_TAG=$(oci_ngts_image_tag)" >> "$(GITHUB_OUTPUT)" + @echo "NGTS_IMAGE_DIGEST=$$(head -1 $(oci_ngts_image_digest_path))" >> "$(GITHUB_OUTPUT)" + @echo "NGTS_CHART=$(NGTS_CHART)" >> "$(GITHUB_OUTPUT)" + @echo "NGTS_CHART_TAG=$(helm_chart_version)" >> "$(GITHUB_OUTPUT)" + @echo "NGTS_CHART_DIGEST=$$(head -1 $(helm_digest_path))" >> "$(GITHUB_OUTPUT)" + + @echo "Release complete!" + +.PHONY: ngts-test-e2e +## Run a basic E2E test on a Kind cluster +## See `hack/ngts/e2e.sh` for the full test script. +## @category NGTS Discovery Agent +ngts-test-e2e: $(NEEDS_KIND) $(NEEDS_KUBECTL) $(NEEDS_HELM) + PATH="$(bin_dir)/tools:${PATH}" ./hack/ngts/test-e2e.sh + +.PHONY: ngts-verify +## Verify the Helm chart +## @category NGTS Discovery Agent +ngts-verify: + INSTALL_OPTIONS="--set-string config.tsgID=1234123412 --set config.clusterName=foo" $(MAKE) verify-helm-lint verify-helm-values verify-pod-security-standards verify-helm-kubeconform \ + helm_chart_source_dir=deploy/charts/discovery-agent \ + helm_chart_image_name=$(NGTS_CHART) + +shared_verify_targets += ngts-verify + +.PHONY: ngts-generate +## Generate Helm chart documentation and schema +## @category NGTS Discovery Agent +ngts-generate: + $(MAKE) generate-helm-docs generate-helm-schema \ + helm_chart_source_dir=deploy/charts/discovery-agent + +shared_generate_targets += ngts-generate diff --git a/pkg/agent/config.go b/pkg/agent/config.go index abbadb17..94c67300 100644 --- a/pkg/agent/config.go +++ b/pkg/agent/config.go @@ -346,7 +346,7 @@ func InitAgentCmdFlags(c *cobra.Command, cfg *AgentCmdFlags) { "ngts", false, "Enables NGTS mode. The agent will authenticate using key pair authentication and send data to NGTS endpoints. "+ - "Must be used in conjunction with --tsg-id, --client-id, and --private-key-path.", + "Must be used in conjunction with --tsg-id and --private-key-path. --client-id is optional if provided in the credentials secret.", ) c.PersistentFlags().StringVar( &cfg.TSGID, @@ -496,7 +496,7 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags) default: return CombinedConfig{}, nil, fmt.Errorf("no output mode specified. " + "To enable one of the output modes, you can:\n" + - " - Use --ngts with --tsg-id, --client-id, and --private-key-path to use the " + string(NGTS) + " mode.\n" + + " - Use --ngts with --tsg-id and --private-key-path to use the " + string(NGTS) + " mode (--client-id is optional if provided in the credentials secret).\n" + " - Use (--venafi-cloud with --credentials-file) or (--client-id with --private-key-path) to use the " + string(VenafiCloudKeypair) + " mode.\n" + " - Use --venafi-connection for the " + string(VenafiCloudVenafiConnection) + " mode.\n" + " - Use --credentials-file alone if you want to use the " + string(JetstackSecureOAuth) + " mode.\n" + @@ -517,9 +517,6 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags) if flags.TSGID == "" { errs = multierror.Append(errs, fmt.Errorf("--tsg-id is required when using --ngts")) } - if flags.ClientID == "" { - errs = multierror.Append(errs, fmt.Errorf("--client-id is required when using --ngts")) - } if flags.PrivateKeyPath == "" { errs = multierror.Append(errs, fmt.Errorf("--private-key-path is required when using --ngts")) } @@ -959,8 +956,8 @@ func validateCredsAndCreateClient(log logr.Logger, flagCredentialsPath, flagClie case NGTS: var creds *client.NGTSServiceAccountCredentials - if flagClientID == "" || flagPrivateKeyPath == "" { - errs = multierror.Append(errs, fmt.Errorf("both --client-id and --private-key-path are required for NGTS mode")) + if flagPrivateKeyPath == "" { + errs = multierror.Append(errs, fmt.Errorf("--private-key-path is required for NGTS mode")) break } diff --git a/pkg/agent/config_test.go b/pkg/agent/config_test.go index bbc69ce7..a01fd946 100644 --- a/pkg/agent/config_test.go +++ b/pkg/agent/config_test.go @@ -195,7 +195,7 @@ func Test_ValidateAndCombineConfig(t *testing.T) { ) assert.EqualError(t, err, testutil.Undent(` no output mode specified. To enable one of the output modes, you can: - - Use --ngts with --tsg-id, --client-id, and --private-key-path to use the NGTS mode. + - Use --ngts with --tsg-id and --private-key-path to use the NGTS mode (--client-id is optional if provided in the credentials secret). - Use (--venafi-cloud with --credentials-file) or (--client-id with --private-key-path) to use the Venafi Cloud Key Pair Service Account mode. - Use --venafi-connection for the Venafi Cloud VenafiConnection mode. - Use --credentials-file alone if you want to use the Jetstack Secure OAuth mode. @@ -1136,7 +1136,7 @@ func Test_ValidateAndCombineConfig_NGTS(t *testing.T) { `)), withCmdLineFlags("--ngts", "--tsg-id", "test-tsg-123", "--private-key-path", privKeyPath)) require.Error(t, err) - assert.Contains(t, err.Error(), "--client-id is required when using --ngts") + assert.Contains(t, err.Error(), "client_id cannot be empty") }) t.Run("ngts: missing --private-key-path should error", func(t *testing.T) { diff --git a/pkg/client/client_ngts.go b/pkg/client/client_ngts.go index b0cc8773..b71f65d0 100644 --- a/pkg/client/client_ngts.go +++ b/pkg/client/client_ngts.go @@ -12,6 +12,7 @@ import ( "io" "net/http" "net/url" + "os" "path" "strconv" "strings" @@ -73,8 +74,10 @@ const ( // The TSG ID is part of the URL. ngtsProdURLFormat = "https://%s.ngts.paloaltonetworks.com" - // ngtsUploadEndpoint matches the CM-SaaS upload endpoint - ngtsUploadEndpoint = defaultVenafiCloudUploadEndpoint + // ngtsUploadEndpoint matches the "new" CM-SaaS upload endpoint + // Note that "no" is always passed to this endpoint in other paths (e.g. in the venafi-connection client and in the venafi-kubernetes-agent chart) + // so we copy that behavior here. + ngtsUploadEndpoint = "v1/tlspk/upload/clusterdata/no" // ngtsAccessTokenEndpoint matches the CM-SaaS token endpoint // TODO: Confirm that this will match in NGTS @@ -89,6 +92,11 @@ const ( // and uploads data to NGTS endpoints. The baseURL parameter can override the default // NGTS server URL for testing purposes. func NewNGTSClient(agentMetadata *api.AgentMetadata, credentials *NGTSServiceAccountCredentials, baseURL string, tsgID string, rootCAs *x509.CertPool) (*NGTSClient, error) { + // Load ClientID from file if not provided directly + if err := credentials.LoadClientIDIfNeeded(); err != nil { + return nil, fmt.Errorf("cannot create NGTSClient: %w", err) + } + if err := credentials.Validate(); err != nil { return nil, fmt.Errorf("cannot create NGTSClient: %w", err) } @@ -150,6 +158,42 @@ func NewNGTSClient(agentMetadata *api.AgentMetadata, credentials *NGTSServiceAcc }, nil } +// LoadClientIDIfNeeded attempts to load the ClientID from a file if it is not already set. +// It looks for a "clientID" file in the same directory as the PrivateKeyFile. +// This allows the ClientID to be provided either as a direct value or via a Kubernetes secret. +func (c *NGTSServiceAccountCredentials) LoadClientIDIfNeeded() error { + if c == nil { + return fmt.Errorf("credentials are nil") + } + + // If ClientID is already set, nothing to do + if c.ClientID != "" { + return nil + } + + // If PrivateKeyFile is not set, we can't determine where to look for the clientID file + if c.PrivateKeyFile == "" { + return nil // Will be caught by Validate() later + } + + // Try to load ClientID from a file in the same directory as the private key + clientIDPath := path.Dir(c.PrivateKeyFile) + "/clientID" + clientIDBytes, err := os.ReadFile(clientIDPath) + if err != nil { + // If the file doesn't exist, that's okay - the ClientID might be required to be set directly + if os.IsNotExist(err) { + return nil + } + return fmt.Errorf("failed to read clientID from %s: %w", clientIDPath, err) + } + + // Trim whitespace from the clientID + c.ClientID = strings.TrimSpace(string(clientIDBytes)) + klog.V(2).Info("Loaded clientID from secret file", "path", clientIDPath) + + return nil +} + // Validate checks that the NGTS service account credentials are valid. func (c *NGTSServiceAccountCredentials) Validate() error { if c == nil { diff --git a/pkg/client/client_ngts_test.go b/pkg/client/client_ngts_test.go index 2bbb86be..06fd027d 100644 --- a/pkg/client/client_ngts_test.go +++ b/pkg/client/client_ngts_test.go @@ -73,7 +73,7 @@ func TestNewNGTSClient(t *testing.T) { errContains: "tsgID cannot be empty", }, { - name: "invalid credentials", + name: "missing clientID without file", credentials: &NGTSServiceAccountCredentials{ ClientID: "", PrivateKeyFile: keyFile, @@ -126,6 +126,67 @@ func TestNewNGTSClient(t *testing.T) { } } +func TestNGTSClient_LoadClientIDFromFile(t *testing.T) { + // Create a temporary directory for the secret files + tmpDir := t.TempDir() + + // Create the private key file + keyFile := tmpDir + "/privatekey.pem" + err := os.WriteFile(keyFile, []byte(fakePrivKeyPEM), 0600) + require.NoError(t, err) + + // Create the clientID file in the same directory + clientIDFile := tmpDir + "/clientID" + err = os.WriteFile(clientIDFile, []byte("test-client-from-file\n"), 0600) + require.NoError(t, err) + + tests := []struct { + name string + credentials *NGTSServiceAccountCredentials + wantErr bool + wantClient string + }{ + { + name: "load clientID from file", + credentials: &NGTSServiceAccountCredentials{ + ClientID: "", // Empty - should be loaded from file + PrivateKeyFile: keyFile, + }, + wantErr: false, + wantClient: "test-client-from-file", + }, + { + name: "explicit clientID takes precedence", + credentials: &NGTSServiceAccountCredentials{ + ClientID: "explicit-client-id", + PrivateKeyFile: keyFile, + }, + wantErr: false, + wantClient: "explicit-client-id", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + metadata := &api.AgentMetadata{ + Version: "test-version", + ClusterID: "test-cluster", + } + + client, err := NewNGTSClient(metadata, tt.credentials, "https://test.example.com", "test-tsg", nil) + + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.NotNil(t, client) + assert.Equal(t, tt.wantClient, client.credentials.ClientID) + }) + } +} + func TestNGTSClient_PostDataReadingsWithOptions(t *testing.T) { keyFile := withFile(t, fakePrivKeyPEM)