// Device Registrar // // API documentation for the device-registrar service. // // swagger:meta package services import ( "context" "net/http" api "edge-infra.dev/pkg/edge/device-registrar/api/v1alpha1" "edge-infra.dev/pkg/edge/device-registrar/config" "edge-infra.dev/pkg/lib/crypto" "github.com/gin-gonic/gin" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ) // Register Device API initiates the creation of device specific data, including short-lived activation code, // edge ID client configuration and TLS client certificate/key pairs. // The activation code can be retrieved when the Registration request completes. // swagger:model DeviceRegistrationResponse // success response after registering a device type DeviceRegistrationResponse struct { // required: true // description: bootstrap URL for the device // example: https://edge.store.ncr.corp/activation-code BootstrapURL string `json:"bootstrapURL"` // HostMapping value // required: true // description: host mapping for the device HostMapping HostMapping `json:"hostMapping"` } // swagger:model ListApplicationsResponse // success response after listing all available external applications type ListApplicationsResponse struct { // required: true // description: unique identifier for the external application // example: 123e4567-e89b-12d3-a456-426614174000 ID string `json:"id"` // required: true // description: name of the external application // example: connected-associates Name string `json:"name"` // required: true // description: description of the external application // example: This application is used to manage connected associates Description string `json:"description"` } // swagger:model HostMapping type HostMapping struct { // required: true // description: base bootstrap url for the device // example: edge-bootstrap.store.ncr.corp Host string `json:"host"` // required: true // description: cluster VIP address // example: 10.10.11.2 VIP string `json:"vip"` } // swagger:route GET /applications additional ListApplications // Lists external applications // // Lists all available external application names. // responses: // // 200: []ListApplicationsResponse Success // 500: description:Internal Server Error func ListApplications(c *gin.Context) { k8sClient, ctx, cancel := config.GetClientandContext(c) defer cancel() externalApplications := &api.ExternalApplicationList{} err := k8sClient.List(ctx, externalApplications, client.InNamespace(config.Namespace)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve applications: " + err.Error()}) return } listApplicationsResponse := []ListApplicationsResponse{} for _, app := range externalApplications.Items { listApplicationsResponse = append(listApplicationsResponse, ListApplicationsResponse{ ID: app.Spec.ID, Name: app.GetName(), Description: app.Spec.Description, }) } c.JSON(http.StatusOK, listApplicationsResponse) } // swagger:route POST /register legacy RegisterDevice // Registers a device // // Register Device API initiates the creation of device specific data, including short-lived activation code, edge ID client configuration and TLS client certificate/key pairs. The activation code can be retrieved when the registration request completes. // // parameters: RegisterDeviceRequest // responses: // // 200: DeviceRegistrationResponse Success // 400: description:Bad Request // 500: description:Internal Server Error func RegisterDevice(c *gin.Context) { req, err := checkRequest(c) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } k8sClient, ctx, cancel := config.GetClientandContext(c) defer cancel() // 1. get/create the device device, err := createDevice(ctx, k8sClient, req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create external device: " + err.Error()}) return } // step 2: loop through applicationIDs to create application and construct deviceBinding applicationSubjects, extApps, err := getAppSubjectsAndExternalApps(ctx, k8sClient, c, req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get External Application: " + err.Error()}) return } // step 3: create DeviceBinding deviceBindingName := device.GetName() + "-device-binding" deviceBinding := &api.DeviceBinding{ ObjectMeta: metav1.ObjectMeta{ Name: deviceBindingName, Namespace: device.GetNamespace(), OwnerReferences: []metav1.OwnerReference{ { APIVersion: api.GroupVersion.String(), Kind: "ExternalDevice", Name: device.GetName(), UID: device.GetUID(), }, }, }, Spec: api.DeviceBindingSpec{ Device: api.DeviceSubject{ APIGroup: api.GroupVersion.Group, Kind: "ExternalDevice", Name: device.GetName(), }, Applications: applicationSubjects, }, } if err := k8sClient.Create(ctx, deviceBinding); err != nil && !k8serrors.IsAlreadyExists(err) { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create device binding: " + err.Error()}) return } // retrieve the deviceBinding if err := k8sClient.Get(ctx, types.NamespacedName{ Name: deviceBindingName, Namespace: device.GetNamespace(), }, deviceBinding); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get device binding: " + err.Error()}) return } // step 4: loop through and create EdgeID client for _, extApp := range extApps { client := createClient(device, extApp, deviceBinding) // override the secret name to make it unique per registration client.Spec.SecretName = device.GetName() + "-" + extApp.GetName() + "-secret" if err := k8sClient.Create(ctx, client); err != nil && !k8serrors.IsAlreadyExists(err) { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create edgeID client: " + err.Error()}) return } } // step 5: create ActivationCode // Generate new activation code ac, err := generateActivationCode() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create activation code: " + err.Error()}) return } deviceBinding.Status.ActivationCode = ac.Plain() deviceBinding.Status.Timestamp = metav1.Now() err = k8sClient.Status().Update(ctx, deviceBinding) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update deviceBinding status: " + err.Error()}) return } storeID := "" if storeID, err = config.GetStoreID(ctx, k8sClient); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve edge-info: " + err.Error()}) return } err = k8sClient.Get(ctx, types.NamespacedName{ Name: device.GetName(), Namespace: config.Namespace, }, device) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve external device: " + err.Error()}) return } cert := createCert(device, storeID) if err := k8sClient.Create(ctx, cert); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create client certificate: " + err.Error()}) return } vip, err := config.GetVIP(ctx, k8sClient) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve VIP address: " + err.Error()}) return } resp := DeviceRegistrationResponse{ BootstrapURL: config.BootstrapURL + "/" + deviceBinding.Status.ActivationCode, HostMapping: HostMapping{ Host: config.BootstrapHost, VIP: vip, }, } c.JSON(http.StatusOK, resp) } func getExternalApplicationByID(ctx context.Context, c *gin.Context, k8sClient client.Client, appID string) (*api.ExternalApplication, error) { applications := &api.ExternalApplicationList{} err := k8sClient.List(ctx, applications, client.InNamespace(config.Namespace)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return nil, err } for _, app := range applications.Items { if app.Spec.ID == appID { //found return &app, nil } } // not found return nil, k8serrors.NewNotFound(api.GroupVersion.WithResource("externalapplications").GroupResource(), appID) } func generateActivationCode() (crypto.Credential, error) { ac, err := crypto.GenerateRandomActivationCode() if err != nil { return nil, err } return ac, nil }