Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions cmd/hostagent/subcmds/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,6 @@ var serveCmd = &cobra.Command{
klog.Fatalf("failed to convert VF config to network request: %v", err)
}

if err := service.NewInstallationService(unCachedClient).Start(true); err != nil {
klog.Fatalf("failed to start installation service: %v", err)
}

mgr, err := ctrl.NewManager(clientCfg, ctrl.Options{
Scheme: scheme,
Metrics: metricsserver.Options{
Expand Down Expand Up @@ -107,6 +103,10 @@ var serveCmd = &cobra.Command{
os.Exit(1)
}

if err := service.NewInstallationService(unCachedClient, nm).Start(true); err != nil {
klog.Fatalf("failed to start installation service: %v", err)
}

reconciler := hostagent.NewHostAgentReconciler(mgr.GetClient(), opts.BFBRegistryAddress, dpuNodeManager, nm)
if err = reconciler.SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "DPU")
Expand Down
53 changes: 49 additions & 4 deletions internal/provisioning/hostagent/service/installation_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,27 @@ const (
rpmRepoDir = "/rpm"
)

// NetworkConfigurator is an interface for triggering host network configuration.
// It is satisfied by networkmanager.NetworkManager.
type NetworkConfigurator interface {
AddNetworkRequest(dpu *provisioningv1.DPU) error
}

type InstallationService struct {
client.Client
handler http.Handler
// mu protects listeners
mu sync.Mutex
// listeners maps interface names to their listeners
listeners map[string]net.Listener
listeners map[string]net.Listener
networkManager NetworkConfigurator
}

func NewInstallationService(client client.Client) *InstallationService {
func NewInstallationService(client client.Client, nm NetworkConfigurator) *InstallationService {
s := &InstallationService{
Client: client,
listeners: make(map[string]net.Listener),
Client: client,
listeners: make(map[string]net.Listener),
networkManager: nm,
}
ws := new(restful.WebService).Path("/")
ws.Route(
Expand All @@ -92,6 +100,11 @@ func NewInstallationService(client client.Client) *InstallationService {
Param(ws.QueryParameter("name", "the name of the object").Required(true)).
Produces(restful.MIME_JSON).
To(s.GetObject))
ws.Route(
ws.POST("/configure-host-vfs").
Consumes(restful.MIME_JSON).
Produces(restful.MIME_JSON).
To(s.ConfigureHostVFs))
ws.Route(ws.GET("/healthz").To(s.HealthCheck))
// Package repositories: serve .deb and .rpm packages for DPU provisioning.
ws.Route(ws.GET("/deb/{subpath:*}").To(serveRepoFile(debRepoDir)))
Expand Down Expand Up @@ -284,6 +297,38 @@ func (s *InstallationService) HealthCheck(req *restful.Request, resp *restful.Re
resp.WriteHeader(http.StatusOK)
}

func (s *InstallationService) ConfigureHostVFs(req *restful.Request, resp *restful.Response) {
var request types.ConfigureHostVFsRequest
if err := req.ReadEntity(&request); err != nil {
klog.Errorf("failed to read configure host VF request: %v", err)
_ = resp.WriteError(http.StatusBadRequest, err)
return
}
klog.Infof("Received configure host VF request: %#v", request)

if s.networkManager == nil {
klog.Errorf("network manager is not configured")
_ = resp.WriteError(http.StatusServiceUnavailable, fmt.Errorf("network manager is not configured"))
return
}

dpu := &provisioningv1.DPU{}
if err := s.Get(req.Request.Context(), client.ObjectKey{Namespace: request.DPUNamespace, Name: request.DPUName}, dpu); err != nil {
klog.Errorf("failed to get DPU %s/%s: %v", request.DPUNamespace, request.DPUName, err)
_ = resp.WriteError(http.StatusNotFound, err)
return
}

if err := s.networkManager.AddNetworkRequest(dpu); err != nil {
klog.Errorf("failed to add network request for DPU %s/%s: %v", request.DPUNamespace, request.DPUName, err)
_ = resp.WriteError(http.StatusInternalServerError, err)
return
}

klog.Infof("Successfully added network request for DPU %s/%s", request.DPUNamespace, request.DPUName)
resp.WriteHeader(http.StatusOK)
}

func (s *InstallationService) UpdateStatus(req *restful.Request, resp *restful.Response) {
var request types.UpdateStatusRequest
if err := req.ReadEntity(&request); err != nil {
Expand Down
108 changes: 107 additions & 1 deletion internal/provisioning/hostagent/service/installation_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
)

type mockNetworkConfigurator struct {
addNetworkRequestFunc func(dpu *provisioningv1.DPU) error
}

func (m *mockNetworkConfigurator) AddNetworkRequest(dpu *provisioningv1.DPU) error {
if m.addNetworkRequestFunc != nil {
return m.addNetworkRequestFunc(dpu)
}
return nil
}

var _ = Describe("InstallationService", func() {
var testNS *corev1.Namespace
var installationService *InstallationService
Expand Down Expand Up @@ -73,7 +84,7 @@ var _ = Describe("InstallationService", func() {
testNS = &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{GenerateName: "installation-service-testns-"}}
Expect(k8sClient.Create(ctx, testNS)).To(Succeed())

installationService = NewInstallationService(k8sClient)
installationService = NewInstallationService(k8sClient, nil)
Expect(installationService.Start(false)).To(Succeed())
// Start() runs the server in a goroutine; wait until it is listening to avoid connection refused.
Eventually(func() error {
Expand Down Expand Up @@ -291,4 +302,99 @@ var _ = Describe("InstallationService", func() {
})
})

Context("configure host VF", func() {
It("should return 400 when request body is malformed", func() {
resp, err := http.Post(fmt.Sprintf("http://%s/configure-host-vfs", address), "application/json", bytes.NewBufferString("not-json"))
Expect(err).To(Succeed())
Expect(resp.StatusCode).To(Equal(http.StatusBadRequest))
})

It("should return 503 when network manager is not configured", func() {
dpu := createDPU("test-dpu", testNS.Name)

request := types.ConfigureHostVFsRequest{
DPUName: dpu.Name,
DPUNamespace: dpu.Namespace,
}
req, err := json.Marshal(request)
Expect(err).To(Succeed())

resp, err := http.Post(fmt.Sprintf("http://%s/configure-host-vfs", address), "application/json", bytes.NewBuffer(req))
Expect(err).To(Succeed())
Expect(resp.StatusCode).To(Equal(http.StatusServiceUnavailable))
})

Context("when network manager is configured", func() {
var mockNM *mockNetworkConfigurator

BeforeEach(func() {
installationService.Stop()
mockNM = &mockNetworkConfigurator{}
installationService = NewInstallationService(k8sClient, mockNM)
Expect(installationService.Start(false)).To(Succeed())
})

It("should successfully configure host VF", func() {
dpu := createDPU("test-dpu", testNS.Name)

var receivedDPU *provisioningv1.DPU
mockNM.addNetworkRequestFunc = func(dpu *provisioningv1.DPU) error {
receivedDPU = dpu
return nil
}

request := types.ConfigureHostVFsRequest{
DPUName: dpu.Name,
DPUNamespace: dpu.Namespace,
}
req, err := json.Marshal(request)
Expect(err).To(Succeed())

resp, err := http.Post(fmt.Sprintf("http://%s/configure-host-vfs", address), "application/json", bytes.NewBuffer(req))
Expect(err).To(Succeed())
Expect(resp.StatusCode).To(Equal(http.StatusOK))
Expect(receivedDPU).NotTo(BeNil())
Expect(receivedDPU.Name).To(Equal(dpu.Name))
Expect(receivedDPU.Namespace).To(Equal(dpu.Namespace))

By("the full DPU spec should be passed to AddNetworkRequest")
Expect(receivedDPU.Spec.SerialNumber).To(Equal(dpu.Spec.SerialNumber))
Expect(receivedDPU.Spec.DPUFlavor).To(Equal(dpu.Spec.DPUFlavor))
Expect(receivedDPU.Spec.BFB).To(Equal(dpu.Spec.BFB))
})

It("should return 404 when DPU not found", func() {
request := types.ConfigureHostVFsRequest{
DPUName: "non-existent-dpu",
DPUNamespace: testNS.Name,
}
req, err := json.Marshal(request)
Expect(err).To(Succeed())

resp, err := http.Post(fmt.Sprintf("http://%s/configure-host-vfs", address), "application/json", bytes.NewBuffer(req))
Expect(err).To(Succeed())
Expect(resp.StatusCode).To(Equal(http.StatusNotFound))
})

It("should return 500 when AddNetworkRequest fails", func() {
dpu := createDPU("test-dpu", testNS.Name)

mockNM.addNetworkRequestFunc = func(dpu *provisioningv1.DPU) error {
return fmt.Errorf("network manager is not initialized")
}

request := types.ConfigureHostVFsRequest{
DPUName: dpu.Name,
DPUNamespace: dpu.Namespace,
}
req, err := json.Marshal(request)
Expect(err).To(Succeed())

resp, err := http.Post(fmt.Sprintf("http://%s/configure-host-vfs", address), "application/json", bytes.NewBuffer(req))
Expect(err).To(Succeed())
Expect(resp.StatusCode).To(Equal(http.StatusInternalServerError))
})
})
})

})
5 changes: 5 additions & 0 deletions internal/provisioning/hostagent/service/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,8 @@ type UpdateStatusRequest struct {
DPUNamespace string `json:"dpuNamespace"`
AgentStatus provisioningv1.AgentStatus `json:"agentStatus"`
}

type ConfigureHostVFsRequest struct {
DPUName string `json:"dpuName"`
DPUNamespace string `json:"dpuNamespace"`
}