Skip to content

Commit 0bfd54d

Browse files
Add DCGM, DCGM Exporter, and DRA validation operands to GPUCluster
Signed-off-by: Karthik Vetrivel <kvetrivel@nvidia.com>
1 parent 011b9dd commit 0bfd54d

26 files changed

Lines changed: 1523 additions & 4 deletions
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/**
2+
# Copyright (c) NVIDIA CORPORATION. All rights reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
**/
16+
17+
package state
18+
19+
import (
20+
"context"
21+
"fmt"
22+
23+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
24+
25+
nvidiav1alpha1 "github.com/NVIDIA/gpu-operator/api/nvidia/v1alpha1"
26+
"github.com/NVIDIA/gpu-operator/internal/image"
27+
)
28+
29+
// configurableState is a State implementation shared by the GPUCluster operands
30+
// that reconcile with the same shape: skip when disabled, resolve the DRA apiVersion,
31+
// label the namespace for adminAccess, resolve the operand image, build the operand's
32+
// render data, and render. Per-operand behavior is injected through the function fields
33+
// so each operand file only declares what actually differs.
34+
type configurableState struct {
35+
stateSkel
36+
37+
// isEnabled reports whether this operand should be deployed for the given CR.
38+
isEnabled func(cr *nvidiav1alpha1.GPUCluster) bool
39+
40+
// imageOverride resolves the (repository, image, version) overrides from the CR,
41+
// which feed image.ImagePath together with imageEnvName. It may be nil for operands
42+
// that carry no per-CR image overrides (e.g. those running the gpu-operator image).
43+
imageOverride func(cr *nvidiav1alpha1.GPUCluster) (repository, image, version string)
44+
45+
// imageEnvName is the fallback env var for the operand image.
46+
imageEnvName string
47+
48+
// buildRenderData builds the operand-specific templating data from the resolved
49+
// image path and DRA apiVersion. It receives ctx and the skeleton so operands that
50+
// need the client or logging (e.g. dcgm-exporter's ServiceMonitor CRD probe) can use them.
51+
buildRenderData func(ctx context.Context, s *configurableState, cr *nvidiav1alpha1.GPUCluster, imagePath, apiVersion string) (interface{}, error)
52+
}
53+
54+
var _ State = (*configurableState)(nil)
55+
56+
func (s *configurableState) Sync(ctx context.Context, customResource interface{}, infoCatalog InfoCatalog) (SyncState, error) {
57+
cr, ok := customResource.(*nvidiav1alpha1.GPUCluster)
58+
if !ok {
59+
return SyncStateError, fmt.Errorf("GPUCluster CR not provided as input to Sync()")
60+
}
61+
62+
objs, err := s.getManifestObjects(ctx, cr, infoCatalog)
63+
if err != nil {
64+
return SyncStateNotReady, fmt.Errorf("failed to create k8s objects from manifests: %w", err)
65+
}
66+
67+
if len(objs) == 0 {
68+
return s.handleStateObjectsDeletion(ctx)
69+
}
70+
71+
return s.syncObjects(ctx, cr, objs)
72+
}
73+
74+
func (s *configurableState) GetWatchSources(mgr ctrlManager) map[string]SyncingSource {
75+
return gpuClusterDaemonSetSource(mgr)
76+
}
77+
78+
func (s *configurableState) getManifestObjects(ctx context.Context, cr *nvidiav1alpha1.GPUCluster, infoCatalog InfoCatalog) ([]*unstructured.Unstructured, error) {
79+
if !s.isEnabled(cr) {
80+
return []*unstructured.Unstructured{}, nil
81+
}
82+
83+
apiVersion, err := draResourceAPIVersion(infoCatalog)
84+
if err != nil {
85+
return nil, err
86+
}
87+
88+
if err := ensureAdminAccessLabel(ctx, s.client, s.namespace); err != nil {
89+
return nil, fmt.Errorf("failed to label namespace for admin access: %w", err)
90+
}
91+
92+
var repository, img, version string
93+
if s.imageOverride != nil {
94+
repository, img, version = s.imageOverride(cr)
95+
}
96+
imagePath, err := image.ImagePath(repository, img, version, s.imageEnvName)
97+
if err != nil {
98+
return nil, fmt.Errorf("failed to construct %s image path: %w", s.name, err)
99+
}
100+
101+
renderData, err := s.buildRenderData(ctx, s, cr, imagePath, apiVersion)
102+
if err != nil {
103+
return nil, err
104+
}
105+
106+
return s.renderObjects(ctx, renderData)
107+
}

