diff --git a/.coderabbit.yaml b/.coderabbit.yaml index a1b544861..907f1981d 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -16,3 +16,14 @@ reviews: profile: chill auto_review: enabled: true + path_filters: + - "!api/gen/**" # Ignore generated protobuf bindings + - "!api/**/zz_generated.*" # Ignore generated deepcopy and conversion code + - "!client-go/client/**" # Ignore generated clientset + - "!client-go/listers/**" # Ignore generated listers + - "!client-go/informers/**" # Ignore generated informers + - "!code-generator/cmd/client-gen/args/gvtype.go" # Ignore copied, unmodified upstream code + - "!code-generator/cmd/client-gen/args/gvpackages.go" # Ignore copied, unmodified upstream code + - "!code-generator/cmd/client-gen/types/**" # Ignore copied, unmodified upstream code + - "!code-generator/cmd/client-gen/generators/scheme/**" # Ignore copied, unmodified upstream code + - "!code-generator/cmd/client-gen/generators/util/**" # Ignore copied, unmodified upstream code diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..b02edc128 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,21 @@ +# ============================================================================== +# GIT ATTRIBUTES +# ============================================================================== +# Use 'linguist-generated=true' to hide generated code from GitHub PR diffs. +# ============================================================================== + +# Hide Kubernetes generated helpers (DeepCopy, Defaults, Conversions, OpenAPI, etc) +zz_generated.*.go linguist-generated=true + +# Hide generated Protobuf Go bindings +*.pb.go linguist-generated=true + +# Hide generated client library +client-go/client/** linguist-generated=true +client-go/listers/** linguist-generated=true +client-go/informers/** linguist-generated=true + +# Hide copied, unmodified upstream code +code-generator/cmd/client-gen/types/** linguist-generated=true +code-generator/cmd/client-gen/generators/scheme/** linguist-generated=true +code-generator/cmd/client-gen/generators/util/** linguist-generated=true diff --git a/.gitignore b/.gitignore index 95aa540bc..a7a20e3d6 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ *.so *.dylib api/bin/* +client-go/bin/* # Test binary, built with `go test -c` *.test diff --git a/.versions.yaml b/.versions.yaml index 785247e92..3fbbe984b 100644 --- a/.versions.yaml +++ b/.versions.yaml @@ -46,6 +46,8 @@ go_tools: setup_envtest: 'latest' goimports: 'v0.30.0' crane: 'v0.20.2' + goverter: 'v1.9.3' + kubernetes_code_gen: 'v0.34.1' # Protocol Buffers / gRPC protobuf: diff --git a/Makefile b/Makefile index a89167e04..6944537d2 100644 --- a/Makefile +++ b/Makefile @@ -340,6 +340,7 @@ license-headers-lint: ## Check license headers in source files -ignore '**/*.toml' \ -ignore '**/*lock.hcl' \ -ignore '**/*pb2*' \ + -ignore '**/hack/**' \ . # Check go.mod files for proper replace directives diff --git a/api/DEVELOPMENT.md b/api/DEVELOPMENT.md new file mode 100644 index 000000000..8c6e7eb81 --- /dev/null +++ b/api/DEVELOPMENT.md @@ -0,0 +1,23 @@ +# Development + +## API Development Workflow +Follow these steps to add new resources or update existing ones (e.g., `gpu_types.go`). + +1. **Go Definitions**: Edit `device/${VERSION}/${TYPE}_types.go`. +2. **Proto Definitions**: Edit `proto/device/${VERSION}/${TYPE}.proto`. +3. **Registration**: Add the types to `addKnownTypes` in `device/${VERSION}/register.go`. +4. **Conversion**: Edit `device/${VERSION}/converter.go`. See [Goverter](https://github.com/jmattheis/goverter) documentation for additional details. +5. **Generate**: Run `make code-gen` to (re)generate Go helper functions (e.g., `zz_generated.deepcopy.go`, `zz_generated.goverter.go`) and Go protobuf API and gRPC service bindings (e.g., `gen/go/device/${version}/${type}.pb.go`, `gen/go/device/${version}/${type}_grpc.pb.go`). + +## Conventions +- **Kubernetes Resource Model (KRM)**: + - All Go type definitions must strictly follow the standard [Kubernetes Resource Model](https://github.com/kubernetes/design-proposals-archive/blob/main/architecture/resource-management.md). + - The Protobuf metadata representations _should_ be a subset of the full Kubernetes metadata containing only the minimum necessary fields. + +## Housekeeping +If you need to reset your environment: + +```bash +# Removes generated code (deepcopy, goverter) +make clean +``` diff --git a/api/Makefile b/api/Makefile index 4bd7652ef..8583fff0e 100644 --- a/api/Makefile +++ b/api/Makefile @@ -16,41 +16,16 @@ # Configuration # ============================================================================== -# Directories -PROTO_DIR := proto -GEN_DIR := gen/go - # Shell setup SHELL = /usr/bin/env bash -o pipefail .SHELLFLAGS = -ec -# Location to install dependencies to -LOCALBIN ?= $(shell pwd)/bin -$(LOCALBIN): - mkdir -p $(LOCALBIN) - -# Tool Binaries -PROTOC_GEN_GO ?= $(LOCALBIN)/protoc-gen-go -PROTOC_GEN_GO_GRPC ?= $(LOCALBIN)/protoc-gen-go-grpc - -# Tool Versions (load from ../.versions.yaml) -# Requires yq to be installed: brew install yq (macOS) or see https://github.com/mikefarah/yq -YQ := $(shell command -v yq 2> /dev/null) -ifndef YQ -$(error yq is required but not found. Install it with: brew install yq (macOS) or see https://github.com/mikefarah/yq) -endif - -VERSIONS_FILE := ../.versions.yaml - -PROTOC_GEN_GO_VERSION := $(shell $(YQ) '.protobuf.protoc_gen_go' $(VERSIONS_FILE)) -PROTOC_GEN_GO_GRPC_VERSION := $(shell $(YQ) '.protobuf.protoc_gen_go_grpc' $(VERSIONS_FILE)) - # ============================================================================== # Targets # ============================================================================== .PHONY: all -all: protos-generate build +all: code-gen test build ## Run code generation, compile all code, and execute tests. ##@ General @@ -60,66 +35,25 @@ help: ## Display this help. ##@ Development -.PHONY: build -build: - go build -v ./... - -.PHONY: protos-generate -protos-generate: $(PROTOC_GEN_GO) $(PROTOC_GEN_GO_GRPC) protos-clean ## Generate Go code from Proto definitions. - @echo "Generating Proto code..." - @mkdir -p $(GEN_DIR) - cd proto && \ - protoc \ - -I . \ - -I ../$(THIRD_PARTY_DIR) \ - --plugin="protoc-gen-go=$(PROTOC_GEN_GO)" \ - --plugin="protoc-gen-go-grpc=$(PROTOC_GEN_GO_GRPC)" \ - --go_out=../$(GEN_DIR) \ - --go_opt=paths=source_relative \ - --go-grpc_out=../$(GEN_DIR) \ - --go-grpc_opt=paths=source_relative \ - device/v1alpha1/*.proto && \ - protoc \ - -I . \ - -I ../$(THIRD_PARTY_DIR) \ - --plugin="protoc-gen-go=$(PROTOC_GEN_GO)" \ - --plugin="protoc-gen-go-grpc=$(PROTOC_GEN_GO_GRPC)" \ - --go_out=../$(GEN_DIR) \ - --go_opt=paths=source_relative \ - --go-grpc_out=../$(GEN_DIR) \ - --go-grpc_opt=paths=source_relative \ - csp/v1alpha1/*.proto - @echo "Cleaning up dependencies..." +.PHONY: code-gen +code-gen: ## Run all code generation targets. + @echo "Generating code..." + ./hack/update-codegen.sh + @echo "Synchronizing module dependencies..." go mod tidy - @echo "Done." - -.PHONY: protos-clean -protos-clean: ## Remove generated code. - @echo "Cleaning old generated Proto code..." - rm -rf $(GEN_DIR) - @echo "Done." -##@ Build Dependencies - -$(PROTOC_GEN_GO): - $(call go-install-tool,$(PROTOC_GEN_GO),google.golang.org/protobuf/cmd/protoc-gen-go,$(PROTOC_GEN_GO_VERSION)) +.PHONY: build +build: code-gen ## Compile all Go code after generation to verify type safety. + @echo "Compiling..." + go build -v ./... -$(PROTOC_GEN_GO_GRPC): - $(call go-install-tool,$(PROTOC_GEN_GO_GRPC),google.golang.org/grpc/cmd/protoc-gen-go-grpc,$(PROTOC_GEN_GO_GRPC_VERSION)) +.PHONY: test +test: code-gen ## Run unit tests and generate coverage after code generation. + @echo "Testing..." + go test -v $$(go list ./... | grep -v /gen/) -coverprofile cover.out -# go-install-tool macro -# $1 - target path with name of binary -# $2 - package url which can be installed -# $3 - specific version of package -define go-install-tool -@[ -f "$(1)-$(3)" ] || { \ -set -e; \ -mkdir -p $(LOCALBIN); \ -package=$(2)@$(3) ;\ -echo "Downloading $${package}" ;\ -rm -f $(1) || true ;\ -GOBIN=$(LOCALBIN) go install $${package} ;\ -mv $(1) $(1)-$(3) ;\ -} ;\ -ln -sf $(1)-$(3) $(1) -endef +.PHONY: clean +clean: ## Remove all generated code. + @echo "Cleaning generated code..." + rm -rf gen/go + find . -name "zz_generated.*.go" -delete diff --git a/api/README.md b/api/README.md new file mode 100644 index 000000000..3642c55ac --- /dev/null +++ b/api/README.md @@ -0,0 +1,18 @@ +# API Definitions +This module contains the canonical API definitions, serving as the source of truth for both Go SDKs and gRPC wire formats. + +## Structure +* **`device/`**: Contains the **Kubernetes API type definitions** (e.g., `GPU` struct with `Spec` and `Status`). +* **`proto/`**: Contains the **Protobuf Message and gRPC Service Definitions**. + * *Note:* The `ObjectMeta` and `ListMeta` messages are subsets of `k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta` and `k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta` respectively. +* **`gen/go/`**: Contains the **Generated Go Protobuf API and gRPC Service Bindings** (e.g., `gpu.pb.go`, `gpu_grpc.pb.go`) compiled from the protobuf definitions in `proto/`. **Do not edit these files manually.** + +## Code Generation +To (re)generate Go helper functions (e.g., `zz_generated.deepcopy.go`) and Go protobuf API and gRPC service bindings, run: + +```bash +make code-gen +``` + +## Development +See [DEVELOPMENT.md](DEVELOPMENT.md) for details on modifying the API definitions. diff --git a/api/device/v1alpha1/converter.go b/api/device/v1alpha1/converter.go new file mode 100644 index 000000000..9d63005fc --- /dev/null +++ b/api/device/v1alpha1/converter.go @@ -0,0 +1,136 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1alpha1 + +import ( + pb "github.com/nvidia/nvsentinel/api/gen/go/device/v1alpha1" + "google.golang.org/protobuf/types/known/timestamppb" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Converter is the interface used to generate type conversion methods between the +// Kubernetes Resource Model structs and the Protobuf message structs. +// +// goverter:converter +// goverter:output:file ./zz_generated.goverter.go +// goverter:extend FromProtobufTypeMeta FromProtobufListTypeMeta FromProtobufTimestamp ToProtobufTimestamp +// goverter:useZeroValueOnPointerInconsistency +type Converter interface { + // FromProtobuf converts a protobuf Gpu message into a GPU object. + // + // goverter:map . TypeMeta | FromProtobufTypeMeta + // goverter:map Metadata ObjectMeta + FromProtobuf(source *pb.Gpu) GPU + + // ToProtobuf converts a GPU object into a protobuf Gpu message. + // + // goverter:map ObjectMeta Metadata + // goverter:ignore state sizeCache unknownFields + ToProtobuf(source GPU) *pb.Gpu + + // FromProtobufList converts a protobuf GpuList message into a GPUList object. + // + // goverter:map . TypeMeta | FromProtobufListTypeMeta + // goverter:map Metadata ListMeta + FromProtobufList(source *pb.GpuList) *GPUList + + // ToProtobufList converts a GPUList object into a protobuf GpuList message. + // + // goverter:map ListMeta Metadata + // goverter:ignore state sizeCache unknownFields + ToProtobufList(source *GPUList) *pb.GpuList + + // FromProtobufObjectMeta converts a protobuf ObjectMeta into a metav1.ObjectMeta object. + // + // goverter:ignoreMissing + FromProtobufObjectMeta(source *pb.ObjectMeta) metav1.ObjectMeta + + // ToProtobufObjectMeta converts a metav1.ObjectMeta into a protobuf Object message. + // + // goverter:ignore state sizeCache unknownFields + ToProtobufObjectMeta(source metav1.ObjectMeta) *pb.ObjectMeta + + // FromProtobufListMeta converts a protobuf ListMeta into a metav1.ListMeta object. + // + // goverter:ignore SelfLink Continue RemainingItemCount + FromProtobufListMeta(source *pb.ListMeta) metav1.ListMeta + + // ToProtobufListMeta converts a metav1.ListMeta into a protobuf ListMeta message. + // + // goverter:ignore state sizeCache unknownFields + ToProtobufListMeta(source metav1.ListMeta) *pb.ListMeta + + // FromProtobufSpec converts a protobuf GpuSpec message into a GPUSpec object. + // + // goverter:map Uuid UUID + FromProtobufSpec(source *pb.GpuSpec) GPUSpec + + // ToProtobufSpec converts a GPUSpec object into a protobuf GpuSpec message. + // + // goverter:map UUID Uuid + // goverter:ignore state sizeCache unknownFields + ToProtobufSpec(source GPUSpec) *pb.GpuSpec + + // FromProtobufStatus converts a protobuf GpuStatus message into a GPUStatus object. + FromProtobufStatus(source *pb.GpuStatus) GPUStatus + + // ToProtobufStatus converts a GPUStatus object into a protobuf GpuStatus message. + // + // goverter:ignore state sizeCache unknownFields + ToProtobufStatus(source GPUStatus) *pb.GpuStatus + + // FromProtobufCondition converts a protobuf Condition message into a metav1.Condition object. + // + // goverter:ignore ObservedGeneration + // Note: ObservedGeneration is specific to k8s and not found in the protobuf Condition message. + FromProtobufCondition(source *pb.Condition) metav1.Condition + + // ToProtobufCondition converts a metav1.Condition object into a protobuf Condition message. + // + // goverter:ignore state sizeCache unknownFields + ToProtobufCondition(source metav1.Condition) *pb.Condition +} + +// FromProtobufTypeMeta generates the standard TypeMeta for the root GPU resource. +func FromProtobufTypeMeta(_ *pb.Gpu) metav1.TypeMeta { + return metav1.TypeMeta{ + Kind: "GPU", + APIVersion: SchemeGroupVersion.String(), + } +} + +// FromProtobufListTypeMeta generates the standard TypeMeta for the GPUList resource. +func FromProtobufListTypeMeta(_ *pb.GpuList) metav1.TypeMeta { + return metav1.TypeMeta{ + Kind: "GPUList", + APIVersion: SchemeGroupVersion.String(), + } +} + +// FromProtobufTimestamp converts a protobuf Timestamp message to a metav1.Time. +func FromProtobufTimestamp(source *timestamppb.Timestamp) metav1.Time { + if source == nil { + return metav1.Time{} + } + return metav1.NewTime(source.AsTime()) +} + +// ToProtobufTimestamp converts a metav1.Time to a protobuf Timestamp message. +func ToProtobufTimestamp(source metav1.Time) *timestamppb.Timestamp { + if source.IsZero() { + return nil + } + return timestamppb.New(source.Time) +} diff --git a/api/device/v1alpha1/doc.go b/api/device/v1alpha1/doc.go new file mode 100644 index 000000000..15af71d19 --- /dev/null +++ b/api/device/v1alpha1/doc.go @@ -0,0 +1,19 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +groupName=device.nvidia.com + +// Package v1alpha1 contains the API Schema definitions for the device.nvidia.com v1alpha1 API group. +// This package includes types for GPU resources and conversions between Kubernetes and protobuf representations. +package v1alpha1 diff --git a/api/device/v1alpha1/gpu_conversion.go b/api/device/v1alpha1/gpu_conversion.go new file mode 100644 index 000000000..76031c5d5 --- /dev/null +++ b/api/device/v1alpha1/gpu_conversion.go @@ -0,0 +1,56 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !goverter + +package v1alpha1 + +import pb "github.com/nvidia/nvsentinel/api/gen/go/device/v1alpha1" + +// converter is the singleton instance of the generated Converter implementation. +var converter Converter = &ConverterImpl{} + +// FromProto converts a protobuf Gpu message pointer to a GPU object pointer. +func FromProto(in *pb.Gpu) *GPU { + if in == nil { + return nil + } + + val := converter.FromProtobuf(in) + return &val +} + +// ToProto converts a GPU object pointer to a protobuf Gpu message pointer. +func ToProto(in *GPU) *pb.Gpu { + if in == nil { + return nil + } + return converter.ToProtobuf(*in) +} + +// FromProtoList converts a protobuf GpuList message pointer to a GPUList object pointer. +func FromProtoList(in *pb.GpuList) *GPUList { + if in == nil { + return nil + } + return converter.FromProtobufList(in) +} + +// ToProtoList converts a GPUList object pointer to a protobuf GpuList message pointer. +func ToProtoList(in *GPUList) *pb.GpuList { + if in == nil { + return nil + } + return converter.ToProtobufList(in) +} diff --git a/api/device/v1alpha1/gpu_conversion_test.go b/api/device/v1alpha1/gpu_conversion_test.go new file mode 100644 index 000000000..00767b5ea --- /dev/null +++ b/api/device/v1alpha1/gpu_conversion_test.go @@ -0,0 +1,153 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1alpha1 + +import ( + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + pb "github.com/nvidia/nvsentinel/api/gen/go/device/v1alpha1" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var lastTransitionTime = time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC) + +func TestGPUConversion_Nil(t *testing.T) { + if out := FromProto(nil); out != nil { + t.Errorf("FromProto(nil): got %#v, expected nil", out) + } + if out := ToProto(nil); out != nil { + t.Errorf("ToProto(nil): got %#v, expected nil", out) + } + if out := FromProtoList(nil); out != nil { + t.Errorf("FromProtoList(nil): got %#v, expected nil", out) + } + if out := ToProtoList(nil); out != nil { + t.Errorf("ToProtoList(nil): got %#v, expected nil", out) + } +} + +func TestGPUConversion(t *testing.T) { + + protoIn := &pb.Gpu{ + Metadata: &pb.ObjectMeta{ + Name: "gpu-1111", + ResourceVersion: "1", + }, + Spec: &pb.GpuSpec{ + Uuid: "GPU-1111", + }, + Status: &pb.GpuStatus{ + Conditions: []*pb.Condition{ + { + Type: "Ready", + Status: "False", + LastTransitionTime: timestamppb.New(lastTransitionTime), + Reason: "DriverCrash", + Message: "The driver has stopped responding.", + }, + }, + RecommendedAction: "ResetGPU", + }, + } + + goStruct := FromProto(protoIn) + + expectedName := strings.ToLower(protoIn.Metadata.Name) + if goStruct.ObjectMeta.Name != expectedName { + t.Errorf("ObjectMeta.Name conversion failed: got %q, want %q", + goStruct.ObjectMeta.Name, expectedName) + } + + expectedResourceVersion := "1" + if goStruct.ObjectMeta.ResourceVersion != expectedResourceVersion { + t.Errorf("ObjectMeta.ResourceVersion conversion failed: got %q, want %q", + goStruct.ObjectMeta.ResourceVersion, expectedResourceVersion) + } + + protoOut := ToProto(goStruct) + + if diff := cmp.Diff(protoIn, protoOut, protocmp.Transform()); diff != "" { + t.Errorf("Conversion failed (-want +got):\n%s", diff) + } +} + +func TestGPUListConversion(t *testing.T) { + protoIn := &pb.GpuList{ + Metadata: &pb.ListMeta{ + ResourceVersion: "2", + }, + Items: []*pb.Gpu{ + { + Metadata: &pb.ObjectMeta{ + Name: "gpu-1111", + ResourceVersion: "1", + }, + Spec: &pb.GpuSpec{ + Uuid: "GPU-1111", + }, + Status: &pb.GpuStatus{ + Conditions: []*pb.Condition{ + { + Type: "Ready", + Status: "True", + LastTransitionTime: timestamppb.New(lastTransitionTime), + Reason: "DriverReady", + Message: "Driver is posting ready status.", + }, + }, + }, + }, + { + Metadata: &pb.ObjectMeta{ + Name: "gpu-2222", + ResourceVersion: "2", + }, + Spec: &pb.GpuSpec{ + Uuid: "GPU-2222", + }, + Status: &pb.GpuStatus{ + Conditions: []*pb.Condition{ + { + Type: "HardwareFailure", + Status: "True", + LastTransitionTime: timestamppb.New(lastTransitionTime.Add(1 * time.Minute)), + Reason: "DoubleBitECCError", + Message: "Double Bit ECC error detected.", + }, + }, + RecommendedAction: "RebootNode", + }, + }, + }, + } + + goList := FromProtoList(protoIn) + + expectedResourceVersion := "2" + if goList.ListMeta.ResourceVersion != expectedResourceVersion { + t.Errorf("ListMeta.ResourceVersion conversion failed: got %q, want %q", + goList.ListMeta.ResourceVersion, expectedResourceVersion) + } + + protoOut := ToProtoList(goList) + + if diff := cmp.Diff(protoIn, protoOut, protocmp.Transform()); diff != "" { + t.Errorf("Conversion failed (-want +got):\n%s", diff) + } +} diff --git a/api/device/v1alpha1/gpu_types.go b/api/device/v1alpha1/gpu_types.go new file mode 100644 index 000000000..ad17601e0 --- /dev/null +++ b/api/device/v1alpha1/gpu_types.go @@ -0,0 +1,71 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// GPUSpec defines the desired state of the GPU. +// +// +k8s:deepcopy-gen=true +type GPUSpec struct { + // UUID is the physical hardware UUID of the GPU. + // + // Format: 'GPU-' + // (e.g., 'GPU-a1b2c3d4-e5f6-a7b8-c9d0-e1f2a3b4c5d6'). + UUID string `json:"uuid"` +} + +// GPUStatus defines the observed state of the GPU. +// +// +k8s:deepcopy-gen=true +type GPUStatus struct { + // Conditions is an array of current gpu conditions. + // + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // RecommendedAction is a suggestion given the current state. + // + // +optional + RecommendedAction string `json:"recommendedAction,omitempty"` +} + +// GPU represents a single GPU resource. +// +// +genclient +// +genclient:nonNamespaced +// +genclient:onlyVerbs=get,list,watch +// +genclient:noStatus +// +k8s:deepcopy-gen=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type GPU struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GPUSpec `json:"spec,omitempty"` + Status GPUStatus `json:"status,omitempty"` +} + +// GPUList contains a list of GPU resources. +// +// +k8s:deepcopy-gen=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type GPUList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []GPU `json:"items"` +} diff --git a/api/device/v1alpha1/register.go b/api/device/v1alpha1/register.go new file mode 100644 index 000000000..1c4974a97 --- /dev/null +++ b/api/device/v1alpha1/register.go @@ -0,0 +1,44 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var SchemeGroupVersion = schema.GroupVersion{Group: "device.nvidia.com", Version: "v1alpha1"} + +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +var ( + // SchemeBuilder initializes a scheme builder. + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + + // AddToScheme adds the type in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) + +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &GPU{}, + &GPUList{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/api/device/v1alpha1/zz_generated.deepcopy.go b/api/device/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 000000000..0c399eb3e --- /dev/null +++ b/api/device/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,125 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GPU) DeepCopyInto(out *GPU) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GPU. +func (in *GPU) DeepCopy() *GPU { + if in == nil { + return nil + } + out := new(GPU) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GPU) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GPUList) DeepCopyInto(out *GPUList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GPU, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GPUList. +func (in *GPUList) DeepCopy() *GPUList { + if in == nil { + return nil + } + out := new(GPUList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GPUList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GPUSpec) DeepCopyInto(out *GPUSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GPUSpec. +func (in *GPUSpec) DeepCopy() *GPUSpec { + if in == nil { + return nil + } + out := new(GPUSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GPUStatus) DeepCopyInto(out *GPUStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GPUStatus. +func (in *GPUStatus) DeepCopy() *GPUStatus { + if in == nil { + return nil + } + out := new(GPUStatus) + in.DeepCopyInto(out) + return out +} diff --git a/api/device/v1alpha1/zz_generated.goverter.go b/api/device/v1alpha1/zz_generated.goverter.go new file mode 100644 index 000000000..fbfb6d12f --- /dev/null +++ b/api/device/v1alpha1/zz_generated.goverter.go @@ -0,0 +1,142 @@ +// Code generated by github.com/jmattheis/goverter, DO NOT EDIT. +//go:build !goverter + +package v1alpha1 + +import ( + v1alpha1 "github.com/nvidia/nvsentinel/api/gen/go/device/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ConverterImpl struct{} + +func (c *ConverterImpl) FromProtobuf(source *v1alpha1.Gpu) GPU { + var v1alpha1GPU GPU + if source != nil { + v1alpha1GPU.TypeMeta = FromProtobufTypeMeta(source) + v1alpha1GPU.ObjectMeta = c.FromProtobufObjectMeta((*source).Metadata) + v1alpha1GPU.Spec = c.FromProtobufSpec((*source).Spec) + v1alpha1GPU.Status = c.FromProtobufStatus((*source).Status) + } + return v1alpha1GPU +} +func (c *ConverterImpl) FromProtobufCondition(source *v1alpha1.Condition) v1.Condition { + var v1Condition v1.Condition + if source != nil { + v1Condition.Type = (*source).Type + v1Condition.Status = v1.ConditionStatus((*source).Status) + v1Condition.LastTransitionTime = FromProtobufTimestamp((*source).LastTransitionTime) + v1Condition.Reason = (*source).Reason + v1Condition.Message = (*source).Message + } + return v1Condition +} +func (c *ConverterImpl) FromProtobufList(source *v1alpha1.GpuList) *GPUList { + var pV1alpha1GPUList *GPUList + if source != nil { + var v1alpha1GPUList GPUList + v1alpha1GPUList.TypeMeta = FromProtobufListTypeMeta(source) + v1alpha1GPUList.ListMeta = c.FromProtobufListMeta((*source).Metadata) + if (*source).Items != nil { + v1alpha1GPUList.Items = make([]GPU, len((*source).Items)) + for i := 0; i < len((*source).Items); i++ { + v1alpha1GPUList.Items[i] = c.FromProtobuf((*source).Items[i]) + } + } + pV1alpha1GPUList = &v1alpha1GPUList + } + return pV1alpha1GPUList +} +func (c *ConverterImpl) FromProtobufListMeta(source *v1alpha1.ListMeta) v1.ListMeta { + var v1ListMeta v1.ListMeta + if source != nil { + v1ListMeta.ResourceVersion = (*source).ResourceVersion + } + return v1ListMeta +} +func (c *ConverterImpl) FromProtobufObjectMeta(source *v1alpha1.ObjectMeta) v1.ObjectMeta { + var v1ObjectMeta v1.ObjectMeta + if source != nil { + v1ObjectMeta.Name = (*source).Name + v1ObjectMeta.ResourceVersion = (*source).ResourceVersion + } + return v1ObjectMeta +} +func (c *ConverterImpl) FromProtobufSpec(source *v1alpha1.GpuSpec) GPUSpec { + var v1alpha1GPUSpec GPUSpec + if source != nil { + v1alpha1GPUSpec.UUID = (*source).Uuid + } + return v1alpha1GPUSpec +} +func (c *ConverterImpl) FromProtobufStatus(source *v1alpha1.GpuStatus) GPUStatus { + var v1alpha1GPUStatus GPUStatus + if source != nil { + if (*source).Conditions != nil { + v1alpha1GPUStatus.Conditions = make([]v1.Condition, len((*source).Conditions)) + for i := 0; i < len((*source).Conditions); i++ { + v1alpha1GPUStatus.Conditions[i] = c.FromProtobufCondition((*source).Conditions[i]) + } + } + v1alpha1GPUStatus.RecommendedAction = (*source).RecommendedAction + } + return v1alpha1GPUStatus +} +func (c *ConverterImpl) ToProtobuf(source GPU) *v1alpha1.Gpu { + var v1alpha1Gpu v1alpha1.Gpu + v1alpha1Gpu.Metadata = c.ToProtobufObjectMeta(source.ObjectMeta) + v1alpha1Gpu.Spec = c.ToProtobufSpec(source.Spec) + v1alpha1Gpu.Status = c.ToProtobufStatus(source.Status) + return &v1alpha1Gpu +} +func (c *ConverterImpl) ToProtobufCondition(source v1.Condition) *v1alpha1.Condition { + var v1alpha1Condition v1alpha1.Condition + v1alpha1Condition.Type = source.Type + v1alpha1Condition.Status = string(source.Status) + v1alpha1Condition.LastTransitionTime = ToProtobufTimestamp(source.LastTransitionTime) + v1alpha1Condition.Reason = source.Reason + v1alpha1Condition.Message = source.Message + return &v1alpha1Condition +} +func (c *ConverterImpl) ToProtobufList(source *GPUList) *v1alpha1.GpuList { + var pV1alpha1GpuList *v1alpha1.GpuList + if source != nil { + var v1alpha1GpuList v1alpha1.GpuList + v1alpha1GpuList.Metadata = c.ToProtobufListMeta((*source).ListMeta) + if (*source).Items != nil { + v1alpha1GpuList.Items = make([]*v1alpha1.Gpu, len((*source).Items)) + for i := 0; i < len((*source).Items); i++ { + v1alpha1GpuList.Items[i] = c.ToProtobuf((*source).Items[i]) + } + } + pV1alpha1GpuList = &v1alpha1GpuList + } + return pV1alpha1GpuList +} +func (c *ConverterImpl) ToProtobufListMeta(source v1.ListMeta) *v1alpha1.ListMeta { + var v1alpha1ListMeta v1alpha1.ListMeta + v1alpha1ListMeta.ResourceVersion = source.ResourceVersion + return &v1alpha1ListMeta +} +func (c *ConverterImpl) ToProtobufObjectMeta(source v1.ObjectMeta) *v1alpha1.ObjectMeta { + var v1alpha1ObjectMeta v1alpha1.ObjectMeta + v1alpha1ObjectMeta.Name = source.Name + v1alpha1ObjectMeta.ResourceVersion = source.ResourceVersion + return &v1alpha1ObjectMeta +} +func (c *ConverterImpl) ToProtobufSpec(source GPUSpec) *v1alpha1.GpuSpec { + var v1alpha1GpuSpec v1alpha1.GpuSpec + v1alpha1GpuSpec.Uuid = source.UUID + return &v1alpha1GpuSpec +} +func (c *ConverterImpl) ToProtobufStatus(source GPUStatus) *v1alpha1.GpuStatus { + var v1alpha1GpuStatus v1alpha1.GpuStatus + if source.Conditions != nil { + v1alpha1GpuStatus.Conditions = make([]*v1alpha1.Condition, len(source.Conditions)) + for i := 0; i < len(source.Conditions); i++ { + v1alpha1GpuStatus.Conditions[i] = c.ToProtobufCondition(source.Conditions[i]) + } + } + v1alpha1GpuStatus.RecommendedAction = source.RecommendedAction + return &v1alpha1GpuStatus +} diff --git a/api/gen/go/device/v1alpha1/gpu.pb.go b/api/gen/go/device/v1alpha1/gpu.pb.go index cfb9d138f..3e41dee3c 100644 --- a/api/gen/go/device/v1alpha1/gpu.pb.go +++ b/api/gen/go/device/v1alpha1/gpu.pb.go @@ -36,17 +36,129 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// ObjectMeta is a subset of k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta. +type ObjectMeta struct { + state protoimpl.MessageState `protogen:"open.v1"` + // name is the unique logical identifier of the resource. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // resource_version represents the internal version of this object. + // + // Value must be treated as opaque by clients and passed unmodified back to the server. + // Populated by the system. + // Read-only. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + ResourceVersion string `protobuf:"bytes,2,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ObjectMeta) Reset() { + *x = ObjectMeta{} + mi := &file_device_v1alpha1_gpu_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ObjectMeta) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ObjectMeta) ProtoMessage() {} + +func (x *ObjectMeta) ProtoReflect() protoreflect.Message { + mi := &file_device_v1alpha1_gpu_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ObjectMeta.ProtoReflect.Descriptor instead. +func (*ObjectMeta) Descriptor() ([]byte, []int) { + return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{0} +} + +func (x *ObjectMeta) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ObjectMeta) GetResourceVersion() string { + if x != nil { + return x.ResourceVersion + } + return "" +} + +// ListMeta is a subset of k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta. +type ListMeta struct { + state protoimpl.MessageState `protogen:"open.v1"` + // resource_version identifies the version of the list snapshot. + // Clients can use this version to establish a watch from a consistent point in time. + // + // Value must be treated as opaque by clients and passed unmodified back to the server. + // Populated by the system. + // Read-only. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + ResourceVersion string `protobuf:"bytes,1,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListMeta) Reset() { + *x = ListMeta{} + mi := &file_device_v1alpha1_gpu_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListMeta) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListMeta) ProtoMessage() {} + +func (x *ListMeta) ProtoReflect() protoreflect.Message { + mi := &file_device_v1alpha1_gpu_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListMeta.ProtoReflect.Descriptor instead. +func (*ListMeta) Descriptor() ([]byte, []int) { + return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{1} +} + +func (x *ListMeta) GetResourceVersion() string { + if x != nil { + return x.ResourceVersion + } + return "" +} + // Gpu represents a single GPU resource. // // Its structure follows the Kubernetes Resource Model pattern (Spec/Status). +// +// The resource name (metadata.name) is typically the lowercased GPU UUID, +// but may take other forms. +// +// Example: "gpu-a1b2c3d4-e5f6-a7b8-c9d0-e1f2a3b4c5d6" type Gpu struct { - state protoimpl.MessageState `protogen:"open.v1"` - // name is the unique logical identifier of the GPU resource. - // - // This is typically the lowercased GPU UUID, but may take other forms. - // - // Example: "gpu-a1b2c3d4-e5f6-a7b8-c9d0-e1f2a3b4c5d6" - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Metadata *ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` // spec defines the identity and desired attributes of the GPU resource. Spec *GpuSpec `protobuf:"bytes,2,opt,name=spec,proto3" json:"spec,omitempty"` // status contains the most recently observed state of the GPU resource. @@ -58,7 +170,7 @@ type Gpu struct { func (x *Gpu) Reset() { *x = Gpu{} - mi := &file_device_v1alpha1_gpu_proto_msgTypes[0] + mi := &file_device_v1alpha1_gpu_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -70,7 +182,7 @@ func (x *Gpu) String() string { func (*Gpu) ProtoMessage() {} func (x *Gpu) ProtoReflect() protoreflect.Message { - mi := &file_device_v1alpha1_gpu_proto_msgTypes[0] + mi := &file_device_v1alpha1_gpu_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -83,14 +195,14 @@ func (x *Gpu) ProtoReflect() protoreflect.Message { // Deprecated: Use Gpu.ProtoReflect.Descriptor instead. func (*Gpu) Descriptor() ([]byte, []int) { - return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{0} + return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{2} } -func (x *Gpu) GetName() string { +func (x *Gpu) GetMetadata() *ObjectMeta { if x != nil { - return x.Name + return x.Metadata } - return "" + return nil } func (x *Gpu) GetSpec() *GpuSpec { @@ -109,16 +221,17 @@ func (x *Gpu) GetStatus() *GpuStatus { // GpuList is a collection of GPU resources. type GpuList struct { - state protoimpl.MessageState `protogen:"open.v1"` + state protoimpl.MessageState `protogen:"open.v1"` + Metadata *ListMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` // items is the list of GPU resources. - Items []*Gpu `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + Items []*Gpu `protobuf:"bytes,2,rep,name=items,proto3" json:"items,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GpuList) Reset() { *x = GpuList{} - mi := &file_device_v1alpha1_gpu_proto_msgTypes[1] + mi := &file_device_v1alpha1_gpu_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -130,7 +243,7 @@ func (x *GpuList) String() string { func (*GpuList) ProtoMessage() {} func (x *GpuList) ProtoReflect() protoreflect.Message { - mi := &file_device_v1alpha1_gpu_proto_msgTypes[1] + mi := &file_device_v1alpha1_gpu_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -143,7 +256,14 @@ func (x *GpuList) ProtoReflect() protoreflect.Message { // Deprecated: Use GpuList.ProtoReflect.Descriptor instead. func (*GpuList) Descriptor() ([]byte, []int) { - return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{1} + return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{3} +} + +func (x *GpuList) GetMetadata() *ListMeta { + if x != nil { + return x.Metadata + } + return nil } func (x *GpuList) GetItems() []*Gpu { @@ -166,7 +286,7 @@ type GpuSpec struct { func (x *GpuSpec) Reset() { *x = GpuSpec{} - mi := &file_device_v1alpha1_gpu_proto_msgTypes[2] + mi := &file_device_v1alpha1_gpu_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -178,7 +298,7 @@ func (x *GpuSpec) String() string { func (*GpuSpec) ProtoMessage() {} func (x *GpuSpec) ProtoReflect() protoreflect.Message { - mi := &file_device_v1alpha1_gpu_proto_msgTypes[2] + mi := &file_device_v1alpha1_gpu_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -191,7 +311,7 @@ func (x *GpuSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use GpuSpec.ProtoReflect.Descriptor instead. func (*GpuSpec) Descriptor() ([]byte, []int) { - return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{2} + return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{4} } func (x *GpuSpec) GetUuid() string { @@ -214,7 +334,7 @@ type GpuStatus struct { func (x *GpuStatus) Reset() { *x = GpuStatus{} - mi := &file_device_v1alpha1_gpu_proto_msgTypes[3] + mi := &file_device_v1alpha1_gpu_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -226,7 +346,7 @@ func (x *GpuStatus) String() string { func (*GpuStatus) ProtoMessage() {} func (x *GpuStatus) ProtoReflect() protoreflect.Message { - mi := &file_device_v1alpha1_gpu_proto_msgTypes[3] + mi := &file_device_v1alpha1_gpu_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -239,7 +359,7 @@ func (x *GpuStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use GpuStatus.ProtoReflect.Descriptor instead. func (*GpuStatus) Descriptor() ([]byte, []int) { - return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{3} + return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{5} } func (x *GpuStatus) GetConditions() []*Condition { @@ -279,7 +399,7 @@ type Condition struct { func (x *Condition) Reset() { *x = Condition{} - mi := &file_device_v1alpha1_gpu_proto_msgTypes[4] + mi := &file_device_v1alpha1_gpu_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -291,7 +411,7 @@ func (x *Condition) String() string { func (*Condition) ProtoMessage() {} func (x *Condition) ProtoReflect() protoreflect.Message { - mi := &file_device_v1alpha1_gpu_proto_msgTypes[4] + mi := &file_device_v1alpha1_gpu_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -304,7 +424,7 @@ func (x *Condition) ProtoReflect() protoreflect.Message { // Deprecated: Use Condition.ProtoReflect.Descriptor instead. func (*Condition) Descriptor() ([]byte, []int) { - return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{4} + return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{6} } func (x *Condition) GetType() string { @@ -353,7 +473,7 @@ type GetGpuRequest struct { func (x *GetGpuRequest) Reset() { *x = GetGpuRequest{} - mi := &file_device_v1alpha1_gpu_proto_msgTypes[5] + mi := &file_device_v1alpha1_gpu_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -365,7 +485,7 @@ func (x *GetGpuRequest) String() string { func (*GetGpuRequest) ProtoMessage() {} func (x *GetGpuRequest) ProtoReflect() protoreflect.Message { - mi := &file_device_v1alpha1_gpu_proto_msgTypes[5] + mi := &file_device_v1alpha1_gpu_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -378,7 +498,7 @@ func (x *GetGpuRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetGpuRequest.ProtoReflect.Descriptor instead. func (*GetGpuRequest) Descriptor() ([]byte, []int) { - return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{5} + return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{7} } func (x *GetGpuRequest) GetName() string { @@ -399,7 +519,7 @@ type GetGpuResponse struct { func (x *GetGpuResponse) Reset() { *x = GetGpuResponse{} - mi := &file_device_v1alpha1_gpu_proto_msgTypes[6] + mi := &file_device_v1alpha1_gpu_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -411,7 +531,7 @@ func (x *GetGpuResponse) String() string { func (*GetGpuResponse) ProtoMessage() {} func (x *GetGpuResponse) ProtoReflect() protoreflect.Message { - mi := &file_device_v1alpha1_gpu_proto_msgTypes[6] + mi := &file_device_v1alpha1_gpu_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -424,7 +544,7 @@ func (x *GetGpuResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetGpuResponse.ProtoReflect.Descriptor instead. func (*GetGpuResponse) Descriptor() ([]byte, []int) { - return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{6} + return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{8} } func (x *GetGpuResponse) GetGpu() *Gpu { @@ -435,18 +555,17 @@ func (x *GetGpuResponse) GetGpu() *Gpu { } // ListGpusRequest specifies the criteria for listing GPU resources. -// -// NOTE: The request is currently empty, but reserved for future support -// of filtering and pagination. type ListGpusRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + // resource_version allows the client to list resources at a specific version. + ResourceVersion string `protobuf:"bytes,1,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ListGpusRequest) Reset() { *x = ListGpusRequest{} - mi := &file_device_v1alpha1_gpu_proto_msgTypes[7] + mi := &file_device_v1alpha1_gpu_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -458,7 +577,7 @@ func (x *ListGpusRequest) String() string { func (*ListGpusRequest) ProtoMessage() {} func (x *ListGpusRequest) ProtoReflect() protoreflect.Message { - mi := &file_device_v1alpha1_gpu_proto_msgTypes[7] + mi := &file_device_v1alpha1_gpu_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -471,7 +590,14 @@ func (x *ListGpusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListGpusRequest.ProtoReflect.Descriptor instead. func (*ListGpusRequest) Descriptor() ([]byte, []int) { - return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{7} + return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{9} +} + +func (x *ListGpusRequest) GetResourceVersion() string { + if x != nil { + return x.ResourceVersion + } + return "" } // ListGpusResponse contains the list of GPU resources. @@ -485,7 +611,7 @@ type ListGpusResponse struct { func (x *ListGpusResponse) Reset() { *x = ListGpusResponse{} - mi := &file_device_v1alpha1_gpu_proto_msgTypes[8] + mi := &file_device_v1alpha1_gpu_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -497,7 +623,7 @@ func (x *ListGpusResponse) String() string { func (*ListGpusResponse) ProtoMessage() {} func (x *ListGpusResponse) ProtoReflect() protoreflect.Message { - mi := &file_device_v1alpha1_gpu_proto_msgTypes[8] + mi := &file_device_v1alpha1_gpu_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -510,7 +636,7 @@ func (x *ListGpusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListGpusResponse.ProtoReflect.Descriptor instead. func (*ListGpusResponse) Descriptor() ([]byte, []int) { - return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{8} + return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{10} } func (x *ListGpusResponse) GetGpuList() *GpuList { @@ -521,18 +647,17 @@ func (x *ListGpusResponse) GetGpuList() *GpuList { } // WatchGpusRequest specifies the parameters for the watch stream. -// -// NOTE: The request is currently empty, but reserved for future support -// of filtering and resource versioning (resumption). type WatchGpusRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + // resource_version allows the client to start watching at a specific version. + ResourceVersion string `protobuf:"bytes,1,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *WatchGpusRequest) Reset() { *x = WatchGpusRequest{} - mi := &file_device_v1alpha1_gpu_proto_msgTypes[9] + mi := &file_device_v1alpha1_gpu_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -544,7 +669,7 @@ func (x *WatchGpusRequest) String() string { func (*WatchGpusRequest) ProtoMessage() {} func (x *WatchGpusRequest) ProtoReflect() protoreflect.Message { - mi := &file_device_v1alpha1_gpu_proto_msgTypes[9] + mi := &file_device_v1alpha1_gpu_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -557,7 +682,14 @@ func (x *WatchGpusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchGpusRequest.ProtoReflect.Descriptor instead. func (*WatchGpusRequest) Descriptor() ([]byte, []int) { - return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{9} + return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{11} +} + +func (x *WatchGpusRequest) GetResourceVersion() string { + if x != nil { + return x.ResourceVersion + } + return "" } // WatchGpusResponse describes a change event for a GPU resource. @@ -582,7 +714,7 @@ type WatchGpusResponse struct { func (x *WatchGpusResponse) Reset() { *x = WatchGpusResponse{} - mi := &file_device_v1alpha1_gpu_proto_msgTypes[10] + mi := &file_device_v1alpha1_gpu_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -594,7 +726,7 @@ func (x *WatchGpusResponse) String() string { func (*WatchGpusResponse) ProtoMessage() {} func (x *WatchGpusResponse) ProtoReflect() protoreflect.Message { - mi := &file_device_v1alpha1_gpu_proto_msgTypes[10] + mi := &file_device_v1alpha1_gpu_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -607,7 +739,7 @@ func (x *WatchGpusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchGpusResponse.ProtoReflect.Descriptor instead. func (*WatchGpusResponse) Descriptor() ([]byte, []int) { - return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{10} + return file_device_v1alpha1_gpu_proto_rawDescGZIP(), []int{12} } func (x *WatchGpusResponse) GetType() string { @@ -628,13 +760,20 @@ var File_device_v1alpha1_gpu_proto protoreflect.FileDescriptor const file_device_v1alpha1_gpu_proto_rawDesc = "" + "\n" + - "\x19device/v1alpha1/gpu.proto\x12\x1anvidia.nvsentinel.v1alpha1\x1a\x1fgoogle/protobuf/timestamp.proto\"\x91\x01\n" + - "\x03Gpu\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x127\n" + + "\x19device/v1alpha1/gpu.proto\x12\x1anvidia.nvsentinel.v1alpha1\x1a\x1fgoogle/protobuf/timestamp.proto\"K\n" + + "\n" + + "ObjectMeta\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12)\n" + + "\x10resource_version\x18\x02 \x01(\tR\x0fresourceVersion\"5\n" + + "\bListMeta\x12)\n" + + "\x10resource_version\x18\x01 \x01(\tR\x0fresourceVersion\"\xc1\x01\n" + + "\x03Gpu\x12B\n" + + "\bmetadata\x18\x01 \x01(\v2&.nvidia.nvsentinel.v1alpha1.ObjectMetaR\bmetadata\x127\n" + "\x04spec\x18\x02 \x01(\v2#.nvidia.nvsentinel.v1alpha1.GpuSpecR\x04spec\x12=\n" + - "\x06status\x18\x03 \x01(\v2%.nvidia.nvsentinel.v1alpha1.GpuStatusR\x06status\"@\n" + - "\aGpuList\x125\n" + - "\x05items\x18\x01 \x03(\v2\x1f.nvidia.nvsentinel.v1alpha1.GpuR\x05items\"\x1d\n" + + "\x06status\x18\x03 \x01(\v2%.nvidia.nvsentinel.v1alpha1.GpuStatusR\x06status\"\x82\x01\n" + + "\aGpuList\x12@\n" + + "\bmetadata\x18\x01 \x01(\v2$.nvidia.nvsentinel.v1alpha1.ListMetaR\bmetadata\x125\n" + + "\x05items\x18\x02 \x03(\v2\x1f.nvidia.nvsentinel.v1alpha1.GpuR\x05items\"\x1d\n" + "\aGpuSpec\x12\x12\n" + "\x04uuid\x18\x01 \x01(\tR\x04uuid\"\x81\x01\n" + "\tGpuStatus\x12E\n" + @@ -651,11 +790,13 @@ const file_device_v1alpha1_gpu_proto_rawDesc = "" + "\rGetGpuRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\"C\n" + "\x0eGetGpuResponse\x121\n" + - "\x03gpu\x18\x01 \x01(\v2\x1f.nvidia.nvsentinel.v1alpha1.GpuR\x03gpu\"\x11\n" + - "\x0fListGpusRequest\"R\n" + + "\x03gpu\x18\x01 \x01(\v2\x1f.nvidia.nvsentinel.v1alpha1.GpuR\x03gpu\"<\n" + + "\x0fListGpusRequest\x12)\n" + + "\x10resource_version\x18\x01 \x01(\tR\x0fresourceVersion\"R\n" + "\x10ListGpusResponse\x12>\n" + - "\bgpu_list\x18\x01 \x01(\v2#.nvidia.nvsentinel.v1alpha1.GpuListR\agpuList\"\x12\n" + - "\x10WatchGpusRequest\"`\n" + + "\bgpu_list\x18\x01 \x01(\v2#.nvidia.nvsentinel.v1alpha1.GpuListR\agpuList\"=\n" + + "\x10WatchGpusRequest\x12)\n" + + "\x10resource_version\x18\x01 \x01(\tR\x0fresourceVersion\"`\n" + "\x11WatchGpusResponse\x12\x12\n" + "\x04type\x18\x01 \x01(\tR\x04type\x127\n" + "\x06object\x18\x02 \x01(\v2\x1f.nvidia.nvsentinel.v1alpha1.GpuR\x06object2\xc0\x02\n" + @@ -677,41 +818,45 @@ func file_device_v1alpha1_gpu_proto_rawDescGZIP() []byte { return file_device_v1alpha1_gpu_proto_rawDescData } -var file_device_v1alpha1_gpu_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_device_v1alpha1_gpu_proto_msgTypes = make([]protoimpl.MessageInfo, 13) var file_device_v1alpha1_gpu_proto_goTypes = []any{ - (*Gpu)(nil), // 0: nvidia.nvsentinel.v1alpha1.Gpu - (*GpuList)(nil), // 1: nvidia.nvsentinel.v1alpha1.GpuList - (*GpuSpec)(nil), // 2: nvidia.nvsentinel.v1alpha1.GpuSpec - (*GpuStatus)(nil), // 3: nvidia.nvsentinel.v1alpha1.GpuStatus - (*Condition)(nil), // 4: nvidia.nvsentinel.v1alpha1.Condition - (*GetGpuRequest)(nil), // 5: nvidia.nvsentinel.v1alpha1.GetGpuRequest - (*GetGpuResponse)(nil), // 6: nvidia.nvsentinel.v1alpha1.GetGpuResponse - (*ListGpusRequest)(nil), // 7: nvidia.nvsentinel.v1alpha1.ListGpusRequest - (*ListGpusResponse)(nil), // 8: nvidia.nvsentinel.v1alpha1.ListGpusResponse - (*WatchGpusRequest)(nil), // 9: nvidia.nvsentinel.v1alpha1.WatchGpusRequest - (*WatchGpusResponse)(nil), // 10: nvidia.nvsentinel.v1alpha1.WatchGpusResponse - (*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp + (*ObjectMeta)(nil), // 0: nvidia.nvsentinel.v1alpha1.ObjectMeta + (*ListMeta)(nil), // 1: nvidia.nvsentinel.v1alpha1.ListMeta + (*Gpu)(nil), // 2: nvidia.nvsentinel.v1alpha1.Gpu + (*GpuList)(nil), // 3: nvidia.nvsentinel.v1alpha1.GpuList + (*GpuSpec)(nil), // 4: nvidia.nvsentinel.v1alpha1.GpuSpec + (*GpuStatus)(nil), // 5: nvidia.nvsentinel.v1alpha1.GpuStatus + (*Condition)(nil), // 6: nvidia.nvsentinel.v1alpha1.Condition + (*GetGpuRequest)(nil), // 7: nvidia.nvsentinel.v1alpha1.GetGpuRequest + (*GetGpuResponse)(nil), // 8: nvidia.nvsentinel.v1alpha1.GetGpuResponse + (*ListGpusRequest)(nil), // 9: nvidia.nvsentinel.v1alpha1.ListGpusRequest + (*ListGpusResponse)(nil), // 10: nvidia.nvsentinel.v1alpha1.ListGpusResponse + (*WatchGpusRequest)(nil), // 11: nvidia.nvsentinel.v1alpha1.WatchGpusRequest + (*WatchGpusResponse)(nil), // 12: nvidia.nvsentinel.v1alpha1.WatchGpusResponse + (*timestamppb.Timestamp)(nil), // 13: google.protobuf.Timestamp } var file_device_v1alpha1_gpu_proto_depIdxs = []int32{ - 2, // 0: nvidia.nvsentinel.v1alpha1.Gpu.spec:type_name -> nvidia.nvsentinel.v1alpha1.GpuSpec - 3, // 1: nvidia.nvsentinel.v1alpha1.Gpu.status:type_name -> nvidia.nvsentinel.v1alpha1.GpuStatus - 0, // 2: nvidia.nvsentinel.v1alpha1.GpuList.items:type_name -> nvidia.nvsentinel.v1alpha1.Gpu - 4, // 3: nvidia.nvsentinel.v1alpha1.GpuStatus.conditions:type_name -> nvidia.nvsentinel.v1alpha1.Condition - 11, // 4: nvidia.nvsentinel.v1alpha1.Condition.last_transition_time:type_name -> google.protobuf.Timestamp - 0, // 5: nvidia.nvsentinel.v1alpha1.GetGpuResponse.gpu:type_name -> nvidia.nvsentinel.v1alpha1.Gpu - 1, // 6: nvidia.nvsentinel.v1alpha1.ListGpusResponse.gpu_list:type_name -> nvidia.nvsentinel.v1alpha1.GpuList - 0, // 7: nvidia.nvsentinel.v1alpha1.WatchGpusResponse.object:type_name -> nvidia.nvsentinel.v1alpha1.Gpu - 5, // 8: nvidia.nvsentinel.v1alpha1.GpuService.GetGpu:input_type -> nvidia.nvsentinel.v1alpha1.GetGpuRequest - 7, // 9: nvidia.nvsentinel.v1alpha1.GpuService.ListGpus:input_type -> nvidia.nvsentinel.v1alpha1.ListGpusRequest - 9, // 10: nvidia.nvsentinel.v1alpha1.GpuService.WatchGpus:input_type -> nvidia.nvsentinel.v1alpha1.WatchGpusRequest - 6, // 11: nvidia.nvsentinel.v1alpha1.GpuService.GetGpu:output_type -> nvidia.nvsentinel.v1alpha1.GetGpuResponse - 8, // 12: nvidia.nvsentinel.v1alpha1.GpuService.ListGpus:output_type -> nvidia.nvsentinel.v1alpha1.ListGpusResponse - 10, // 13: nvidia.nvsentinel.v1alpha1.GpuService.WatchGpus:output_type -> nvidia.nvsentinel.v1alpha1.WatchGpusResponse - 11, // [11:14] is the sub-list for method output_type - 8, // [8:11] is the sub-list for method input_type - 8, // [8:8] is the sub-list for extension type_name - 8, // [8:8] is the sub-list for extension extendee - 0, // [0:8] is the sub-list for field type_name + 0, // 0: nvidia.nvsentinel.v1alpha1.Gpu.metadata:type_name -> nvidia.nvsentinel.v1alpha1.ObjectMeta + 4, // 1: nvidia.nvsentinel.v1alpha1.Gpu.spec:type_name -> nvidia.nvsentinel.v1alpha1.GpuSpec + 5, // 2: nvidia.nvsentinel.v1alpha1.Gpu.status:type_name -> nvidia.nvsentinel.v1alpha1.GpuStatus + 1, // 3: nvidia.nvsentinel.v1alpha1.GpuList.metadata:type_name -> nvidia.nvsentinel.v1alpha1.ListMeta + 2, // 4: nvidia.nvsentinel.v1alpha1.GpuList.items:type_name -> nvidia.nvsentinel.v1alpha1.Gpu + 6, // 5: nvidia.nvsentinel.v1alpha1.GpuStatus.conditions:type_name -> nvidia.nvsentinel.v1alpha1.Condition + 13, // 6: nvidia.nvsentinel.v1alpha1.Condition.last_transition_time:type_name -> google.protobuf.Timestamp + 2, // 7: nvidia.nvsentinel.v1alpha1.GetGpuResponse.gpu:type_name -> nvidia.nvsentinel.v1alpha1.Gpu + 3, // 8: nvidia.nvsentinel.v1alpha1.ListGpusResponse.gpu_list:type_name -> nvidia.nvsentinel.v1alpha1.GpuList + 2, // 9: nvidia.nvsentinel.v1alpha1.WatchGpusResponse.object:type_name -> nvidia.nvsentinel.v1alpha1.Gpu + 7, // 10: nvidia.nvsentinel.v1alpha1.GpuService.GetGpu:input_type -> nvidia.nvsentinel.v1alpha1.GetGpuRequest + 9, // 11: nvidia.nvsentinel.v1alpha1.GpuService.ListGpus:input_type -> nvidia.nvsentinel.v1alpha1.ListGpusRequest + 11, // 12: nvidia.nvsentinel.v1alpha1.GpuService.WatchGpus:input_type -> nvidia.nvsentinel.v1alpha1.WatchGpusRequest + 8, // 13: nvidia.nvsentinel.v1alpha1.GpuService.GetGpu:output_type -> nvidia.nvsentinel.v1alpha1.GetGpuResponse + 10, // 14: nvidia.nvsentinel.v1alpha1.GpuService.ListGpus:output_type -> nvidia.nvsentinel.v1alpha1.ListGpusResponse + 12, // 15: nvidia.nvsentinel.v1alpha1.GpuService.WatchGpus:output_type -> nvidia.nvsentinel.v1alpha1.WatchGpusResponse + 13, // [13:16] is the sub-list for method output_type + 10, // [10:13] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name } func init() { file_device_v1alpha1_gpu_proto_init() } @@ -725,7 +870,7 @@ func file_device_v1alpha1_gpu_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_device_v1alpha1_gpu_proto_rawDesc), len(file_device_v1alpha1_gpu_proto_rawDesc)), NumEnums: 0, - NumMessages: 11, + NumMessages: 13, NumExtensions: 0, NumServices: 1, }, diff --git a/api/go.mod b/api/go.mod index c99562dcb..3044319aa 100644 --- a/api/go.mod +++ b/api/go.mod @@ -1,9 +1,10 @@ module github.com/nvidia/nvsentinel/api -go 1.25 +go 1.25.5 require ( - google.golang.org/grpc v1.78.0 + github.com/google/go-cmp v0.7.0 + google.golang.org/grpc v1.77.0 google.golang.org/protobuf v1.36.11 ) @@ -12,4 +13,22 @@ require ( golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.31.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect + k8s.io/apimachinery v0.34.1 +) + +require ( + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect ) diff --git a/api/go.sum b/api/go.sum index f3fc747f6..9227b66ee 100644 --- a/api/go.sum +++ b/api/go.sum @@ -1,13 +1,43 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= @@ -20,17 +50,64 @@ go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6 go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/api/hack/boilerplate.go.txt b/api/hack/boilerplate.go.txt new file mode 100644 index 000000000..e1732e8d5 --- /dev/null +++ b/api/hack/boilerplate.go.txt @@ -0,0 +1,13 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. diff --git a/api/hack/update-codegen.sh b/api/hack/update-codegen.sh new file mode 100755 index 000000000..921ee1574 --- /dev/null +++ b/api/hack/update-codegen.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)" +CODEGEN_ROOT="${REPO_ROOT}/code-generator" + +export KUBE_CODEGEN_ROOT="${CODEGEN_ROOT}" + +source "${CODEGEN_ROOT}/kube_codegen.sh" + +kube::codegen::gen_proto_bindings \ + --output-dir "gen/go" \ + --proto-root "proto" \ + "${REPO_ROOT}/api" + +kube::codegen::gen_helpers \ + --boilerplate "${REPO_ROOT}/api/hack/boilerplate.go.txt" \ + "${REPO_ROOT}/api" diff --git a/api/proto/device/v1alpha1/gpu.proto b/api/proto/device/v1alpha1/gpu.proto index 2a4cf7f86..5ccc3a54f 100644 --- a/api/proto/device/v1alpha1/gpu.proto +++ b/api/proto/device/v1alpha1/gpu.proto @@ -24,16 +24,42 @@ import "google/protobuf/timestamp.proto"; // Resource Definitions // ========================================== +// ObjectMeta is a subset of k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta. +message ObjectMeta { + // name is the unique logical identifier of the resource. + string name = 1; + + // resource_version represents the internal version of this object. + // + // Value must be treated as opaque by clients and passed unmodified back to the server. + // Populated by the system. + // Read-only. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + string resource_version = 2; +} + +// ListMeta is a subset of k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta. +message ListMeta { + // resource_version identifies the version of the list snapshot. + // Clients can use this version to establish a watch from a consistent point in time. + // + // Value must be treated as opaque by clients and passed unmodified back to the server. + // Populated by the system. + // Read-only. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + string resource_version = 1; +} + // Gpu represents a single GPU resource. // // Its structure follows the Kubernetes Resource Model pattern (Spec/Status). +// +// The resource name (metadata.name) is typically the lowercased GPU UUID, +// but may take other forms. +// +// Example: "gpu-a1b2c3d4-e5f6-a7b8-c9d0-e1f2a3b4c5d6" message Gpu { - // name is the unique logical identifier of the GPU resource. - // - // This is typically the lowercased GPU UUID, but may take other forms. - // - // Example: "gpu-a1b2c3d4-e5f6-a7b8-c9d0-e1f2a3b4c5d6" - string name = 1; + ObjectMeta metadata = 1; // spec defines the identity and desired attributes of the GPU resource. GpuSpec spec = 2; @@ -45,8 +71,10 @@ message Gpu { // GpuList is a collection of GPU resources. message GpuList { + ListMeta metadata = 1; + // items is the list of GPU resources. - repeated Gpu items = 1; + repeated Gpu items = 2; } // GpuSpec describes the identity and desired attributes of the GPU. @@ -121,10 +149,10 @@ message GetGpuResponse { } // ListGpusRequest specifies the criteria for listing GPU resources. -// -// NOTE: The request is currently empty, but reserved for future support -// of filtering and pagination. -message ListGpusRequest {} +message ListGpusRequest { + // resource_version allows the client to list resources at a specific version. + string resource_version = 1; +} // ListGpusResponse contains the list of GPU resources. message ListGpusResponse { @@ -133,10 +161,10 @@ message ListGpusResponse { } // WatchGpusRequest specifies the parameters for the watch stream. -// -// NOTE: The request is currently empty, but reserved for future support -// of filtering and resource versioning (resumption). -message WatchGpusRequest {} +message WatchGpusRequest { + // resource_version allows the client to start watching at a specific version. + string resource_version = 1; +} // WatchGpusResponse describes a change event for a GPU resource. message WatchGpusResponse { diff --git a/client-go/DEVELOPMENT.md b/client-go/DEVELOPMENT.md new file mode 100644 index 000000000..be0ff2acc --- /dev/null +++ b/client-go/DEVELOPMENT.md @@ -0,0 +1,85 @@ +# NVIDIA Device API Go Client: Development Guide +This document outlines the workflow for developing and maintaining the `nvidia/client-go` SDK. Because this library provides Kubernetes-native interfaces, it relies heavily on **code generation**. Most of the code in this directory should not be edited manually. + +## Prerequisites +* **Go**: Must match the version specified in `go.mod`. +* **Make**: Standard build tool. + +## Structure +- `client/`: [Generated] The Clientset. **Do not edit manually.** +- `listers/`: [Generated] Type-safe listers. **Do not edit manually.** +- `informers/`: [Generated] Shared Index Informers. **Do not edit manually.** +- `nvgrpc/`: [Manual] The gRPC transport layer, interceptors, and connection management logic. +- `version/`: [Manual] Version injection functionality via `ldflags`. + +## Workflow + +### 1. Code Generation +To (re)generate the client, run: + +```bash +# Downloads codegen tools and generates clients/listers/informers +make code-gen +``` + +> [!TIP] +> **Did you modify the API?** +> +> If you have changed the types in the `../api` module (Proto or Go), you must run `make code-gen` **inside that directory first**. +> This ensures that the low-level bindings (Protobufs, DeepCopy, Conversions) are up-to-date before this client attempts to generate the high-level interfaces. + +### 2. Building & Testing +Verify that the generated code compiles and passes unit tests. + +```bash +# Compile everything (verifies type safety of generated code) +make build + +# Run unit tests (focuses on the transport layer and manual logic) +make test +``` + +### 3. Full Cycle +To run the complete pipeline (Generation → Test → Build) in one go: + +```bash +make all +``` + +## Code Generation Pipeline +This SDK is automatically generated from the Protocol Buffer definitions and Go types found in the `../api` module. We use standard Kubernetes code generators, including a customized build of `client-gen` to handle gRPC transport natively. + +```mermaid +graph TD + API["API Definitions
(nvidia/nvsentinel/api)"] -->|Input| CG(client-gen
*Custom Build*) + API -->|Input| LG(lister-gen) + + CG -->|Generates| CLIENT[client/versioned] + LG -->|Generates| LISTERS[listers/] + + CLIENT & LISTERS -->|Input| IG(informer-gen) + IG -->|Generates| INFORMERS[informers/] + + CLIENT & LISTERS & INFORMERS -->|Final Output| SDK[Ready-to-use SDK] +``` + +### Components +1. `client-gen` (**Custom**): Generates the **Clientset**. This provides access to the API Resources (e.g., `DeviceV1alpha1().GPUs()`) and maps standard Kubernetes verbs (Get, List, Watch) to the node-local gRPC transport. +2. `lister-gen`: Generates **Listers**. These provide a read-only, cached view of resources, allowing for fast lookups without making network calls. +3. `informer-gen`: Generates **Informers**. These coordinate the Client and Listers to watch for updates and sync the local cache. + +### Modifying Generated Code +> [!WARNING] +> **Do not edit generated files directly.** +> +> Files in `client/`, `listers/`, and `informers/` are overwritten every time you run `make code-gen`. +> To change the client behavior, you must modify the generator source code in `../code-generator/cmd/client-gen` or the API definitions in the `../api` module. + + +## Housekeeping +If you need to reset your environment: + +```bash +# Removes generated code (client, listers, informers) +make clean +``` diff --git a/client-go/Makefile b/client-go/Makefile new file mode 100644 index 000000000..640547430 --- /dev/null +++ b/client-go/Makefile @@ -0,0 +1,67 @@ +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ============================================================================== +# Configuration +# ============================================================================== + +# Versioning +GIT_VERSION ?= $(shell git describe --tags --always --dirty) +VERSION_PACKAGE := github.com/nvidia/nvsentinel/client-go/version +VERSION_FLAGS := -X '$(VERSION_PACKAGE).GitVersion=$(GIT_VERSION)' + +# Linker Flags +EXTRA_LDFLAGS ?= +LDFLAGS := -ldflags "$(VERSION_FLAGS) $(EXTRA_LDFLAGS)" + +# Shell setup +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +# ============================================================================== +# Targets +# ============================================================================== + +.PHONY: all +all: code-gen test build ## Run code generation, compile all code, and execute tests. + +##@ General + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: code-gen +code-gen: ## Run code generation (Client, Listers, Informers). + @echo "Generating code..." + ./hack/update-codegen.sh + @echo "Synchronizing module dependencies..." + go mod tidy + +.PHONY: build +build: code-gen ## Compile all Go code after generation to verify type safety. + @echo "Compiling..." + go build -v $(LDFLAGS) $$(go list ./... | grep -vE '/hack/overlays/|/examples/|/tests/') + +.PHONY: test +test: code-gen ## Run unit tests and generate coverage. + @echo "Testing..." + go test -v $$(go list ./... | grep -vE '/hack/overlays/|/client/|/listers/|informers/|/fake|/scheme|/typed/|/examples/') -coverprofile cover.out + +.PHONY: clean +clean: ## Remove all generated code. + @echo "Cleaning generated code..." + rm -rf client listers informers diff --git a/client-go/README.md b/client-go/README.md new file mode 100644 index 000000000..44d2d9ef7 --- /dev/null +++ b/client-go/README.md @@ -0,0 +1,110 @@ +# NVIDIA Device API Go Client +`nvidia/client-go` is the official Go SDK for interacting with the node-local NVIDIA Device API. It provides a Kubernetes-native developer experience for building node-level agents, telemetry sidecars, and operators **driven by local device state.** + +By utilizing a node-local gRPC transport, this SDK allows agents to query device telemetry and status **without putting load on the central Kubernetes API server**. This architecture enables fine-grained **hardware monitoring** that scales independently of the cluster control plane. + +> [!WARNING] +> **Experimental Preview Release** +> +> This is an experimental release of the NVIDIA Device API Go client. Use at your own risk in production environments. The software is provided "as is" without warranties of any kind. Features, APIs, and configurations may change without notice in future releases. For production deployments, thoroughly test in non-critical environments first. + +## Key Features +- **Kubernetes-Native API**: Provides generated versioned clientsets, informers, and listers that work exactly like standard K8s clients. +- **gRPC Transport**: Optimized for low-latency, node-local communication via Unix domain sockets (UDS). +- **controller-runtime Integration**: Supports **Informer Injection** to drive standard Reconcilers with node-local gRPC streams. +- **Observability**: Includes **Prometheus metrics**, **error logging**, and full support for structured logging. + +## Installation +```bash +go get github.com/nvidia/nvsentinel/client-go +``` + +## Quick Start +The following snippet demonstrates how to initialize the client and **retrieve a list of GPUs from the local node**. +```go +package main + +import ( + "context" + "fmt" + "log" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/api/meta" + "github.com/nvidia/nvsentinel/client-go/pkg/clientset/versioned" + "github.com/nvidia/nvsentinel/client-go/pkg/nvgrpc" +) +func main() { + config := &nvgrpc.Config{Target: "unix:///var/run/nvidia-device-api/device-api.sock"} + clientset := versioned.NewForConfigOrDie(config) + + gpus, err := clientset.DeviceV1alpha1().GPUs().List(context.Background(), metav1.ListOptions{}) + if err != nil { + log.Fatalf("failed to list GPUs: %v", err) + } + + for _, gpu := range gpus.Items { + isReady := meta.IsStatusConditionTrue(gpu.Status.Conditions, "Ready") + fmt.Printf("GPU: %s | Ready: %v\n", gpu.Name, isReady) + } +} +``` + +## Capabilities +Currently, this SDK supports **Read-Only** APIs only. +- ✅ **Supported**: `Get`, `List`, `Watch` +- ❌ **Unsupported**: `Create`, `Update`, `UpdateStatus`, `Patch`, `Delete` + +## Usage +This repository includes a comprehensive set of examples demonstrating different integration patterns using Kubernetes-idiomatic Go. + +| Example | Focus | Description | +| :--- | :--- | :--- | +| **[basic-client](./examples/basic-client)** | **Reference Implementation** | Foundational SDK usage: initializing the clientset, listing resources, and inspecting status. | +| **[streaming-daemon](./examples/streaming-daemon)** | **Event-Driven Agent** | Production patterns: using gRPC interceptors and asynchronous `Watch` streams. | +| **[controller-shim](./examples/controller-shim)** | **Operator Integration** | **Informer Injection**: Driving a `controller-runtime` reconciler with node-local gRPC data. | + +See the [Examples directory](./examples) for detailed instructions on running these locally using the included "Fake Server". + +## Advanced Use: Informer Injection +For high-performance use cases, this SDK supports **Informer Injection**. + +This pattern allows `controller-runtime` Managers to source NVIDIA device state directly from the node-local gRPC stream via a `SharedIndexInformer`, while continuing to watch standard Cluster resources (like Pods or Nodes) from the central API server. + +### Why use this? +- **Latency**: React to hardware changes in milliseconds. +- **Scale**: No additional load on the K8s API server, even with thousands of nodes updating devices frequently. + +See the [Controller Shim Example](./examples/controller-shim) for a complete reference on implementing this hybrid reconciliation pattern. + +## Deployment Patterns +Clients built with this SDK are typically deployed as a **DaemonSet**. To ensure connectivity on **nodes equipped with NVIDIA devices (e.g., GPUs)**, the following Pod configuration is required. + +### Volume Mounts +The gRPC socket must be exposed to the container. Map the host directory containing the socket to the path expected by the client. + +```yaml +volumeMounts: +- name: device-api-socket + mountPath: /var/run/nvidia-device-api + readOnly: true +volumes: +- name: device-api-socket + hostPath: + path: /var/run/nvidia-device-api # Must match the location on the node + type: DirectoryOrCreate +``` + +### Environment Variables +Configure the client connection using the following environment variables. + +| Variable | Description | Default | +| :--- | :--- | :--- | +| **`NVIDIA_DEVICE_API_TARGET`** | The gRPC target address (URI) for the device API socket. | `unix:///var/run/nvidia-device-api/device-api.sock` | + +### Security +- **Filesystem Permissions**: The user running inside the container must have read/write permissions to the Unix socket file. +- **Kubernetes RBAC**: While device data is retrieved via gRPC, the `ServiceAccount` still requires standard RBAC permissions for cluster-level resources (e.g., `nodes`, `pods`) your app interacts with. + +## Development +For instructions on building the SDK, running tests, and understanding the code generation pipeline, please refer to [DEVELOPMENT.md](./DEVELOPMENT.md). diff --git a/client-go/client/versioned/clientset.go b/client-go/client/versioned/clientset.go new file mode 100644 index 000000000..c5e4be164 --- /dev/null +++ b/client-go/client/versioned/clientset.go @@ -0,0 +1,99 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by client-gen. DO NOT EDIT. + +package versioned + +import ( + fmt "fmt" + + devicev1alpha1 "github.com/nvidia/nvsentinel/client-go/client/versioned/typed/device/v1alpha1" + nvgrpc "github.com/nvidia/nvsentinel/client-go/nvgrpc" + grpc "google.golang.org/grpc" +) + +type Interface interface { + DeviceV1alpha1() devicev1alpha1.DeviceV1alpha1Interface +} + +// Clientset contains the clients for groups. +type Clientset struct { + deviceV1alpha1 *devicev1alpha1.DeviceV1alpha1Client +} + +// DeviceV1alpha1 retrieves the DeviceV1alpha1Client +func (c *Clientset) DeviceV1alpha1() devicev1alpha1.DeviceV1alpha1Interface { + return c.deviceV1alpha1 +} + +// NewForConfig creates a new Clientset for the given config. +// NewForConfig is equivalent to NewForConfigAndClient(c, clientConn), +// where clientConn was generated with nvgrpc.ClientConnFor(c). +// +// If you need to customize the connection (e.g. set a logger), +// use nvgrpc.ClientConnFor() manually and pass the connection to NewForConfigAndClient. +func NewForConfig(c *nvgrpc.Config) (*Clientset, error) { + if c == nil { + return nil, fmt.Errorf("config cannot be nil") + } + + configShallowCopy := *c // Shallow copy to avoid mutation + conn, err := nvgrpc.ClientConnFor(&configShallowCopy) + if err != nil { + return nil, err + } + + return NewForConfigAndClient(&configShallowCopy, conn) +} + +// NewForConfigAndClient creates a new Clientset for the given config and gRPC client connection. +// The provided gRPC client connection provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *nvgrpc.Config, conn grpc.ClientConnInterface) (*Clientset, error) { + if c == nil { + return nil, fmt.Errorf("config cannot be nil") + } + if conn == nil { + return nil, fmt.Errorf("gRPC connection cannot be nil") + } + + configShallowCopy := *c // Shallow copy to avoid mutation + + var cs Clientset + var err error + cs.deviceV1alpha1, err = devicev1alpha1.NewForConfigAndClient(&configShallowCopy, conn) + if err != nil { + return nil, err + } + + return &cs, nil +} + +// NewForConfigOrDie creates a new Clientset for the given config and +// panics if there is an error in the config or connection setup. +func NewForConfigOrDie(c *nvgrpc.Config) *Clientset { + cs, err := NewForConfig(c) + if err != nil { + panic(err) + } + return cs +} + +// New creates a new Clientset for the given gRPC client connection. +func New(conn grpc.ClientConnInterface) *Clientset { + var cs Clientset + cs.deviceV1alpha1 = devicev1alpha1.New(conn) + + return &cs +} diff --git a/client-go/client/versioned/scheme/doc.go b/client-go/client/versioned/scheme/doc.go new file mode 100644 index 000000000..55f52dc51 --- /dev/null +++ b/client-go/client/versioned/scheme/doc.go @@ -0,0 +1,18 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by client-gen. DO NOT EDIT. + +// This package contains the scheme of the automatically generated clientset. +package scheme diff --git a/client-go/client/versioned/scheme/register.go b/client-go/client/versioned/scheme/register.go new file mode 100644 index 000000000..97cf5a8ff --- /dev/null +++ b/client-go/client/versioned/scheme/register.go @@ -0,0 +1,54 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by client-gen. DO NOT EDIT. + +package scheme + +import ( + devicev1alpha1 "github.com/nvidia/nvsentinel/api/device/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +var Scheme = runtime.NewScheme() +var Codecs = serializer.NewCodecFactory(Scheme) +var ParameterCodec = runtime.NewParameterCodec(Scheme) +var localSchemeBuilder = runtime.SchemeBuilder{ + devicev1alpha1.AddToScheme, +} + +// AddToScheme adds all types of this clientset into the given scheme. This allows composition +// of clientsets, like in: +// +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) +// +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// +// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types +// correctly. +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(AddToScheme(Scheme)) +} diff --git a/client-go/client/versioned/typed/device/v1alpha1/device_client.go b/client-go/client/versioned/typed/device/v1alpha1/device_client.go new file mode 100644 index 000000000..74aeaf588 --- /dev/null +++ b/client-go/client/versioned/typed/device/v1alpha1/device_client.go @@ -0,0 +1,100 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + fmt "fmt" + + logr "github.com/go-logr/logr" + nvgrpc "github.com/nvidia/nvsentinel/client-go/nvgrpc" + grpc "google.golang.org/grpc" +) + +type DeviceV1alpha1Interface interface { + ClientConn() grpc.ClientConnInterface + GPUsGetter +} + +// DeviceV1alpha1Client is used to interact with features provided by the device.nvidia.com group. +type DeviceV1alpha1Client struct { + conn grpc.ClientConnInterface + logger logr.Logger +} + +func (c *DeviceV1alpha1Client) GPUs() GPUInterface { + return newGPUs(c) +} + +// NewForConfig creates a new DeviceV1alpha1Client for the given config. +// NewForConfig is equivalent to NewForConfigAndClient(c, clientConn), +// where clientConn was generated with nvgrpc.ClientConnFor(c). +func NewForConfig(c *nvgrpc.Config) (*DeviceV1alpha1Client, error) { + if c == nil { + return nil, fmt.Errorf("config cannot be nil") + } + + config := *c // Shallow copy to avoid mutation + conn, err := nvgrpc.ClientConnFor(&config) + if err != nil { + return nil, err + } + + return NewForConfigAndClient(&config, conn) +} + +// NewForConfigAndClient creates a new DeviceV1alpha1Client for the given config and gRPC client connection. +// Note the grpc client connection provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *nvgrpc.Config, conn grpc.ClientConnInterface) (*DeviceV1alpha1Client, error) { + if c == nil { + return nil, fmt.Errorf("config cannot be nil") + } + if conn == nil { + return nil, fmt.Errorf("gRPC connection cannot be nil") + } + + return &DeviceV1alpha1Client{ + conn: conn, + logger: c.GetLogger().WithName("device.nvidia.com.v1alpha1"), + }, nil +} + +// NewForConfigOrDie creates a new DeviceV1alpha1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *nvgrpc.Config) *DeviceV1alpha1Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new DeviceV1alpha1Client for the given gRPC client connection. +func New(c grpc.ClientConnInterface) *DeviceV1alpha1Client { + return &DeviceV1alpha1Client{ + conn: c, + logger: logr.Discard(), + } +} + +// ClientConn returns a gRPC client connection that is used to communicate +// with API server by this client implementation. +func (c *DeviceV1alpha1Client) ClientConn() grpc.ClientConnInterface { + if c == nil { + return nil + } + return c.conn +} diff --git a/client-go/client/versioned/typed/device/v1alpha1/doc.go b/client-go/client/versioned/typed/device/v1alpha1/doc.go new file mode 100644 index 000000000..7749c1800 --- /dev/null +++ b/client-go/client/versioned/typed/device/v1alpha1/doc.go @@ -0,0 +1,18 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1alpha1 diff --git a/client-go/client/versioned/typed/device/v1alpha1/generated_expansion.go b/client-go/client/versioned/typed/device/v1alpha1/generated_expansion.go new file mode 100644 index 000000000..c99bbb48c --- /dev/null +++ b/client-go/client/versioned/typed/device/v1alpha1/generated_expansion.go @@ -0,0 +1,19 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +type GPUExpansion interface{} diff --git a/client-go/client/versioned/typed/device/v1alpha1/gpu.go b/client-go/client/versioned/typed/device/v1alpha1/gpu.go new file mode 100644 index 000000000..844a5efda --- /dev/null +++ b/client-go/client/versioned/typed/device/v1alpha1/gpu.go @@ -0,0 +1,129 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + logr "github.com/go-logr/logr" + devicev1alpha1 "github.com/nvidia/nvsentinel/api/device/v1alpha1" + pb "github.com/nvidia/nvsentinel/api/gen/go/device/v1alpha1" + nvgrpc "github.com/nvidia/nvsentinel/client-go/nvgrpc" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" +) + +// GPUsGetter has a method to return a GPUInterface. +// A group's client should implement this interface. +type GPUsGetter interface { + GPUs() GPUInterface +} + +// GPUInterface has methods to work with GPU resources. +type GPUInterface interface { + Get(ctx context.Context, name string, opts v1.GetOptions) (*devicev1alpha1.GPU, error) + List(ctx context.Context, opts v1.ListOptions) (*devicev1alpha1.GPUList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + GPUExpansion +} + +// gpus implements GPUInterface +type gpus struct { + client pb.GpuServiceClient + logger logr.Logger +} + +// newGPUs returns a gpus +func newGPUs(c *DeviceV1alpha1Client) *gpus { + return &gpus{ + client: pb.NewGpuServiceClient(c.ClientConn()), + logger: c.logger.WithName("gpus"), + } +} + +func (c *gpus) Get(ctx context.Context, name string, opts v1.GetOptions) (*devicev1alpha1.GPU, error) { + resp, err := c.client.GetGpu(ctx, &pb.GetGpuRequest{ + Name: name, + }) + if err != nil { + return nil, err + } + + obj := devicev1alpha1.FromProto(resp.GetGpu()) + c.logger.V(6).Info("Fetched GPU", + "name", name, + "resource-version", obj.GetResourceVersion(), + ) + + return obj, nil +} + +func (c *gpus) List(ctx context.Context, opts v1.ListOptions) (*devicev1alpha1.GPUList, error) { + resp, err := c.client.ListGpus(ctx, &pb.ListGpusRequest{ + ResourceVersion: opts.ResourceVersion, + }) + if err != nil { + return nil, err + } + + list := devicev1alpha1.FromProtoList(resp.GetGpuList()) + c.logger.V(5).Info("Listed GPUs", + "count", len(list.Items), + "resource-version", list.GetResourceVersion(), + ) + + return list, nil +} + +func (c *gpus) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + c.logger.V(4).Info("Opening watch stream", + "resource", "gpus", + "resource-version", opts.ResourceVersion, + ) + + ctx, cancel := context.WithCancel(ctx) + stream, err := c.client.WatchGpus(ctx, &pb.WatchGpusRequest{ + ResourceVersion: opts.ResourceVersion, + }) + if err != nil { + cancel() + return nil, err + } + + return nvgrpc.NewWatcher(&gpusStreamAdapter{stream: stream}, cancel, c.logger), nil +} + +// gpusStreamAdapter wraps the GPU gRPC stream to provide events. +type gpusStreamAdapter struct { + stream pb.GpuService_WatchGpusClient +} + +func (a *gpusStreamAdapter) Next() (string, runtime.Object, error) { + resp, err := a.stream.Recv() + if err != nil { + return "", nil, err + } + + obj := devicev1alpha1.FromProto(resp.GetObject()) + + return resp.GetType(), obj, nil +} + +func (a *gpusStreamAdapter) Close() error { + return a.stream.CloseSend() +} diff --git a/client-go/examples/README.md b/client-go/examples/README.md new file mode 100644 index 000000000..ea4adadec --- /dev/null +++ b/client-go/examples/README.md @@ -0,0 +1,31 @@ +# NVIDIA Device API Go Client: Examples +This directory contains a suite of examples demonstrating how to use `nvidia/client-go` to interact with the node-local NVIDIA Device API using Kubernetes-idiomatic patterns. + +## Usage Examples + +| Directory | Focus | Complexity | Description | +| :--- | :--- | :--- | :--- | +| **[basic-client](./basic-client)** | **Reference Implementation** | Basic | Foundational SDK usage: initializing the clientset and performing standard operations. | +| **[streaming-daemon](./streaming-daemon)** | **Event-Driven Agent** | Intermediate | Demonstrates long-running processes using gRPC interceptors and asynchronous `Watch` streams. | +| **[controller-shim](./controller-shim)** | **Operator Integration** | Advanced | **Informer Injection**: Shows how to drive a `controller-runtime` reconciler using node-local cached data. | + +## Prerequisites +All examples are designed to run locally on your development machine using the included **Fake Server**. + +### 1. Start the Fake Server +The server simulates a node with 8 GPUs and generates random status change events to test `Watch` and Informer capabilities. + +```bash +sudo go run ./fake-server/main.go +``` +**Note:** `sudo` is required because the default socket path is in `/var/run/`. To run without root privileges, override the socket path to a user-writable location with the `NVIDIA_DEVICE_API_TARGET` environment variable. + +## Run an Example +Once the server is running, navigate to any example directory and run it: + +```bash +# Running the reference implementation +cd examples/basic-client +sudo go run main.go +``` +**Note:** `sudo` is required because the default socket path is in `/var/run/`. If you started the server with a non-default target, override the socket path with the `NVIDIA_DEVICE_API_TARGET` environment variable to the same URI here. diff --git a/client-go/examples/basic-client/README.md b/client-go/examples/basic-client/README.md new file mode 100644 index 000000000..35dbde23f --- /dev/null +++ b/client-go/examples/basic-client/README.md @@ -0,0 +1,27 @@ +# Basic Client: Reference Implementation +This example serves as the **reference implementation** for the NVIDIA Device API Go Client. It demonstrates the idiomatic way to initialize the `clientset`, interact with node-local resources, and inspect object fields using standard Kubernetes `meta` helpers. + +## Key Concepts Covered +* **Client Initialization**: Setting up a gRPC connection over a Unix domain socket (UDS). +* **K8s-Native Verbs**: Using standard `Get` and `List` operations to retrieve resource snapshots. +* **Metadata Inspection**: Utilizing `metav1` helpers to parse status conditions and object metadata. + +## Running +1. Ensure the [Fake Server](../fake-server) is running. +2. Run the example: + +```bash +sudo go run main.go +``` +**Note:** `sudo` is required because the default socket path is in `/var/run/`. If you started the server with a non-default target, override the socket path with the `NVIDIA_DEVICE_API_TARGET` environment variable to the same URI here. + +## Expected Output +```text +"level"=0 "msg"="discovered GPUs" "count"=8 "target"="unix:///var/run/nvidia-device-api/device-api.sock" +"level"=0 "msg"="details" "name"="gpu-0" "uuid"="GPU-6e5b6a57..." +"level"=0 "msg"="status" "name"="gpu-0" "uuid"="GPU-6e5b6a57..." "status"="Ready" +"level"=0 "msg"="status" "name"="gpu-1" "uuid"="GPU-2b418863..." "status"="Ready" +"level"=0 "msg"="status" "name"="gpu-2" "uuid"="GPU-4e4e629e..." "status"="Ready" +... +"level"=0 "msg"="status" "name"="gpu-7" "uuid"="GPU-66ba2ccd..." "status"="NotReady" +``` diff --git a/client-go/examples/basic-client/main.go b/client-go/examples/basic-client/main.go new file mode 100644 index 000000000..41aec374d --- /dev/null +++ b/client-go/examples/basic-client/main.go @@ -0,0 +1,88 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package main demonstrates the basic usage of the NVIDIA Device API client. +// +// It connects to the device-api server, lists all available GPUs, inspects +// their status fields using standard Kubernetes meta helpers, and logs the +// results to stdout. +package main + +import ( + "context" + "log" + "os" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/go-logr/stdr" + "github.com/nvidia/nvsentinel/client-go/client/versioned" + "github.com/nvidia/nvsentinel/client-go/nvgrpc" +) + +func main() { + logger := stdr.New(log.New(os.Stdout, "", log.LstdFlags)) + + // Determine the connection target. + // If the environment variable NVIDIA_DEVICE_API_TARGET is not set, use the + // default socket path: unix:///var/run/nvidia-device-api/device-api.sock + target := os.Getenv(nvgrpc.NvidiaDeviceAPITargetEnvVar) + if target == "" { + target = nvgrpc.DefaultNvidiaDeviceAPISocket + } + + // Initialize the versioned clientset using the gRPC transport. + config := &nvgrpc.Config{Target: target} + clientset, err := versioned.NewForConfig(config) + if err != nil { + logger.Error(err, "unable to create clientset") + os.Exit(1) + } + + // List all GPUs to discover what is available on the node. + gpus, err := clientset.DeviceV1alpha1().GPUs().List(context.Background(), metav1.ListOptions{}) + if err != nil { + logger.Error(err, "failed to list GPUs") + os.Exit(1) + } + logger.Info("discovered GPUs", "count", len(gpus.Items), "target", target) + + // Fetch a specific GPU by name. + if len(gpus.Items) > 0 { + firstName := gpus.Items[0].Name + gpu, err := clientset.DeviceV1alpha1().GPUs().Get(context.Background(), firstName, metav1.GetOptions{}) + if err != nil { + logger.Error(err, "failed to fetch GPU", "name", firstName) + os.Exit(1) + } + logger.Info("details", "name", gpu.Name, "uuid", gpu.Spec.UUID) + } + + // Inspect status conditions. + for _, gpu := range gpus.Items { + // Use standard K8s meta helpers to check status conditions safely. + isReady := meta.IsStatusConditionTrue(gpu.Status.Conditions, "Ready") + status := "NotReady" + if isReady { + status = "Ready" + } + + logger.Info("status", + "name", gpu.Name, + "uuid", gpu.Spec.UUID, + "status", status, + ) + } +} diff --git a/client-go/examples/controller-shim/README.md b/client-go/examples/controller-shim/README.md new file mode 100644 index 000000000..1c877d96c --- /dev/null +++ b/client-go/examples/controller-shim/README.md @@ -0,0 +1,34 @@ +# Controller Shim: Operator Integration Reference +This example demonstrates the Advanced **Informer Injection** pattern. It enables `controller-runtime` to drive standard Reconcilers using a node-local gRPC stream as a high-performance data source, while maintaining connectivity to the central Kubernetes API server. + +## Key Concepts Covered +- **Informer Injection**: Overriding the standard `cache.Options` to inject a gRPC-backed `SharedIndexInformer` for Device types. +- **Hybrid Connectivity**: Managing a `Manager` that maintains both a REST client (to K8s API) and a gRPC client (to Device API). +- **Transparent Caching**: Ensuring `mgr.GetClient()` reads from the local gRPC-backed cache automatically for Device types. +- **Controller-Runtime Integration**: Using standard `builder` patterns to set up a controller that reacts to local UDS events. + - _TIP_: Review the `NewInformer` setup in `main.go`. This is the "magic" that allows the standard `controller-runtime` machinery to work over gRPC/UDS without modifying the core controller logic. + +## Running +1. Ensure the [Fake Server](../fake-server) is running. +2. Run the controller: + +```bash +sudo go run main.go +``` +**Note:** `sudo` is required because the default socket path is in `/var/run/`. If you started the server with a non-default target, override the socket path with the `NVIDIA_DEVICE_API_TARGET` environment variable to the same URI here. + +To stop the controller, press `Ctrl+C` + +## Expected Output +The controller will initialize, start the internal metrics server, and immediately begin reconciling the 8 GPUs provided by the local fake server. + +```text +INFO setup starting manager +INFO controller-runtime.metrics Starting metrics server +INFO Starting EventSource {"controller": "gpu", "source": "kind source: *v1alpha1.GPU"} +INFO Starting Controller {"controller": "gpu"} +INFO Starting workers {"controller": "gpu", "worker count": 1} +INFO Reconciled GPU {"controller": "gpu", "name": "gpu-1", "uuid": "GPU-6bc0eaf0..."} +INFO Reconciled GPU {"controller": "gpu", "name": "gpu-2", "uuid": "GPU-0851fc7c..."} +... +``` diff --git a/client-go/examples/controller-shim/main.go b/client-go/examples/controller-shim/main.go new file mode 100644 index 000000000..1454ee834 --- /dev/null +++ b/client-go/examples/controller-shim/main.go @@ -0,0 +1,152 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package main demonstrates how to build a Kubernetes Controller for local devices. +// +// It uses controller-runtime to reconcile GPU resources, injecting a custom +// gRPC-backed Informer to bypass the standard Kubernetes API server and +// read directly from the local NVIDIA Device API socket. +package main + +import ( + "context" + "net/http" + "os" + "time" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/rest" + toolscache "k8s.io/client-go/tools/cache" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + devicev1alpha1 "github.com/nvidia/nvsentinel/api/device/v1alpha1" + "github.com/nvidia/nvsentinel/client-go/client/versioned" + "github.com/nvidia/nvsentinel/client-go/client/versioned/scheme" + informers "github.com/nvidia/nvsentinel/client-go/informers/externalversions" + "github.com/nvidia/nvsentinel/client-go/nvgrpc" +) + +func main() { + ctrl.SetLogger(zap.New(zap.UseDevMode(true))) + setupLog := ctrl.Log.WithName("setup") + + // Determine the connection target. + // If the environment variable NVIDIA_DEVICE_API_TARGET is not set, use the + // default socket path: unix:///var/run/nvidia-device-api/device-api.sock + target := os.Getenv(nvgrpc.NvidiaDeviceAPITargetEnvVar) + if target == "" { + target = nvgrpc.DefaultNvidiaDeviceAPISocket + } + + // Initialize the versioned Clientset using the gRPC transport. + config := &nvgrpc.Config{Target: target} + clientset, err := versioned.NewForConfig(config) + if err != nil { + setupLog.Error(err, "unable to create clientset") + os.Exit(1) + } + + // Create a factory for the gRPC-backed informers. + // We use a 10-minute resync period to ensure cache consistency. + // Note: We do not start the factory here; the Manager will start the injected informer. + factory := informers.NewSharedInformerFactory(clientset, 10*time.Minute) + gpuInformer := factory.Device().V1alpha1().GPUs().Informer() + + // Initialize the controller-runtime Manager. + // A dummy rest.Config is used here as we are not connecting to a real K8s API server. + mgr, err := ctrl.NewManager(&rest.Config{Host: "http://localhost:0"}, ctrl.Options{ + Scheme: scheme.Scheme, + // MapperProvider returns a RESTMapper that defines GPU resources as root-scoped. + // Required because the gRPC endpoint does not provide discovery APIs. + MapperProvider: func(c *rest.Config, httpClient *http.Client) (meta.RESTMapper, error) { + mapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{devicev1alpha1.SchemeGroupVersion}) + mapper.Add(devicev1alpha1.SchemeGroupVersion.WithKind("GPU"), meta.RESTScopeRoot) + return mapper, nil + }, + Cache: cache.Options{ + // NewInformer allows injecting a specific informer for a GroupVersionKind. + // This bypasses the default API server ListerWatcher for GPU resources. + NewInformer: func(lw toolscache.ListerWatcher, obj runtime.Object, resync time.Duration, indexers toolscache.Indexers) toolscache.SharedIndexInformer { + if _, ok := obj.(*devicev1alpha1.GPU); ok { + // Merge the indexers required by controller-runtime with those + // already present in the gRPC informer. Conflicting keys (e.g., "namespace") + // are skipped to prefer the existing implementation. + existingIndexers := gpuInformer.GetIndexer().GetIndexers() + for key, indexerFunc := range indexers { + if _, exists := existingIndexers[key]; !exists { + err := gpuInformer.AddIndexers(toolscache.Indexers{key: indexerFunc}) + if err != nil { + setupLog.Error(err, "failed to add indexer to informer", "key", key) + os.Exit(1) + } + } + } + return gpuInformer + } + // Fallback: For all other types, return a standard informer. This allows the + // manager to still handle standard Kubernetes resources (like Pods or Events) + // using the default API server transport, enabling "hybrid" reconciliation. + return toolscache.NewSharedIndexInformer(lw, obj, resync, indexers) + }, + }, + }) + if err != nil { + setupLog.Error(err, "unable to create manager") + os.Exit(1) + } + + if err = (&GPUReconciler{ + Client: mgr.GetClient(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "gpu") + os.Exit(1) + } + + ctx := ctrl.SetupSignalHandler() + + setupLog.Info("starting manager") + if err := mgr.Start(ctx); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} + +// GPUReconciler reconciles GPUs using the local gRPC cache. +type GPUReconciler struct { + client.Client +} + +func (r *GPUReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := ctrl.LoggerFrom(ctx) + + var gpu devicev1alpha1.GPU + // The Get call is transparently routed through the injected gRPC-backed informer. + if err := r.Get(ctx, req.NamespacedName, &gpu); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + log.Info("Reconciled GPU", "name", gpu.Name, "uuid", gpu.Spec.UUID) + return ctrl.Result{}, nil +} + +func (r *GPUReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&devicev1alpha1.GPU{}). + Complete(r) +} diff --git a/client-go/examples/fake-server/README.md b/client-go/examples/fake-server/README.md new file mode 100644 index 000000000..42a34fc95 --- /dev/null +++ b/client-go/examples/fake-server/README.md @@ -0,0 +1,29 @@ +# Fake Device API Server + +This program simulates a running NVIDIA Device API service. It is used to run the SDK examples locally without requiring a physical GPU node or root privileges. + +## Usage + +Run the server in a dedicated terminal window. It will create a Unix domain socket (UDS) and block until interrupted. + +```bash +sudo go run main.go +# Output: Fake Device API listening on /var/run/nvidia-device-api/device-api.sock +``` +To stop the server, press `Ctrl+C` + +**Note:** `sudo` is required because the default socket path is in `/var/run/`. + +### Running without Root +To run without root privileges, override the socket path to a user-writable location: + +```bash +export NVIDIA_DEVICE_API_TARGET=unix:///tmp/device-api.sock +go run main.go +``` +**Important**: If you change the socket path here, you must also export the same `NVIDIA_DEVICE_API_TARGET` environment variable in every terminal window where you run the client examples. + +## Behavior +- **Endpoint**: Defaults to `/var/run/nvidia-device-api/device-api.sock` +- **Inventory**: Simulates 8 NVIDIA GPUs (`gpu-0` through `gpu-7`) +- **Simulation**: Every 2 seconds, it randomly selects a GPU and toggles its status between `Ready` and `NotReady`. This generates events for testing `Watch` streams and controllers. diff --git a/client-go/examples/fake-server/main.go b/client-go/examples/fake-server/main.go new file mode 100644 index 000000000..e0efd31ae --- /dev/null +++ b/client-go/examples/fake-server/main.go @@ -0,0 +1,285 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package main implements a fake NVIDIA Device API server. +// +// It simulates a running device-api service over a Unix Domain Socket (UDS), +// maintaining an in-memory inventory of GPU resources and periodically +// toggling their readiness status to generate Watch events. +package main + +import ( + "context" + "fmt" + "log" + "math/rand" + "net" + "os" + "os/signal" + "path/filepath" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + devicev1alpha1 "github.com/nvidia/nvsentinel/api/device/v1alpha1" + pb "github.com/nvidia/nvsentinel/api/gen/go/device/v1alpha1" + "github.com/nvidia/nvsentinel/client-go/nvgrpc" +) + +func main() { + fmt.Printf("Starting server...\n") + + // Determine the connection target. + // If the environment variable NVIDIA_DEVICE_API_TARGET is not set, use the + // default socket path: unix:///var/run/nvidia-device-api/device-api.sock + target := os.Getenv(nvgrpc.NvidiaDeviceAPITargetEnvVar) + if target == "" { + target = nvgrpc.DefaultNvidiaDeviceAPISocket + } + + socketPath := strings.TrimPrefix(target, "unix://") + fmt.Printf("socketPath: %s\n", socketPath) + + if err := os.MkdirAll(filepath.Dir(socketPath), 0755); err != nil { + log.Fatalf("failed to create socket directory: %v", err) + } + + if _, err := os.Stat(socketPath); err == nil { + if err := os.Remove(socketPath); err != nil { + log.Fatalf("failed to remove stale socket: %v", err) + } + } + + listener, err := net.Listen("unix", socketPath) + if err != nil { + log.Fatalf("failed to listen on %s: %v", socketPath, err) + } + + serverImpl := newFakeServer() + go serverImpl.simulateChanges() + + srv := grpc.NewServer() + pb.RegisterGpuServiceServer(srv, serverImpl) + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + fmt.Println("\nStopping server...") + srv.GracefulStop() + os.Remove(socketPath) + os.Exit(0) + }() + + fmt.Printf("Fake Device API listening on %s\n", socketPath) + if err := srv.Serve(listener); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} + +type fakeServer struct { + pb.UnimplementedGpuServiceServer + mu sync.RWMutex + gpus []devicev1alpha1.GPU + listeners map[chan struct{}]chan devicev1alpha1.GPU + currentRV int +} + +func newFakeServer() *fakeServer { + s := &fakeServer{ + gpus: make([]devicev1alpha1.GPU, 8), + listeners: make(map[chan struct{}]chan devicev1alpha1.GPU), + } + + for i := 0; i < 8; i++ { + s.gpus[i] = devicev1alpha1.GPU{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("gpu-%d", i), + ResourceVersion: "1", + }, + Spec: devicev1alpha1.GPUSpec{ + UUID: generateGPUUUID(), + }, + Status: devicev1alpha1.GPUStatus{ + Conditions: []metav1.Condition{ + { + Type: "Ready", + Status: metav1.ConditionTrue, + Reason: "DriverReady", + Message: "driver is posting ready status", + }, + }, + }, + } + } + return s +} + +// simulateChanges flips the Ready status of a random GPU every few seconds +// to generate events for active Watch streams. +func (s *fakeServer) simulateChanges() { + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for range ticker.C { + s.mu.Lock() + + // Pick a random GPU to update + idx := rand.Intn(len(s.gpus)) + gpu := &s.gpus[idx] + + // Increment ResourceVersion for K8s watch semantics + s.currentRV++ + newRVStr := strconv.Itoa(s.currentRV) + gpu.ResourceVersion = newRVStr + + // Toggle the Ready condition + isReady := gpu.Status.Conditions[0].Status == metav1.ConditionTrue + var newStatus metav1.ConditionStatus + if isReady { + newStatus = metav1.ConditionFalse + } else { + newStatus = metav1.ConditionTrue + } + + gpu.Status.Conditions[0].Status = newStatus + gpu.Status.Conditions[0].LastTransitionTime = metav1.Now() + + updatedGPU := *gpu.DeepCopy() + s.mu.Unlock() + + s.broadcast(updatedGPU) + } +} + +func (s *fakeServer) broadcast(gpu devicev1alpha1.GPU) { + s.mu.RLock() + defer s.mu.RUnlock() + for _, ch := range s.listeners { + select { + case ch <- gpu: + default: + } + } +} + +func (s *fakeServer) GetGpu(ctx context.Context, req *pb.GetGpuRequest) (*pb.GetGpuResponse, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + for _, gpu := range s.gpus { + if req.Name == gpu.Name { + return &pb.GetGpuResponse{Gpu: devicev1alpha1.ToProto(&gpu)}, nil + } + } + + return nil, status.Errorf(codes.NotFound, "gpu %q not found", req.Name) +} + +func (s *fakeServer) ListGpus(ctx context.Context, req *pb.ListGpusRequest) (*pb.ListGpusResponse, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + gpuList := &pb.GpuList{ + Items: make([]*pb.Gpu, 0, len(s.gpus)), + } + + for _, gpu := range s.gpus { + gpuList.Items = append(gpuList.Items, devicev1alpha1.ToProto(&gpu)) + } + + gpuList.Metadata = &pb.ListMeta{ + ResourceVersion: strconv.Itoa(s.currentRV), + } + + return &pb.ListGpusResponse{GpuList: gpuList}, nil +} + +func (s *fakeServer) WatchGpus(req *pb.WatchGpusRequest, stream pb.GpuService_WatchGpusServer) error { + var requestRV int + if req.ResourceVersion != "" { + requestRV, _ = strconv.Atoi(req.ResourceVersion) + } + + if requestRV == 0 { + // Send Initial State (ADDED events) + var initial []devicev1alpha1.GPU + s.mu.RLock() + initial = make([]devicev1alpha1.GPU, len(s.gpus)) + for i, g := range s.gpus { + initial[i] = *g.DeepCopy() + } + s.mu.RUnlock() + + for _, gpu := range initial { + if err := stream.Send(&pb.WatchGpusResponse{ + Type: "ADDED", + Object: devicev1alpha1.ToProto(&gpu), + }); err != nil { + return err + } + } + } + + // Register for updates + updateCh := make(chan devicev1alpha1.GPU, 10) + stopKey := make(chan struct{}) + + s.mu.Lock() + s.listeners[stopKey] = updateCh + s.mu.Unlock() + + defer func() { + s.mu.Lock() + delete(s.listeners, stopKey) + s.mu.Unlock() + }() + + log.Printf("Watch stream connected (starting RV: %d)", requestRV) + + // Stream updates + for { + select { + case <-stream.Context().Done(): + log.Println("Watch stream disconnected") + return nil + case gpu := <-updateCh: + gpuRV, _ := strconv.Atoi(gpu.ResourceVersion) + if gpuRV > requestRV { + err := stream.Send(&pb.WatchGpusResponse{ + Type: "MODIFIED", + Object: devicev1alpha1.ToProto(&gpu), + }) + if err != nil { + return err + } + } + } + } +} + +func generateGPUUUID() string { + b := make([]byte, 16) + _, _ = rand.Read(b) + b[6] = (b[6] & 0x0f) | 0x40 + b[8] = (b[8] & 0x3f) | 0x80 + return fmt.Sprintf("GPU-%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) +} diff --git a/client-go/examples/streaming-daemon/README.md b/client-go/examples/streaming-daemon/README.md new file mode 100644 index 000000000..092650fd4 --- /dev/null +++ b/client-go/examples/streaming-daemon/README.md @@ -0,0 +1,32 @@ +# Streaming Daemon: Event-Driven Agent Reference +This example demonstrates **production-grade usage patterns** for the NVIDIA Device API Go Client, specifically focusing on long-lived, asynchronous operations and telemetry. + +This reference shows how to build a robust, event-driven agent that reacts to real-time device state changes without polling. + +## Key Concepts Covered +* **Manual Connection Management**: Constructing a `grpc.ClientConn` with custom dialers for Unix domain sockets (UDS). +* **Middleware (Interceptors)**: Injecting telemetry (Request IDs) and structured logging into the gRPC transport layer. +* **Stream Processing**: Handling long-lived `Watch()` streams and implementing event-loop logic. +* **Context Handling**: Proper management of signal cancellation (SIGTERM) and stream lifecycle. + +## Running +1. Ensure the [Fake Server](../fake-server) is running. +2. Run the example: + +```bash +sudo go run main.go +``` +**Note:** `sudo` is required because the default socket path is in `/var/run/`. If you started the server with a non-default target, override the socket path with the `NVIDIA_DEVICE_API_TARGET` environment variable to the same URI here. + +To stop the application, press `Ctrl+C` + +## Expected Output +```text +"level"=0 "msg"="retrieved GPU list" "count"=8 +"level"=0 "msg"="starting long-lived watch stream" "method"="/nvidia.nvsentinel.v1alpha1.GpuService/WatchGpus" +"level"=0 "msg"="watch stream established, waiting for events..." +"level"=0 "msg"="gpu status changed" "event"="ADDED" "name"="gpu-0" "uuid"="GPU-b56c1d18..." "status"="NotReady" +"level"=0 "msg"="gpu status changed" "event"="MODIFIED" "name"="gpu-0" "uuid"="GPU-b56c1d18..." "status"="Ready" +... +"level"=0 "msg"="gpu status changed" "event"="MODIFIED" "name"="gpu-1" "uuid"="GPU-2e6d5c15..." "status"="NotReady" +``` diff --git a/client-go/examples/streaming-daemon/main.go b/client-go/examples/streaming-daemon/main.go new file mode 100644 index 000000000..df07ceeaf --- /dev/null +++ b/client-go/examples/streaming-daemon/main.go @@ -0,0 +1,155 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package main demonstrates a long-running, event-driven agent. +// +// It establishes a persistent Watch stream with the device-api server, +// handling gRPC connection lifecycles, custom interceptors for telemetry, +// and real-time event processing for device state changes. +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "syscall" + + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/watch" + + "github.com/go-logr/stdr" + devicev1alpha1 "github.com/nvidia/nvsentinel/api/device/v1alpha1" + "github.com/nvidia/nvsentinel/client-go/client/versioned" + "github.com/nvidia/nvsentinel/client-go/nvgrpc" +) + +func main() { + // Initialize a standard logger for transport-level visibility. + logger := stdr.New(log.New(os.Stdout, "", log.LstdFlags)) + stdr.SetVerbosity(1) + + // Determine the connection target. + // If the environment variable NVIDIA_DEVICE_API_TARGET is not set, use the + // default socket path: unix:///var/run/nvidia-device-api/device-api.sock + target := os.Getenv(nvgrpc.NvidiaDeviceAPITargetEnvVar) + if target == "" { + target = nvgrpc.DefaultNvidiaDeviceAPISocket + } + + // tracingInterceptor injects metadata (x-request-id) into outgoing requests. + // This enables request tracking across the gRPC boundary. + tracingInterceptor := func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + ctx = metadata.AppendToOutgoingContext(ctx, "x-request-id", "nv-trace-123") + return invoker(ctx, method, req, reply, cc, opts...) + } + + // watchMonitorInterceptor logs the start of long-lived Watch streams. + watchMonitorInterceptor := func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { + logger.Info("starting long-lived watch stream", "method", method) + return streamer(ctx, desc, cc, method, opts...) + } + + // Configure manual DialOptions for transport-level control. + opts := []nvgrpc.DialOption{ + nvgrpc.WithLogger(logger), + nvgrpc.WithUnaryInterceptor(tracingInterceptor), + nvgrpc.WithStreamInterceptor(watchMonitorInterceptor), + } + + // Initialize the underlying gRPC connection manually. + config := &nvgrpc.Config{Target: target} + conn, err := nvgrpc.ClientConnFor(config, opts...) + if err != nil { + logger.Error(err, "unable to connect to gRPC target") + os.Exit(1) + } + defer conn.Close() + + // Initialize the Clientset using the existing connection. + // This is required when specific gRPC lifecycle or interceptor management is needed. + clientset, err := versioned.NewForConfigAndClient(config, conn) + if err != nil { + logger.Error(err, "unable to create clientset") + os.Exit(1) + } + + // Create a context that is canceled when the app receives SIGINT or SIGTERM. + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + + // List GPUs. This triggers the Unary interceptor. + list, err := clientset.DeviceV1alpha1().GPUs().List(ctx, metav1.ListOptions{}) + if err != nil { + logger.Error(err, "failed to list GPUs") + os.Exit(1) + } + logger.Info("retrieved GPU list", "count", len(list.Items)) + + // Watch GPUs. This triggers the Stream interceptor. + watcher, err := clientset.DeviceV1alpha1().GPUs().Watch(ctx, metav1.ListOptions{ + ResourceVersion: list.ResourceVersion, + }) + if err != nil { + logger.Error(err, "failed to establish watch stream") + os.Exit(1) + } else { + defer watcher.Stop() + logger.Info("watch stream established, waiting for events...") + + for { + select { + case event, ok := <-watcher.ResultChan(): + if !ok { + logger.Info("watch channel closed by server") + return + } + + if event.Type == watch.Error { + if status, ok := event.Object.(*metav1.Status); ok { + logger.Info("received watch error from server", "reason", status.Reason, "message", status.Message) + } + return + } + + gpu, ok := event.Object.(*devicev1alpha1.GPU) + if !ok { + logger.Info("received unknown object type", "type", fmt.Sprintf("%T", event.Object)) + continue + } + + isReady := meta.IsStatusConditionTrue(gpu.Status.Conditions, "Ready") + status := "NotReady" + if isReady { + status = "Ready" + } + + logger.Info("gpu status changed", + "event", event.Type, + "name", gpu.Name, + "uuid", gpu.Spec.UUID, + "status", status, + ) + + case <-ctx.Done(): + logger.Info("received shutdown signal, stopping watch") + return + } + } + } +} diff --git a/client-go/go.mod b/client-go/go.mod new file mode 100644 index 000000000..72edf172e --- /dev/null +++ b/client-go/go.mod @@ -0,0 +1,78 @@ +module github.com/nvidia/nvsentinel/client-go + +go 1.25.5 + +replace github.com/nvidia/nvsentinel/api => ../api + +replace github.com/nvidia/nvsentinel/client-go => . + +require ( + github.com/go-logr/logr v1.4.3 + github.com/go-logr/stdr v1.2.2 + github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 + github.com/nvidia/nvsentinel/api v0.0.0 + github.com/prometheus/client_golang v1.22.0 + google.golang.org/grpc v1.77.0 + k8s.io/apimachinery v0.34.1 + k8s.io/client-go v0.34.1 + sigs.k8s.io/controller-runtime v0.22.4 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.32.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/time v0.9.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.34.1 // indirect + k8s.io/apiextensions-apiserver v0.34.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) diff --git a/client-go/go.sum b/client-go/go.sum new file mode 100644 index 000000000..ce1d9a939 --- /dev/null +++ b/client-go/go.sum @@ -0,0 +1,225 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0 h1:FbSCl+KggFl+Ocym490i/EyXF4lPgLoUtcSWquBM0Rs= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= +k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A= +sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/client-go/hack/boilerplate.go.txt b/client-go/hack/boilerplate.go.txt new file mode 100644 index 000000000..e1732e8d5 --- /dev/null +++ b/client-go/hack/boilerplate.go.txt @@ -0,0 +1,13 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. diff --git a/client-go/hack/update-codegen.sh b/client-go/hack/update-codegen.sh new file mode 100755 index 000000000..8e6f8e6a9 --- /dev/null +++ b/client-go/hack/update-codegen.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)" +CODEGEN_ROOT="${REPO_ROOT}/code-generator" + +export KUBE_CODEGEN_ROOT="${CODEGEN_ROOT}" + +source "${CODEGEN_ROOT}/kube_codegen.sh" + +kube::codegen::gen_client \ + --proto-base "github.com/nvidia/nvsentinel/api/gen/go" \ + --output-dir "${REPO_ROOT}/client-go" \ + --output-pkg "github.com/nvidia/nvsentinel/client-go" \ + --boilerplate "${REPO_ROOT}/client-go/hack/boilerplate.go.txt" \ + --clientset-name "client" \ + --versioned-name "versioned" \ + --with-watch \ + --listers-name "listers" \ + --informers-name "informers" \ + "${REPO_ROOT}/api" diff --git a/client-go/informers/externalversions/device/interface.go b/client-go/informers/externalversions/device/interface.go new file mode 100644 index 000000000..661ca6287 --- /dev/null +++ b/client-go/informers/externalversions/device/interface.go @@ -0,0 +1,44 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by informer-gen. DO NOT EDIT. + +package device + +import ( + v1alpha1 "github.com/nvidia/nvsentinel/client-go/informers/externalversions/device/v1alpha1" + internalinterfaces "github.com/nvidia/nvsentinel/client-go/informers/externalversions/internalinterfaces" +) + +// Interface provides access to each of this group's versions. +type Interface interface { + // V1alpha1 provides access to shared informers for resources in V1alpha1. + V1alpha1() v1alpha1.Interface +} + +type group struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// V1alpha1 returns a new v1alpha1.Interface. +func (g *group) V1alpha1() v1alpha1.Interface { + return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/client-go/informers/externalversions/device/v1alpha1/gpu.go b/client-go/informers/externalversions/device/v1alpha1/gpu.go new file mode 100644 index 000000000..901280510 --- /dev/null +++ b/client-go/informers/externalversions/device/v1alpha1/gpu.go @@ -0,0 +1,99 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + time "time" + + apidevicev1alpha1 "github.com/nvidia/nvsentinel/api/device/v1alpha1" + versioned "github.com/nvidia/nvsentinel/client-go/client/versioned" + internalinterfaces "github.com/nvidia/nvsentinel/client-go/informers/externalversions/internalinterfaces" + devicev1alpha1 "github.com/nvidia/nvsentinel/client-go/listers/device/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// GPUInformer provides access to a shared informer and lister for +// GPUs. +type GPUInformer interface { + Informer() cache.SharedIndexInformer + Lister() devicev1alpha1.GPULister +} + +type gPUInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// NewGPUInformer constructs a new informer for GPU type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewGPUInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredGPUInformer(client, resyncPeriod, indexers, nil) +} + +// NewFilteredGPUInformer constructs a new informer for GPU type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredGPUInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.DeviceV1alpha1().GPUs().List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.DeviceV1alpha1().GPUs().Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.DeviceV1alpha1().GPUs().List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.DeviceV1alpha1().GPUs().Watch(ctx, options) + }, + }, + &apidevicev1alpha1.GPU{}, + resyncPeriod, + indexers, + ) +} + +func (f *gPUInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredGPUInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *gPUInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apidevicev1alpha1.GPU{}, f.defaultInformer) +} + +func (f *gPUInformer) Lister() devicev1alpha1.GPULister { + return devicev1alpha1.NewGPULister(f.Informer().GetIndexer()) +} diff --git a/client-go/informers/externalversions/device/v1alpha1/interface.go b/client-go/informers/externalversions/device/v1alpha1/interface.go new file mode 100644 index 000000000..1681622f9 --- /dev/null +++ b/client-go/informers/externalversions/device/v1alpha1/interface.go @@ -0,0 +1,43 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + internalinterfaces "github.com/nvidia/nvsentinel/client-go/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // GPUs returns a GPUInformer. + GPUs() GPUInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// GPUs returns a GPUInformer. +func (v *version) GPUs() GPUInformer { + return &gPUInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} +} diff --git a/client-go/informers/externalversions/factory.go b/client-go/informers/externalversions/factory.go new file mode 100644 index 000000000..b2c78b7ba --- /dev/null +++ b/client-go/informers/externalversions/factory.go @@ -0,0 +1,260 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by informer-gen. DO NOT EDIT. + +package externalversions + +import ( + reflect "reflect" + sync "sync" + time "time" + + versioned "github.com/nvidia/nvsentinel/client-go/client/versioned" + device "github.com/nvidia/nvsentinel/client-go/informers/externalversions/device" + internalinterfaces "github.com/nvidia/nvsentinel/client-go/informers/externalversions/internalinterfaces" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" +) + +// SharedInformerOption defines the functional option type for SharedInformerFactory. +type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory + +type sharedInformerFactory struct { + client versioned.Interface + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc + lock sync.Mutex + defaultResync time.Duration + customResync map[reflect.Type]time.Duration + transform cache.TransformFunc + + informers map[reflect.Type]cache.SharedIndexInformer + // startedInformers is used for tracking which informers have been started. + // This allows Start() to be called multiple times safely. + startedInformers map[reflect.Type]bool + // wg tracks how many goroutines were started. + wg sync.WaitGroup + // shuttingDown is true when Shutdown has been called. It may still be running + // because it needs to wait for goroutines. + shuttingDown bool +} + +// WithCustomResyncConfig sets a custom resync period for the specified informer types. +func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + for k, v := range resyncConfig { + factory.customResync[reflect.TypeOf(k)] = v + } + return factory + } +} + +// WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory. +func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.tweakListOptions = tweakListOptions + return factory + } +} + +// WithNamespace limits the SharedInformerFactory to the specified namespace. +func WithNamespace(namespace string) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.namespace = namespace + return factory + } +} + +// WithTransform sets a transform on all informers. +func WithTransform(transform cache.TransformFunc) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.transform = transform + return factory + } +} + +// NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces. +func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory { + return NewSharedInformerFactoryWithOptions(client, defaultResync) +} + +// NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory. +// Listers obtained via this SharedInformerFactory will be subject to the same filters +// as specified here. +// Deprecated: Please use NewSharedInformerFactoryWithOptions instead +func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory { + return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions)) +} + +// NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options. +func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory { + factory := &sharedInformerFactory{ + client: client, + namespace: v1.NamespaceAll, + defaultResync: defaultResync, + informers: make(map[reflect.Type]cache.SharedIndexInformer), + startedInformers: make(map[reflect.Type]bool), + customResync: make(map[reflect.Type]time.Duration), + } + + // Apply all options + for _, opt := range options { + factory = opt(factory) + } + + return factory +} + +func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { + f.lock.Lock() + defer f.lock.Unlock() + + if f.shuttingDown { + return + } + + for informerType, informer := range f.informers { + if !f.startedInformers[informerType] { + f.wg.Add(1) + // We need a new variable in each loop iteration, + // otherwise the goroutine would use the loop variable + // and that keeps changing. + informer := informer + go func() { + defer f.wg.Done() + informer.Run(stopCh) + }() + f.startedInformers[informerType] = true + } + } +} + +func (f *sharedInformerFactory) Shutdown() { + f.lock.Lock() + f.shuttingDown = true + f.lock.Unlock() + + // Will return immediately if there is nothing to wait for. + f.wg.Wait() +} + +func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { + informers := func() map[reflect.Type]cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informers := map[reflect.Type]cache.SharedIndexInformer{} + for informerType, informer := range f.informers { + if f.startedInformers[informerType] { + informers[informerType] = informer + } + } + return informers + }() + + res := map[reflect.Type]bool{} + for informType, informer := range informers { + res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) + } + return res +} + +// InformerFor returns the SharedIndexInformer for obj using an internal +// client. +func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informerType := reflect.TypeOf(obj) + informer, exists := f.informers[informerType] + if exists { + return informer + } + + resyncPeriod, exists := f.customResync[informerType] + if !exists { + resyncPeriod = f.defaultResync + } + + informer = newFunc(f.client, resyncPeriod) + informer.SetTransform(f.transform) + f.informers[informerType] = informer + + return informer +} + +// SharedInformerFactory provides shared informers for resources in all known +// API group versions. +// +// It is typically used like this: +// +// ctx, cancel := context.Background() +// defer cancel() +// factory := NewSharedInformerFactory(client, resyncPeriod) +// defer factory.WaitForStop() // Returns immediately if nothing was started. +// genericInformer := factory.ForResource(resource) +// typedInformer := factory.SomeAPIGroup().V1().SomeType() +// factory.Start(ctx.Done()) // Start processing these informers. +// synced := factory.WaitForCacheSync(ctx.Done()) +// for v, ok := range synced { +// if !ok { +// fmt.Fprintf(os.Stderr, "caches failed to sync: %v", v) +// return +// } +// } +// +// // Creating informers can also be created after Start, but then +// // Start must be called again: +// anotherGenericInformer := factory.ForResource(resource) +// factory.Start(ctx.Done()) +type SharedInformerFactory interface { + internalinterfaces.SharedInformerFactory + + // Start initializes all requested informers. They are handled in goroutines + // which run until the stop channel gets closed. + // Warning: Start does not block. When run in a go-routine, it will race with a later WaitForCacheSync. + Start(stopCh <-chan struct{}) + + // Shutdown marks a factory as shutting down. At that point no new + // informers can be started anymore and Start will return without + // doing anything. + // + // In addition, Shutdown blocks until all goroutines have terminated. For that + // to happen, the close channel(s) that they were started with must be closed, + // either before Shutdown gets called or while it is waiting. + // + // Shutdown may be called multiple times, even concurrently. All such calls will + // block until all goroutines have terminated. + Shutdown() + + // WaitForCacheSync blocks until all started informers' caches were synced + // or the stop channel gets closed. + WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool + + // ForResource gives generic access to a shared informer of the matching type. + ForResource(resource schema.GroupVersionResource) (GenericInformer, error) + + // InformerFor returns the SharedIndexInformer for obj using an internal + // client. + InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer + + Device() device.Interface +} + +func (f *sharedInformerFactory) Device() device.Interface { + return device.New(f, f.namespace, f.tweakListOptions) +} diff --git a/client-go/informers/externalversions/generic.go b/client-go/informers/externalversions/generic.go new file mode 100644 index 000000000..f8ccccacc --- /dev/null +++ b/client-go/informers/externalversions/generic.go @@ -0,0 +1,60 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by informer-gen. DO NOT EDIT. + +package externalversions + +import ( + fmt "fmt" + + v1alpha1 "github.com/nvidia/nvsentinel/api/device/v1alpha1" + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" +) + +// GenericInformer is type of SharedIndexInformer which will locate and delegate to other +// sharedInformers based on type +type GenericInformer interface { + Informer() cache.SharedIndexInformer + Lister() cache.GenericLister +} + +type genericInformer struct { + informer cache.SharedIndexInformer + resource schema.GroupResource +} + +// Informer returns the SharedIndexInformer. +func (f *genericInformer) Informer() cache.SharedIndexInformer { + return f.informer +} + +// Lister returns the GenericLister. +func (f *genericInformer) Lister() cache.GenericLister { + return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) +} + +// ForResource gives generic access to a shared informer of the matching type +// TODO extend this to unknown resources with a client pool +func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { + switch resource { + // Group=device.nvidia.com, Version=v1alpha1 + case v1alpha1.SchemeGroupVersion.WithResource("gpus"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Device().V1alpha1().GPUs().Informer()}, nil + + } + + return nil, fmt.Errorf("no informer found for %v", resource) +} diff --git a/client-go/informers/externalversions/internalinterfaces/factory_interfaces.go b/client-go/informers/externalversions/internalinterfaces/factory_interfaces.go new file mode 100644 index 000000000..ba0352657 --- /dev/null +++ b/client-go/informers/externalversions/internalinterfaces/factory_interfaces.go @@ -0,0 +1,38 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by informer-gen. DO NOT EDIT. + +package internalinterfaces + +import ( + time "time" + + versioned "github.com/nvidia/nvsentinel/client-go/client/versioned" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + cache "k8s.io/client-go/tools/cache" +) + +// NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. +type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer + +// SharedInformerFactory a small interface to allow for adding an informer without an import cycle +type SharedInformerFactory interface { + Start(stopCh <-chan struct{}) + InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer +} + +// TweakListOptionsFunc is a function that transforms a v1.ListOptions. +type TweakListOptionsFunc func(*v1.ListOptions) diff --git a/client-go/listers/device/v1alpha1/expansion_generated.go b/client-go/listers/device/v1alpha1/expansion_generated.go new file mode 100644 index 000000000..1aa65cee4 --- /dev/null +++ b/client-go/listers/device/v1alpha1/expansion_generated.go @@ -0,0 +1,21 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +// GPUListerExpansion allows custom methods to be added to +// GPULister. +type GPUListerExpansion interface{} diff --git a/client-go/listers/device/v1alpha1/gpu.go b/client-go/listers/device/v1alpha1/gpu.go new file mode 100644 index 000000000..709bd429f --- /dev/null +++ b/client-go/listers/device/v1alpha1/gpu.go @@ -0,0 +1,46 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + devicev1alpha1 "github.com/nvidia/nvsentinel/api/device/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// GPULister helps list GPUs. +// All objects returned here must be treated as read-only. +type GPULister interface { + // List lists all GPUs in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*devicev1alpha1.GPU, err error) + // Get retrieves the GPU from the index for a given name. + // Objects returned here must be treated as read-only. + Get(name string) (*devicev1alpha1.GPU, error) + GPUListerExpansion +} + +// gPULister implements the GPULister interface. +type gPULister struct { + listers.ResourceIndexer[*devicev1alpha1.GPU] +} + +// NewGPULister returns a new GPULister. +func NewGPULister(indexer cache.Indexer) GPULister { + return &gPULister{listers.New[*devicev1alpha1.GPU](indexer, devicev1alpha1.Resource("gpu"))} +} diff --git a/client-go/nvgrpc/client_conn.go b/client-go/nvgrpc/client_conn.go new file mode 100644 index 000000000..8522b0d62 --- /dev/null +++ b/client-go/nvgrpc/client_conn.go @@ -0,0 +1,87 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nvgrpc + +import ( + "fmt" + + grpcprom "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus" + "github.com/prometheus/client_golang/prometheus" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/keepalive" +) + +// clientMetrics represents a collection of metrics to be registered on a +// Prometheus metrics registry for a gRPC client. +var clientMetrics = grpcprom.NewClientMetrics( + grpcprom.WithClientHandlingTimeHistogram(), +) + +// init registers the client metrics with the default Prometheus registry. +func init() { + prometheus.MustRegister(clientMetrics) +} + +// ClientConnFor creates a new gRPC connection using the provided configuration and options. +func ClientConnFor(config *Config, opts ...DialOption) (*grpc.ClientConn, error) { + cfg := *config // Shallow copy to avoid mutation + + dOpts := &dialOptions{} + for _, opt := range opts { + opt(dOpts) + } + cfg.logger = dOpts.logger + + cfg.Default() + if err := cfg.Validate(); err != nil { + return nil, err + } + + logger := cfg.GetLogger() + + grpcOpts := []grpc.DialOption{ + grpc.WithUserAgent(cfg.UserAgent), + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithKeepaliveParams(keepalive.ClientParameters{ + Time: DefaultKeepAliveTime, + Timeout: DefaultKeepAliveTimeout, + PermitWithoutStream: true, // Allow keepalive pings even with no active RPCs. + }), + } + + // Build the unary interceptor chain. + unaryInterceptors := []grpc.UnaryClientInterceptor{ + clientMetrics.UnaryClientInterceptor(), + NewErrorLoggingUnaryInterceptor(logger), + } + unaryInterceptors = append(unaryInterceptors, dOpts.unaryInterceptors...) + grpcOpts = append(grpcOpts, grpc.WithChainUnaryInterceptor(unaryInterceptors...)) + + // Build the stream interceptor chain. + streamInterceptors := []grpc.StreamClientInterceptor{ + clientMetrics.StreamClientInterceptor(), + NewErrorLoggingStreamInterceptor(logger), + } + streamInterceptors = append(streamInterceptors, dOpts.streamInterceptors...) + grpcOpts = append(grpcOpts, grpc.WithChainStreamInterceptor(streamInterceptors...)) + + conn, err := grpc.NewClient(cfg.Target, grpcOpts...) + if err != nil { + return nil, fmt.Errorf("failed to create gRPC client for %s: %w", cfg.Target, err) + } + + return conn, nil +} diff --git a/client-go/nvgrpc/client_conn_test.go b/client-go/nvgrpc/client_conn_test.go new file mode 100644 index 000000000..06b6a1c12 --- /dev/null +++ b/client-go/nvgrpc/client_conn_test.go @@ -0,0 +1,57 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nvgrpc + +import ( + "testing" + + "github.com/go-logr/logr" +) + +func TestClientConnFor(t *testing.T) { + t.Run("Config defaulting does not mutate original", func(t *testing.T) { + cfg := &Config{} + conn, err := ClientConnFor(cfg) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer conn.Close() + + if cfg.Target != "" { + t.Errorf("Original config was mutated! Target is %q", cfg.Target) + } + }) + + t.Run("Config.Default() works correctly", func(t *testing.T) { + cfg := &Config{} + cfg.Default() + + if cfg.Target == "" { + t.Error("Target was not defaulted") + } + if cfg.UserAgent == "" { + t.Error("UserAgent was not defaulted") + } + }) + + t.Run("Client creation respects WithLogger option", func(t *testing.T) { + cfg := &Config{Target: "unix:///tmp/test.sock"} + conn, err := ClientConnFor(cfg, WithLogger(logr.Discard())) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + conn.Close() + }) +} diff --git a/client-go/nvgrpc/config.go b/client-go/nvgrpc/config.go new file mode 100644 index 000000000..c3b94cef2 --- /dev/null +++ b/client-go/nvgrpc/config.go @@ -0,0 +1,89 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nvgrpc + +import ( + "fmt" + "os" + "time" + + "github.com/go-logr/logr" + "github.com/nvidia/nvsentinel/client-go/version" +) + +const ( + // NvidiaDeviceAPITargetEnvVar is the environment variable that overrides the gRPC target. + NvidiaDeviceAPITargetEnvVar = "NVIDIA_DEVICE_API_TARGET" + + // DefaultNvidiaDeviceAPISocket is the default Unix domain socket path. + DefaultNvidiaDeviceAPISocket = "unix:///var/run/nvidia-device-api/device-api.sock" + + // DefaultKeepAliveTime is the default frequency of keepalive pings. + DefaultKeepAliveTime = 5 * time.Minute + + // DefaultKeepAliveTimeout is the default time to wait for a keepalive pong. + DefaultKeepAliveTimeout = 20 * time.Second +) + +// Config holds configuration for the Device API client. +type Config struct { + // Target is the address of the gRPC server (e.g. "unix:///path/to/socket"). + Target string + + // UserAgent is the string to use for the gRPC User-Agent header. + UserAgent string + + logger logr.Logger +} + +// Default populates unset fields in the Config with default values. +func (c *Config) Default() { + if c.Target == "" { + c.Target = os.Getenv(NvidiaDeviceAPITargetEnvVar) + } + if c.Target == "" { + c.Target = DefaultNvidiaDeviceAPISocket + } + + if c.UserAgent == "" { + c.UserAgent = version.UserAgent() + } + + if c.logger.GetSink() == nil { + c.logger = logr.Discard() + } +} + +// Validate checks if the Config is valid and returns an error if not. +func (c *Config) Validate() error { + if c.Target == "" { + return fmt.Errorf("gRPC target address is required; verify %s is not empty", NvidiaDeviceAPITargetEnvVar) + } + + if c.UserAgent == "" { + return fmt.Errorf("user-agent cannot be empty") + } + + return nil +} + +// GetLogger returns the configured logger. If no logger is set, it returns a discard logger. +func (c *Config) GetLogger() logr.Logger { + if c.logger.GetSink() == nil { + return logr.Discard() + } + + return c.logger +} diff --git a/client-go/nvgrpc/config_test.go b/client-go/nvgrpc/config_test.go new file mode 100644 index 000000000..437afafb0 --- /dev/null +++ b/client-go/nvgrpc/config_test.go @@ -0,0 +1,120 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package nvgrpc + +import ( + "testing" +) + +func TestConfig_Default_TargetPrecedence(t *testing.T) { + tests := []struct { + name string + argTarget string + envTarget string + wantTarget string + }{ + { + name: "Explicit target is preserved", + argTarget: "unix:///arg.sock", + envTarget: "unix:///env.sock", + wantTarget: "unix:///arg.sock", + }, + { + name: "Env var used when target is empty", + argTarget: "", + envTarget: "unix:///env.sock", + wantTarget: "unix:///env.sock", + }, + { + name: "Default used when both are empty", + argTarget: "", + envTarget: "", + wantTarget: DefaultNvidiaDeviceAPISocket, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv(NvidiaDeviceAPITargetEnvVar, tt.envTarget) + + cfg := &Config{Target: tt.argTarget} + cfg.Default() + + if cfg.Target != tt.wantTarget { + t.Errorf("Target = %q, want %q", cfg.Target, tt.wantTarget) + } + }) + } +} + +func TestConfig_Default_UserAgent(t *testing.T) { + t.Run("Populates default UserAgent if empty", func(t *testing.T) { + cfg := &Config{} + cfg.Default() + + if cfg.UserAgent == "" { + t.Error("UserAgent should have been populated with version-based default") + } + }) + + t.Run("Preserves custom UserAgent", func(t *testing.T) { + custom := "my-custom-agent/1.0" + cfg := &Config{UserAgent: custom} + cfg.Default() + + if cfg.UserAgent != custom { + t.Errorf("UserAgent = %q, want %q", cfg.UserAgent, custom) + } + }) +} + +func TestConfig_Validate(t *testing.T) { + tests := []struct { + name string + cfg Config + wantErr bool + }{ + { + name: "Valid config", + cfg: Config{ + Target: "unix:///var/run/test.sock", + UserAgent: "test/1.0", + }, + wantErr: false, + }, + { + name: "Missing target", + cfg: Config{ + UserAgent: "test/1.0", + }, + wantErr: true, + }, + { + name: "Missing user agent", + cfg: Config{ + Target: "unix:///var/run/test.sock", + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.cfg.Validate() + if (err != nil) != tt.wantErr { + t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/client-go/nvgrpc/doc.go b/client-go/nvgrpc/doc.go new file mode 100644 index 000000000..4e5512b8e --- /dev/null +++ b/client-go/nvgrpc/doc.go @@ -0,0 +1,16 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package nvgrpc provides helper utilities and common configurations for gRPC services. +package nvgrpc diff --git a/client-go/nvgrpc/interceptors.go b/client-go/nvgrpc/interceptors.go new file mode 100644 index 000000000..7200c26aa --- /dev/null +++ b/client-go/nvgrpc/interceptors.go @@ -0,0 +1,85 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nvgrpc + +import ( + "context" + + "github.com/go-logr/logr" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// NewErrorLoggingUnaryInterceptor returns an interceptor that logs the status of unary RPCs. +func NewErrorLoggingUnaryInterceptor(logger logr.Logger) grpc.UnaryClientInterceptor { + return func(ctx context.Context, method string, req, reply interface{}, + cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + + err := invoker(ctx, method, req, reply, cc, opts...) + + if err != nil || logger.V(6).Enabled() { + s := status.Convert(err) + code := s.Code() + + kv := []interface{}{ + "grpc.method", method, + "code", int(code), + } + + if err != nil { + if code == codes.Canceled || code == codes.DeadlineExceeded { + logger.V(4).Info("RPC finished with context error", kv...) + } else { + logger.Error(err, "RPC failed", kv...) + } + } else { + logger.V(6).Info("RPC succeeded", kv...) + } + } + + return err + } +} + +// NewErrorLoggingStreamInterceptor returns an interceptor that logs the status of stream establishment. +func NewErrorLoggingStreamInterceptor(logger logr.Logger) grpc.StreamClientInterceptor { + return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, + method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { + + stream, err := streamer(ctx, desc, cc, method, opts...) + + if err != nil || logger.V(4).Enabled() { + s := status.Convert(err) + code := s.Code() + + kv := []interface{}{ + "grpc.method", method, + "code", int(code), + } + + if err != nil { + if code == codes.Canceled || code == codes.DeadlineExceeded { + logger.V(4).Info("Stream establishment canceled", kv...) + } else { + logger.Error(err, "Stream establishment failed", kv...) + } + } else { + logger.V(4).Info("Stream started", kv...) + } + } + return stream, err + } +} diff --git a/client-go/nvgrpc/interceptors_test.go b/client-go/nvgrpc/interceptors_test.go new file mode 100644 index 000000000..b0b6f3254 --- /dev/null +++ b/client-go/nvgrpc/interceptors_test.go @@ -0,0 +1,97 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nvgrpc + +import ( + "context" + "errors" + "testing" + + "github.com/go-logr/logr" + logrtesting "github.com/go-logr/logr/testing" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestErrorLoggingUnaryInterceptor(t *testing.T) { + tests := []struct { + name string + method string + log bool + invokerErr error + expectedCode codes.Code + }{ + {"Returns OK code for successful call", "/svc/success", true, nil, codes.OK}, + {"Returns internal status error on internal error", "/svc/internal_error", true, status.Error(codes.Internal, "fail"), codes.Internal}, + {"Returns canceled status error when canceled", "/svc/cancel", true, status.Error(codes.Canceled, ""), codes.Canceled}, + {"Returns deadline exceeded status error on timeout", "/svc/timeout", true, status.Error(codes.DeadlineExceeded, ""), codes.DeadlineExceeded}, + {"Does not log if logger disabled", "/svc/skip_log", false, nil, codes.OK}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logger := logr.Discard() + if tt.log { + logger = logrtesting.NewTestLogger(t) + } + + interceptor := NewErrorLoggingUnaryInterceptor(logger) + + invoker := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error { + return tt.invokerErr + } + + err := interceptor(context.Background(), tt.method, nil, nil, nil, invoker) + if !errors.Is(err, tt.invokerErr) { + t.Fatalf("Returned error mismatch. Got %v, want %v", err, tt.invokerErr) + } + }) + } +} + +func TestErrorLoggingStreamInterceptor(t *testing.T) { + tests := []struct { + name string + method string + log bool + streamerErr error + }{ + {"Returns nil on successful start of stream", "/svc/start_stream", true, nil}, + {"Returns internal status error for failed stream", "/svc/stream_fail", true, status.Error(codes.Internal, "fail")}, + {"Does not log if logger disabled", "/svc/skip_log", false, nil}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logger := logr.Discard() + if tt.log { + logger = logrtesting.NewTestLogger(t) + } + + interceptor := NewErrorLoggingStreamInterceptor(logger) + desc := &grpc.StreamDesc{} + + streamer := func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) { + return nil, tt.streamerErr + } + + _, err := interceptor(context.Background(), desc, nil, tt.method, streamer) + if !errors.Is(err, tt.streamerErr) { + t.Fatalf("Returned error mismatch. Got %v, want %v", err, tt.streamerErr) + } + }) + } +} diff --git a/client-go/nvgrpc/options.go b/client-go/nvgrpc/options.go new file mode 100644 index 000000000..d25991a50 --- /dev/null +++ b/client-go/nvgrpc/options.go @@ -0,0 +1,50 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nvgrpc + +import ( + "github.com/go-logr/logr" + "google.golang.org/grpc" +) + +// DialOption configures the gRPC client connection. +type DialOption func(*dialOptions) + +type dialOptions struct { + logger logr.Logger + unaryInterceptors []grpc.UnaryClientInterceptor + streamInterceptors []grpc.StreamClientInterceptor +} + +// WithLogger sets the logger to be used by the client. +func WithLogger(logger logr.Logger) DialOption { + return func(opts *dialOptions) { + opts.logger = logger + } +} + +// WithUnaryInterceptor adds a unary client interceptor to the chain. +func WithUnaryInterceptor(interceptor grpc.UnaryClientInterceptor) DialOption { + return func(opts *dialOptions) { + opts.unaryInterceptors = append(opts.unaryInterceptors, interceptor) + } +} + +// WithStreamInterceptor adds a stream client interceptor to the chain. +func WithStreamInterceptor(interceptor grpc.StreamClientInterceptor) DialOption { + return func(opts *dialOptions) { + opts.streamInterceptors = append(opts.streamInterceptors, interceptor) + } +} diff --git a/client-go/nvgrpc/options_test.go b/client-go/nvgrpc/options_test.go new file mode 100644 index 000000000..43559dc00 --- /dev/null +++ b/client-go/nvgrpc/options_test.go @@ -0,0 +1,65 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nvgrpc + +import ( + "context" + "testing" + + "github.com/go-logr/logr" + "google.golang.org/grpc" +) + +func TestDialOptions(t *testing.T) { + testLogger := logr.Discard() + + dummyUnary := func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + return invoker(ctx, method, req, reply, cc, opts...) + } + dummyStream := func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { + return streamer(ctx, desc, cc, method, opts...) + } + + opts := []DialOption{ + WithLogger(testLogger), + WithUnaryInterceptor(dummyUnary), + WithStreamInterceptor(dummyStream), + WithUnaryInterceptor(dummyUnary), // Test multiple appends + WithStreamInterceptor(dummyStream), + } + + dOpts := &dialOptions{} + for _, opt := range opts { + opt(dOpts) + } + + t.Run("Logger is correctly assigned", func(t *testing.T) { + if dOpts.logger != testLogger { + t.Errorf("expected logger to be set, got %v", dOpts.logger) + } + }) + + t.Run("Unary interceptors are correctly appended", func(t *testing.T) { + if len(dOpts.unaryInterceptors) != 2 { + t.Errorf("expected 2 unary interceptors, got %d", len(dOpts.unaryInterceptors)) + } + }) + + t.Run("Stream interceptors are correctly appended", func(t *testing.T) { + if len(dOpts.streamInterceptors) != 2 { + t.Errorf("expected 2 stream interceptors, got %d", len(dOpts.streamInterceptors)) + } + }) +} diff --git a/client-go/nvgrpc/watcher.go b/client-go/nvgrpc/watcher.go new file mode 100644 index 000000000..83046b183 --- /dev/null +++ b/client-go/nvgrpc/watcher.go @@ -0,0 +1,170 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nvgrpc + +import ( + "context" + "errors" + "io" + "sync" + + "github.com/go-logr/logr" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" +) + +// Source defines the interface for reading events from a source. +type Source interface { + Next() (eventType string, obj runtime.Object, err error) + Close() error +} + +// Watcher implements watch.Interface. +type Watcher struct { + cancel context.CancelFunc // cancels the event source + result chan watch.Event // channel delivering watch events + source Source // the underlying event source + done chan struct{} // closed when watcher stops + stopOnce sync.Once // ensures stop is idempotent + logger logr.Logger +} + +// NewWatcher creates a Watcher and starts receiving events. +func NewWatcher( + source Source, + cancel context.CancelFunc, + logger logr.Logger, +) watch.Interface { + w := &Watcher{ + cancel: cancel, + result: make(chan watch.Event, 100), + source: source, + done: make(chan struct{}), + logger: logger.WithName("watcher"), + } + + go w.receive() + return w +} + +// Stop cancels the context and closes the event source. +func (w *Watcher) Stop() { + w.stopOnce.Do(func() { + w.logger.V(4).Info("Stopping watcher") + w.cancel() + if err := w.source.Close(); err != nil { + w.logger.V(4).Info("Error closing source during stop", "err", err) + } + close(w.done) + }) +} + +// ResultChan returns the channel delivering watch events. +func (w *Watcher) ResultChan() <-chan watch.Event { + return w.result +} + +// receive reads events from the source and sends them to result channel. +func (w *Watcher) receive() { + defer func() { + w.logger.V(4).Info("Watcher receive loop exiting") + close(w.result) + }() + defer w.Stop() + + for { + w.logger.V(6).Info("Waiting for next event from source") + typeStr, obj, err := w.source.Next() + + if err != nil { + if errors.Is(err, io.EOF) || status.Code(err) == codes.Canceled { + w.logger.V(3).Info("Watch stream closed normally") + return + } + w.logger.Error(err, "Watch stream encountered unexpected error") + w.sendError(err) + return + } + + var eventType watch.EventType + switch typeStr { + case "ADDED": + eventType = watch.Added + case "MODIFIED": + eventType = watch.Modified + case "DELETED": + eventType = watch.Deleted + case "ERROR": + w.logger.V(4).Info("Received explicit ERROR event from server") + w.result <- watch.Event{Type: watch.Error, Object: obj} + return + default: + w.logger.V(2).Info("Skipping unknown event type from server", "rawType", typeStr) + continue + } + + select { + case <-w.done: + w.logger.V(3).Info("Watcher stopping; aborting receive loop") + return + case w.result <- watch.Event{Type: eventType, Object: obj}: + if meta, ok := obj.(metav1.Object); ok { + w.logger.V(6).Info("Event dispatched to Informer", + "type", eventType, + "name", meta.GetName(), + "resourceVersion", meta.GetResourceVersion(), + ) + } + } + } +} + +func (w *Watcher) sendError(err error) { + st := status.Convert(err) + statusErr := &metav1.Status{ + Status: metav1.StatusFailure, + Message: st.Message(), + Code: int32(st.Code()), + } + + switch st.Code() { + case codes.OutOfRange, codes.ResourceExhausted, codes.InvalidArgument: + // CRITICAL for Informers: This tells the Reflector to perform a new List operation. + statusErr.Reason = metav1.StatusReasonExpired + statusErr.Code = 410 + case codes.PermissionDenied: + statusErr.Reason = metav1.StatusReasonForbidden + statusErr.Code = 403 + case codes.NotFound: + statusErr.Reason = metav1.StatusReasonNotFound + statusErr.Code = 404 + } + + w.logger.V(4).Info("Sending error event to informer", + "code", statusErr.Code, + "reason", statusErr.Reason, + "message", statusErr.Message, + ) + + select { + case <-w.done: + w.logger.V(4).Info("Watcher already done, dropping error event") + case w.result <- watch.Event{Type: watch.Error, Object: statusErr}: + } +} diff --git a/client-go/nvgrpc/watcher_test.go b/client-go/nvgrpc/watcher_test.go new file mode 100644 index 000000000..d220c3ae8 --- /dev/null +++ b/client-go/nvgrpc/watcher_test.go @@ -0,0 +1,237 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nvgrpc + +import ( + "context" + "io" + "testing" + "time" + + "github.com/go-logr/logr" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" +) + +func TestWatcher_NormalEvents(t *testing.T) { + _, cancel := context.WithCancel(context.Background()) + defer cancel() + + events := make(chan testEvent, 3) + events <- testEvent{"ADDED", &FakeObject{Name: "obj1"}} + events <- testEvent{"MODIFIED", &FakeObject{Name: "obj1"}} + events <- testEvent{"DELETED", &FakeObject{Name: "obj1"}} + close(events) + + source := &FakeSource{events: events, done: make(chan struct{})} + w := NewWatcher(source, cancel, logr.Discard()) + + var got []watch.Event + for e := range w.ResultChan() { + got = append(got, e) + } + + wantTypes := []watch.EventType{watch.Added, watch.Modified, watch.Deleted} + if len(got) != len(wantTypes) { + t.Fatalf("got %d events, want %d", len(got), len(wantTypes)) + } + for i, ev := range got { + if ev.Type != wantTypes[i] { + t.Errorf("event %d: got type %v, want %v", i, ev.Type, wantTypes[i]) + } + } +} + +func TestWatcher_UnknownEventType_Ignored(t *testing.T) { + _, cancel := context.WithCancel(context.Background()) + defer cancel() + + events := make(chan testEvent, 2) + events <- testEvent{"UNKNOWN", &FakeObject{Name: "obj1"}} + events <- testEvent{"ADDED", &FakeObject{Name: "obj2"}} + close(events) + + source := &FakeSource{events: events, done: make(chan struct{})} + w := NewWatcher(source, cancel, logr.Discard()) + + var got []watch.Event + for e := range w.ResultChan() { + got = append(got, e) + } + + if len(got) != 1 { + t.Fatalf("expected 1 event, got %d", len(got)) + } + if got[0].Type != watch.Added { + t.Errorf("expected ADDED, got %v", got[0].Type) + } +} + +func TestWatcher_Errors(t *testing.T) { + cases := []struct { + name string + err error + wantReason metav1.StatusReason + wantCode int32 + }{ + {"Internal", status.Error(codes.Internal, "err"), "", int32(codes.Internal)}, + {"OutOfRange", status.Error(codes.OutOfRange, "err"), metav1.StatusReasonExpired, 410}, + {"InvalidArgument", status.Error(codes.InvalidArgument, "err"), metav1.StatusReasonExpired, 410}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, cancel := context.WithCancel(context.Background()) + defer cancel() + + source := &FakeSource{errs: []error{tc.err}, done: make(chan struct{})} + w := NewWatcher(source, cancel, logr.Discard()) + + e := <-w.ResultChan() + if e.Type != watch.Error { + t.Fatalf("expected watch.Error, got %v", e.Type) + } + st, ok := e.Object.(*metav1.Status) + if !ok { + t.Fatal("expected metav1.Status object") + } + if st.Reason != tc.wantReason || st.Code != tc.wantCode { + t.Errorf("got %+v, wantReason %v, wantCode %d", st, tc.wantReason, tc.wantCode) + } + }) + } +} + +func TestWatcher_ErrorTerminatesStream(t *testing.T) { + _, cancel := context.WithCancel(context.Background()) + defer cancel() + + source := &FakeSource{ + errs: []error{ + status.Error(codes.Internal, "fatal error"), + status.Error(codes.Internal, "should never be reached"), + }, + done: make(chan struct{}), + } + w := NewWatcher(source, cancel, logr.Discard()) + + count := 0 + timeout := time.After(500 * time.Millisecond) + +Receive: + for { + select { + case _, ok := <-w.ResultChan(): + if !ok { + break Receive + } + count++ + case <-timeout: + t.Fatal("Test timed out waiting for ResultChan to close") + } + } + + if count != 1 { + t.Errorf("expected 1 error event, got %d", count) + } +} + +func TestWatcher_Stop(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + source := NewFakeSource() + + w := NewWatcher(source, cancel, logr.Discard()) + w.Stop() + + select { + case <-ctx.Done(): + case <-time.After(time.Second): + t.Error("context not cancelled after Stop()") + } + + select { + case _, ok := <-w.ResultChan(): + if ok { + t.Error("ResultChan not closed") + } + case <-time.After(time.Second): + t.Error("ResultChan hang") + } +} + +// FakeObject is a minimal implementation of runtime.Object. +type FakeObject struct { + metav1.TypeMeta + Name string +} + +func (f *FakeObject) DeepCopyObject() runtime.Object { + return &FakeObject{ + TypeMeta: f.TypeMeta, + Name: f.Name, + } +} + +type testEvent struct { + eventType string + obj runtime.Object +} + +// FakeSource implements nvgrpc.Source. +type FakeSource struct { + events chan testEvent + errs []error + done chan struct{} +} + +func NewFakeSource() *FakeSource { + return &FakeSource{ + events: make(chan testEvent, 10), + done: make(chan struct{}), + } +} + +func (f *FakeSource) Next() (string, runtime.Object, error) { + if len(f.errs) > 0 { + err := f.errs[0] + f.errs = f.errs[1:] + return "", nil, err + } + + select { + case <-f.done: + return "", nil, io.EOF + case e, ok := <-f.events: + if !ok { + return "", nil, io.EOF + } + return e.eventType, e.obj, nil + } +} + +func (f *FakeSource) Close() error { + if f.done == nil { + return nil + } + select { + case <-f.done: + default: + close(f.done) + } + return nil +} diff --git a/client-go/tests/integration/device/v1alpha1/clientset_test.go b/client-go/tests/integration/device/v1alpha1/clientset_test.go new file mode 100644 index 000000000..92f6e70e9 --- /dev/null +++ b/client-go/tests/integration/device/v1alpha1/clientset_test.go @@ -0,0 +1,353 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1alpha1_test + +import ( + "context" + "fmt" + "net" + "sync" + "testing" + "time" + + devicev1alpha1 "github.com/nvidia/nvsentinel/api/device/v1alpha1" + "github.com/nvidia/nvsentinel/client-go/client/versioned" + "github.com/nvidia/nvsentinel/client-go/client/versioned/scheme" + informers "github.com/nvidia/nvsentinel/client-go/informers/externalversions" + "github.com/nvidia/nvsentinel/client-go/nvgrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/test/bufconn" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/cache" + ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + + pb "github.com/nvidia/nvsentinel/api/gen/go/device/v1alpha1" +) + +func TestClientset_EndToEnd(t *testing.T) { + lis := bufconn.Listen(1024 * 1024) + s := grpc.NewServer() + + mock := newMockGpuServer() + pb.RegisterGpuServiceServer(s, mock) + + go s.Serve(lis) + defer s.Stop() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + dialer := func(context.Context, string) (net.Conn, error) { + return lis.Dial() + } + + conn, err := grpc.DialContext(ctx, "bufconn", + grpc.WithContextDialer(dialer), + grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("Failed to dial bufconn: %v", err) + } + defer conn.Close() + + config := &nvgrpc.Config{Target: "passthrough://bufconn"} + cs, err := versioned.NewForConfigAndClient(config, conn) + if err != nil { + t.Fatalf("Failed to create clientset: %v", err) + } + + t.Run("Get", func(t *testing.T) { + gpu, err := cs.DeviceV1alpha1().GPUs().Get(ctx, "gpu-1", metav1.GetOptions{}) + if err != nil { + t.Fatalf("Get failed: %v", err) + } + if gpu.Name != "gpu-1" { + t.Errorf("Expected gpu-1, got %s", gpu.Name) + } + }) + + t.Run("List", func(t *testing.T) { + list, err := cs.DeviceV1alpha1().GPUs().List(ctx, metav1.ListOptions{}) + if err != nil { + t.Fatalf("List failed: %v", err) + } + + if len(list.Items) != 1 { + t.Errorf("Expected 1 item in list, got %d", len(list.Items)) + } + if list.Items[0].ResourceVersion != "7" { + t.Errorf("Expected ResourceVersion 7, got %s", list.Items[0].ResourceVersion) + } + }) + + t.Run("Watch flow with initial snapshot", func(t *testing.T) { + w, err := cs.DeviceV1alpha1().GPUs().Watch(ctx, metav1.ListOptions{}) + if err != nil { + t.Fatalf("Watch failed: %v", err) + } + defer w.Stop() + + // Consume the initial snapshot + select { + case event := <-w.ResultChan(): + if event.Type != watch.Added { + t.Errorf("Expected initial ADDED event, got %v", event.Type) + } + case <-time.After(2 * time.Second): + t.Fatal("Timed out waiting for initial snapshot") + } + + // Trigger event + mock.sendEvent(&pb.WatchGpusResponse{ + Type: "MODIFIED", + Object: &pb.Gpu{ + Metadata: &pb.ObjectMeta{ + Name: "gpu-1", + ResourceVersion: "8", + }, + }, + }) + + select { + case event := <-w.ResultChan(): + if event.Type != watch.Modified { + t.Errorf("Expected event type MODIFIED, got %v", event.Type) + } + + gpu := event.Object.(*devicev1alpha1.GPU) + if gpu.ResourceVersion != "8" { + t.Errorf("Expected ResourceVersion 8, got %s", gpu.ResourceVersion) + } + case <-time.After(2 * time.Second): + t.Fatal("Timed out waiting for modified event") + } + }) + + t.Run("Informer and Lister Sync", func(t *testing.T) { + subCtx, subCancel := context.WithTimeout(ctx, 5*time.Second) + defer subCancel() + + factory := informers.NewSharedInformerFactory(cs, 0) + + // Informer must be instantiated BEFORE starting the factory to register it with the factory. + gpuInformer := factory.Device().V1alpha1().GPUs() + _ = gpuInformer.Informer() + + stopCh := make(chan struct{}) + defer close(stopCh) + + factory.Start(stopCh) + + if !cache.WaitForCacheSync(subCtx.Done(), gpuInformer.Informer().HasSynced) { + t.Fatal("Timed out waiting for cache sync") + } + + // Initial snapshot + lister := gpuInformer.Lister() + gpu, err := lister.Get("gpu-1") + if err != nil { + t.Fatalf("Lister failed to find gpu-1 in cache: %v", err) + } + if gpu.ResourceVersion != "7" { + t.Errorf("Expected cached RV 7, got %s", gpu.ResourceVersion) + } + + // Trigger event + mock.sendEvent(&pb.WatchGpusResponse{ + Type: "MODIFIED", + Object: &pb.Gpu{ + Metadata: &pb.ObjectMeta{ + Name: "gpu-1", + ResourceVersion: "8", + }, + }, + }) + + err = wait.PollUntilContextTimeout(subCtx, 100*time.Millisecond, 3*time.Second, true, func(ctx context.Context) (bool, error) { + updated, err := lister.Get("gpu-1") + if err != nil { + return false, nil + } + return updated.ResourceVersion == "8", nil + }) + + if err != nil { + t.Errorf("Informer failed to update cache with new ResourceVersion: %v", err) + } + }) + + t.Run("Controller-runtime Compatibility", func(t *testing.T) { + factory := informers.NewSharedInformerFactory(cs, 0) + gpuInformer := factory.Device().V1alpha1().GPUs() + + mapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{devicev1alpha1.SchemeGroupVersion}) + mapper.Add(devicev1alpha1.SchemeGroupVersion.WithKind("GPU"), meta.RESTScopeRoot) + + c, err := ctrlcache.New(&rest.Config{Host: "http://localhost:0"}, ctrlcache.Options{ + Scheme: scheme.Scheme, + Mapper: mapper, + NewInformer: func(lw cache.ListerWatcher, obj runtime.Object, resync time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + if _, ok := obj.(*devicev1alpha1.GPU); ok { + return gpuInformer.Informer() + } + return cache.NewSharedIndexInformer(lw, obj, resync, indexers) + }, + }) + if err != nil { + t.Fatalf("Failed to create controller-runtime cache: %v", err) + } + + stopCh := make(chan struct{}) + defer close(stopCh) + + factory.Start(stopCh) + go func() { + if err := c.Start(ctx); err != nil { + if ctx.Err() == nil { + // Errors during Start are expected when context is cancelled during cleanup. + t.Logf("Cache start error (may be expected): %v", err) + } + } + }() + + if !c.WaitForCacheSync(ctx) { + t.Fatal("Controller-runtime cache failed to sync") + } + + // Initial snapshot + var gpu devicev1alpha1.GPU + key := client.ObjectKey{Name: "gpu-1"} + if err := c.Get(ctx, key, &gpu); err != nil { + t.Fatalf("Failed to read initial state from cache: %v", err) + } + if gpu.ResourceVersion != "7" { + t.Errorf("Expected RV 7, got %s", gpu.ResourceVersion) + } + + // Trigger event + mock.sendEvent(&pb.WatchGpusResponse{ + Type: "MODIFIED", + Object: &pb.Gpu{ + Metadata: &pb.ObjectMeta{ + Name: "gpu-1", + ResourceVersion: "8", + }, + }, + }) + + err = wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, 2*time.Second, true, func(ctx context.Context) (bool, error) { + var updated devicev1alpha1.GPU + if err := c.Get(ctx, key, &updated); err != nil { + return false, nil + } + return updated.ResourceVersion == "8", nil + }) + + if err != nil { + t.Errorf("Controller-runtime cache failed to reflect gRPC event: %v", err) + } + }) +} + +// --- Mock Server Implementation --- + +type mockGpuServer struct { + pb.UnimplementedGpuServiceServer + mu sync.RWMutex + gpus map[string]*pb.Gpu + watch chan *pb.WatchGpusResponse +} + +func newMockGpuServer() *mockGpuServer { + return &mockGpuServer{ + gpus: map[string]*pb.Gpu{ + "gpu-1": { + Metadata: &pb.ObjectMeta{ + Name: "gpu-1", + ResourceVersion: "7", + }, + Spec: &pb.GpuSpec{Uuid: "GPU-1"}, + }, + }, + watch: make(chan *pb.WatchGpusResponse, 10), + } +} + +func (m *mockGpuServer) GetGpu(ctx context.Context, req *pb.GetGpuRequest) (*pb.GetGpuResponse, error) { + m.mu.RLock() + defer m.mu.RUnlock() + gpu, ok := m.gpus[req.Name] + if !ok { + return nil, fmt.Errorf("%s not found", req.Name) + } + return &pb.GetGpuResponse{Gpu: gpu}, nil +} + +func (m *mockGpuServer) ListGpus(ctx context.Context, req *pb.ListGpusRequest) (*pb.ListGpusResponse, error) { + m.mu.RLock() + defer m.mu.RUnlock() + list := &pb.GpuList{} + for _, g := range m.gpus { + list.Items = append(list.Items, g) + } + return &pb.ListGpusResponse{GpuList: list}, nil +} + +func (m *mockGpuServer) WatchGpus(req *pb.WatchGpusRequest, stream pb.GpuService_WatchGpusServer) error { + m.mu.RLock() + // Send the initial snapshot (Current state) + for _, g := range m.gpus { + select { + case <-stream.Context().Done(): + m.mu.RUnlock() + return nil + default: + if err := stream.Send(&pb.WatchGpusResponse{ + Type: "ADDED", + Object: g, + }); err != nil { + m.mu.RUnlock() + return err + } + } + } + m.mu.RUnlock() + + // Continuous watch (Live events) + for { + select { + case <-stream.Context().Done(): + return nil + case ev, ok := <-m.watch: + if !ok { + return nil + } + if err := stream.Send(ev); err != nil { + return err + } + } + } +} + +func (m *mockGpuServer) sendEvent(ev *pb.WatchGpusResponse) { + m.watch <- ev +} diff --git a/client-go/version/version.go b/client-go/version/version.go new file mode 100644 index 000000000..e8d3cb43c --- /dev/null +++ b/client-go/version/version.go @@ -0,0 +1,35 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package version + +import ( + "fmt" + "runtime" +) + +// GitVersion is the semantic version of the client. +// It is set at build time via -ldflags +// e.g., -ldflags "-X 'github.com/nvidia/nvsentinel/client-go/version.GitVersion=v1.2.3'" +var GitVersion = "devel" + +// UserAgent returns the standard user agent string. +func UserAgent() string { + return fmt.Sprintf( + "nvidia-device-api-client/%s (%s/%s)", + GitVersion, + runtime.GOOS, + runtime.GOARCH, + ) +} diff --git a/client-go/version/version_test.go b/client-go/version/version_test.go new file mode 100644 index 000000000..604242567 --- /dev/null +++ b/client-go/version/version_test.go @@ -0,0 +1,33 @@ +// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package version + +import ( + "fmt" + "runtime" + "testing" +) + +func TestUserAgent(t *testing.T) { + originalVersion := GitVersion + defer func() { GitVersion = originalVersion }() + + GitVersion = "v1.2.3-test" + + expected := fmt.Sprintf("nvidia-device-api-client/v1.2.3-test (%s/%s)", runtime.GOOS, runtime.GOARCH) + if got := UserAgent(); got != expected { + t.Errorf("UserAgent() = %q, want %q", got, expected) + } +} diff --git a/code-generator/CONFIGURATION.md b/code-generator/CONFIGURATION.md new file mode 100644 index 000000000..e35ce63a4 --- /dev/null +++ b/code-generator/CONFIGURATION.md @@ -0,0 +1,28 @@ +# Configuration +The code generation pipeline relies on specific versions of upstream tools (like Kubernetes generators, Protobuf compilers, and Goverter). + +The default versions for all tools are managed in the root `.versions.yaml` file. + +## Overriding Versions +You can override the versions defined in `.versions.yaml` by setting specific environment variables before sourcing the script. + +| Environment Variable | Description | Key in `.versions.yaml` | +|----------------------|-------------|-------------------------| +| `KUBE_CODEGEN_TAG` | Version for upstream K8s tools (`client-gen`, `informer-gen`, etc.) | `kubernetes_code_gen` | +| `PROTOC_GEN_GO_TAG` | Version for `protoc-gen-go` | `protoc_gen_go` | +| `PROTOC_GEN_GO_GRPC_TAG` | Version for `protoc-gen-go-grpc` | `protoc_gen_go_grpc` | +| `GOVERTER_TAG` | Version for `goverter` | `goverter` | + +To run the code generation with a different Kubernetes generator version than what is checked into the repo: + +```bash +# Force a specific version for this run only +export KUBE_CODEGEN_TAG="v0.32.0" +./hack/update-codegen.sh +``` + +## Version Precedence +Versions are resolved in this order: +1. **Override**: Environment variables (e.g., `KUBE_CODEGEN_TAG`). +2. **Default**: The `.versions.yaml` file in this repository. +3. **Implicit**: If neither is set, `go install` defaults to the version defined in the **code-generator's** `go.mod` file (or the latest version in your Go environment). diff --git a/code-generator/LICENSE b/code-generator/LICENSE new file mode 100644 index 000000000..f4f87bd4e --- /dev/null +++ b/code-generator/LICENSE @@ -0,0 +1,203 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + \ No newline at end of file diff --git a/code-generator/README.md b/code-generator/README.md new file mode 100644 index 000000000..04efce3ba --- /dev/null +++ b/code-generator/README.md @@ -0,0 +1,48 @@ +# code-generator +Custom Golang code-generators used to implement [Kubernetes-style API types](https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md) backed by gRPC transport. + +These code-generators are used in the context of the node-local NVIDIA Device API to build native, versioned clients. + +## Structure +* **Code Generation**: A bash library (`kube_codegen.sh`) for orchestrating code generation across the repository. +* **Custom Generator**: A modified version of `client-gen` (in `cmd/client-gen`) that injects gRPC transport logic into the generated clientset. + +## Usage +The `kube_codegen.sh` script is designed to be **sourced** by other build scripts, not executed directly. + +To use it, create a wrapper script in your project (conventionally named `hack/update-codegen.sh`) containing the following: + +```bash +#!/usr/bin/env bash + +# file: hack/update-codegen.sh + +# 1. Define Roots +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)" + +# 2. Point to the code-generator. +# If you are running 'kube_codegen.sh' from outside of github.com/nvidia/nvsentinel, +# override the default by setting 'export CODEGEN_ROOT=/path/to/code-generator' +CODEGEN_ROOT="${CODEGEN_ROOT:-${REPO_ROOT}/code-generator}" + +# 3. Source the library +source "${CODEGEN_ROOT}/kube_codegen.sh" + +# 4. Invoke the generator +kube::codegen::gen_client \ + --proto-base "github.com/my-org/my-project/api/gen/go" \ + --output-dir "${REPO_ROOT}/client" \ + --output-pkg "github.com/my-org/my-project/client" \ + --boilerplate "${REPO_ROOT}/hack/boilerplate.go.txt" \ + "${REPO_ROOT}/api" +``` + +## Available Functions +- `kube::codegen::gen_proto_bindings`: Scans for `.proto` files and generates Go bindings (`.pb.go`) and gRPC interfaces (`_grpc.pb.go`). +- `kube::codegen::gen_helpers`: Runs upstream Kubernetes generators (`deepcopy`, `defaulter`, `validation`) and [Goverter](https://github.com/jmattheis/goverter) to handle Proto-to-Go type mapping. +- `kube::codegen::gen_client`: Compiles the local custom gRPC `client-gen` binary and runs it to generate the standard Kubernetes client stack: **Clientset**, **Listers**, and **Informers**. + +## Configuration +Tool versions (e.g., `protoc-gen-go`, `kubernetes_code_gen`) are managed in the root `.versions.yaml` file of this repository, but can be overridden by setting corresponding environment variables (e.g., `KUBE_CODEGEN_TAG`) before sourcing the script. + +See [CONFIGURATION.md](CONFIGURATION.md) for more details. diff --git a/code-generator/cmd/client-gen/README.md b/code-generator/cmd/client-gen/README.md new file mode 100644 index 000000000..fc255edb1 --- /dev/null +++ b/code-generator/cmd/client-gen/README.md @@ -0,0 +1,61 @@ +# client-gen +This is a customized version of the `v0.34.1` Kubernetes [client-gen](https://github.com/kubernetes/code-generator/tree/master/cmd/client-gen). + +It generates a typed Go **Clientset** for accessing API resources. Unlike the standard generator which defaults to REST/HTTP, this version creates clients that natively support **gRPC transport** for the NVIDIA Device API. + +## Workflow +The generation process follows three main steps: + +### 1. Tagging API Types +In your API definition files (e.g., `api/device/v1alpha1/types.go`), mark the types (e.g., `GPU`) that you want to generate clients for using `// +genclient`. If the resource is *not* namespaced, append `// +genclient:nonNamespaced`. + +#### Supported Tags + +* `// +genclient` - Generate default client verb functions (`create`, `update`, `delete`, `get`, `list`, `patch`, `watch`, and `updateStatus`). +* `// +genclient:nonNamespaced` - Generate verb functions without namespace parameters. +* `// +genclient:onlyVerbs=,` - Generate **only** the listed verbs. +* `// +genclient:skipVerbs=,` - Generate all default verbs **except** the listed ones. +* `// +genclient:noStatus` - Skip `updateStatus` verb even if the `.Status` struct field exists. +* `// +groupName=policy.authorization.k8s.io` – Overrides the API group name (defaults to the package name). +* `// +groupGoName=AuthorizationPolicy` – Sets a custom Golang identifier to de-conflict groups with identical prefixes (defaults to the upper-case first segment of the group name). + +### 2. Running the Generator +> [!NOTE] +> This binary is typically invoked automatically via the `kube_codegen.sh` script (see root README). + +If running manually, use the following flags to link your API types to the Protobuf stubs: +```bash +client-gen \ + --output-dir "client" \ + --output-pkg "github.com/my-org/my-project/client" \ + --clientset-name "versioned" \ + --input-base "github.com/my-org/my-project/api" \ + --input "mygroup/v1alpha1" \ + --proto-base "github.com/my-org/my-project/api/gen/go" +``` +The generator resolves packages by combining `--input-base` and `--input` (e.g., `github.com/my-org/my-project/api/mygroup/v1alpha1`). + +### 3. Adding Expansion Methods +`client-gen` only generates standard CRUD methods. Add additional methods through the expansion interface by creating a file named `${TYPE}_expansion.go` in the generated typed directory, defining a `${TYPE}Expansion` interface, and implementing the methods. + +The generator automatically detects this file and embeds the custom expansion interface into the generated client. + +## Output Structure +- **Clientset**: Generated at `--output-dir` / `--clientset-name` (e.g., `client/versioned`). +- **Typed Clients**: Generated at `client/versioned/typed/${GROUP}/${VERSION}/`. + +## Flags + +| Flag | Required | Description | +|------|----------|-------------| +| **`--proto-base`** | **Yes** | The base Go import path for the generated Protobuf stubs (e.g., `github.com/org/repo/api/gen/go`). Essential for gRPC linking. | +| **`--input`** | **Yes** | Comma-separated list of groups/versions to generate (e.g., `device/v1alpha1,networking/v1`). | +| **`--input-base`** | **Yes** | Base import path for the API types (e.g., `github.com/org/repo/api`). | +| **`--output-pkg`** | **Yes** | Go package path for the generated files (e.g., `github.com/org/repo/client`). | +| **`--output-dir`** | **Yes** | Base directory for the output on disk (e.g., `./client`). | +| `--boilerplate` | No | Path to a header file (copyright/license) to prepend to generated files. Default: `hack/boilerplate.go.txt`. | +| `--clientset-name` | No | Name of the generated package/directory. Default: `clientset`. | +| `--versioned-name` | No | Name of the versioned clientset directory. Default: `versioned`. | +| `--plural-exceptions`| No | Comma-separated list of `Type:PluralizedType` overrides. | +| `--prefers-protobuf` | **N/A** | **Removed.** This generator assumes Protobuf/gRPC support is always enabled. | +| `--fake-clientset` | **N/A** | **Removed.** Generation of fake clientsets is currently disabled. | diff --git a/code-generator/cmd/client-gen/args/args.go b/code-generator/cmd/client-gen/args/args.go new file mode 100644 index 000000000..ba631b45a --- /dev/null +++ b/code-generator/cmd/client-gen/args/args.go @@ -0,0 +1,152 @@ +/* +Copyright 2015 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Portions Copyright (c) 2025 NVIDIA CORPORATION. All rights reserved. + +Modified from the original to support gRPC transport. +Origin: https://github.com/kubernetes/code-generator/blob/v0.34.1/cmd/client-gen/args/args.go +*/ + +package args + +import ( + "fmt" + + "github.com/spf13/pflag" + + "github.com/nvidia/nvsentinel/code-generator/cmd/client-gen/types" +) + +type Args struct { + // The directory for the generated results. + OutputDir string + + // The Go import-path of the generated results. + OutputPkg string + + // The boilerplate header for Go files. + GoHeaderFile string + + // A sorted list of group versions to generate. For each of them the package path is found + // in GroupVersionToInputPath. + Groups []types.GroupVersions + + // Overrides for which types should be included in the client. + IncludedTypesOverrides map[types.GroupVersion][]string + + // ClientsetName is the name of the clientset to be generated. It's + // populated from command-line arguments. + ClientsetName string + // ClientsetAPIPath is the default API HTTP path for generated clients. + ClientsetAPIPath string + // ClientsetOnly determines if we should generate the clients for groups and + // types along with the clientset. It's populated from command-line + // arguments. + ClientsetOnly bool + // PluralExceptions specify list of exceptions used when pluralizing certain types. + // For example 'Endpoints:Endpoints', otherwise the pluralizer will generate 'Endpointes'. + PluralExceptions []string + + // ApplyConfigurationPackage is the package of apply builders generated by + // applyconfiguration-gen. + // If non-empty, Apply functions are generated for each type and reference the apply builders. + // If empty (""), Apply functions are not generated. + ApplyConfigurationPackage string + + // ProtoBase is the base Go import-path of the protobuf stubs. + ProtoBase string +} + +func New() *Args { + return &Args{ + ClientsetName: "internalclientset", + ClientsetAPIPath: "/apis", + ClientsetOnly: false, + ApplyConfigurationPackage: "", + } +} + +func (a *Args) AddFlags(fs *pflag.FlagSet, inputBase string) { + gvsBuilder := NewGroupVersionsBuilder(&a.Groups) + fs.StringVar(&a.OutputDir, "output-dir", "", + "the base directory under which to generate results") + fs.StringVar(&a.OutputPkg, "output-pkg", a.OutputPkg, + "the Go import-path of the generated results") + fs.StringVar(&a.GoHeaderFile, "go-header-file", "", + "the path to a file containing boilerplate header text; the string \"YEAR\" will be replaced with the current 4-digit year") + fs.Var(NewGVPackagesValue(gvsBuilder, nil), "input", + `group/versions that client-gen will generate clients for. At most one version per group is allowed. Specified in the format "group1/version1,group2/version2...".`) + fs.Var(NewGVTypesValue(&a.IncludedTypesOverrides, []string{}), "included-types-overrides", + "list of group/version/type for which client should be generated. By default, client is generated for all types which have genclient in types.go. This overrides that. For each groupVersion in this list, only the types mentioned here will be included. The default check of genclient will be used for other group versions.") + fs.Var(NewInputBasePathValue(gvsBuilder, inputBase), "input-base", + "base path to look for the api group.") + fs.StringVarP(&a.ClientsetName, "clientset-name", "n", a.ClientsetName, + "the name of the generated clientset package.") + fs.StringVarP(&a.ClientsetAPIPath, "clientset-api-path", "", a.ClientsetAPIPath, + "the value of default API HTTP path, starting with / and without trailing /.") + fs.BoolVar(&a.ClientsetOnly, "clientset-only", a.ClientsetOnly, + "when set, client-gen only generates the clientset shell, without generating the individual typed clients") + fs.StringSliceVar(&a.PluralExceptions, "plural-exceptions", a.PluralExceptions, + "list of comma separated plural exception definitions in Type:PluralizedType form") + fs.StringVar(&a.ApplyConfigurationPackage, "apply-configuration-package", a.ApplyConfigurationPackage, + "optional package of apply configurations, generated by applyconfiguration-gen, that are required to generate Apply functions for each type in the clientset. By default Apply functions are not generated.") + fs.StringVar(&a.ProtoBase, "proto-base", "", + "the base Go import-path of the protobuf stubs") + + // support old flags + fs.SetNormalizeFunc(mapFlagName("clientset-path", "output-pkg", fs.GetNormalizeFunc())) +} + +func (a *Args) Validate() error { + if len(a.OutputDir) == 0 { + return fmt.Errorf("--output-dir must be specified") + } + if len(a.OutputPkg) == 0 { + return fmt.Errorf("--output-pkg must be specified") + } + if len(a.ClientsetName) == 0 { + return fmt.Errorf("--clientset-name must be specified") + } + if len(a.ClientsetAPIPath) == 0 { + return fmt.Errorf("--clientset-api-path cannot be empty") + } + if len(a.ProtoBase) == 0 { + return fmt.Errorf("--proto-base must be specified") + } + + return nil +} + +// GroupVersionPackages returns a map from GroupVersion to the package with the types.go. +func (a *Args) GroupVersionPackages() map[types.GroupVersion]string { + res := map[types.GroupVersion]string{} + for _, pkg := range a.Groups { + for _, v := range pkg.Versions { + res[types.GroupVersion{Group: pkg.Group, Version: v.Version}] = v.Package + } + } + return res +} + +func mapFlagName(from, to string, old func(fs *pflag.FlagSet, name string) pflag.NormalizedName) func(fs *pflag.FlagSet, name string) pflag.NormalizedName { + return func(fs *pflag.FlagSet, name string) pflag.NormalizedName { + if name == from { + name = to + } + return old(fs, name) + } +} diff --git a/code-generator/cmd/client-gen/args/gvpackages.go b/code-generator/cmd/client-gen/args/gvpackages.go new file mode 100644 index 000000000..64331d4c0 --- /dev/null +++ b/code-generator/cmd/client-gen/args/gvpackages.go @@ -0,0 +1,182 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Portions Copyright (c) 2025 NVIDIA CORPORATION. All rights reserved. + +Modified from the original to support gRPC transport. +Origin: https://github.com/kubernetes/code-generator/blob/v0.34.1/cmd/client-gen/args/gvpackages.go +*/ + +package args + +import ( + "bytes" + "encoding/csv" + "flag" + "path" + "sort" + "strings" + + "github.com/nvidia/nvsentinel/code-generator/cmd/client-gen/generators/util" + "github.com/nvidia/nvsentinel/code-generator/cmd/client-gen/types" +) + +type inputBasePathValue struct { + builder *groupVersionsBuilder +} + +var _ flag.Value = &inputBasePathValue{} + +func NewInputBasePathValue(builder *groupVersionsBuilder, def string) *inputBasePathValue { + v := &inputBasePathValue{ + builder: builder, + } + v.Set(def) + return v +} + +func (s *inputBasePathValue) Set(val string) error { + s.builder.importBasePath = val + return s.builder.update() +} + +func (s *inputBasePathValue) Type() string { + return "string" +} + +func (s *inputBasePathValue) String() string { + return s.builder.importBasePath +} + +type gvPackagesValue struct { + builder *groupVersionsBuilder + groups []string + changed bool +} + +func NewGVPackagesValue(builder *groupVersionsBuilder, def []string) *gvPackagesValue { + gvp := new(gvPackagesValue) + gvp.builder = builder + if def != nil { + if err := gvp.set(def); err != nil { + panic(err) + } + } + return gvp +} + +var _ flag.Value = &gvPackagesValue{} + +func (s *gvPackagesValue) set(vs []string) error { + if s.changed { + s.groups = append(s.groups, vs...) + } else { + s.groups = append([]string(nil), vs...) + } + + s.builder.groups = s.groups + return s.builder.update() +} + +func (s *gvPackagesValue) Set(val string) error { + vs, err := readAsCSV(val) + if err != nil { + return err + } + if err := s.set(vs); err != nil { + return err + } + s.changed = true + return nil +} + +func (s *gvPackagesValue) Type() string { + return "stringSlice" +} + +func (s *gvPackagesValue) String() string { + str, _ := writeAsCSV(s.groups) + return "[" + str + "]" +} + +type groupVersionsBuilder struct { + value *[]types.GroupVersions + groups []string + importBasePath string +} + +func NewGroupVersionsBuilder(groups *[]types.GroupVersions) *groupVersionsBuilder { + return &groupVersionsBuilder{ + value: groups, + } +} + +func (p *groupVersionsBuilder) update() error { + var seenGroups = make(map[types.Group]*types.GroupVersions) + for _, v := range p.groups { + pth, gvString := util.ParsePathGroupVersion(v) + gv, err := types.ToGroupVersion(gvString) + if err != nil { + return err + } + + versionPkg := types.PackageVersion{Package: path.Join(p.importBasePath, pth, gv.Group.NonEmpty(), gv.Version.String()), Version: gv.Version} + if group, ok := seenGroups[gv.Group]; ok { + vers := group.Versions + vers = append(vers, versionPkg) + seenGroups[gv.Group].Versions = vers + } else { + seenGroups[gv.Group] = &types.GroupVersions{ + PackageName: gv.Group.NonEmpty(), + Group: gv.Group, + Versions: []types.PackageVersion{versionPkg}, + } + } + } + + var groupNames []string + for groupName := range seenGroups { + groupNames = append(groupNames, groupName.String()) + } + sort.Strings(groupNames) + *p.value = []types.GroupVersions{} + for _, groupName := range groupNames { + *p.value = append(*p.value, *seenGroups[types.Group(groupName)]) + } + + return nil +} + +func readAsCSV(val string) ([]string, error) { + if val == "" { + return []string{}, nil + } + stringReader := strings.NewReader(val) + csvReader := csv.NewReader(stringReader) + return csvReader.Read() +} + +func writeAsCSV(vals []string) (string, error) { + b := &bytes.Buffer{} + w := csv.NewWriter(b) + err := w.Write(vals) + if err != nil { + return "", err + } + w.Flush() + return strings.TrimSuffix(b.String(), "\n"), nil +} diff --git a/code-generator/cmd/client-gen/args/gvpackages_test.go b/code-generator/cmd/client-gen/args/gvpackages_test.go new file mode 100644 index 000000000..5bceefb04 --- /dev/null +++ b/code-generator/cmd/client-gen/args/gvpackages_test.go @@ -0,0 +1,123 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Portions Copyright (c) 2025 NVIDIA CORPORATION. All rights reserved. + +Modified from the original to support gRPC transport. +Origin: https://github.com/kubernetes/code-generator/blob/v0.34.1/cmd/client-gen/args/gvpackages_test.go +*/ + +package args + +import ( + "fmt" + "reflect" + "strings" + "testing" + + "github.com/spf13/pflag" + + "github.com/nvidia/nvsentinel/code-generator/cmd/client-gen/types" +) + +func TestGVPackageFlag(t *testing.T) { + tests := []struct { + args []string + def []string + importBasePath string + expected map[types.GroupVersion]string + expectedGroups []types.GroupVersions + parseError string + }{ + { + args: []string{}, + expected: map[types.GroupVersion]string{}, + expectedGroups: []types.GroupVersions{}, + }, + { + args: []string{"foo/bar/v1", "foo/bar/v2", "foo/bar/", "foo/v1"}, + expectedGroups: []types.GroupVersions{ + {PackageName: "bar", Group: types.Group("bar"), Versions: []types.PackageVersion{ + {Version: "v1", Package: "foo/bar/v1"}, + {Version: "v2", Package: "foo/bar/v2"}, + {Version: "", Package: "foo/bar"}, + }}, + {PackageName: "foo", Group: types.Group("foo"), Versions: []types.PackageVersion{ + {Version: "v1", Package: "foo/v1"}, + }}, + }, + }, + { + args: []string{"foo/bar/v1", "foo/bar/v2", "foo/bar/", "foo/v1"}, + def: []string{"foo/bar/v1alpha1", "foo/v1"}, + expectedGroups: []types.GroupVersions{ + {PackageName: "bar", Group: types.Group("bar"), Versions: []types.PackageVersion{ + {Version: "v1", Package: "foo/bar/v1"}, + {Version: "v2", Package: "foo/bar/v2"}, + {Version: "", Package: "foo/bar"}, + }}, + {PackageName: "foo", Group: types.Group("foo"), Versions: []types.PackageVersion{ + {Version: "v1", Package: "foo/v1"}, + }}, + }, + }, + { + args: []string{"api/v1", "api"}, + expectedGroups: []types.GroupVersions{ + {PackageName: "api", Group: types.Group("api"), Versions: []types.PackageVersion{ + {Version: "v1", Package: "api/v1"}, + {Version: "", Package: "api"}, + }}, + }, + }, + { + args: []string{"foo/v1"}, + importBasePath: "k8s.io/api", + expectedGroups: []types.GroupVersions{ + {PackageName: "foo", Group: types.Group("foo"), Versions: []types.PackageVersion{ + {Version: "v1", Package: "k8s.io/api/foo/v1"}, + }}, + }, + }, + } + for i, test := range tests { + fs := pflag.NewFlagSet("testGVPackage", pflag.ContinueOnError) + groups := []types.GroupVersions{} + builder := NewGroupVersionsBuilder(&groups) + fs.Var(NewGVPackagesValue(builder, test.def), "input", "usage") + fs.Var(NewInputBasePathValue(builder, test.importBasePath), "input-base-path", "usage") + + args := []string{} + for _, a := range test.args { + args = append(args, fmt.Sprintf("--input=%s", a)) + } + + err := fs.Parse(args) + if test.parseError != "" { + if err == nil { + t.Errorf("%d: expected error %q, got nil", i, test.parseError) + } else if !strings.Contains(err.Error(), test.parseError) { + t.Errorf("%d: expected error %q, got %q", i, test.parseError, err) + } + } else if err != nil { + t.Errorf("%d: expected nil error, got %v", i, err) + } + if !reflect.DeepEqual(groups, test.expectedGroups) { + t.Errorf("%d: expected groups %+v, got groups %+v", i, test.expectedGroups, groups) + } + } +} diff --git a/code-generator/cmd/client-gen/args/gvtype.go b/code-generator/cmd/client-gen/args/gvtype.go new file mode 100644 index 000000000..90a8e3373 --- /dev/null +++ b/code-generator/cmd/client-gen/args/gvtype.go @@ -0,0 +1,117 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Portions Copyright (c) 2025 NVIDIA CORPORATION. All rights reserved. + +Modified from the original to support gRPC transport. +Origin: https://github.com/kubernetes/code-generator/blob/v0.34.1/cmd/client-gen/args/gvtype.go +*/ + +package args + +import ( + "flag" + "fmt" + "strings" + + "github.com/nvidia/nvsentinel/code-generator/cmd/client-gen/types" +) + +type gvTypeValue struct { + gvToTypes *map[types.GroupVersion][]string + changed bool +} + +func NewGVTypesValue(gvToTypes *map[types.GroupVersion][]string, def []string) *gvTypeValue { + gvt := new(gvTypeValue) + gvt.gvToTypes = gvToTypes + if def != nil { + if err := gvt.set(def); err != nil { + panic(err) + } + } + return gvt +} + +var _ flag.Value = &gvTypeValue{} + +func (s *gvTypeValue) set(vs []string) error { + if !s.changed { + *s.gvToTypes = map[types.GroupVersion][]string{} + } + + for _, input := range vs { + gvString, typeStr, err := parseGroupVersionType(input) + if err != nil { + return err + } + gv, err := types.ToGroupVersion(gvString) + if err != nil { + return err + } + types, ok := (*s.gvToTypes)[gv] + if !ok { + types = []string{} + } + types = append(types, typeStr) + (*s.gvToTypes)[gv] = types + } + + return nil +} + +func (s *gvTypeValue) Set(val string) error { + vs, err := readAsCSV(val) + if err != nil { + return err + } + if err := s.set(vs); err != nil { + return err + } + s.changed = true + return nil +} + +func (s *gvTypeValue) Type() string { + return "stringSlice" +} + +func (s *gvTypeValue) String() string { + strs := make([]string, 0, len(*s.gvToTypes)) + for gv, ts := range *s.gvToTypes { + for _, t := range ts { + strs = append(strs, gv.Group.String()+"/"+gv.Version.String()+"/"+t) + } + } + str, _ := writeAsCSV(strs) + return "[" + str + "]" +} + +func parseGroupVersionType(gvtString string) (gvString string, typeStr string, err error) { + invalidFormatErr := fmt.Errorf("invalid value: %s, should be of the form group/version/type", gvtString) + subs := strings.Split(gvtString, "/") + length := len(subs) + switch length { + case 2: + // gvtString of the form group/type, e.g. api/Service,extensions/ReplicaSet + return subs[0] + "/", subs[1], nil + case 3: + return strings.Join(subs[:length-1], "/"), subs[length-1], nil + default: + return "", "", invalidFormatErr + } +} diff --git a/code-generator/cmd/client-gen/generators/client_generator.go b/code-generator/cmd/client-gen/generators/client_generator.go new file mode 100644 index 000000000..9dd665cff --- /dev/null +++ b/code-generator/cmd/client-gen/generators/client_generator.go @@ -0,0 +1,452 @@ +/* +Copyright 2015 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Portions Copyright (c) 2025 NVIDIA CORPORATION. All rights reserved. + +Modified from the original to support gRPC transport. +Origin: https://github.com/kubernetes/code-generator/blob/v0.34.1/cmd/client-gen/generators/client_generator.go +*/ + +// Package generators has the generators for the client-gen utility. +package generators + +import ( + "fmt" + "path" + "path/filepath" + "strings" + + "github.com/nvidia/nvsentinel/code-generator/cmd/client-gen/args" + "github.com/nvidia/nvsentinel/code-generator/cmd/client-gen/generators/scheme" + "github.com/nvidia/nvsentinel/code-generator/cmd/client-gen/generators/util" + clientgentypes "github.com/nvidia/nvsentinel/code-generator/cmd/client-gen/types" + + codegennamer "k8s.io/code-generator/pkg/namer" + genutil "k8s.io/code-generator/pkg/util" + "k8s.io/gengo/v2" + "k8s.io/gengo/v2/generator" + "k8s.io/gengo/v2/namer" + "k8s.io/gengo/v2/types" + + "k8s.io/klog/v2" +) + +// NameSystems returns the name system used by the generators in this package. +func NameSystems(pluralExceptions map[string]string) namer.NameSystems { + lowercaseNamer := namer.NewAllLowercasePluralNamer(pluralExceptions) + + publicNamer := &ExceptionNamer{ + Exceptions: map[string]string{ + // these exceptions are used to deconflict the generated code + // you can put your fully qualified package like + // to generate a name that doesn't conflict with your group. + // "k8s.io/apis/events/v1beta1.Event": "EventResource" + }, + KeyFunc: func(t *types.Type) string { + return t.Name.Package + "." + t.Name.Name + }, + Delegate: namer.NewPublicNamer(0), + } + privateNamer := &ExceptionNamer{ + Exceptions: map[string]string{ + // these exceptions are used to deconflict the generated code + // you can put your fully qualified package like + // to generate a name that doesn't conflict with your group. + // "k8s.io/apis/events/v1beta1.Event": "eventResource" + }, + KeyFunc: func(t *types.Type) string { + return t.Name.Package + "." + t.Name.Name + }, + Delegate: namer.NewPrivateNamer(0), + } + publicPluralNamer := &ExceptionNamer{ + Exceptions: map[string]string{ + // these exceptions are used to deconflict the generated code + // you can put your fully qualified package like + // to generate a name that doesn't conflict with your group. + // "k8s.io/apis/events/v1beta1.Event": "EventResource" + }, + KeyFunc: func(t *types.Type) string { + return t.Name.Package + "." + t.Name.Name + }, + Delegate: namer.NewPublicPluralNamer(pluralExceptions), + } + privatePluralNamer := &ExceptionNamer{ + Exceptions: map[string]string{ + // you can put your fully qualified package like + // to generate a name that doesn't conflict with your group. + // "k8s.io/apis/events/v1beta1.Event": "eventResource" + // these exceptions are used to deconflict the generated code + "k8s.io/apis/events/v1beta1.Event": "eventResources", + "k8s.io/kubernetes/pkg/apis/events.Event": "eventResources", + }, + KeyFunc: func(t *types.Type) string { + return t.Name.Package + "." + t.Name.Name + }, + Delegate: namer.NewPrivatePluralNamer(pluralExceptions), + } + + return namer.NameSystems{ + "singularKind": namer.NewPublicNamer(0), + "public": publicNamer, + "private": privateNamer, + "raw": namer.NewRawNamer("", nil), + "publicPlural": publicPluralNamer, + "privatePlural": privatePluralNamer, + "allLowercasePlural": lowercaseNamer, + "resource": codegennamer.NewTagOverrideNamer("resourceName", lowercaseNamer), + } +} + +// ExceptionNamer allows you specify exceptional cases with exact names. This allows you to have control +// for handling various conflicts, like group and resource names for instance. +type ExceptionNamer struct { + Exceptions map[string]string + KeyFunc func(*types.Type) string + + Delegate namer.Namer +} + +// Name provides the requested name for a type. +func (n *ExceptionNamer) Name(t *types.Type) string { + key := n.KeyFunc(t) + if exception, ok := n.Exceptions[key]; ok { + return exception + } + return n.Delegate.Name(t) +} + +// DefaultNameSystem returns the default name system for ordering the types to be +// processed by the generators in this package. +func DefaultNameSystem() string { + return "public" +} +func targetForGroup(gv clientgentypes.GroupVersion, typeList []*types.Type, clientsetDir, clientsetPkg string, groupPkgName string, groupGoName string, apiPath string, inputPkg string, applyBuilderPkg string, boilerplate []byte, protoPkg clientgentypes.ProtobufPackage) generator.Target { + + subdir := []string{"typed", strings.ToLower(groupPkgName), strings.ToLower(gv.Version.NonEmpty())} + gvDir := filepath.Join(clientsetDir, filepath.Join(subdir...)) + gvPkg := path.Join(clientsetPkg, path.Join(subdir...)) + + return &generator.SimpleTarget{ + PkgName: strings.ToLower(gv.Version.NonEmpty()), + PkgPath: gvPkg, + PkgDir: gvDir, + HeaderComment: boilerplate, + PkgDocComment: []byte("// This package has the automatically generated typed clients.\n"), + // GeneratorsFunc returns a list of generators. Each generator makes a + // single file. + GeneratorsFunc: func(c *generator.Context) (generators []generator.Generator) { + generators = []generator.Generator{ + // Always generate a "doc.go" file. + generator.GoGenerator{OutputFilename: "doc.go"}, + } + // Since we want a file per type that we generate a client for, we + // have to provide a function for this. + for _, t := range typeList { + generators = append(generators, &genClientForType{ + GoGenerator: generator.GoGenerator{ + OutputFilename: strings.ToLower(c.Namers["private"].Name(t)) + ".go", + }, + outputPackage: gvPkg, + inputPackage: inputPkg, + clientsetPackage: clientsetPkg, + applyConfigurationPackage: applyBuilderPkg, + group: gv.Group.NonEmpty(), + version: gv.Version.String(), + groupGoName: groupGoName, + typeToMatch: t, + imports: generator.NewImportTrackerForPackage(gvPkg), + protoPackage: protoPkg, + }) + } + + generators = append(generators, &genGroup{ + GoGenerator: generator.GoGenerator{ + OutputFilename: groupPkgName + "_client.go", + }, + outputPackage: gvPkg, + inputPackage: inputPkg, + clientsetPackage: clientsetPkg, + group: gv.Group.NonEmpty(), + version: gv.Version.String(), + groupGoName: groupGoName, + apiPath: apiPath, + types: typeList, + imports: generator.NewImportTrackerForPackage(gvPkg), + }) + + expansionFileName := "generated_expansion.go" + generators = append(generators, &genExpansion{ + groupPackagePath: gvDir, + GoGenerator: generator.GoGenerator{ + OutputFilename: expansionFileName, + }, + types: typeList, + }) + + return generators + }, + FilterFunc: func(c *generator.Context, t *types.Type) bool { + return util.MustParseClientGenTags(append(t.SecondClosestCommentLines, t.CommentLines...)).GenerateClient + }, + } +} + +func targetForClientset(args *args.Args, clientsetDir, clientsetPkg string, groupGoNames map[clientgentypes.GroupVersion]string, boilerplate []byte) generator.Target { + return &generator.SimpleTarget{ + PkgName: args.ClientsetName, + PkgPath: clientsetPkg, + PkgDir: clientsetDir, + HeaderComment: boilerplate, + // GeneratorsFunc returns a list of generators. Each generator generates a + // single file. + GeneratorsFunc: func(c *generator.Context) (generators []generator.Generator) { + generators = []generator.Generator{ + &genClientset{ + GoGenerator: generator.GoGenerator{ + OutputFilename: "clientset.go", + }, + groups: args.Groups, + groupGoNames: groupGoNames, + clientsetPackage: clientsetPkg, + imports: generator.NewImportTrackerForPackage(clientsetPkg), + }, + } + return generators + }, + } +} + +func targetForScheme(args *args.Args, clientsetDir, clientsetPkg string, groupGoNames map[clientgentypes.GroupVersion]string, boilerplate []byte) generator.Target { + schemeDir := filepath.Join(clientsetDir, "scheme") + schemePkg := path.Join(clientsetPkg, "scheme") + + // create runtime.Registry for internal client because it has to know about group versions + internalClient := false +NextGroup: + for _, group := range args.Groups { + for _, v := range group.Versions { + if v.String() == "" { + internalClient = true + break NextGroup + } + } + } + + return &generator.SimpleTarget{ + PkgName: "scheme", + PkgPath: schemePkg, + PkgDir: schemeDir, + HeaderComment: boilerplate, + PkgDocComment: []byte("// This package contains the scheme of the automatically generated clientset.\n"), + // GeneratorsFunc returns a list of generators. Each generator generates a + // single file. + GeneratorsFunc: func(c *generator.Context) (generators []generator.Generator) { + generators = []generator.Generator{ + // Always generate a "doc.go" file. + generator.GoGenerator{OutputFilename: "doc.go"}, + + &scheme.GenScheme{ + GoGenerator: generator.GoGenerator{ + OutputFilename: "register.go", + }, + InputPackages: args.GroupVersionPackages(), + OutputPkg: schemePkg, + OutputPath: schemeDir, + Groups: args.Groups, + GroupGoNames: groupGoNames, + ImportTracker: generator.NewImportTrackerForPackage(schemePkg), + CreateRegistry: internalClient, + }, + } + return generators + }, + } +} + +// applyGroupOverrides applies group name overrides to each package, if applicable. If there is a +// comment of the form "// +groupName=somegroup" or "// +groupName=somegroup.foo.bar.io", use the +// first field (somegroup) as the name of the group in Go code, e.g. as the func name in a clientset. +// +// If the first field of the groupName is not unique within the clientset, use "// +groupName=unique +func applyGroupOverrides(universe types.Universe, args *args.Args) error { + // Create a map from "old GV" to "new GV" so we know what changes we need to make. + changes := make(map[clientgentypes.GroupVersion]clientgentypes.GroupVersion) + for gv, inputDir := range args.GroupVersionPackages() { + p := universe.Package(inputDir) + override, err := genutil.ExtractCommentTagsWithoutArguments("+", []string{"groupName"}, p.Comments) + if err != nil { + return fmt.Errorf("cannot extract groupName tags: %w", err) + } + if override["groupName"] != nil { + newGV := clientgentypes.GroupVersion{ + Group: clientgentypes.Group(override["groupName"][0]), + Version: gv.Version, + } + changes[gv] = newGV + } + } + + // Modify args.Groups based on the groupName overrides. + newGroups := make([]clientgentypes.GroupVersions, 0, len(args.Groups)) + for _, gvs := range args.Groups { + if len(gvs.Versions) == 0 { + return fmt.Errorf("group %q has no versions", gvs.Group.String()) + } + gv := clientgentypes.GroupVersion{ + Group: gvs.Group, + Version: gvs.Versions[0].Version, // we only need a version, and the first will do + } + if newGV, ok := changes[gv]; ok { + // There's an override, so use it. + newGVS := clientgentypes.GroupVersions{ + PackageName: gvs.PackageName, + Group: newGV.Group, + Versions: gvs.Versions, + } + newGroups = append(newGroups, newGVS) + } else { + // No override. + newGroups = append(newGroups, gvs) + } + } + args.Groups = newGroups + return nil +} + +// Because we try to assemble inputs from an input-base and a set of +// group-version arguments, sometimes that comes in as a filesystem path. This +// function rewrites them all as their canonical Go import-paths. +// +// TODO: Change this tool to just take inputs as Go "patterns" like every other +// gengo tool, then extract GVs from those. +func sanitizePackagePaths(context *generator.Context, args *args.Args) error { + for i := range args.Groups { + pkg := &args.Groups[i] + for j := range pkg.Versions { + ver := &pkg.Versions[j] + input := ver.Package + p := context.Universe[input] + if p == nil || p.Name == "" { + pkgs, err := context.FindPackages(input) + if err != nil { + return fmt.Errorf("can't find input package %q: %w", input, err) + } + p = context.Universe[pkgs[0]] + if p == nil { + return fmt.Errorf("can't find input package %q in universe", input) + } + ver.Package = p.Path + } + } + } + return nil +} + +// GetTargets makes the client target definition. +func GetTargets(context *generator.Context, args *args.Args) []generator.Target { + boilerplate, err := gengo.GoBoilerplate(args.GoHeaderFile, "", gengo.StdGeneratedBy) + if err != nil { + klog.Fatalf("Failed loading boilerplate: %v", err) + } + + includedTypesOverrides := args.IncludedTypesOverrides + + if err := sanitizePackagePaths(context, args); err != nil { + klog.Fatalf("cannot sanitize inputs: %v", err) + } + if err := applyGroupOverrides(context.Universe, args); err != nil { + klog.Fatalf("cannot apply group overrides: %v", err) + } + + gvToTypes := map[clientgentypes.GroupVersion][]*types.Type{} + groupGoNames := make(map[clientgentypes.GroupVersion]string) + for gv, inputDir := range args.GroupVersionPackages() { + p := context.Universe.Package(inputDir) + + // If there's a comment of the form "// +groupGoName=SomeUniqueShortName", use that as + // the Go group identifier in CamelCase. It defaults + groupGoNames[gv] = namer.IC(strings.Split(gv.Group.NonEmpty(), ".")[0]) + override, err := genutil.ExtractCommentTagsWithoutArguments("+", []string{"groupGoName"}, p.Comments) + if err != nil { + klog.Fatalf("cannot extract groupGoName tags: %v", err) + } + if override["groupGoName"] != nil { + groupGoNames[gv] = namer.IC(override["groupGoName"][0]) + } + + for n, t := range p.Types { + // filter out types which are not included in user specified overrides. + typesOverride, ok := includedTypesOverrides[gv] + if ok { + found := false + for _, typeStr := range typesOverride { + if typeStr == n { + found = true + break + } + } + if !found { + continue + } + } else { + // User has not specified any override for this group version. + // filter out types which don't have genclient. + if tags := util.MustParseClientGenTags(append(t.SecondClosestCommentLines, t.CommentLines...)); !tags.GenerateClient { + continue + } + } + if _, found := gvToTypes[gv]; !found { + gvToTypes[gv] = []*types.Type{} + } + gvToTypes[gv] = append(gvToTypes[gv], t) + } + } + + clientsetDir := filepath.Join(args.OutputDir, args.ClientsetName) + clientsetPkg := path.Join(args.OutputPkg, args.ClientsetName) + + var targetList []generator.Target + + targetList = append(targetList, + targetForClientset(args, clientsetDir, clientsetPkg, groupGoNames, boilerplate)) + targetList = append(targetList, + targetForScheme(args, clientsetDir, clientsetPkg, groupGoNames, boilerplate)) + + // If --clientset-only=true, we don't regenerate the individual typed clients. + if args.ClientsetOnly { + return []generator.Target(targetList) + } + + orderer := namer.Orderer{Namer: namer.NewPrivateNamer(0)} + gvPackages := args.GroupVersionPackages() + for _, group := range args.Groups { + for _, version := range group.Versions { + gv := clientgentypes.GroupVersion{Group: group.Group, Version: version.Version} + types := gvToTypes[gv] + inputPath := gvPackages[gv] + protoPackage := clientgentypes.NewProtobufPackage(args.ProtoBase, group.PackageName, version.Version.String()) + targetList = append(targetList, + targetForGroup( + gv, orderer.OrderTypes(types), clientsetDir, clientsetPkg, + group.PackageName, groupGoNames[gv], args.ClientsetAPIPath, + inputPath, args.ApplyConfigurationPackage, boilerplate, protoPackage)) + } + } + + return targetList +} diff --git a/code-generator/cmd/client-gen/generators/generator_for_clientset.go b/code-generator/cmd/client-gen/generators/generator_for_clientset.go new file mode 100644 index 000000000..961ff4f58 --- /dev/null +++ b/code-generator/cmd/client-gen/generators/generator_for_clientset.go @@ -0,0 +1,191 @@ +/* +Copyright 2015 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Portions Copyright (c) 2025 NVIDIA CORPORATION. All rights reserved. + +Modified from the original to support gRPC transport. +Origin: https://github.com/kubernetes/code-generator/blob/v0.34.1/cmd/client-gen/generators/generator_for_clientset.go +*/ + +// Package generators has the generators for the client-gen utility. +package generators + +import ( + "fmt" + "io" + "path" + "strings" + + clientgentypes "github.com/nvidia/nvsentinel/code-generator/cmd/client-gen/types" + + "k8s.io/gengo/v2/generator" + "k8s.io/gengo/v2/namer" + "k8s.io/gengo/v2/types" +) + +// genClientset generates a package for a clientset. +type genClientset struct { + generator.GoGenerator + groups []clientgentypes.GroupVersions + groupGoNames map[clientgentypes.GroupVersion]string + clientsetPackage string // must be a Go import-path + imports namer.ImportTracker + clientsetGenerated bool +} + +var _ generator.Generator = &genClientset{} + +func (g *genClientset) Namers(c *generator.Context) namer.NameSystems { + return namer.NameSystems{ + "raw": namer.NewRawNamer(g.clientsetPackage, g.imports), + } +} + +// We only want to call GenerateType() once. +func (g *genClientset) Filter(c *generator.Context, t *types.Type) bool { + ret := !g.clientsetGenerated + g.clientsetGenerated = true + return ret +} + +func (g *genClientset) Imports(c *generator.Context) (imports []string) { + imports = append(imports, g.imports.ImportLines()...) + for _, group := range g.groups { + for _, version := range group.Versions { + typedClientPath := path.Join(g.clientsetPackage, "typed", strings.ToLower(group.PackageName), strings.ToLower(version.NonEmpty())) + groupAlias := strings.ToLower(g.groupGoNames[clientgentypes.GroupVersion{Group: group.Group, Version: version.Version}]) + imports = append(imports, fmt.Sprintf("%s%s \"%s\"", groupAlias, strings.ToLower(version.NonEmpty()), typedClientPath)) + } + } + return +} + +func (g *genClientset) GenerateType(c *generator.Context, t *types.Type, w io.Writer) error { + // TODO: We actually don't need any type information to generate the clientset, + // perhaps we can adapt the go2ild framework to this kind of usage. + sw := generator.NewSnippetWriter(w, c, "$", "$") + + allGroups := clientgentypes.ToGroupVersionInfo(g.groups, g.groupGoNames) + m := map[string]interface{}{ + "allGroups": allGroups, + "fmtErrorf": c.Universe.Type(types.Name{Package: "fmt", Name: "Errorf"}), + "Config": c.Universe.Type(types.Name{Package: "github.com/nvidia/nvsentinel/client-go/nvgrpc", Name: "Config"}), + "ClientConnFor": c.Universe.Function(types.Name{Package: "github.com/nvidia/nvsentinel/client-go/nvgrpc", Name: "ClientConnFor"}), + "ClientConnInterface": c.Universe.Type(types.Name{Package: "google.golang.org/grpc", Name: "ClientConnInterface"}), + } + + sw.Do(clientsetInterface, m) + sw.Do(clientsetTemplate, m) + for _, g := range allGroups { + sw.Do(clientsetInterfaceImplTemplate, g) + } + sw.Do(newClientsetForConfigTemplate, m) + sw.Do(newClientsetForConfigAndClientTemplate, m) + sw.Do(newClientsetForConfigOrDieTemplate, m) + sw.Do(newClientsetForGrpcClientTemplate, m) + + return sw.Error() +} + +var clientsetInterface = ` +type Interface interface { + $range .allGroups$$.GroupGoName$$.Version$() $.PackageAlias$.$.GroupGoName$$.Version$Interface + $end$ +} +` +var clientsetTemplate = ` +// Clientset contains the clients for groups. +type Clientset struct { + $range .allGroups$$.LowerCaseGroupGoName$$.Version$ *$.PackageAlias$.$.GroupGoName$$.Version$Client + $end$ +} +` + +var clientsetInterfaceImplTemplate = ` +// $.GroupGoName$$.Version$ retrieves the $.GroupGoName$$.Version$Client +func (c *Clientset) $.GroupGoName$$.Version$() $.PackageAlias$.$.GroupGoName$$.Version$Interface { + return c.$.LowerCaseGroupGoName$$.Version$ +} +` + +var newClientsetForConfigTemplate = ` +// NewForConfig creates a new Clientset for the given config. +// NewForConfig is equivalent to NewForConfigAndClient(c, clientConn), +// where clientConn was generated with nvgrpc.ClientConnFor(c). +// +// If you need to customize the connection (e.g. set a logger), +// use nvgrpc.ClientConnFor() manually and pass the connection to NewForConfigAndClient. +func NewForConfig(c *$.Config|raw$) (*Clientset, error) { + if c == nil { + return nil, $.fmtErrorf|raw$("config cannot be nil") + } + + configShallowCopy := *c // Shallow copy to avoid mutation + conn, err := $.ClientConnFor|raw$(&configShallowCopy) + if err != nil { + return nil, err + } + + return NewForConfigAndClient(&configShallowCopy, conn) +} +` + +var newClientsetForConfigAndClientTemplate = ` +// NewForConfigAndClient creates a new Clientset for the given config and gRPC client connection. +// The provided gRPC client connection provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *$.Config|raw$, conn $.ClientConnInterface|raw$) (*Clientset, error) { + if c == nil { + return nil, $.fmtErrorf|raw$("config cannot be nil") + } + if conn == nil { + return nil, $.fmtErrorf|raw$("gRPC connection cannot be nil") + } + + configShallowCopy := *c // Shallow copy to avoid mutation + + var cs Clientset + var err error +$range .allGroups$ cs.$.LowerCaseGroupGoName$$.Version$, err = $.PackageAlias$.NewForConfigAndClient(&configShallowCopy, conn) + if err != nil { + return nil, err + } +$end$ + return &cs, nil +} +` + +var newClientsetForConfigOrDieTemplate = ` +// NewForConfigOrDie creates a new Clientset for the given config and +// panics if there is an error in the config or connection setup. +func NewForConfigOrDie(c *$.Config|raw$) *Clientset { + cs, err := NewForConfig(c) + if err != nil { + panic(err) + } + return cs +} +` + +var newClientsetForGrpcClientTemplate = ` +// New creates a new Clientset for the given gRPC client connection. +func New(conn $.ClientConnInterface|raw$) *Clientset { + var cs Clientset +$range .allGroups$ cs.$.LowerCaseGroupGoName$$.Version$ = $.PackageAlias$.New(conn) +$end$ + return &cs +} +` diff --git a/code-generator/cmd/client-gen/generators/generator_for_expansion.go b/code-generator/cmd/client-gen/generators/generator_for_expansion.go new file mode 100644 index 000000000..91d4463f0 --- /dev/null +++ b/code-generator/cmd/client-gen/generators/generator_for_expansion.go @@ -0,0 +1,61 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Copied unmodified from the original. +Reason: The upstream package is internal and cannot be imported. +Origin: https://github.com/kubernetes/code-generator/blob/v0.34.1/cmd/client-gen/generators/generator_for_expansion.go +*/ + +// Package generators has the generators for the client-gen utility. +package generators + +import ( + "io" + "os" + "path/filepath" + "strings" + + "k8s.io/gengo/v2/generator" + "k8s.io/gengo/v2/types" +) + +// genExpansion produces a file for a group client, e.g. ExtensionsClient for the extension group. +type genExpansion struct { + generator.GoGenerator + groupPackagePath string + // types in a group + types []*types.Type +} + +// We only want to call GenerateType() once per group. +func (g *genExpansion) Filter(c *generator.Context, t *types.Type) bool { + return len(g.types) == 0 || t == g.types[0] +} + +func (g *genExpansion) GenerateType(c *generator.Context, t *types.Type, w io.Writer) error { + sw := generator.NewSnippetWriter(w, c, "$", "$") + for _, t := range g.types { + if _, err := os.Stat(filepath.Join(g.groupPackagePath, strings.ToLower(t.Name.Name+"_expansion.go"))); os.IsNotExist(err) { + sw.Do(expansionInterfaceTemplate, t) + } + } + return sw.Error() +} + +var expansionInterfaceTemplate = ` +type $.|public$Expansion interface {} +` diff --git a/code-generator/cmd/client-gen/generators/generator_for_group.go b/code-generator/cmd/client-gen/generators/generator_for_group.go new file mode 100644 index 000000000..26828f53c --- /dev/null +++ b/code-generator/cmd/client-gen/generators/generator_for_group.go @@ -0,0 +1,227 @@ +/* +Copyright 2015 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Portions Copyright (c) 2025 NVIDIA CORPORATION. All rights reserved. + +Modified from the original to support gRPC transport. +Origin: https://github.com/kubernetes/code-generator/blob/v0.34.1/cmd/client-gen/generators/generator_for_group.go +*/ + +// Package generators has the generators for the client-gen utility. +package generators + +import ( + "io" + + genutil "k8s.io/code-generator/pkg/util" + "k8s.io/gengo/v2/generator" + "k8s.io/gengo/v2/namer" + "k8s.io/gengo/v2/types" + + "github.com/nvidia/nvsentinel/code-generator/cmd/client-gen/generators/util" +) + +// genGroup produces a file for a group client, e.g. ExtensionsClient for the extension group. +type genGroup struct { + generator.GoGenerator + outputPackage string + group string + version string + groupGoName string + apiPath string + // types in this group + types []*types.Type + imports namer.ImportTracker + inputPackage string + clientsetPackage string // must be a Go import-path + // If the genGroup has been called. This generator should only execute once. + called bool +} + +var _ generator.Generator = &genGroup{} + +// We only want to call GenerateType() once per group. +func (g *genGroup) Filter(c *generator.Context, t *types.Type) bool { + if !g.called { + g.called = true + return true + } + return false +} + +func (g *genGroup) Namers(c *generator.Context) namer.NameSystems { + return namer.NameSystems{ + "raw": namer.NewRawNamer(g.outputPackage, g.imports), + } +} + +func (g *genGroup) Imports(c *generator.Context) (imports []string) { + imports = append(imports, g.imports.ImportLines()...) + return +} + +func (g *genGroup) GenerateType(c *generator.Context, t *types.Type, w io.Writer) error { + sw := generator.NewSnippetWriter(w, c, "$", "$") + + // allow user to define a group name that's different from the one parsed from the directory. + p := c.Universe.Package(g.inputPackage) + groupName := g.group + override, err := genutil.ExtractCommentTagsWithoutArguments("+", []string{"groupName"}, p.Comments) + if err != nil { + return err + } + if values, ok := override["groupName"]; ok { + groupName = values[0] + } + + m := map[string]interface{}{ + "version": g.version, + "groupName": groupName, + "GroupGoName": g.groupGoName, + "Version": namer.IC(g.version), + "types": g.types, + "ClientConnInterface": c.Universe.Type(types.Name{Package: "google.golang.org/grpc", Name: "ClientConnInterface"}), + "Config": c.Universe.Type(types.Name{Package: "github.com/nvidia/nvsentinel/client-go/nvgrpc", Name: "Config"}), + "ClientConnFor": c.Universe.Function(types.Name{Package: "github.com/nvidia/nvsentinel/client-go/nvgrpc", Name: "ClientConnFor"}), + "Logger": c.Universe.Type(types.Name{Package: "github.com/go-logr/logr", Name: "Logger"}), + "Discard": c.Universe.Function(types.Name{Package: "github.com/go-logr/logr", Name: "Discard"}), + "fmtErrorf": c.Universe.Function(types.Name{Package: "fmt", Name: "Errorf"}), + } + sw.Do(groupInterfaceTemplate, m) + sw.Do(groupClientTemplate, m) + for _, t := range g.types { + tags, err := util.ParseClientGenTags(append(t.SecondClosestCommentLines, t.CommentLines...)) + if err != nil { + return err + } + wrapper := map[string]interface{}{ + "type": t, + "GroupGoName": g.groupGoName, + "Version": namer.IC(g.version), + } + if tags.NonNamespaced { + sw.Do(getterImplNonNamespaced, wrapper) + } else { + sw.Do(getterImplNamespaced, wrapper) + } + } + sw.Do(newClientForConfigTemplate, m) + sw.Do(newClientForConfigAndClientTemplate, m) + sw.Do(newClientForConfigOrDieTemplate, m) + sw.Do(newClientForGrpcConnTemplate, m) + sw.Do(getClientConn, m) + + return sw.Error() +} + +var groupInterfaceTemplate = ` +type $.GroupGoName$$.Version$Interface interface { + ClientConn() $.ClientConnInterface|raw$ + $range .types$ $.|publicPlural$Getter + $end$ +} +` + +var groupClientTemplate = ` +// $.GroupGoName$$.Version$Client is used to interact with features provided by the $.groupName$ group. +type $.GroupGoName$$.Version$Client struct { + conn $.ClientConnInterface|raw$ + logger $.Logger|raw$ +} +` + +var getterImplNamespaced = ` +func (c *$.GroupGoName$$.Version$Client) $.type|publicPlural$(namespace string) $.type|public$Interface { + return new$.type|publicPlural$(c, namespace) +} +` + +var getterImplNonNamespaced = ` +func (c *$.GroupGoName$$.Version$Client) $.type|publicPlural$() $.type|public$Interface { + return new$.type|publicPlural$(c) +} +` + +var newClientForConfigTemplate = ` +// NewForConfig creates a new $.GroupGoName$$.Version$Client for the given config. +// NewForConfig is equivalent to NewForConfigAndClient(c, clientConn), +// where clientConn was generated with nvgrpc.ClientConnFor(c). +func NewForConfig(c *$.Config|raw$) (*$.GroupGoName$$.Version$Client, error) { + if c == nil { + return nil, $.fmtErrorf|raw$("config cannot be nil") + } + + config := *c // Shallow copy to avoid mutation + conn, err := $.ClientConnFor|raw$(&config) + if err != nil { + return nil, err + } + + return NewForConfigAndClient(&config, conn) +} +` + +var newClientForConfigAndClientTemplate = ` +// NewForConfigAndClient creates a new $.GroupGoName$$.Version$Client for the given config and gRPC client connection. +// Note the grpc client connection provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *$.Config|raw$, conn $.ClientConnInterface|raw$) (*$.GroupGoName$$.Version$Client, error) { + if c == nil { + return nil, $.fmtErrorf|raw$("config cannot be nil") + } + if conn == nil { + return nil, $.fmtErrorf|raw$("gRPC connection cannot be nil") + } + + return &$.GroupGoName$$.Version$Client{ + conn: conn, + logger: c.GetLogger().WithName("$.groupName$.$.version$"), + }, nil +} +` + +var newClientForConfigOrDieTemplate = ` +// NewForConfigOrDie creates a new $.GroupGoName$$.Version$Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *$.Config|raw$) *$.GroupGoName$$.Version$Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} +` + +var getClientConn = ` +// ClientConn returns a gRPC client connection that is used to communicate +// with API server by this client implementation. +func (c *$.GroupGoName$$.Version$Client) ClientConn() $.ClientConnInterface|raw$ { + if c == nil { + return nil + } + return c.conn +} +` + +var newClientForGrpcConnTemplate = ` +// New creates a new $.GroupGoName$$.Version$Client for the given gRPC client connection. +func New(c $.ClientConnInterface|raw$) *$.GroupGoName$$.Version$Client { + return &$.GroupGoName$$.Version$Client{ + conn: c, + logger: $.Discard|raw$(), + } +} +` diff --git a/code-generator/cmd/client-gen/generators/generator_for_type.go b/code-generator/cmd/client-gen/generators/generator_for_type.go new file mode 100644 index 000000000..32d457446 --- /dev/null +++ b/code-generator/cmd/client-gen/generators/generator_for_type.go @@ -0,0 +1,389 @@ +/* +Copyright 2015 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Portions Copyright (c) 2025 NVIDIA CORPORATION. All rights reserved. + +Modified from the original to support gRPC transport. +Origin: https://github.com/kubernetes/code-generator/blob/v0.34.1/cmd/client-gen/generators/generator_for_type.go +*/ + +// Package generators has the generators for the client-gen utility. +package generators + +import ( + "io" + "path" + "strings" + + "golang.org/x/text/cases" + "golang.org/x/text/language" + + "k8s.io/gengo/v2/generator" + "k8s.io/gengo/v2/namer" + "k8s.io/gengo/v2/types" + + "github.com/nvidia/nvsentinel/code-generator/cmd/client-gen/generators/util" + clientgentypes "github.com/nvidia/nvsentinel/code-generator/cmd/client-gen/types" +) + +// genClientForType produces a file for each top-level type. +type genClientForType struct { + generator.GoGenerator + outputPackage string // must be a Go import-path + inputPackage string + clientsetPackage string // must be a Go import-path + applyConfigurationPackage string // must be a Go import-path + group string + version string + groupGoName string + typeToMatch *types.Type + imports namer.ImportTracker + protoPackage clientgentypes.ProtobufPackage +} + +var _ generator.Generator = &genClientForType{} + +var titler = cases.Title(language.Und) + +// Filter ignores all but one type because we're making a single file per type. +func (g *genClientForType) Filter(c *generator.Context, t *types.Type) bool { + return t == g.typeToMatch +} + +func (g *genClientForType) Namers(c *generator.Context) namer.NameSystems { + return namer.NameSystems{ + "raw": namer.NewRawNamer(g.outputPackage, g.imports), + } +} + +func (g *genClientForType) Imports(c *generator.Context) (imports []string) { + return append( + g.imports.ImportLines(), + g.protoPackage.ImportLines()..., + ) +} + +// Ideally, we'd like genStatus to return true if there is a subresource path +// registered for "status" in the API server, but we do not have that +// information, so genStatus returns true if the type has a status field. +func genStatus(t *types.Type) bool { + // Default to true if we have a Status member + hasStatus := false + for _, m := range t.Members { + if m.Name == "Status" { + hasStatus = true + break + } + } + return hasStatus && !util.MustParseClientGenTags(append(t.SecondClosestCommentLines, t.CommentLines...)).NoStatus +} + +func (g *genClientForType) GenerateType(c *generator.Context, t *types.Type, w io.Writer) error { + sw := generator.NewSnippetWriter(w, c, "$", "$") + pkg := path.Base(t.Name.Package) + tags, err := util.ParseClientGenTags(append(t.SecondClosestCommentLines, t.CommentLines...)) + if err != nil { + return err + } + protoType := titler.String(t.Name.Name) + m := map[string]interface{}{ + "type": t, + "inputType": t, + "resultType": t, + "package": pkg, + "Package": namer.IC(pkg), + "namespaced": !tags.NonNamespaced, + "Group": namer.IC(g.group), + "GroupGoName": g.groupGoName, + "Version": namer.IC(g.version), + "ProtoType": protoType, + "context": c.Universe.Type(types.Name{Package: "context", Name: "Context"}), + "fmtErrorf": c.Universe.Function(types.Name{Package: "fmt", Name: "Errorf"}), + "metav1": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/apis/meta/v1", Name: "Option"}), + "GetOptions": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/apis/meta/v1", Name: "GetOptions"}), + "ListOptions": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/apis/meta/v1", Name: "ListOptions"}), + "CreateOptions": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/apis/meta/v1", Name: "CreateOptions"}), + "DeleteOptions": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/apis/meta/v1", Name: "DeleteOptions"}), + "UpdateOptions": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/apis/meta/v1", Name: "UpdateOptions"}), + "PatchOptions": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/apis/meta/v1", Name: "PatchOptions"}), + "PatchType": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/types", Name: "PatchType"}), + "runtime": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/runtime", Name: "Object"}), + "watchInterface": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/watch", Name: "Interface"}), + "logr": c.Universe.Type(types.Name{Package: "github.com/go-logr/logr", Name: "Logger"}), + "nvgrpc": c.Universe.Type(types.Name{Package: "github.com/nvidia/nvsentinel/client-go/nvgrpc", Name: "NewWatcher"}), + "pb": g.protoPackage.Alias, + "apiPackage": c.Universe.Type(types.Name{Package: g.inputPackage, Name: "Ignored"}), + "FromProto": c.Universe.Function(types.Name{Package: g.inputPackage, Name: "FromProto"}), + "FromProtoList": c.Universe.Function(types.Name{Package: g.inputPackage, Name: "FromProtoList"}), + "NewServiceClient": g.protoPackage.ServiceClientConstructorFor(protoType), + } + + sw.Do(getterComment, m) + if tags.NonNamespaced { + sw.Do(getterNonNamespaced, m) + } else { + sw.Do(getterNamespaced, m) + } + + sw.Do(generateInterfaceTemplate(tags, t), m) + + if tags.NonNamespaced { + sw.Do(structTemplateNonNamespaced, m) + sw.Do(constructorTemplateNonNamespaced, m) + } else { + sw.Do(structTemplateNamespaced, m) + sw.Do(constructorTemplateNamespaced, m) + } + + if tags.HasVerb("get") { + sw.Do(getTemplate, m) + } + + if tags.HasVerb("list") { + sw.Do(listTemplate, m) + } + + if tags.HasVerb("watch") { + sw.Do(watchTemplate, m) + sw.Do(watchAdapterTemplate, m) + } + + if tags.HasVerb("create") { + sw.Do(createTemplate, m) + } + + if tags.HasVerb("update") { + sw.Do(updateTemplate, m) + } + + if genStatus(t) && tags.HasVerb("updateStatus") { + sw.Do(updateStatusTemplate, m) + } + + if tags.HasVerb("delete") { + sw.Do(deleteTemplate, m) + } + + if tags.HasVerb("patch") { + sw.Do(patchTemplate, m) + } + + return sw.Error() +} + +func generateInterfaceTemplate(tags util.Tags, t *types.Type) string { + lines := []string{} + + lines = append(lines, ` +// $.type|public$Interface has methods to work with $.type|public$ resources. +type $.type|public$Interface interface {`) + + if tags.HasVerb("create") { + lines = append(lines, ` Create(ctx $.context|raw$, $.type|private$ *$.type|raw$, opts $.CreateOptions|raw$) (*$.type|raw$, error)`) + } + if tags.HasVerb("update") { + lines = append(lines, ` Update(ctx $.context|raw$, $.type|private$ *$.type|raw$, opts $.UpdateOptions|raw$) (*$.type|raw$, error)`) + } + if genStatus(t) && tags.HasVerb("updateStatus") { + lines = append(lines, ` UpdateStatus(ctx $.context|raw$, $.type|private$ *$.type|raw$, opts $.UpdateOptions|raw$) (*$.type|raw$, error)`) + } + if tags.HasVerb("delete") { + lines = append(lines, ` Delete(ctx $.context|raw$, name string, opts $.DeleteOptions|raw$) error`) + } + if tags.HasVerb("get") { + lines = append(lines, ` Get(ctx $.context|raw$, name string, opts $.GetOptions|raw$) (*$.type|raw$, error)`) + } + if tags.HasVerb("list") { + lines = append(lines, ` List(ctx $.context|raw$, opts $.ListOptions|raw$) (*$.type|raw$List, error)`) + } + if tags.HasVerb("watch") { + lines = append(lines, ` Watch(ctx $.context|raw$, opts $.ListOptions|raw$) ($.watchInterface|raw$, error)`) + } + if tags.HasVerb("patch") { + lines = append(lines, ` Patch(ctx $.context|raw$, name string, pt $.PatchType|raw$, data []byte, opts $.PatchOptions|raw$, subresources ...string) (result *$.type|raw$, err error)`) + } + + lines = append(lines, ` $.type|public$Expansion +} +`) + + return strings.Join(lines, "\n") +} + +var getterComment = ` +// $.type|publicPlural$Getter has a method to return a $.type|public$Interface. +// A group's client should implement this interface.` + +var getterNamespaced = ` +type $.type|publicPlural$Getter interface { + $.type|publicPlural$(namespace string) $.type|public$Interface +} +` + +var getterNonNamespaced = ` +type $.type|publicPlural$Getter interface { + $.type|publicPlural$() $.type|public$Interface +} +` + +var structTemplateNamespaced = ` +// $.type|allLowercasePlural$ implements $.type|public$Interface +type $.type|allLowercasePlural$ struct { + client $.pb$.$.ProtoType$ServiceClient + logger $.logr|raw$ + ns string +} +` + +var structTemplateNonNamespaced = ` +// $.type|allLowercasePlural$ implements $.type|public$Interface +type $.type|allLowercasePlural$ struct { + client $.pb$.$.ProtoType$ServiceClient + logger $.logr|raw$ +} +` + +var constructorTemplateNamespaced = ` +// new$.type|publicPlural$ returns a $.type|allLowercasePlural$ +func new$.type|publicPlural$(c *$.GroupGoName$$.Version$Client, namespace string) *$.type|allLowercasePlural$ { + return &$.type|allLowercasePlural${ + client: $.NewServiceClient$(c.ClientConn()), + logger: c.logger.WithName("$.type|allLowercasePlural$"), + ns: namespace, + } +} +` + +var constructorTemplateNonNamespaced = ` +// new$.type|publicPlural$ returns a $.type|allLowercasePlural$ +func new$.type|publicPlural$(c *$.GroupGoName$$.Version$Client) *$.type|allLowercasePlural$ { + return &$.type|allLowercasePlural${ + client: $.NewServiceClient$(c.ClientConn()), + logger: c.logger.WithName("$.type|allLowercasePlural$"), + } +} +` + +var listTemplate = ` +func (c *$.type|allLowercasePlural$) List(ctx $.context|raw$, opts $.ListOptions|raw$) (*$.type|raw$List, error) { + resp, err := c.client.List$.ProtoType$s(ctx, &$.pb$.List$.ProtoType$sRequest{ + ResourceVersion: opts.ResourceVersion, + }) + if err != nil { + return nil, err + } + + list := $.FromProtoList|raw$(resp.Get$.ProtoType$List()) + c.logger.V(5).Info("Listed $.type|public$s", + "count", len(list.Items), + "resource-version", list.GetResourceVersion(), + ) + + return list, nil +} +` + +var getTemplate = ` +func (c *$.type|allLowercasePlural$) Get(ctx $.context|raw$, name string, opts $.GetOptions|raw$) (*$.type|raw$, error) { + resp, err := c.client.Get$.ProtoType$(ctx, &$.pb$.Get$.ProtoType$Request{ + Name: name, + }) + if err != nil { + return nil, err + } + + obj := $.FromProto|raw$(resp.Get$.ProtoType$()) + c.logger.V(6).Info("Fetched $.type|public$", + "name", name, + "resource-version", obj.GetResourceVersion(), + ) + + return obj, nil +} +` + +var deleteTemplate = ` +func (c *$.type|allLowercasePlural$) Delete(ctx $.context|raw$, name string, opts $.DeleteOptions|raw$) error { + return $.fmtErrorf|raw$("Delete not implemented for gRPC transport") +} +` + +var createTemplate = ` +func (c *$.type|allLowercasePlural$) Create(ctx $.context|raw$, $.type|allLowercasePlural$ *$.type|raw$, opts $.CreateOptions|raw$) (*$.type|raw$, error) { + return nil, $.fmtErrorf|raw$("Create not implemented for gRPC transport") +} +` + +var updateTemplate = ` +func (c *$.type|allLowercasePlural$) Update(ctx $.context|raw$, $.type|allLowercasePlural$ *$.type|raw$, opts $.UpdateOptions|raw$) (*$.type|raw$, error) { + return nil, $.fmtErrorf|raw$("Update not implemented for gRPC transport") +} +` + +var updateStatusTemplate = ` +func (c *$.type|allLowercasePlural$) UpdateStatus(ctx $.context|raw$, $.type|allLowercasePlural$ *$.type|raw$, opts $.UpdateOptions|raw$) (*$.type|raw$, error) { + return nil, $.fmtErrorf|raw$("UpdateStatus not implemented for gRPC transport") +} +` + +var watchTemplate = ` +func (c *$.type|allLowercasePlural$) Watch(ctx $.context|raw$, opts $.ListOptions|raw$) ($.watchInterface|raw$, error) { + c.logger.V(4).Info("Opening watch stream", + "resource", "$.type|allLowercasePlural$", + "resource-version", opts.ResourceVersion, + ) + + ctx, cancel := context.WithCancel(ctx) + stream, err := c.client.Watch$.ProtoType$s(ctx, &$.pb$.Watch$.ProtoType$sRequest{ + ResourceVersion: opts.ResourceVersion, + }) + if err != nil { + cancel() + return nil, err + } + + return $.nvgrpc|raw$(&$.type|allLowercasePlural$StreamAdapter{stream: stream}, cancel, c.logger), nil +} +` + +var watchAdapterTemplate = ` +// $.type|allLowercasePlural$StreamAdapter wraps the $.type|public$ gRPC stream to provide events. +type $.type|allLowercasePlural$StreamAdapter struct { + stream $.pb$.$.ProtoType$Service_Watch$.ProtoType$sClient +} + +func (a *$.type|allLowercasePlural$StreamAdapter) Next() (string, $.runtime|raw$, error) { + resp, err := a.stream.Recv() + if err != nil { + return "", nil, err + } + + obj := $.FromProto|raw$(resp.GetObject()) + + return resp.GetType(), obj, nil +} + +func (a *$.type|allLowercasePlural$StreamAdapter) Close() error { + return a.stream.CloseSend() +} +` + +var patchTemplate = ` +func (c *$.type|allLowercasePlural$) Patch(ctx $.context|raw$, name string, pt $.PatchType|raw$, data []byte, opts $.PatchOptions|raw$, subresources ...string) (result *$.type|raw$, err error) { + return nil, $.fmtErrorf|raw$("Patch not implemented for gRPC transport") +} +` diff --git a/code-generator/cmd/client-gen/generators/scheme/generator_for_scheme.go b/code-generator/cmd/client-gen/generators/scheme/generator_for_scheme.go new file mode 100644 index 000000000..c5b57a25d --- /dev/null +++ b/code-generator/cmd/client-gen/generators/scheme/generator_for_scheme.go @@ -0,0 +1,195 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Portions Copyright (c) 2025 NVIDIA CORPORATION. All rights reserved. + +Modified from the original to support gRPC transport. +Origin: https://github.com/kubernetes/code-generator/blob/v0.34.1/cmd/client-gen/generators/scheme/generator_for_scheme.go +*/ + +package scheme + +import ( + "fmt" + "io" + "os" + "path" + "path/filepath" + "strings" + + clientgentypes "github.com/nvidia/nvsentinel/code-generator/cmd/client-gen/types" + + "k8s.io/gengo/v2/generator" + "k8s.io/gengo/v2/namer" + "k8s.io/gengo/v2/types" +) + +// GenScheme produces a package for a clientset with the scheme, codecs and parameter codecs. +type GenScheme struct { + generator.GoGenerator + OutputPkg string // Must be a Go import-path + OutputPath string // optional + Groups []clientgentypes.GroupVersions + GroupGoNames map[clientgentypes.GroupVersion]string + InputPackages map[clientgentypes.GroupVersion]string + ImportTracker namer.ImportTracker + PrivateScheme bool + CreateRegistry bool + schemeGenerated bool +} + +func (g *GenScheme) Namers(c *generator.Context) namer.NameSystems { + return namer.NameSystems{ + "raw": namer.NewRawNamer(g.OutputPkg, g.ImportTracker), + } +} + +// We only want to call GenerateType() once. +func (g *GenScheme) Filter(c *generator.Context, t *types.Type) bool { + ret := !g.schemeGenerated + g.schemeGenerated = true + return ret +} + +func (g *GenScheme) Imports(c *generator.Context) (imports []string) { + imports = append(imports, g.ImportTracker.ImportLines()...) + for _, group := range g.Groups { + for _, version := range group.Versions { + packagePath := g.InputPackages[clientgentypes.GroupVersion{Group: group.Group, Version: version.Version}] + groupAlias := strings.ToLower(g.GroupGoNames[clientgentypes.GroupVersion{Group: group.Group, Version: version.Version}]) + if g.CreateRegistry { + // import the install package for internal clientsets instead of the type package with register.go + if version.Version != "" { + packagePath = path.Dir(packagePath) + } + packagePath = path.Join(packagePath, "install") + + imports = append(imports, fmt.Sprintf("%s \"%s\"", groupAlias, packagePath)) + break + } else { + imports = append(imports, fmt.Sprintf("%s%s \"%s\"", groupAlias, strings.ToLower(version.Version.NonEmpty()), packagePath)) + } + } + } + return +} + +func (g *GenScheme) GenerateType(c *generator.Context, t *types.Type, w io.Writer) error { + sw := generator.NewSnippetWriter(w, c, "$", "$") + + allGroupVersions := clientgentypes.ToGroupVersionInfo(g.Groups, g.GroupGoNames) + allInstallGroups := clientgentypes.ToGroupInstallPackages(g.Groups, g.GroupGoNames) + + m := map[string]interface{}{ + "publicScheme": !g.PrivateScheme, + "allGroupVersions": allGroupVersions, + "allInstallGroups": allInstallGroups, + "customRegister": false, + "runtimeNewParameterCodec": c.Universe.Function(types.Name{Package: "k8s.io/apimachinery/pkg/runtime", Name: "NewParameterCodec"}), + "runtimeNewScheme": c.Universe.Function(types.Name{Package: "k8s.io/apimachinery/pkg/runtime", Name: "NewScheme"}), + "serializerNewCodecFactory": c.Universe.Function(types.Name{Package: "k8s.io/apimachinery/pkg/runtime/serializer", Name: "NewCodecFactory"}), + "runtimeScheme": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/runtime", Name: "Scheme"}), + "runtimeSchemeBuilder": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/runtime", Name: "SchemeBuilder"}), + "runtimeUtilMust": c.Universe.Function(types.Name{Package: "k8s.io/apimachinery/pkg/util/runtime", Name: "Must"}), + "schemaGroupVersion": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/runtime/schema", Name: "GroupVersion"}), + "metav1AddToGroupVersion": c.Universe.Function(types.Name{Package: "k8s.io/apimachinery/pkg/apis/meta/v1", Name: "AddToGroupVersion"}), + } + globals := map[string]string{ + "Scheme": "Scheme", + "Codecs": "Codecs", + "ParameterCodec": "ParameterCodec", + "Registry": "Registry", + } + for k, v := range globals { + if g.PrivateScheme { + m[k] = strings.ToLower(v[0:1]) + v[1:] + } else { + m[k] = v + } + } + + sw.Do(globalsTemplate, m) + + if g.OutputPath != "" { + if _, err := os.Stat(filepath.Join(g.OutputPath, strings.ToLower("register_custom.go"))); err == nil { + m["customRegister"] = true + } + } + + if g.CreateRegistry { + sw.Do(registryRegistration, m) + } else { + sw.Do(simpleRegistration, m) + } + + return sw.Error() +} + +var globalsTemplate = ` +var $.Scheme$ = $.runtimeNewScheme|raw$() +var $.Codecs$ = $.serializerNewCodecFactory|raw$($.Scheme$) +$if .publicScheme$var $.ParameterCodec$ = $.runtimeNewParameterCodec|raw$($.Scheme$)$end -$` + +var registryRegistration = ` + +func init() { + $.metav1AddToGroupVersion|raw$($.Scheme$, $.schemaGroupVersion|raw${Version: "v1"}) + Install($.Scheme$) +} + +// Install registers the API group and adds types to a scheme +func Install(scheme *$.runtimeScheme|raw$) { + $- range .allInstallGroups$ + $.InstallPackageAlias$.Install(scheme) + $- end$ + $if .customRegister$ + ExtraInstall(scheme) + $end -$ +} +` + +var simpleRegistration = ` +var localSchemeBuilder = $.runtimeSchemeBuilder|raw${ + $- range .allGroupVersions$ + $.PackageAlias$.AddToScheme, + $- end$ + $if .customRegister$ + ExtraAddToScheme, + $end -$ +} + +// AddToScheme adds all types of this clientset into the given scheme. This allows composition +// of clientsets, like in: +// +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) +// +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// +// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types +// correctly. +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + $.metav1AddToGroupVersion|raw$($.Scheme$, $.schemaGroupVersion|raw${Version: "v1"}) + $.runtimeUtilMust|raw$(AddToScheme($.Scheme$)) +} +` diff --git a/code-generator/cmd/client-gen/generators/util/gvpackages.go b/code-generator/cmd/client-gen/generators/util/gvpackages.go new file mode 100644 index 000000000..302693fea --- /dev/null +++ b/code-generator/cmd/client-gen/generators/util/gvpackages.go @@ -0,0 +1,36 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Copied unmodified from the original. +Reason: The upstream package is internal and cannot be imported. +Origin: https://github.com/kubernetes/code-generator/blob/v0.34.1/cmd/client-gen/generators/util/gvpackages.go +*/ + +package util + +import "strings" + +func ParsePathGroupVersion(pgvString string) (gvPath string, gvString string) { + subs := strings.Split(pgvString, "/") + length := len(subs) + switch length { + case 0, 1, 2: + return "", pgvString + default: + return strings.Join(subs[:length-2], "/"), strings.Join(subs[length-2:], "/") + } +} diff --git a/code-generator/cmd/client-gen/generators/util/tags.go b/code-generator/cmd/client-gen/generators/util/tags.go new file mode 100644 index 000000000..b092d0046 --- /dev/null +++ b/code-generator/cmd/client-gen/generators/util/tags.go @@ -0,0 +1,350 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Copied unmodified from the original. +Reason: The upstream package is internal and cannot be imported. +Origin: https://github.com/kubernetes/code-generator/blob/v0.34.1/cmd/client-gen/generators/util/tags.go +*/ + +package util + +import ( + "errors" + "fmt" + "strings" + + "k8s.io/gengo/v2" +) + +var supportedTags = []string{ + "genclient", + "genclient:nonNamespaced", + "genclient:noVerbs", + "genclient:onlyVerbs", + "genclient:skipVerbs", + "genclient:noStatus", + "genclient:readonly", + "genclient:method", +} + +// SupportedVerbs is a list of supported verbs for +onlyVerbs and +skipVerbs. +var SupportedVerbs = []string{ + "create", + "update", + "updateStatus", + "delete", + "deleteCollection", + "get", + "list", + "watch", + "patch", + "apply", + "applyStatus", +} + +// ReadonlyVerbs represents a list of read-only verbs. +var ReadonlyVerbs = []string{ + "get", + "list", + "watch", +} + +// genClientPrefix is the default prefix for all genclient tags. +const genClientPrefix = "genclient:" + +// unsupportedExtensionVerbs is a list of verbs we don't support generating +// extension client functions for. +var unsupportedExtensionVerbs = []string{ + "updateStatus", + "deleteCollection", + "watch", + "delete", +} + +// inputTypeSupportedVerbs is a list of verb types that supports overriding the +// input argument type. +var inputTypeSupportedVerbs = []string{ + "create", + "update", + "apply", +} + +// resultTypeSupportedVerbs is a list of verb types that supports overriding the +// resulting type. +var resultTypeSupportedVerbs = []string{ + "create", + "update", + "get", + "list", + "patch", + "apply", +} + +// Extensions allows to extend the default set of client verbs +// (CRUD+watch+patch+list+deleteCollection) for a given type with custom defined +// verbs. Custom verbs can have custom input and result types and also allow to +// use a sub-resource in a request instead of top-level resource type. +// +// Example: +// +// +genclient:method=UpdateScale,verb=update,subresource=scale,input=Scale,result=Scale +// +// type ReplicaSet struct { ... } +// +// The 'method=UpdateScale' is the name of the client function. +// The 'verb=update' here means the client function will use 'PUT' action. +// The 'subresource=scale' means we will use SubResource template to generate this client function. +// The 'input' is the input type used for creation (function argument). +// The 'result' (not needed in this case) is the result type returned from the +// client function. +type extension struct { + // VerbName is the name of the custom verb (Scale, Instantiate, etc..) + VerbName string + // VerbType is the type of the verb (only verbs from SupportedVerbs are + // supported) + VerbType string + // SubResourcePath defines a path to a sub-resource to use in the request. + // (optional) + SubResourcePath string + // InputTypeOverride overrides the input parameter type for the verb. By + // default the original type is used. Overriding the input type only works for + // "create" and "update" verb types. The given type must exists in the same + // package as the original type. + // (optional) + InputTypeOverride string + // ResultTypeOverride overrides the resulting object type for the verb. By + // default the original type is used. Overriding the result type works. + // (optional) + ResultTypeOverride string +} + +// IsSubresource indicates if this extension should generate the sub-resource. +func (e *extension) IsSubresource() bool { + return len(e.SubResourcePath) > 0 +} + +// HasVerb checks if the extension matches the given verb. +func (e *extension) HasVerb(verb string) bool { + return e.VerbType == verb +} + +// Input returns the input override package path and the type. +func (e *extension) Input() (string, string) { + parts := strings.Split(e.InputTypeOverride, ".") + return parts[len(parts)-1], strings.Join(parts[0:len(parts)-1], ".") +} + +// Result returns the result override package path and the type. +func (e *extension) Result() (string, string) { + parts := strings.Split(e.ResultTypeOverride, ".") + return parts[len(parts)-1], strings.Join(parts[0:len(parts)-1], ".") +} + +// Tags represents a genclient configuration for a single type. +type Tags struct { + // +genclient + GenerateClient bool + // +genclient:nonNamespaced + NonNamespaced bool + // +genclient:noStatus + NoStatus bool + // +genclient:noVerbs + NoVerbs bool + // +genclient:skipVerbs=get,update + // +genclient:onlyVerbs=create,delete + SkipVerbs []string + // +genclient:method=UpdateScale,verb=update,subresource=scale,input=Scale,result=Scale + Extensions []extension +} + +// HasVerb returns true if we should include the given verb in final client interface and +// generate the function for it. +func (t Tags) HasVerb(verb string) bool { + if len(t.SkipVerbs) == 0 { + return true + } + for _, s := range t.SkipVerbs { + if verb == s { + return false + } + } + return true +} + +// MustParseClientGenTags calls ParseClientGenTags but instead of returning error it panics. +func MustParseClientGenTags(lines []string) Tags { + tags, err := ParseClientGenTags(lines) + if err != nil { + panic(err.Error()) + } + return tags +} + +// ParseClientGenTags parse the provided genclient tags and validates that no unknown +// tags are provided. +func ParseClientGenTags(lines []string) (Tags, error) { + ret := Tags{} + values := gengo.ExtractCommentTags("+", lines) + var value []string + value, ret.GenerateClient = values["genclient"] + // Check the old format and error when used to avoid generating client when //+genclient=false + if len(value) > 0 && len(value[0]) > 0 { + return ret, fmt.Errorf("+genclient=%s is invalid, use //+genclient if you want to generate client or omit it when you want to disable generation", value) + } + _, ret.NonNamespaced = values[genClientPrefix+"nonNamespaced"] + // Check the old format and error when used + if value := values["nonNamespaced"]; len(value) > 0 && len(value[0]) > 0 { + return ret, fmt.Errorf("+nonNamespaced=%s is invalid, use //+genclient:nonNamespaced instead", value[0]) + } + _, ret.NoVerbs = values[genClientPrefix+"noVerbs"] + _, ret.NoStatus = values[genClientPrefix+"noStatus"] + onlyVerbs := []string{} + if _, isReadonly := values[genClientPrefix+"readonly"]; isReadonly { + onlyVerbs = ReadonlyVerbs + } + // Check the old format and error when used + if value := values["readonly"]; len(value) > 0 && len(value[0]) > 0 { + return ret, fmt.Errorf("+readonly=%s is invalid, use //+genclient:readonly instead", value[0]) + } + if v, exists := values[genClientPrefix+"skipVerbs"]; exists { + ret.SkipVerbs = strings.Split(v[0], ",") + } + if v, exists := values[genClientPrefix+"onlyVerbs"]; exists || len(onlyVerbs) > 0 { + if len(v) > 0 { + onlyVerbs = append(onlyVerbs, strings.Split(v[0], ",")...) + } + skipVerbs := []string{} + for _, m := range SupportedVerbs { + skip := true + for _, o := range onlyVerbs { + if o == m { + skip = false + break + } + } + // Check for conflicts + for _, v := range skipVerbs { + if v == m { + return ret, fmt.Errorf("verb %q used both in genclient:skipVerbs and genclient:onlyVerbs", v) + } + } + if skip { + skipVerbs = append(skipVerbs, m) + } + } + ret.SkipVerbs = skipVerbs + } + var err error + if ret.Extensions, err = parseClientExtensions(values); err != nil { + return ret, err + } + return ret, validateClientGenTags(values) +} + +func parseClientExtensions(tags map[string][]string) ([]extension, error) { + var ret []extension + for name, values := range tags { + if !strings.HasPrefix(name, genClientPrefix+"method") { + continue + } + for _, value := range values { + // the value comes in this form: "Foo,verb=create" + ext := extension{} + parts := strings.Split(value, ",") + if len(parts) == 0 { + return nil, fmt.Errorf("invalid of empty extension verb name: %q", value) + } + // The first part represents the name of the extension + ext.VerbName = parts[0] + if len(ext.VerbName) == 0 { + return nil, fmt.Errorf("must specify a verb name (// +genclient:method=Foo,verb=create)") + } + // Parse rest of the arguments + params := parts[1:] + for _, p := range params { + parts := strings.Split(p, "=") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid extension tag specification %q", p) + } + key, val := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) + if len(val) == 0 { + return nil, fmt.Errorf("empty value of %q for %q extension", key, ext.VerbName) + } + switch key { + case "verb": + ext.VerbType = val + case "subresource": + ext.SubResourcePath = val + case "input": + ext.InputTypeOverride = val + case "result": + ext.ResultTypeOverride = val + default: + return nil, fmt.Errorf("unknown extension configuration key %q", key) + } + } + // Validate resulting extension configuration + if len(ext.VerbType) == 0 { + return nil, fmt.Errorf("verb type must be specified (use '// +genclient:method=%s,verb=create')", ext.VerbName) + } + if len(ext.ResultTypeOverride) > 0 { + supported := false + for _, v := range resultTypeSupportedVerbs { + if ext.VerbType == v { + supported = true + break + } + } + if !supported { + return nil, fmt.Errorf("%s: result type is not supported for %q verbs (supported verbs: %#v)", ext.VerbName, ext.VerbType, resultTypeSupportedVerbs) + } + } + if len(ext.InputTypeOverride) > 0 { + supported := false + for _, v := range inputTypeSupportedVerbs { + if ext.VerbType == v { + supported = true + break + } + } + if !supported { + return nil, fmt.Errorf("%s: input type is not supported for %q verbs (supported verbs: %#v)", ext.VerbName, ext.VerbType, inputTypeSupportedVerbs) + } + } + for _, t := range unsupportedExtensionVerbs { + if ext.VerbType == t { + return nil, fmt.Errorf("verb %q is not supported by extension generator", ext.VerbType) + } + } + ret = append(ret, ext) + } + } + return ret, nil +} + +// validateTags validates that only supported genclient tags were provided. +func validateClientGenTags(values map[string][]string) error { + for _, k := range supportedTags { + delete(values, k) + } + for key := range values { + if strings.HasPrefix(key, strings.TrimSuffix(genClientPrefix, ":")) { + return errors.New("unknown tag detected: " + key) + } + } + return nil +} diff --git a/code-generator/cmd/client-gen/generators/util/tags_test.go b/code-generator/cmd/client-gen/generators/util/tags_test.go new file mode 100644 index 000000000..4a433e639 --- /dev/null +++ b/code-generator/cmd/client-gen/generators/util/tags_test.go @@ -0,0 +1,154 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Copied unmodified from the original. +Reason: The upstream package is internal and cannot be imported. +Origin: https://github.com/kubernetes/code-generator/blob/v0.34.1/cmd/client-gen/generators/util/tags_test.go +*/ + +package util + +import ( + "reflect" + "testing" +) + +func TestParseTags(t *testing.T) { + testCases := map[string]struct { + lines []string + expectTags Tags + expectError bool + }{ + "genclient": { + lines: []string{`+genclient`}, + expectTags: Tags{GenerateClient: true}, + }, + "genclient=true": { + lines: []string{`+genclient=true`}, + expectError: true, + }, + "nonNamespaced=true": { + lines: []string{`+genclient=true`, `+nonNamespaced=true`}, + expectError: true, + }, + "readonly=true": { + lines: []string{`+genclient=true`, `+readonly=true`}, + expectError: true, + }, + "genclient:nonNamespaced": { + lines: []string{`+genclient`, `+genclient:nonNamespaced`}, + expectTags: Tags{GenerateClient: true, NonNamespaced: true}, + }, + "genclient:noVerbs": { + lines: []string{`+genclient`, `+genclient:noVerbs`}, + expectTags: Tags{GenerateClient: true, NoVerbs: true}, + }, + "genclient:noStatus": { + lines: []string{`+genclient`, `+genclient:noStatus`}, + expectTags: Tags{GenerateClient: true, NoStatus: true}, + }, + "genclient:onlyVerbs": { + lines: []string{`+genclient`, `+genclient:onlyVerbs=create,delete`}, + expectTags: Tags{GenerateClient: true, SkipVerbs: []string{"update", "updateStatus", "deleteCollection", "get", "list", "watch", "patch", "apply", "applyStatus"}}, + }, + "genclient:readonly": { + lines: []string{`+genclient`, `+genclient:readonly`}, + expectTags: Tags{GenerateClient: true, SkipVerbs: []string{"create", "update", "updateStatus", "delete", "deleteCollection", "patch", "apply", "applyStatus"}}, + }, + "genclient:conflict": { + lines: []string{`+genclient`, `+genclient:onlyVerbs=create`, `+genclient:skipVerbs=create`}, + expectError: true, + }, + "genclient:invalid": { + lines: []string{`+genclient`, `+genclient:invalid`}, + expectError: true, + }, + } + for key, c := range testCases { + result, err := ParseClientGenTags(c.lines) + if err != nil && !c.expectError { + t.Fatalf("unexpected error: %v", err) + } + if !c.expectError && !reflect.DeepEqual(result, c.expectTags) { + t.Errorf("[%s] expected %#v to be %#v", key, result, c.expectTags) + } + } +} + +func TestParseTagsExtension(t *testing.T) { + testCases := map[string]struct { + lines []string + expectedExtensions []extension + expectError bool + }{ + "simplest extension": { + lines: []string{`+genclient:method=Foo,verb=create`}, + expectedExtensions: []extension{{VerbName: "Foo", VerbType: "create"}}, + }, + "multiple extensions": { + lines: []string{`+genclient:method=Foo,verb=create`, `+genclient:method=Bar,verb=get`}, + expectedExtensions: []extension{{VerbName: "Foo", VerbType: "create"}, {VerbName: "Bar", VerbType: "get"}}, + }, + "extension without verb": { + lines: []string{`+genclient:method`}, + expectError: true, + }, + "extension without verb type": { + lines: []string{`+genclient:method=Foo`}, + expectError: true, + }, + "sub-resource extension": { + lines: []string{`+genclient:method=Foo,verb=create,subresource=bar`}, + expectedExtensions: []extension{{VerbName: "Foo", VerbType: "create", SubResourcePath: "bar"}}, + }, + "output type extension": { + lines: []string{`+genclient:method=Foos,verb=list,result=Bars`}, + expectedExtensions: []extension{{VerbName: "Foos", VerbType: "list", ResultTypeOverride: "Bars"}}, + }, + "input type extension": { + lines: []string{`+genclient:method=Foo,verb=update,input=Bar`}, + expectedExtensions: []extension{{VerbName: "Foo", VerbType: "update", InputTypeOverride: "Bar"}}, + }, + "unknown verb type extension": { + lines: []string{`+genclient:method=Foo,verb=explode`}, + expectedExtensions: nil, + expectError: true, + }, + "invalid verb extension": { + lines: []string{`+genclient:method=Foo,unknown=bar`}, + expectedExtensions: nil, + expectError: true, + }, + "empty verb extension subresource": { + lines: []string{`+genclient:method=Foo,verb=get,subresource=`}, + expectedExtensions: nil, + expectError: true, + }, + } + for key, c := range testCases { + result, err := ParseClientGenTags(c.lines) + if err != nil && !c.expectError { + t.Fatalf("[%s] unexpected error: %v", key, err) + } + if err != nil && c.expectError { + t.Logf("[%s] got expected error: %+v", key, err) + } + if !c.expectError && !reflect.DeepEqual(result.Extensions, c.expectedExtensions) { + t.Errorf("[%s] expected %#+v to be %#+v", key, result.Extensions, c.expectedExtensions) + } + } +} diff --git a/code-generator/cmd/client-gen/main.go b/code-generator/cmd/client-gen/main.go new file mode 100644 index 000000000..320af1cbc --- /dev/null +++ b/code-generator/cmd/client-gen/main.go @@ -0,0 +1,78 @@ +/* +Copyright 2015 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Portions Copyright (c) 2025 NVIDIA CORPORATION. All rights reserved. + +Modified from the original to support gRPC transport. +Origin: https://github.com/kubernetes/code-generator/blob/v0.34.1/cmd/client-gen/main.go +*/ + +// client-gen makes the individual typed clients using gengo. +package main + +import ( + "flag" + "slices" + + "github.com/spf13/pflag" + "k8s.io/klog/v2" + + "github.com/nvidia/nvsentinel/code-generator/cmd/client-gen/args" + "github.com/nvidia/nvsentinel/code-generator/cmd/client-gen/generators" + + "k8s.io/code-generator/pkg/util" + "k8s.io/gengo/v2" + "k8s.io/gengo/v2/generator" +) + +func main() { + klog.InitFlags(nil) + args := args.New() + + args.AddFlags(pflag.CommandLine, "k8s.io/kubernetes/pkg/apis") // TODO: move this input path out of client-gen + flag.Set("logtostderr", "true") + pflag.CommandLine.AddGoFlagSet(flag.CommandLine) + pflag.Parse() + + // add group version package as input dirs for gengo + inputPkgs := []string{} + for _, pkg := range args.Groups { + for _, v := range pkg.Versions { + inputPkgs = append(inputPkgs, v.Package) + } + } + // ensure stable code generation output + slices.Sort(inputPkgs) + + if err := args.Validate(); err != nil { + klog.Fatalf("Error: %v", err) + } + + myTargets := func(context *generator.Context) []generator.Target { + return generators.GetTargets(context, args) + } + + if err := gengo.Execute( + generators.NameSystems(util.PluralExceptionListToMapOrDie(args.PluralExceptions)), + generators.DefaultNameSystem(), + myTargets, + gengo.StdBuildTag, + inputPkgs, + ); err != nil { + klog.Fatalf("Error: %v", err) + } +} diff --git a/code-generator/cmd/client-gen/types/helpers.go b/code-generator/cmd/client-gen/types/helpers.go new file mode 100644 index 000000000..08ea0cda2 --- /dev/null +++ b/code-generator/cmd/client-gen/types/helpers.go @@ -0,0 +1,127 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Copied unmodified from the original. +Reason: The upstream package is internal and cannot be imported. +Origin: https://github.com/kubernetes/code-generator/blob/v0.34.1/cmd/client-gen/types/helpers.go +*/ + +package types + +import ( + "fmt" + "regexp" + "sort" + "strings" + + "k8s.io/gengo/v2/namer" +) + +// ToGroupVersion turns "group/version" string into a GroupVersion struct. It reports error +// if it cannot parse the string. +func ToGroupVersion(gv string) (GroupVersion, error) { + // this can be the internal version for the legacy kube types + // TODO once we've cleared the last uses as strings, this special case should be removed. + if (len(gv) == 0) || (gv == "/") { + return GroupVersion{}, nil + } + + switch strings.Count(gv, "/") { + case 0: + return GroupVersion{Group(gv), ""}, nil + case 1: + i := strings.Index(gv, "/") + return GroupVersion{Group(gv[:i]), Version(gv[i+1:])}, nil + default: + return GroupVersion{}, fmt.Errorf("unexpected GroupVersion string: %v", gv) + } +} + +type sortableSliceOfVersions []string + +func (a sortableSliceOfVersions) Len() int { return len(a) } +func (a sortableSliceOfVersions) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a sortableSliceOfVersions) Less(i, j int) bool { + vi, vj := strings.TrimLeft(a[i], "v"), strings.TrimLeft(a[j], "v") + major := regexp.MustCompile("^[0-9]+") + viMajor, vjMajor := major.FindString(vi), major.FindString(vj) + viRemaining, vjRemaining := strings.TrimLeft(vi, viMajor), strings.TrimLeft(vj, vjMajor) + switch { + case len(viRemaining) == 0 && len(vjRemaining) == 0: + return viMajor < vjMajor + case len(viRemaining) == 0 && len(vjRemaining) != 0: + // stable version is greater than unstable version + return false + case len(viRemaining) != 0 && len(vjRemaining) == 0: + // stable version is greater than unstable version + return true + } + // neither are stable versions + if viMajor != vjMajor { + return viMajor < vjMajor + } + // assuming at most we have one alpha or one beta version, so if vi contains "alpha", it's the lesser one. + return strings.Contains(viRemaining, "alpha") +} + +// Determine the default version among versions. If a user calls a group client +// without specifying the version (e.g., c.CoreV1(), instead of c.CoreV1()), the +// default version will be returned. +func defaultVersion(versions []PackageVersion) Version { + var versionStrings []string + for _, version := range versions { + versionStrings = append(versionStrings, version.Version.String()) + } + sort.Sort(sortableSliceOfVersions(versionStrings)) + return Version(versionStrings[len(versionStrings)-1]) +} + +// ToGroupVersionInfo is a helper function used by generators for groups. +func ToGroupVersionInfo(groups []GroupVersions, groupGoNames map[GroupVersion]string) []GroupVersionInfo { + var groupVersionPackages []GroupVersionInfo + for _, group := range groups { + for _, version := range group.Versions { + groupGoName := groupGoNames[GroupVersion{Group: group.Group, Version: version.Version}] + groupVersionPackages = append(groupVersionPackages, GroupVersionInfo{ + Group: Group(namer.IC(group.Group.NonEmpty())), + Version: Version(namer.IC(version.Version.String())), + PackageAlias: strings.ToLower(groupGoName + version.Version.NonEmpty()), + GroupGoName: groupGoName, + LowerCaseGroupGoName: namer.IL(groupGoName), + }) + } + } + return groupVersionPackages +} + +func ToGroupInstallPackages(groups []GroupVersions, groupGoNames map[GroupVersion]string) []GroupInstallPackage { + var groupInstallPackages []GroupInstallPackage + for _, group := range groups { + defaultVersion := defaultVersion(group.Versions) + groupGoName := groupGoNames[GroupVersion{Group: group.Group, Version: defaultVersion}] + groupInstallPackages = append(groupInstallPackages, GroupInstallPackage{ + Group: Group(namer.IC(group.Group.NonEmpty())), + InstallPackageAlias: strings.ToLower(groupGoName), + }) + } + return groupInstallPackages +} + +// NormalizeGroupVersion calls normalizes the GroupVersion. +// func NormalizeGroupVersion(gv GroupVersion) GroupVersion { +// return GroupVersion{Group: gv.Group.NonEmpty(), Version: gv.Version, NonEmptyVersion: normalization.Version(gv.Version)} +// } diff --git a/code-generator/cmd/client-gen/types/helpers_test.go b/code-generator/cmd/client-gen/types/helpers_test.go new file mode 100644 index 000000000..3917b1d60 --- /dev/null +++ b/code-generator/cmd/client-gen/types/helpers_test.go @@ -0,0 +1,38 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Copied unmodified from the original. +Reason: The upstream package is internal and cannot be imported. +Origin: https://github.com/kubernetes/code-generator/blob/v0.34.1/cmd/client-gen/types/helpers_test.go +*/ + +package types + +import ( + "reflect" + "sort" + "testing" +) + +func TestVersionSort(t *testing.T) { + unsortedVersions := []string{"v4beta1", "v2beta1", "v2alpha1", "v3", "v1"} + expected := []string{"v2alpha1", "v2beta1", "v4beta1", "v1", "v3"} + sort.Sort(sortableSliceOfVersions(unsortedVersions)) + if !reflect.DeepEqual(unsortedVersions, expected) { + t.Errorf("expected %#v\ngot %#v", expected, unsortedVersions) + } +} diff --git a/code-generator/cmd/client-gen/types/types.go b/code-generator/cmd/client-gen/types/types.go new file mode 100644 index 000000000..20e8c5708 --- /dev/null +++ b/code-generator/cmd/client-gen/types/types.go @@ -0,0 +1,141 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Portions Copyright (c) 2025 NVIDIA CORPORATION. All rights reserved. + +Modified from the original to support gRPC transport. +Origin: https://github.com/kubernetes/code-generator/blob/v0.34.1/cmd/client-gen/types/types.go +*/ + +package types + +import ( + "fmt" + "path" + "strings" +) + +type Version string + +func (v Version) String() string { + return string(v) +} + +func (v Version) NonEmpty() string { + if v == "" { + return "internalVersion" + } + return v.String() +} + +func (v Version) PackageName() string { + return strings.ToLower(v.NonEmpty()) +} + +type Group string + +func (g Group) String() string { + return string(g) +} + +func (g Group) NonEmpty() string { + if g == "" { + return "core" + } + return string(g) +} + +func (g Group) PackageName() string { + parts := strings.Split(g.NonEmpty(), ".") + if parts[0] == "internal" && len(parts) > 1 { + return strings.ToLower(parts[1] + parts[0]) + } + return strings.ToLower(parts[0]) +} + +type Kind string + +type PackageVersion struct { + Version + // The fully qualified package, e.g. k8s.io/kubernetes/pkg/apis/apps, where the types.go is found. + Package string +} + +type GroupVersion struct { + Group Group + Version Version +} + +type GroupVersionKind struct { + Group Group + Version Version + Kind Kind +} + +func (gv GroupVersion) ToAPIVersion() string { + if len(gv.Group) > 0 && gv.Group != "" { + return gv.Group.String() + "/" + gv.Version.String() + } else { + return gv.Version.String() + } +} + +func (gv GroupVersion) WithKind(kind Kind) GroupVersionKind { + return GroupVersionKind{Group: gv.Group, Version: gv.Version, Kind: kind} +} + +type GroupVersions struct { + // The name of the package for this group, e.g. apps. + PackageName string + Group Group + Versions []PackageVersion +} + +// GroupVersionInfo contains all the info around a group version. +type GroupVersionInfo struct { + Group Group + Version Version + PackageAlias string + GroupGoName string + LowerCaseGroupGoName string +} + +type GroupInstallPackage struct { + Group Group + InstallPackageAlias string +} + +// ProtobufPackage contains the Go package containing protobuf stubs. +type ProtobufPackage struct { + Alias string + Package string // The Go import-path +} + +func NewProtobufPackage(protoBase string, groupPkgName string, version string) ProtobufPackage { + return ProtobufPackage{ + Alias: "pb", + Package: path.Join(protoBase, groupPkgName, version), + } +} + +func (p ProtobufPackage) ImportLines() []string { + return []string{fmt.Sprintf("%s \"%s\"", p.Alias, p.Package)} +} + +func (p ProtobufPackage) ServiceClientConstructorFor(protoType string) string { + return fmt.Sprintf("%s.New%sServiceClient", p.Alias, protoType) +} diff --git a/code-generator/go.mod b/code-generator/go.mod new file mode 100644 index 000000000..0355c270b --- /dev/null +++ b/code-generator/go.mod @@ -0,0 +1,18 @@ +module github.com/nvidia/nvsentinel/code-generator + +go 1.25.5 + +require ( + github.com/spf13/pflag v1.0.10 + golang.org/x/text v0.23.0 + k8s.io/code-generator v0.34.1 + k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b + k8s.io/klog/v2 v2.130.1 +) + +require ( + github.com/go-logr/logr v1.4.3 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/tools v0.38.0 // indirect +) diff --git a/code-generator/go.sum b/code-generator/go.sum new file mode 100644 index 000000000..2d26133e8 --- /dev/null +++ b/code-generator/go.sum @@ -0,0 +1,20 @@ +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +k8s.io/code-generator v0.34.1 h1:WpphT26E+j7tEgIUfFr5WfbJrktCGzB3JoJH9149xYc= +k8s.io/code-generator v0.34.1/go.mod h1:DeWjekbDnJWRwpw3s0Jat87c+e0TgkxoR4ar608yqvg= +k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b h1:gMplByicHV/TJBizHd9aVEsTYoJBnnUAT5MHlTkbjhQ= +k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= diff --git a/code-generator/kube_codegen.sh b/code-generator/kube_codegen.sh new file mode 100755 index 000000000..6e845c2e7 --- /dev/null +++ b/code-generator/kube_codegen.sh @@ -0,0 +1,737 @@ +#!/usr/bin/env bash + +# Copyright 2023 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Portions Copyright (c) 2025 NVIDIA CORPORATION. All rights reserved. +# +# Modified from the original to support gRPC transport. +# Origin: https://github.com/kubernetes/code-generator/blob/v0.34.1/kube_codegen.sh + +set -o errexit +set -o nounset +set -o pipefail + +KUBE_CODEGEN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" + +function kube::codegen::internal::get_goversion() { + version=$(grep '^go ' "${KUBE_CODEGEN_ROOT}/go.mod" | awk '{print $2}') + version=${version:-1.25} + echo "$version" +} + +function kube::codegen::internal::get_version() { + local key="$1" + local versions_file="${KUBE_CODEGEN_ROOT}/../.versions.yaml" + if [[ -f "${versions_file}" ]]; then + grep "${key}:" "${versions_file}" | sed -E 's/.*: *//' | tr -d " \"'" || true + fi +} + +if [[ -z "${KUBE_CODEGEN_TAG:-}" ]]; then + if version=$(kube::codegen::internal::get_version "kubernetes_code_gen"); then + KUBE_CODEGEN_TAG="${version}" + fi +fi + +# Callers which want a specific tag of the k8s.io/code-generator repo should +# set the KUBE_CODEGEN_TAG to the tag name, e.g. KUBE_CODEGEN_TAG="release-1.32" +# before sourcing this file. +CODEGEN_VERSION_SPEC="${KUBE_CODEGEN_TAG:+"@${KUBE_CODEGEN_TAG}"}" + +if [[ -z "${PROTOC_GEN_GO_TAG:-}" ]]; then + if version=$(kube::codegen::internal::get_version "protoc_gen_go"); then + PROTOC_GEN_GO_TAG="${version}" + fi +fi + +# Callers which want a specific tag of the google.golang.org/protobuf repo should +# set the PROTOC_GEN_GO_TAG to the tag name, e.g. PROTOC_GEN_GO_TAG="v1.36.10" +# before sourcing this file. +PROTOC_GEN_GO_VERSION_SPEC="${PROTOC_GEN_GO_TAG:+"@${PROTOC_GEN_GO_TAG}"}" + +if [[ -z "${PROTOC_GEN_GO_GRPC_TAG:-}" ]]; then + if version=$(kube::codegen::internal::get_version "protoc_gen_go_grpc"); then + PROTOC_GEN_GO_GRPC_TAG="${version}" + fi +fi + +# Callers which want a specific tag of the google.golang.org/grpc repo should +# set the PROTOC_GEN_GO_GRPC_TAG to the tag name, e.g. PROTOC_GEN_GO_GRPC_TAG="v1.5.1" +# before sourcing this file. +PROTOC_GEN_GO_GRPC_VERSION_SPEC="${PROTOC_GEN_GO_GRPC_TAG:+"@${PROTOC_GEN_GO_GRPC_TAG}"}" + +if [[ -z "${GOVERTER_TAG:-}" ]]; then + if version=$(kube::codegen::internal::get_version "goverter"); then + GOVERTER_TAG="${version}" + fi +fi + +# Callers which want a specific tag of the x repo should +# set the GOVERTER_TAG to the tag name, e.g. GOVERTER_TAG="v1.9.2" +# before sourcing this file. +GOVERTER_VERSION_SPEC="${GOVERTER_TAG:+"@${GOVERTER_TAG}"}" + +# Go installs in $GOBIN if defined, and $GOPATH/bin otherwise. We want to know +# which one it is, so we can use it later. +function get_gobin() { + local from_env + from_env="$(go env GOBIN)" + if [[ -n "${from_env}" ]]; then + echo "${from_env}" + else + echo "$(go env GOPATH)/bin" + fi +} +GOBIN="$(get_gobin)" +export GOBIN + +function kube::codegen::internal::findz() { + # We use `find` rather than `git ls-files` because sometimes external + # projects use this across repos. This is an imperfect wrapper of find, + # but good enough for this script. + find "$@" -print0 +} + +function kube::codegen::internal::grep() { + # We use `grep` rather than `git grep` because sometimes external projects + # use this across repos. + grep "$@" \ + --exclude-dir .git \ + --exclude-dir _output \ + --exclude-dir vendor +} + +# Generate protobuf bindings +# +# USAGE: kube::codegen::gen_proto_bindings [FLAGS] +# +# +# The root directory under which to search for Protobuf files to generate +# bindings for. This must be a local path, not a Go package. +# +# FLAGS: +# +# --output-dir +# The relative path under which to emit code. +# +# --proto-root +# The relative path under which to search for Protobuf definitions. +function kube::codegen::gen_proto_bindings(){ + local in_dir="" + local out_dir="gen/go" + local proto_root="proto" + local v="${KUBE_VERBOSE:-0}" + + while [ "$#" -gt 0 ]; do + case "$1" in + "--output-dir") + out_dir="$2" + shift 2 + ;; + "--proto-root") + proto_root="$2" + shift 2 + ;; + *) + if [[ "$1" =~ ^-- ]]; then + echo "unknown argument: $1" >&2 + return 1 + fi + if [ -n "$in_dir" ]; then + echo "too many arguments: $1 (already have $in_dir)" >&2 + return 1 + fi + in_dir="$1" + shift + ;; + esac + done + + if [ -z "${in_dir}" ]; then + echo "input-dir argument is required" >&2 + return 1 + fi + + ( + # To support running this from anywhere, first cd into this directory, + # and then install with forced module mode on and fully qualified name. + cd "${KUBE_CODEGEN_ROOT}" + UPSTREAM_BINS=( + google.golang.org/protobuf/cmd/protoc-gen-go"${PROTOC_GEN_GO_VERSION_SPEC}" + google.golang.org/grpc/cmd/protoc-gen-go-grpc"${PROTOC_GEN_GO_GRPC_VERSION_SPEC}" + ) + echo "Installing upstream generators..." + for bin in "${UPSTREAM_BINS[@]}"; do + echo " - ${bin}" + GO111MODULE=on go install "${bin}" + done + ) + + # Go bindings + # + local input_versions=() + while read -r dir; do + local version="${dir#"${in_dir}/${proto_root}/"}" + input_versions+=("${version}") + done < <( + ( kube::codegen::internal::findz \ + "${in_dir}/${proto_root}" \ + -type f \ + -name '*.proto' \ + || true \ + ) | while read -r -d $'\0' F; do dirname "${F}"; done \ + | LC_ALL=C sort -u + ) + + if [ "${#input_versions[@]}" != 0 ]; then + echo "Generating Go protobuf bindings for ${#input_versions[@]} targets" + + for version in "${input_versions[@]}"; do + if [ -d "${in_dir}/${out_dir}/${version}" ]; then + ( kube::codegen::internal::findz \ + "${in_dir}/${out_dir}/${version}" \ + -maxdepth 1 \ + -type f \ + -name '*.pb.go' \ + || true \ + ) | xargs -0 rm -f + fi + done + + ( + cd "${in_dir}/${proto_root}" + for version in "${input_versions[@]}"; do + mkdir -p "../${out_dir}/${version}" + protoc \ + -I . \ + --plugin="protoc-gen-go=${GOBIN}/protoc-gen-go" \ + --plugin="protoc-gen-go-grpc=${GOBIN}/protoc-gen-go-grpc" \ + --go_out="../${out_dir}" \ + --go_opt=paths="source_relative" \ + --go-grpc_out="../${out_dir}" \ + --go-grpc_opt=paths="source_relative" \ + "${version}"/*.proto + done + ) + fi +} + +# Generate tagged helper code: conversions, deepcopy, defaults and validations +# +# USAGE: kube::codegen::gen_helpers [FLAGS] +# +# +# The root directory under which to search for Go files which request code to +# be generated. This must be a local path, not a Go package. +# +# See note at the top about package structure below that. +# +# FLAGS: +# +# --boilerplate +# An optional override for the header file to insert into generated files. +# +# --extra-peer-dir +# An optional list (this flag may be specified multiple times) of "extra" +# directories to consider during conversion generation. +# +function kube::codegen::gen_helpers() { + local in_dir="" + local boilerplate="${KUBE_CODEGEN_ROOT}/hack/boilerplate.go.txt" + local v="${KUBE_VERBOSE:-0}" + local extra_peers=() + + while [ "$#" -gt 0 ]; do + case "$1" in + "--boilerplate") + boilerplate="$2" + shift 2 + ;; + "--extra-peer-dir") + extra_peers+=("$2") + shift 2 + ;; + *) + if [[ "$1" =~ ^-- ]]; then + echo "unknown argument: $1" >&2 + return 1 + fi + if [ -n "$in_dir" ]; then + echo "too many arguments: $1 (already have $in_dir)" >&2 + return 1 + fi + in_dir="$1" + shift + ;; + esac + done + + if [ -z "${in_dir}" ]; then + echo "input-dir argument is required" >&2 + return 1 + fi + + ( + # To support running this from anywhere, first cd into this directory, + # and then install with forced module mode on and fully qualified name. + cd "${KUBE_CODEGEN_ROOT}" + UPSTREAM_BINS=( + "k8s.io/code-generator/cmd/conversion-gen${CODEGEN_VERSION_SPEC}" + "k8s.io/code-generator/cmd/deepcopy-gen${CODEGEN_VERSION_SPEC}" + "k8s.io/code-generator/cmd/defaulter-gen${CODEGEN_VERSION_SPEC}" + "k8s.io/code-generator/cmd/validation-gen${CODEGEN_VERSION_SPEC}" + ) + echo "Installing upstream generators..." + for bin in "${UPSTREAM_BINS[@]}"; do + echo " - ${bin}" + done + # shellcheck disable=2046 # printf word-splitting is intentional + GO111MODULE=on go install -a $(printf "%s " "${UPSTREAM_BINS[@]}") + + echo "Installing goverter..." + rm -f "${GOBIN}/goverter" + local tmp_dir + tmp_dir=$(mktemp -d) + trap 'rm -rf -- "$tmp_dir"' EXIT + + local goversion + goversion=$(kube::codegen::internal::get_goversion) + + pushd "${tmp_dir}" > /dev/null + go mod init build-goverter > /dev/null 2>&1 + go mod edit -go="${goversion}" > /dev/null 2>&1 + export GOTOOLCHAIN=auto + go get "github.com/jmattheis/goverter${GOVERTER_VERSION_SPEC}" > /dev/null 2>&1 + go build -o "${GOBIN}/goverter" "github.com/jmattheis/goverter/cmd/goverter" > /dev/null + popd > /dev/null + echo " - github.com/jmattheis/goverter${GOVERTER_VERSION_SPEC}" + ) + + # Deepcopy + # + local input_pkgs=() + while read -r dir; do + pkg="$(cd "${dir}" && GO111MODULE=on go list -find .)" + input_pkgs+=("${pkg}") + done < <( + ( kube::codegen::internal::grep -l --null \ + -e '^\s*//\s*+k8s:deepcopy-gen=' \ + -r "${in_dir}" \ + --include '*.go' \ + || true \ + ) | while read -r -d $'\0' F; do dirname "${F}"; done \ + | LC_ALL=C sort -u + ) + + if [ "${#input_pkgs[@]}" != 0 ]; then + echo "Generating deepcopy code for ${#input_pkgs[@]} targets" + + kube::codegen::internal::findz \ + "${in_dir}" \ + -type f \ + -name zz_generated.deepcopy.go \ + | xargs -0 rm -f + + "${GOBIN}/deepcopy-gen" \ + -v "${v}" \ + --output-file zz_generated.deepcopy.go \ + --go-header-file "${boilerplate}" \ + "${input_pkgs[@]}" + fi + + # Validations + # + local input_pkgs=() + while read -r dir; do + pkg="$(cd "${dir}" && GO111MODULE=on go list -find .)" + input_pkgs+=("${pkg}") + done < <( + ( kube::codegen::internal::grep -l --null \ + -e '^\s*//\s*+k8s:validation-gen=' \ + -r "${in_dir}" \ + --include '*.go' \ + || true \ + ) | while read -r -d $'\0' F; do dirname "${F}"; done \ + | LC_ALL=C sort -u + ) + + if [ "${#input_pkgs[@]}" != 0 ]; then + echo "Generating validation code for ${#input_pkgs[@]} targets" + + kube::codegen::internal::findz \ + "${in_dir}" \ + -type f \ + -name zz_generated.validations.go \ + | xargs -0 rm -f + + "${GOBIN}/validation-gen" \ + -v "${v}" \ + --output-file zz_generated.validations.go \ + --go-header-file "${boilerplate}" \ + "${input_pkgs[@]}" + fi + + # Defaults + # + local input_pkgs=() + while read -r dir; do + pkg="$(cd "${dir}" && GO111MODULE=on go list -find .)" + input_pkgs+=("${pkg}") + done < <( + ( kube::codegen::internal::grep -l --null \ + -e '^\s*//\s*+k8s:defaulter-gen=' \ + -r "${in_dir}" \ + --include '*.go' \ + || true \ + ) | while read -r -d $'\0' F; do dirname "${F}"; done \ + | LC_ALL=C sort -u + ) + + if [ "${#input_pkgs[@]}" != 0 ]; then + echo "Generating defaulter code for ${#input_pkgs[@]} targets" + + kube::codegen::internal::findz \ + "${in_dir}" \ + -type f \ + -name zz_generated.defaults.go \ + | xargs -0 rm -f + + "${GOBIN}/defaulter-gen" \ + -v "${v}" \ + --output-file zz_generated.defaults.go \ + --go-header-file "${boilerplate}" \ + "${input_pkgs[@]}" + fi + + # Conversions + # + local input_pkgs=() + while read -r dir; do + pkg="$(cd "${dir}" && GO111MODULE=on go list -find .)" + input_pkgs+=("${pkg}") + done < <( + ( kube::codegen::internal::grep -l --null \ + -e '^\s*//\s*+k8s:conversion-gen=' \ + -r "${in_dir}" \ + --include '*.go' \ + || true \ + ) | while read -r -d $'\0' F; do dirname "${F}"; done \ + | LC_ALL=C sort -u + ) + + if [ "${#input_pkgs[@]}" != 0 ]; then + echo "Generating conversion code for ${#input_pkgs[@]} targets" + + kube::codegen::internal::findz \ + "${in_dir}" \ + -type f \ + -name zz_generated.conversion.go \ + | xargs -0 rm -f + + local extra_peer_args=() + for arg in "${extra_peers[@]:+"${extra_peers[@]}"}"; do + extra_peer_args+=("--extra-peer-dirs" "$arg") + done + "${GOBIN}/conversion-gen" \ + -v "${v}" \ + --output-file zz_generated.conversion.go \ + --go-header-file "${boilerplate}" \ + "${extra_peer_args[@]:+"${extra_peer_args[@]}"}" \ + "${input_pkgs[@]}" + fi + + local input_dirs=() + while read -r dir; do + input_dirs+=("${dir}") + done < <( + ( kube::codegen::internal::grep -l --null \ + -e '^\s*//\s*goverter:converter' \ + -r "${in_dir}" \ + --include '*.go' \ + || true \ + ) | while read -r -d $'\0' F; do dirname "${F}"; done \ + | LC_ALL=C sort -u + ) + + if [ "${#input_dirs[@]}" != 0 ]; then + echo "Generating goverter conversion code for ${#input_dirs[@]} targets" + + kube::codegen::internal::findz \ + "${in_dir}" \ + -type f \ + -name zz_generated.goverter.go \ + | xargs -0 rm -f + + for dir in "${input_dirs[@]}"; do + "${GOBIN}/goverter" \ + gen \ + "${dir}" + done + fi +} + +# Generate client code +# +# USAGE: kube::codegen::gen_client [FLAGS] +# +# +# The root package under which to search for Go files which request clients +# to be generated. This must be a local path, not a Go package. +# +# FLAGS: +# --one-input-api +# A specific API (a directory) under the input-dir for which to generate a +# client. If this is not set, clients for all APIs under the input-dir +# will be generated (under the --output-pkg). +# +# --output-dir +# The root directory under which to emit code. Each aspect of client +# generation will make one or more subdirectories. +# +# --output-pkg +# The Go package path (import path) of the --output-dir. Each aspect of +# client generation will make one or more sub-packages. +# +# --boilerplate +# An optional override for the header file to insert into generated files. +# +# --clientset-name +# An optional override for the leaf name of the generated "clientset" directory. +# +# --versioned-name +# An optional override for the leaf name of the generated +# "/versioned" directory. +# +# --with-watch +# Enables generation of listers and informers for APIs which support WATCH. +# +# --listers-name +# An optional override for the leaf name of the generated "listers" directory. +# +# --informers-name +# An optional override for the leaf name of the generated "informers" directory. +# +# --plural-exceptions +# An optional list of comma separated plural exception definitions in Type:PluralizedType form. +# +# --proto-base +# The base Go import-path of the protobuf stubs. +# +function kube::codegen::gen_client() { + local in_dir="" + local one_input_api="" + local out_dir="" + local out_pkg="" + local clientset_subdir="clientset" + local clientset_versioned_name="versioned" + local watchable="false" + local listers_subdir="listers" + local informers_subdir="informers" + local boilerplate="${KUBE_CODEGEN_ROOT}/hack/boilerplate.go.txt" + local plural_exceptions="" + local v="${KUBE_VERBOSE:-0}" + local proto_base="" + + while [ "$#" -gt 0 ]; do + case "$1" in + "--one-input-api") + one_input_api="/$2" + shift 2 + ;; + "--output-dir") + out_dir="$2" + shift 2 + ;; + "--output-pkg") + out_pkg="$2" + shift 2 + ;; + "--boilerplate") + boilerplate="$2" + shift 2 + ;; + "--clientset-name") + clientset_subdir="$2" + shift 2 + ;; + "--versioned-name") + clientset_versioned_name="$2" + shift 2 + ;; + "--with-watch") + watchable="true" + shift + ;; + "--listers-name") + listers_subdir="$2" + shift 2 + ;; + "--informers-name") + informers_subdir="$2" + shift 2 + ;; + "--plural-exceptions") + plural_exceptions="$2" + shift 2 + ;; + "--proto-base") + proto_base="$2" + shift 2 + ;; + *) + if [[ "$1" =~ ^-- ]]; then + echo "unknown argument: $1" >&2 + return 1 + fi + if [ -n "$in_dir" ]; then + echo "too many arguments: $1 (already have $in_dir)" >&2 + return 1 + fi + in_dir="$1" + shift + ;; + esac + done + + if [ -z "${in_dir}" ]; then + echo "input-dir argument is required" >&2 + return 1 + fi + if [ -z "${out_dir}" ]; then + echo "--output-dir is required" >&2 + return 1 + fi + if [ -z "${out_pkg}" ]; then + echo "--output-pkg is required" >&2 + return 1 + fi + if [ -z "${proto_base}" ]; then + echo "--proto-base is required for gRPC client generation" >&2 + return 1 + fi + + mkdir -p "${out_dir}" + + ( + # To support running this from anywhere, first cd into this directory, + # and then install with forced module mode on and fully qualified name. + cd "${KUBE_CODEGEN_ROOT}" + + UPSTREAM_BINS=( + informer-gen"${CODEGEN_VERSION_SPEC}" + lister-gen"${CODEGEN_VERSION_SPEC}" + ) + echo "Installing upstream generators..." + for bin in "${UPSTREAM_BINS[@]}"; do + echo " - k8s.io/code-generator/cmd/${bin}" + done + # shellcheck disable=2046 # printf word-splitting is intentional + GO111MODULE=on go install $(printf "k8s.io/code-generator/cmd/%s " "${UPSTREAM_BINS[@]}") + + echo "Installing local generators..." + rm -f "${GOBIN}/client-gen" + GO111MODULE=on go build -a -o "${GOBIN}/client-gen" ./cmd/client-gen + echo " - github.com/nvidia/nvsentinel/code-generator/cmd/client-gen${CODEGEN_VERSION_SPEC}" + ) + + local group_versions=() + local input_pkgs=() + while read -r dir; do + pkg="$(cd "${dir}" && GO111MODULE=on go list -find .)" + leaf="$(basename "${dir}")" + if grep -E -q '^v[0-9]+((alpha|beta)[0-9]+)?$' <<< "${leaf}"; then + input_pkgs+=("${pkg}") + + dir2="$(dirname "${dir}")" + leaf2="$(basename "${dir2}")" + group_versions+=("${leaf2}/${leaf}") + fi + done < <( + ( kube::codegen::internal::grep -l --null \ + -e '^[[:space:]]*//[[:space:]]*+genclient' \ + -r "${in_dir}${one_input_api}" \ + --include '*.go' \ + || true \ + ) | while read -r -d $'\0' F; do dirname "${F}"; done \ + | LC_ALL=C sort -u + ) + + if [ "${#group_versions[@]}" == 0 ]; then + return 0 + fi + + echo "Generating client code for ${#group_versions[@]} targets" + + ( kube::codegen::internal::grep -l --null \ + -e '^// Code generated by client-gen. DO NOT EDIT.$' \ + -r "${out_dir}/${clientset_subdir}" \ + --include '*.go' \ + || true \ + ) | xargs -0 rm -f + + local inputs=() + for arg in "${group_versions[@]}"; do + inputs+=("--input" "$arg") + done + + "${GOBIN}/client-gen" \ + -v "${v}" \ + --go-header-file "${boilerplate}" \ + --output-dir "${out_dir}/${clientset_subdir}" \ + --output-pkg "${out_pkg}/${clientset_subdir}" \ + --clientset-name "${clientset_versioned_name}" \ + --input-base "$(cd "${in_dir}" && pwd -P)" \ + --plural-exceptions "${plural_exceptions}" \ + --proto-base="${proto_base}" \ + "${inputs[@]}" + + if [ "${watchable}" == "true" ]; then + echo "Generating lister code for ${#input_pkgs[@]} targets" + + ( kube::codegen::internal::grep -l --null \ + -e '^// Code generated by lister-gen. DO NOT EDIT.$' \ + -r "${out_dir}/${listers_subdir}" \ + --include '*.go' \ + || true \ + ) | xargs -0 rm -f + + "${GOBIN}/lister-gen" \ + -v "${v}" \ + --go-header-file "${boilerplate}" \ + --output-dir "${out_dir}/${listers_subdir}" \ + --output-pkg "${out_pkg}/${listers_subdir}" \ + --plural-exceptions "${plural_exceptions}" \ + "${input_pkgs[@]}" + + echo "Generating informer code for ${#input_pkgs[@]} targets" + + ( kube::codegen::internal::grep -l --null \ + -e '^// Code generated by informer-gen. DO NOT EDIT.$' \ + -r "${out_dir}/${informers_subdir}" \ + --include '*.go' \ + || true \ + ) | xargs -0 rm -f + + "${GOBIN}/informer-gen" \ + -v "${v}" \ + --go-header-file "${boilerplate}" \ + --output-dir "${out_dir}/${informers_subdir}" \ + --output-pkg "${out_pkg}/${informers_subdir}" \ + --versioned-clientset-package "${out_pkg}/${clientset_subdir}/${clientset_versioned_name}" \ + --listers-package "${out_pkg}/${listers_subdir}" \ + --plural-exceptions "${plural_exceptions}" \ + "${input_pkgs[@]}" + fi +}