...

Source file src/edge-infra.dev/pkg/edge/device-registrar/services/device_connect_service.go

Documentation: edge-infra.dev/pkg/edge/device-registrar/services

     1  package services
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"time"
     9  
    10  	"github.com/gin-gonic/gin"
    11  	corev1 "k8s.io/api/core/v1"
    12  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    13  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  	"k8s.io/apimachinery/pkg/types"
    15  	"sigs.k8s.io/controller-runtime/pkg/client"
    16  
    17  	cmApi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
    18  	cmMeta "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
    19  
    20  	api "edge-infra.dev/pkg/edge/device-registrar/api/v1alpha1"
    21  	"edge-infra.dev/pkg/edge/device-registrar/config"
    22  	clientApi "edge-infra.dev/pkg/edge/iam/api/v1alpha1"
    23  )
    24  
    25  // swagger:model DeviceConnectResponse
    26  type DeviceConnectResponse struct {
    27  	Applications []ApplicationResponse `json:"applications"`
    28  	DeviceAuth   DeviceAuth            `json:"deviceAuth,omitempty"`
    29  	HostMapping  HostMapping           `json:"hostMapping"`
    30  }
    31  
    32  // swagger:model DeviceConnectRequest
    33  type DeviceConnectRequest struct {
    34  	Device         api.ExternalDeviceSpec `json:"device" binding:"required"`
    35  	ApplicationIDs []string               `json:"applicationIDs" binding:"required,min=1"`
    36  }
    37  
    38  // ApplicationResponse contains external application details
    39  // swagger:model ApplicationResponse
    40  type ApplicationResponse struct {
    41  	// example: 1234567-e89b-12d3-a456-426614174000
    42  	ID string `json:"id"`
    43  	// example: connected-associates
    44  	Name string `json:"name"`
    45  	// Config EdgeIDClient
    46  	Config EdgeIDClient `json:"config"`
    47  }
    48  
    49  // EdgeIDClient contains the EdgeID client details
    50  // swagger:model EdgeIDClient
    51  type EdgeIDClient struct {
    52  	// example: e00a1c4b-2bdc-450b-8fc6-1234567890
    53  	ClientID string `json:"clientID"`
    54  	// example: bnTV~gAZj.abcdefghi~ks4wE
    55  	ClientSecret string `json:"clientSecret"`
    56  }
    57  
    58  // DeviceAuth contains the device authentication details
    59  // swagger:model DeviceAuth
    60  type DeviceAuth struct {
    61  	Crt []byte `json:"tls.crt,omitempty"`
    62  	Key []byte `json:"tls.key,omitempty"`
    63  }
    64  
    65  var errDeviceBindingNotFound = errors.New("invalid activation code")
    66  var errActivationCodeNotFound = errors.New("activationCode is required")
    67  var errActivationCodeExpired = errors.New("activation code has expired")
    68  var errActivationCodeUsed = errors.New("activation code has already been used")
    69  
    70  // swagger:route POST /connect core ConnectDevice
    71  // Connect a device to the system
    72  //
    73  // Connect Device API initiates the connection of a device to the system.
    74  //
    75  // responses:
    76  //
    77  //	200: DeviceConnectResponse Success
    78  //	400: description:Bad Request
    79  //	500: description:Internal Server Error
    80  func ConnectDevice(c *gin.Context) {
    81  	req, err := checkRequest(c)
    82  	if err != nil {
    83  		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    84  		return
    85  	}
    86  
    87  	k8sClient, ctx, cancel := config.GetClientandContext(c)
    88  	defer cancel()
    89  
    90  	// verify the deviceBinding
    91  	deviceBinding, err := retrieveDeviceRegistration(ctx, k8sClient, c)
    92  	if err == errActivationCodeNotFound {
    93  		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    94  		return
    95  	} else if err != nil {
    96  		c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
    97  		return
    98  	}
    99  
   100  	if err := register(ctx, k8sClient, c, req, deviceBinding); err != nil {
   101  		return
   102  	}
   103  
   104  	// grab the device binding
   105  	devb := &api.DeviceBinding{}
   106  	if err := k8sClient.Get(ctx, types.NamespacedName{
   107  		Name:      deviceBinding.Name,
   108  		Namespace: deviceBinding.Namespace,
   109  	}, devb); err != nil {
   110  		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get device binding: " + err.Error()})
   111  		return
   112  	}
   113  
   114  	if err := bootstrap(ctx, k8sClient, c, devb); err != nil {
   115  		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
   116  		return
   117  	}
   118  }
   119  
   120  func register(ctx context.Context, k8sClient client.Client, c *gin.Context, req DeviceConnectRequest, deviceBinding *api.DeviceBinding) error {
   121  	// 1. get/create the device
   122  	device, applicationSubjects, extApps, err := getDeviceAndAppSubjects(ctx, k8sClient, c, req)
   123  	if err != nil {
   124  		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
   125  		return err
   126  	}
   127  
   128  	// step 3: update DeviceBinding
   129  	err = updateDeviceBinding(ctx, k8sClient, device, deviceBinding.GetName(), deviceBinding.GetNamespace(), applicationSubjects)
   130  	if err != nil {
   131  		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update device binding: " + err.Error()})
   132  		return err
   133  	}
   134  
   135  	// step 4: create client cert
   136  	storeID := ""
   137  	if storeID, err = config.GetStoreID(ctx, k8sClient); err != nil {
   138  		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve edge-info: " + err.Error()})
   139  		return err
   140  	}
   141  
   142  	cert := createCert(device, storeID)
   143  	if err := k8sClient.Create(ctx, cert); err != nil {
   144  		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create client certificate: " + err.Error()})
   145  		return err
   146  	}
   147  
   148  	// step 5: loop through and create EdgeID client
   149  	for _, extApp := range extApps {
   150  		client := createClient(device, extApp, deviceBinding)
   151  		// override the secret name to make it unique per registration
   152  		client.Spec.SecretName = device.GetName() + "-" + extApp.GetName() + "-secret"
   153  		if err := k8sClient.Create(ctx, client); err != nil && !k8serrors.IsAlreadyExists(err) {
   154  			c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create edgeID client: " + err.Error()})
   155  			return err
   156  		}
   157  
   158  		_, err = waitForSecret(ctx, k8sClient, device.GetNamespace(), client.Spec.SecretName)
   159  		if err != nil {
   160  			c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to wait for client secret: " + err.Error()})
   161  			return err
   162  		}
   163  	}
   164  
   165  	return nil
   166  }
   167  
   168  func bootstrap(ctx context.Context, k8sClient client.Client, c *gin.Context, devb *api.DeviceBinding) error {
   169  	applicationResponse, err := getApplicationResponse(ctx, k8sClient, devb)
   170  	if err != nil {
   171  		return fmt.Errorf("failed to find EdgeID client secret: %w", err)
   172  	}
   173  
   174  	vip, err := config.GetVIP(ctx, k8sClient)
   175  	if err != nil {
   176  		return fmt.Errorf("failed to retrieve VIP address: %w", err)
   177  	}
   178  
   179  	secretName := devb.Spec.Device.Name + "-cert"
   180  	deviceAuthSecret, err := waitForSecret(ctx, k8sClient, "emissary", secretName)
   181  	if err != nil {
   182  		return fmt.Errorf("failed to wait for device auth secret, %w", err)
   183  	}
   184  
   185  	resp := DeviceConnectResponse{
   186  		Applications: applicationResponse,
   187  		HostMapping: HostMapping{
   188  			Host: config.Host,
   189  			VIP:  vip,
   190  		},
   191  		DeviceAuth: DeviceAuth{
   192  			Crt: deviceAuthSecret.Data["tls.crt"],
   193  			Key: deviceAuthSecret.Data["tls.key"],
   194  		},
   195  	}
   196  	c.JSON(http.StatusOK, resp)
   197  	return nil
   198  }
   199  
   200  func updateDeviceBinding(ctx context.Context, k8sClient client.Client, device *api.ExternalDevice,
   201  	deviceBindingName string, namespace string, applicationSubjects []api.ApplicationSubject) error {
   202  	// Step 1: Retrieve the existing deviceBinding
   203  	deviceBinding := &api.DeviceBinding{}
   204  	if err := k8sClient.Get(ctx, types.NamespacedName{
   205  		Name:      deviceBindingName,
   206  		Namespace: namespace,
   207  	}, deviceBinding); err != nil {
   208  		return fmt.Errorf("failed to get device binding: %w", err)
   209  	}
   210  
   211  	// Step 2: Update the OwnerReferences
   212  	deviceBinding.ObjectMeta.OwnerReferences = []metav1.OwnerReference{
   213  		{
   214  			APIVersion: api.GroupVersion.String(),
   215  			Kind:       "ExternalDevice",
   216  			Name:       device.GetName(),
   217  			UID:        device.GetUID(),
   218  		},
   219  	}
   220  
   221  	// Step 3: Update the fields
   222  	deviceBinding.Spec.Device = api.DeviceSubject{
   223  		APIGroup: api.GroupVersion.Group,
   224  		Kind:     "ExternalDevice",
   225  		Name:     device.GetName(),
   226  	}
   227  	deviceBinding.Spec.Applications = applicationSubjects
   228  
   229  	// Step 4: Update the deviceBinding object
   230  	if err := k8sClient.Update(ctx, deviceBinding); err != nil {
   231  		return fmt.Errorf("failed to update device binding: %w", err)
   232  	}
   233  
   234  	deviceBinding.Status.CodeUsed = true
   235  	if err := k8sClient.Status().Update(ctx, deviceBinding); err != nil {
   236  		return fmt.Errorf("failed to update device status: %w", err)
   237  	}
   238  
   239  	return nil
   240  }
   241  
   242  // createDevice creates an external device in the cluster. Checks if one already exists and if so,
   243  // deletes it before creating a new one
   244  func createDevice(ctx context.Context, k8sClient client.Client, req DeviceConnectRequest) (*api.ExternalDevice, error) {
   245  	// check for old external device
   246  	device := &api.ExternalDevice{}
   247  	err := k8sClient.Get(ctx, types.NamespacedName{
   248  		Name:      req.Device.Name,
   249  		Namespace: config.Namespace,
   250  	}, device)
   251  
   252  	// no error -> a device was found that we can delete
   253  	if err == nil {
   254  		if err := deleteDevice(ctx, k8sClient, device); err != nil {
   255  			return nil, err
   256  		}
   257  	} else if !k8serrors.IsNotFound(err) {
   258  		// device wasn't found, but another error occurred, so return the error
   259  		return nil, err
   260  	}
   261  
   262  	// recreate it
   263  	device = &api.ExternalDevice{
   264  		ObjectMeta: metav1.ObjectMeta{
   265  			Name:      req.Device.Name,
   266  			Namespace: config.Namespace,
   267  		},
   268  		Spec: req.Device,
   269  	}
   270  	if err := k8sClient.Create(ctx, device); err != nil {
   271  		return nil, fmt.Errorf("timeout waiting for client secret to be created")
   272  	}
   273  
   274  	return device, nil
   275  }
   276  
   277  func deleteDevice(ctx context.Context, k8sClient client.Client, device *api.ExternalDevice) error {
   278  	// delete device
   279  	err := k8sClient.Delete(ctx, device)
   280  	if err != nil {
   281  		return err
   282  	}
   283  	err = k8sClient.Delete(ctx, &cmApi.Certificate{
   284  		ObjectMeta: metav1.ObjectMeta{
   285  			Name:      device.GetName(),
   286  			Namespace: "emissary",
   287  		},
   288  	})
   289  	if err != nil && !k8serrors.IsNotFound(err) {
   290  		return err
   291  	}
   292  	err = k8sClient.Delete(ctx, &corev1.Secret{
   293  		ObjectMeta: metav1.ObjectMeta{
   294  			Name:      device.GetName() + "-cert",
   295  			Namespace: "emissary",
   296  		},
   297  	})
   298  	if err != nil && !k8serrors.IsNotFound(err) {
   299  		return err
   300  	}
   301  	return nil
   302  }
   303  
   304  func checkRequest(c *gin.Context) (DeviceConnectRequest, error) {
   305  	var req DeviceConnectRequest
   306  	if err := c.ShouldBindJSON(&req); err != nil {
   307  		return req, fmt.Errorf("failed to bind JSON: %w", err)
   308  	}
   309  
   310  	// Check that the device name has been provided
   311  	if req.Device.Name == "" {
   312  		return req, fmt.Errorf("Device Name is required")
   313  	}
   314  
   315  	// Check that the device SN has been provided
   316  	if req.Device.SN == "" {
   317  		return req, fmt.Errorf("Serial Number is required")
   318  	}
   319  
   320  	return req, nil
   321  }
   322  
   323  // step 2: loop through applicationIDs to create application and construct deviceBinding
   324  func getAppSubjectsAndExternalApps(ctx context.Context, k8sClient client.Client, c *gin.Context, req DeviceConnectRequest) ([]api.ApplicationSubject, []*api.ExternalApplication, error) {
   325  	applicationSubjects := []api.ApplicationSubject{}
   326  	extApps := []*api.ExternalApplication{}
   327  	for _, appID := range req.ApplicationIDs {
   328  		extApp, err := getExternalApplicationByID(ctx, c, k8sClient, appID)
   329  		if err != nil {
   330  			return nil, nil, fmt.Errorf("failed to get External Application: %w", err)
   331  		}
   332  		extApps = append(extApps, extApp)
   333  		applicationSubjects = append(applicationSubjects,
   334  			api.ApplicationSubject{
   335  				APIGroup: api.GroupVersion.Group,
   336  				Kind:     "ExternalApplication",
   337  				Name:     extApp.GetName(),
   338  				ID:       extApp.Spec.ID,
   339  			},
   340  		)
   341  	}
   342  	return applicationSubjects, extApps, nil
   343  }
   344  
   345  func createCert(device *api.ExternalDevice, storeID string) *cmApi.Certificate {
   346  	return &cmApi.Certificate{
   347  		ObjectMeta: metav1.ObjectMeta{
   348  			Name:      device.GetName(),
   349  			Namespace: "emissary",
   350  		},
   351  		Spec: cmApi.CertificateSpec{
   352  			SecretName: device.GetName() + "-cert",
   353  			IssuerRef: cmMeta.ObjectReference{
   354  				Name:  "device-registrar-ca-issuer",
   355  				Kind:  "Issuer",
   356  				Group: "cert-manager.io",
   357  			},
   358  			Usages: []cmApi.KeyUsage{
   359  				cmApi.UsageClientAuth,
   360  			},
   361  			Duration:    &metav1.Duration{Duration: 8760 * time.Hour}, // 1 year
   362  			RenewBefore: &metav1.Duration{Duration: 2184 * time.Hour}, // 91 days
   363  			DNSNames:    []string{"edge.store.ncr.corp"},
   364  			Subject: &cmApi.X509Subject{
   365  				OrganizationalUnits: []string{
   366  					"urn:deviceName:" + device.GetName(),
   367  					"urn:storeID:" + storeID,
   368  					"urn:SN:" + device.Spec.SN,
   369  				},
   370  			},
   371  		},
   372  	}
   373  }
   374  
   375  func createClient(device *api.ExternalDevice, extApp *api.ExternalApplication, deviceBinding *api.DeviceBinding) *clientApi.Client {
   376  	clientName := device.GetName() + "-" + extApp.GetName() + "-client"
   377  	return &clientApi.Client{
   378  		ObjectMeta: metav1.ObjectMeta{
   379  			GenerateName: clientName + "-",
   380  			Namespace:    device.GetNamespace(),
   381  			OwnerReferences: []metav1.OwnerReference{
   382  				{
   383  					APIVersion: api.GroupVersion.String(),
   384  					Kind:       "DeviceBinding",
   385  					Name:       deviceBinding.GetName(),
   386  					UID:        deviceBinding.GetUID(),
   387  				},
   388  			},
   389  		},
   390  		Spec: extApp.Spec.ClientTemplate.Spec,
   391  	}
   392  }
   393  
   394  // Waits for a secret to be created with a timeout of 10 seconds
   395  func waitForSecret(ctx context.Context, k8sClient client.Client, ns string, secretName string) (corev1.Secret, error) {
   396  	err := error(nil)
   397  	secretFound := false
   398  	timeout := time.After(30 * time.Second) // Example timeout duration, adjust as needed
   399  
   400  	secret := &corev1.Secret{}
   401  	for !secretFound {
   402  		select {
   403  		case <-timeout:
   404  			return corev1.Secret{}, fmt.Errorf("timeout waiting for client secret to be created %w", err)
   405  		default:
   406  			err := k8sClient.Get(ctx, types.NamespacedName{
   407  				Name:      secretName,
   408  				Namespace: ns,
   409  			}, secret)
   410  			if err == nil {
   411  				secretFound = true
   412  				break
   413  			}
   414  			if !k8serrors.IsNotFound(err) {
   415  				return corev1.Secret{}, fmt.Errorf("timeout waiting for client secret to be created %w", err)
   416  			}
   417  			time.Sleep(1 * time.Second) // Wait for 1 second before checking again
   418  		}
   419  	}
   420  
   421  	return *secret, nil
   422  }
   423  
   424  func getApplicationResponse(ctx context.Context, k8sClient client.Client, devb *api.DeviceBinding) ([]ApplicationResponse, error) {
   425  	applicationResponse := []ApplicationResponse{}
   426  	for _, app := range devb.Spec.Applications {
   427  		clientSecretName := devb.Spec.Device.Name + "-" + app.Name + "-secret"
   428  		deviceSecret := &corev1.Secret{}
   429  		err := k8sClient.Get(ctx, types.NamespacedName{Name: clientSecretName, Namespace: devb.Namespace}, deviceSecret)
   430  		if err != nil {
   431  			return nil, err
   432  		}
   433  		applicationResponse = append(applicationResponse, ApplicationResponse{
   434  			ID:   app.ID,
   435  			Name: app.Name,
   436  			Config: EdgeIDClient{
   437  				ClientID:     string(deviceSecret.Data["client_id"]),
   438  				ClientSecret: string(deviceSecret.Data["client_secret"]),
   439  			},
   440  		})
   441  	}
   442  	return applicationResponse, nil
   443  }
   444  
   445  func getDeviceAndAppSubjects(ctx context.Context, k8sClient client.Client, c *gin.Context, req DeviceConnectRequest) (*api.ExternalDevice, []api.ApplicationSubject, []*api.ExternalApplication, error) {
   446  	device, err := createDevice(ctx, k8sClient, req)
   447  	if err != nil {
   448  		return nil, nil, nil, fmt.Errorf("failed to create external device: %w", err)
   449  	}
   450  
   451  	applicationSubjects, extApps, err := getAppSubjectsAndExternalApps(ctx, k8sClient, c, req)
   452  	if err != nil {
   453  		return nil, nil, nil, fmt.Errorf("failed to get External Application: %w", err)
   454  	}
   455  
   456  	return device, applicationSubjects, extApps, nil
   457  }
   458  
   459  // retrieveDeviceRegistration retrieves the device registration CR based on the device ID
   460  func retrieveDeviceRegistration(ctx context.Context, k8sClient client.Client, c *gin.Context) (*api.DeviceBinding, error) {
   461  	activationCode := c.Param("activationCode")
   462  	if activationCode == "" {
   463  		return nil, errActivationCodeNotFound
   464  	}
   465  
   466  	// Get all device registrations
   467  	deviceBindingList := &api.DeviceBindingList{}
   468  	err := k8sClient.List(ctx, deviceBindingList)
   469  	if err != nil {
   470  		return nil, fmt.Errorf("failed to list device bindings: %w", err)
   471  	}
   472  
   473  	for i := range deviceBindingList.Items {
   474  		device := deviceBindingList.Items[i] // Create a copy to avoid memory aliasing
   475  		if device.Status.ActivationCode == activationCode {
   476  			if device.Status.CodeUsed {
   477  				return nil, errActivationCodeUsed
   478  			}
   479  			timestamp := device.Status.Timestamp.Time
   480  			if time.Since(timestamp) > time.Hour {
   481  				return nil, errActivationCodeExpired
   482  			}
   483  			return &device, err
   484  		}
   485  	}
   486  	return nil, errDeviceBindingNotFound
   487  }
   488  
   489  // Everything below this point is used for documentation purposes and exists for
   490  // the purpose of generating the swagger spec only.
   491  
   492  // The DeviceConnectRequest parameters
   493  //
   494  // swagger:parameters ConnectDevice
   495  type DeviceConnectRequestParams struct {
   496  	// in: body
   497  	Body DeviceConnectRequest
   498  }
   499  

View as plain text