internal/state/dcgm.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
# Copyright (c) NVIDIA CORPORATION. All rights reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
**/
16+
17+
package state
18+
19+
import (
20+
"context"
21+
22+
"k8s.io/apimachinery/pkg/runtime"
23+
"sigs.k8s.io/controller-runtime/pkg/client"
24+
25+
nvidiav1alpha1 "github.com/NVIDIA/gpu-operator/api/nvidia/v1alpha1"
26+
)
27+
28+
const (
29+
// dcgmImageEnvName is the fallback env var for the DCGM image when the CR does not
30+
// specify repository/image/version.
31+
dcgmImageEnvName = "DCGM_IMAGE"
32+
)
33+
34+
func NewStateDCGM(
35+
k8sClient client.Client,
36+
namespace string,
37+
scheme *runtime.Scheme,
38+
manifestDir string) (State, error) {
39+
40+
skel, err := newStateSkel(k8sClient, namespace, scheme, manifestDir,
41+
"state-dcgm", "NVIDIA DCGM hostengine deployed in the cluster")
42+
if err != nil {
43+
return nil, err
44+
}
45+
return &configurableState{
46+
stateSkel: skel,
47+
isEnabled: dcgmEnabled,
48+
imageOverride: func(cr *nvidiav1alpha1.GPUCluster) (string, string, string) {
49+
spec := cr.Spec.DCGM
50+
return spec.Repository, spec.Image, spec.Version
51+
},
52+
imageEnvName: dcgmImageEnvName,
53+
buildRenderData: buildDCGMRenderData,
54+
}, nil
55+
}
56+
57+
func buildDCGMRenderData(_ context.Context, s *configurableState, cr *nvidiav1alpha1.GPUCluster, imagePath, apiVersion string) (interface{}, error) {
58+
daemonsets := cr.Spec.Daemonsets
59+
return &dcgmRenderData{
60+
DCGM: &dcgmSpec{Spec: cr.Spec.DCGM, ImagePath: imagePath},
61+
Daemonsets: &daemonsets,
62+
Namespace: s.namespace,
63+
ResourceClaimAPIVersion: apiVersion,
64+
}, nil
65+
}

