Merge pull request #125163 from pohly/dra-kubelet-api-version-independent-no-rest-proxy
DRA: make kubelet independent of the resource.k8s.io API version
This commit is contained in:
		| @@ -30,7 +30,7 @@ import ( | |||||||
| 	clientset "k8s.io/client-go/kubernetes" | 	clientset "k8s.io/client-go/kubernetes" | ||||||
| 	"k8s.io/dynamic-resource-allocation/resourceclaim" | 	"k8s.io/dynamic-resource-allocation/resourceclaim" | ||||||
| 	"k8s.io/klog/v2" | 	"k8s.io/klog/v2" | ||||||
| 	drapb "k8s.io/kubelet/pkg/apis/dra/v1alpha3" | 	drapb "k8s.io/kubelet/pkg/apis/dra/v1alpha4" | ||||||
| 	dra "k8s.io/kubernetes/pkg/kubelet/cm/dra/plugin" | 	dra "k8s.io/kubernetes/pkg/kubelet/cm/dra/plugin" | ||||||
| 	"k8s.io/kubernetes/pkg/kubelet/config" | 	"k8s.io/kubernetes/pkg/kubelet/config" | ||||||
| 	kubecontainer "k8s.io/kubernetes/pkg/kubelet/container" | 	kubecontainer "k8s.io/kubernetes/pkg/kubelet/container" | ||||||
| @@ -224,13 +224,9 @@ func (m *ManagerImpl) PrepareResources(pod *v1.Pod) error { | |||||||
| 			// Loop through all plugins and prepare for calling NodePrepareResources. | 			// Loop through all plugins and prepare for calling NodePrepareResources. | ||||||
| 			for _, resourceHandle := range claimInfo.ResourceHandles { | 			for _, resourceHandle := range claimInfo.ResourceHandles { | ||||||
| 				claim := &drapb.Claim{ | 				claim := &drapb.Claim{ | ||||||
| 					Namespace:      claimInfo.Namespace, | 					Namespace: claimInfo.Namespace, | ||||||
| 					Uid:            string(claimInfo.ClaimUID), | 					Uid:       string(claimInfo.ClaimUID), | ||||||
| 					Name:           claimInfo.ClaimName, | 					Name:      claimInfo.ClaimName, | ||||||
| 					ResourceHandle: resourceHandle.Data, |  | ||||||
| 				} |  | ||||||
| 				if resourceHandle.StructuredData != nil { |  | ||||||
| 					claim.StructuredResourceHandle = []*resourceapi.StructuredResourceHandle{resourceHandle.StructuredData} |  | ||||||
| 				} | 				} | ||||||
| 				pluginName := resourceHandle.DriverName | 				pluginName := resourceHandle.DriverName | ||||||
| 				batches[pluginName] = append(batches[pluginName], claim) | 				batches[pluginName] = append(batches[pluginName], claim) | ||||||
| @@ -455,13 +451,9 @@ func (m *ManagerImpl) unprepareResources(podUID types.UID, namespace string, cla | |||||||
| 			// Loop through all plugins and prepare for calling NodeUnprepareResources. | 			// Loop through all plugins and prepare for calling NodeUnprepareResources. | ||||||
| 			for _, resourceHandle := range claimInfo.ResourceHandles { | 			for _, resourceHandle := range claimInfo.ResourceHandles { | ||||||
| 				claim := &drapb.Claim{ | 				claim := &drapb.Claim{ | ||||||
| 					Namespace:      claimInfo.Namespace, | 					Namespace: claimInfo.Namespace, | ||||||
| 					Uid:            string(claimInfo.ClaimUID), | 					Uid:       string(claimInfo.ClaimUID), | ||||||
| 					Name:           claimInfo.ClaimName, | 					Name:      claimInfo.ClaimName, | ||||||
| 					ResourceHandle: resourceHandle.Data, |  | ||||||
| 				} |  | ||||||
| 				if resourceHandle.StructuredData != nil { |  | ||||||
| 					claim.StructuredResourceHandle = []*resourceapi.StructuredResourceHandle{resourceHandle.StructuredData} |  | ||||||
| 				} | 				} | ||||||
| 				pluginName := resourceHandle.DriverName | 				pluginName := resourceHandle.DriverName | ||||||
| 				batches[pluginName] = append(batches[pluginName], claim) | 				batches[pluginName] = append(batches[pluginName], claim) | ||||||
|   | |||||||
| @@ -36,7 +36,7 @@ import ( | |||||||
| 	"k8s.io/apimachinery/pkg/util/sets" | 	"k8s.io/apimachinery/pkg/util/sets" | ||||||
| 	"k8s.io/client-go/kubernetes/fake" | 	"k8s.io/client-go/kubernetes/fake" | ||||||
| 	"k8s.io/dynamic-resource-allocation/resourceclaim" | 	"k8s.io/dynamic-resource-allocation/resourceclaim" | ||||||
| 	drapbv1 "k8s.io/kubelet/pkg/apis/dra/v1alpha3" | 	drapb "k8s.io/kubelet/pkg/apis/dra/v1alpha4" | ||||||
| 	"k8s.io/kubernetes/pkg/kubelet/cm/dra/plugin" | 	"k8s.io/kubernetes/pkg/kubelet/cm/dra/plugin" | ||||||
| 	"k8s.io/kubernetes/pkg/kubelet/cm/dra/state" | 	"k8s.io/kubernetes/pkg/kubelet/cm/dra/state" | ||||||
| 	kubecontainer "k8s.io/kubernetes/pkg/kubelet/container" | 	kubecontainer "k8s.io/kubernetes/pkg/kubelet/container" | ||||||
| @@ -48,16 +48,16 @@ const ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| type fakeDRADriverGRPCServer struct { | type fakeDRADriverGRPCServer struct { | ||||||
| 	drapbv1.UnimplementedNodeServer | 	drapb.UnimplementedNodeServer | ||||||
| 	driverName                 string | 	driverName                 string | ||||||
| 	timeout                    *time.Duration | 	timeout                    *time.Duration | ||||||
| 	prepareResourceCalls       atomic.Uint32 | 	prepareResourceCalls       atomic.Uint32 | ||||||
| 	unprepareResourceCalls     atomic.Uint32 | 	unprepareResourceCalls     atomic.Uint32 | ||||||
| 	prepareResourcesResponse   *drapbv1.NodePrepareResourcesResponse | 	prepareResourcesResponse   *drapb.NodePrepareResourcesResponse | ||||||
| 	unprepareResourcesResponse *drapbv1.NodeUnprepareResourcesResponse | 	unprepareResourcesResponse *drapb.NodeUnprepareResourcesResponse | ||||||
| } | } | ||||||
|  |  | ||||||
| func (s *fakeDRADriverGRPCServer) NodePrepareResources(ctx context.Context, req *drapbv1.NodePrepareResourcesRequest) (*drapbv1.NodePrepareResourcesResponse, error) { | func (s *fakeDRADriverGRPCServer) NodePrepareResources(ctx context.Context, req *drapb.NodePrepareResourcesRequest) (*drapb.NodePrepareResourcesResponse, error) { | ||||||
| 	s.prepareResourceCalls.Add(1) | 	s.prepareResourceCalls.Add(1) | ||||||
|  |  | ||||||
| 	if s.timeout != nil { | 	if s.timeout != nil { | ||||||
| @@ -67,8 +67,8 @@ func (s *fakeDRADriverGRPCServer) NodePrepareResources(ctx context.Context, req | |||||||
| 	if s.prepareResourcesResponse == nil { | 	if s.prepareResourcesResponse == nil { | ||||||
| 		deviceName := "claim-" + req.Claims[0].Uid | 		deviceName := "claim-" + req.Claims[0].Uid | ||||||
| 		result := s.driverName + "/" + driverClassName + "=" + deviceName | 		result := s.driverName + "/" + driverClassName + "=" + deviceName | ||||||
| 		return &drapbv1.NodePrepareResourcesResponse{ | 		return &drapb.NodePrepareResourcesResponse{ | ||||||
| 			Claims: map[string]*drapbv1.NodePrepareResourceResponse{ | 			Claims: map[string]*drapb.NodePrepareResourceResponse{ | ||||||
| 				req.Claims[0].Uid: { | 				req.Claims[0].Uid: { | ||||||
| 					CDIDevices: []string{result}, | 					CDIDevices: []string{result}, | ||||||
| 				}, | 				}, | ||||||
| @@ -79,7 +79,7 @@ func (s *fakeDRADriverGRPCServer) NodePrepareResources(ctx context.Context, req | |||||||
| 	return s.prepareResourcesResponse, nil | 	return s.prepareResourcesResponse, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (s *fakeDRADriverGRPCServer) NodeUnprepareResources(ctx context.Context, req *drapbv1.NodeUnprepareResourcesRequest) (*drapbv1.NodeUnprepareResourcesResponse, error) { | func (s *fakeDRADriverGRPCServer) NodeUnprepareResources(ctx context.Context, req *drapb.NodeUnprepareResourcesRequest) (*drapb.NodeUnprepareResourcesResponse, error) { | ||||||
| 	s.unprepareResourceCalls.Add(1) | 	s.unprepareResourceCalls.Add(1) | ||||||
|  |  | ||||||
| 	if s.timeout != nil { | 	if s.timeout != nil { | ||||||
| @@ -87,8 +87,8 @@ func (s *fakeDRADriverGRPCServer) NodeUnprepareResources(ctx context.Context, re | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if s.unprepareResourcesResponse == nil { | 	if s.unprepareResourcesResponse == nil { | ||||||
| 		return &drapbv1.NodeUnprepareResourcesResponse{ | 		return &drapb.NodeUnprepareResourcesResponse{ | ||||||
| 			Claims: map[string]*drapbv1.NodeUnprepareResourceResponse{ | 			Claims: map[string]*drapb.NodeUnprepareResourceResponse{ | ||||||
| 				req.Claims[0].Uid: {}, | 				req.Claims[0].Uid: {}, | ||||||
| 			}, | 			}, | ||||||
| 		}, nil | 		}, nil | ||||||
| @@ -108,7 +108,7 @@ type fakeDRAServerInfo struct { | |||||||
| 	teardownFn tearDown | 	teardownFn tearDown | ||||||
| } | } | ||||||
|  |  | ||||||
| func setupFakeDRADriverGRPCServer(shouldTimeout bool, pluginClientTimeout *time.Duration, prepareResourcesResponse *drapbv1.NodePrepareResourcesResponse, unprepareResourcesResponse *drapbv1.NodeUnprepareResourcesResponse) (fakeDRAServerInfo, error) { | func setupFakeDRADriverGRPCServer(shouldTimeout bool, pluginClientTimeout *time.Duration, prepareResourcesResponse *drapb.NodePrepareResourcesResponse, unprepareResourcesResponse *drapb.NodeUnprepareResourcesResponse) (fakeDRAServerInfo, error) { | ||||||
| 	socketDir, err := os.MkdirTemp("", "dra") | 	socketDir, err := os.MkdirTemp("", "dra") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fakeDRAServerInfo{ | 		return fakeDRAServerInfo{ | ||||||
| @@ -147,7 +147,7 @@ func setupFakeDRADriverGRPCServer(shouldTimeout bool, pluginClientTimeout *time. | |||||||
| 		fakeDRADriverGRPCServer.timeout = &timeout | 		fakeDRADriverGRPCServer.timeout = &timeout | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	drapbv1.RegisterNodeServer(s, fakeDRADriverGRPCServer) | 	drapb.RegisterNodeServer(s, fakeDRADriverGRPCServer) | ||||||
|  |  | ||||||
| 	go func() { | 	go func() { | ||||||
| 		go s.Serve(l) | 		go s.Serve(l) | ||||||
| @@ -345,7 +345,7 @@ func TestPrepareResources(t *testing.T) { | |||||||
| 		pod                  *v1.Pod | 		pod                  *v1.Pod | ||||||
| 		claimInfo            *ClaimInfo | 		claimInfo            *ClaimInfo | ||||||
| 		resourceClaim        *resourcev1alpha2.ResourceClaim | 		resourceClaim        *resourcev1alpha2.ResourceClaim | ||||||
| 		resp                 *drapbv1.NodePrepareResourcesResponse | 		resp                 *drapb.NodePrepareResourcesResponse | ||||||
| 		wantErr              bool | 		wantErr              bool | ||||||
| 		wantTimeout          bool | 		wantTimeout          bool | ||||||
| 		wantResourceSkipped  bool | 		wantResourceSkipped  bool | ||||||
| @@ -484,7 +484,7 @@ func TestPrepareResources(t *testing.T) { | |||||||
| 					}, | 					}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			resp:                 &drapbv1.NodePrepareResourcesResponse{Claims: map[string]*drapbv1.NodePrepareResourceResponse{"test-reserved": nil}}, | 			resp:                 &drapb.NodePrepareResourcesResponse{Claims: map[string]*drapb.NodePrepareResourceResponse{"test-reserved": nil}}, | ||||||
| 			expectedCDIDevices:   []string{}, | 			expectedCDIDevices:   []string{}, | ||||||
| 			ExpectedPrepareCalls: 1, | 			ExpectedPrepareCalls: 1, | ||||||
| 		}, | 		}, | ||||||
| @@ -541,7 +541,7 @@ func TestPrepareResources(t *testing.T) { | |||||||
| 					}, | 					}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			resp:                 &drapbv1.NodePrepareResourcesResponse{Claims: map[string]*drapbv1.NodePrepareResourceResponse{"test-reserved": nil}}, | 			resp:                 &drapb.NodePrepareResourcesResponse{Claims: map[string]*drapb.NodePrepareResourceResponse{"test-reserved": nil}}, | ||||||
| 			expectedCDIDevices:   []string{}, | 			expectedCDIDevices:   []string{}, | ||||||
| 			ExpectedPrepareCalls: 1, | 			ExpectedPrepareCalls: 1, | ||||||
| 		}, | 		}, | ||||||
| @@ -748,8 +748,8 @@ func TestPrepareResources(t *testing.T) { | |||||||
| 					}, | 					}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			resp: &drapbv1.NodePrepareResourcesResponse{ | 			resp: &drapb.NodePrepareResourcesResponse{ | ||||||
| 				Claims: map[string]*drapbv1.NodePrepareResourceResponse{ | 				Claims: map[string]*drapb.NodePrepareResourceResponse{ | ||||||
| 					"test-reserved": {CDIDevices: []string{fmt.Sprintf("%s/%s=claim-test-reserved", driverName, driverClassName)}}, | 					"test-reserved": {CDIDevices: []string{fmt.Sprintf("%s/%s=claim-test-reserved", driverName, driverClassName)}}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| @@ -810,8 +810,8 @@ func TestPrepareResources(t *testing.T) { | |||||||
| 					}, | 					}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			resp: &drapbv1.NodePrepareResourcesResponse{ | 			resp: &drapb.NodePrepareResourcesResponse{ | ||||||
| 				Claims: map[string]*drapbv1.NodePrepareResourceResponse{ | 				Claims: map[string]*drapb.NodePrepareResourceResponse{ | ||||||
| 					"test-reserved": {CDIDevices: []string{fmt.Sprintf("%s/%s=claim-test-reserved", driverName, driverClassName)}}, | 					"test-reserved": {CDIDevices: []string{fmt.Sprintf("%s/%s=claim-test-reserved", driverName, driverClassName)}}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| @@ -884,8 +884,8 @@ func TestPrepareResources(t *testing.T) { | |||||||
| 					}, | 					}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			resp: &drapbv1.NodePrepareResourcesResponse{ | 			resp: &drapb.NodePrepareResourcesResponse{ | ||||||
| 				Claims: map[string]*drapbv1.NodePrepareResourceResponse{ | 				Claims: map[string]*drapb.NodePrepareResourceResponse{ | ||||||
| 					"test-reserved": {CDIDevices: []string{fmt.Sprintf("%s/%s=claim-test-reserved", driverName, driverClassName)}}, | 					"test-reserved": {CDIDevices: []string{fmt.Sprintf("%s/%s=claim-test-reserved", driverName, driverClassName)}}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| @@ -977,7 +977,7 @@ func TestUnprepareResources(t *testing.T) { | |||||||
| 		driverName             string | 		driverName             string | ||||||
| 		pod                    *v1.Pod | 		pod                    *v1.Pod | ||||||
| 		claimInfo              *ClaimInfo | 		claimInfo              *ClaimInfo | ||||||
| 		resp                   *drapbv1.NodeUnprepareResourcesResponse | 		resp                   *drapb.NodeUnprepareResourcesResponse | ||||||
| 		wantErr                bool | 		wantErr                bool | ||||||
| 		wantTimeout            bool | 		wantTimeout            bool | ||||||
| 		wantResourceSkipped    bool | 		wantResourceSkipped    bool | ||||||
| @@ -1117,7 +1117,7 @@ func TestUnprepareResources(t *testing.T) { | |||||||
| 					}, | 					}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			resp:                   &drapbv1.NodeUnprepareResourcesResponse{Claims: map[string]*drapbv1.NodeUnprepareResourceResponse{"test-reserved": {}}}, | 			resp:                   &drapb.NodeUnprepareResourcesResponse{Claims: map[string]*drapb.NodeUnprepareResourceResponse{"test-reserved": {}}}, | ||||||
| 			wantErr:                true, | 			wantErr:                true, | ||||||
| 			wantTimeout:            true, | 			wantTimeout:            true, | ||||||
| 			expectedUnprepareCalls: 1, | 			expectedUnprepareCalls: 1, | ||||||
| @@ -1168,7 +1168,7 @@ func TestUnprepareResources(t *testing.T) { | |||||||
| 				}, | 				}, | ||||||
| 				prepared: true, | 				prepared: true, | ||||||
| 			}, | 			}, | ||||||
| 			resp:                   &drapbv1.NodeUnprepareResourcesResponse{Claims: map[string]*drapbv1.NodeUnprepareResourceResponse{"": {}}}, | 			resp:                   &drapb.NodeUnprepareResourcesResponse{Claims: map[string]*drapb.NodeUnprepareResourceResponse{"": {}}}, | ||||||
| 			expectedUnprepareCalls: 1, | 			expectedUnprepareCalls: 1, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| @@ -1217,7 +1217,7 @@ func TestUnprepareResources(t *testing.T) { | |||||||
| 				}, | 				}, | ||||||
| 				prepared: false, | 				prepared: false, | ||||||
| 			}, | 			}, | ||||||
| 			resp:                   &drapbv1.NodeUnprepareResourcesResponse{Claims: map[string]*drapbv1.NodeUnprepareResourceResponse{"": {}}}, | 			resp:                   &drapb.NodeUnprepareResourcesResponse{Claims: map[string]*drapb.NodeUnprepareResourceResponse{"": {}}}, | ||||||
| 			expectedUnprepareCalls: 1, | 			expectedUnprepareCalls: 1, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| @@ -1267,7 +1267,7 @@ func TestUnprepareResources(t *testing.T) { | |||||||
| 				}, | 				}, | ||||||
| 				prepared: true, | 				prepared: true, | ||||||
| 			}, | 			}, | ||||||
| 			resp:                   &drapbv1.NodeUnprepareResourcesResponse{Claims: map[string]*drapbv1.NodeUnprepareResourceResponse{"test-reserved": nil}}, | 			resp:                   &drapb.NodeUnprepareResourcesResponse{Claims: map[string]*drapb.NodeUnprepareResourceResponse{"test-reserved": nil}}, | ||||||
| 			expectedUnprepareCalls: 1, | 			expectedUnprepareCalls: 1, | ||||||
| 		}, | 		}, | ||||||
| 	} { | 	} { | ||||||
|   | |||||||
| @@ -18,18 +18,28 @@ package plugin | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"net" | ||||||
|  | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"google.golang.org/grpc" | 	"google.golang.org/grpc" | ||||||
|  | 	"google.golang.org/grpc/connectivity" | ||||||
|  | 	"google.golang.org/grpc/credentials/insecure" | ||||||
|  |  | ||||||
|  | 	utilversion "k8s.io/apimachinery/pkg/util/version" | ||||||
| 	"k8s.io/klog/v2" | 	"k8s.io/klog/v2" | ||||||
| 	drapb "k8s.io/kubelet/pkg/apis/dra/v1alpha3" | 	drapb "k8s.io/kubelet/pkg/apis/dra/v1alpha4" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const PluginClientTimeout = 45 * time.Second | const PluginClientTimeout = 45 * time.Second | ||||||
|  |  | ||||||
| func NewDRAPluginClient(pluginName string) (drapb.NodeClient, error) { | // NewDRAPluginClient returns a wrapper around those gRPC methods of a DRA | ||||||
|  | // driver kubelet plugin which need to be called by kubelet. The wrapper | ||||||
|  | // handles gRPC connection management and logging. Connections are reused | ||||||
|  | // across different NewDRAPluginClient calls. | ||||||
|  | func NewDRAPluginClient(pluginName string) (*Plugin, error) { | ||||||
| 	if pluginName == "" { | 	if pluginName == "" { | ||||||
| 		return nil, fmt.Errorf("plugin name is empty") | 		return nil, fmt.Errorf("plugin name is empty") | ||||||
| 	} | 	} | ||||||
| @@ -42,13 +52,64 @@ func NewDRAPluginClient(pluginName string) (drapb.NodeClient, error) { | |||||||
| 	return existingPlugin, nil | 	return existingPlugin, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (p *plugin) NodePrepareResources( | type Plugin struct { | ||||||
|  | 	backgroundCtx context.Context | ||||||
|  | 	cancel        func(cause error) | ||||||
|  |  | ||||||
|  | 	mutex                   sync.Mutex | ||||||
|  | 	conn                    *grpc.ClientConn | ||||||
|  | 	endpoint                string | ||||||
|  | 	highestSupportedVersion *utilversion.Version | ||||||
|  | 	clientTimeout           time.Duration | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *Plugin) getOrCreateGRPCConn() (*grpc.ClientConn, error) { | ||||||
|  | 	p.mutex.Lock() | ||||||
|  | 	defer p.mutex.Unlock() | ||||||
|  |  | ||||||
|  | 	if p.conn != nil { | ||||||
|  | 		return p.conn, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx := p.backgroundCtx | ||||||
|  | 	logger := klog.FromContext(ctx) | ||||||
|  |  | ||||||
|  | 	network := "unix" | ||||||
|  | 	logger.V(4).Info("Creating new gRPC connection", "protocol", network, "endpoint", p.endpoint) | ||||||
|  | 	// grpc.Dial is deprecated. grpc.NewClient should be used instead. | ||||||
|  | 	// For now this gets ignored because this function is meant to establish | ||||||
|  | 	// the connection, with the one second timeout below. Perhaps that | ||||||
|  | 	// approach should be reconsidered? | ||||||
|  | 	//nolint:staticcheck | ||||||
|  | 	conn, err := grpc.Dial( | ||||||
|  | 		p.endpoint, | ||||||
|  | 		grpc.WithTransportCredentials(insecure.NewCredentials()), | ||||||
|  | 		grpc.WithContextDialer(func(ctx context.Context, target string) (net.Conn, error) { | ||||||
|  | 			return (&net.Dialer{}).DialContext(ctx, network, target) | ||||||
|  | 		}), | ||||||
|  | 	) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx, cancel := context.WithTimeout(context.Background(), time.Second) | ||||||
|  | 	defer cancel() | ||||||
|  |  | ||||||
|  | 	if ok := conn.WaitForStateChange(ctx, connectivity.Connecting); !ok { | ||||||
|  | 		return nil, errors.New("timed out waiting for gRPC connection to be ready") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	p.conn = conn | ||||||
|  | 	return p.conn, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *Plugin) NodePrepareResources( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
| 	req *drapb.NodePrepareResourcesRequest, | 	req *drapb.NodePrepareResourcesRequest, | ||||||
| 	opts ...grpc.CallOption, | 	opts ...grpc.CallOption, | ||||||
| ) (*drapb.NodePrepareResourcesResponse, error) { | ) (*drapb.NodePrepareResourcesResponse, error) { | ||||||
| 	logger := klog.FromContext(ctx) | 	logger := klog.FromContext(ctx) | ||||||
| 	logger.V(4).Info(log("calling NodePrepareResources rpc"), "request", req) | 	logger.V(4).Info("Calling NodePrepareResources rpc", "request", req) | ||||||
|  |  | ||||||
| 	conn, err := p.getOrCreateGRPCConn() | 	conn, err := p.getOrCreateGRPCConn() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -60,17 +121,17 @@ func (p *plugin) NodePrepareResources( | |||||||
|  |  | ||||||
| 	nodeClient := drapb.NewNodeClient(conn) | 	nodeClient := drapb.NewNodeClient(conn) | ||||||
| 	response, err := nodeClient.NodePrepareResources(ctx, req) | 	response, err := nodeClient.NodePrepareResources(ctx, req) | ||||||
| 	logger.V(4).Info(log("done calling NodePrepareResources rpc"), "response", response, "err", err) | 	logger.V(4).Info("Done calling NodePrepareResources rpc", "response", response, "err", err) | ||||||
| 	return response, err | 	return response, err | ||||||
| } | } | ||||||
|  |  | ||||||
| func (p *plugin) NodeUnprepareResources( | func (p *Plugin) NodeUnprepareResources( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
| 	req *drapb.NodeUnprepareResourcesRequest, | 	req *drapb.NodeUnprepareResourcesRequest, | ||||||
| 	opts ...grpc.CallOption, | 	opts ...grpc.CallOption, | ||||||
| ) (*drapb.NodeUnprepareResourcesResponse, error) { | ) (*drapb.NodeUnprepareResourcesResponse, error) { | ||||||
| 	logger := klog.FromContext(ctx) | 	logger := klog.FromContext(ctx) | ||||||
| 	logger.V(4).Info(log("calling NodeUnprepareResource rpc"), "request", req) | 	logger.V(4).Info("Calling NodeUnprepareResource rpc", "request", req) | ||||||
|  |  | ||||||
| 	conn, err := p.getOrCreateGRPCConn() | 	conn, err := p.getOrCreateGRPCConn() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -82,23 +143,6 @@ func (p *plugin) NodeUnprepareResources( | |||||||
|  |  | ||||||
| 	nodeClient := drapb.NewNodeClient(conn) | 	nodeClient := drapb.NewNodeClient(conn) | ||||||
| 	response, err := nodeClient.NodeUnprepareResources(ctx, req) | 	response, err := nodeClient.NodeUnprepareResources(ctx, req) | ||||||
| 	logger.V(4).Info(log("done calling NodeUnprepareResources rpc"), "response", response, "err", err) | 	logger.V(4).Info("Done calling NodeUnprepareResources rpc", "response", response, "err", err) | ||||||
| 	return response, err | 	return response, err | ||||||
| } | } | ||||||
|  |  | ||||||
| func (p *plugin) NodeListAndWatchResources( |  | ||||||
| 	ctx context.Context, |  | ||||||
| 	req *drapb.NodeListAndWatchResourcesRequest, |  | ||||||
| 	opts ...grpc.CallOption, |  | ||||||
| ) (drapb.Node_NodeListAndWatchResourcesClient, error) { |  | ||||||
| 	logger := klog.FromContext(ctx) |  | ||||||
| 	logger.V(4).Info(log("calling NodeListAndWatchResources rpc"), "request", req) |  | ||||||
|  |  | ||||||
| 	conn, err := p.getOrCreateGRPCConn() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	nodeClient := drapb.NewNodeClient(conn) |  | ||||||
| 	return nodeClient.NodeListAndWatchResources(ctx, req, opts...) |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -27,32 +27,27 @@ import ( | |||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| 	"google.golang.org/grpc" | 	"google.golang.org/grpc" | ||||||
| 	drapbv1alpha3 "k8s.io/kubelet/pkg/apis/dra/v1alpha3" | 	drapb "k8s.io/kubelet/pkg/apis/dra/v1alpha4" | ||||||
|  | 	"k8s.io/kubernetes/test/utils/ktesting" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type fakeV1alpha3GRPCServer struct { | const ( | ||||||
| 	drapbv1alpha3.UnimplementedNodeServer | 	v1alpha4Version = "v1alpha4" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type fakeV1alpha4GRPCServer struct { | ||||||
|  | 	drapb.UnimplementedNodeServer | ||||||
| } | } | ||||||
|  |  | ||||||
| var _ drapbv1alpha3.NodeServer = &fakeV1alpha3GRPCServer{} | var _ drapb.NodeServer = &fakeV1alpha4GRPCServer{} | ||||||
|  |  | ||||||
| func (f *fakeV1alpha3GRPCServer) NodePrepareResources(ctx context.Context, in *drapbv1alpha3.NodePrepareResourcesRequest) (*drapbv1alpha3.NodePrepareResourcesResponse, error) { | func (f *fakeV1alpha4GRPCServer) NodePrepareResources(ctx context.Context, in *drapb.NodePrepareResourcesRequest) (*drapb.NodePrepareResourcesResponse, error) { | ||||||
| 	return &drapbv1alpha3.NodePrepareResourcesResponse{Claims: map[string]*drapbv1alpha3.NodePrepareResourceResponse{"dummy": {CDIDevices: []string{"dummy"}}}}, nil | 	return &drapb.NodePrepareResourcesResponse{Claims: map[string]*drapb.NodePrepareResourceResponse{"dummy": {CDIDevices: []string{"dummy"}}}}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (f *fakeV1alpha3GRPCServer) NodeUnprepareResources(ctx context.Context, in *drapbv1alpha3.NodeUnprepareResourcesRequest) (*drapbv1alpha3.NodeUnprepareResourcesResponse, error) { | func (f *fakeV1alpha4GRPCServer) NodeUnprepareResources(ctx context.Context, in *drapb.NodeUnprepareResourcesRequest) (*drapb.NodeUnprepareResourcesResponse, error) { | ||||||
|  |  | ||||||
| 	return &drapbv1alpha3.NodeUnprepareResourcesResponse{}, nil | 	return &drapb.NodeUnprepareResourcesResponse{}, nil | ||||||
| } |  | ||||||
|  |  | ||||||
| func (f *fakeV1alpha3GRPCServer) NodeListAndWatchResources(req *drapbv1alpha3.NodeListAndWatchResourcesRequest, srv drapbv1alpha3.Node_NodeListAndWatchResourcesServer) error { |  | ||||||
| 	if err := srv.Send(&drapbv1alpha3.NodeListAndWatchResourcesResponse{}); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	if err := srv.Send(&drapbv1alpha3.NodeListAndWatchResourcesResponse{}); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } | } | ||||||
|  |  | ||||||
| type tearDown func() | type tearDown func() | ||||||
| @@ -78,9 +73,9 @@ func setupFakeGRPCServer(version string) (string, tearDown, error) { | |||||||
|  |  | ||||||
| 	s := grpc.NewServer() | 	s := grpc.NewServer() | ||||||
| 	switch version { | 	switch version { | ||||||
| 	case v1alpha3Version: | 	case v1alpha4Version: | ||||||
| 		fakeGRPCServer := &fakeV1alpha3GRPCServer{} | 		fakeGRPCServer := &fakeV1alpha4GRPCServer{} | ||||||
| 		drapbv1alpha3.RegisterNodeServer(s, fakeGRPCServer) | 		drapb.RegisterNodeServer(s, fakeGRPCServer) | ||||||
| 	default: | 	default: | ||||||
| 		return "", nil, fmt.Errorf("unsupported version: %s", version) | 		return "", nil, fmt.Errorf("unsupported version: %s", version) | ||||||
| 	} | 	} | ||||||
| @@ -95,7 +90,8 @@ func setupFakeGRPCServer(version string) (string, tearDown, error) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func TestGRPCConnIsReused(t *testing.T) { | func TestGRPCConnIsReused(t *testing.T) { | ||||||
| 	addr, teardown, err := setupFakeGRPCServer(v1alpha3Version) | 	ctx := ktesting.Init(t) | ||||||
|  | 	addr, teardown, err := setupFakeGRPCServer(v1alpha4Version) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| @@ -105,8 +101,9 @@ func TestGRPCConnIsReused(t *testing.T) { | |||||||
| 	wg := sync.WaitGroup{} | 	wg := sync.WaitGroup{} | ||||||
| 	m := sync.Mutex{} | 	m := sync.Mutex{} | ||||||
|  |  | ||||||
| 	p := &plugin{ | 	p := &Plugin{ | ||||||
| 		endpoint: addr, | 		backgroundCtx: ctx, | ||||||
|  | 		endpoint:      addr, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	conn, err := p.getOrCreateGRPCConn() | 	conn, err := p.getOrCreateGRPCConn() | ||||||
| @@ -135,21 +132,20 @@ func TestGRPCConnIsReused(t *testing.T) { | |||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			req := &drapbv1alpha3.NodePrepareResourcesRequest{ | 			req := &drapb.NodePrepareResourcesRequest{ | ||||||
| 				Claims: []*drapbv1alpha3.Claim{ | 				Claims: []*drapb.Claim{ | ||||||
| 					{ | 					{ | ||||||
| 						Namespace:      "dummy-namespace", | 						Namespace: "dummy-namespace", | ||||||
| 						Uid:            "dummy-uid", | 						Uid:       "dummy-uid", | ||||||
| 						Name:           "dummy-claim", | 						Name:      "dummy-claim", | ||||||
| 						ResourceHandle: "dummy-resource", |  | ||||||
| 					}, | 					}, | ||||||
| 				}, | 				}, | ||||||
| 			} | 			} | ||||||
| 			client.NodePrepareResources(context.TODO(), req) | 			client.NodePrepareResources(context.TODO(), req) | ||||||
|  |  | ||||||
| 			client.(*plugin).Lock() | 			client.mutex.Lock() | ||||||
| 			conn := client.(*plugin).conn | 			conn := client.conn | ||||||
| 			client.(*plugin).Unlock() | 			client.mutex.Unlock() | ||||||
|  |  | ||||||
| 			m.Lock() | 			m.Lock() | ||||||
| 			defer m.Unlock() | 			defer m.Unlock() | ||||||
| @@ -193,7 +189,7 @@ func TestNewDRAPluginClient(t *testing.T) { | |||||||
| 		{ | 		{ | ||||||
| 			description: "plugin exists", | 			description: "plugin exists", | ||||||
| 			setup: func(name string) tearDown { | 			setup: func(name string) tearDown { | ||||||
| 				draPlugins.add(name, &plugin{}) | 				draPlugins.add(name, &Plugin{}) | ||||||
| 				return func() { | 				return func() { | ||||||
| 					draPlugins.delete(name) | 					draPlugins.delete(name) | ||||||
| 				} | 				} | ||||||
| @@ -222,23 +218,25 @@ func TestNodeUnprepareResources(t *testing.T) { | |||||||
| 		description   string | 		description   string | ||||||
| 		serverSetup   func(string) (string, tearDown, error) | 		serverSetup   func(string) (string, tearDown, error) | ||||||
| 		serverVersion string | 		serverVersion string | ||||||
| 		request       *drapbv1alpha3.NodeUnprepareResourcesRequest | 		request       *drapb.NodeUnprepareResourcesRequest | ||||||
| 	}{ | 	}{ | ||||||
| 		{ | 		{ | ||||||
| 			description:   "server supports v1alpha3", | 			description:   "server supports v1alpha4", | ||||||
| 			serverSetup:   setupFakeGRPCServer, | 			serverSetup:   setupFakeGRPCServer, | ||||||
| 			serverVersion: v1alpha3Version, | 			serverVersion: v1alpha4Version, | ||||||
| 			request:       &drapbv1alpha3.NodeUnprepareResourcesRequest{}, | 			request:       &drapb.NodeUnprepareResourcesRequest{}, | ||||||
| 		}, | 		}, | ||||||
| 	} { | 	} { | ||||||
| 		t.Run(test.description, func(t *testing.T) { | 		t.Run(test.description, func(t *testing.T) { | ||||||
|  | 			ctx := ktesting.Init(t) | ||||||
| 			addr, teardown, err := setupFakeGRPCServer(test.serverVersion) | 			addr, teardown, err := setupFakeGRPCServer(test.serverVersion) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				t.Fatal(err) | 				t.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 			defer teardown() | 			defer teardown() | ||||||
|  |  | ||||||
| 			p := &plugin{ | 			p := &Plugin{ | ||||||
|  | 				backgroundCtx: ctx, | ||||||
| 				endpoint:      addr, | 				endpoint:      addr, | ||||||
| 				clientTimeout: PluginClientTimeout, | 				clientTimeout: PluginClientTimeout, | ||||||
| 			} | 			} | ||||||
| @@ -269,74 +267,3 @@ func TestNodeUnprepareResources(t *testing.T) { | |||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestListAndWatchResources(t *testing.T) { |  | ||||||
| 	for _, test := range []struct { |  | ||||||
| 		description   string |  | ||||||
| 		serverSetup   func(string) (string, tearDown, error) |  | ||||||
| 		serverVersion string |  | ||||||
| 		request       *drapbv1alpha3.NodeListAndWatchResourcesRequest |  | ||||||
| 		responses     []*drapbv1alpha3.NodeListAndWatchResourcesResponse |  | ||||||
| 		expectError   string |  | ||||||
| 	}{ |  | ||||||
| 		{ |  | ||||||
| 			description:   "server supports NodeResources API", |  | ||||||
| 			serverSetup:   setupFakeGRPCServer, |  | ||||||
| 			serverVersion: v1alpha3Version, |  | ||||||
| 			request:       &drapbv1alpha3.NodeListAndWatchResourcesRequest{}, |  | ||||||
| 			responses: []*drapbv1alpha3.NodeListAndWatchResourcesResponse{ |  | ||||||
| 				{}, |  | ||||||
| 				{}, |  | ||||||
| 			}, |  | ||||||
| 			expectError: "EOF", |  | ||||||
| 		}, |  | ||||||
| 	} { |  | ||||||
| 		t.Run(test.description, func(t *testing.T) { |  | ||||||
| 			addr, teardown, err := setupFakeGRPCServer(test.serverVersion) |  | ||||||
| 			if err != nil { |  | ||||||
| 				t.Fatal(err) |  | ||||||
| 			} |  | ||||||
| 			defer teardown() |  | ||||||
|  |  | ||||||
| 			p := &plugin{ |  | ||||||
| 				endpoint: addr, |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			conn, err := p.getOrCreateGRPCConn() |  | ||||||
| 			defer func() { |  | ||||||
| 				err := conn.Close() |  | ||||||
| 				if err != nil { |  | ||||||
| 					t.Error(err) |  | ||||||
| 				} |  | ||||||
| 			}() |  | ||||||
| 			if err != nil { |  | ||||||
| 				t.Fatal(err) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			draPlugins.add("dummy-plugin", p) |  | ||||||
| 			defer draPlugins.delete("dummy-plugin") |  | ||||||
|  |  | ||||||
| 			client, err := NewDRAPluginClient("dummy-plugin") |  | ||||||
| 			if err != nil { |  | ||||||
| 				t.Fatal(err) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			stream, err := client.NodeListAndWatchResources(context.Background(), test.request) |  | ||||||
| 			if err != nil { |  | ||||||
| 				t.Fatal(err) |  | ||||||
| 			} |  | ||||||
| 			var actualResponses []*drapbv1alpha3.NodeListAndWatchResourcesResponse |  | ||||||
| 			var actualErr error |  | ||||||
| 			for { |  | ||||||
| 				resp, err := stream.Recv() |  | ||||||
| 				if err != nil { |  | ||||||
| 					actualErr = err |  | ||||||
| 					break |  | ||||||
| 				} |  | ||||||
| 				actualResponses = append(actualResponses, resp) |  | ||||||
| 			} |  | ||||||
| 			assert.Equal(t, test.responses, actualResponses) |  | ||||||
| 			assert.Contains(t, actualErr.Error(), test.expectError) |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -1,520 +0,0 @@ | |||||||
| /* |  | ||||||
| Copyright 2024 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. |  | ||||||
| */ |  | ||||||
|  |  | ||||||
| package plugin |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"io" |  | ||||||
| 	"sync" |  | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	"github.com/google/go-cmp/cmp" |  | ||||||
| 	"google.golang.org/grpc/codes" |  | ||||||
| 	"google.golang.org/grpc/status" |  | ||||||
|  |  | ||||||
| 	v1 "k8s.io/api/core/v1" |  | ||||||
| 	resourceapi "k8s.io/api/resource/v1alpha2" |  | ||||||
| 	apiequality "k8s.io/apimachinery/pkg/api/equality" |  | ||||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |  | ||||||
| 	utilruntime "k8s.io/apimachinery/pkg/util/runtime" |  | ||||||
| 	"k8s.io/apimachinery/pkg/util/sets" |  | ||||||
| 	resourceinformers "k8s.io/client-go/informers/resource/v1alpha2" |  | ||||||
| 	"k8s.io/client-go/kubernetes" |  | ||||||
| 	"k8s.io/client-go/tools/cache" |  | ||||||
| 	"k8s.io/client-go/util/flowcontrol" |  | ||||||
| 	"k8s.io/client-go/util/workqueue" |  | ||||||
| 	"k8s.io/klog/v2" |  | ||||||
| 	drapb "k8s.io/kubelet/pkg/apis/dra/v1alpha3" |  | ||||||
| 	"k8s.io/utils/ptr" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	// resyncPeriod for informer |  | ||||||
| 	// TODO (https://github.com/kubernetes/kubernetes/issues/123688): disable? |  | ||||||
| 	resyncPeriod   = time.Duration(10 * time.Minute) |  | ||||||
| 	retryPeriod    = 5 * time.Second |  | ||||||
| 	maxRetryPeriod = 180 * time.Second |  | ||||||
| 	backoffFactor  = 2.0 // Introduce a backoff multiplier as jitter factor |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // nodeResourcesController collects resource information from all registered |  | ||||||
| // plugins and synchronizes that information with ResourceSlice objects. |  | ||||||
| type nodeResourcesController struct { |  | ||||||
| 	ctx        context.Context |  | ||||||
| 	kubeClient kubernetes.Interface |  | ||||||
| 	getNode    func() (*v1.Node, error) |  | ||||||
| 	wg         sync.WaitGroup |  | ||||||
| 	queue      workqueue.TypedRateLimitingInterface[string] |  | ||||||
| 	sliceStore cache.Store |  | ||||||
|  |  | ||||||
| 	mutex         sync.RWMutex |  | ||||||
| 	activePlugins map[string]*activePlugin |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // activePlugin holds the resource information about one plugin |  | ||||||
| // and the gRPC stream that is used to retrieve that. The context |  | ||||||
| // used by that stream can be canceled separately to stop |  | ||||||
| // the monitoring. |  | ||||||
| type activePlugin struct { |  | ||||||
| 	// cancel is the function which cancels the monitorPlugin goroutine |  | ||||||
| 	// for this plugin. |  | ||||||
| 	cancel func(reason error) |  | ||||||
|  |  | ||||||
| 	// resources is protected by the nodeResourcesController read/write lock. |  | ||||||
| 	// When receiving updates from the driver, the entire slice gets replaced, |  | ||||||
| 	// so it is okay to not do a deep copy of it. Only retrieving the slice |  | ||||||
| 	// must be protected by a read lock. |  | ||||||
| 	resources []*resourceapi.ResourceModel |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // startNodeResourcesController constructs a new controller and starts it. |  | ||||||
| // |  | ||||||
| // If a kubeClient is provided, then it synchronizes ResourceSlices |  | ||||||
| // with the resource information provided by plugins. Without it, |  | ||||||
| // the controller is inactive. This can happen when kubelet is run stand-alone |  | ||||||
| // without an apiserver. In that case we can't and don't need to publish |  | ||||||
| // ResourceSlices. |  | ||||||
| func startNodeResourcesController(ctx context.Context, kubeClient kubernetes.Interface, getNode func() (*v1.Node, error)) *nodeResourcesController { |  | ||||||
| 	if kubeClient == nil { |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	logger := klog.FromContext(ctx) |  | ||||||
| 	logger = klog.LoggerWithName(logger, "node resources controller") |  | ||||||
| 	ctx = klog.NewContext(ctx, logger) |  | ||||||
|  |  | ||||||
| 	c := &nodeResourcesController{ |  | ||||||
| 		ctx:        ctx, |  | ||||||
| 		kubeClient: kubeClient, |  | ||||||
| 		getNode:    getNode, |  | ||||||
| 		queue: workqueue.NewTypedRateLimitingQueueWithConfig( |  | ||||||
| 			workqueue.DefaultTypedControllerRateLimiter[string](), |  | ||||||
| 			workqueue.TypedRateLimitingQueueConfig[string]{Name: "node_resource_slices"}, |  | ||||||
| 		), |  | ||||||
| 		activePlugins: make(map[string]*activePlugin), |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	c.wg.Add(1) |  | ||||||
| 	go func() { |  | ||||||
| 		defer c.wg.Done() |  | ||||||
| 		c.run(ctx) |  | ||||||
| 	}() |  | ||||||
|  |  | ||||||
| 	return c |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // waitForStop blocks until all background activity spawned by |  | ||||||
| // the controller has stopped. The context passed to start must |  | ||||||
| // be canceled for that to happen. |  | ||||||
| // |  | ||||||
| // Not needed at the moment, but if it was, this is what it would |  | ||||||
| // look like... |  | ||||||
| // func (c *nodeResourcesController) waitForStop() { |  | ||||||
| // 	if c == nil { |  | ||||||
| // 		return |  | ||||||
| // 	} |  | ||||||
| // |  | ||||||
| // 	c.wg.Wait() |  | ||||||
| // } |  | ||||||
|  |  | ||||||
| // addPlugin is called whenever a plugin has been (re-)registered. |  | ||||||
| func (c *nodeResourcesController) addPlugin(driverName string, pluginInstance *plugin) { |  | ||||||
| 	if c == nil { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	klog.FromContext(c.ctx).V(2).Info("Adding plugin", "driverName", driverName) |  | ||||||
| 	c.mutex.Lock() |  | ||||||
| 	defer c.mutex.Unlock() |  | ||||||
|  |  | ||||||
| 	if active := c.activePlugins[driverName]; active != nil { |  | ||||||
| 		active.cancel(errors.New("plugin has re-registered")) |  | ||||||
| 	} |  | ||||||
| 	active := &activePlugin{} |  | ||||||
| 	cancelCtx, cancel := context.WithCancelCause(c.ctx) |  | ||||||
| 	active.cancel = cancel |  | ||||||
| 	c.activePlugins[driverName] = active |  | ||||||
| 	c.queue.Add(driverName) |  | ||||||
|  |  | ||||||
| 	c.wg.Add(1) |  | ||||||
| 	go func() { |  | ||||||
| 		defer c.wg.Done() |  | ||||||
| 		c.monitorPlugin(cancelCtx, active, driverName, pluginInstance) |  | ||||||
| 	}() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // removePlugin is called whenever a plugin has been unregistered. |  | ||||||
| func (c *nodeResourcesController) removePlugin(driverName string) { |  | ||||||
| 	if c == nil { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	klog.FromContext(c.ctx).V(2).Info("Removing plugin", "driverName", driverName) |  | ||||||
| 	c.mutex.Lock() |  | ||||||
| 	defer c.mutex.Unlock() |  | ||||||
| 	if active, ok := c.activePlugins[driverName]; ok { |  | ||||||
| 		active.cancel(errors.New("plugin has unregistered")) |  | ||||||
| 		delete(c.activePlugins, driverName) |  | ||||||
| 		c.queue.Add(driverName) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // monitorPlugin calls the plugin to retrieve resource information and caches |  | ||||||
| // all responses that it gets for processing in the sync method. It keeps |  | ||||||
| // retrying until an error or EOF response indicates that no further data is |  | ||||||
| // going to be sent, then watch resources of the plugin stops until it |  | ||||||
| // re-registers. |  | ||||||
| func (c *nodeResourcesController) monitorPlugin(ctx context.Context, active *activePlugin, driverName string, pluginInstance *plugin) { |  | ||||||
| 	logger := klog.FromContext(ctx) |  | ||||||
| 	logger = klog.LoggerWithValues(logger, "driverName", driverName) |  | ||||||
| 	logger.Info("Starting to monitor node resources of the plugin") |  | ||||||
| 	defer func() { |  | ||||||
| 		r := recover() |  | ||||||
| 		logger.Info("Stopping to monitor node resources of the plugin", "reason", context.Cause(ctx), "err", ctx.Err(), "recover", r) |  | ||||||
| 	}() |  | ||||||
|  |  | ||||||
| 	backOff := flowcontrol.NewBackOffWithJitter(retryPeriod, maxRetryPeriod, backoffFactor) |  | ||||||
| 	backOffID := "retry" |  | ||||||
|  |  | ||||||
| 	// Keep trying until canceled. |  | ||||||
| 	for ctx.Err() == nil { |  | ||||||
| 		logger.V(5).Info("Calling NodeListAndWatchResources") |  | ||||||
| 		stream, err := pluginInstance.NodeListAndWatchResources(ctx, new(drapb.NodeListAndWatchResourcesRequest)) |  | ||||||
| 		if err != nil { |  | ||||||
| 			switch { |  | ||||||
| 			case status.Convert(err).Code() == codes.Unimplemented: |  | ||||||
| 				// The plugin simply doesn't provide node resources. |  | ||||||
| 				active.cancel(errors.New("plugin does not support node resource reporting")) |  | ||||||
| 			default: |  | ||||||
| 				// This is a problem, report it and retry. |  | ||||||
| 				logger.Error(err, "Creating gRPC stream for node resources failed") |  | ||||||
| 				select { |  | ||||||
| 				case <-time.After(backOff.Get(backOffID)): |  | ||||||
| 					backOff.Next(backOffID, time.Now()) |  | ||||||
| 				case <-ctx.Done(): |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 		for { |  | ||||||
| 			response, err := stream.Recv() |  | ||||||
| 			if err != nil { |  | ||||||
| 				switch { |  | ||||||
| 				case errors.Is(err, io.EOF): |  | ||||||
| 					// This is okay. Some plugins might never change their |  | ||||||
| 					// resources after reporting them once. |  | ||||||
| 					active.cancel(errors.New("plugin has closed the stream")) |  | ||||||
| 				case status.Convert(err).Code() == codes.Unimplemented: |  | ||||||
| 					// The plugin has the method, does not really implement it. |  | ||||||
| 					active.cancel(errors.New("plugin does not support node resource reporting")) |  | ||||||
| 				case ctx.Err() == nil: |  | ||||||
| 					// This is a problem, report it and retry. |  | ||||||
| 					logger.Error(err, "Reading node resources from gRPC stream failed") |  | ||||||
| 					select { |  | ||||||
| 					case <-time.After(backOff.Get(backOffID)): |  | ||||||
| 						backOff.Next(backOffID, time.Now()) |  | ||||||
| 					case <-ctx.Done(): |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 				break |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if loggerV := logger.V(6); loggerV.Enabled() { |  | ||||||
| 				loggerV.Info("Driver resources updated", "resources", response.Resources) |  | ||||||
| 			} else { |  | ||||||
| 				logger.V(5).Info("Driver resources updated", "numResources", len(response.Resources)) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			c.mutex.Lock() |  | ||||||
| 			active.resources = response.Resources |  | ||||||
| 			c.mutex.Unlock() |  | ||||||
| 			c.queue.Add(driverName) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // run is running in the background. It handles blocking initialization (like |  | ||||||
| // syncing the informer) and then syncs the actual with the desired state. |  | ||||||
| func (c *nodeResourcesController) run(ctx context.Context) { |  | ||||||
| 	logger := klog.FromContext(ctx) |  | ||||||
|  |  | ||||||
| 	// When kubelet starts, we have two choices: |  | ||||||
| 	// - Sync immediately, which in practice will delete all ResourceSlices |  | ||||||
| 	//   because no plugin has registered yet. We could do a DeleteCollection |  | ||||||
| 	//   to speed this up. |  | ||||||
| 	// - Wait a bit, then sync. If all plugins have re-registered in the meantime, |  | ||||||
| 	//   we might not need to change any ResourceSlice. |  | ||||||
| 	// |  | ||||||
| 	// For now syncing starts immediately, with no DeleteCollection. This |  | ||||||
| 	// can be reconsidered later. |  | ||||||
|  |  | ||||||
| 	// Wait until we're able to get a Node object. |  | ||||||
| 	// This means that the object is created on the API server, |  | ||||||
| 	// the kubeclient is functional and the node informer cache is populated with the node object. |  | ||||||
| 	// Without this it doesn't make sense to proceed further as we need a node name and |  | ||||||
| 	// a node UID for this controller to work. |  | ||||||
| 	var node *v1.Node |  | ||||||
| 	var err error |  | ||||||
| 	for { |  | ||||||
| 		node, err = c.getNode() |  | ||||||
| 		if err == nil { |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
| 		logger.V(5).Info("Getting Node object failed, waiting", "err", err) |  | ||||||
| 		select { |  | ||||||
| 		case <-ctx.Done(): |  | ||||||
| 			return |  | ||||||
| 		case <-time.After(time.Second): |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// We could use an indexer on driver name, but that seems overkill. |  | ||||||
| 	informer := resourceinformers.NewFilteredResourceSliceInformer(c.kubeClient, resyncPeriod, nil, func(options *metav1.ListOptions) { |  | ||||||
| 		options.FieldSelector = "nodeName=" + node.Name |  | ||||||
| 	}) |  | ||||||
| 	c.sliceStore = informer.GetStore() |  | ||||||
| 	handler, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ |  | ||||||
| 		AddFunc: func(obj any) { |  | ||||||
| 			slice, ok := obj.(*resourceapi.ResourceSlice) |  | ||||||
| 			if !ok { |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 			logger.V(5).Info("ResourceSlice add", "slice", klog.KObj(slice)) |  | ||||||
| 			c.queue.Add(slice.DriverName) |  | ||||||
| 		}, |  | ||||||
| 		UpdateFunc: func(old, new any) { |  | ||||||
| 			oldSlice, ok := old.(*resourceapi.ResourceSlice) |  | ||||||
| 			if !ok { |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 			newSlice, ok := new.(*resourceapi.ResourceSlice) |  | ||||||
| 			if !ok { |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 			if loggerV := logger.V(6); loggerV.Enabled() { |  | ||||||
| 				loggerV.Info("ResourceSlice update", "slice", klog.KObj(newSlice), "diff", cmp.Diff(oldSlice, newSlice)) |  | ||||||
| 			} else { |  | ||||||
| 				logger.V(5).Info("ResourceSlice update", "slice", klog.KObj(newSlice)) |  | ||||||
| 			} |  | ||||||
| 			c.queue.Add(newSlice.DriverName) |  | ||||||
| 		}, |  | ||||||
| 		DeleteFunc: func(obj any) { |  | ||||||
| 			if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok { |  | ||||||
| 				obj = tombstone.Obj |  | ||||||
| 			} |  | ||||||
| 			slice, ok := obj.(*resourceapi.ResourceSlice) |  | ||||||
| 			if !ok { |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 			logger.V(5).Info("ResourceSlice delete", "slice", klog.KObj(slice)) |  | ||||||
| 			c.queue.Add(slice.DriverName) |  | ||||||
| 		}, |  | ||||||
| 	}) |  | ||||||
| 	if err != nil { |  | ||||||
| 		logger.Error(err, "Registering event handler on the ResourceSlice informer failed, disabling resource monitoring") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Start informer and wait for our cache to be populated. |  | ||||||
| 	c.wg.Add(1) |  | ||||||
| 	go func() { |  | ||||||
| 		defer c.wg.Done() |  | ||||||
| 		informer.Run(ctx.Done()) |  | ||||||
| 	}() |  | ||||||
| 	for !handler.HasSynced() { |  | ||||||
| 		select { |  | ||||||
| 		case <-time.After(time.Second): |  | ||||||
| 		case <-ctx.Done(): |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	logger.Info("ResourceSlice informer has synced") |  | ||||||
|  |  | ||||||
| 	for c.processNextWorkItem(ctx) { |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (c *nodeResourcesController) processNextWorkItem(ctx context.Context) bool { |  | ||||||
| 	key, shutdown := c.queue.Get() |  | ||||||
| 	if shutdown { |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
| 	defer c.queue.Done(key) |  | ||||||
|  |  | ||||||
| 	driverName := key |  | ||||||
|  |  | ||||||
| 	// Panics are caught and treated like errors. |  | ||||||
| 	var err error |  | ||||||
| 	func() { |  | ||||||
| 		defer func() { |  | ||||||
| 			if r := recover(); r != nil { |  | ||||||
| 				err = fmt.Errorf("internal error: %v", r) |  | ||||||
| 			} |  | ||||||
| 		}() |  | ||||||
| 		err = c.sync(ctx, driverName) |  | ||||||
| 	}() |  | ||||||
|  |  | ||||||
| 	if err != nil { |  | ||||||
| 		// TODO (https://github.com/kubernetes/enhancements/issues/3077): contextual logging in utilruntime |  | ||||||
| 		utilruntime.HandleError(fmt.Errorf("processing driver %v: %v", driverName, err)) |  | ||||||
| 		c.queue.AddRateLimited(key) |  | ||||||
|  |  | ||||||
| 		// Return without removing the work item from the queue. |  | ||||||
| 		// It will be retried. |  | ||||||
| 		return true |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	c.queue.Forget(key) |  | ||||||
| 	return true |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (c *nodeResourcesController) sync(ctx context.Context, driverName string) error { |  | ||||||
| 	logger := klog.FromContext(ctx) |  | ||||||
|  |  | ||||||
| 	// Gather information about the actual and desired state. |  | ||||||
| 	slices := c.sliceStore.List() |  | ||||||
| 	var driverResources []*resourceapi.ResourceModel |  | ||||||
| 	c.mutex.RLock() |  | ||||||
| 	if active, ok := c.activePlugins[driverName]; ok { |  | ||||||
| 		// No need for a deep copy, the entire slice gets replaced on writes. |  | ||||||
| 		driverResources = active.resources |  | ||||||
| 	} |  | ||||||
| 	c.mutex.RUnlock() |  | ||||||
|  |  | ||||||
| 	// Resources that are not yet stored in any slice need to be published. |  | ||||||
| 	// Here we track the indices of any resources that are already stored. |  | ||||||
| 	storedResourceIndices := sets.New[int]() |  | ||||||
|  |  | ||||||
| 	// Slices that don't match any driver resource can either be updated (if there |  | ||||||
| 	// are new driver resources that need to be stored) or they need to be deleted. |  | ||||||
| 	obsoleteSlices := make([]*resourceapi.ResourceSlice, 0, len(slices)) |  | ||||||
|  |  | ||||||
| 	// Match slices with resource information. |  | ||||||
| 	for _, obj := range slices { |  | ||||||
| 		slice := obj.(*resourceapi.ResourceSlice) |  | ||||||
| 		if slice.DriverName != driverName { |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		index := indexOfModel(driverResources, &slice.ResourceModel) |  | ||||||
| 		if index >= 0 { |  | ||||||
| 			storedResourceIndices.Insert(index) |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		obsoleteSlices = append(obsoleteSlices, slice) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if loggerV := logger.V(6); loggerV.Enabled() { |  | ||||||
| 		// Dump entire resource information. |  | ||||||
| 		loggerV.Info("Syncing existing driver node resource slices with driver resources", "slices", klog.KObjSlice(slices), "resources", driverResources) |  | ||||||
| 	} else { |  | ||||||
| 		logger.V(5).Info("Syncing existing driver node resource slices with driver resources", "slices", klog.KObjSlice(slices), "numResources", len(driverResources)) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Update stale slices before removing what's left. |  | ||||||
| 	// |  | ||||||
| 	// We don't really know which of these slices might have |  | ||||||
| 	// been used for "the" driver resource because they don't |  | ||||||
| 	// have a unique ID. In practice, a driver is most likely |  | ||||||
| 	// to just give us one ResourceModel, in which case |  | ||||||
| 	// this isn't a problem at all. If we have more than one, |  | ||||||
| 	// then at least conceptually it currently doesn't matter |  | ||||||
| 	// where we publish it. |  | ||||||
| 	// |  | ||||||
| 	// The long-term goal is to move the handling of |  | ||||||
| 	// ResourceSlice objects into the driver, with kubelet |  | ||||||
| 	// just acting as a REST proxy. The advantage of that will |  | ||||||
| 	// be that kubelet won't need to support the same |  | ||||||
| 	// resource API version as the driver and the control plane. |  | ||||||
| 	// With that approach, the driver will be able to match |  | ||||||
| 	// up objects more intelligently. |  | ||||||
| 	numObsoleteSlices := len(obsoleteSlices) |  | ||||||
| 	for index, resource := range driverResources { |  | ||||||
| 		if storedResourceIndices.Has(index) { |  | ||||||
| 			// No need to do anything, it is already stored exactly |  | ||||||
| 			// like this in an existing slice. |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if numObsoleteSlices > 0 { |  | ||||||
| 			// Update one existing slice. |  | ||||||
| 			slice := obsoleteSlices[numObsoleteSlices-1] |  | ||||||
| 			numObsoleteSlices-- |  | ||||||
| 			slice = slice.DeepCopy() |  | ||||||
| 			slice.ResourceModel = *resource |  | ||||||
| 			logger.V(5).Info("Reusing existing node resource slice", "slice", klog.KObj(slice)) |  | ||||||
| 			if _, err := c.kubeClient.ResourceV1alpha2().ResourceSlices().Update(ctx, slice, metav1.UpdateOptions{}); err != nil { |  | ||||||
| 				return fmt.Errorf("update node resource slice: %w", err) |  | ||||||
| 			} |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// Although node name and UID are unlikely to change |  | ||||||
| 		// we're getting updated node object just to be on the safe side. |  | ||||||
| 		// It's a cheap operation as it gets an object from the node informer cache. |  | ||||||
| 		node, err := c.getNode() |  | ||||||
| 		if err != nil { |  | ||||||
| 			return fmt.Errorf("retrieve node object: %w", err) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// Create a new slice. |  | ||||||
| 		slice := &resourceapi.ResourceSlice{ |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				GenerateName: node.Name + "-" + driverName + "-", |  | ||||||
| 				OwnerReferences: []metav1.OwnerReference{ |  | ||||||
| 					{ |  | ||||||
| 						APIVersion: v1.SchemeGroupVersion.WithKind("Node").Version, |  | ||||||
| 						Kind:       v1.SchemeGroupVersion.WithKind("Node").Kind, |  | ||||||
| 						Name:       node.Name, |  | ||||||
| 						UID:        node.UID, |  | ||||||
| 						Controller: ptr.To(true), |  | ||||||
| 					}, |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			NodeName:      node.Name, |  | ||||||
| 			DriverName:    driverName, |  | ||||||
| 			ResourceModel: *resource, |  | ||||||
| 		} |  | ||||||
| 		logger.V(5).Info("Creating new node resource slice", "slice", klog.KObj(slice)) |  | ||||||
| 		if _, err := c.kubeClient.ResourceV1alpha2().ResourceSlices().Create(ctx, slice, metav1.CreateOptions{}); err != nil { |  | ||||||
| 			return fmt.Errorf("create node resource slice: %w", err) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// All remaining slices are truly orphaned. |  | ||||||
| 	for i := 0; i < numObsoleteSlices; i++ { |  | ||||||
| 		slice := obsoleteSlices[i] |  | ||||||
| 		logger.V(5).Info("Deleting obsolete node resource slice", "slice", klog.KObj(slice)) |  | ||||||
| 		if err := c.kubeClient.ResourceV1alpha2().ResourceSlices().Delete(ctx, slice.Name, metav1.DeleteOptions{}); err != nil { |  | ||||||
| 			return fmt.Errorf("delete node resource slice: %w", err) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func indexOfModel(models []*resourceapi.ResourceModel, model *resourceapi.ResourceModel) int { |  | ||||||
| 	for index, m := range models { |  | ||||||
| 		if apiequality.Semantic.DeepEqual(m, model) { |  | ||||||
| 			return index |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return -1 |  | ||||||
| } |  | ||||||
| @@ -20,94 +20,125 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net" |  | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" |  | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"google.golang.org/grpc" |  | ||||||
| 	"google.golang.org/grpc/connectivity" |  | ||||||
| 	"google.golang.org/grpc/credentials/insecure" |  | ||||||
| 	v1 "k8s.io/api/core/v1" | 	v1 "k8s.io/api/core/v1" | ||||||
|  | 	apierrors "k8s.io/apimachinery/pkg/api/errors" | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/fields" | ||||||
| 	utilversion "k8s.io/apimachinery/pkg/util/version" | 	utilversion "k8s.io/apimachinery/pkg/util/version" | ||||||
|  | 	"k8s.io/apimachinery/pkg/util/wait" | ||||||
| 	"k8s.io/client-go/kubernetes" | 	"k8s.io/client-go/kubernetes" | ||||||
| 	"k8s.io/klog/v2" | 	"k8s.io/klog/v2" | ||||||
|  | 	"k8s.io/kubernetes/pkg/kubelet/pluginmanager/cache" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	// DRAPluginName is the name of the in-tree DRA Plugin. |  | ||||||
| 	DRAPluginName   = "kubernetes.io/dra" |  | ||||||
| 	v1alpha3Version = "v1alpha3" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // Plugin is a description of a DRA Plugin, defined by an endpoint. |  | ||||||
| type plugin struct { |  | ||||||
| 	sync.Mutex |  | ||||||
| 	conn                    *grpc.ClientConn |  | ||||||
| 	endpoint                string |  | ||||||
| 	highestSupportedVersion *utilversion.Version |  | ||||||
| 	clientTimeout           time.Duration |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (p *plugin) getOrCreateGRPCConn() (*grpc.ClientConn, error) { |  | ||||||
| 	p.Lock() |  | ||||||
| 	defer p.Unlock() |  | ||||||
|  |  | ||||||
| 	if p.conn != nil { |  | ||||||
| 		return p.conn, nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	network := "unix" |  | ||||||
| 	klog.V(4).InfoS(log("creating new gRPC connection"), "protocol", network, "endpoint", p.endpoint) |  | ||||||
| 	conn, err := grpc.Dial( |  | ||||||
| 		p.endpoint, |  | ||||||
| 		grpc.WithTransportCredentials(insecure.NewCredentials()), |  | ||||||
| 		grpc.WithContextDialer(func(ctx context.Context, target string) (net.Conn, error) { |  | ||||||
| 			return (&net.Dialer{}).DialContext(ctx, network, target) |  | ||||||
| 		}), |  | ||||||
| 	) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	ctx, cancel := context.WithTimeout(context.Background(), time.Second) |  | ||||||
| 	defer cancel() |  | ||||||
|  |  | ||||||
| 	if ok := conn.WaitForStateChange(ctx, connectivity.Connecting); !ok { |  | ||||||
| 		return nil, errors.New("timed out waiting for gRPC connection to be ready") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	p.conn = conn |  | ||||||
| 	return p.conn, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // RegistrationHandler is the handler which is fed to the pluginwatcher API. | // RegistrationHandler is the handler which is fed to the pluginwatcher API. | ||||||
| type RegistrationHandler struct { | type RegistrationHandler struct { | ||||||
| 	controller *nodeResourcesController | 	// backgroundCtx is used for all future activities of the handler. | ||||||
|  | 	// This is necessary because it implements APIs which don't | ||||||
|  | 	// provide a context. | ||||||
|  | 	backgroundCtx context.Context | ||||||
|  | 	kubeClient    kubernetes.Interface | ||||||
|  | 	getNode       func() (*v1.Node, error) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | var _ cache.PluginHandler = &RegistrationHandler{} | ||||||
|  |  | ||||||
| // NewPluginHandler returns new registration handler. | // NewPluginHandler returns new registration handler. | ||||||
| // | // | ||||||
| // Must only be called once per process because it manages global state. | // Must only be called once per process because it manages global state. | ||||||
| // If a kubeClient is provided, then it synchronizes ResourceSlices | // If a kubeClient is provided, then it synchronizes ResourceSlices | ||||||
| // with the resource information provided by plugins. | // with the resource information provided by plugins. | ||||||
| func NewRegistrationHandler(kubeClient kubernetes.Interface, getNode func() (*v1.Node, error)) *RegistrationHandler { | func NewRegistrationHandler(kubeClient kubernetes.Interface, getNode func() (*v1.Node, error)) *RegistrationHandler { | ||||||
| 	handler := &RegistrationHandler{} | 	handler := &RegistrationHandler{ | ||||||
|  | 		// The context and thus logger should come from the caller. | ||||||
|  | 		backgroundCtx: klog.NewContext(context.TODO(), klog.LoggerWithName(klog.TODO(), "DRA registration handler")), | ||||||
|  | 		kubeClient:    kubeClient, | ||||||
|  | 		getNode:       getNode, | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// If kubelet ever gets an API for stopping registration handlers, then | 	// When kubelet starts up, no DRA driver has registered yet. None of | ||||||
| 	// that would need to be hooked up with stopping the controller. | 	// the drivers are usable until they come back, which might not happen | ||||||
| 	handler.controller = startNodeResourcesController(context.TODO(), kubeClient, getNode) | 	// at all. Therefore it is better to not advertise any local resources | ||||||
|  | 	// because pods could get stuck on the node waiting for the driver | ||||||
|  | 	// to start up. | ||||||
|  | 	// | ||||||
|  | 	// This has to run in the background. | ||||||
|  | 	go handler.wipeResourceSlices("") | ||||||
|  |  | ||||||
| 	return handler | 	return handler | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // wipeResourceSlices deletes ResourceSlices of the node, optionally just for a specific driver. | ||||||
|  | func (h *RegistrationHandler) wipeResourceSlices(pluginName string) { | ||||||
|  | 	if h.kubeClient == nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	ctx := h.backgroundCtx | ||||||
|  | 	logger := klog.FromContext(ctx) | ||||||
|  |  | ||||||
|  | 	backoff := wait.Backoff{ | ||||||
|  | 		Duration: time.Second, | ||||||
|  | 		Factor:   2, | ||||||
|  | 		Jitter:   0.2, | ||||||
|  | 		Cap:      5 * time.Minute, | ||||||
|  | 		Steps:    100, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Error logging is done inside the loop. Context cancellation doesn't get logged. | ||||||
|  | 	_ = wait.ExponentialBackoffWithContext(ctx, backoff, func(ctx context.Context) (bool, error) { | ||||||
|  | 		node, err := h.getNode() | ||||||
|  | 		if apierrors.IsNotFound(err) { | ||||||
|  | 			return false, nil | ||||||
|  | 		} | ||||||
|  | 		if err != nil { | ||||||
|  | 			logger.Error(err, "Unexpected error checking for node") | ||||||
|  | 			return false, nil | ||||||
|  | 		} | ||||||
|  | 		fieldSelector := fields.Set{"nodeName": node.Name} | ||||||
|  | 		if pluginName != "" { | ||||||
|  | 			fieldSelector["driverName"] = pluginName | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		err = h.kubeClient.ResourceV1alpha2().ResourceSlices().DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{FieldSelector: fieldSelector.String()}) | ||||||
|  | 		switch { | ||||||
|  | 		case err == nil: | ||||||
|  | 			logger.V(3).Info("Deleted ResourceSlices", "fieldSelector", fieldSelector) | ||||||
|  | 			return true, nil | ||||||
|  | 		case apierrors.IsUnauthorized(err): | ||||||
|  | 			// This can happen while kubelet is still figuring out | ||||||
|  | 			// its credentials. | ||||||
|  | 			logger.V(5).Info("Deleting ResourceSlice failed, retrying", "fieldSelector", fieldSelector, "err", err) | ||||||
|  | 			return false, nil | ||||||
|  | 		default: | ||||||
|  | 			// Log and retry for other errors. | ||||||
|  | 			logger.V(3).Info("Deleting ResourceSlice failed, retrying", "fieldSelector", fieldSelector, "err", err) | ||||||
|  | 			return false, nil | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
| // RegisterPlugin is called when a plugin can be registered. | // RegisterPlugin is called when a plugin can be registered. | ||||||
| func (h *RegistrationHandler) RegisterPlugin(pluginName string, endpoint string, versions []string, pluginClientTimeout *time.Duration) error { | func (h *RegistrationHandler) RegisterPlugin(pluginName string, endpoint string, versions []string, pluginClientTimeout *time.Duration) error { | ||||||
| 	klog.InfoS("Register new DRA plugin", "name", pluginName, "endpoint", endpoint) | 	// Prepare a context with its own logger for the plugin. | ||||||
|  | 	// | ||||||
|  | 	// The lifecycle of the plugin's background activities is tied to our | ||||||
|  | 	// root context, so canceling that will also cancel the plugin. | ||||||
|  | 	// | ||||||
|  | 	// The logger injects the plugin name as additional value | ||||||
|  | 	// into all log output related to the plugin. | ||||||
|  | 	ctx := h.backgroundCtx | ||||||
|  | 	logger := klog.FromContext(ctx) | ||||||
|  | 	logger = klog.LoggerWithValues(logger, "pluginName", pluginName) | ||||||
|  | 	ctx = klog.NewContext(ctx, logger) | ||||||
|  |  | ||||||
| 	highestSupportedVersion, err := h.validateVersions("RegisterPlugin", pluginName, versions) | 	logger.V(3).Info("Register new DRA plugin", "endpoint", endpoint) | ||||||
|  |  | ||||||
|  | 	highestSupportedVersion, err := h.validateVersions(pluginName, versions) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return fmt.Errorf("version check of plugin %s failed: %w", pluginName, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var timeout time.Duration | 	var timeout time.Duration | ||||||
| @@ -117,7 +148,11 @@ func (h *RegistrationHandler) RegisterPlugin(pluginName string, endpoint string, | |||||||
| 		timeout = *pluginClientTimeout | 		timeout = *pluginClientTimeout | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	pluginInstance := &plugin{ | 	ctx, cancel := context.WithCancelCause(ctx) | ||||||
|  |  | ||||||
|  | 	pluginInstance := &Plugin{ | ||||||
|  | 		backgroundCtx:           ctx, | ||||||
|  | 		cancel:                  cancel, | ||||||
| 		conn:                    nil, | 		conn:                    nil, | ||||||
| 		endpoint:                endpoint, | 		endpoint:                endpoint, | ||||||
| 		highestSupportedVersion: highestSupportedVersion, | 		highestSupportedVersion: highestSupportedVersion, | ||||||
| @@ -126,40 +161,27 @@ func (h *RegistrationHandler) RegisterPlugin(pluginName string, endpoint string, | |||||||
|  |  | ||||||
| 	// Storing endpoint of newly registered DRA Plugin into the map, where plugin name will be the key | 	// Storing endpoint of newly registered DRA Plugin into the map, where plugin name will be the key | ||||||
| 	// all other DRA components will be able to get the actual socket of DRA plugins by its name. | 	// all other DRA components will be able to get the actual socket of DRA plugins by its name. | ||||||
| 	// By default we assume the supported plugin version is v1alpha3 | 	if draPlugins.add(pluginName, pluginInstance) { | ||||||
| 	draPlugins.add(pluginName, pluginInstance) | 		logger.V(1).Info("Already registered, previous plugin was replaced") | ||||||
| 	h.controller.addPlugin(pluginName, pluginInstance) | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (h *RegistrationHandler) validateVersions( | func (h *RegistrationHandler) validateVersions( | ||||||
| 	callerName string, |  | ||||||
| 	pluginName string, | 	pluginName string, | ||||||
| 	versions []string, | 	versions []string, | ||||||
| ) (*utilversion.Version, error) { | ) (*utilversion.Version, error) { | ||||||
| 	if len(versions) == 0 { | 	if len(versions) == 0 { | ||||||
| 		return nil, errors.New( | 		return nil, errors.New("empty list for supported versions") | ||||||
| 			log( |  | ||||||
| 				"%s for DRA plugin %q failed. Plugin returned an empty list for supported versions", |  | ||||||
| 				callerName, |  | ||||||
| 				pluginName, |  | ||||||
| 			), |  | ||||||
| 		) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Validate version | 	// Validate version | ||||||
| 	newPluginHighestVersion, err := utilversion.HighestSupportedVersion(versions) | 	newPluginHighestVersion, err := utilversion.HighestSupportedVersion(versions) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, errors.New( | 		// HighestSupportedVersion includes the list of versions in its error | ||||||
| 			log( | 		// if relevant, no need to repeat it here. | ||||||
| 				"%s for DRA plugin %q failed. None of the versions specified %q are supported. err=%v", | 		return nil, fmt.Errorf("none of the versions are supported: %w", err) | ||||||
| 				callerName, |  | ||||||
| 				pluginName, |  | ||||||
| 				versions, |  | ||||||
| 				err, |  | ||||||
| 			), |  | ||||||
| 		) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	existingPlugin := draPlugins.get(pluginName) | 	existingPlugin := draPlugins.get(pluginName) | ||||||
| @@ -169,26 +191,26 @@ func (h *RegistrationHandler) validateVersions( | |||||||
| 	if existingPlugin.highestSupportedVersion.LessThan(newPluginHighestVersion) { | 	if existingPlugin.highestSupportedVersion.LessThan(newPluginHighestVersion) { | ||||||
| 		return newPluginHighestVersion, nil | 		return newPluginHighestVersion, nil | ||||||
| 	} | 	} | ||||||
| 	return nil, errors.New( | 	return nil, fmt.Errorf("another plugin instance is already registered with a higher supported version: %q < %q", newPluginHighestVersion, existingPlugin.highestSupportedVersion) | ||||||
| 		log( |  | ||||||
| 			"%s for DRA plugin %q failed. Another plugin with the same name is already registered with a higher supported version: %q", |  | ||||||
| 			callerName, |  | ||||||
| 			pluginName, |  | ||||||
| 			existingPlugin.highestSupportedVersion, |  | ||||||
| 		), |  | ||||||
| 	) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func deregisterPlugin(pluginName string) { |  | ||||||
| 	draPlugins.delete(pluginName) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // DeRegisterPlugin is called when a plugin has removed its socket, | // DeRegisterPlugin is called when a plugin has removed its socket, | ||||||
| // signaling it is no longer available. | // signaling it is no longer available. | ||||||
| func (h *RegistrationHandler) DeRegisterPlugin(pluginName string) { | func (h *RegistrationHandler) DeRegisterPlugin(pluginName string) { | ||||||
| 	klog.InfoS("DeRegister DRA plugin", "name", pluginName) | 	if p := draPlugins.delete(pluginName); p != nil { | ||||||
| 	deregisterPlugin(pluginName) | 		logger := klog.FromContext(p.backgroundCtx) | ||||||
| 	h.controller.removePlugin(pluginName) | 		logger.V(3).Info("Deregister DRA plugin", "endpoint", p.endpoint) | ||||||
|  |  | ||||||
|  | 		// Clean up the ResourceSlices for the deleted Plugin since it | ||||||
|  | 		// may have died without doing so itself and might never come | ||||||
|  | 		// back. | ||||||
|  | 		go h.wipeResourceSlices(pluginName) | ||||||
|  |  | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	logger := klog.FromContext(h.backgroundCtx) | ||||||
|  | 	logger.V(3).Info("Deregister DRA plugin not necessary, was already removed") | ||||||
| } | } | ||||||
|  |  | ||||||
| // ValidatePlugin is called by kubelet's plugin watcher upon detection | // ValidatePlugin is called by kubelet's plugin watcher upon detection | ||||||
| @@ -196,15 +218,10 @@ func (h *RegistrationHandler) DeRegisterPlugin(pluginName string) { | |||||||
| func (h *RegistrationHandler) ValidatePlugin(pluginName string, endpoint string, versions []string) error { | func (h *RegistrationHandler) ValidatePlugin(pluginName string, endpoint string, versions []string) error { | ||||||
| 	klog.InfoS("Validate DRA plugin", "name", pluginName, "endpoint", endpoint, "versions", strings.Join(versions, ",")) | 	klog.InfoS("Validate DRA plugin", "name", pluginName, "endpoint", endpoint, "versions", strings.Join(versions, ",")) | ||||||
|  |  | ||||||
| 	_, err := h.validateVersions("ValidatePlugin", pluginName, versions) | 	_, err := h.validateVersions(pluginName, versions) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("validation failed for DRA plugin %s at endpoint %s: %+v", pluginName, endpoint, err) | 		return fmt.Errorf("invalid versions of plugin %s: %w", pluginName, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  |  | ||||||
| // log prepends log string with `kubernetes.io/dra`. |  | ||||||
| func log(msg string, parts ...interface{}) string { |  | ||||||
| 	return fmt.Sprintf(fmt.Sprintf("%s: %s", DRAPluginName, msg), parts...) |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -17,15 +17,14 @@ limitations under the License. | |||||||
| package plugin | package plugin | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"sync" | 	"sync" | ||||||
|  |  | ||||||
| 	"k8s.io/klog/v2" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // PluginsStore holds a list of DRA Plugins. | // PluginsStore holds a list of DRA Plugins. | ||||||
| type pluginsStore struct { | type pluginsStore struct { | ||||||
| 	sync.RWMutex | 	sync.RWMutex | ||||||
| 	store map[string]*plugin | 	store map[string]*Plugin | ||||||
| } | } | ||||||
|  |  | ||||||
| // draPlugins map keeps track of all registered DRA plugins on the node | // draPlugins map keeps track of all registered DRA plugins on the node | ||||||
| @@ -34,7 +33,7 @@ var draPlugins = &pluginsStore{} | |||||||
|  |  | ||||||
| // Get lets you retrieve a DRA Plugin by name. | // Get lets you retrieve a DRA Plugin by name. | ||||||
| // This method is protected by a mutex. | // This method is protected by a mutex. | ||||||
| func (s *pluginsStore) get(pluginName string) *plugin { | func (s *pluginsStore) get(pluginName string) *Plugin { | ||||||
| 	s.RLock() | 	s.RLock() | ||||||
| 	defer s.RUnlock() | 	defer s.RUnlock() | ||||||
|  |  | ||||||
| @@ -43,26 +42,33 @@ func (s *pluginsStore) get(pluginName string) *plugin { | |||||||
|  |  | ||||||
| // Set lets you save a DRA Plugin to the list and give it a specific name. | // Set lets you save a DRA Plugin to the list and give it a specific name. | ||||||
| // This method is protected by a mutex. | // This method is protected by a mutex. | ||||||
| func (s *pluginsStore) add(pluginName string, p *plugin) { | func (s *pluginsStore) add(pluginName string, p *Plugin) (replaced bool) { | ||||||
| 	s.Lock() | 	s.Lock() | ||||||
| 	defer s.Unlock() | 	defer s.Unlock() | ||||||
|  |  | ||||||
| 	if s.store == nil { | 	if s.store == nil { | ||||||
| 		s.store = make(map[string]*plugin) | 		s.store = make(map[string]*Plugin) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	_, exists := s.store[pluginName] | 	_, exists := s.store[pluginName] | ||||||
| 	if exists { |  | ||||||
| 		klog.V(1).InfoS(log("plugin: %s already registered, previous plugin will be overridden", pluginName)) |  | ||||||
| 	} |  | ||||||
| 	s.store[pluginName] = p | 	s.store[pluginName] = p | ||||||
|  | 	return exists | ||||||
| } | } | ||||||
|  |  | ||||||
| // Delete lets you delete a DRA Plugin by name. | // Delete lets you delete a DRA Plugin by name. | ||||||
| // This method is protected by a mutex. | // This method is protected by a mutex. | ||||||
| func (s *pluginsStore) delete(pluginName string) { | func (s *pluginsStore) delete(pluginName string) *Plugin { | ||||||
| 	s.Lock() | 	s.Lock() | ||||||
| 	defer s.Unlock() | 	defer s.Unlock() | ||||||
|  |  | ||||||
|  | 	p, exists := s.store[pluginName] | ||||||
|  | 	if !exists { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	if p.cancel != nil { | ||||||
|  | 		p.cancel(errors.New("plugin got removed")) | ||||||
|  | 	} | ||||||
| 	delete(s.store, pluginName) | 	delete(s.store, pluginName) | ||||||
|  |  | ||||||
|  | 	return p | ||||||
| } | } | ||||||
|   | |||||||
| @@ -641,8 +641,14 @@ func (p *Plugin) admitCSINode(nodeName string, a admission.Attributes) error { | |||||||
|  |  | ||||||
| func (p *Plugin) admitResourceSlice(nodeName string, a admission.Attributes) error { | func (p *Plugin) admitResourceSlice(nodeName string, a admission.Attributes) error { | ||||||
| 	// The create request must come from a node with the same name as the NodeName field. | 	// The create request must come from a node with the same name as the NodeName field. | ||||||
| 	// Other requests gets checked by the node authorizer. | 	// Same when deleting an object. | ||||||
| 	if a.GetOperation() == admission.Create { | 	// | ||||||
|  | 	// Other requests get checked by the node authorizer. The checks here are necessary | ||||||
|  | 	// because the node authorizer does not know the object content for a create request | ||||||
|  | 	// and not each deleted object in a DeleteCollection. DeleteCollection checks each | ||||||
|  | 	// individual object. | ||||||
|  | 	switch a.GetOperation() { | ||||||
|  | 	case admission.Create: | ||||||
| 		slice, ok := a.GetObject().(*resource.ResourceSlice) | 		slice, ok := a.GetObject().(*resource.ResourceSlice) | ||||||
| 		if !ok { | 		if !ok { | ||||||
| 			return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject())) | 			return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject())) | ||||||
| @@ -651,6 +657,15 @@ func (p *Plugin) admitResourceSlice(nodeName string, a admission.Attributes) err | |||||||
| 		if slice.NodeName != nodeName { | 		if slice.NodeName != nodeName { | ||||||
| 			return admission.NewForbidden(a, errors.New("can only create ResourceSlice with the same NodeName as the requesting node")) | 			return admission.NewForbidden(a, errors.New("can only create ResourceSlice with the same NodeName as the requesting node")) | ||||||
| 		} | 		} | ||||||
|  | 	case admission.Delete: | ||||||
|  | 		slice, ok := a.GetOldObject().(*resource.ResourceSlice) | ||||||
|  | 		if !ok { | ||||||
|  | 			return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetOldObject())) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if slice.NodeName != nodeName { | ||||||
|  | 			return admission.NewForbidden(a, errors.New("can only delete ResourceSlice with the same NodeName as the requesting node")) | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
|   | |||||||
| @@ -1607,7 +1607,8 @@ func TestAdmitResourceSlice(t *testing.T) { | |||||||
| 	apiResource := resourceapi.SchemeGroupVersion.WithResource("resourceslices") | 	apiResource := resourceapi.SchemeGroupVersion.WithResource("resourceslices") | ||||||
| 	nodename := "mynode" | 	nodename := "mynode" | ||||||
| 	mynode := &user.DefaultInfo{Name: "system:node:" + nodename, Groups: []string{"system:nodes"}} | 	mynode := &user.DefaultInfo{Name: "system:node:" + nodename, Groups: []string{"system:nodes"}} | ||||||
| 	err := "can only create ResourceSlice with the same NodeName as the requesting node" | 	createErr := "can only create ResourceSlice with the same NodeName as the requesting node" | ||||||
|  | 	deleteErr := "can only delete ResourceSlice with the same NodeName as the requesting node" | ||||||
|  |  | ||||||
| 	sliceNode := &resourceapi.ResourceSlice{ | 	sliceNode := &resourceapi.ResourceSlice{ | ||||||
| 		ObjectMeta: metav1.ObjectMeta{ | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
| @@ -1624,53 +1625,88 @@ func TestAdmitResourceSlice(t *testing.T) { | |||||||
|  |  | ||||||
| 	tests := map[string]struct { | 	tests := map[string]struct { | ||||||
| 		operation      admission.Operation | 		operation      admission.Operation | ||||||
| 		obj            runtime.Object | 		options        runtime.Object | ||||||
|  | 		obj, oldObj    runtime.Object | ||||||
| 		featureEnabled bool | 		featureEnabled bool | ||||||
| 		expectError    string | 		expectError    string | ||||||
| 	}{ | 	}{ | ||||||
| 		"create allowed, enabled": { | 		"create allowed, enabled": { | ||||||
| 			operation:      admission.Create, | 			operation:      admission.Create, | ||||||
|  | 			options:        &metav1.CreateOptions{}, | ||||||
| 			obj:            sliceNode, | 			obj:            sliceNode, | ||||||
| 			featureEnabled: true, | 			featureEnabled: true, | ||||||
| 			expectError:    "", | 			expectError:    "", | ||||||
| 		}, | 		}, | ||||||
| 		"create disallowed, enabled": { | 		"create disallowed, enabled": { | ||||||
| 			operation:      admission.Create, | 			operation:      admission.Create, | ||||||
|  | 			options:        &metav1.CreateOptions{}, | ||||||
| 			obj:            sliceOtherNode, | 			obj:            sliceOtherNode, | ||||||
| 			featureEnabled: true, | 			featureEnabled: true, | ||||||
| 			expectError:    err, | 			expectError:    createErr, | ||||||
| 		}, | 		}, | ||||||
| 		"create allowed, disabled": { | 		"create allowed, disabled": { | ||||||
| 			operation:      admission.Create, | 			operation:      admission.Create, | ||||||
|  | 			options:        &metav1.CreateOptions{}, | ||||||
| 			obj:            sliceNode, | 			obj:            sliceNode, | ||||||
| 			featureEnabled: false, | 			featureEnabled: false, | ||||||
| 			expectError:    "", | 			expectError:    "", | ||||||
| 		}, | 		}, | ||||||
| 		"create disallowed, disabled": { | 		"create disallowed, disabled": { | ||||||
| 			operation:      admission.Create, | 			operation:      admission.Create, | ||||||
|  | 			options:        &metav1.CreateOptions{}, | ||||||
| 			obj:            sliceOtherNode, | 			obj:            sliceOtherNode, | ||||||
| 			featureEnabled: false, | 			featureEnabled: false, | ||||||
| 			expectError:    err, | 			expectError:    createErr, | ||||||
| 		}, | 		}, | ||||||
| 		"update allowed, same node": { | 		"update allowed, same node": { | ||||||
| 			operation:      admission.Update, | 			operation:      admission.Update, | ||||||
|  | 			options:        &metav1.UpdateOptions{}, | ||||||
| 			obj:            sliceNode, | 			obj:            sliceNode, | ||||||
| 			featureEnabled: true, | 			featureEnabled: true, | ||||||
| 			expectError:    "", | 			expectError:    "", | ||||||
| 		}, | 		}, | ||||||
| 		"update allowed, other node": { | 		"update allowed, other node": { | ||||||
| 			operation:      admission.Update, | 			operation:      admission.Update, | ||||||
|  | 			options:        &metav1.UpdateOptions{}, | ||||||
| 			obj:            sliceOtherNode, | 			obj:            sliceOtherNode, | ||||||
| 			featureEnabled: true, | 			featureEnabled: true, | ||||||
| 			expectError:    "", | 			expectError:    "", | ||||||
| 		}, | 		}, | ||||||
|  | 		"delete allowed, enabled": { | ||||||
|  | 			operation:      admission.Delete, | ||||||
|  | 			options:        &metav1.DeleteOptions{}, | ||||||
|  | 			oldObj:         sliceNode, | ||||||
|  | 			featureEnabled: true, | ||||||
|  | 			expectError:    "", | ||||||
|  | 		}, | ||||||
|  | 		"delete disallowed, enabled": { | ||||||
|  | 			operation:      admission.Delete, | ||||||
|  | 			options:        &metav1.DeleteOptions{}, | ||||||
|  | 			oldObj:         sliceOtherNode, | ||||||
|  | 			featureEnabled: true, | ||||||
|  | 			expectError:    deleteErr, | ||||||
|  | 		}, | ||||||
|  | 		"delete allowed, disabled": { | ||||||
|  | 			operation:      admission.Delete, | ||||||
|  | 			options:        &metav1.DeleteOptions{}, | ||||||
|  | 			oldObj:         sliceNode, | ||||||
|  | 			featureEnabled: false, | ||||||
|  | 			expectError:    "", | ||||||
|  | 		}, | ||||||
|  | 		"delete disallowed, disabled": { | ||||||
|  | 			operation:      admission.Delete, | ||||||
|  | 			options:        &metav1.DeleteOptions{}, | ||||||
|  | 			oldObj:         sliceOtherNode, | ||||||
|  | 			featureEnabled: false, | ||||||
|  | 			expectError:    deleteErr, | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for name, test := range tests { | 	for name, test := range tests { | ||||||
| 		t.Run(name, func(t *testing.T) { | 		t.Run(name, func(t *testing.T) { | ||||||
| 			attributes := admission.NewAttributesRecord( | 			attributes := admission.NewAttributesRecord( | ||||||
| 				test.obj, nil, schema.GroupVersionKind{}, | 				test.obj, test.oldObj, schema.GroupVersionKind{}, | ||||||
| 				"", "foo", apiResource, "", test.operation, &metav1.CreateOptions{}, false, mynode) | 				"", "foo", apiResource, "", test.operation, test.options, false, mynode) | ||||||
| 			featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.DynamicResourceAllocation, test.featureEnabled) | 			featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.DynamicResourceAllocation, test.featureEnabled) | ||||||
| 			a := &admitTestCase{ | 			a := &admitTestCase{ | ||||||
| 				name:       name, | 				name:       name, | ||||||
|   | |||||||
| @@ -309,30 +309,34 @@ func (r *NodeAuthorizer) authorizeResourceSlice(nodeName string, attrs authorize | |||||||
| 		return authorizer.DecisionNoOpinion, "cannot authorize ResourceSlice subresources", nil | 		return authorizer.DecisionNoOpinion, "cannot authorize ResourceSlice subresources", nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// allowed verbs: get, create, update, patch, delete | 	// allowed verbs: get, create, update, patch, delete, watch, list, deletecollection | ||||||
| 	verb := attrs.GetVerb() | 	verb := attrs.GetVerb() | ||||||
| 	switch verb { | 	switch verb { | ||||||
| 	case "get", "create", "update", "patch", "delete": | 	case "create": | ||||||
| 		// Okay, but check individual object permission below. | 		// The request must come from a node with the same name as the ResourceSlice.NodeName field. | ||||||
| 	case "watch", "list": | 		// | ||||||
| 		// Okay. The kubelet is trusted to use a filter for its own objects. | 		// For create, the noderestriction admission plugin is performing this check. | ||||||
|  | 		// Here we don't have access to the content of the new object. | ||||||
|  | 		return authorizer.DecisionAllow, "", nil | ||||||
|  | 	case "get", "update", "patch", "delete": | ||||||
|  | 		// Checking the existing object must have established that access | ||||||
|  | 		// is allowed by recording a graph edge. | ||||||
|  | 		return r.authorize(nodeName, sliceVertexType, attrs) | ||||||
|  | 	case "watch", "list", "deletecollection": | ||||||
|  | 		// Okay. The kubelet is trusted to use a filter for its own objects in watch and list. | ||||||
|  | 		// The NodeRestriction admission plugin (plugin/pkg/admission/noderestriction) | ||||||
|  | 		// ensures that the node is not deleting some ResourceSlice belonging to | ||||||
|  | 		// some other node. | ||||||
|  | 		// | ||||||
|  | 		// TODO (https://github.com/kubernetes/kubernetes/issues/125355): | ||||||
|  | 		// Once https://github.com/kubernetes/enhancements/pull/4600 is implemented, | ||||||
|  | 		// this code needs to be extended to verify that the node filter is indeed set. | ||||||
|  | 		// Then the admission check can be removed. | ||||||
| 		return authorizer.DecisionAllow, "", nil | 		return authorizer.DecisionAllow, "", nil | ||||||
| 	default: | 	default: | ||||||
| 		klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs) | 		klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs) | ||||||
| 		return authorizer.DecisionNoOpinion, "can only get, create, update, patch, or delete a ResourceSlice", nil | 		return authorizer.DecisionNoOpinion, "only the following verbs are allowed for a ResourceSlice: get, watch, list, create, update, patch, delete, deletecollection", nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// The request must come from a node with the same name as the ResourceSlice.NodeName field. |  | ||||||
| 	// |  | ||||||
| 	// For create, the noderestriction admission plugin is performing this check. |  | ||||||
| 	// Here we don't have access to the content of the new object. |  | ||||||
| 	if verb == "create" { |  | ||||||
| 		return authorizer.DecisionAllow, "", nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// For any other verb, checking the existing object must have established that access |  | ||||||
| 	// is allowed by recording a graph edge. |  | ||||||
| 	return r.authorize(nodeName, sliceVertexType, attrs) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // hasPathFrom returns true if there is a directed path from the specified type/namespace/name to the specified Node | // hasPathFrom returns true if there is a directed path from the specified type/namespace/name to the specified Node | ||||||
|   | |||||||
| @@ -181,6 +181,7 @@ func NodeRules() []rbacv1.PolicyRule { | |||||||
| 	// DRA Resource Claims | 	// DRA Resource Claims | ||||||
| 	if utilfeature.DefaultFeatureGate.Enabled(features.DynamicResourceAllocation) { | 	if utilfeature.DefaultFeatureGate.Enabled(features.DynamicResourceAllocation) { | ||||||
| 		nodePolicyRules = append(nodePolicyRules, rbacv1helpers.NewRule("get").Groups(resourceGroup).Resources("resourceclaims").RuleOrDie()) | 		nodePolicyRules = append(nodePolicyRules, rbacv1helpers.NewRule("get").Groups(resourceGroup).Resources("resourceclaims").RuleOrDie()) | ||||||
|  | 		nodePolicyRules = append(nodePolicyRules, rbacv1helpers.NewRule("deletecollection").Groups(resourceGroup).Resources("resourceslices").RuleOrDie()) | ||||||
| 	} | 	} | ||||||
| 	// Kubelet needs access to ClusterTrustBundles to support the pemTrustAnchors volume type. | 	// Kubelet needs access to ClusterTrustBundles to support the pemTrustAnchors volume type. | ||||||
| 	if utilfeature.DefaultFeatureGate.Enabled(features.ClusterTrustBundle) { | 	if utilfeature.DefaultFeatureGate.Enabled(features.ClusterTrustBundle) { | ||||||
|   | |||||||
| @@ -17,14 +17,20 @@ limitations under the License. | |||||||
| package kubeletplugin | package kubeletplugin | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net" | 	"net" | ||||||
|  | 	"sync" | ||||||
|  |  | ||||||
| 	"google.golang.org/grpc" | 	"google.golang.org/grpc" | ||||||
| 	"k8s.io/klog/v2" | 	"k8s.io/klog/v2" | ||||||
|  |  | ||||||
| 	drapbv1alpha3 "k8s.io/kubelet/pkg/apis/dra/v1alpha3" | 	resourceapi "k8s.io/api/resource/v1alpha2" | ||||||
|  | 	"k8s.io/apimachinery/pkg/types" | ||||||
|  | 	"k8s.io/client-go/kubernetes" | ||||||
|  | 	"k8s.io/dynamic-resource-allocation/resourceslice" | ||||||
|  | 	drapb "k8s.io/kubelet/pkg/apis/dra/v1alpha4" | ||||||
| 	registerapi "k8s.io/kubelet/pkg/apis/pluginregistration/v1" | 	registerapi "k8s.io/kubelet/pkg/apis/pluginregistration/v1" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -39,6 +45,18 @@ type DRAPlugin interface { | |||||||
| 	// received yet. | 	// received yet. | ||||||
| 	RegistrationStatus() *registerapi.RegistrationStatus | 	RegistrationStatus() *registerapi.RegistrationStatus | ||||||
|  |  | ||||||
|  | 	// PublishResources may be called one or more times to publish | ||||||
|  | 	// resource information in ResourceSlice objects. If it never gets | ||||||
|  | 	// called, then the kubelet plugin does not manage any ResourceSlice | ||||||
|  | 	// objects. | ||||||
|  | 	// | ||||||
|  | 	// PublishResources does not block, so it might still take a while | ||||||
|  | 	// after it returns before all information is actually written | ||||||
|  | 	// to the API server. | ||||||
|  | 	// | ||||||
|  | 	// The caller must not modify the content of the slice. | ||||||
|  | 	PublishResources(ctx context.Context, nodeResources []*resourceapi.ResourceModel) | ||||||
|  |  | ||||||
| 	// This unexported method ensures that we can modify the interface | 	// This unexported method ensures that we can modify the interface | ||||||
| 	// without causing an API break of the package | 	// without causing an API break of the package | ||||||
| 	// (https://pkg.go.dev/golang.org/x/exp/apidiff#section-readme). | 	// (https://pkg.go.dev/golang.org/x/exp/apidiff#section-readme). | ||||||
| @@ -57,14 +75,6 @@ func DriverName(driverName string) Option { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Logger overrides the default klog.Background logger. |  | ||||||
| func Logger(logger klog.Logger) Option { |  | ||||||
| 	return func(o *options) error { |  | ||||||
| 		o.logger = logger |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // GRPCVerbosity sets the verbosity for logging gRPC calls. Default is 4. A negative | // GRPCVerbosity sets the verbosity for logging gRPC calls. Default is 4. A negative | ||||||
| // value disables logging. | // value disables logging. | ||||||
| func GRPCVerbosity(level int) Option { | func GRPCVerbosity(level int) Option { | ||||||
| @@ -162,15 +172,50 @@ func NodeV1alpha3(enabled bool) Option { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // KubeClient grants the plugin access to the API server. This is needed | ||||||
|  | // for syncing ResourceSlice objects. It's the responsibility of the DRA driver | ||||||
|  | // developer to ensure that this client has permission to read, write, | ||||||
|  | // patch and list such objects. It also needs permission to read node objects. | ||||||
|  | // Ideally, a validating admission policy should be used to limit write | ||||||
|  | // access to ResourceSlices which belong to the node. | ||||||
|  | func KubeClient(kubeClient kubernetes.Interface) Option { | ||||||
|  | 	return func(o *options) error { | ||||||
|  | 		o.kubeClient = kubeClient | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NodeName tells the plugin on which node it is running. This is needed for | ||||||
|  | // syncing ResourceSlice objects. | ||||||
|  | func NodeName(nodeName string) Option { | ||||||
|  | 	return func(o *options) error { | ||||||
|  | 		o.nodeName = nodeName | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NodeUID tells the plugin the UID of the v1.Node object. This is used | ||||||
|  | // when syncing ResourceSlice objects, but doesn't have to be used. If | ||||||
|  | // not supplied, the controller will look up the object once. | ||||||
|  | func NodeUID(nodeUID types.UID) Option { | ||||||
|  | 	return func(o *options) error { | ||||||
|  | 		o.nodeUID = nodeUID | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| type options struct { | type options struct { | ||||||
| 	logger                     klog.Logger | 	logger                     klog.Logger | ||||||
| 	grpcVerbosity              int | 	grpcVerbosity              int | ||||||
| 	driverName                 string | 	driverName                 string | ||||||
|  | 	nodeName                   string | ||||||
|  | 	nodeUID                    types.UID | ||||||
| 	draEndpoint                endpoint | 	draEndpoint                endpoint | ||||||
| 	draAddress                 string | 	draAddress                 string | ||||||
| 	pluginRegistrationEndpoint endpoint | 	pluginRegistrationEndpoint endpoint | ||||||
| 	unaryInterceptors          []grpc.UnaryServerInterceptor | 	unaryInterceptors          []grpc.UnaryServerInterceptor | ||||||
| 	streamInterceptors         []grpc.StreamServerInterceptor | 	streamInterceptors         []grpc.StreamServerInterceptor | ||||||
|  | 	kubeClient                 kubernetes.Interface | ||||||
|  |  | ||||||
| 	nodeV1alpha3 bool | 	nodeV1alpha3 bool | ||||||
| } | } | ||||||
| @@ -178,18 +223,36 @@ type options struct { | |||||||
| // draPlugin combines the kubelet registration service and the DRA node plugin | // draPlugin combines the kubelet registration service and the DRA node plugin | ||||||
| // service. | // service. | ||||||
| type draPlugin struct { | type draPlugin struct { | ||||||
| 	registrar *nodeRegistrar | 	// backgroundCtx is for activities that are started later. | ||||||
| 	plugin    *grpcServer | 	backgroundCtx context.Context | ||||||
|  | 	// cancel cancels the backgroundCtx. | ||||||
|  | 	cancel     func(cause error) | ||||||
|  | 	wg         sync.WaitGroup | ||||||
|  | 	registrar  *nodeRegistrar | ||||||
|  | 	plugin     *grpcServer | ||||||
|  | 	driverName string | ||||||
|  | 	nodeName   string | ||||||
|  | 	nodeUID    types.UID | ||||||
|  | 	kubeClient kubernetes.Interface | ||||||
|  |  | ||||||
|  | 	// Information about resource publishing changes concurrently and thus | ||||||
|  | 	// must be protected by the mutex. The controller gets started only | ||||||
|  | 	// if needed. | ||||||
|  | 	mutex                   sync.Mutex | ||||||
|  | 	resourceSliceController *resourceslice.Controller | ||||||
| } | } | ||||||
|  |  | ||||||
| // Start sets up two gRPC servers (one for registration, one for the DRA node | // Start sets up two gRPC servers (one for registration, one for the DRA node | ||||||
| // client). By default, all APIs implemented by the nodeServer get registered. | // client). By default, all APIs implemented by the nodeServer get registered. | ||||||
| func Start(nodeServer interface{}, opts ...Option) (result DRAPlugin, finalErr error) { | // | ||||||
| 	d := &draPlugin{} | // The context and/or DRAPlugin.Stop can be used to stop all background activity. | ||||||
|  | // Stop also blocks. A logger can be stored in the context to add values or | ||||||
|  | // a name to all log entries. | ||||||
|  | func Start(ctx context.Context, nodeServer interface{}, opts ...Option) (result DRAPlugin, finalErr error) { | ||||||
|  | 	logger := klog.FromContext(ctx) | ||||||
| 	o := options{ | 	o := options{ | ||||||
| 		logger:        klog.Background(), | 		logger:        klog.Background(), | ||||||
| 		grpcVerbosity: 4, | 		grpcVerbosity: 6, // Logs requests and responses, which can be large. | ||||||
| 		nodeV1alpha3:  true, | 		nodeV1alpha3:  true, | ||||||
| 	} | 	} | ||||||
| 	for _, option := range opts { | 	for _, option := range opts { | ||||||
| @@ -212,12 +275,42 @@ func Start(nodeServer interface{}, opts ...Option) (result DRAPlugin, finalErr e | |||||||
| 		return nil, errors.New("a Unix domain socket path and/or listener must be set for the registrar") | 		return nil, errors.New("a Unix domain socket path and/or listener must be set for the registrar") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	d := &draPlugin{ | ||||||
|  | 		driverName: o.driverName, | ||||||
|  | 		nodeName:   o.nodeName, | ||||||
|  | 		nodeUID:    o.nodeUID, | ||||||
|  | 		kubeClient: o.kubeClient, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Stop calls cancel and therefore both cancellation | ||||||
|  | 	// and Stop cause goroutines to stop. | ||||||
|  | 	ctx, cancel := context.WithCancelCause(ctx) | ||||||
|  | 	d.backgroundCtx, d.cancel = ctx, cancel | ||||||
|  | 	logger.V(3).Info("Starting") | ||||||
|  | 	d.wg.Add(1) | ||||||
|  | 	go func() { | ||||||
|  | 		defer d.wg.Done() | ||||||
|  | 		defer logger.V(3).Info("Stopping") | ||||||
|  | 		<-ctx.Done() | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	// Clean up if we don't finish succcessfully. | ||||||
|  | 	defer func() { | ||||||
|  | 		if r := recover(); r != nil { | ||||||
|  | 			d.Stop() | ||||||
|  | 			panic(r) | ||||||
|  | 		} | ||||||
|  | 		if finalErr != nil { | ||||||
|  | 			d.Stop() | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  |  | ||||||
| 	// Run the node plugin gRPC server first to ensure that it is ready. | 	// Run the node plugin gRPC server first to ensure that it is ready. | ||||||
| 	implemented := false | 	implemented := false | ||||||
| 	plugin, err := startGRPCServer(klog.LoggerWithName(o.logger, "dra"), o.grpcVerbosity, o.unaryInterceptors, o.streamInterceptors, o.draEndpoint, func(grpcServer *grpc.Server) { | 	plugin, err := startGRPCServer(klog.NewContext(ctx, klog.LoggerWithName(logger, "dra")), o.grpcVerbosity, o.unaryInterceptors, o.streamInterceptors, o.draEndpoint, func(grpcServer *grpc.Server) { | ||||||
| 		if nodeServer, ok := nodeServer.(drapbv1alpha3.NodeServer); ok && o.nodeV1alpha3 { | 		if nodeServer, ok := nodeServer.(drapb.NodeServer); ok && o.nodeV1alpha3 { | ||||||
| 			o.logger.V(5).Info("registering drapbv1alpha3.NodeServer") | 			logger.V(5).Info("registering drapbv1alpha3.NodeServer") | ||||||
| 			drapbv1alpha3.RegisterNodeServer(grpcServer, nodeServer) | 			drapb.RegisterNodeServer(grpcServer, nodeServer) | ||||||
| 			implemented = true | 			implemented = true | ||||||
| 		} | 		} | ||||||
| 	}) | 	}) | ||||||
| @@ -225,38 +318,77 @@ func Start(nodeServer interface{}, opts ...Option) (result DRAPlugin, finalErr e | |||||||
| 		return nil, fmt.Errorf("start node client: %v", err) | 		return nil, fmt.Errorf("start node client: %v", err) | ||||||
| 	} | 	} | ||||||
| 	d.plugin = plugin | 	d.plugin = plugin | ||||||
| 	defer func() { |  | ||||||
| 		// Clean up if we didn't finish succcessfully. |  | ||||||
| 		if r := recover(); r != nil { |  | ||||||
| 			plugin.stop() |  | ||||||
| 			panic(r) |  | ||||||
| 		} |  | ||||||
| 		if finalErr != nil { |  | ||||||
| 			plugin.stop() |  | ||||||
| 		} |  | ||||||
| 	}() |  | ||||||
| 	if !implemented { | 	if !implemented { | ||||||
| 		return nil, errors.New("no supported DRA gRPC API is implemented and enabled") | 		return nil, errors.New("no supported DRA gRPC API is implemented and enabled") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Now make it available to kubelet. | 	// Now make it available to kubelet. | ||||||
| 	registrar, err := startRegistrar(klog.LoggerWithName(o.logger, "registrar"), o.grpcVerbosity, o.unaryInterceptors, o.streamInterceptors, o.driverName, o.draAddress, o.pluginRegistrationEndpoint) | 	registrar, err := startRegistrar(klog.NewContext(ctx, klog.LoggerWithName(logger, "registrar")), o.grpcVerbosity, o.unaryInterceptors, o.streamInterceptors, o.driverName, o.draAddress, o.pluginRegistrationEndpoint) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("start registrar: %v", err) | 		return nil, fmt.Errorf("start registrar: %v", err) | ||||||
| 	} | 	} | ||||||
| 	d.registrar = registrar | 	d.registrar = registrar | ||||||
|  |  | ||||||
|  | 	// startGRPCServer and startRegistrar don't implement cancellation | ||||||
|  | 	// themselves, we add that for both here. | ||||||
|  | 	d.wg.Add(1) | ||||||
|  | 	go func() { | ||||||
|  | 		defer d.wg.Done() | ||||||
|  | 		<-ctx.Done() | ||||||
|  |  | ||||||
|  | 		// Time to stop. | ||||||
|  | 		d.plugin.stop() | ||||||
|  | 		d.registrar.stop() | ||||||
|  |  | ||||||
|  | 		// d.resourceSliceController is set concurrently. | ||||||
|  | 		d.mutex.Lock() | ||||||
|  | 		d.resourceSliceController.Stop() | ||||||
|  | 		d.mutex.Unlock() | ||||||
|  | 	}() | ||||||
|  |  | ||||||
| 	return d, nil | 	return d, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Stop implements [DRAPlugin.Stop]. | ||||||
| func (d *draPlugin) Stop() { | func (d *draPlugin) Stop() { | ||||||
| 	if d == nil { | 	if d == nil { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	d.registrar.stop() | 	d.cancel(errors.New("DRA plugin was stopped")) | ||||||
| 	d.plugin.stop() | 	// Wait for goroutines in Start to clean up and exit. | ||||||
|  | 	d.wg.Wait() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // PublishResources implements [DRAPlugin.PublishResources]. | ||||||
|  | func (d *draPlugin) PublishResources(ctx context.Context, nodeResources []*resourceapi.ResourceModel) { | ||||||
|  | 	d.mutex.Lock() | ||||||
|  | 	defer d.mutex.Unlock() | ||||||
|  |  | ||||||
|  | 	owner := resourceslice.Owner{ | ||||||
|  | 		APIVersion: "v1", | ||||||
|  | 		Kind:       "Node", | ||||||
|  | 		Name:       d.nodeName, | ||||||
|  | 		UID:        d.nodeUID, // Optional, will be determined by controller if empty. | ||||||
|  | 	} | ||||||
|  | 	resources := &resourceslice.Resources{NodeResources: nodeResources} | ||||||
|  | 	if d.resourceSliceController == nil { | ||||||
|  | 		// Start publishing the information. The controller is using | ||||||
|  | 		// our background context, not the one passed into this | ||||||
|  | 		// function, and thus is connected to the lifecycle of the | ||||||
|  | 		// plugin. | ||||||
|  | 		controllerCtx := d.backgroundCtx | ||||||
|  | 		controllerLogger := klog.FromContext(controllerCtx) | ||||||
|  | 		controllerLogger = klog.LoggerWithName(controllerLogger, "ResourceSlice controller") | ||||||
|  | 		controllerCtx = klog.NewContext(controllerCtx, controllerLogger) | ||||||
|  | 		d.resourceSliceController = resourceslice.StartController(controllerCtx, d.kubeClient, d.driverName, owner, resources) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Inform running controller about new information. | ||||||
|  | 	d.resourceSliceController.Update(resources) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RegistrationStatus implements [DRAPlugin.RegistrationStatus]. | ||||||
| func (d *draPlugin) RegistrationStatus() *registerapi.RegistrationStatus { | func (d *draPlugin) RegistrationStatus() *registerapi.RegistrationStatus { | ||||||
| 	if d.registrar == nil { | 	if d.registrar == nil { | ||||||
| 		return nil | 		return nil | ||||||
|   | |||||||
| @@ -17,30 +17,30 @@ limitations under the License. | |||||||
| package kubeletplugin | package kubeletplugin | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  |  | ||||||
| 	"google.golang.org/grpc" | 	"google.golang.org/grpc" | ||||||
| 	"k8s.io/klog/v2" |  | ||||||
| 	registerapi "k8s.io/kubelet/pkg/apis/pluginregistration/v1" | 	registerapi "k8s.io/kubelet/pkg/apis/pluginregistration/v1" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type nodeRegistrar struct { | type nodeRegistrar struct { | ||||||
| 	logger klog.Logger |  | ||||||
| 	registrationServer | 	registrationServer | ||||||
| 	server *grpcServer | 	server *grpcServer | ||||||
| } | } | ||||||
|  |  | ||||||
| // startRegistrar returns a running instance. | // startRegistrar returns a running instance. | ||||||
| func startRegistrar(logger klog.Logger, grpcVerbosity int, interceptors []grpc.UnaryServerInterceptor, streamInterceptors []grpc.StreamServerInterceptor, driverName string, endpoint string, pluginRegistrationEndpoint endpoint) (*nodeRegistrar, error) { | // | ||||||
|  | // The context is only used for additional values, cancellation is ignored. | ||||||
|  | func startRegistrar(valueCtx context.Context, grpcVerbosity int, interceptors []grpc.UnaryServerInterceptor, streamInterceptors []grpc.StreamServerInterceptor, driverName string, endpoint string, pluginRegistrationEndpoint endpoint) (*nodeRegistrar, error) { | ||||||
| 	n := &nodeRegistrar{ | 	n := &nodeRegistrar{ | ||||||
| 		logger: logger, |  | ||||||
| 		registrationServer: registrationServer{ | 		registrationServer: registrationServer{ | ||||||
| 			driverName:        driverName, | 			driverName:        driverName, | ||||||
| 			endpoint:          endpoint, | 			endpoint:          endpoint, | ||||||
| 			supportedVersions: []string{"1.0.0"}, // TODO: is this correct? | 			supportedVersions: []string{"1.0.0"}, // TODO: is this correct? | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 	s, err := startGRPCServer(logger, grpcVerbosity, interceptors, streamInterceptors, pluginRegistrationEndpoint, func(grpcServer *grpc.Server) { | 	s, err := startGRPCServer(valueCtx, grpcVerbosity, interceptors, streamInterceptors, pluginRegistrationEndpoint, func(grpcServer *grpc.Server) { | ||||||
| 		registerapi.RegisterRegistrationServer(grpcServer, n) | 		registerapi.RegisterRegistrationServer(grpcServer, n) | ||||||
| 	}) | 	}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|   | |||||||
| @@ -26,15 +26,17 @@ import ( | |||||||
|  |  | ||||||
| 	"google.golang.org/grpc" | 	"google.golang.org/grpc" | ||||||
| 	"k8s.io/klog/v2" | 	"k8s.io/klog/v2" | ||||||
|  |  | ||||||
|  | 	utilruntime "k8s.io/apimachinery/pkg/util/runtime" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | var requestID int64 | ||||||
|  |  | ||||||
| type grpcServer struct { | type grpcServer struct { | ||||||
| 	logger        klog.Logger |  | ||||||
| 	grpcVerbosity int | 	grpcVerbosity int | ||||||
| 	wg            sync.WaitGroup | 	wg            sync.WaitGroup | ||||||
| 	endpoint      endpoint | 	endpoint      endpoint | ||||||
| 	server        *grpc.Server | 	server        *grpc.Server | ||||||
| 	requestID     int64 |  | ||||||
| } | } | ||||||
|  |  | ||||||
| type registerService func(s *grpc.Server) | type registerService func(s *grpc.Server) | ||||||
| @@ -54,9 +56,11 @@ type endpoint struct { | |||||||
|  |  | ||||||
| // startGRPCServer sets up the GRPC server on a Unix domain socket and spawns a goroutine | // startGRPCServer sets up the GRPC server on a Unix domain socket and spawns a goroutine | ||||||
| // which handles requests for arbitrary services. | // which handles requests for arbitrary services. | ||||||
| func startGRPCServer(logger klog.Logger, grpcVerbosity int, unaryInterceptors []grpc.UnaryServerInterceptor, streamInterceptors []grpc.StreamServerInterceptor, endpoint endpoint, services ...registerService) (*grpcServer, error) { | // | ||||||
|  | // The context is only used for additional values, cancellation is ignored. | ||||||
|  | func startGRPCServer(valueCtx context.Context, grpcVerbosity int, unaryInterceptors []grpc.UnaryServerInterceptor, streamInterceptors []grpc.StreamServerInterceptor, endpoint endpoint, services ...registerService) (*grpcServer, error) { | ||||||
|  | 	logger := klog.FromContext(valueCtx) | ||||||
| 	s := &grpcServer{ | 	s := &grpcServer{ | ||||||
| 		logger:        logger, |  | ||||||
| 		endpoint:      endpoint, | 		endpoint:      endpoint, | ||||||
| 		grpcVerbosity: grpcVerbosity, | 		grpcVerbosity: grpcVerbosity, | ||||||
| 	} | 	} | ||||||
| @@ -79,10 +83,11 @@ func startGRPCServer(logger klog.Logger, grpcVerbosity int, unaryInterceptors [] | |||||||
| 	// Run a gRPC server. It will close the listening socket when | 	// Run a gRPC server. It will close the listening socket when | ||||||
| 	// shutting down, so we don't need to do that. | 	// shutting down, so we don't need to do that. | ||||||
| 	var opts []grpc.ServerOption | 	var opts []grpc.ServerOption | ||||||
| 	var finalUnaryInterceptors []grpc.UnaryServerInterceptor | 	finalUnaryInterceptors := []grpc.UnaryServerInterceptor{unaryContextInterceptor(valueCtx)} | ||||||
| 	var finalStreamInterceptors []grpc.StreamServerInterceptor | 	finalStreamInterceptors := []grpc.StreamServerInterceptor{streamContextInterceptor(valueCtx)} | ||||||
| 	if grpcVerbosity >= 0 { | 	if grpcVerbosity >= 0 { | ||||||
| 		finalUnaryInterceptors = append(finalUnaryInterceptors, s.interceptor) | 		finalUnaryInterceptors = append(finalUnaryInterceptors, s.interceptor) | ||||||
|  | 		finalStreamInterceptors = append(finalStreamInterceptors, s.streamInterceptor) | ||||||
| 	} | 	} | ||||||
| 	finalUnaryInterceptors = append(finalUnaryInterceptors, unaryInterceptors...) | 	finalUnaryInterceptors = append(finalUnaryInterceptors, unaryInterceptors...) | ||||||
| 	finalStreamInterceptors = append(finalStreamInterceptors, streamInterceptors...) | 	finalStreamInterceptors = append(finalStreamInterceptors, streamInterceptors...) | ||||||
| @@ -103,16 +108,66 @@ func startGRPCServer(logger klog.Logger, grpcVerbosity int, unaryInterceptors [] | |||||||
| 		} | 		} | ||||||
| 	}() | 	}() | ||||||
|  |  | ||||||
| 	logger.Info("GRPC server started") | 	logger.V(3).Info("GRPC server started") | ||||||
| 	return s, nil | 	return s, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // unaryContextInterceptor injects values from the context into the context | ||||||
|  | // used by the call chain. | ||||||
|  | func unaryContextInterceptor(valueCtx context.Context) grpc.UnaryServerInterceptor { | ||||||
|  | 	return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { | ||||||
|  | 		ctx = mergeContexts(ctx, valueCtx) | ||||||
|  | 		return handler(ctx, req) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // streamContextInterceptor does the same as UnaryContextInterceptor for streams. | ||||||
|  | func streamContextInterceptor(valueCtx context.Context) grpc.StreamServerInterceptor { | ||||||
|  | 	return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { | ||||||
|  | 		ctx := mergeContexts(ss.Context(), valueCtx) | ||||||
|  | 		return handler(srv, mergeServerStream{ServerStream: ss, ctx: ctx}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type mergeServerStream struct { | ||||||
|  | 	grpc.ServerStream | ||||||
|  | 	ctx context.Context | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m mergeServerStream) Context() context.Context { | ||||||
|  | 	return m.ctx | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // mergeContexts creates a new context where cancellation is handled by the | ||||||
|  | // root context. The values stored by the value context are used as fallback if | ||||||
|  | // the root context doesn't have a certain value. | ||||||
|  | func mergeContexts(rootCtx, valueCtx context.Context) context.Context { | ||||||
|  | 	return mergeCtx{ | ||||||
|  | 		Context:  rootCtx, | ||||||
|  | 		valueCtx: valueCtx, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type mergeCtx struct { | ||||||
|  | 	context.Context | ||||||
|  | 	valueCtx context.Context | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m mergeCtx) Value(i interface{}) interface{} { | ||||||
|  | 	if v := m.Context.Value(i); v != nil { | ||||||
|  | 		return v | ||||||
|  | 	} | ||||||
|  | 	return m.valueCtx.Value(i) | ||||||
|  | } | ||||||
|  |  | ||||||
| // interceptor is called for each request. It creates a logger with a unique, | // interceptor is called for each request. It creates a logger with a unique, | ||||||
| // sequentially increasing request ID and adds that logger to the context. It | // sequentially increasing request ID and adds that logger to the context. It | ||||||
| // also logs request and response. | // also logs request and response. | ||||||
| func (s *grpcServer) interceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { | func (s *grpcServer) interceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { | ||||||
| 	requestID := atomic.AddInt64(&s.requestID, 1) |  | ||||||
| 	logger := klog.LoggerWithValues(s.logger, "requestID", requestID) | 	requestID := atomic.AddInt64(&requestID, 1) | ||||||
|  | 	logger := klog.FromContext(ctx) | ||||||
|  | 	logger = klog.LoggerWithValues(logger, "requestID", requestID, "method", info.FullMethod) | ||||||
| 	ctx = klog.NewContext(ctx, logger) | 	ctx = klog.NewContext(ctx, logger) | ||||||
| 	logger.V(s.grpcVerbosity).Info("handling request", "request", req) | 	logger.V(s.grpcVerbosity).Info("handling request", "request", req) | ||||||
| 	defer func() { | 	defer func() { | ||||||
| @@ -123,13 +178,57 @@ func (s *grpcServer) interceptor(ctx context.Context, req interface{}, info *grp | |||||||
| 	}() | 	}() | ||||||
| 	resp, err = handler(ctx, req) | 	resp, err = handler(ctx, req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logger.Error(err, "handling request failed", "request", req) | 		logger.Error(err, "handling request failed") | ||||||
| 	} else { | 	} else { | ||||||
| 		logger.V(s.grpcVerbosity).Info("handling request succeeded", "response", resp) | 		logger.V(s.grpcVerbosity).Info("handling request succeeded", "response", resp) | ||||||
| 	} | 	} | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (s *grpcServer) streamInterceptor(server interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { | ||||||
|  | 	requestID := atomic.AddInt64(&requestID, 1) | ||||||
|  | 	ctx := stream.Context() | ||||||
|  | 	logger := klog.FromContext(ctx) | ||||||
|  | 	logger = klog.LoggerWithValues(logger, "requestID", requestID, "method", info.FullMethod) | ||||||
|  | 	ctx = klog.NewContext(ctx, logger) | ||||||
|  | 	stream = logStream{ | ||||||
|  | 		ServerStream:  stream, | ||||||
|  | 		ctx:           ctx, | ||||||
|  | 		grpcVerbosity: s.grpcVerbosity, | ||||||
|  | 	} | ||||||
|  | 	logger.V(s.grpcVerbosity).Info("handling stream") | ||||||
|  | 	err := handler(server, stream) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.Error(err, "handling stream failed") | ||||||
|  | 	} else { | ||||||
|  | 		logger.V(s.grpcVerbosity).Info("handling stream succeeded") | ||||||
|  | 	} | ||||||
|  | 	return err | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type logStream struct { | ||||||
|  | 	grpc.ServerStream | ||||||
|  | 	ctx           context.Context | ||||||
|  | 	grpcVerbosity int | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (l logStream) Context() context.Context { | ||||||
|  | 	return l.ctx | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (l logStream) SendMsg(msg interface{}) error { | ||||||
|  | 	logger := klog.FromContext(l.ctx) | ||||||
|  | 	logger.V(l.grpcVerbosity).Info("sending stream message", "message", msg) | ||||||
|  | 	err := l.ServerStream.SendMsg(msg) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.Error(err, "sending stream message failed") | ||||||
|  | 	} else { | ||||||
|  | 		logger.V(l.grpcVerbosity).Info("sending stream message succeeded") | ||||||
|  | 	} | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
| // stop ensures that the server is not running anymore and cleans up all resources. | // stop ensures that the server is not running anymore and cleans up all resources. | ||||||
| // It is idempotent and may be called with a nil pointer. | // It is idempotent and may be called with a nil pointer. | ||||||
| func (s *grpcServer) stop() { | func (s *grpcServer) stop() { | ||||||
| @@ -143,8 +242,7 @@ func (s *grpcServer) stop() { | |||||||
| 	s.server = nil | 	s.server = nil | ||||||
| 	if s.endpoint.path != "" { | 	if s.endpoint.path != "" { | ||||||
| 		if err := os.Remove(s.endpoint.path); err != nil && !os.IsNotExist(err) { | 		if err := os.Remove(s.endpoint.path); err != nil && !os.IsNotExist(err) { | ||||||
| 			s.logger.Error(err, "remove Unix socket") | 			utilruntime.HandleError(fmt.Errorf("remove Unix socket: %w", err)) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	s.logger.V(3).Info("GRPC server stopped") |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,391 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2024 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. | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | package resourceslice | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"sync" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/google/go-cmp/cmp" | ||||||
|  |  | ||||||
|  | 	resourceapi "k8s.io/api/resource/v1alpha2" | ||||||
|  | 	apiequality "k8s.io/apimachinery/pkg/api/equality" | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/fields" | ||||||
|  | 	"k8s.io/apimachinery/pkg/types" | ||||||
|  | 	utilruntime "k8s.io/apimachinery/pkg/util/runtime" | ||||||
|  | 	"k8s.io/apimachinery/pkg/util/sets" | ||||||
|  | 	resourceinformers "k8s.io/client-go/informers/resource/v1alpha2" | ||||||
|  | 	"k8s.io/client-go/kubernetes" | ||||||
|  | 	"k8s.io/client-go/tools/cache" | ||||||
|  | 	"k8s.io/client-go/util/workqueue" | ||||||
|  | 	"k8s.io/klog/v2" | ||||||
|  | 	"k8s.io/utils/ptr" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	// resyncPeriod for informer | ||||||
|  | 	// TODO (https://github.com/kubernetes/kubernetes/issues/123688): disable? | ||||||
|  | 	resyncPeriod = time.Duration(10 * time.Minute) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Controller synchronizes information about resources of one | ||||||
|  | // driver with ResourceSlice objects. It currently supports node-local | ||||||
|  | // resources. A DRA driver for node-local resources typically runs this | ||||||
|  | // controller as part of its kubelet plugin. | ||||||
|  | // | ||||||
|  | // Support for network-attached resources will be added later. | ||||||
|  | type Controller struct { | ||||||
|  | 	cancel     func(cause error) | ||||||
|  | 	driverName string | ||||||
|  | 	owner      Owner | ||||||
|  | 	kubeClient kubernetes.Interface | ||||||
|  | 	wg         sync.WaitGroup | ||||||
|  | 	queue      workqueue.TypedRateLimitingInterface[string] | ||||||
|  | 	sliceStore cache.Store | ||||||
|  |  | ||||||
|  | 	mutex sync.RWMutex | ||||||
|  |  | ||||||
|  | 	// When receiving updates from the driver, the entire pointer replaced, | ||||||
|  | 	// so it is okay to not do a deep copy of it when reading it. Only reading | ||||||
|  | 	// the pointer itself must be protected by a read lock. | ||||||
|  | 	resources *Resources | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Resources is a complete description of all resources synchronized by the controller. | ||||||
|  | type Resources struct { | ||||||
|  | 	// NodeResources are resources that are local to one node. | ||||||
|  | 	NodeResources []*resourceapi.ResourceModel | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Owner struct { | ||||||
|  | 	APIVersion string | ||||||
|  | 	Kind       string | ||||||
|  | 	Name       string | ||||||
|  | 	UID        types.UID | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // StartController constructs a new controller and starts it. | ||||||
|  | // If the owner is a v1.Node, then the NodeName field in the | ||||||
|  | // ResourceSlice objects is set and used to identify objects | ||||||
|  | // managed by the controller. The UID is not needed in that | ||||||
|  | // case, the controller will determine it automatically. | ||||||
|  | // | ||||||
|  | // If a kubeClient is provided, then it synchronizes ResourceSlices | ||||||
|  | // with the resource information provided by plugins. Without it, | ||||||
|  | // the controller is inactive. This can happen when kubelet is run stand-alone | ||||||
|  | // without an apiserver. In that case we can't and don't need to publish | ||||||
|  | // ResourceSlices. | ||||||
|  | func StartController(ctx context.Context, kubeClient kubernetes.Interface, driverName string, owner Owner, resources *Resources) *Controller { | ||||||
|  | 	if kubeClient == nil { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	logger := klog.FromContext(ctx) | ||||||
|  | 	ctx, cancel := context.WithCancelCause(ctx) | ||||||
|  |  | ||||||
|  | 	c := &Controller{ | ||||||
|  | 		cancel:     cancel, | ||||||
|  | 		kubeClient: kubeClient, | ||||||
|  | 		driverName: driverName, | ||||||
|  | 		owner:      owner, | ||||||
|  | 		queue: workqueue.NewTypedRateLimitingQueueWithConfig( | ||||||
|  | 			workqueue.DefaultTypedControllerRateLimiter[string](), | ||||||
|  | 			workqueue.TypedRateLimitingQueueConfig[string]{Name: "node_resource_slices"}, | ||||||
|  | 		), | ||||||
|  | 		resources: resources, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	logger.V(3).Info("Starting") | ||||||
|  | 	c.wg.Add(1) | ||||||
|  | 	go func() { | ||||||
|  | 		defer c.wg.Done() | ||||||
|  | 		defer logger.V(3).Info("Stopping") | ||||||
|  | 		c.run(ctx) | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	// Sync once. | ||||||
|  | 	c.queue.Add("") | ||||||
|  |  | ||||||
|  | 	return c | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Stop cancels all background activity and blocks until the controller has stopped. | ||||||
|  | func (c *Controller) Stop() { | ||||||
|  | 	if c == nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	c.cancel(errors.New("ResourceSlice controller was asked to stop")) | ||||||
|  | 	c.wg.Wait() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Update sets the new desired state of the resource information. | ||||||
|  | func (c *Controller) Update(resources *Resources) { | ||||||
|  | 	c.mutex.Lock() | ||||||
|  | 	defer c.mutex.Unlock() | ||||||
|  |  | ||||||
|  | 	c.resources = resources | ||||||
|  | 	c.queue.Add("") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // run is running in the background. It handles blocking initialization (like | ||||||
|  | // syncing the informer) and then syncs the actual with the desired state. | ||||||
|  | func (c *Controller) run(ctx context.Context) { | ||||||
|  | 	logger := klog.FromContext(ctx) | ||||||
|  |  | ||||||
|  | 	// We always filter by driver name, by node name only for node-local resources. | ||||||
|  | 	selector := fields.Set{"driverName": c.driverName} | ||||||
|  | 	if c.owner.APIVersion == "v1" && c.owner.Kind == "Node" { | ||||||
|  | 		selector["nodeName"] = c.owner.Name | ||||||
|  | 	} | ||||||
|  | 	informer := resourceinformers.NewFilteredResourceSliceInformer(c.kubeClient, resyncPeriod, nil, func(options *metav1.ListOptions) { | ||||||
|  | 		options.FieldSelector = selector.String() | ||||||
|  | 	}) | ||||||
|  | 	c.sliceStore = informer.GetStore() | ||||||
|  | 	handler, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ | ||||||
|  | 		AddFunc: func(obj any) { | ||||||
|  | 			slice, ok := obj.(*resourceapi.ResourceSlice) | ||||||
|  | 			if !ok { | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			logger.V(5).Info("ResourceSlice add", "slice", klog.KObj(slice)) | ||||||
|  | 			c.queue.Add("") | ||||||
|  | 		}, | ||||||
|  | 		UpdateFunc: func(old, new any) { | ||||||
|  | 			oldSlice, ok := old.(*resourceapi.ResourceSlice) | ||||||
|  | 			if !ok { | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			newSlice, ok := new.(*resourceapi.ResourceSlice) | ||||||
|  | 			if !ok { | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			if loggerV := logger.V(6); loggerV.Enabled() { | ||||||
|  | 				loggerV.Info("ResourceSlice update", "slice", klog.KObj(newSlice), "diff", cmp.Diff(oldSlice, newSlice)) | ||||||
|  | 			} else { | ||||||
|  | 				logger.V(5).Info("ResourceSlice update", "slice", klog.KObj(newSlice)) | ||||||
|  | 			} | ||||||
|  | 			c.queue.Add("") | ||||||
|  | 		}, | ||||||
|  | 		DeleteFunc: func(obj any) { | ||||||
|  | 			if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok { | ||||||
|  | 				obj = tombstone.Obj | ||||||
|  | 			} | ||||||
|  | 			slice, ok := obj.(*resourceapi.ResourceSlice) | ||||||
|  | 			if !ok { | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			logger.V(5).Info("ResourceSlice delete", "slice", klog.KObj(slice)) | ||||||
|  | 			c.queue.Add("") | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.Error(err, "Registering event handler on the ResourceSlice informer failed, disabling resource monitoring") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Start informer and wait for our cache to be populated. | ||||||
|  | 	logger.V(3).Info("Starting ResourceSlice informer and waiting for it to sync") | ||||||
|  | 	c.wg.Add(1) | ||||||
|  | 	go func() { | ||||||
|  | 		defer c.wg.Done() | ||||||
|  | 		defer logger.V(3).Info("ResourceSlice informer has stopped") | ||||||
|  | 		defer c.queue.ShutDown() // Once we get here, we must have been asked to stop. | ||||||
|  | 		informer.Run(ctx.Done()) | ||||||
|  | 	}() | ||||||
|  | 	for !handler.HasSynced() { | ||||||
|  | 		select { | ||||||
|  | 		case <-time.After(time.Second): | ||||||
|  | 		case <-ctx.Done(): | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	logger.V(3).Info("ResourceSlice informer has synced") | ||||||
|  |  | ||||||
|  | 	for c.processNextWorkItem(ctx) { | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Controller) processNextWorkItem(ctx context.Context) bool { | ||||||
|  | 	key, shutdown := c.queue.Get() | ||||||
|  | 	if shutdown { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	defer c.queue.Done(key) | ||||||
|  |  | ||||||
|  | 	// Panics are caught and treated like errors. | ||||||
|  | 	var err error | ||||||
|  | 	func() { | ||||||
|  | 		defer func() { | ||||||
|  | 			if r := recover(); r != nil { | ||||||
|  | 				err = fmt.Errorf("internal error: %v", r) | ||||||
|  | 			} | ||||||
|  | 		}() | ||||||
|  | 		err = c.sync(ctx) | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		utilruntime.HandleErrorWithContext(ctx, err, "processing ResourceSlice objects") | ||||||
|  | 		c.queue.AddRateLimited(key) | ||||||
|  |  | ||||||
|  | 		// Return without removing the work item from the queue. | ||||||
|  | 		// It will be retried. | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	c.queue.Forget(key) | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Controller) sync(ctx context.Context) error { | ||||||
|  | 	logger := klog.FromContext(ctx) | ||||||
|  |  | ||||||
|  | 	// Gather information about the actual and desired state. | ||||||
|  | 	slices := c.sliceStore.List() | ||||||
|  | 	var resources *Resources | ||||||
|  | 	c.mutex.RLock() | ||||||
|  | 	resources = c.resources | ||||||
|  | 	c.mutex.RUnlock() | ||||||
|  |  | ||||||
|  | 	// Resources that are not yet stored in any slice need to be published. | ||||||
|  | 	// Here we track the indices of any resources that are already stored. | ||||||
|  | 	storedResourceIndices := sets.New[int]() | ||||||
|  |  | ||||||
|  | 	// Slices that don't match any driver resource can either be updated (if there | ||||||
|  | 	// are new driver resources that need to be stored) or they need to be deleted. | ||||||
|  | 	obsoleteSlices := make([]*resourceapi.ResourceSlice, 0, len(slices)) | ||||||
|  |  | ||||||
|  | 	// Match slices with resource information. | ||||||
|  | 	for _, obj := range slices { | ||||||
|  | 		slice := obj.(*resourceapi.ResourceSlice) | ||||||
|  |  | ||||||
|  | 		// TODO: network-attached resources. | ||||||
|  | 		index := indexOfModel(resources.NodeResources, &slice.ResourceModel) | ||||||
|  | 		if index >= 0 { | ||||||
|  | 			storedResourceIndices.Insert(index) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		obsoleteSlices = append(obsoleteSlices, slice) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if loggerV := logger.V(6); loggerV.Enabled() { | ||||||
|  | 		// Dump entire resource information. | ||||||
|  | 		loggerV.Info("Syncing existing driver resource slices with driver resources", "slices", klog.KObjSlice(slices), "resources", resources) | ||||||
|  | 	} else { | ||||||
|  | 		logger.V(5).Info("Syncing existing driver resource slices with driver resources", "slices", klog.KObjSlice(slices), "numResources", len(resources.NodeResources)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Retrieve node object to get UID? | ||||||
|  | 	// The result gets cached and is expected to not change while | ||||||
|  | 	// the controller runs. | ||||||
|  | 	if c.owner.UID == "" && c.owner.APIVersion == "v1" && c.owner.Kind == "Node" { | ||||||
|  | 		node, err := c.kubeClient.CoreV1().Nodes().Get(ctx, c.owner.Name, metav1.GetOptions{}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return fmt.Errorf("retrieve node %q: %w", c.owner.Name, err) | ||||||
|  | 		} | ||||||
|  | 		// There is only one worker, so no locking needed. | ||||||
|  | 		c.owner.UID = node.UID | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Update stale slices before removing what's left. | ||||||
|  | 	// | ||||||
|  | 	// We don't really know which of these slices might have | ||||||
|  | 	// been used for "the" driver resource because they don't | ||||||
|  | 	// have a unique ID. In practice, a driver is most likely | ||||||
|  | 	// to just give us one ResourceModel, in which case | ||||||
|  | 	// this isn't a problem at all. If we have more than one, | ||||||
|  | 	// then at least conceptually it currently doesn't matter | ||||||
|  | 	// where we publish it. | ||||||
|  | 	// | ||||||
|  | 	// The long-term goal is to move the handling of | ||||||
|  | 	// ResourceSlice objects into the driver, with kubelet | ||||||
|  | 	// just acting as a REST proxy. The advantage of that will | ||||||
|  | 	// be that kubelet won't need to support the same | ||||||
|  | 	// resource API version as the driver and the control plane. | ||||||
|  | 	// With that approach, the driver will be able to match | ||||||
|  | 	// up objects more intelligently. | ||||||
|  | 	numObsoleteSlices := len(obsoleteSlices) | ||||||
|  | 	for index, resource := range resources.NodeResources { | ||||||
|  | 		if storedResourceIndices.Has(index) { | ||||||
|  | 			// No need to do anything, it is already stored exactly | ||||||
|  | 			// like this in an existing slice. | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if numObsoleteSlices > 0 { | ||||||
|  | 			// Update one existing slice. | ||||||
|  | 			slice := obsoleteSlices[numObsoleteSlices-1] | ||||||
|  | 			numObsoleteSlices-- | ||||||
|  | 			slice = slice.DeepCopy() | ||||||
|  | 			slice.ResourceModel = *resource | ||||||
|  | 			logger.V(5).Info("Reusing existing resource slice", "slice", klog.KObj(slice)) | ||||||
|  | 			if _, err := c.kubeClient.ResourceV1alpha2().ResourceSlices().Update(ctx, slice, metav1.UpdateOptions{}); err != nil { | ||||||
|  | 				return fmt.Errorf("update resource slice: %w", err) | ||||||
|  | 			} | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Create a new slice. | ||||||
|  | 		slice := &resourceapi.ResourceSlice{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				GenerateName: c.owner.Name + "-" + c.driverName + "-", | ||||||
|  | 				OwnerReferences: []metav1.OwnerReference{ | ||||||
|  | 					{ | ||||||
|  | 						APIVersion: c.owner.APIVersion, | ||||||
|  | 						Kind:       c.owner.Kind, | ||||||
|  | 						Name:       c.owner.Name, | ||||||
|  | 						UID:        c.owner.UID, | ||||||
|  | 						Controller: ptr.To(true), | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			DriverName:    c.driverName, | ||||||
|  | 			ResourceModel: *resource, | ||||||
|  | 		} | ||||||
|  | 		if c.owner.APIVersion == "v1" && c.owner.Kind == "Node" { | ||||||
|  | 			slice.NodeName = c.owner.Name | ||||||
|  | 		} | ||||||
|  | 		logger.V(5).Info("Creating new resource slice", "slice", klog.KObj(slice)) | ||||||
|  | 		if _, err := c.kubeClient.ResourceV1alpha2().ResourceSlices().Create(ctx, slice, metav1.CreateOptions{}); err != nil { | ||||||
|  | 			return fmt.Errorf("create resource slice: %w", err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// All remaining slices are truly orphaned. | ||||||
|  | 	for i := 0; i < numObsoleteSlices; i++ { | ||||||
|  | 		slice := obsoleteSlices[i] | ||||||
|  | 		logger.V(5).Info("Deleting obsolete resource slice", "slice", klog.KObj(slice)) | ||||||
|  | 		if err := c.kubeClient.ResourceV1alpha2().ResourceSlices().Delete(ctx, slice.Name, metav1.DeleteOptions{}); err != nil { | ||||||
|  | 			return fmt.Errorf("delete resource slice: %w", err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func indexOfModel(models []*resourceapi.ResourceModel, model *resourceapi.ResourceModel) int { | ||||||
|  | 	for index, m := range models { | ||||||
|  | 		if apiequality.Semantic.DeepEqual(m, model) { | ||||||
|  | 			return index | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return -1 | ||||||
|  | } | ||||||
| @@ -17,7 +17,7 @@ limitations under the License. | |||||||
| // Code generated by protoc-gen-gogo. DO NOT EDIT. | // Code generated by protoc-gen-gogo. DO NOT EDIT. | ||||||
| // source: api.proto | // source: api.proto | ||||||
| 
 | 
 | ||||||
| package v1alpha3 | package v1alpha4 | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	context "context" | 	context "context" | ||||||
| @@ -29,7 +29,6 @@ import ( | |||||||
| 	codes "google.golang.org/grpc/codes" | 	codes "google.golang.org/grpc/codes" | ||||||
| 	status "google.golang.org/grpc/status" | 	status "google.golang.org/grpc/status" | ||||||
| 	io "io" | 	io "io" | ||||||
| 	v1alpha2 "k8s.io/api/resource/v1alpha2" |  | ||||||
| 	math "math" | 	math "math" | ||||||
| 	math_bits "math/bits" | 	math_bits "math/bits" | ||||||
| 	reflect "reflect" | 	reflect "reflect" | ||||||
| @@ -342,88 +341,6 @@ func (m *NodeUnprepareResourceResponse) GetError() string { | |||||||
| 	return "" | 	return "" | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type NodeListAndWatchResourcesRequest struct { |  | ||||||
| 	XXX_NoUnkeyedLiteral struct{} `json:"-"` |  | ||||||
| 	XXX_sizecache        int32    `json:"-"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *NodeListAndWatchResourcesRequest) Reset()      { *m = NodeListAndWatchResourcesRequest{} } |  | ||||||
| func (*NodeListAndWatchResourcesRequest) ProtoMessage() {} |  | ||||||
| func (*NodeListAndWatchResourcesRequest) Descriptor() ([]byte, []int) { |  | ||||||
| 	return fileDescriptor_00212fb1f9d3bf1c, []int{6} |  | ||||||
| } |  | ||||||
| func (m *NodeListAndWatchResourcesRequest) XXX_Unmarshal(b []byte) error { |  | ||||||
| 	return m.Unmarshal(b) |  | ||||||
| } |  | ||||||
| func (m *NodeListAndWatchResourcesRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { |  | ||||||
| 	if deterministic { |  | ||||||
| 		return xxx_messageInfo_NodeListAndWatchResourcesRequest.Marshal(b, m, deterministic) |  | ||||||
| 	} else { |  | ||||||
| 		b = b[:cap(b)] |  | ||||||
| 		n, err := m.MarshalToSizedBuffer(b) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 		return b[:n], nil |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| func (m *NodeListAndWatchResourcesRequest) XXX_Merge(src proto.Message) { |  | ||||||
| 	xxx_messageInfo_NodeListAndWatchResourcesRequest.Merge(m, src) |  | ||||||
| } |  | ||||||
| func (m *NodeListAndWatchResourcesRequest) XXX_Size() int { |  | ||||||
| 	return m.Size() |  | ||||||
| } |  | ||||||
| func (m *NodeListAndWatchResourcesRequest) XXX_DiscardUnknown() { |  | ||||||
| 	xxx_messageInfo_NodeListAndWatchResourcesRequest.DiscardUnknown(m) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| var xxx_messageInfo_NodeListAndWatchResourcesRequest proto.InternalMessageInfo |  | ||||||
| 
 |  | ||||||
| type NodeListAndWatchResourcesResponse struct { |  | ||||||
| 	Resources            []*v1alpha2.ResourceModel `protobuf:"bytes,1,rep,name=resources,proto3" json:"resources,omitempty"` |  | ||||||
| 	XXX_NoUnkeyedLiteral struct{}                  `json:"-"` |  | ||||||
| 	XXX_sizecache        int32                     `json:"-"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *NodeListAndWatchResourcesResponse) Reset()      { *m = NodeListAndWatchResourcesResponse{} } |  | ||||||
| func (*NodeListAndWatchResourcesResponse) ProtoMessage() {} |  | ||||||
| func (*NodeListAndWatchResourcesResponse) Descriptor() ([]byte, []int) { |  | ||||||
| 	return fileDescriptor_00212fb1f9d3bf1c, []int{7} |  | ||||||
| } |  | ||||||
| func (m *NodeListAndWatchResourcesResponse) XXX_Unmarshal(b []byte) error { |  | ||||||
| 	return m.Unmarshal(b) |  | ||||||
| } |  | ||||||
| func (m *NodeListAndWatchResourcesResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { |  | ||||||
| 	if deterministic { |  | ||||||
| 		return xxx_messageInfo_NodeListAndWatchResourcesResponse.Marshal(b, m, deterministic) |  | ||||||
| 	} else { |  | ||||||
| 		b = b[:cap(b)] |  | ||||||
| 		n, err := m.MarshalToSizedBuffer(b) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 		return b[:n], nil |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| func (m *NodeListAndWatchResourcesResponse) XXX_Merge(src proto.Message) { |  | ||||||
| 	xxx_messageInfo_NodeListAndWatchResourcesResponse.Merge(m, src) |  | ||||||
| } |  | ||||||
| func (m *NodeListAndWatchResourcesResponse) XXX_Size() int { |  | ||||||
| 	return m.Size() |  | ||||||
| } |  | ||||||
| func (m *NodeListAndWatchResourcesResponse) XXX_DiscardUnknown() { |  | ||||||
| 	xxx_messageInfo_NodeListAndWatchResourcesResponse.DiscardUnknown(m) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| var xxx_messageInfo_NodeListAndWatchResourcesResponse proto.InternalMessageInfo |  | ||||||
| 
 |  | ||||||
| func (m *NodeListAndWatchResourcesResponse) GetResources() []*v1alpha2.ResourceModel { |  | ||||||
| 	if m != nil { |  | ||||||
| 		return m.Resources |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type Claim struct { | type Claim struct { | ||||||
| 	// The ResourceClaim namespace (ResourceClaim.meta.Namespace). | 	// The ResourceClaim namespace (ResourceClaim.meta.Namespace). | ||||||
| 	// This field is REQUIRED. | 	// This field is REQUIRED. | ||||||
| @@ -433,24 +350,15 @@ type Claim struct { | |||||||
| 	Uid string `protobuf:"bytes,2,opt,name=uid,proto3" json:"uid,omitempty"` | 	Uid string `protobuf:"bytes,2,opt,name=uid,proto3" json:"uid,omitempty"` | ||||||
| 	// The name of the Resource claim (ResourceClaim.meta.Name) | 	// The name of the Resource claim (ResourceClaim.meta.Name) | ||||||
| 	// This field is REQUIRED. | 	// This field is REQUIRED. | ||||||
| 	Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` | 	Name                 string   `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` | ||||||
| 	// Resource handle (AllocationResult.ResourceHandles[*].Data) | 	XXX_NoUnkeyedLiteral struct{} `json:"-"` | ||||||
| 	// This field is REQUIRED. | 	XXX_sizecache        int32    `json:"-"` | ||||||
| 	ResourceHandle string `protobuf:"bytes,4,opt,name=resource_handle,json=resourceHandle,proto3" json:"resource_handle,omitempty"` |  | ||||||
| 	// Structured parameter resource handle (AllocationResult.ResourceHandles[*].StructuredData). |  | ||||||
| 	// This field is OPTIONAL. If present, it needs to be used |  | ||||||
| 	// instead of resource_handle. It will only have a single entry. |  | ||||||
| 	// |  | ||||||
| 	// Using "repeated" instead of "optional" is a workaround for https://github.com/gogo/protobuf/issues/713. |  | ||||||
| 	StructuredResourceHandle []*v1alpha2.StructuredResourceHandle `protobuf:"bytes,5,rep,name=structured_resource_handle,json=structuredResourceHandle,proto3" json:"structured_resource_handle,omitempty"` |  | ||||||
| 	XXX_NoUnkeyedLiteral     struct{}                             `json:"-"` |  | ||||||
| 	XXX_sizecache            int32                                `json:"-"` |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (m *Claim) Reset()      { *m = Claim{} } | func (m *Claim) Reset()      { *m = Claim{} } | ||||||
| func (*Claim) ProtoMessage() {} | func (*Claim) ProtoMessage() {} | ||||||
| func (*Claim) Descriptor() ([]byte, []int) { | func (*Claim) Descriptor() ([]byte, []int) { | ||||||
| 	return fileDescriptor_00212fb1f9d3bf1c, []int{8} | 	return fileDescriptor_00212fb1f9d3bf1c, []int{6} | ||||||
| } | } | ||||||
| func (m *Claim) XXX_Unmarshal(b []byte) error { | func (m *Claim) XXX_Unmarshal(b []byte) error { | ||||||
| 	return m.Unmarshal(b) | 	return m.Unmarshal(b) | ||||||
| @@ -500,20 +408,6 @@ func (m *Claim) GetName() string { | |||||||
| 	return "" | 	return "" | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (m *Claim) GetResourceHandle() string { |  | ||||||
| 	if m != nil { |  | ||||||
| 		return m.ResourceHandle |  | ||||||
| 	} |  | ||||||
| 	return "" |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *Claim) GetStructuredResourceHandle() []*v1alpha2.StructuredResourceHandle { |  | ||||||
| 	if m != nil { |  | ||||||
| 		return m.StructuredResourceHandle |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func init() { | func init() { | ||||||
| 	proto.RegisterType((*NodePrepareResourcesRequest)(nil), "v1alpha3.NodePrepareResourcesRequest") | 	proto.RegisterType((*NodePrepareResourcesRequest)(nil), "v1alpha3.NodePrepareResourcesRequest") | ||||||
| 	proto.RegisterType((*NodePrepareResourcesResponse)(nil), "v1alpha3.NodePrepareResourcesResponse") | 	proto.RegisterType((*NodePrepareResourcesResponse)(nil), "v1alpha3.NodePrepareResourcesResponse") | ||||||
| @@ -523,55 +417,44 @@ func init() { | |||||||
| 	proto.RegisterType((*NodeUnprepareResourcesResponse)(nil), "v1alpha3.NodeUnprepareResourcesResponse") | 	proto.RegisterType((*NodeUnprepareResourcesResponse)(nil), "v1alpha3.NodeUnprepareResourcesResponse") | ||||||
| 	proto.RegisterMapType((map[string]*NodeUnprepareResourceResponse)(nil), "v1alpha3.NodeUnprepareResourcesResponse.ClaimsEntry") | 	proto.RegisterMapType((map[string]*NodeUnprepareResourceResponse)(nil), "v1alpha3.NodeUnprepareResourcesResponse.ClaimsEntry") | ||||||
| 	proto.RegisterType((*NodeUnprepareResourceResponse)(nil), "v1alpha3.NodeUnprepareResourceResponse") | 	proto.RegisterType((*NodeUnprepareResourceResponse)(nil), "v1alpha3.NodeUnprepareResourceResponse") | ||||||
| 	proto.RegisterType((*NodeListAndWatchResourcesRequest)(nil), "v1alpha3.NodeListAndWatchResourcesRequest") |  | ||||||
| 	proto.RegisterType((*NodeListAndWatchResourcesResponse)(nil), "v1alpha3.NodeListAndWatchResourcesResponse") |  | ||||||
| 	proto.RegisterType((*Claim)(nil), "v1alpha3.Claim") | 	proto.RegisterType((*Claim)(nil), "v1alpha3.Claim") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func init() { proto.RegisterFile("api.proto", fileDescriptor_00212fb1f9d3bf1c) } | func init() { proto.RegisterFile("api.proto", fileDescriptor_00212fb1f9d3bf1c) } | ||||||
| 
 | 
 | ||||||
| var fileDescriptor_00212fb1f9d3bf1c = []byte{ | var fileDescriptor_00212fb1f9d3bf1c = []byte{ | ||||||
| 	// 631 bytes of a gzipped FileDescriptorProto | 	// 481 bytes of a gzipped FileDescriptorProto | ||||||
| 	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x55, 0xcf, 0x6e, 0xd3, 0x4c, | 	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x54, 0x4d, 0x6f, 0xd3, 0x40, | ||||||
| 	0x10, 0xcf, 0xb6, 0x4d, 0xf5, 0x65, 0x22, 0xb5, 0x9f, 0x56, 0x15, 0x0a, 0xa6, 0x98, 0x60, 0x51, | 	0x10, 0xcd, 0x36, 0x4d, 0x85, 0x27, 0x12, 0xa0, 0x55, 0x85, 0xa2, 0x50, 0x4c, 0x64, 0x51, 0x92, | ||||||
| 	0x12, 0x51, 0xb0, 0xc1, 0x05, 0x54, 0x81, 0x38, 0xd0, 0x16, 0xd4, 0xa2, 0x82, 0x90, 0x11, 0x42, | 	0x0b, 0xb6, 0x48, 0x8b, 0x54, 0x81, 0xb8, 0xa4, 0x05, 0xf1, 0x25, 0x84, 0x2c, 0x71, 0xe1, 0x02, | ||||||
| 	0xe2, 0x52, 0x36, 0xde, 0xc5, 0xb1, 0xe2, 0xd8, 0x66, 0xd7, 0xae, 0xe8, 0x8d, 0x47, 0xe0, 0xb1, | 	0x6b, 0x7b, 0x70, 0x57, 0xf9, 0xd8, 0x65, 0xd7, 0x8e, 0xd4, 0x1b, 0x3f, 0x81, 0x9f, 0xd5, 0x03, | ||||||
| 	0x7a, 0xe0, 0x80, 0x38, 0xf5, 0x84, 0x68, 0xb8, 0xf1, 0x14, 0xc8, 0x6b, 0x6f, 0xda, 0x44, 0x4e, | 	0x07, 0xc4, 0x89, 0x53, 0x45, 0xcd, 0x1f, 0x41, 0x5e, 0x3b, 0xe9, 0x87, 0x9c, 0x26, 0x52, 0x6f, | ||||||
| 	0x52, 0x89, 0xdb, 0xec, 0xfc, 0xf9, 0xcd, 0xce, 0x6f, 0x66, 0x76, 0xa1, 0x46, 0x62, 0xdf, 0x8c, | 	0x33, 0xe3, 0x9d, 0x79, 0x6f, 0xde, 0x1b, 0x19, 0x2c, 0x26, 0xb9, 0x2b, 0x95, 0x48, 0x04, 0xbd, | ||||||
| 	0x79, 0x94, 0x44, 0xf8, 0xbf, 0xc3, 0x7b, 0x24, 0x88, 0xbb, 0x64, 0x43, 0xbb, 0xe3, 0xf9, 0x49, | 	0x31, 0x7d, 0xcc, 0x46, 0xf2, 0x90, 0xed, 0xb4, 0x1f, 0xc5, 0x3c, 0x39, 0x4c, 0x03, 0x37, 0x14, | ||||||
| 	0x37, 0xed, 0x98, 0x6e, 0xd4, 0xb7, 0xbc, 0xc8, 0x8b, 0x2c, 0xe9, 0xd0, 0x49, 0x3f, 0xca, 0x93, | 	0x63, 0x2f, 0x16, 0xb1, 0xf0, 0xcc, 0x83, 0x20, 0xfd, 0x6a, 0x32, 0x93, 0x98, 0xa8, 0x68, 0x74, | ||||||
| 	0x3c, 0x48, 0x29, 0x0f, 0xd4, 0x6e, 0xf7, 0x36, 0x85, 0xe9, 0x47, 0x16, 0x89, 0x7d, 0x8b, 0x33, | 	0x5e, 0xc2, 0xdd, 0xf7, 0x22, 0xc2, 0x0f, 0x0a, 0x25, 0x53, 0xe8, 0xa3, 0x16, 0xa9, 0x0a, 0x51, | ||||||
| 	0x11, 0xa5, 0xdc, 0x65, 0x56, 0x01, 0x66, 0x5b, 0x1e, 0x0b, 0x19, 0x27, 0x09, 0xa3, 0xb9, 0xb7, | 	0xfb, 0xf8, 0x2d, 0x45, 0x9d, 0xd0, 0x2e, 0x6c, 0x84, 0x23, 0xc6, 0xc7, 0xba, 0x45, 0x3a, 0xf5, | ||||||
| 	0xf1, 0x1c, 0xae, 0xbc, 0x8a, 0x28, 0x7b, 0xcd, 0x59, 0x4c, 0x38, 0x73, 0x0a, 0x7f, 0xe1, 0xb0, | 	0x5e, 0xb3, 0x7f, 0xcb, 0x9d, 0x01, 0xb9, 0xfb, 0x79, 0xdd, 0x2f, 0x3f, 0x3b, 0x3f, 0x09, 0x6c, | ||||||
| 	0x4f, 0x29, 0x13, 0x09, 0x6e, 0xc1, 0xa2, 0x1b, 0x10, 0xbf, 0x2f, 0x1a, 0xa8, 0x39, 0xdf, 0xae, | 	0x55, 0x0f, 0xd2, 0x52, 0x4c, 0x34, 0xd2, 0x37, 0x97, 0x26, 0xf5, 0xcf, 0x26, 0x5d, 0xd5, 0x57, | ||||||
| 	0xdb, 0xcb, 0xa6, 0xba, 0x96, 0xb9, 0x9d, 0xe9, 0x9d, 0xc2, 0x6c, 0x7c, 0x43, 0xb0, 0x5a, 0x0e, | 	0xc0, 0xe8, 0x17, 0x93, 0x44, 0x1d, 0xcd, 0xc0, 0xda, 0x5f, 0xa0, 0x79, 0xae, 0x4c, 0x6f, 0x43, | ||||||
| 	0x24, 0xe2, 0x28, 0x14, 0x0c, 0xbf, 0x18, 0x43, 0xb2, 0xcf, 0x90, 0xa6, 0xc5, 0xe5, 0x69, 0xc4, | 	0x7d, 0x88, 0x47, 0x2d, 0xd2, 0x21, 0x3d, 0xcb, 0xcf, 0x43, 0xfa, 0x0c, 0x1a, 0x53, 0x36, 0x4a, | ||||||
| 	0xb3, 0x30, 0xe1, 0x47, 0x2a, 0x99, 0xf6, 0x01, 0xea, 0xe7, 0xd4, 0xf8, 0x7f, 0x98, 0xef, 0xb1, | 	0xb1, 0xb5, 0xd6, 0x21, 0xbd, 0x66, 0x7f, 0xfb, 0x4a, 0xac, 0x19, 0x94, 0x5f, 0xf4, 0x3c, 0x5d, | ||||||
| 	0xa3, 0x06, 0x6a, 0xa2, 0x76, 0xcd, 0xc9, 0x44, 0xfc, 0x18, 0xaa, 0x87, 0x24, 0x48, 0x59, 0x63, | 	0xdb, 0x23, 0x4e, 0x54, 0x29, 0xcb, 0x7c, 0x19, 0x0f, 0x9a, 0x61, 0xc4, 0x3f, 0x47, 0x38, 0xe5, | ||||||
| 	0xae, 0x89, 0xda, 0x75, 0x7b, 0x6d, 0x6a, 0x2e, 0x95, 0xca, 0xc9, 0x63, 0x1e, 0xcd, 0x6d, 0x22, | 	0x21, 0x16, 0x1b, 0x59, 0x83, 0x9b, 0xd9, 0xc9, 0x7d, 0xd8, 0x3f, 0x78, 0x7d, 0x50, 0x54, 0x7d, | ||||||
| 	0x83, 0x96, 0xd2, 0x32, 0x2c, 0xc6, 0x82, 0xba, 0x4b, 0xfd, 0x03, 0xca, 0x0e, 0x7d, 0x97, 0xe5, | 	0x08, 0x23, 0x5e, 0xc6, 0x74, 0x13, 0x1a, 0xa8, 0x94, 0x50, 0x86, 0x90, 0xe5, 0x17, 0x89, 0xf3, | ||||||
| 	0x15, 0xd5, 0xb6, 0x96, 0x06, 0x3f, 0xaf, 0xc1, 0xf6, 0xce, 0xde, 0x4e, 0xae, 0x75, 0xc0, 0xa5, | 	0x0a, 0xee, 0xe5, 0x28, 0x1f, 0x27, 0xf2, 0xba, 0xf2, 0xff, 0x26, 0x60, 0x2f, 0x1a, 0x55, 0x72, | ||||||
| 	0x7e, 0x21, 0xe3, 0x15, 0xa8, 0x32, 0xce, 0x23, 0x2e, 0x2f, 0x54, 0x73, 0xf2, 0x83, 0xb1, 0x0b, | 	0x7e, 0x77, 0x69, 0xd6, 0xee, 0x45, 0x51, 0x16, 0x77, 0x56, 0x5a, 0x10, 0x2c, 0xb3, 0xe0, 0xf9, | ||||||
| 	0x57, 0xb3, 0x2c, 0x6f, 0xc3, 0xf8, 0x5f, 0xe9, 0xff, 0x81, 0x40, 0x9f, 0x04, 0x55, 0xdc, 0x79, | 	0x45, 0x0b, 0xba, 0x4b, 0xd0, 0xaa, 0x4c, 0x78, 0xb2, 0x40, 0x9e, 0xf9, 0x4a, 0x73, 0x55, 0xc9, | ||||||
| 	0x7f, 0x0c, 0xeb, 0xfe, 0x28, 0x29, 0x93, 0x23, 0x4b, 0x5b, 0xd0, 0x99, 0xd5, 0x82, 0x27, 0xa3, | 	0x79, 0x55, 0xdf, 0x42, 0xc3, 0x50, 0xa3, 0x5b, 0x60, 0x4d, 0xd8, 0x18, 0xb5, 0x64, 0x21, 0x96, | ||||||
| 	0x2d, 0x68, 0xcd, 0xc8, 0x56, 0xd6, 0x84, 0x07, 0x13, 0xe8, 0x19, 0x96, 0x34, 0x64, 0x15, 0x9d, | 	0x4f, 0xce, 0x0a, 0x39, 0xe5, 0x94, 0x47, 0xa5, 0x21, 0x79, 0x48, 0x29, 0xac, 0xe7, 0x9f, 0x5b, | ||||||
| 	0x67, 0xd5, 0x80, 0x66, 0x16, 0xb6, 0xef, 0x8b, 0xe4, 0x69, 0x48, 0xdf, 0x91, 0xc4, 0xed, 0x8e, | 	0x75, 0x53, 0x32, 0x71, 0xff, 0x84, 0xc0, 0x7a, 0x4e, 0x82, 0xc6, 0xb0, 0x59, 0x75, 0xa7, 0x74, | ||||||
| 	0x13, 0x6b, 0x84, 0x70, 0x7d, 0x8a, 0x4f, 0x01, 0xbf, 0x07, 0x35, 0xb5, 0x40, 0x8a, 0xb4, 0x75, | 	0x7b, 0xd9, 0x1d, 0x1b, 0x27, 0xdb, 0x0f, 0x57, 0x3b, 0x77, 0xa7, 0x46, 0xc7, 0x70, 0xa7, 0xda, | ||||||
| 	0x33, 0xdf, 0x2e, 0x33, 0x5b, 0x54, 0x65, 0x54, 0xa5, 0xd9, 0xa6, 0xc2, 0x78, 0x19, 0x51, 0x16, | 	0x0f, 0xda, 0x5d, 0xee, 0x58, 0x01, 0xd6, 0x5b, 0xd5, 0x5a, 0xa7, 0x36, 0x18, 0x1c, 0x9f, 0xda, | ||||||
| 	0x38, 0x67, 0xd1, 0xc6, 0x1f, 0x04, 0x55, 0xc9, 0x17, 0x5e, 0x85, 0x5a, 0x48, 0xfa, 0x4c, 0xc4, | 	0xe4, 0xcf, 0xa9, 0x5d, 0xfb, 0x9e, 0xd9, 0xe4, 0x38, 0xb3, 0xc9, 0xaf, 0xcc, 0x26, 0x7f, 0x33, | ||||||
| 	0xc4, 0x65, 0xc5, 0xbd, 0xcf, 0x14, 0x19, 0x8f, 0xa9, 0x4f, 0x8b, 0x29, 0xc9, 0x44, 0x8c, 0x61, | 	0x9b, 0xfc, 0xf8, 0x67, 0xd7, 0x3e, 0x3d, 0x18, 0xee, 0x69, 0x97, 0x0b, 0x6f, 0x98, 0x06, 0x38, | ||||||
| 	0x21, 0x33, 0x37, 0xe6, 0xa5, 0x4a, 0xca, 0xb8, 0x05, 0xcb, 0x0a, 0xfa, 0xa0, 0x4b, 0x42, 0x1a, | 	0xc2, 0xc4, 0x93, 0xc3, 0xd8, 0x63, 0x92, 0x6b, 0x2f, 0x52, 0xcc, 0x2b, 0x41, 0x76, 0x83, 0x0d, | ||||||
| 	0xb0, 0xc6, 0x82, 0x34, 0x2f, 0x29, 0xf5, 0xae, 0xd4, 0xe2, 0x04, 0x34, 0x91, 0xf0, 0xd4, 0x4d, | 	0xf3, 0x2f, 0xd9, 0xf9, 0x1f, 0x00, 0x00, 0xff, 0xff, 0x65, 0xc5, 0xc2, 0x0e, 0x91, 0x04, 0x00, | ||||||
| 	0x52, 0xce, 0xe8, 0xc1, 0x78, 0x4c, 0x55, 0x96, 0xf4, 0x70, 0x7a, 0x49, 0x6f, 0x86, 0xf1, 0xce, | 	0x00, | ||||||
| 	0x08, 0xb6, 0xd3, 0x10, 0x13, 0x2c, 0xf6, 0xc9, 0x1c, 0x2c, 0x64, 0xec, 0x62, 0x0f, 0x56, 0xca, |  | ||||||
| 	0x76, 0x1b, 0xaf, 0xcd, 0xda, 0x7d, 0xd9, 0x24, 0xed, 0xe6, 0xc5, 0x9e, 0x08, 0xa3, 0x82, 0xfb, |  | ||||||
| 	0x70, 0xa9, 0x7c, 0x86, 0x71, 0x6b, 0xf6, 0x94, 0xe7, 0xc9, 0xda, 0x17, 0x5d, 0x07, 0xa3, 0x82, |  | ||||||
| 	0x3f, 0xc3, 0xe5, 0x89, 0xd3, 0x83, 0x6f, 0x8d, 0x02, 0x4d, 0x1b, 0x43, 0x6d, 0xfd, 0x42, 0xbe, |  | ||||||
| 	0x2a, 0xef, 0x5d, 0xb4, 0xb5, 0x75, 0x7c, 0xaa, 0xa3, 0x93, 0x53, 0xbd, 0xf2, 0x65, 0xa0, 0xa3, |  | ||||||
| 	0xe3, 0x81, 0x8e, 0xbe, 0x0f, 0x74, 0xf4, 0x6b, 0xa0, 0xa3, 0xaf, 0xbf, 0xf5, 0xca, 0xfb, 0x1b, |  | ||||||
| 	0xc5, 0xd3, 0xdf, 0x4b, 0x3b, 0x2c, 0x60, 0x89, 0x15, 0xf7, 0xbc, 0xec, 0x1b, 0x10, 0x16, 0xe5, |  | ||||||
| 	0x44, 0x7d, 0x01, 0x1b, 0x9d, 0x45, 0xf9, 0xf2, 0x6f, 0xfc, 0x0d, 0x00, 0x00, 0xff, 0xff, 0xb2, |  | ||||||
| 	0x0b, 0x57, 0x0c, 0x6d, 0x06, 0x00, 0x00, |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Reference imports to suppress errors if they are not otherwise used. | // Reference imports to suppress errors if they are not otherwise used. | ||||||
| @@ -594,10 +477,6 @@ type NodeClient interface { | |||||||
| 	// NodeUnprepareResources is the opposite of NodePrepareResources. | 	// NodeUnprepareResources is the opposite of NodePrepareResources. | ||||||
| 	// The same error handling rules apply, | 	// The same error handling rules apply, | ||||||
| 	NodeUnprepareResources(ctx context.Context, in *NodeUnprepareResourcesRequest, opts ...grpc.CallOption) (*NodeUnprepareResourcesResponse, error) | 	NodeUnprepareResources(ctx context.Context, in *NodeUnprepareResourcesRequest, opts ...grpc.CallOption) (*NodeUnprepareResourcesResponse, error) | ||||||
| 	// NodeListAndWatchResources returns a stream of NodeResourcesResponse objects. |  | ||||||
| 	// At the start and whenever resource availability changes, the |  | ||||||
| 	// plugin must send one such object with all information to the Kubelet. |  | ||||||
| 	NodeListAndWatchResources(ctx context.Context, in *NodeListAndWatchResourcesRequest, opts ...grpc.CallOption) (Node_NodeListAndWatchResourcesClient, error) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type nodeClient struct { | type nodeClient struct { | ||||||
| @@ -626,38 +505,6 @@ func (c *nodeClient) NodeUnprepareResources(ctx context.Context, in *NodeUnprepa | |||||||
| 	return out, nil | 	return out, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (c *nodeClient) NodeListAndWatchResources(ctx context.Context, in *NodeListAndWatchResourcesRequest, opts ...grpc.CallOption) (Node_NodeListAndWatchResourcesClient, error) { |  | ||||||
| 	stream, err := c.cc.NewStream(ctx, &_Node_serviceDesc.Streams[0], "/v1alpha3.Node/NodeListAndWatchResources", opts...) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	x := &nodeNodeListAndWatchResourcesClient{stream} |  | ||||||
| 	if err := x.ClientStream.SendMsg(in); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	if err := x.ClientStream.CloseSend(); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	return x, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type Node_NodeListAndWatchResourcesClient interface { |  | ||||||
| 	Recv() (*NodeListAndWatchResourcesResponse, error) |  | ||||||
| 	grpc.ClientStream |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type nodeNodeListAndWatchResourcesClient struct { |  | ||||||
| 	grpc.ClientStream |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (x *nodeNodeListAndWatchResourcesClient) Recv() (*NodeListAndWatchResourcesResponse, error) { |  | ||||||
| 	m := new(NodeListAndWatchResourcesResponse) |  | ||||||
| 	if err := x.ClientStream.RecvMsg(m); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	return m, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // NodeServer is the server API for Node service. | // NodeServer is the server API for Node service. | ||||||
| type NodeServer interface { | type NodeServer interface { | ||||||
| 	// NodePrepareResources prepares several ResourceClaims | 	// NodePrepareResources prepares several ResourceClaims | ||||||
| @@ -668,10 +515,6 @@ type NodeServer interface { | |||||||
| 	// NodeUnprepareResources is the opposite of NodePrepareResources. | 	// NodeUnprepareResources is the opposite of NodePrepareResources. | ||||||
| 	// The same error handling rules apply, | 	// The same error handling rules apply, | ||||||
| 	NodeUnprepareResources(context.Context, *NodeUnprepareResourcesRequest) (*NodeUnprepareResourcesResponse, error) | 	NodeUnprepareResources(context.Context, *NodeUnprepareResourcesRequest) (*NodeUnprepareResourcesResponse, error) | ||||||
| 	// NodeListAndWatchResources returns a stream of NodeResourcesResponse objects. |  | ||||||
| 	// At the start and whenever resource availability changes, the |  | ||||||
| 	// plugin must send one such object with all information to the Kubelet. |  | ||||||
| 	NodeListAndWatchResources(*NodeListAndWatchResourcesRequest, Node_NodeListAndWatchResourcesServer) error |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // UnimplementedNodeServer can be embedded to have forward compatible implementations. | // UnimplementedNodeServer can be embedded to have forward compatible implementations. | ||||||
| @@ -684,9 +527,6 @@ func (*UnimplementedNodeServer) NodePrepareResources(ctx context.Context, req *N | |||||||
| func (*UnimplementedNodeServer) NodeUnprepareResources(ctx context.Context, req *NodeUnprepareResourcesRequest) (*NodeUnprepareResourcesResponse, error) { | func (*UnimplementedNodeServer) NodeUnprepareResources(ctx context.Context, req *NodeUnprepareResourcesRequest) (*NodeUnprepareResourcesResponse, error) { | ||||||
| 	return nil, status.Errorf(codes.Unimplemented, "method NodeUnprepareResources not implemented") | 	return nil, status.Errorf(codes.Unimplemented, "method NodeUnprepareResources not implemented") | ||||||
| } | } | ||||||
| func (*UnimplementedNodeServer) NodeListAndWatchResources(req *NodeListAndWatchResourcesRequest, srv Node_NodeListAndWatchResourcesServer) error { |  | ||||||
| 	return status.Errorf(codes.Unimplemented, "method NodeListAndWatchResources not implemented") |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| func RegisterNodeServer(s *grpc.Server, srv NodeServer) { | func RegisterNodeServer(s *grpc.Server, srv NodeServer) { | ||||||
| 	s.RegisterService(&_Node_serviceDesc, srv) | 	s.RegisterService(&_Node_serviceDesc, srv) | ||||||
| @@ -728,27 +568,6 @@ func _Node_NodeUnprepareResources_Handler(srv interface{}, ctx context.Context, | |||||||
| 	return interceptor(ctx, in, info, handler) | 	return interceptor(ctx, in, info, handler) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func _Node_NodeListAndWatchResources_Handler(srv interface{}, stream grpc.ServerStream) error { |  | ||||||
| 	m := new(NodeListAndWatchResourcesRequest) |  | ||||||
| 	if err := stream.RecvMsg(m); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return srv.(NodeServer).NodeListAndWatchResources(m, &nodeNodeListAndWatchResourcesServer{stream}) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type Node_NodeListAndWatchResourcesServer interface { |  | ||||||
| 	Send(*NodeListAndWatchResourcesResponse) error |  | ||||||
| 	grpc.ServerStream |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type nodeNodeListAndWatchResourcesServer struct { |  | ||||||
| 	grpc.ServerStream |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (x *nodeNodeListAndWatchResourcesServer) Send(m *NodeListAndWatchResourcesResponse) error { |  | ||||||
| 	return x.ServerStream.SendMsg(m) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| var _Node_serviceDesc = grpc.ServiceDesc{ | var _Node_serviceDesc = grpc.ServiceDesc{ | ||||||
| 	ServiceName: "v1alpha3.Node", | 	ServiceName: "v1alpha3.Node", | ||||||
| 	HandlerType: (*NodeServer)(nil), | 	HandlerType: (*NodeServer)(nil), | ||||||
| @@ -762,13 +581,7 @@ var _Node_serviceDesc = grpc.ServiceDesc{ | |||||||
| 			Handler:    _Node_NodeUnprepareResources_Handler, | 			Handler:    _Node_NodeUnprepareResources_Handler, | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
| 	Streams: []grpc.StreamDesc{ | 	Streams:  []grpc.StreamDesc{}, | ||||||
| 		{ |  | ||||||
| 			StreamName:    "NodeListAndWatchResources", |  | ||||||
| 			Handler:       _Node_NodeListAndWatchResources_Handler, |  | ||||||
| 			ServerStreams: true, |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	Metadata: "api.proto", | 	Metadata: "api.proto", | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @@ -1013,66 +826,6 @@ func (m *NodeUnprepareResourceResponse) MarshalToSizedBuffer(dAtA []byte) (int, | |||||||
| 	return len(dAtA) - i, nil | 	return len(dAtA) - i, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (m *NodeListAndWatchResourcesRequest) Marshal() (dAtA []byte, err error) { |  | ||||||
| 	size := m.Size() |  | ||||||
| 	dAtA = make([]byte, size) |  | ||||||
| 	n, err := m.MarshalToSizedBuffer(dAtA[:size]) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	return dAtA[:n], nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *NodeListAndWatchResourcesRequest) MarshalTo(dAtA []byte) (int, error) { |  | ||||||
| 	size := m.Size() |  | ||||||
| 	return m.MarshalToSizedBuffer(dAtA[:size]) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *NodeListAndWatchResourcesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { |  | ||||||
| 	i := len(dAtA) |  | ||||||
| 	_ = i |  | ||||||
| 	var l int |  | ||||||
| 	_ = l |  | ||||||
| 	return len(dAtA) - i, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *NodeListAndWatchResourcesResponse) Marshal() (dAtA []byte, err error) { |  | ||||||
| 	size := m.Size() |  | ||||||
| 	dAtA = make([]byte, size) |  | ||||||
| 	n, err := m.MarshalToSizedBuffer(dAtA[:size]) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	return dAtA[:n], nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *NodeListAndWatchResourcesResponse) MarshalTo(dAtA []byte) (int, error) { |  | ||||||
| 	size := m.Size() |  | ||||||
| 	return m.MarshalToSizedBuffer(dAtA[:size]) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *NodeListAndWatchResourcesResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { |  | ||||||
| 	i := len(dAtA) |  | ||||||
| 	_ = i |  | ||||||
| 	var l int |  | ||||||
| 	_ = l |  | ||||||
| 	if len(m.Resources) > 0 { |  | ||||||
| 		for iNdEx := len(m.Resources) - 1; iNdEx >= 0; iNdEx-- { |  | ||||||
| 			{ |  | ||||||
| 				size, err := m.Resources[iNdEx].MarshalToSizedBuffer(dAtA[:i]) |  | ||||||
| 				if err != nil { |  | ||||||
| 					return 0, err |  | ||||||
| 				} |  | ||||||
| 				i -= size |  | ||||||
| 				i = encodeVarintApi(dAtA, i, uint64(size)) |  | ||||||
| 			} |  | ||||||
| 			i-- |  | ||||||
| 			dAtA[i] = 0xa |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return len(dAtA) - i, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *Claim) Marshal() (dAtA []byte, err error) { | func (m *Claim) Marshal() (dAtA []byte, err error) { | ||||||
| 	size := m.Size() | 	size := m.Size() | ||||||
| 	dAtA = make([]byte, size) | 	dAtA = make([]byte, size) | ||||||
| @@ -1093,27 +846,6 @@ func (m *Claim) MarshalToSizedBuffer(dAtA []byte) (int, error) { | |||||||
| 	_ = i | 	_ = i | ||||||
| 	var l int | 	var l int | ||||||
| 	_ = l | 	_ = l | ||||||
| 	if len(m.StructuredResourceHandle) > 0 { |  | ||||||
| 		for iNdEx := len(m.StructuredResourceHandle) - 1; iNdEx >= 0; iNdEx-- { |  | ||||||
| 			{ |  | ||||||
| 				size, err := m.StructuredResourceHandle[iNdEx].MarshalToSizedBuffer(dAtA[:i]) |  | ||||||
| 				if err != nil { |  | ||||||
| 					return 0, err |  | ||||||
| 				} |  | ||||||
| 				i -= size |  | ||||||
| 				i = encodeVarintApi(dAtA, i, uint64(size)) |  | ||||||
| 			} |  | ||||||
| 			i-- |  | ||||||
| 			dAtA[i] = 0x2a |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	if len(m.ResourceHandle) > 0 { |  | ||||||
| 		i -= len(m.ResourceHandle) |  | ||||||
| 		copy(dAtA[i:], m.ResourceHandle) |  | ||||||
| 		i = encodeVarintApi(dAtA, i, uint64(len(m.ResourceHandle))) |  | ||||||
| 		i-- |  | ||||||
| 		dAtA[i] = 0x22 |  | ||||||
| 	} |  | ||||||
| 	if len(m.Name) > 0 { | 	if len(m.Name) > 0 { | ||||||
| 		i -= len(m.Name) | 		i -= len(m.Name) | ||||||
| 		copy(dAtA[i:], m.Name) | 		copy(dAtA[i:], m.Name) | ||||||
| @@ -1255,30 +987,6 @@ func (m *NodeUnprepareResourceResponse) Size() (n int) { | |||||||
| 	return n | 	return n | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (m *NodeListAndWatchResourcesRequest) Size() (n int) { |  | ||||||
| 	if m == nil { |  | ||||||
| 		return 0 |  | ||||||
| 	} |  | ||||||
| 	var l int |  | ||||||
| 	_ = l |  | ||||||
| 	return n |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *NodeListAndWatchResourcesResponse) Size() (n int) { |  | ||||||
| 	if m == nil { |  | ||||||
| 		return 0 |  | ||||||
| 	} |  | ||||||
| 	var l int |  | ||||||
| 	_ = l |  | ||||||
| 	if len(m.Resources) > 0 { |  | ||||||
| 		for _, e := range m.Resources { |  | ||||||
| 			l = e.Size() |  | ||||||
| 			n += 1 + l + sovApi(uint64(l)) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return n |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *Claim) Size() (n int) { | func (m *Claim) Size() (n int) { | ||||||
| 	if m == nil { | 	if m == nil { | ||||||
| 		return 0 | 		return 0 | ||||||
| @@ -1297,16 +1005,6 @@ func (m *Claim) Size() (n int) { | |||||||
| 	if l > 0 { | 	if l > 0 { | ||||||
| 		n += 1 + l + sovApi(uint64(l)) | 		n += 1 + l + sovApi(uint64(l)) | ||||||
| 	} | 	} | ||||||
| 	l = len(m.ResourceHandle) |  | ||||||
| 	if l > 0 { |  | ||||||
| 		n += 1 + l + sovApi(uint64(l)) |  | ||||||
| 	} |  | ||||||
| 	if len(m.StructuredResourceHandle) > 0 { |  | ||||||
| 		for _, e := range m.StructuredResourceHandle { |  | ||||||
| 			l = e.Size() |  | ||||||
| 			n += 1 + l + sovApi(uint64(l)) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return n | 	return n | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @@ -1407,45 +1105,14 @@ func (this *NodeUnprepareResourceResponse) String() string { | |||||||
| 	}, "") | 	}, "") | ||||||
| 	return s | 	return s | ||||||
| } | } | ||||||
| func (this *NodeListAndWatchResourcesRequest) String() string { |  | ||||||
| 	if this == nil { |  | ||||||
| 		return "nil" |  | ||||||
| 	} |  | ||||||
| 	s := strings.Join([]string{`&NodeListAndWatchResourcesRequest{`, |  | ||||||
| 		`}`, |  | ||||||
| 	}, "") |  | ||||||
| 	return s |  | ||||||
| } |  | ||||||
| func (this *NodeListAndWatchResourcesResponse) String() string { |  | ||||||
| 	if this == nil { |  | ||||||
| 		return "nil" |  | ||||||
| 	} |  | ||||||
| 	repeatedStringForResources := "[]*ResourceModel{" |  | ||||||
| 	for _, f := range this.Resources { |  | ||||||
| 		repeatedStringForResources += strings.Replace(fmt.Sprintf("%v", f), "ResourceModel", "v1alpha2.ResourceModel", 1) + "," |  | ||||||
| 	} |  | ||||||
| 	repeatedStringForResources += "}" |  | ||||||
| 	s := strings.Join([]string{`&NodeListAndWatchResourcesResponse{`, |  | ||||||
| 		`Resources:` + repeatedStringForResources + `,`, |  | ||||||
| 		`}`, |  | ||||||
| 	}, "") |  | ||||||
| 	return s |  | ||||||
| } |  | ||||||
| func (this *Claim) String() string { | func (this *Claim) String() string { | ||||||
| 	if this == nil { | 	if this == nil { | ||||||
| 		return "nil" | 		return "nil" | ||||||
| 	} | 	} | ||||||
| 	repeatedStringForStructuredResourceHandle := "[]*StructuredResourceHandle{" |  | ||||||
| 	for _, f := range this.StructuredResourceHandle { |  | ||||||
| 		repeatedStringForStructuredResourceHandle += strings.Replace(fmt.Sprintf("%v", f), "StructuredResourceHandle", "v1alpha2.StructuredResourceHandle", 1) + "," |  | ||||||
| 	} |  | ||||||
| 	repeatedStringForStructuredResourceHandle += "}" |  | ||||||
| 	s := strings.Join([]string{`&Claim{`, | 	s := strings.Join([]string{`&Claim{`, | ||||||
| 		`Namespace:` + fmt.Sprintf("%v", this.Namespace) + `,`, | 		`Namespace:` + fmt.Sprintf("%v", this.Namespace) + `,`, | ||||||
| 		`Uid:` + fmt.Sprintf("%v", this.Uid) + `,`, | 		`Uid:` + fmt.Sprintf("%v", this.Uid) + `,`, | ||||||
| 		`Name:` + fmt.Sprintf("%v", this.Name) + `,`, | 		`Name:` + fmt.Sprintf("%v", this.Name) + `,`, | ||||||
| 		`ResourceHandle:` + fmt.Sprintf("%v", this.ResourceHandle) + `,`, |  | ||||||
| 		`StructuredResourceHandle:` + repeatedStringForStructuredResourceHandle + `,`, |  | ||||||
| 		`}`, | 		`}`, | ||||||
| 	}, "") | 	}, "") | ||||||
| 	return s | 	return s | ||||||
| @@ -2180,140 +1847,6 @@ func (m *NodeUnprepareResourceResponse) Unmarshal(dAtA []byte) error { | |||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| func (m *NodeListAndWatchResourcesRequest) Unmarshal(dAtA []byte) error { |  | ||||||
| 	l := len(dAtA) |  | ||||||
| 	iNdEx := 0 |  | ||||||
| 	for iNdEx < l { |  | ||||||
| 		preIndex := iNdEx |  | ||||||
| 		var wire uint64 |  | ||||||
| 		for shift := uint(0); ; shift += 7 { |  | ||||||
| 			if shift >= 64 { |  | ||||||
| 				return ErrIntOverflowApi |  | ||||||
| 			} |  | ||||||
| 			if iNdEx >= l { |  | ||||||
| 				return io.ErrUnexpectedEOF |  | ||||||
| 			} |  | ||||||
| 			b := dAtA[iNdEx] |  | ||||||
| 			iNdEx++ |  | ||||||
| 			wire |= uint64(b&0x7F) << shift |  | ||||||
| 			if b < 0x80 { |  | ||||||
| 				break |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		fieldNum := int32(wire >> 3) |  | ||||||
| 		wireType := int(wire & 0x7) |  | ||||||
| 		if wireType == 4 { |  | ||||||
| 			return fmt.Errorf("proto: NodeListAndWatchResourcesRequest: wiretype end group for non-group") |  | ||||||
| 		} |  | ||||||
| 		if fieldNum <= 0 { |  | ||||||
| 			return fmt.Errorf("proto: NodeListAndWatchResourcesRequest: illegal tag %d (wire type %d)", fieldNum, wire) |  | ||||||
| 		} |  | ||||||
| 		switch fieldNum { |  | ||||||
| 		default: |  | ||||||
| 			iNdEx = preIndex |  | ||||||
| 			skippy, err := skipApi(dAtA[iNdEx:]) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
| 			if (skippy < 0) || (iNdEx+skippy) < 0 { |  | ||||||
| 				return ErrInvalidLengthApi |  | ||||||
| 			} |  | ||||||
| 			if (iNdEx + skippy) > l { |  | ||||||
| 				return io.ErrUnexpectedEOF |  | ||||||
| 			} |  | ||||||
| 			iNdEx += skippy |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if iNdEx > l { |  | ||||||
| 		return io.ErrUnexpectedEOF |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| func (m *NodeListAndWatchResourcesResponse) Unmarshal(dAtA []byte) error { |  | ||||||
| 	l := len(dAtA) |  | ||||||
| 	iNdEx := 0 |  | ||||||
| 	for iNdEx < l { |  | ||||||
| 		preIndex := iNdEx |  | ||||||
| 		var wire uint64 |  | ||||||
| 		for shift := uint(0); ; shift += 7 { |  | ||||||
| 			if shift >= 64 { |  | ||||||
| 				return ErrIntOverflowApi |  | ||||||
| 			} |  | ||||||
| 			if iNdEx >= l { |  | ||||||
| 				return io.ErrUnexpectedEOF |  | ||||||
| 			} |  | ||||||
| 			b := dAtA[iNdEx] |  | ||||||
| 			iNdEx++ |  | ||||||
| 			wire |= uint64(b&0x7F) << shift |  | ||||||
| 			if b < 0x80 { |  | ||||||
| 				break |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		fieldNum := int32(wire >> 3) |  | ||||||
| 		wireType := int(wire & 0x7) |  | ||||||
| 		if wireType == 4 { |  | ||||||
| 			return fmt.Errorf("proto: NodeListAndWatchResourcesResponse: wiretype end group for non-group") |  | ||||||
| 		} |  | ||||||
| 		if fieldNum <= 0 { |  | ||||||
| 			return fmt.Errorf("proto: NodeListAndWatchResourcesResponse: illegal tag %d (wire type %d)", fieldNum, wire) |  | ||||||
| 		} |  | ||||||
| 		switch fieldNum { |  | ||||||
| 		case 1: |  | ||||||
| 			if wireType != 2 { |  | ||||||
| 				return fmt.Errorf("proto: wrong wireType = %d for field Resources", wireType) |  | ||||||
| 			} |  | ||||||
| 			var msglen int |  | ||||||
| 			for shift := uint(0); ; shift += 7 { |  | ||||||
| 				if shift >= 64 { |  | ||||||
| 					return ErrIntOverflowApi |  | ||||||
| 				} |  | ||||||
| 				if iNdEx >= l { |  | ||||||
| 					return io.ErrUnexpectedEOF |  | ||||||
| 				} |  | ||||||
| 				b := dAtA[iNdEx] |  | ||||||
| 				iNdEx++ |  | ||||||
| 				msglen |= int(b&0x7F) << shift |  | ||||||
| 				if b < 0x80 { |  | ||||||
| 					break |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 			if msglen < 0 { |  | ||||||
| 				return ErrInvalidLengthApi |  | ||||||
| 			} |  | ||||||
| 			postIndex := iNdEx + msglen |  | ||||||
| 			if postIndex < 0 { |  | ||||||
| 				return ErrInvalidLengthApi |  | ||||||
| 			} |  | ||||||
| 			if postIndex > l { |  | ||||||
| 				return io.ErrUnexpectedEOF |  | ||||||
| 			} |  | ||||||
| 			m.Resources = append(m.Resources, &v1alpha2.ResourceModel{}) |  | ||||||
| 			if err := m.Resources[len(m.Resources)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
| 			iNdEx = postIndex |  | ||||||
| 		default: |  | ||||||
| 			iNdEx = preIndex |  | ||||||
| 			skippy, err := skipApi(dAtA[iNdEx:]) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
| 			if (skippy < 0) || (iNdEx+skippy) < 0 { |  | ||||||
| 				return ErrInvalidLengthApi |  | ||||||
| 			} |  | ||||||
| 			if (iNdEx + skippy) > l { |  | ||||||
| 				return io.ErrUnexpectedEOF |  | ||||||
| 			} |  | ||||||
| 			iNdEx += skippy |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if iNdEx > l { |  | ||||||
| 		return io.ErrUnexpectedEOF |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| func (m *Claim) Unmarshal(dAtA []byte) error { | func (m *Claim) Unmarshal(dAtA []byte) error { | ||||||
| 	l := len(dAtA) | 	l := len(dAtA) | ||||||
| 	iNdEx := 0 | 	iNdEx := 0 | ||||||
| @@ -2439,72 +1972,6 @@ func (m *Claim) Unmarshal(dAtA []byte) error { | |||||||
| 			} | 			} | ||||||
| 			m.Name = string(dAtA[iNdEx:postIndex]) | 			m.Name = string(dAtA[iNdEx:postIndex]) | ||||||
| 			iNdEx = postIndex | 			iNdEx = postIndex | ||||||
| 		case 4: |  | ||||||
| 			if wireType != 2 { |  | ||||||
| 				return fmt.Errorf("proto: wrong wireType = %d for field ResourceHandle", wireType) |  | ||||||
| 			} |  | ||||||
| 			var stringLen uint64 |  | ||||||
| 			for shift := uint(0); ; shift += 7 { |  | ||||||
| 				if shift >= 64 { |  | ||||||
| 					return ErrIntOverflowApi |  | ||||||
| 				} |  | ||||||
| 				if iNdEx >= l { |  | ||||||
| 					return io.ErrUnexpectedEOF |  | ||||||
| 				} |  | ||||||
| 				b := dAtA[iNdEx] |  | ||||||
| 				iNdEx++ |  | ||||||
| 				stringLen |= uint64(b&0x7F) << shift |  | ||||||
| 				if b < 0x80 { |  | ||||||
| 					break |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 			intStringLen := int(stringLen) |  | ||||||
| 			if intStringLen < 0 { |  | ||||||
| 				return ErrInvalidLengthApi |  | ||||||
| 			} |  | ||||||
| 			postIndex := iNdEx + intStringLen |  | ||||||
| 			if postIndex < 0 { |  | ||||||
| 				return ErrInvalidLengthApi |  | ||||||
| 			} |  | ||||||
| 			if postIndex > l { |  | ||||||
| 				return io.ErrUnexpectedEOF |  | ||||||
| 			} |  | ||||||
| 			m.ResourceHandle = string(dAtA[iNdEx:postIndex]) |  | ||||||
| 			iNdEx = postIndex |  | ||||||
| 		case 5: |  | ||||||
| 			if wireType != 2 { |  | ||||||
| 				return fmt.Errorf("proto: wrong wireType = %d for field StructuredResourceHandle", wireType) |  | ||||||
| 			} |  | ||||||
| 			var msglen int |  | ||||||
| 			for shift := uint(0); ; shift += 7 { |  | ||||||
| 				if shift >= 64 { |  | ||||||
| 					return ErrIntOverflowApi |  | ||||||
| 				} |  | ||||||
| 				if iNdEx >= l { |  | ||||||
| 					return io.ErrUnexpectedEOF |  | ||||||
| 				} |  | ||||||
| 				b := dAtA[iNdEx] |  | ||||||
| 				iNdEx++ |  | ||||||
| 				msglen |= int(b&0x7F) << shift |  | ||||||
| 				if b < 0x80 { |  | ||||||
| 					break |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 			if msglen < 0 { |  | ||||||
| 				return ErrInvalidLengthApi |  | ||||||
| 			} |  | ||||||
| 			postIndex := iNdEx + msglen |  | ||||||
| 			if postIndex < 0 { |  | ||||||
| 				return ErrInvalidLengthApi |  | ||||||
| 			} |  | ||||||
| 			if postIndex > l { |  | ||||||
| 				return io.ErrUnexpectedEOF |  | ||||||
| 			} |  | ||||||
| 			m.StructuredResourceHandle = append(m.StructuredResourceHandle, &v1alpha2.StructuredResourceHandle{}) |  | ||||||
| 			if err := m.StructuredResourceHandle[len(m.StructuredResourceHandle)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
| 			iNdEx = postIndex |  | ||||||
| 		default: | 		default: | ||||||
| 			iNdEx = preIndex | 			iNdEx = preIndex | ||||||
| 			skippy, err := skipApi(dAtA[iNdEx:]) | 			skippy, err := skipApi(dAtA[iNdEx:]) | ||||||
| @@ -19,10 +19,9 @@ limitations under the License. | |||||||
| syntax = "proto3"; | syntax = "proto3"; | ||||||
| 
 | 
 | ||||||
| package v1alpha3; | package v1alpha3; | ||||||
| option go_package = "k8s.io/kubelet/pkg/apis/dra/v1alpha3"; | option go_package = "k8s.io/kubelet/pkg/apis/dra/v1alpha4"; | ||||||
| 
 | 
 | ||||||
| import "github.com/gogo/protobuf/gogoproto/gogo.proto"; | import "github.com/gogo/protobuf/gogoproto/gogo.proto"; | ||||||
| import "k8s.io/api/resource/v1alpha2/generated.proto"; |  | ||||||
| 
 | 
 | ||||||
| option (gogoproto.goproto_stringer_all) = false; | option (gogoproto.goproto_stringer_all) = false; | ||||||
| option (gogoproto.stringer_all) =  true; | option (gogoproto.stringer_all) =  true; | ||||||
| @@ -44,12 +43,6 @@ service Node { | |||||||
|   // The same error handling rules apply, |   // The same error handling rules apply, | ||||||
|   rpc NodeUnprepareResources (NodeUnprepareResourcesRequest) |   rpc NodeUnprepareResources (NodeUnprepareResourcesRequest) | ||||||
|     returns (NodeUnprepareResourcesResponse) {} |     returns (NodeUnprepareResourcesResponse) {} | ||||||
| 
 |  | ||||||
|   // NodeListAndWatchResources returns a stream of NodeResourcesResponse objects. |  | ||||||
|   // At the start and whenever resource availability changes, the |  | ||||||
|   // plugin must send one such object with all information to the Kubelet. |  | ||||||
|   rpc NodeListAndWatchResources(NodeListAndWatchResourcesRequest) |  | ||||||
|     returns (stream NodeListAndWatchResourcesResponse) {} |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| message NodePrepareResourcesRequest { | message NodePrepareResourcesRequest { | ||||||
| @@ -94,13 +87,6 @@ message NodeUnprepareResourceResponse { | |||||||
|     string error = 1; |     string error = 1; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| message NodeListAndWatchResourcesRequest { |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| message NodeListAndWatchResourcesResponse { |  | ||||||
|     repeated k8s.io.api.resource.v1alpha2.ResourceModel resources = 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| message Claim { | message Claim { | ||||||
|     // The ResourceClaim namespace (ResourceClaim.meta.Namespace). |     // The ResourceClaim namespace (ResourceClaim.meta.Namespace). | ||||||
|     // This field is REQUIRED. |     // This field is REQUIRED. | ||||||
| @@ -111,13 +97,4 @@ message Claim { | |||||||
|     // The name of the Resource claim (ResourceClaim.meta.Name) |     // The name of the Resource claim (ResourceClaim.meta.Name) | ||||||
|     // This field is REQUIRED. |     // This field is REQUIRED. | ||||||
|     string name = 3; |     string name = 3; | ||||||
|     // Resource handle (AllocationResult.ResourceHandles[*].Data) |  | ||||||
|     // This field is REQUIRED. |  | ||||||
|     string resource_handle = 4; |  | ||||||
|     // Structured parameter resource handle (AllocationResult.ResourceHandles[*].StructuredData). |  | ||||||
|     // This field is OPTIONAL. If present, it needs to be used |  | ||||||
|     // instead of resource_handle. It will only have a single entry. |  | ||||||
|     // |  | ||||||
|     // Using "repeated" instead of "optional" is a workaround for https://github.com/gogo/protobuf/issues/713. |  | ||||||
|     repeated k8s.io.api.resource.v1alpha2.StructuredResourceHandle structured_resource_handle = 5; |  | ||||||
| } | } | ||||||
| @@ -19,6 +19,7 @@ package dra | |||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"context" | 	"context" | ||||||
|  | 	_ "embed" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net" | 	"net" | ||||||
| @@ -38,10 +39,19 @@ import ( | |||||||
| 	v1 "k8s.io/api/core/v1" | 	v1 "k8s.io/api/core/v1" | ||||||
| 	resourcev1alpha2 "k8s.io/api/resource/v1alpha2" | 	resourcev1alpha2 "k8s.io/api/resource/v1alpha2" | ||||||
| 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" | 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" | ||||||
|  | 	apierrors "k8s.io/apimachinery/pkg/api/errors" | ||||||
|  | 	"k8s.io/apimachinery/pkg/api/meta" | ||||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||||||
| 	"k8s.io/apimachinery/pkg/labels" | 	"k8s.io/apimachinery/pkg/labels" | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime/schema" | ||||||
| 	"k8s.io/apimachinery/pkg/selection" | 	"k8s.io/apimachinery/pkg/selection" | ||||||
|  | 	"k8s.io/apiserver/pkg/authentication/serviceaccount" | ||||||
|  | 	"k8s.io/client-go/discovery/cached/memory" | ||||||
| 	resourceapiinformer "k8s.io/client-go/informers/resource/v1alpha2" | 	resourceapiinformer "k8s.io/client-go/informers/resource/v1alpha2" | ||||||
|  | 	"k8s.io/client-go/kubernetes" | ||||||
|  | 	"k8s.io/client-go/rest" | ||||||
|  | 	"k8s.io/client-go/restmapper" | ||||||
| 	"k8s.io/client-go/tools/cache" | 	"k8s.io/client-go/tools/cache" | ||||||
| 	"k8s.io/dynamic-resource-allocation/kubeletplugin" | 	"k8s.io/dynamic-resource-allocation/kubeletplugin" | ||||||
| 	"k8s.io/klog/v2" | 	"k8s.io/klog/v2" | ||||||
| @@ -52,22 +62,26 @@ import ( | |||||||
| 	e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper" | 	e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper" | ||||||
| 	"k8s.io/kubernetes/test/e2e/storage/drivers/proxy" | 	"k8s.io/kubernetes/test/e2e/storage/drivers/proxy" | ||||||
| 	"k8s.io/kubernetes/test/e2e/storage/utils" | 	"k8s.io/kubernetes/test/e2e/storage/utils" | ||||||
|  | 	"sigs.k8s.io/yaml" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| 	NodePrepareResourcesMethod      = "/v1alpha3.Node/NodePrepareResources" | 	NodePrepareResourcesMethod   = "/v1alpha3.Node/NodePrepareResources" | ||||||
| 	NodeUnprepareResourcesMethod    = "/v1alpha3.Node/NodeUnprepareResources" | 	NodeUnprepareResourcesMethod = "/v1alpha3.Node/NodeUnprepareResources" | ||||||
| 	NodeListAndWatchResourcesMethod = "/v1alpha3.Node/NodeListAndWatchResources" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Nodes struct { | type Nodes struct { | ||||||
| 	NodeNames []string | 	NodeNames []string | ||||||
| } | } | ||||||
|  |  | ||||||
|  | //go:embed test-driver/deploy/example/plugin-permissions.yaml | ||||||
|  | var pluginPermissions string | ||||||
|  |  | ||||||
| // NewNodes selects nodes to run the test on. | // NewNodes selects nodes to run the test on. | ||||||
| func NewNodes(f *framework.Framework, minNodes, maxNodes int) *Nodes { | func NewNodes(f *framework.Framework, minNodes, maxNodes int) *Nodes { | ||||||
| 	nodes := &Nodes{} | 	nodes := &Nodes{} | ||||||
| 	ginkgo.BeforeEach(func(ctx context.Context) { | 	ginkgo.BeforeEach(func(ctx context.Context) { | ||||||
|  |  | ||||||
| 		ginkgo.By("selecting nodes") | 		ginkgo.By("selecting nodes") | ||||||
| 		// The kubelet plugin is harder. We deploy the builtin manifest | 		// The kubelet plugin is harder. We deploy the builtin manifest | ||||||
| 		// after patching in the driver name and all nodes on which we | 		// after patching in the driver name and all nodes on which we | ||||||
| @@ -167,15 +181,19 @@ type MethodInstance struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| type Driver struct { | type Driver struct { | ||||||
| 	f       *framework.Framework | 	f                  *framework.Framework | ||||||
| 	ctx     context.Context | 	ctx                context.Context | ||||||
| 	cleanup []func() // executed first-in-first-out | 	cleanup            []func() // executed first-in-first-out | ||||||
| 	wg      sync.WaitGroup | 	wg                 sync.WaitGroup | ||||||
|  | 	serviceAccountName string | ||||||
|  |  | ||||||
| 	NameSuffix string | 	NameSuffix string | ||||||
| 	Controller *app.ExampleController | 	Controller *app.ExampleController | ||||||
| 	Name       string | 	Name       string | ||||||
| 	Nodes      map[string]*app.ExamplePlugin |  | ||||||
|  | 	// Nodes contains entries for each node selected for a test when the test runs. | ||||||
|  | 	// In addition, there is one entry for a fictional node. | ||||||
|  | 	Nodes map[string]KubeletPlugin | ||||||
|  |  | ||||||
| 	parameterMode         parameterMode | 	parameterMode         parameterMode | ||||||
| 	parameterAPIGroup     string | 	parameterAPIGroup     string | ||||||
| @@ -190,6 +208,11 @@ type Driver struct { | |||||||
| 	callCounts map[MethodInstance]int64 | 	callCounts map[MethodInstance]int64 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type KubeletPlugin struct { | ||||||
|  | 	*app.ExamplePlugin | ||||||
|  | 	ClientSet kubernetes.Interface | ||||||
|  | } | ||||||
|  |  | ||||||
| type parameterMode string | type parameterMode string | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| @@ -200,7 +223,7 @@ const ( | |||||||
|  |  | ||||||
| func (d *Driver) SetUp(nodes *Nodes, resources app.Resources) { | func (d *Driver) SetUp(nodes *Nodes, resources app.Resources) { | ||||||
| 	ginkgo.By(fmt.Sprintf("deploying driver on nodes %v", nodes.NodeNames)) | 	ginkgo.By(fmt.Sprintf("deploying driver on nodes %v", nodes.NodeNames)) | ||||||
| 	d.Nodes = map[string]*app.ExamplePlugin{} | 	d.Nodes = make(map[string]KubeletPlugin) | ||||||
| 	d.Name = d.f.UniqueName + d.NameSuffix + ".k8s.io" | 	d.Name = d.f.UniqueName + d.NameSuffix + ".k8s.io" | ||||||
| 	resources.DriverName = d.Name | 	resources.DriverName = d.Name | ||||||
|  |  | ||||||
| @@ -251,6 +274,13 @@ func (d *Driver) SetUp(nodes *Nodes, resources app.Resources) { | |||||||
| 		framework.Failf("unknown test driver parameter mode: %s", d.parameterMode) | 		framework.Failf("unknown test driver parameter mode: %s", d.parameterMode) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// Create service account and corresponding RBAC rules. | ||||||
|  | 	d.serviceAccountName = "dra-kubelet-plugin-" + d.Name + "-service-account" | ||||||
|  | 	content := pluginPermissions | ||||||
|  | 	content = strings.ReplaceAll(content, "dra-kubelet-plugin-namespace", d.f.Namespace.Name) | ||||||
|  | 	content = strings.ReplaceAll(content, "dra-kubelet-plugin", "dra-kubelet-plugin-"+d.Name) | ||||||
|  | 	d.createFromYAML(ctx, []byte(content), d.f.Namespace.Name) | ||||||
|  |  | ||||||
| 	instanceKey := "app.kubernetes.io/instance" | 	instanceKey := "app.kubernetes.io/instance" | ||||||
| 	rsName := "" | 	rsName := "" | ||||||
| 	draAddr := path.Join(framework.TestContext.KubeletRootDir, "plugins", d.Name+".sock") | 	draAddr := path.Join(framework.TestContext.KubeletRootDir, "plugins", d.Name+".sock") | ||||||
| @@ -263,6 +293,7 @@ func (d *Driver) SetUp(nodes *Nodes, resources app.Resources) { | |||||||
| 			item.Spec.Replicas = &numNodes | 			item.Spec.Replicas = &numNodes | ||||||
| 			item.Spec.Selector.MatchLabels[instanceKey] = d.Name | 			item.Spec.Selector.MatchLabels[instanceKey] = d.Name | ||||||
| 			item.Spec.Template.Labels[instanceKey] = d.Name | 			item.Spec.Template.Labels[instanceKey] = d.Name | ||||||
|  | 			item.Spec.Template.Spec.ServiceAccountName = d.serviceAccountName | ||||||
| 			item.Spec.Template.Spec.Affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution[0].LabelSelector.MatchLabels[instanceKey] = d.Name | 			item.Spec.Template.Spec.Affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution[0].LabelSelector.MatchLabels[instanceKey] = d.Name | ||||||
| 			item.Spec.Template.Spec.Affinity.NodeAffinity = &v1.NodeAffinity{ | 			item.Spec.Template.Spec.Affinity.NodeAffinity = &v1.NodeAffinity{ | ||||||
| 				RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ | 				RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ | ||||||
| @@ -306,15 +337,31 @@ func (d *Driver) SetUp(nodes *Nodes, resources app.Resources) { | |||||||
| 	framework.ExpectNoError(err, "list proxy pods") | 	framework.ExpectNoError(err, "list proxy pods") | ||||||
| 	gomega.Expect(numNodes).To(gomega.Equal(int32(len(pods.Items))), "number of proxy pods") | 	gomega.Expect(numNodes).To(gomega.Equal(int32(len(pods.Items))), "number of proxy pods") | ||||||
|  |  | ||||||
| 	// Run registar and plugin for each of the pods. | 	// Run registrar and plugin for each of the pods. | ||||||
| 	for _, pod := range pods.Items { | 	for _, pod := range pods.Items { | ||||||
| 		// Need a local variable, not the loop variable, for the anonymous | 		// Need a local variable, not the loop variable, for the anonymous | ||||||
| 		// callback functions below. | 		// callback functions below. | ||||||
| 		pod := pod | 		pod := pod | ||||||
| 		nodename := pod.Spec.NodeName | 		nodename := pod.Spec.NodeName | ||||||
|  |  | ||||||
|  | 		// Authenticate the plugin so that it has the exact same | ||||||
|  | 		// permissions as the daemonset pod. This includes RBAC and a | ||||||
|  | 		// validating admission policy which limits writes to per-node | ||||||
|  | 		// ResourceSlices. | ||||||
|  | 		// | ||||||
|  | 		// We could retrieve | ||||||
|  | 		// /var/run/secrets/kubernetes.io/serviceaccount/token from | ||||||
|  | 		// each pod and use it. That would check that | ||||||
|  | 		// ServiceAccountTokenNodeBindingValidation works. But that's | ||||||
|  | 		// better covered by a test owned by SIG Auth (like the one in | ||||||
|  | 		// https://github.com/kubernetes/kubernetes/pull/124711). | ||||||
|  | 		// | ||||||
|  | 		// Here we merely use impersonation, which is faster. | ||||||
|  | 		driverClient := d.impersonateKubeletPlugin(&pod) | ||||||
|  |  | ||||||
| 		logger := klog.LoggerWithValues(klog.LoggerWithName(klog.Background(), "kubelet plugin"), "node", pod.Spec.NodeName, "pod", klog.KObj(&pod)) | 		logger := klog.LoggerWithValues(klog.LoggerWithName(klog.Background(), "kubelet plugin"), "node", pod.Spec.NodeName, "pod", klog.KObj(&pod)) | ||||||
| 		loggerCtx := klog.NewContext(ctx, logger) | 		loggerCtx := klog.NewContext(ctx, logger) | ||||||
| 		plugin, err := app.StartPlugin(loggerCtx, "/cdi", d.Name, nodename, | 		plugin, err := app.StartPlugin(loggerCtx, "/cdi", d.Name, driverClient, nodename, | ||||||
| 			app.FileOperations{ | 			app.FileOperations{ | ||||||
| 				Create: func(name string, content []byte) error { | 				Create: func(name string, content []byte) error { | ||||||
| 					klog.Background().Info("creating CDI file", "node", nodename, "filename", name, "content", string(content)) | 					klog.Background().Info("creating CDI file", "node", nodename, "filename", name, "content", string(content)) | ||||||
| @@ -343,7 +390,7 @@ func (d *Driver) SetUp(nodes *Nodes, resources app.Resources) { | |||||||
| 			// Depends on cancel being called first. | 			// Depends on cancel being called first. | ||||||
| 			plugin.Stop() | 			plugin.Stop() | ||||||
| 		}) | 		}) | ||||||
| 		d.Nodes[nodename] = plugin | 		d.Nodes[nodename] = KubeletPlugin{ExamplePlugin: plugin, ClientSet: driverClient} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Wait for registration. | 	// Wait for registration. | ||||||
| @@ -360,6 +407,26 @@ func (d *Driver) SetUp(nodes *Nodes, resources app.Resources) { | |||||||
| 	}).WithTimeout(time.Minute).Should(gomega.BeEmpty(), "hosts where the plugin has not been registered yet") | 	}).WithTimeout(time.Minute).Should(gomega.BeEmpty(), "hosts where the plugin has not been registered yet") | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (d *Driver) impersonateKubeletPlugin(pod *v1.Pod) kubernetes.Interface { | ||||||
|  | 	ginkgo.GinkgoHelper() | ||||||
|  | 	driverUserInfo := (&serviceaccount.ServiceAccountInfo{ | ||||||
|  | 		Name:      d.serviceAccountName, | ||||||
|  | 		Namespace: pod.Namespace, | ||||||
|  | 		NodeName:  pod.Spec.NodeName, | ||||||
|  | 		PodName:   pod.Name, | ||||||
|  | 		PodUID:    string(pod.UID), | ||||||
|  | 	}).UserInfo() | ||||||
|  | 	driverClientConfig := d.f.ClientConfig() | ||||||
|  | 	driverClientConfig.Impersonate = rest.ImpersonationConfig{ | ||||||
|  | 		UserName: driverUserInfo.GetName(), | ||||||
|  | 		Groups:   driverUserInfo.GetGroups(), | ||||||
|  | 		Extra:    driverUserInfo.GetExtra(), | ||||||
|  | 	} | ||||||
|  | 	driverClient, err := kubernetes.NewForConfig(driverClientConfig) | ||||||
|  | 	framework.ExpectNoError(err, "create client for driver") | ||||||
|  | 	return driverClient | ||||||
|  | } | ||||||
|  |  | ||||||
| func (d *Driver) createFile(pod *v1.Pod, name string, content []byte) error { | func (d *Driver) createFile(pod *v1.Pod, name string, content []byte) error { | ||||||
| 	buffer := bytes.NewBuffer(content) | 	buffer := bytes.NewBuffer(content) | ||||||
| 	// Writing the content can be slow. Better create a temporary file and | 	// Writing the content can be slow. Better create a temporary file and | ||||||
| @@ -376,6 +443,57 @@ func (d *Driver) removeFile(pod *v1.Pod, name string) error { | |||||||
| 	return d.podIO(pod).RemoveAll(name) | 	return d.podIO(pod).RemoveAll(name) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (d *Driver) createFromYAML(ctx context.Context, content []byte, namespace string) { | ||||||
|  | 	// Not caching the discovery result isn't very efficient, but good enough. | ||||||
|  | 	discoveryCache := memory.NewMemCacheClient(d.f.ClientSet.Discovery()) | ||||||
|  | 	restMapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryCache) | ||||||
|  |  | ||||||
|  | 	for _, content := range bytes.Split(content, []byte("---\n")) { | ||||||
|  | 		if len(content) == 0 { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		var obj *unstructured.Unstructured | ||||||
|  | 		framework.ExpectNoError(yaml.UnmarshalStrict(content, &obj), fmt.Sprintf("Full YAML:\n%s\n", string(content))) | ||||||
|  |  | ||||||
|  | 		gv, err := schema.ParseGroupVersion(obj.GetAPIVersion()) | ||||||
|  | 		framework.ExpectNoError(err, fmt.Sprintf("extract group+version from object %q", klog.KObj(obj))) | ||||||
|  | 		gk := schema.GroupKind{Group: gv.Group, Kind: obj.GetKind()} | ||||||
|  |  | ||||||
|  | 		mapping, err := restMapper.RESTMapping(gk, gv.Version) | ||||||
|  | 		framework.ExpectNoError(err, fmt.Sprintf("map %q to resource", gk)) | ||||||
|  |  | ||||||
|  | 		resourceClient := d.f.DynamicClient.Resource(mapping.Resource) | ||||||
|  | 		options := metav1.CreateOptions{ | ||||||
|  | 			// If the YAML input is invalid, then we want the | ||||||
|  | 			// apiserver to tell us via an error. This can | ||||||
|  | 			// happen because decoding into an unstructured object | ||||||
|  | 			// doesn't validate. | ||||||
|  | 			FieldValidation: "Strict", | ||||||
|  | 		} | ||||||
|  | 		switch mapping.Scope.Name() { | ||||||
|  | 		case meta.RESTScopeNameRoot: | ||||||
|  | 			_, err = resourceClient.Create(ctx, obj, options) | ||||||
|  | 		case meta.RESTScopeNameNamespace: | ||||||
|  | 			if namespace == "" { | ||||||
|  | 				framework.Failf("need namespace for object type %s", gk) | ||||||
|  | 			} | ||||||
|  | 			_, err = resourceClient.Namespace(namespace).Create(ctx, obj, options) | ||||||
|  | 		} | ||||||
|  | 		framework.ExpectNoError(err, "create object") | ||||||
|  | 		ginkgo.DeferCleanup(func(ctx context.Context) { | ||||||
|  | 			del := resourceClient.Delete | ||||||
|  | 			if mapping.Scope.Name() == meta.RESTScopeNameNamespace { | ||||||
|  | 				del = resourceClient.Namespace(namespace).Delete | ||||||
|  | 			} | ||||||
|  | 			err := del(ctx, obj.GetName(), metav1.DeleteOptions{}) | ||||||
|  | 			if !apierrors.IsNotFound(err) { | ||||||
|  | 				framework.ExpectNoError(err, fmt.Sprintf("deleting %s.%s %s", obj.GetKind(), obj.GetAPIVersion(), klog.KObj(obj))) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| func (d *Driver) podIO(pod *v1.Pod) proxy.PodDirIO { | func (d *Driver) podIO(pod *v1.Pod) proxy.PodDirIO { | ||||||
| 	logger := klog.Background() | 	logger := klog.Background() | ||||||
| 	return proxy.PodDirIO{ | 	return proxy.PodDirIO{ | ||||||
|   | |||||||
| @@ -29,6 +29,7 @@ import ( | |||||||
| 	"github.com/onsi/gomega" | 	"github.com/onsi/gomega" | ||||||
| 	"github.com/onsi/gomega/gcustom" | 	"github.com/onsi/gomega/gcustom" | ||||||
| 	"github.com/onsi/gomega/gstruct" | 	"github.com/onsi/gomega/gstruct" | ||||||
|  | 	"github.com/onsi/gomega/types" | ||||||
|  |  | ||||||
| 	v1 "k8s.io/api/core/v1" | 	v1 "k8s.io/api/core/v1" | ||||||
| 	resourcev1alpha2 "k8s.io/api/resource/v1alpha2" | 	resourcev1alpha2 "k8s.io/api/resource/v1alpha2" | ||||||
| @@ -891,17 +892,98 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation, | |||||||
| 			driver := NewDriver(f, nodes, perNode(1, nodes)) | 			driver := NewDriver(f, nodes, perNode(1, nodes)) | ||||||
| 			driver.parameterMode = parameterModeStructured | 			driver.parameterMode = parameterModeStructured | ||||||
|  |  | ||||||
| 			f.It("must manage ResourceSlices", f.WithSlow(), func(ctx context.Context) { | 			f.It("must apply per-node permission checks", func(ctx context.Context) { | ||||||
| 				nodeName := nodes.NodeNames[0] | 				// All of the operations use the client set of a kubelet plugin for | ||||||
| 				driverName := driver.Name | 				// a fictional node which both don't exist, so nothing interferes | ||||||
|  | 				// when we actually manage to create a slice. | ||||||
|  | 				fictionalNodeName := "dra-fictional-node" | ||||||
|  | 				gomega.Expect(nodes.NodeNames).NotTo(gomega.ContainElement(fictionalNodeName)) | ||||||
|  | 				fictionalNodeClient := driver.impersonateKubeletPlugin(&v1.Pod{ | ||||||
|  | 					ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 						Name:      fictionalNodeName + "-dra-plugin", | ||||||
|  | 						Namespace: f.Namespace.Name, | ||||||
|  | 						UID:       "12345", | ||||||
|  | 					}, | ||||||
|  | 					Spec: v1.PodSpec{ | ||||||
|  | 						NodeName: fictionalNodeName, | ||||||
|  | 					}, | ||||||
|  | 				}) | ||||||
|  |  | ||||||
| 				// Check for gRPC call on one node. If that already fails, then | 				// This is for some actual node in the cluster. | ||||||
| 				// we have a fundamental problem. | 				realNodeName := nodes.NodeNames[0] | ||||||
| 				m := MethodInstance{nodeName, NodeListAndWatchResourcesMethod} | 				realNodeClient := driver.Nodes[realNodeName].ClientSet | ||||||
| 				ginkgo.By("wait for NodeListAndWatchResources call") |  | ||||||
| 				gomega.Eventually(ctx, func() int64 { | 				// This is the slice that we try to create. It needs to be deleted | ||||||
| 					return driver.CallCount(m) | 				// after testing, if it still exists at that time. | ||||||
| 				}).WithTimeout(podStartTimeout).Should(gomega.BeNumerically(">", int64(0)), "NodeListAndWatchResources call count") | 				fictionalNodeSlice := &resourcev1alpha2.ResourceSlice{ | ||||||
|  | 					ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 						Name: fictionalNodeName + "-slice", | ||||||
|  | 					}, | ||||||
|  | 					NodeName:   fictionalNodeName, | ||||||
|  | 					DriverName: "dra.example.com", | ||||||
|  | 					ResourceModel: resourcev1alpha2.ResourceModel{ | ||||||
|  | 						NamedResources: &resourcev1alpha2.NamedResourcesResources{}, | ||||||
|  | 					}, | ||||||
|  | 				} | ||||||
|  | 				ginkgo.DeferCleanup(func(ctx context.Context) { | ||||||
|  | 					err := f.ClientSet.ResourceV1alpha2().ResourceSlices().Delete(ctx, fictionalNodeSlice.Name, metav1.DeleteOptions{}) | ||||||
|  | 					if !apierrors.IsNotFound(err) { | ||||||
|  | 						framework.ExpectNoError(err) | ||||||
|  | 					} | ||||||
|  | 				}) | ||||||
|  |  | ||||||
|  | 				// Message from test-driver/deploy/example/plugin-permissions.yaml | ||||||
|  | 				matchVAPDeniedError := gomega.MatchError(gomega.ContainSubstring("may only modify resourceslices that belong to the node the pod is running on")) | ||||||
|  |  | ||||||
|  | 				mustCreate := func(clientSet kubernetes.Interface, clientName string, slice *resourcev1alpha2.ResourceSlice) *resourcev1alpha2.ResourceSlice { | ||||||
|  | 					ginkgo.GinkgoHelper() | ||||||
|  | 					slice, err := clientSet.ResourceV1alpha2().ResourceSlices().Create(ctx, slice, metav1.CreateOptions{}) | ||||||
|  | 					framework.ExpectNoError(err, fmt.Sprintf("CREATE: %s + %s", clientName, slice.Name)) | ||||||
|  | 					return slice | ||||||
|  | 				} | ||||||
|  | 				mustUpdate := func(clientSet kubernetes.Interface, clientName string, slice *resourcev1alpha2.ResourceSlice) *resourcev1alpha2.ResourceSlice { | ||||||
|  | 					ginkgo.GinkgoHelper() | ||||||
|  | 					slice, err := clientSet.ResourceV1alpha2().ResourceSlices().Update(ctx, slice, metav1.UpdateOptions{}) | ||||||
|  | 					framework.ExpectNoError(err, fmt.Sprintf("UPDATE: %s + %s", clientName, slice.Name)) | ||||||
|  | 					return slice | ||||||
|  | 				} | ||||||
|  | 				mustDelete := func(clientSet kubernetes.Interface, clientName string, slice *resourcev1alpha2.ResourceSlice) { | ||||||
|  | 					ginkgo.GinkgoHelper() | ||||||
|  | 					err := clientSet.ResourceV1alpha2().ResourceSlices().Delete(ctx, slice.Name, metav1.DeleteOptions{}) | ||||||
|  | 					framework.ExpectNoError(err, fmt.Sprintf("DELETE: %s + %s", clientName, slice.Name)) | ||||||
|  | 				} | ||||||
|  | 				mustFailToCreate := func(clientSet kubernetes.Interface, clientName string, slice *resourcev1alpha2.ResourceSlice, matchError types.GomegaMatcher) { | ||||||
|  | 					ginkgo.GinkgoHelper() | ||||||
|  | 					_, err := clientSet.ResourceV1alpha2().ResourceSlices().Create(ctx, slice, metav1.CreateOptions{}) | ||||||
|  | 					gomega.Expect(err).To(matchError, fmt.Sprintf("CREATE: %s + %s", clientName, slice.Name)) | ||||||
|  | 				} | ||||||
|  | 				mustFailToUpdate := func(clientSet kubernetes.Interface, clientName string, slice *resourcev1alpha2.ResourceSlice, matchError types.GomegaMatcher) { | ||||||
|  | 					ginkgo.GinkgoHelper() | ||||||
|  | 					_, err := clientSet.ResourceV1alpha2().ResourceSlices().Update(ctx, slice, metav1.UpdateOptions{}) | ||||||
|  | 					gomega.Expect(err).To(matchError, fmt.Sprintf("UPDATE: %s + %s", clientName, slice.Name)) | ||||||
|  | 				} | ||||||
|  | 				mustFailToDelete := func(clientSet kubernetes.Interface, clientName string, slice *resourcev1alpha2.ResourceSlice, matchError types.GomegaMatcher) { | ||||||
|  | 					ginkgo.GinkgoHelper() | ||||||
|  | 					err := clientSet.ResourceV1alpha2().ResourceSlices().Delete(ctx, slice.Name, metav1.DeleteOptions{}) | ||||||
|  | 					gomega.Expect(err).To(matchError, fmt.Sprintf("DELETE: %s + %s", clientName, slice.Name)) | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				// Create with different clients, keep it in the end. | ||||||
|  | 				mustFailToCreate(realNodeClient, "real plugin", fictionalNodeSlice, matchVAPDeniedError) | ||||||
|  | 				createdFictionalNodeSlice := mustCreate(fictionalNodeClient, "fictional plugin", fictionalNodeSlice) | ||||||
|  |  | ||||||
|  | 				// Update with different clients. | ||||||
|  | 				mustFailToUpdate(realNodeClient, "real plugin", createdFictionalNodeSlice, matchVAPDeniedError) | ||||||
|  | 				createdFictionalNodeSlice = mustUpdate(fictionalNodeClient, "fictional plugin", createdFictionalNodeSlice) | ||||||
|  | 				createdFictionalNodeSlice = mustUpdate(f.ClientSet, "admin", createdFictionalNodeSlice) | ||||||
|  |  | ||||||
|  | 				// Delete with different clients. | ||||||
|  | 				mustFailToDelete(realNodeClient, "real plugin", createdFictionalNodeSlice, matchVAPDeniedError) | ||||||
|  | 				mustDelete(fictionalNodeClient, "fictional plugin", createdFictionalNodeSlice) | ||||||
|  | 			}) | ||||||
|  |  | ||||||
|  | 			f.It("must manage ResourceSlices", f.WithSlow(), func(ctx context.Context) { | ||||||
|  | 				driverName := driver.Name | ||||||
|  |  | ||||||
| 				// Now check for exactly the right set of objects for all nodes. | 				// Now check for exactly the right set of objects for all nodes. | ||||||
| 				ginkgo.By("check if ResourceSlice object(s) exist on the API server") | 				ginkgo.By("check if ResourceSlice object(s) exist on the API server") | ||||||
| @@ -1109,6 +1191,7 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation, | |||||||
| 			b := newBuilder(f, driver) | 			b := newBuilder(f, driver) | ||||||
| 			preScheduledTests(b, driver, resourcev1alpha2.AllocationModeImmediate) | 			preScheduledTests(b, driver, resourcev1alpha2.AllocationModeImmediate) | ||||||
| 			claimTests(b, driver, resourcev1alpha2.AllocationModeImmediate) | 			claimTests(b, driver, resourcev1alpha2.AllocationModeImmediate) | ||||||
|  |  | ||||||
| 		}) | 		}) | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -66,7 +66,7 @@ go run ./test/e2e/dra/test-driver --feature-gates ContextualLogging=true -v=5 co | |||||||
| In yet another: | In yet another: | ||||||
| ```console | ```console | ||||||
| sudo mkdir -p /var/run/cdi && sudo chmod a+rwx /var/run/cdi /var/lib/kubelet/plugins_registry | sudo mkdir -p /var/run/cdi && sudo chmod a+rwx /var/run/cdi /var/lib/kubelet/plugins_registry | ||||||
| go run ./test/e2e/dra/test-driver --feature-gates ContextualLogging=true -v=5 kubelet-plugin | go run ./test/e2e/dra/test-driver --feature-gates ContextualLogging=true -v=5 kubelet-plugin --node-name=127.0.0.1 | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| And finally: | And finally: | ||||||
|   | |||||||
| @@ -26,24 +26,25 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
|  |  | ||||||
| 	"github.com/google/go-cmp/cmp" |  | ||||||
| 	"google.golang.org/grpc" | 	"google.golang.org/grpc" | ||||||
| 	"google.golang.org/grpc/codes" |  | ||||||
| 	"google.golang.org/grpc/status" |  | ||||||
|  |  | ||||||
| 	resourceapi "k8s.io/api/resource/v1alpha2" | 	resourceapi "k8s.io/api/resource/v1alpha2" | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/runtime" | 	"k8s.io/apimachinery/pkg/runtime" | ||||||
|  | 	"k8s.io/apimachinery/pkg/types" | ||||||
| 	"k8s.io/apimachinery/pkg/util/sets" | 	"k8s.io/apimachinery/pkg/util/sets" | ||||||
|  | 	"k8s.io/client-go/kubernetes" | ||||||
| 	"k8s.io/dynamic-resource-allocation/kubeletplugin" | 	"k8s.io/dynamic-resource-allocation/kubeletplugin" | ||||||
| 	"k8s.io/klog/v2" | 	"k8s.io/klog/v2" | ||||||
| 	drapbv1alpha3 "k8s.io/kubelet/pkg/apis/dra/v1alpha3" | 	drapb "k8s.io/kubelet/pkg/apis/dra/v1alpha4" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type ExamplePlugin struct { | type ExamplePlugin struct { | ||||||
| 	stopCh  <-chan struct{} | 	stopCh     <-chan struct{} | ||||||
| 	logger  klog.Logger | 	logger     klog.Logger | ||||||
| 	d       kubeletplugin.DRAPlugin | 	kubeClient kubernetes.Interface | ||||||
| 	fileOps FileOperations | 	d          kubeletplugin.DRAPlugin | ||||||
|  | 	fileOps    FileOperations | ||||||
|  |  | ||||||
| 	cdiDir     string | 	cdiDir     string | ||||||
| 	driverName string | 	driverName string | ||||||
| @@ -52,7 +53,7 @@ type ExamplePlugin struct { | |||||||
|  |  | ||||||
| 	mutex          sync.Mutex | 	mutex          sync.Mutex | ||||||
| 	instancesInUse sets.Set[string] | 	instancesInUse sets.Set[string] | ||||||
| 	prepared       map[ClaimID]any | 	prepared       map[ClaimID][]string // instance names | ||||||
| 	gRPCCalls      []GRPCCall | 	gRPCCalls      []GRPCCall | ||||||
|  |  | ||||||
| 	blockPrepareResourcesMutex   sync.Mutex | 	blockPrepareResourcesMutex   sync.Mutex | ||||||
| @@ -87,7 +88,7 @@ type ClaimID struct { | |||||||
| 	UID  string | 	UID  string | ||||||
| } | } | ||||||
|  |  | ||||||
| var _ drapbv1alpha3.NodeServer = &ExamplePlugin{} | var _ drapb.NodeServer = &ExamplePlugin{} | ||||||
|  |  | ||||||
| // getJSONFilePath returns the absolute path where CDI file is/should be. | // getJSONFilePath returns the absolute path where CDI file is/should be. | ||||||
| func (ex *ExamplePlugin) getJSONFilePath(claimUID string) string { | func (ex *ExamplePlugin) getJSONFilePath(claimUID string) string { | ||||||
| @@ -111,8 +112,9 @@ type FileOperations struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| // StartPlugin sets up the servers that are necessary for a DRA kubelet plugin. | // StartPlugin sets up the servers that are necessary for a DRA kubelet plugin. | ||||||
| func StartPlugin(ctx context.Context, cdiDir, driverName string, nodeName string, fileOps FileOperations, opts ...kubeletplugin.Option) (*ExamplePlugin, error) { | func StartPlugin(ctx context.Context, cdiDir, driverName string, kubeClient kubernetes.Interface, nodeName string, fileOps FileOperations, opts ...kubeletplugin.Option) (*ExamplePlugin, error) { | ||||||
| 	logger := klog.FromContext(ctx) | 	logger := klog.FromContext(ctx) | ||||||
|  |  | ||||||
| 	if fileOps.Create == nil { | 	if fileOps.Create == nil { | ||||||
| 		fileOps.Create = func(name string, content []byte) error { | 		fileOps.Create = func(name string, content []byte) error { | ||||||
| 			return os.WriteFile(name, content, os.FileMode(0644)) | 			return os.WriteFile(name, content, os.FileMode(0644)) | ||||||
| @@ -129,13 +131,14 @@ func StartPlugin(ctx context.Context, cdiDir, driverName string, nodeName string | |||||||
| 	ex := &ExamplePlugin{ | 	ex := &ExamplePlugin{ | ||||||
| 		stopCh:         ctx.Done(), | 		stopCh:         ctx.Done(), | ||||||
| 		logger:         logger, | 		logger:         logger, | ||||||
|  | 		kubeClient:     kubeClient, | ||||||
| 		fileOps:        fileOps, | 		fileOps:        fileOps, | ||||||
| 		cdiDir:         cdiDir, | 		cdiDir:         cdiDir, | ||||||
| 		driverName:     driverName, | 		driverName:     driverName, | ||||||
| 		nodeName:       nodeName, | 		nodeName:       nodeName, | ||||||
| 		instances:      sets.New[string](), | 		instances:      sets.New[string](), | ||||||
| 		instancesInUse: sets.New[string](), | 		instancesInUse: sets.New[string](), | ||||||
| 		prepared:       make(map[ClaimID]any), | 		prepared:       make(map[ClaimID][]string), | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for i := 0; i < ex.fileOps.NumResourceInstances; i++ { | 	for i := 0; i < ex.fileOps.NumResourceInstances; i++ { | ||||||
| @@ -143,17 +146,33 @@ func StartPlugin(ctx context.Context, cdiDir, driverName string, nodeName string | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	opts = append(opts, | 	opts = append(opts, | ||||||
| 		kubeletplugin.Logger(logger), |  | ||||||
| 		kubeletplugin.DriverName(driverName), | 		kubeletplugin.DriverName(driverName), | ||||||
|  | 		kubeletplugin.NodeName(nodeName), | ||||||
|  | 		kubeletplugin.KubeClient(kubeClient), | ||||||
| 		kubeletplugin.GRPCInterceptor(ex.recordGRPCCall), | 		kubeletplugin.GRPCInterceptor(ex.recordGRPCCall), | ||||||
| 		kubeletplugin.GRPCStreamInterceptor(ex.recordGRPCStream), | 		kubeletplugin.GRPCStreamInterceptor(ex.recordGRPCStream), | ||||||
| 	) | 	) | ||||||
| 	d, err := kubeletplugin.Start(ex, opts...) | 	d, err := kubeletplugin.Start(ctx, ex, opts...) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("start kubelet plugin: %w", err) | 		return nil, fmt.Errorf("start kubelet plugin: %w", err) | ||||||
| 	} | 	} | ||||||
| 	ex.d = d | 	ex.d = d | ||||||
|  |  | ||||||
|  | 	if fileOps.NumResourceInstances >= 0 { | ||||||
|  | 		instances := make([]resourceapi.NamedResourcesInstance, ex.fileOps.NumResourceInstances) | ||||||
|  | 		for i := 0; i < ex.fileOps.NumResourceInstances; i++ { | ||||||
|  | 			instances[i].Name = fmt.Sprintf("instance-%02d", i) | ||||||
|  | 		} | ||||||
|  | 		nodeResources := []*resourceapi.ResourceModel{ | ||||||
|  | 			{ | ||||||
|  | 				NamedResources: &resourceapi.NamedResourcesResources{ | ||||||
|  | 					Instances: instances, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 		ex.d.PublishResources(ctx, nodeResources) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return ex, nil | 	return ex, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -230,19 +249,47 @@ func (ex *ExamplePlugin) getUnprepareResourcesFailure() error { | |||||||
| // a deterministic name to simplify NodeUnprepareResource (no need to remember | // a deterministic name to simplify NodeUnprepareResource (no need to remember | ||||||
| // or discover the name) and idempotency (when called again, the file simply | // or discover the name) and idempotency (when called again, the file simply | ||||||
| // gets written again). | // gets written again). | ||||||
| func (ex *ExamplePlugin) nodePrepareResource(ctx context.Context, claimName string, claimUID string, resourceHandle string, structuredResourceHandle []*resourceapi.StructuredResourceHandle) ([]string, error) { | func (ex *ExamplePlugin) nodePrepareResource(ctx context.Context, claimReq *drapb.Claim) ([]string, error) { | ||||||
| 	logger := klog.FromContext(ctx) | 	logger := klog.FromContext(ctx) | ||||||
|  |  | ||||||
|  | 	// The plugin must retrieve the claim itself to get it in the version | ||||||
|  | 	// that it understands. | ||||||
|  | 	var resourceHandle string | ||||||
|  | 	var structuredResourceHandle *resourceapi.StructuredResourceHandle | ||||||
|  | 	claim, err := ex.kubeClient.ResourceV1alpha2().ResourceClaims(claimReq.Namespace).Get(ctx, claimReq.Name, metav1.GetOptions{}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("retrieve claim %s/%s: %w", claimReq.Namespace, claimReq.Name, err) | ||||||
|  | 	} | ||||||
|  | 	if claim.Status.Allocation == nil { | ||||||
|  | 		return nil, fmt.Errorf("claim %s/%s not allocated", claimReq.Namespace, claimReq.Name) | ||||||
|  | 	} | ||||||
|  | 	if claim.UID != types.UID(claimReq.Uid) { | ||||||
|  | 		return nil, fmt.Errorf("claim %s/%s got replaced", claimReq.Namespace, claimReq.Name) | ||||||
|  | 	} | ||||||
|  | 	haveResources := false | ||||||
|  | 	for _, handle := range claim.Status.Allocation.ResourceHandles { | ||||||
|  | 		if handle.DriverName == ex.driverName { | ||||||
|  | 			haveResources = true | ||||||
|  | 			resourceHandle = handle.Data | ||||||
|  | 			structuredResourceHandle = handle.StructuredData | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if !haveResources { | ||||||
|  | 		// Nothing to do. | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	ex.mutex.Lock() | 	ex.mutex.Lock() | ||||||
| 	defer ex.mutex.Unlock() | 	defer ex.mutex.Unlock() | ||||||
| 	ex.blockPrepareResourcesMutex.Lock() | 	ex.blockPrepareResourcesMutex.Lock() | ||||||
| 	defer ex.blockPrepareResourcesMutex.Unlock() | 	defer ex.blockPrepareResourcesMutex.Unlock() | ||||||
|  |  | ||||||
| 	deviceName := "claim-" + claimUID | 	deviceName := "claim-" + claimReq.Uid | ||||||
| 	vendor := ex.driverName | 	vendor := ex.driverName | ||||||
| 	class := "test" | 	class := "test" | ||||||
| 	dev := vendor + "/" + class + "=" + deviceName | 	dev := vendor + "/" + class + "=" + deviceName | ||||||
| 	claimID := ClaimID{Name: claimName, UID: claimUID} | 	claimID := ClaimID{Name: claimReq.Name, UID: claimReq.Uid} | ||||||
| 	if _, ok := ex.prepared[claimID]; ok { | 	if _, ok := ex.prepared[claimID]; ok { | ||||||
| 		// Idempotent call, nothing to do. | 		// Idempotent call, nothing to do. | ||||||
| 		return []string{dev}, nil | 		return []string{dev}, nil | ||||||
| @@ -250,29 +297,22 @@ func (ex *ExamplePlugin) nodePrepareResource(ctx context.Context, claimName stri | |||||||
|  |  | ||||||
| 	// Determine environment variables. | 	// Determine environment variables. | ||||||
| 	var p parameters | 	var p parameters | ||||||
| 	var actualResourceHandle any |  | ||||||
| 	var instanceNames []string | 	var instanceNames []string | ||||||
| 	switch len(structuredResourceHandle) { | 	if structuredResourceHandle == nil { | ||||||
| 	case 0: |  | ||||||
| 		// Control plane controller did the allocation. | 		// Control plane controller did the allocation. | ||||||
| 		if err := json.Unmarshal([]byte(resourceHandle), &p); err != nil { | 		if err := json.Unmarshal([]byte(resourceHandle), &p); err != nil { | ||||||
| 			return nil, fmt.Errorf("unmarshal resource handle: %w", err) | 			return nil, fmt.Errorf("unmarshal resource handle: %w", err) | ||||||
| 		} | 		} | ||||||
| 		actualResourceHandle = resourceHandle | 	} else { | ||||||
| 	case 1: |  | ||||||
| 		// Scheduler did the allocation with structured parameters. | 		// Scheduler did the allocation with structured parameters. | ||||||
| 		handle := structuredResourceHandle[0] | 		p.NodeName = structuredResourceHandle.NodeName | ||||||
| 		if handle == nil { | 		if err := extractParameters(structuredResourceHandle.VendorClassParameters, &p.EnvVars, "admin"); err != nil { | ||||||
| 			return nil, errors.New("unexpected nil StructuredResourceHandle") |  | ||||||
| 		} |  | ||||||
| 		p.NodeName = handle.NodeName |  | ||||||
| 		if err := extractParameters(handle.VendorClassParameters, &p.EnvVars, "admin"); err != nil { |  | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 		if err := extractParameters(handle.VendorClaimParameters, &p.EnvVars, "user"); err != nil { | 		if err := extractParameters(structuredResourceHandle.VendorClaimParameters, &p.EnvVars, "user"); err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 		for _, result := range handle.Results { | 		for _, result := range structuredResourceHandle.Results { | ||||||
| 			if err := extractParameters(result.VendorRequestParameters, &p.EnvVars, "user"); err != nil { | 			if err := extractParameters(result.VendorRequestParameters, &p.EnvVars, "user"); err != nil { | ||||||
| 				return nil, err | 				return nil, err | ||||||
| 			} | 			} | ||||||
| @@ -292,10 +332,6 @@ func (ex *ExamplePlugin) nodePrepareResource(ctx context.Context, claimName stri | |||||||
| 			} | 			} | ||||||
| 			instanceNames = append(instanceNames, instanceName) | 			instanceNames = append(instanceNames, instanceName) | ||||||
| 		} | 		} | ||||||
| 		actualResourceHandle = handle |  | ||||||
| 	default: |  | ||||||
| 		// Huh? |  | ||||||
| 		return nil, fmt.Errorf("invalid length of NodePrepareResourceRequest.StructuredResourceHandle: %d", len(structuredResourceHandle)) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Sanity check scheduling. | 	// Sanity check scheduling. | ||||||
| @@ -323,7 +359,7 @@ func (ex *ExamplePlugin) nodePrepareResource(ctx context.Context, claimName stri | |||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 	filePath := ex.getJSONFilePath(claimUID) | 	filePath := ex.getJSONFilePath(claimReq.Uid) | ||||||
| 	buffer, err := json.Marshal(spec) | 	buffer, err := json.Marshal(spec) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("marshal spec: %w", err) | 		return nil, fmt.Errorf("marshal spec: %w", err) | ||||||
| @@ -332,7 +368,7 @@ func (ex *ExamplePlugin) nodePrepareResource(ctx context.Context, claimName stri | |||||||
| 		return nil, fmt.Errorf("failed to write CDI file %v", err) | 		return nil, fmt.Errorf("failed to write CDI file %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	ex.prepared[claimID] = actualResourceHandle | 	ex.prepared[claimID] = instanceNames | ||||||
| 	for _, instanceName := range instanceNames { | 	for _, instanceName := range instanceNames { | ||||||
| 		ex.instancesInUse.Insert(instanceName) | 		ex.instancesInUse.Insert(instanceName) | ||||||
| 	} | 	} | ||||||
| @@ -358,9 +394,9 @@ func extractParameters(parameters runtime.RawExtension, env *map[string]string, | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (ex *ExamplePlugin) NodePrepareResources(ctx context.Context, req *drapbv1alpha3.NodePrepareResourcesRequest) (*drapbv1alpha3.NodePrepareResourcesResponse, error) { | func (ex *ExamplePlugin) NodePrepareResources(ctx context.Context, req *drapb.NodePrepareResourcesRequest) (*drapb.NodePrepareResourcesResponse, error) { | ||||||
| 	resp := &drapbv1alpha3.NodePrepareResourcesResponse{ | 	resp := &drapb.NodePrepareResourcesResponse{ | ||||||
| 		Claims: make(map[string]*drapbv1alpha3.NodePrepareResourceResponse), | 		Claims: make(map[string]*drapb.NodePrepareResourceResponse), | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if failure := ex.getPrepareResourcesFailure(); failure != nil { | 	if failure := ex.getPrepareResourcesFailure(); failure != nil { | ||||||
| @@ -368,13 +404,13 @@ func (ex *ExamplePlugin) NodePrepareResources(ctx context.Context, req *drapbv1a | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, claimReq := range req.Claims { | 	for _, claimReq := range req.Claims { | ||||||
| 		cdiDevices, err := ex.nodePrepareResource(ctx, claimReq.Name, claimReq.Uid, claimReq.ResourceHandle, claimReq.StructuredResourceHandle) | 		cdiDevices, err := ex.nodePrepareResource(ctx, claimReq) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			resp.Claims[claimReq.Uid] = &drapbv1alpha3.NodePrepareResourceResponse{ | 			resp.Claims[claimReq.Uid] = &drapb.NodePrepareResourceResponse{ | ||||||
| 				Error: err.Error(), | 				Error: err.Error(), | ||||||
| 			} | 			} | ||||||
| 		} else { | 		} else { | ||||||
| 			resp.Claims[claimReq.Uid] = &drapbv1alpha3.NodePrepareResourceResponse{ | 			resp.Claims[claimReq.Uid] = &drapb.NodePrepareResourceResponse{ | ||||||
| 				CDIDevices: cdiDevices, | 				CDIDevices: cdiDevices, | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @@ -385,13 +421,13 @@ func (ex *ExamplePlugin) NodePrepareResources(ctx context.Context, req *drapbv1a | |||||||
| // NodeUnprepareResource removes the CDI file created by | // NodeUnprepareResource removes the CDI file created by | ||||||
| // NodePrepareResource. It's idempotent, therefore it is not an error when that | // NodePrepareResource. It's idempotent, therefore it is not an error when that | ||||||
| // file is already gone. | // file is already gone. | ||||||
| func (ex *ExamplePlugin) nodeUnprepareResource(ctx context.Context, claimName string, claimUID string, resourceHandle string, structuredResourceHandle []*resourceapi.StructuredResourceHandle) error { | func (ex *ExamplePlugin) nodeUnprepareResource(ctx context.Context, claimReq *drapb.Claim) error { | ||||||
| 	ex.blockUnprepareResourcesMutex.Lock() | 	ex.blockUnprepareResourcesMutex.Lock() | ||||||
| 	defer ex.blockUnprepareResourcesMutex.Unlock() | 	defer ex.blockUnprepareResourcesMutex.Unlock() | ||||||
|  |  | ||||||
| 	logger := klog.FromContext(ctx) | 	logger := klog.FromContext(ctx) | ||||||
|  |  | ||||||
| 	filePath := ex.getJSONFilePath(claimUID) | 	filePath := ex.getJSONFilePath(claimReq.Uid) | ||||||
| 	if err := ex.fileOps.Remove(filePath); err != nil { | 	if err := ex.fileOps.Remove(filePath); err != nil { | ||||||
| 		return fmt.Errorf("error removing CDI file: %w", err) | 		return fmt.Errorf("error removing CDI file: %w", err) | ||||||
| 	} | 	} | ||||||
| @@ -400,40 +436,24 @@ func (ex *ExamplePlugin) nodeUnprepareResource(ctx context.Context, claimName st | |||||||
| 	ex.mutex.Lock() | 	ex.mutex.Lock() | ||||||
| 	defer ex.mutex.Unlock() | 	defer ex.mutex.Unlock() | ||||||
|  |  | ||||||
| 	claimID := ClaimID{Name: claimName, UID: claimUID} | 	claimID := ClaimID{Name: claimReq.Name, UID: claimReq.Uid} | ||||||
| 	expectedResourceHandle, ok := ex.prepared[claimID] | 	instanceNames, ok := ex.prepared[claimID] | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		// Idempotent call, nothing to do. | 		// Idempotent call, nothing to do. | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var actualResourceHandle any = resourceHandle |  | ||||||
| 	if structuredResourceHandle != nil { |  | ||||||
| 		if len(structuredResourceHandle) != 1 { |  | ||||||
| 			return fmt.Errorf("unexpected number of entries in StructuredResourceHandle: %d", len(structuredResourceHandle)) |  | ||||||
| 		} |  | ||||||
| 		actualResourceHandle = structuredResourceHandle[0] |  | ||||||
| 	} |  | ||||||
| 	if diff := cmp.Diff(expectedResourceHandle, actualResourceHandle); diff != "" { |  | ||||||
| 		return fmt.Errorf("difference between expected (-) and actual resource handle (+):\n%s", diff) |  | ||||||
| 	} |  | ||||||
| 	delete(ex.prepared, claimID) | 	delete(ex.prepared, claimID) | ||||||
| 	if structuredResourceHandle := structuredResourceHandle; structuredResourceHandle != nil { | 	for _, instanceName := range instanceNames { | ||||||
| 		for _, handle := range structuredResourceHandle { | 		ex.instancesInUse.Delete(instanceName) | ||||||
| 			for _, result := range handle.Results { |  | ||||||
| 				instanceName := result.NamedResources.Name |  | ||||||
| 				ex.instancesInUse.Delete(instanceName) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| 	delete(ex.prepared, ClaimID{Name: claimName, UID: claimUID}) |  | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (ex *ExamplePlugin) NodeUnprepareResources(ctx context.Context, req *drapbv1alpha3.NodeUnprepareResourcesRequest) (*drapbv1alpha3.NodeUnprepareResourcesResponse, error) { | func (ex *ExamplePlugin) NodeUnprepareResources(ctx context.Context, req *drapb.NodeUnprepareResourcesRequest) (*drapb.NodeUnprepareResourcesResponse, error) { | ||||||
| 	resp := &drapbv1alpha3.NodeUnprepareResourcesResponse{ | 	resp := &drapb.NodeUnprepareResourcesResponse{ | ||||||
| 		Claims: make(map[string]*drapbv1alpha3.NodeUnprepareResourceResponse), | 		Claims: make(map[string]*drapb.NodeUnprepareResourceResponse), | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if failure := ex.getUnprepareResourcesFailure(); failure != nil { | 	if failure := ex.getUnprepareResourcesFailure(); failure != nil { | ||||||
| @@ -441,51 +461,18 @@ func (ex *ExamplePlugin) NodeUnprepareResources(ctx context.Context, req *drapbv | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, claimReq := range req.Claims { | 	for _, claimReq := range req.Claims { | ||||||
| 		err := ex.nodeUnprepareResource(ctx, claimReq.Name, claimReq.Uid, claimReq.ResourceHandle, claimReq.StructuredResourceHandle) | 		err := ex.nodeUnprepareResource(ctx, claimReq) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			resp.Claims[claimReq.Uid] = &drapbv1alpha3.NodeUnprepareResourceResponse{ | 			resp.Claims[claimReq.Uid] = &drapb.NodeUnprepareResourceResponse{ | ||||||
| 				Error: err.Error(), | 				Error: err.Error(), | ||||||
| 			} | 			} | ||||||
| 		} else { | 		} else { | ||||||
| 			resp.Claims[claimReq.Uid] = &drapbv1alpha3.NodeUnprepareResourceResponse{} | 			resp.Claims[claimReq.Uid] = &drapb.NodeUnprepareResourceResponse{} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return resp, nil | 	return resp, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (ex *ExamplePlugin) NodeListAndWatchResources(req *drapbv1alpha3.NodeListAndWatchResourcesRequest, stream drapbv1alpha3.Node_NodeListAndWatchResourcesServer) error { |  | ||||||
| 	if ex.fileOps.NumResourceInstances < 0 { |  | ||||||
| 		ex.logger.Info("Sending no NodeResourcesResponse") |  | ||||||
| 		return status.New(codes.Unimplemented, "node resource support disabled").Err() |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	instances := make([]resourceapi.NamedResourcesInstance, len(ex.instances)) |  | ||||||
| 	for i, name := range sets.List(ex.instances) { |  | ||||||
| 		instances[i].Name = name |  | ||||||
| 	} |  | ||||||
| 	resp := &drapbv1alpha3.NodeListAndWatchResourcesResponse{ |  | ||||||
| 		Resources: []*resourceapi.ResourceModel{ |  | ||||||
| 			{ |  | ||||||
| 				NamedResources: &resourceapi.NamedResourcesResources{ |  | ||||||
| 					Instances: instances, |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	ex.logger.Info("Sending NodeListAndWatchResourcesResponse", "response", resp) |  | ||||||
| 	if err := stream.Send(resp); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Keep the stream open until the test is done. |  | ||||||
| 	// TODO: test sending more updates later |  | ||||||
| 	<-ex.stopCh |  | ||||||
| 	ex.logger.Info("Done sending NodeListAndWatchResourcesResponse, closing stream") |  | ||||||
|  |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (ex *ExamplePlugin) GetPreparedResources() []ClaimID { | func (ex *ExamplePlugin) GetPreparedResources() []ClaimID { | ||||||
| 	ex.mutex.Lock() | 	ex.mutex.Lock() | ||||||
| 	defer ex.mutex.Unlock() | 	defer ex.mutex.Unlock() | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ package app | |||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net" | 	"net" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| @@ -272,6 +273,7 @@ func NewCommand() *cobra.Command { | |||||||
| 	draAddress := fs.String("dra-address", "/var/lib/kubelet/plugins/test-driver/dra.sock", "The Unix domain socket that kubelet will connect to for dynamic resource allocation requests, in the filesystem of kubelet.") | 	draAddress := fs.String("dra-address", "/var/lib/kubelet/plugins/test-driver/dra.sock", "The Unix domain socket that kubelet will connect to for dynamic resource allocation requests, in the filesystem of kubelet.") | ||||||
| 	fs = kubeletPluginFlagSets.FlagSet("CDI") | 	fs = kubeletPluginFlagSets.FlagSet("CDI") | ||||||
| 	cdiDir := fs.String("cdi-dir", "/var/run/cdi", "directory for dynamically created CDI JSON files") | 	cdiDir := fs.String("cdi-dir", "/var/run/cdi", "directory for dynamically created CDI JSON files") | ||||||
|  | 	nodeName := fs.String("node-name", "", "name of the node that the kubelet plugin is responsible for") | ||||||
| 	fs = kubeletPlugin.Flags() | 	fs = kubeletPlugin.Flags() | ||||||
| 	for _, f := range kubeletPluginFlagSets.FlagSets { | 	for _, f := range kubeletPluginFlagSets.FlagSets { | ||||||
| 		fs.AddFlagSet(f) | 		fs.AddFlagSet(f) | ||||||
| @@ -287,7 +289,11 @@ func NewCommand() *cobra.Command { | |||||||
| 			return fmt.Errorf("create socket directory: %w", err) | 			return fmt.Errorf("create socket directory: %w", err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		plugin, err := StartPlugin(cmd.Context(), *cdiDir, *driverName, "", FileOperations{}, | 		if *nodeName == "" { | ||||||
|  | 			return errors.New("--node-name not set") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		plugin, err := StartPlugin(cmd.Context(), *cdiDir, *driverName, clientset, *nodeName, FileOperations{}, | ||||||
| 			kubeletplugin.PluginSocketPath(*endpoint), | 			kubeletplugin.PluginSocketPath(*endpoint), | ||||||
| 			kubeletplugin.RegistrarSocketPath(path.Join(*pluginRegistrationPath, *driverName+"-reg.sock")), | 			kubeletplugin.RegistrarSocketPath(path.Join(*pluginRegistrationPath, *driverName+"-reg.sock")), | ||||||
| 			kubeletplugin.KubeletPluginSocketPath(*draAddress), | 			kubeletplugin.KubeletPluginSocketPath(*draAddress), | ||||||
|   | |||||||
| @@ -0,0 +1,84 @@ | |||||||
|  | # Real driver deployments must replace all occurrences of "dra-kubelet-plugin" | ||||||
|  | # with something specific to their driver. | ||||||
|  |  | ||||||
|  | apiVersion: v1 | ||||||
|  | kind: ServiceAccount | ||||||
|  | metadata: | ||||||
|  |   name: dra-kubelet-plugin-service-account | ||||||
|  |   namespace: dra-kubelet-plugin-namespace | ||||||
|  | --- | ||||||
|  | apiVersion: rbac.authorization.k8s.io/v1 | ||||||
|  | kind: ClusterRole | ||||||
|  | metadata: | ||||||
|  |   name: dra-kubelet-plugin-role | ||||||
|  | rules: | ||||||
|  | - apiGroups: ["resource.k8s.io"] | ||||||
|  |   resources: ["resourceclaims"] | ||||||
|  |   verbs: ["get"] | ||||||
|  | - apiGroups: [""] | ||||||
|  |   resources: ["nodes"] | ||||||
|  |   verbs: ["get"] | ||||||
|  | - apiGroups: ["resource.k8s.io"] | ||||||
|  |   resources: ["resourceslices"] | ||||||
|  |   verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] | ||||||
|  | --- | ||||||
|  | apiVersion: rbac.authorization.k8s.io/v1 | ||||||
|  | kind: ClusterRoleBinding | ||||||
|  | metadata: | ||||||
|  |   name: dra-kubelet-plugin-role-binding | ||||||
|  | subjects: | ||||||
|  | - kind: ServiceAccount | ||||||
|  |   name: dra-kubelet-plugin-service-account | ||||||
|  |   namespace: dra-kubelet-plugin-namespace | ||||||
|  | roleRef: | ||||||
|  |   kind: ClusterRole | ||||||
|  |   name: dra-kubelet-plugin-role | ||||||
|  |   apiGroup: rbac.authorization.k8s.io | ||||||
|  | --- | ||||||
|  | # This ValidatingAdmissionPolicy is specific to the DRA driver's kubelet plugin | ||||||
|  | # because it checks the ServiceAccount defined for it above. An admin could | ||||||
|  | # also define a single policy for all drivers. | ||||||
|  | apiVersion: admissionregistration.k8s.io/v1 | ||||||
|  | kind: ValidatingAdmissionPolicy | ||||||
|  | metadata: | ||||||
|  |   name: resourceslices-policy-dra-kubelet-plugin | ||||||
|  | spec: | ||||||
|  |   failurePolicy: Fail | ||||||
|  |   matchConstraints: | ||||||
|  |     resourceRules: | ||||||
|  |     - apiGroups:   ["resource.k8s.io"] | ||||||
|  |       apiVersions: ["v1alpha2"] | ||||||
|  |       operations:  ["CREATE", "UPDATE", "DELETE"] | ||||||
|  |       resources:   ["resourceslices"] | ||||||
|  |   variables: | ||||||
|  |   - name: hasNodeName | ||||||
|  |     expression: >- | ||||||
|  |       "authentication.kubernetes.io/node-name" in request.userInfo.extra | ||||||
|  |   - name: isKubeletPlugin | ||||||
|  |     expression: >- | ||||||
|  |       request.userInfo.username == "system:serviceaccount:dra-kubelet-plugin-namespace:dra-kubelet-plugin-service-account" | ||||||
|  |   - name: objectNodeName | ||||||
|  |     expression: >- | ||||||
|  |       (request.operation == "DELETE" ? oldObject : object).?nodeName.orValue("") | ||||||
|  |   validations: | ||||||
|  |   - expression: >- | ||||||
|  |       !variables.isKubeletPlugin || variables.hasNodeName | ||||||
|  |     message: This user must have a "authentication.kubernetes.io/node-name" claim. ServiceAccountTokenNodeBindingValidation must be enabled in the cluster. | ||||||
|  |   - expression: >- | ||||||
|  |       !variables.isKubeletPlugin || !variables.hasNodeName || | ||||||
|  |       variables.objectNodeName == request.userInfo.extra["authentication.kubernetes.io/node-name"][0] | ||||||
|  |     message: This DRA kubelet plugin may only modify resourceslices that belong to the node the pod is running on. | ||||||
|  |     # This is useful for debugging. Can be dropped in a production deployment. | ||||||
|  |     messageExpression: >- | ||||||
|  |       "The DRA kubelet plugin on node " + request.userInfo.extra["authentication.kubernetes.io/node-name"][0] + | ||||||
|  |       " may only modify resourceslices that belong to the node the pod is running on, not " + | ||||||
|  |       (variables.objectNodeName == "" ? variables.objectNodeName : "a cluster-scoped slice") + "." | ||||||
|  | --- | ||||||
|  | apiVersion: admissionregistration.k8s.io/v1 | ||||||
|  | kind: ValidatingAdmissionPolicyBinding | ||||||
|  | metadata: | ||||||
|  |   name: resourceslices-policy-dra-kubelet-plugin | ||||||
|  | spec: | ||||||
|  |   policyName: resourceslices-policy-dra-kubelet-plugin | ||||||
|  |   validationActions: [Deny] | ||||||
|  |   # All ResourceSlices are matched. | ||||||
| @@ -34,9 +34,12 @@ import ( | |||||||
|  |  | ||||||
| 	"github.com/onsi/ginkgo/v2" | 	"github.com/onsi/ginkgo/v2" | ||||||
| 	"github.com/onsi/gomega" | 	"github.com/onsi/gomega" | ||||||
|  | 	"github.com/onsi/gomega/gstruct" | ||||||
|  | 	"github.com/onsi/gomega/types" | ||||||
|  |  | ||||||
| 	v1 "k8s.io/api/core/v1" | 	v1 "k8s.io/api/core/v1" | ||||||
| 	resourcev1alpha2 "k8s.io/api/resource/v1alpha2" | 	resourcev1alpha2 "k8s.io/api/resource/v1alpha2" | ||||||
|  | 	apierrors "k8s.io/apimachinery/pkg/api/errors" | ||||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| 	"k8s.io/client-go/kubernetes" | 	"k8s.io/client-go/kubernetes" | ||||||
| 	"k8s.io/klog/v2" | 	"k8s.io/klog/v2" | ||||||
| @@ -66,14 +69,19 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation, | |||||||
| 	f := framework.NewDefaultFramework("dra-node") | 	f := framework.NewDefaultFramework("dra-node") | ||||||
| 	f.NamespacePodSecurityLevel = admissionapi.LevelBaseline | 	f.NamespacePodSecurityLevel = admissionapi.LevelBaseline | ||||||
|  |  | ||||||
| 	var kubeletPlugin, kubeletPlugin1, kubeletPlugin2 *testdriver.ExamplePlugin | 	ginkgo.BeforeEach(func() { | ||||||
|  | 		ginkgo.DeferCleanup(func(ctx context.Context) { | ||||||
|  | 			// When plugin and kubelet get killed at the end of the tests, they leave ResourceSlices behind. | ||||||
|  | 			// Perhaps garbage collection would eventually remove them (not sure how the node instance | ||||||
|  | 			// is managed), but this could take time. Let's clean up explicitly. | ||||||
|  | 			framework.ExpectNoError(f.ClientSet.ResourceV1alpha2().ResourceSlices().DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{})) | ||||||
|  | 		}) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
| 	f.Context("Resource Kubelet Plugin", f.WithSerial(), func() { | 	f.Context("Resource Kubelet Plugin", f.WithSerial(), func() { | ||||||
| 		ginkgo.BeforeEach(func(ctx context.Context) { |  | ||||||
| 			kubeletPlugin = newKubeletPlugin(ctx, getNodeName(ctx, f), driverName) |  | ||||||
| 		}) |  | ||||||
|  |  | ||||||
| 		ginkgo.It("must register after Kubelet restart", func(ctx context.Context) { | 		ginkgo.It("must register after Kubelet restart", func(ctx context.Context) { | ||||||
|  | 			kubeletPlugin := newKubeletPlugin(ctx, f.ClientSet, getNodeName(ctx, f), driverName) | ||||||
|  |  | ||||||
| 			oldCalls := kubeletPlugin.GetGRPCCalls() | 			oldCalls := kubeletPlugin.GetGRPCCalls() | ||||||
| 			getNewCalls := func() []testdriver.GRPCCall { | 			getNewCalls := func() []testdriver.GRPCCall { | ||||||
| 				calls := kubeletPlugin.GetGRPCCalls() | 				calls := kubeletPlugin.GetGRPCCalls() | ||||||
| @@ -88,16 +96,21 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation, | |||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
| 		ginkgo.It("must register after plugin restart", func(ctx context.Context) { | 		ginkgo.It("must register after plugin restart", func(ctx context.Context) { | ||||||
|  | 			kubeletPlugin := newKubeletPlugin(ctx, f.ClientSet, getNodeName(ctx, f), driverName) | ||||||
|  |  | ||||||
| 			ginkgo.By("restart Kubelet Plugin") | 			ginkgo.By("restart Kubelet Plugin") | ||||||
| 			kubeletPlugin.Stop() | 			kubeletPlugin.Stop() | ||||||
| 			kubeletPlugin = newKubeletPlugin(ctx, getNodeName(ctx, f), driverName) | 			kubeletPlugin = newKubeletPlugin(ctx, f.ClientSet, getNodeName(ctx, f), driverName) | ||||||
|  |  | ||||||
| 			ginkgo.By("wait for Kubelet plugin re-registration") | 			ginkgo.By("wait for Kubelet plugin re-registration") | ||||||
| 			gomega.Eventually(kubeletPlugin.GetGRPCCalls).WithTimeout(pluginRegistrationTimeout).Should(testdriver.BeRegistered) | 			gomega.Eventually(kubeletPlugin.GetGRPCCalls).WithTimeout(pluginRegistrationTimeout).Should(testdriver.BeRegistered) | ||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
| 		ginkgo.It("must process pod created when kubelet is not running", func(ctx context.Context) { | 		ginkgo.It("must process pod created when kubelet is not running", func(ctx context.Context) { | ||||||
|  | 			newKubeletPlugin(ctx, f.ClientSet, getNodeName(ctx, f), driverName) | ||||||
|  |  | ||||||
| 			// Stop Kubelet | 			// Stop Kubelet | ||||||
|  | 			ginkgo.By("stop kubelet") | ||||||
| 			startKubelet := stopKubelet() | 			startKubelet := stopKubelet() | ||||||
| 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", true, []string{driverName}) | 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", true, []string{driverName}) | ||||||
| 			// Pod must be in pending state | 			// Pod must be in pending state | ||||||
| @@ -106,6 +119,7 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation, | |||||||
| 			}) | 			}) | ||||||
| 			framework.ExpectNoError(err) | 			framework.ExpectNoError(err) | ||||||
| 			// Start Kubelet | 			// Start Kubelet | ||||||
|  | 			ginkgo.By("restart kubelet") | ||||||
| 			startKubelet() | 			startKubelet() | ||||||
| 			// Pod should succeed | 			// Pod should succeed | ||||||
| 			err = e2epod.WaitForPodSuccessInNamespaceTimeout(ctx, f.ClientSet, pod.Name, f.Namespace.Name, framework.PodStartShortTimeout) | 			err = e2epod.WaitForPodSuccessInNamespaceTimeout(ctx, f.ClientSet, pod.Name, f.Namespace.Name, framework.PodStartShortTimeout) | ||||||
| @@ -113,6 +127,8 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation, | |||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
| 		ginkgo.It("must keep pod in pending state if NodePrepareResources times out", func(ctx context.Context) { | 		ginkgo.It("must keep pod in pending state if NodePrepareResources times out", func(ctx context.Context) { | ||||||
|  | 			kubeletPlugin := newKubeletPlugin(ctx, f.ClientSet, getNodeName(ctx, f), driverName) | ||||||
|  |  | ||||||
| 			unblock := kubeletPlugin.BlockNodePrepareResources() | 			unblock := kubeletPlugin.BlockNodePrepareResources() | ||||||
| 			defer unblock() | 			defer unblock() | ||||||
| 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", true, []string{driverName}) | 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", true, []string{driverName}) | ||||||
| @@ -131,6 +147,8 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation, | |||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
| 		ginkgo.It("must run pod if NodePrepareResources fails and then succeeds", func(ctx context.Context) { | 		ginkgo.It("must run pod if NodePrepareResources fails and then succeeds", func(ctx context.Context) { | ||||||
|  | 			kubeletPlugin := newKubeletPlugin(ctx, f.ClientSet, getNodeName(ctx, f), driverName) | ||||||
|  |  | ||||||
| 			unset := kubeletPlugin.SetNodePrepareResourcesFailureMode() | 			unset := kubeletPlugin.SetNodePrepareResourcesFailureMode() | ||||||
| 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", true, []string{driverName}) | 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", true, []string{driverName}) | ||||||
|  |  | ||||||
| @@ -154,6 +172,8 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation, | |||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
| 		ginkgo.It("must run pod if NodeUnprepareResources fails and then succeeds", func(ctx context.Context) { | 		ginkgo.It("must run pod if NodeUnprepareResources fails and then succeeds", func(ctx context.Context) { | ||||||
|  | 			kubeletPlugin := newKubeletPlugin(ctx, f.ClientSet, getNodeName(ctx, f), driverName) | ||||||
|  |  | ||||||
| 			unset := kubeletPlugin.SetNodeUnprepareResourcesFailureMode() | 			unset := kubeletPlugin.SetNodeUnprepareResourcesFailureMode() | ||||||
| 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", true, []string{driverName}) | 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", true, []string{driverName}) | ||||||
|  |  | ||||||
| @@ -174,6 +194,8 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation, | |||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
| 		ginkgo.It("must retry NodePrepareResources after Kubelet restart", func(ctx context.Context) { | 		ginkgo.It("must retry NodePrepareResources after Kubelet restart", func(ctx context.Context) { | ||||||
|  | 			kubeletPlugin := newKubeletPlugin(ctx, f.ClientSet, getNodeName(ctx, f), driverName) | ||||||
|  |  | ||||||
| 			unset := kubeletPlugin.SetNodePrepareResourcesFailureMode() | 			unset := kubeletPlugin.SetNodePrepareResourcesFailureMode() | ||||||
| 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", true, []string{driverName}) | 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", true, []string{driverName}) | ||||||
|  |  | ||||||
| @@ -203,6 +225,8 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation, | |||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
| 		ginkgo.It("must retry NodeUnprepareResources after Kubelet restart", func(ctx context.Context) { | 		ginkgo.It("must retry NodeUnprepareResources after Kubelet restart", func(ctx context.Context) { | ||||||
|  | 			kubeletPlugin := newKubeletPlugin(ctx, f.ClientSet, getNodeName(ctx, f), driverName) | ||||||
|  |  | ||||||
| 			unset := kubeletPlugin.SetNodeUnprepareResourcesFailureMode() | 			unset := kubeletPlugin.SetNodeUnprepareResourcesFailureMode() | ||||||
| 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", true, []string{driverName}) | 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", true, []string{driverName}) | ||||||
| 			ginkgo.By("wait for NodePrepareResources call to succeed") | 			ginkgo.By("wait for NodePrepareResources call to succeed") | ||||||
| @@ -228,6 +252,8 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation, | |||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
| 		ginkgo.It("must call NodeUnprepareResources for deleted pod", func(ctx context.Context) { | 		ginkgo.It("must call NodeUnprepareResources for deleted pod", func(ctx context.Context) { | ||||||
|  | 			kubeletPlugin := newKubeletPlugin(ctx, f.ClientSet, getNodeName(ctx, f), driverName) | ||||||
|  |  | ||||||
| 			unset := kubeletPlugin.SetNodeUnprepareResourcesFailureMode() | 			unset := kubeletPlugin.SetNodeUnprepareResourcesFailureMode() | ||||||
| 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", false, []string{driverName}) | 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", false, []string{driverName}) | ||||||
|  |  | ||||||
| @@ -250,6 +276,8 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation, | |||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
| 		ginkgo.It("must call NodeUnprepareResources for deleted pod after Kubelet restart", func(ctx context.Context) { | 		ginkgo.It("must call NodeUnprepareResources for deleted pod after Kubelet restart", func(ctx context.Context) { | ||||||
|  | 			kubeletPlugin := newKubeletPlugin(ctx, f.ClientSet, getNodeName(ctx, f), driverName) | ||||||
|  |  | ||||||
| 			unset := kubeletPlugin.SetNodeUnprepareResourcesFailureMode() | 			unset := kubeletPlugin.SetNodeUnprepareResourcesFailureMode() | ||||||
| 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", false, []string{driverName}) | 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", false, []string{driverName}) | ||||||
|  |  | ||||||
| @@ -279,6 +307,8 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation, | |||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
| 		ginkgo.It("must not call NodePrepareResources for deleted pod after Kubelet restart", func(ctx context.Context) { | 		ginkgo.It("must not call NodePrepareResources for deleted pod after Kubelet restart", func(ctx context.Context) { | ||||||
|  | 			kubeletPlugin := newKubeletPlugin(ctx, f.ClientSet, getNodeName(ctx, f), driverName) | ||||||
|  |  | ||||||
| 			unblock := kubeletPlugin.BlockNodePrepareResources() | 			unblock := kubeletPlugin.BlockNodePrepareResources() | ||||||
| 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", false, []string{driverName}) | 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", false, []string{driverName}) | ||||||
|  |  | ||||||
| @@ -306,16 +336,21 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation, | |||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	f.Context("Two resource Kubelet Plugins", f.WithSerial(), func() { | 	f.Context("Two resource Kubelet Plugins", f.WithSerial(), func() { | ||||||
| 		ginkgo.BeforeEach(func(ctx context.Context) { | 		// start creates plugins which will get stopped when the context gets canceled. | ||||||
| 			kubeletPlugin1 = newKubeletPlugin(ctx, getNodeName(ctx, f), kubeletPlugin1Name) | 		start := func(ctx context.Context) (*testdriver.ExamplePlugin, *testdriver.ExamplePlugin) { | ||||||
| 			kubeletPlugin2 = newKubeletPlugin(ctx, getNodeName(ctx, f), kubeletPlugin2Name) | 			kubeletPlugin1 := newKubeletPlugin(ctx, f.ClientSet, getNodeName(ctx, f), kubeletPlugin1Name) | ||||||
|  | 			kubeletPlugin2 := newKubeletPlugin(ctx, f.ClientSet, getNodeName(ctx, f), kubeletPlugin2Name) | ||||||
|  |  | ||||||
| 			ginkgo.By("wait for Kubelet plugin registration") | 			ginkgo.By("wait for Kubelet plugin registration") | ||||||
| 			gomega.Eventually(kubeletPlugin1.GetGRPCCalls()).WithTimeout(pluginRegistrationTimeout).Should(testdriver.BeRegistered) | 			gomega.Eventually(kubeletPlugin1.GetGRPCCalls()).WithTimeout(pluginRegistrationTimeout).Should(testdriver.BeRegistered) | ||||||
| 			gomega.Eventually(kubeletPlugin2.GetGRPCCalls()).WithTimeout(pluginRegistrationTimeout).Should(testdriver.BeRegistered) | 			gomega.Eventually(kubeletPlugin2.GetGRPCCalls()).WithTimeout(pluginRegistrationTimeout).Should(testdriver.BeRegistered) | ||||||
| 		}) |  | ||||||
|  | 			return kubeletPlugin1, kubeletPlugin2 | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		ginkgo.It("must prepare and unprepare resources", func(ctx context.Context) { | 		ginkgo.It("must prepare and unprepare resources", func(ctx context.Context) { | ||||||
|  | 			kubeletPlugin1, kubeletPlugin2 := start(ctx) | ||||||
|  |  | ||||||
| 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", true, []string{kubeletPlugin1Name, kubeletPlugin2Name}) | 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", true, []string{kubeletPlugin1Name, kubeletPlugin2Name}) | ||||||
|  |  | ||||||
| 			ginkgo.By("wait for pod to succeed") | 			ginkgo.By("wait for pod to succeed") | ||||||
| @@ -332,6 +367,8 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation, | |||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
| 		ginkgo.It("must run pod if NodePrepareResources fails for one plugin and then succeeds", func(ctx context.Context) { | 		ginkgo.It("must run pod if NodePrepareResources fails for one plugin and then succeeds", func(ctx context.Context) { | ||||||
|  | 			_, kubeletPlugin2 := start(ctx) | ||||||
|  |  | ||||||
| 			unset := kubeletPlugin2.SetNodePrepareResourcesFailureMode() | 			unset := kubeletPlugin2.SetNodePrepareResourcesFailureMode() | ||||||
| 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", true, []string{kubeletPlugin1Name, kubeletPlugin2Name}) | 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", true, []string{kubeletPlugin1Name, kubeletPlugin2Name}) | ||||||
|  |  | ||||||
| @@ -355,6 +392,8 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation, | |||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
| 		ginkgo.It("must run pod if NodeUnprepareResources fails for one plugin and then succeeds", func(ctx context.Context) { | 		ginkgo.It("must run pod if NodeUnprepareResources fails for one plugin and then succeeds", func(ctx context.Context) { | ||||||
|  | 			kubeletPlugin1, kubeletPlugin2 := start(ctx) | ||||||
|  |  | ||||||
| 			unset := kubeletPlugin2.SetNodeUnprepareResourcesFailureMode() | 			unset := kubeletPlugin2.SetNodeUnprepareResourcesFailureMode() | ||||||
| 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", true, []string{kubeletPlugin1Name, kubeletPlugin2Name}) | 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", true, []string{kubeletPlugin1Name, kubeletPlugin2Name}) | ||||||
|  |  | ||||||
| @@ -378,6 +417,9 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation, | |||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
| 		ginkgo.It("must run pod if NodePrepareResources is in progress for one plugin when Kubelet restarts", func(ctx context.Context) { | 		ginkgo.It("must run pod if NodePrepareResources is in progress for one plugin when Kubelet restarts", func(ctx context.Context) { | ||||||
|  | 			_, kubeletPlugin2 := start(ctx) | ||||||
|  | 			kubeletPlugin := newKubeletPlugin(ctx, f.ClientSet, getNodeName(ctx, f), driverName) | ||||||
|  |  | ||||||
| 			unblock := kubeletPlugin.BlockNodePrepareResources() | 			unblock := kubeletPlugin.BlockNodePrepareResources() | ||||||
| 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", true, []string{kubeletPlugin1Name, kubeletPlugin2Name}) | 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", true, []string{kubeletPlugin1Name, kubeletPlugin2Name}) | ||||||
|  |  | ||||||
| @@ -401,6 +443,8 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation, | |||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
| 		ginkgo.It("must call NodeUnprepareResources again if it's in progress for one plugin when Kubelet restarts", func(ctx context.Context) { | 		ginkgo.It("must call NodeUnprepareResources again if it's in progress for one plugin when Kubelet restarts", func(ctx context.Context) { | ||||||
|  | 			kubeletPlugin1, kubeletPlugin2 := start(ctx) | ||||||
|  |  | ||||||
| 			unblock := kubeletPlugin2.BlockNodeUnprepareResources() | 			unblock := kubeletPlugin2.BlockNodeUnprepareResources() | ||||||
| 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", true, []string{kubeletPlugin1Name, kubeletPlugin2Name}) | 			pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drapod", true, []string{kubeletPlugin1Name, kubeletPlugin2Name}) | ||||||
|  |  | ||||||
| @@ -423,10 +467,70 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation, | |||||||
| 			framework.ExpectNoError(err) | 			framework.ExpectNoError(err) | ||||||
| 		}) | 		}) | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
|  | 	f.Context("ResourceSlice", f.WithSerial(), func() { | ||||||
|  | 		listResources := func(ctx context.Context) ([]resourcev1alpha2.ResourceSlice, error) { | ||||||
|  | 			slices, err := f.ClientSet.ResourceV1alpha2().ResourceSlices().List(ctx, metav1.ListOptions{}) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 			return slices.Items, nil | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		matchResourcesByNodeName := func(nodeName string) types.GomegaMatcher { | ||||||
|  | 			return gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ | ||||||
|  | 				"NodeName": gomega.Equal(nodeName), | ||||||
|  | 			}) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		f.It("must be removed on kubelet startup", f.WithDisruptive(), func(ctx context.Context) { | ||||||
|  | 			ginkgo.By("stop kubelet") | ||||||
|  | 			startKubelet := stopKubelet() | ||||||
|  | 			ginkgo.DeferCleanup(func() { | ||||||
|  | 				if startKubelet != nil { | ||||||
|  | 					startKubelet() | ||||||
|  | 				} | ||||||
|  | 			}) | ||||||
|  |  | ||||||
|  | 			ginkgo.By("create some ResourceSlices") | ||||||
|  | 			nodeName := getNodeName(ctx, f) | ||||||
|  | 			otherNodeName := nodeName + "-other" | ||||||
|  | 			createTestResourceSlice(ctx, f.ClientSet, nodeName, driverName) | ||||||
|  | 			createTestResourceSlice(ctx, f.ClientSet, nodeName+"-other", driverName) | ||||||
|  |  | ||||||
|  | 			matchAll := gomega.ConsistOf(matchResourcesByNodeName(nodeName), matchResourcesByNodeName(otherNodeName)) | ||||||
|  | 			matchOtherNode := gomega.ConsistOf(matchResourcesByNodeName(otherNodeName)) | ||||||
|  |  | ||||||
|  | 			gomega.Consistently(ctx, listResources).WithTimeout(5*time.Second).Should(matchAll, "ResourceSlices without kubelet") | ||||||
|  |  | ||||||
|  | 			ginkgo.By("start kubelet") | ||||||
|  | 			startKubelet() | ||||||
|  | 			startKubelet = nil | ||||||
|  |  | ||||||
|  | 			ginkgo.By("wait for exactly the node's ResourceSlice to get deleted") | ||||||
|  | 			gomega.Eventually(ctx, listResources).Should(matchOtherNode, "ResourceSlices with kubelet") | ||||||
|  | 			gomega.Consistently(ctx, listResources).WithTimeout(5*time.Second).Should(matchOtherNode, "ResourceSlices with kubelet") | ||||||
|  | 		}) | ||||||
|  |  | ||||||
|  | 		f.It("must be removed after plugin unregistration", func(ctx context.Context) { | ||||||
|  | 			nodeName := getNodeName(ctx, f) | ||||||
|  | 			matchNode := gomega.ConsistOf(matchResourcesByNodeName(nodeName)) | ||||||
|  |  | ||||||
|  | 			ginkgo.By("start plugin and wait for ResourceSlice") | ||||||
|  | 			kubeletPlugin := newKubeletPlugin(ctx, f.ClientSet, getNodeName(ctx, f), driverName) | ||||||
|  | 			gomega.Eventually(ctx, listResources).Should(matchNode, "ResourceSlice from kubelet plugin") | ||||||
|  | 			gomega.Consistently(ctx, listResources).WithTimeout(5*time.Second).Should(matchNode, "ResourceSlice from kubelet plugin") | ||||||
|  |  | ||||||
|  | 			ginkgo.By("stop plugin and wait for ResourceSlice removal") | ||||||
|  | 			kubeletPlugin.Stop() | ||||||
|  | 			gomega.Eventually(ctx, listResources).Should(gomega.BeEmpty(), "ResourceSlices with no plugin") | ||||||
|  | 			gomega.Consistently(ctx, listResources).WithTimeout(5*time.Second).Should(gomega.BeEmpty(), "ResourceSlices with no plugin") | ||||||
|  | 		}) | ||||||
|  | 	}) | ||||||
| }) | }) | ||||||
|  |  | ||||||
| // Run Kubelet plugin and wait until it's registered | // Run Kubelet plugin and wait until it's registered | ||||||
| func newKubeletPlugin(ctx context.Context, nodeName, pluginName string) *testdriver.ExamplePlugin { | func newKubeletPlugin(ctx context.Context, clientSet kubernetes.Interface, nodeName, pluginName string) *testdriver.ExamplePlugin { | ||||||
| 	ginkgo.By("start Kubelet plugin") | 	ginkgo.By("start Kubelet plugin") | ||||||
| 	logger := klog.LoggerWithValues(klog.LoggerWithName(klog.Background(), "kubelet plugin "+pluginName), "node", nodeName) | 	logger := klog.LoggerWithValues(klog.LoggerWithName(klog.Background(), "kubelet plugin "+pluginName), "node", nodeName) | ||||||
| 	ctx = klog.NewContext(ctx, logger) | 	ctx = klog.NewContext(ctx, logger) | ||||||
| @@ -444,7 +548,8 @@ func newKubeletPlugin(ctx context.Context, nodeName, pluginName string) *testdri | |||||||
| 		ctx, | 		ctx, | ||||||
| 		cdiDir, | 		cdiDir, | ||||||
| 		pluginName, | 		pluginName, | ||||||
| 		"", | 		clientSet, | ||||||
|  | 		nodeName, | ||||||
| 		testdriver.FileOperations{}, | 		testdriver.FileOperations{}, | ||||||
| 		kubeletplugin.PluginSocketPath(endpoint), | 		kubeletplugin.PluginSocketPath(endpoint), | ||||||
| 		kubeletplugin.RegistrarSocketPath(path.Join(pluginRegistrationPath, pluginName+"-reg.sock")), | 		kubeletplugin.RegistrarSocketPath(path.Join(pluginRegistrationPath, pluginName+"-reg.sock")), | ||||||
| @@ -454,6 +559,11 @@ func newKubeletPlugin(ctx context.Context, nodeName, pluginName string) *testdri | |||||||
|  |  | ||||||
| 	gomega.Eventually(plugin.GetGRPCCalls).WithTimeout(pluginRegistrationTimeout).Should(testdriver.BeRegistered) | 	gomega.Eventually(plugin.GetGRPCCalls).WithTimeout(pluginRegistrationTimeout).Should(testdriver.BeRegistered) | ||||||
|  |  | ||||||
|  | 	ginkgo.DeferCleanup(func(ctx context.Context) { | ||||||
|  | 		// kubelet should do this eventually, but better make sure. | ||||||
|  | 		// A separate test checks this explicitly. | ||||||
|  | 		framework.ExpectNoError(clientSet.ResourceV1alpha2().ResourceSlices().DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{FieldSelector: "driverName=" + driverName})) | ||||||
|  | 	}) | ||||||
| 	ginkgo.DeferCleanup(plugin.Stop) | 	ginkgo.DeferCleanup(plugin.Stop) | ||||||
|  |  | ||||||
| 	return plugin | 	return plugin | ||||||
| @@ -549,3 +659,27 @@ func createTestObjects(ctx context.Context, clientSet kubernetes.Interface, node | |||||||
|  |  | ||||||
| 	return pod | 	return pod | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func createTestResourceSlice(ctx context.Context, clientSet kubernetes.Interface, nodeName, driverName string) { | ||||||
|  | 	slice := &resourcev1alpha2.ResourceSlice{ | ||||||
|  | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 			Name: nodeName, | ||||||
|  | 		}, | ||||||
|  | 		NodeName:   nodeName, | ||||||
|  | 		DriverName: driverName, | ||||||
|  | 		ResourceModel: resourcev1alpha2.ResourceModel{ | ||||||
|  | 			NamedResources: &resourcev1alpha2.NamedResourcesResources{}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ginkgo.By(fmt.Sprintf("Creating ResourceSlice %s", nodeName)) | ||||||
|  | 	slice, err := clientSet.ResourceV1alpha2().ResourceSlices().Create(ctx, slice, metav1.CreateOptions{}) | ||||||
|  | 	framework.ExpectNoError(err, "create ResourceSlice") | ||||||
|  | 	ginkgo.DeferCleanup(func(ctx context.Context) { | ||||||
|  | 		ginkgo.By(fmt.Sprintf("Deleting ResourceSlice %s", nodeName)) | ||||||
|  | 		err := clientSet.ResourceV1alpha2().ResourceSlices().Delete(ctx, slice.Name, metav1.DeleteOptions{}) | ||||||
|  | 		if !apierrors.IsNotFound(err) { | ||||||
|  | 			framework.ExpectNoError(err, "delete ResourceSlice") | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -41,6 +41,7 @@ import ( | |||||||
| 	"k8s.io/kubernetes/pkg/features" | 	"k8s.io/kubernetes/pkg/features" | ||||||
| 	"k8s.io/kubernetes/test/integration/framework" | 	"k8s.io/kubernetes/test/integration/framework" | ||||||
| 	"k8s.io/utils/pointer" | 	"k8s.io/utils/pointer" | ||||||
|  | 	"k8s.io/utils/ptr" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestNodeAuthorizer(t *testing.T) { | func TestNodeAuthorizer(t *testing.T) { | ||||||
| @@ -113,6 +114,13 @@ func TestNodeAuthorizer(t *testing.T) { | |||||||
| 	if _, err := superuserClient.ResourceV1alpha2().ResourceClaims("ns").Create(context.TODO(), &v1alpha2.ResourceClaim{ObjectMeta: metav1.ObjectMeta{Name: "mytemplatizedresourceclaim"}, Spec: v1alpha2.ResourceClaimSpec{ResourceClassName: "example.com"}}, metav1.CreateOptions{}); err != nil { | 	if _, err := superuserClient.ResourceV1alpha2().ResourceClaims("ns").Create(context.TODO(), &v1alpha2.ResourceClaim{ObjectMeta: metav1.ObjectMeta{Name: "mytemplatizedresourceclaim"}, Spec: v1alpha2.ResourceClaimSpec{ResourceClassName: "example.com"}}, metav1.CreateOptions{}); err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  | 	model := v1alpha2.ResourceModel{NamedResources: &v1alpha2.NamedResourcesResources{}} | ||||||
|  | 	if _, err := superuserClient.ResourceV1alpha2().ResourceSlices().Create(context.TODO(), &v1alpha2.ResourceSlice{ObjectMeta: metav1.ObjectMeta{Name: "myslice1"}, NodeName: "node1", DriverName: "dra.example.com", ResourceModel: model}, metav1.CreateOptions{}); err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	if _, err := superuserClient.ResourceV1alpha2().ResourceSlices().Create(context.TODO(), &v1alpha2.ResourceSlice{ObjectMeta: metav1.ObjectMeta{Name: "myslice2"}, NodeName: "node2", DriverName: "dra.example.com", ResourceModel: model}, metav1.CreateOptions{}); err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	pvName := "mypv" | 	pvName := "mypv" | ||||||
| 	if _, err := superuserClientExternal.StorageV1().VolumeAttachments().Create(context.TODO(), &storagev1.VolumeAttachment{ | 	if _, err := superuserClientExternal.StorageV1().VolumeAttachments().Create(context.TODO(), &storagev1.VolumeAttachment{ | ||||||
| @@ -195,6 +203,15 @@ func TestNodeAuthorizer(t *testing.T) { | |||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | 	deleteResourceSliceCollection := func(client clientset.Interface, nodeName *string) func() error { | ||||||
|  | 		return func() error { | ||||||
|  | 			var listOptions metav1.ListOptions | ||||||
|  | 			if nodeName != nil { | ||||||
|  | 				listOptions.FieldSelector = "nodeName=" + *nodeName | ||||||
|  | 			} | ||||||
|  | 			return client.ResourceV1alpha2().ResourceSlices().DeleteCollection(context.TODO(), metav1.DeleteOptions{}, listOptions) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 	addResourceClaimTemplateReference := func(client clientset.Interface) func() error { | 	addResourceClaimTemplateReference := func(client clientset.Interface) func() error { | ||||||
| 		return func() error { | 		return func() error { | ||||||
| 			_, err := client.CoreV1().Pods("ns").Patch(context.TODO(), "node2normalpod", types.MergePatchType, | 			_, err := client.CoreV1().Pods("ns").Patch(context.TODO(), "node2normalpod", types.MergePatchType, | ||||||
| @@ -640,6 +657,32 @@ func TestNodeAuthorizer(t *testing.T) { | |||||||
| 	expectForbidden(t, updateNode1CSINode(csiNode2Client)) | 	expectForbidden(t, updateNode1CSINode(csiNode2Client)) | ||||||
| 	expectForbidden(t, patchNode1CSINode(csiNode2Client)) | 	expectForbidden(t, patchNode1CSINode(csiNode2Client)) | ||||||
| 	expectForbidden(t, deleteNode1CSINode(csiNode2Client)) | 	expectForbidden(t, deleteNode1CSINode(csiNode2Client)) | ||||||
|  |  | ||||||
|  | 	// Always allowed. Permission to delete specific objects is checked per object. | ||||||
|  | 	// Beware, this is destructive! | ||||||
|  | 	expectAllowed(t, deleteResourceSliceCollection(csiNode1Client, ptr.To("node1"))) | ||||||
|  |  | ||||||
|  | 	// One slice must have been deleted, the other not. | ||||||
|  | 	slices, err := superuserClient.ResourceV1alpha2().ResourceSlices().List(context.TODO(), metav1.ListOptions{}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	if len(slices.Items) != 1 { | ||||||
|  | 		t.Fatalf("unexpected slices: %v", slices.Items) | ||||||
|  | 	} | ||||||
|  | 	if slices.Items[0].NodeName != "node2" { | ||||||
|  | 		t.Fatal("wrong slice deleted") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Superuser can delete. | ||||||
|  | 	expectAllowed(t, deleteResourceSliceCollection(superuserClient, nil)) | ||||||
|  | 	slices, err = superuserClient.ResourceV1alpha2().ResourceSlices().List(context.TODO(), metav1.ListOptions{}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	if len(slices.Items) != 0 { | ||||||
|  | 		t.Fatalf("unexpected slices: %v", slices.Items) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // expect executes a function a set number of times until it either returns the | // expect executes a function a set number of times until it either returns the | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Kubernetes Prow Robot
					Kubernetes Prow Robot