package activationcode import ( "context" "database/sql" "encoding/hex" "errors" "fmt" "k8s.io/utils/ptr" gcperror "edge-infra.dev/pkg/edge/api/apierror/gcp" "edge-infra.dev/pkg/edge/api/graph/model" "edge-infra.dev/pkg/edge/api/services" "edge-infra.dev/pkg/edge/api/services/edgenode/common" "edge-infra.dev/pkg/lib/crypto" pxe "edge-infra.dev/pkg/sds/ien/k8s/controllers/pxe/common" ) type service struct { SQLDB *sql.DB TerminalService services.TerminalService StoreClusterService services.StoreClusterService ClusterConfigService services.ClusterConfigService SecretService services.SecretService GCPService services.GCPService BannerService services.BannerService } var ( ErrInvalidActivationSecretCount = errors.New("invalid number of activation code secrets") ErrInvalidActivationSecret = errors.New("invalid activation code secret") ErrDeletingClusterActivationCodes = errors.New("error deleting cluster's activation codes") ) func (s *service) Fetch(ctx context.Context, terminalID string) (*string, error) { return common.FetchActivationCode(ctx, s.SQLDB, terminalID) } func (s *service) FetchFromSecret(ctx context.Context, terminalID string) (string, error) { terminal, err := s.TerminalService.GetTerminal(ctx, terminalID, ptr.To(false)) if err != nil { return "", err } cluster, err := s.StoreClusterService.GetCluster(ctx, terminal.ClusterEdgeID) if err != nil { return "", err } secretName := fmt.Sprintf("%s-%s", SecretType, terminalID) secrets, err := s.GCPService.GetSecrets(ctx, &secretName, ptr.To(Workload), ptr.To(SecretType), true, cluster.ProjectID) if err != nil { return "", err } if len(secrets) != 1 { return "", ErrInvalidActivationSecretCount } if len(secrets[0].Values) != 1 { return "", ErrInvalidActivationSecret } return secrets[0].Values[0].Value, nil } func (s *service) Refresh(ctx context.Context, terminalID string) (string, error) { activationCode, err := crypto.GenerateRandomActivationCode() if err != nil { return "", err } terminal, err := s.TerminalService.GetTerminal(ctx, terminalID, ptr.To(false)) if err != nil { return "", err } if err := s.Create(ctx, activationCode, terminal); err != nil { return "", err } return activationCode.Plain(), nil } func (s *service) Create(ctx context.Context, activationCode crypto.Credential, terminal *model.Terminal) error { _, err := s.SQLDB.ExecContext(ctx, SetActivationCode, hex.EncodeToString(activationCode.Hashed()), terminal.TerminalID) if err != nil { return err } cluster, err := s.StoreClusterService.GetCluster(ctx, terminal.ClusterEdgeID) if err != nil { return err } return s.SyncToStore(ctx, terminal.TerminalID, cluster, activationCode.Plain()) } func (s *service) SyncToStore(ctx context.Context, terminalID string, cluster *model.Cluster, activationCode string) error { // create the secret manager secret to store the activation code values := []*model.KeyValues{{Key: SecretKey, Value: activationCode}} secretName := fmt.Sprintf("%s-%s", SecretType, terminalID) return s.GCPService.AddSecret(ctx, secretName, Workload, SecretType, values, cluster.ProjectID, ptr.To(Workload), nil) } func (s *service) SyncAllToStore(ctx context.Context, clusterEdgeID string) error { terminalList, err := s.TerminalService.GetTerminalsByClusterID(ctx, clusterEdgeID) if err != nil { return err } cluster, err := s.StoreClusterService.GetCluster(ctx, clusterEdgeID) if err != nil { return err } var allErrs error for _, terminal := range terminalList { activationCode, err := s.FetchFromSecret(ctx, terminal.TerminalID) if err != nil { // Activation code does not exist or is used continue } if err := s.SyncToStore(ctx, terminal.TerminalID, cluster, activationCode); err != nil { allErrs = errors.Join(allErrs, err) } } return allErrs } func (s *service) MarkUsed(ctx context.Context, terminalID string, cluster *model.Cluster, clusterInfra *model.Cluster) error { // set to empty string to indicate it has been consumed _, err := s.SQLDB.ExecContext(ctx, SetActivationCode, "", terminalID) if err != nil { return err } return s.delete(ctx, terminalID, cluster, clusterInfra) } func (s *service) CleanupStore(ctx context.Context, cluster *model.Cluster) error { terminalList, err := s.TerminalService.GetTerminalsByClusterID(ctx, cluster.ClusterEdgeID) if err != nil { return err } clusterInfra, err := s.BannerService.GetClusterInfraInfo(ctx, cluster.BannerEdgeID) if err != nil { return err } var allErrs error for _, terminal := range terminalList { if err := s.delete(ctx, terminal.TerminalID, cluster, clusterInfra); err != nil { allErrs = errors.Join(allErrs, fmt.Errorf("%w: %v", ErrDeletingClusterActivationCodes, err)) } } return allErrs } func (s *service) CleanupTerminal(ctx context.Context, terminalID string, clusterEdgeID string) error { cluster, err := s.StoreClusterService.GetCluster(ctx, clusterEdgeID) if err != nil { return err } clusterInfra, err := s.BannerService.GetClusterInfraInfo(ctx, cluster.BannerEdgeID) if err != nil { return err } return s.delete(ctx, terminalID, cluster, clusterInfra) } // delete removes the relevant resources for the terminal's activation code func (s *service) delete(ctx context.Context, terminalID string, cluster *model.Cluster, clusterInfra *model.Cluster) error { secretName := fmt.Sprintf("%s-%s", SecretType, terminalID) if _, err := s.GCPService.DeleteSecret(ctx, secretName, cluster.ProjectID); gcperror.IgnoreNotFound(err) != nil { return err } // TODO: Remove the following line in 0.25 // - Related issue: https://github.com/ncrvoyix-swt-retail/edge-roadmap/issues/13462 return s.SecretService.DeleteExternalSecret(ctx, secretName, pxe.PXENamespace, cluster.ProjectID, cluster, clusterInfra, secretName) } // New returns an instance of the EdgeNode ActivationCode service func New(sqlDB *sql.DB, terminalSvc services.TerminalService, storeSvc services.StoreClusterService, clusterConfigSvc services.ClusterConfigService, secretSvc services.SecretService, gcpSvc services.GCPService, bannerSvc services.BannerService) common.ActivationCode { return &service{ SQLDB: sqlDB, TerminalService: terminalSvc, StoreClusterService: storeSvc, ClusterConfigService: clusterConfigSvc, SecretService: secretSvc, GCPService: gcpSvc, BannerService: bannerSvc, } }