internal/state/dcgm_exporter.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
# Copyright (c) NVIDIA CORPORATION. All rights reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
**/
16+
17+
package state
18+
19+
import (
20+
"context"
21+
"path/filepath"
22+
"strings"
23+
24+
"k8s.io/apimachinery/pkg/runtime"
25+
"k8s.io/apimachinery/pkg/runtime/schema"
26+
"sigs.k8s.io/controller-runtime/pkg/client"
27+
"sigs.k8s.io/controller-runtime/pkg/log"
28+
29+
nvidiav1alpha1 "github.com/NVIDIA/gpu-operator/api/nvidia/v1alpha1"
30+
"github.com/NVIDIA/gpu-operator/internal/consts"
31+
)
32+
33+
const (
34+
// dcgmExporterImageEnvName is the fallback env var for the dcgm-exporter image when the
35+
// CR does not specify repository/image/version.
36+
dcgmExporterImageEnvName = "DCGM_EXPORTER_IMAGE"
37+
38+
// dcgmRemoteHostEngine points dcgm-exporter at the standalone nvidia-dcgm hostengine
39+
// Service (manifests/state-dcgm/0600_service.yaml).
40+
dcgmRemoteHostEngine = "nvidia-dcgm:5555"
41+
42+
dcgmExporterDefaultCollectors = "/etc/dcgm-exporter/dcp-metrics-included.csv"
43+
dcgmExporterCustomCollectors = "/etc/dcgm-exporter/dcgm-metrics.csv"
44+
dcgmExporterDefaultKubeletRootDir = "/var/lib/kubelet"
45+
dcgmExporterDefaultJobMappingDir = "/var/lib/dcgm-exporter/job-mapping"
46+
)
47+
48+
func NewStateDCGMExporter(
49+
k8sClient client.Client,
50+
namespace string,
51+
scheme *runtime.Scheme,
52+
manifestDir string) (State, error) {
53+
54+
skel, err := newStateSkel(k8sClient, namespace, scheme, manifestDir,
55+
"state-dcgm-exporter", "NVIDIA DCGM Exporter deployed in the cluster")
56+
if err != nil {
57+
return nil, err
58+
}
59+
return &configurableState{
60+
stateSkel: skel,
61+
isEnabled: func(cr *nvidiav1alpha1.GPUCluster) bool {
62+
return cr.Spec.DCGMExporter != nil && cr.Spec.DCGMExporter.IsEnabled()
63+
},
64+
imageOverride: func(cr *nvidiav1alpha1.GPUCluster) (string, string, string) {
65+
spec := cr.Spec.DCGMExporter
66+
return spec.Repository, spec.Image, spec.Version
67+
},
68+
imageEnvName: dcgmExporterImageEnvName,
69+
buildRenderData: buildDCGMExporterRenderData,
70+
}, nil
71+
}
72+
73+
func buildDCGMExporterRenderData(ctx context.Context, s *configurableState, cr *nvidiav1alpha1.GPUCluster, imagePath, apiVersion string) (interface{}, error) {
74+
spec := cr.Spec.DCGMExporter
75+
76+
// When standalone DCGM is enabled the exporter targets it; otherwise it runs embedded.
77+
remoteHostEngine := ""
78+
if dcgmEnabled(cr) {
79+
remoteHostEngine = dcgmRemoteHostEngine
80+
}
81+
82+
collectors := dcgmExporterDefaultCollectors
83+
metricsConfigName := ""
84+
if spec.MetricsConfig != nil && spec.MetricsConfig.Name != "" {
85+
metricsConfigName = spec.MetricsConfig.Name
86+
collectors = dcgmExporterCustomCollectors
87+
}
88+
89+
hpcJobMappingDir := ""
90+
if spec.IsHPCJobMappingEnabled() {
91+
hpcJobMappingDir = spec.GetHPCJobMappingDirectory()
92+
if hpcJobMappingDir == "" {
93+
hpcJobMappingDir = dcgmExporterDefaultJobMappingDir
94+
}
95+
}
96+
97+
kubeletRootDir := cr.Spec.HostPaths.KubeletRootDir
98+
if kubeletRootDir == "" {
99+
kubeletRootDir = dcgmExporterDefaultKubeletRootDir
100+
}
101+
102+
// Skip the ServiceMonitor when its CRD is absent so a default install without the
103+
// Prometheus Operator does not fail (matches the ClusterPolicy path).
104+
serviceMonitorEnabled := spec.ServiceMonitor != nil &&
105+
spec.ServiceMonitor.Enabled != nil && *spec.ServiceMonitor.Enabled
106+
if serviceMonitorEnabled && !serviceMonitorCRDServed(s.client) {
107+
log.FromContext(ctx).V(consts.LogLevelInfo).Info(
108+
"ServiceMonitor CRD not served; skipping dcgm-exporter ServiceMonitor creation")
109+
serviceMonitorEnabled = false
110+
}
111+
112+
serviceType := "ClusterIP"
113+
serviceInternalTrafficPolicy := ""
114+
if spec.ServiceSpec != nil {
115+
if spec.ServiceSpec.Type != "" {
116+
serviceType = string(spec.ServiceSpec.Type)
117+
}
118+
if spec.ServiceSpec.InternalTrafficPolicy != nil {
119+
serviceInternalTrafficPolicy = string(*spec.ServiceSpec.InternalTrafficPolicy)
120+
}
121+
}
122+
123+
daemonsets := cr.Spec.Daemonsets
124+
return &dcgmExporterRenderData{
125+
DCGMExporter: &dcgmExporterSpec{Spec: spec, ImagePath: imagePath},
126+
Daemonsets: &daemonsets,
127+
Namespace: s.namespace,
128+
ResourceClaimAPIVersion: apiVersion,
129+
RemoteHostEngine: remoteHostEngine,
130+
Collectors: collectors,
131+
HPCJobMappingDir: hpcJobMappingDir,
132+
PodLabelAllowlistRegex: strings.Join(spec.PodLabelAllowlistRegex, ","),
133+
PodMetadataEnabled: spec.IsKubernetesPodMetadataEnabled(),
134+
EnablePodLabels: spec.IsPodLabelsEnabled(),
135+
EnablePodUID: spec.IsPodUIDEnabled(),
136+
HostPID: spec.IsHostPIDEnabled(),
137+
HostNetwork: spec.IsHostNetworkEnabled(),
138+
MetricsConfigName: metricsConfigName,
139+
ServiceMonitorEnabled: serviceMonitorEnabled,
140+
PodResourcesDir: filepath.Join(kubeletRootDir, "pod-resources"),
141+
ServiceType: serviceType,
142+
ServiceInternalTrafficPolicy: serviceInternalTrafficPolicy,
143+
}, nil
144+
}
145+
146+
// serviceMonitorCRDServed reports whether the cluster serves the monitoring.coreos.com
147+
// ServiceMonitor kind (i.e. the Prometheus Operator CRDs are installed).
148+
func serviceMonitorCRDServed(k8sClient client.Client) bool {
149+
_, err := k8sClient.RESTMapper().RESTMapping(
150+
schema.GroupKind{Group: "monitoring.coreos.com", Kind: "ServiceMonitor"}, "v1")
151+
return err == nil
152+
}

0 commit comments

Comments
 (0)