...

Source file src/edge-infra.dev/pkg/edge/api/graph/resolver/bootstrap_queries_orchestrator.go

Documentation: edge-infra.dev/pkg/edge/api/graph/resolver

     1  package resolver
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net/http"
     8  	"regexp"
     9  	"strings"
    10  
    11  	"github.com/rs/zerolog/log"
    12  	corev1 "k8s.io/api/core/v1"
    13  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  
    15  	"edge-infra.dev/pkg/edge/api/graph/mapper"
    16  	"edge-infra.dev/pkg/edge/api/graph/model"
    17  	"edge-infra.dev/pkg/edge/api/graphqlhelpers"
    18  	"edge-infra.dev/pkg/edge/api/middleware"
    19  	"edge-infra.dev/pkg/edge/api/services"
    20  	"edge-infra.dev/pkg/edge/api/utils"
    21  	"edge-infra.dev/pkg/edge/constants"
    22  	ctypes "edge-infra.dev/pkg/edge/constants/api/cluster"
    23  
    24  	"edge-infra.dev/pkg/edge/constants/api/fleet"
    25  	palletconst "edge-infra.dev/pkg/edge/constants/api/pallet"
    26  	"edge-infra.dev/pkg/edge/externalsecrets"
    27  	"edge-infra.dev/pkg/edge/flux/bootstrap"
    28  	"edge-infra.dev/pkg/edge/k8objectsutils"
    29  	"edge-infra.dev/pkg/f8n/warehouse"
    30  	ff "edge-infra.dev/pkg/lib/featureflag"
    31  	edgeiam "edge-infra.dev/pkg/lib/gcp/iam"
    32  	"edge-infra.dev/pkg/lib/gcp/iam/roles"
    33  	"edge-infra.dev/pkg/lib/runtime/version"
    34  	"edge-infra.dev/pkg/lib/uuid"
    35  	"edge-infra.dev/pkg/sds/k8s/bootstrap/tokens"
    36  )
    37  
    38  func (r *mutationResolver) beginBootstrapCluster(ctx context.Context, payload model.BootstrapPayload) (*model.BootstrapResponse, error) { //nolint:gocyclo
    39  	if err := r.checkEdgeBootstrapClusterEdgeID(ctx, payload.ClusterEdgeID); err != nil {
    40  		return nil, err
    41  	}
    42  
    43  	if err := r.TerminalService.RemoveClusterEdgeBootstrapToken(ctx, payload.ClusterEdgeID); err != nil {
    44  		return nil, err
    45  	}
    46  
    47  	//get project id
    48  	var (
    49  		err       error
    50  		projectID string
    51  		cluster   *model.Cluster
    52  	)
    53  	//todo remove the old cluster id option when rest of infra is updated
    54  	if payload.ClusterEdgeID != "" {
    55  		cluster, err = r.StoreClusterService.GetCluster(ctx, payload.ClusterEdgeID)
    56  	} else {
    57  		return nil, err
    58  	}
    59  	if err != nil {
    60  		log.Ctx(ctx).Err(err).Msg("error getting cluster from edge db. was the cluster registered?")
    61  		return nil, err
    62  	}
    63  	clusterActive := cluster.Active != nil && *cluster.Active
    64  	forceBootstrap := payload.Force != nil && *payload.Force
    65  	if clusterActive && !forceBootstrap {
    66  		return nil, fmt.Errorf("cluster is already bootstrapped, to bootstrap again use the `force` flag")
    67  	}
    68  	labelKeys, err := r.LabelService.GetEdgeClusterLabelKeys(ctx, cluster.ClusterEdgeID)
    69  	if err != nil {
    70  		log.Ctx(ctx).Err(err).Msg("error getting cluster label keys: fleet/cluster type")
    71  		return nil, err
    72  	}
    73  	fleetType, err := mapper.ToFleetType(labelKeys)
    74  	if err != nil {
    75  		log.Ctx(ctx).Err(err).Msg("error getting cluster label keys: fleet/cluster type")
    76  		return nil, err
    77  	}
    78  	clusterType, err := mapper.ToClusterType(labelKeys)
    79  	if err != nil {
    80  		log.Ctx(ctx).Err(err).Msg("error getting cluster label keys: fleet/cluster type")
    81  		return nil, err
    82  	}
    83  	//cluster infra (for old banners) and banner infra clusters always go to foreman project
    84  	if fleetType == fleet.Banner {
    85  		projectID = r.Config.Bff.TopLevelProjectID
    86  	} else {
    87  		projectID = cluster.ProjectID
    88  	}
    89  
    90  	var secrets []string
    91  	clusterPath := cluster.ClusterEdgeID
    92  
    93  	log.Ctx(ctx).Debug().Msgf("bootstrap api called %v", payload)
    94  
    95  	//create external secret SA
    96  	extSecretString, err := r.createExternalSecretSA(ctx, cluster, projectID)
    97  	if err != nil {
    98  		return nil, err
    99  	}
   100  	secrets = append(secrets, extSecretString)
   101  
   102  	//create flux SA
   103  	if clusterType != ctypes.GKE {
   104  		fluxSAString, err := r.createFluxSA(ctx, cluster, projectID)
   105  		if err != nil {
   106  			return nil, err
   107  		}
   108  		secrets = append(secrets, fluxSAString)
   109  	}
   110  
   111  	//create lumper iam sa//create flux SA
   112  	if clusterType != ctypes.GKE {
   113  		lumperSAString, err := r.createLumperSA(ctx, cluster, projectID, r.Config.Bff.TopLevelProjectID)
   114  		if err != nil {
   115  			return nil, err
   116  		} else if lumperSAString != "" {
   117  			secrets = append(secrets, lumperSAString)
   118  		}
   119  	}
   120  	//create warehouse docker pull for lumper
   121  	dps, err := r.getDockerPullSecret(ctx, warehouse.WarehouseNamespace)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  	secrets = append(secrets, dps)
   126  
   127  	//create flux config
   128  	bucketBuilder := bootstrap.BucketFluxConfig().
   129  		Name(constants.EdgeBucketName).
   130  		Namespace(constants.FluxEdgeNamespace).
   131  		BucketName(projectID).
   132  		ForCluster(clusterPath)
   133  	if clusterType != ctypes.GKE {
   134  		bucketBuilder = bucketBuilder.SecretName(constants.TenantBucketSecret)
   135  	}
   136  	bucket := bucketBuilder.Build()
   137  	fluxBucket, err := json.Marshal(bucket)
   138  	if err != nil {
   139  		err := fmt.Errorf("error marshaling flux bucket: %w", err)
   140  		log.Ctx(ctx).Err(err).Msg("flux config marshaling failed")
   141  		return nil, err
   142  	}
   143  	fluxBucketString := string(fluxBucket)
   144  
   145  	//create kustomize file for cluster
   146  	kustomizations, err := services.CreateKustomizations(ctx, constants.EdgeBucketName, clusterPath, cluster.FleetVersion)
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  
   151  	err = r.RegistrationService.UpdateClusterSQLEntry(ctx, true, cluster.ClusterEdgeID)
   152  	if err != nil {
   153  		err := fmt.Errorf("error updating cluster entry with active = true in sql: %w", err)
   154  		log.Ctx(ctx).Err(err).Msg("cluster entry failed")
   155  		return nil, err
   156  	}
   157  
   158  	if payload.ClusterCaHash != nil {
   159  		// validate cluster ca hash
   160  		if validHash, _ := regexp.MatchString("^[A-Fa-f0-9]{64}$", *payload.ClusterCaHash); !validHash {
   161  			return nil, fmt.Errorf("cluster CA Hash is not valid")
   162  		}
   163  		err := r.RegistrationService.UploadClusterCaHash(ctx, cluster.ClusterEdgeID, *payload.ClusterCaHash)
   164  		if err != nil {
   165  			err = fmt.Errorf("error upload cluster ca hash: %w", err)
   166  			return nil, err
   167  		}
   168  	}
   169  
   170  	// these packages must be listed in config/instance/<env>/package-lock.yaml in order for `:latest` tags
   171  	// to get updated in non-dev environments
   172  	bsStrings, err := r.BootstrapService.GetManifests(ctx, clusterType, palletconst.BootstrapPallets, cluster)
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  
   177  	// get any namespaces needed before bootstrapping
   178  	namespaces, err := r.getBootstrapNamespaces(constants.FluxEdgeNamespace)
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  
   183  	bsStrings = append(bsStrings, namespaces...)
   184  	bsStrings = append(bsStrings, secrets...)
   185  	bsStrings = append(bsStrings, fluxBucketString)
   186  	bsStrings = append(bsStrings, kustomizations...)
   187  
   188  	bootstrapResponse := &model.BootstrapResponse{
   189  		Secrets:   secrets,
   190  		ProjectID: &projectID,
   191  		FluxConfig: &model.FluxBootstrapResponse{
   192  			FluxBucket:    []string{fluxBucketString},
   193  			FluxKustomize: kustomizations,
   194  		},
   195  		InstallManifests: bsStrings,
   196  	}
   197  
   198  	// for dsds cluster get the node agent pallet and other required resources for
   199  	// installation before bootstrapping to configure egress gateway and bandwidth limiting
   200  	if clusterType == ctypes.DSDS {
   201  		preBootstrapManifests, err := r.getDSDSPreBootstrapManifests(ctx, cluster)
   202  		if err != nil {
   203  			return nil, err
   204  		}
   205  		bootstrapResponse.PreBootstrapManifests = preBootstrapManifests
   206  
   207  		preBootstrapStaticManifests, err := r.BootstrapService.GetManifests(
   208  			ctx,
   209  			ctypes.DSDS,
   210  			palletconst.PreBootstrapStaticPallets,
   211  			cluster,
   212  		)
   213  		if err != nil {
   214  			return nil, err
   215  		}
   216  
   217  		bootstrapResponse.PreBootstrapStaticManifests = preBootstrapStaticManifests
   218  	}
   219  
   220  	return bootstrapResponse, nil
   221  }
   222  
   223  func (r *mutationResolver) getBootstrapNamespaces(namespaces ...string) ([]string, error) {
   224  	var nsStrings []string
   225  	if namespaces == nil {
   226  		return nsStrings, nil
   227  	}
   228  	for _, namespace := range namespaces {
   229  		ns, err := json.Marshal(&corev1.Namespace{
   230  			TypeMeta: metav1.TypeMeta{
   231  				Kind:       "Namespace",
   232  				APIVersion: "v1",
   233  			},
   234  			ObjectMeta: metav1.ObjectMeta{
   235  				Name: namespace,
   236  			},
   237  		})
   238  		if err != nil {
   239  			return nil, err
   240  		}
   241  		nsStrings = append(nsStrings, string(ns))
   242  	}
   243  	return nsStrings, nil
   244  }
   245  
   246  func (r *mutationResolver) getDSDSPreBootstrapManifests(ctx context.Context, cluster *model.Cluster) ([]*string, error) {
   247  	//any pre bootstrap manifests that need deploying
   248  	var (
   249  		preBootstrapManifests []*string
   250  		ienCRs                []*string
   251  	)
   252  	// get any prebootstrapping namespaces
   253  	namespaces, err := r.getBootstrapNamespaces(constants.SdsNamespace, constants.SpegelNamespace)
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  
   258  	nadps, err := r.getDockerPullSecret(ctx, constants.SdsNamespace)
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  
   263  	// these packages must be listed in config/instance/<env>/package-lock.yaml in order for `:latest` tags
   264  	// to get updated in non-dev environments
   265  	// the following is a list of pallets that need to be be applied before bootstrapping a cluster
   266  	pbPallets, err := r.BootstrapService.GetManifests(
   267  		ctx,
   268  		ctypes.DSDS,
   269  		palletconst.PreBootstrapPallets,
   270  		cluster,
   271  	)
   272  	if err != nil {
   273  		return nil, err
   274  	}
   275  
   276  	// get IENode CR
   277  	clusterNetworkServices, err := r.StoreClusterService.GetClusterNetworkServices(ctx, cluster.ClusterEdgeID)
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  	terminals, err := r.TerminalService.GetTerminals(ctx, &cluster.ClusterEdgeID, nil)
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  
   286  	for _, terminal := range terminals {
   287  		customLabels, err := r.getTerminalCustomLabels(ctx, terminal.TerminalID)
   288  		if err != nil {
   289  			return nil, err
   290  		}
   291  		dsdsIENode := mapper.TerminalToIENode(terminal, clusterNetworkServices, customLabels, cluster.FleetVersion)
   292  		bits, err := json.Marshal(dsdsIENode)
   293  		if err != nil {
   294  			return nil, err
   295  		}
   296  		ienCrString := string(bits)
   297  		ienCRs = append(ienCRs, &ienCrString)
   298  	}
   299  
   300  	//get cluster config
   301  	clusterConfig, err := r.ClusterConfigService.GetClusterConfig(ctx, cluster.ClusterEdgeID)
   302  	if err != nil {
   303  		return nil, err
   304  	}
   305  
   306  	topologyConfigMap := mapper.CreateTopologyInfoConfigMap(*clusterConfig)
   307  	cfgBits, err := json.Marshal(topologyConfigMap)
   308  	if err != nil {
   309  		return nil, err
   310  	}
   311  	topologyConfigMapString := string(cfgBits)
   312  
   313  	// get artifact registries and create spegel-config
   314  	artifactRegistries, err := r.ArtifactRegistryService.GetArtifactRegistriesForCluster(ctx, cluster.ClusterEdgeID)
   315  	if err != nil {
   316  		return nil, err
   317  	}
   318  	spegelConfig := mapper.CreateSpegelConfigMap(artifactRegistries)
   319  	cfgBits, err = json.Marshal(spegelConfig)
   320  	if err != nil {
   321  		return nil, err
   322  	}
   323  	spegelConfigString := string(cfgBits)
   324  
   325  	for i := range namespaces {
   326  		preBootstrapManifests = append(preBootstrapManifests, &namespaces[i])
   327  	}
   328  	preBootstrapManifests = append(preBootstrapManifests, &nadps)
   329  	preBootstrapManifests = append(preBootstrapManifests, &topologyConfigMapString)
   330  	preBootstrapManifests = append(preBootstrapManifests, &spegelConfigString)
   331  	for i := range pbPallets {
   332  		preBootstrapManifests = append(preBootstrapManifests, &pbPallets[i])
   333  	}
   334  
   335  	preBootstrapManifests = append(preBootstrapManifests, ienCRs...)
   336  	return preBootstrapManifests, nil
   337  }
   338  
   339  func (r *mutationResolver) createExternalSecretSA(ctx context.Context, cluster *model.Cluster, projectID string) (string, error) {
   340  	var (
   341  		// externalSecretSADisplayName is the service account display name for external secrets.
   342  		externalSecretSADisplayName = fmt.Sprintf("%s Kubernetes External Secrets", cluster.Name)
   343  		// externalSecretSADescription is the service account description for external secrets.
   344  		externalSecretSADescription = "Used by external-secrets for a specific cluster."
   345  		// name of the service account
   346  		saName = fmt.Sprintf("ext-sec-%s", uuid.FromUUID(cluster.ClusterEdgeID).Hash())
   347  	)
   348  
   349  	extSecretIAM, err := r.IAMService.CreateSAandSAKey(ctx, projectID, saName, externalSecretSADisplayName, externalSecretSADescription)
   350  	if err != nil {
   351  		err := fmt.Errorf("error creating iam SA for external secrets: %w", err)
   352  		log.Ctx(ctx).Err(err).Msg("external secret SA creation failed")
   353  		return "", err
   354  	}
   355  	extSecretString, err := r.BootstrapService.GetSAKeySecret(externalsecrets.SecretName, externalsecrets.SecretNamespace, externalsecrets.SecretKey, extSecretIAM.APIKey.PrivateKeyData)
   356  	if err != nil {
   357  		err := fmt.Errorf("error creating iam SA key for external secrets: %w", err)
   358  		log.Ctx(ctx).Err(err).Msg("building SA Key secret for external secret failed")
   359  		return "", err
   360  	}
   361  	return extSecretString, nil
   362  }
   363  
   364  // todo refactor this to only grant flux read permissions on bucket
   365  func (r *mutationResolver) createFluxSA(ctx context.Context, cluster *model.Cluster, projectID string) (string, error) {
   366  	var (
   367  
   368  		// fluxSADescription is the service account description for flux.
   369  		fluxSADescription = "Service account for Flux Edge GKE bootstrapper"
   370  		// fluxSADisplayName is the service account display name for flux.
   371  		fluxSADisplayName = fmt.Sprintf("%s Flux edge storage bucket accessor", cluster.ClusterEdgeID)
   372  		// getFluxRole is the iam role for flux to get a gcp bucket and objects.
   373  		getFluxRole = fmt.Sprintf("projects/%s/roles/%s", projectID, bootstrap.FluxRoleGet)
   374  		// listFluxRole is the iam role for flux to list objects in a gcp bucket.
   375  		listFluxRole = fmt.Sprintf("projects/%s/roles/%s", projectID, bootstrap.FluxRoleList)
   376  		// name of the service account
   377  		saName = fmt.Sprintf("flux-%s", uuid.FromUUID(cluster.ClusterEdgeID).Hash())
   378  	)
   379  	fluxIAM, err := r.IAMService.CreateSAandSAKey(ctx, projectID, saName, fluxSADisplayName, fluxSADescription)
   380  	if err != nil {
   381  		err := fmt.Errorf("error creating iam resources for flux: %w", err)
   382  		log.Ctx(ctx).Err(err).Msg("flux SA creation failed")
   383  		return "", err
   384  	}
   385  	fluxIAM, err = r.createEdgeIAMInSingleProject(ctx, projectID, saName, fluxIAM, []services.IAMRole{services.NewIamRole(getFluxRole), services.NewIamRole(listFluxRole)})
   386  	if err != nil {
   387  		err := fmt.Errorf("error update iam resources for flux: %w", err)
   388  		log.Ctx(ctx).Err(err).Msg("flux SA policy update failed")
   389  		return "", err
   390  	}
   391  	fluxSAString, err := r.BootstrapService.GetSAKeySecret(constants.TenantBucketSecret, constants.FluxEdgeNamespace, constants.FluxSAKey, fluxIAM.APIKey.PrivateKeyData)
   392  	if err != nil {
   393  		err := fmt.Errorf("error creating iam SA key for flux: %w", err)
   394  		log.Ctx(ctx).Err(err).Msg("building SA Key secret for flux failed")
   395  		return "", err
   396  	}
   397  	return fluxSAString, err
   398  }
   399  
   400  func (r *mutationResolver) createLumperSA(ctx context.Context, cluster *model.Cluster, projectID string, foremanProjectID string) (string, error) {
   401  	lumperSADescription := "Service account for lumper ctl"
   402  	hash := uuid.FromUUID(cluster.ClusterEdgeID).Hash()
   403  	saName := fmt.Sprintf("lumperctl-%s", hash)
   404  	lumperDisplayName := fmt.Sprintf("%s OCI controller", hash)
   405  	isGKECluster := ctypes.GetClusterTypeFromLabels(cluster.Labels).IsGKE()
   406  
   407  	//roles for lumper
   408  	rolesForLumper := getRoleForLumper(isGKECluster)
   409  
   410  	//create service account
   411  	lumperIAM, err := r.IAMService.CreateSAandSAKey(ctx, projectID, saName, lumperDisplayName, lumperSADescription)
   412  	if err != nil {
   413  		err := fmt.Errorf("error creating iam resources for lumper: %w", err)
   414  		log.Ctx(ctx).Err(err).Msg("lumper SA creation failed")
   415  		return "", err
   416  	}
   417  
   418  	foremanArtifactory, err := ff.FeatureEnabled(ff.ForemanArtifactory, false)
   419  	if err != nil {
   420  		log.Ctx(ctx).Err(err).Msg(fmt.Sprintf("unable to get ld flag for foreman-artifactory. using default value %v.", false))
   421  	}
   422  
   423  	//if foremanArtifactory is true, update the foreman project policy binding to add the new lumper account
   424  	if foremanArtifactory {
   425  		lumperIAM, err = r.IAMService.CreateOrUpdateEdgeIAM(ctx, projectID, foremanProjectID, saName, lumperIAM, rolesForLumper)
   426  		if err != nil {
   427  			err := fmt.Errorf("error creating policy binding for lumper: %w", err)
   428  			log.Ctx(ctx).Err(err).Msg("binding foreman project artifact registry read role for lumper SA failed")
   429  			return "", err
   430  		}
   431  	} else {
   432  		lumperIAM, err = r.createEdgeIAMInSingleProject(ctx, projectID, saName, lumperIAM, rolesForLumper)
   433  		if err != nil {
   434  			err := fmt.Errorf("error creating policy binding for lumper: %w", err)
   435  			log.Ctx(ctx).Err(err).Msg("binding banner project artifact registry read role for lumper SA failed")
   436  			return "", err
   437  		}
   438  	}
   439  
   440  	//Only create secret key is the type is not gke (wi will be used for gke clusters)
   441  	//TODO other secrets will need to be updated to do this as they are installed using pallets like lumper
   442  	if !isGKECluster {
   443  		lumperSAString, err := r.BootstrapService.GetSAKeySecret(warehouse.ServiceAccountKey, warehouse.WarehouseNamespace, warehouse.SecretKey, lumperIAM.APIKey.PrivateKeyData)
   444  		if err != nil {
   445  			err := fmt.Errorf("error creating iam SA key for lumper: %w", err)
   446  			log.Ctx(ctx).Err(err).Msg("building SA Key secret for lumper failed")
   447  			return "", err
   448  		}
   449  		return lumperSAString, err
   450  	}
   451  	return "", err
   452  }
   453  
   454  func (r *Resolver) getDockerPullSecret(ctx context.Context, ns string) (string, error) {
   455  	dp := constants.DockerPullCfg
   456  	dps, err := r.GCPService.GetSecrets(ctx, &dp, nil, nil, true, r.Config.Bff.TopLevelProjectID)
   457  	if err != nil {
   458  		return "", err
   459  	}
   460  	secret := corev1.Secret{
   461  		TypeMeta: metav1.TypeMeta{
   462  			Kind:       "Secret",
   463  			APIVersion: "v1",
   464  		},
   465  		ObjectMeta: metav1.ObjectMeta{
   466  			Name:      constants.EdgeDockerSecret,
   467  			Namespace: ns,
   468  		},
   469  		Type: corev1.SecretTypeDockerConfigJson,
   470  		Data: map[string][]byte{corev1.DockerConfigJsonKey: []byte(dps[0].Values[0].Value)},
   471  	}
   472  	secretBits, err := json.Marshal(secret)
   473  	if err != nil {
   474  		return "", err
   475  	}
   476  	secretString := string(secretBits)
   477  	return secretString, nil
   478  }
   479  
   480  func getRoleForLumper(isGKECluster bool) []services.IAMRole {
   481  	rolesForLumper := []services.IAMRole{services.NewIamRole(roles.ArtifactoryReader)}
   482  	if isGKECluster {
   483  		rolesForLumper = append(rolesForLumper, services.NewWIIamRole(roles.WorkloadIdentityUser, warehouse.WarehouseNamespace, "lumperctl"))
   484  	}
   485  	return rolesForLumper
   486  }
   487  
   488  // beginTerminalBootstrap takes the potentially empty MAC addresses and checks for any matches with the MAC
   489  // addresses registered with the terminal. If no addresses were provided or there was a match then we execute
   490  // terminal bootstrap and return the TerminalBootstrap object with an error. Otherwise, we return a 400 status
   491  // code and an error without consuming the activation code.
   492  func (r *queryResolver) beginTerminalBootstrap(ctx context.Context, macAddresses []string, edgeOSVersion *string) (*model.TerminalBootstrap, error) {
   493  	macAddresses, err := utils.FormatMacAddresses(macAddresses)
   494  	if err != nil {
   495  		return nil, err
   496  	}
   497  
   498  	terminalID, ok := ctx.Value(middleware.TerminalIDCtxKey).(string)
   499  	if !ok {
   500  		return nil, fmt.Errorf("could not retrieve terminalID from context")
   501  	}
   502  
   503  	getLabel := false
   504  
   505  	terminal, err := r.TerminalService.GetTerminal(ctx, terminalID, &getLabel)
   506  	if err != nil {
   507  		return nil, err
   508  	}
   509  
   510  	macAddressMatch, err := utils.MatchTerminalMacAddresses(macAddresses, terminal)
   511  	if err != nil {
   512  		return nil, err
   513  	}
   514  
   515  	//check compatibility before terminal is bootstrapped
   516  	cluster, err := r.StoreClusterService.GetCluster(ctx, terminal.ClusterEdgeID)
   517  	if err != nil {
   518  		return nil, err
   519  	}
   520  
   521  	//nolint:goconst
   522  	if edgeOSVersion != nil && !(strings.HasSuffix(*edgeOSVersion, "-dev") || strings.Contains(*edgeOSVersion, "-rc")) {
   523  		if cluster.FleetVersion == "latest" || cluster.FleetVersion == "" {
   524  			cluster.FleetVersion = version.New().SemVer
   525  		}
   526  		infraBaseVersion, edgeOSMinorVersion := utils.SetVersionToBaseAndMinorVersion(&cluster.FleetVersion, edgeOSVersion)
   527  		edgeOSArtifact := constants.EdgeOSArtifact
   528  		compatibleOSVersions, err := r.CompatibilityService.GetArtifactVersionCompatibility(ctx, model.ArtifactVersion{Name: fleet.Store, Version: infraBaseVersion}, &edgeOSArtifact)
   529  		if err != nil {
   530  			return nil, err
   531  		}
   532  		if len(compatibleOSVersions.CompatibleArtifacts) == 0 {
   533  			return nil, fmt.Errorf("the edge-os compatibility for  %s does not exist in the current environment", cluster.FleetVersion)
   534  		}
   535  		if edgeOSMinorVersion != compatibleOSVersions.CompatibleArtifacts[0].Version {
   536  			return nil, fmt.Errorf("the ien version %s cannot be installed against store infra version %s, any patch version of the release  %s of the ien must be used", *edgeOSVersion, cluster.FleetVersion, compatibleOSVersions.CompatibleArtifacts[0].Version)
   537  		}
   538  	}
   539  
   540  	if macAddresses == nil || macAddressMatch {
   541  		return r.execTerminalBootstrap(ctx, terminal)
   542  	}
   543  
   544  	return nil, fmt.Errorf(`status code %d: the MAC address of this terminal does not match the MAC registered for this activation
   545  		code - please check the activation code and try again`, http.StatusBadRequest)
   546  }
   547  
   548  func (r *queryResolver) execTerminalBootstrap(ctx context.Context, terminal *model.Terminal) (*model.TerminalBootstrap, error) {
   549  	cluster, err := r.StoreClusterService.GetCluster(ctx, terminal.ClusterEdgeID)
   550  	if err != nil {
   551  		return nil, err
   552  	}
   553  
   554  	clusterInfra, err := r.BannerService.GetClusterInfraInfo(ctx, cluster.BannerEdgeID)
   555  	if err != nil {
   556  		return nil, err
   557  	}
   558  
   559  	tenant, err := r.TenantService.GetTenantByBannerID(ctx, cluster.BannerEdgeID)
   560  	if err != nil {
   561  		return nil, err
   562  	}
   563  
   564  	breakglassSecret, err := r.fetchClusterSecret(ctx, cluster.ClusterEdgeID, model.ClusterSecretTypeBreakglass, "latest")
   565  	if err != nil {
   566  		return nil, err
   567  	}
   568  
   569  	grubSecret, err := r.fetchClusterSecret(ctx, cluster.ClusterEdgeID, model.ClusterSecretTypeGrub, "latest")
   570  	if err != nil {
   571  		return nil, err
   572  	}
   573  
   574  	clusterNetworkServices, err := r.StoreClusterService.GetClusterNetworkServices(ctx, cluster.ClusterEdgeID)
   575  	if err != nil {
   576  		return nil, err
   577  	}
   578  
   579  	// First node cannot be a worker node
   580  	if !*cluster.Active && terminal.Role == model.TerminalRoleTypeWorker {
   581  		return nil, fmt.Errorf("cluster is not active and cannot join worker node %s first", terminal.Hostname)
   582  	}
   583  
   584  	// To be first node, cluster must be inactive and joining terminal must be a control-plane node
   585  	isFirstNode := terminalIsFirstNode(cluster, terminal)
   586  
   587  	bootstrapAck := true
   588  	clusterConfig, _ := r.ClusterConfig(ctx, cluster.ClusterEdgeID)
   589  	if clusterConfig != nil {
   590  		bootstrapAck = clusterConfig.BootstrapAck
   591  	}
   592  
   593  	clusterCaHash, bootstrapTokenValues, err := r.getWorkerNodeConfig(ctx, cluster, isFirstNode)
   594  	if err != nil {
   595  		return nil, err
   596  	}
   597  
   598  	endpoint := r.Config.EdgeAPIEndpoint
   599  
   600  	config, err := r.TerminalService.GetTerminalBootstrapConfig(
   601  		ctx,
   602  		terminal,
   603  		clusterNetworkServices,
   604  		breakglassSecret,
   605  		grubSecret,
   606  		bootstrapAck,
   607  		isFirstNode,
   608  		tenant.OrgName,
   609  		bootstrapTokenValues,
   610  		clusterCaHash,
   611  		endpoint,
   612  	)
   613  	if err != nil {
   614  		return nil, err
   615  	}
   616  
   617  	// activation code has been used, mark it as empty string in DB
   618  	// and delete the Secret/ExternalSecret resources
   619  	if err := r.ActivationCodeService.MarkUsed(ctx, terminal.TerminalID, cluster, clusterInfra); err != nil {
   620  		return nil, err
   621  	}
   622  
   623  	//log installation.yaml
   624  	log.Ctx(ctx).Info().Msg(sanitizeTerminalBootstrapConfig(config))
   625  
   626  	return &model.TerminalBootstrap{
   627  		Configuration: config,
   628  	}, nil
   629  }
   630  
   631  func (r *queryResolver) generateBootstrapToken(ctx context.Context, cluster *model.Cluster) ([]*model.KeyValues, error) {
   632  	owner := string(constants.EdgeNamespaceSelector)
   633  	bootstrapTokenValues, expireAt, err := tokens.GenerateBootstrapJoinToken()
   634  	if err != nil {
   635  		return nil, err
   636  	}
   637  
   638  	secretPrefix := "bootstrap-token"
   639  	secretName := k8objectsutils.NameWithPrefix(secretPrefix, bootstrapTokenValues[0].Value)
   640  	workload := string(constants.TenantNamespaceSelector)
   641  
   642  	// Create secret manager secret
   643  	err = r.GCPService.AddSecret(ctx, secretName, owner, secretPrefix, bootstrapTokenValues, cluster.ProjectID, &workload, expireAt)
   644  	if err != nil {
   645  		return nil, err
   646  	}
   647  	// create entry in DB so clusterctl can create relevant SyncedObject containing ExternalSecret
   648  	if err := r.BootstrapService.CreateClusterBootstrapTokenEntry(ctx, cluster.ClusterEdgeID, secretName, *expireAt); err != nil {
   649  		return nil, err
   650  	}
   651  
   652  	return bootstrapTokenValues, nil
   653  }
   654  
   655  // To be first node, cluster must be inactive and joining terminal must be a control-plane node
   656  func terminalIsFirstNode(cluster *model.Cluster, terminal *model.Terminal) bool {
   657  	isControlPlane := terminal.Role == model.TerminalRoleTypeControlplane
   658  	isInactive := !*cluster.Active
   659  	return isControlPlane && isInactive
   660  }
   661  
   662  func (r *queryResolver) getWorkerNodeConfig(ctx context.Context, cluster *model.Cluster, isFirstNode bool) (string, []*model.KeyValues, error) {
   663  	var clusterCaHash string
   664  	var bootstrapTokenValues []*model.KeyValues
   665  	var err error
   666  
   667  	// Generate bootstrap token for subsequent node and retrieve cluster ca hash
   668  	if !isFirstNode {
   669  		bootstrapTokenValues, err = r.generateBootstrapToken(ctx, cluster)
   670  		if err != nil {
   671  			return "", nil, err
   672  		}
   673  
   674  		clusterCaHash, err = r.RegistrationService.ClusterCaHash(ctx, cluster.ClusterEdgeID)
   675  		if err != nil {
   676  			return "", nil, err
   677  		}
   678  
   679  		if clusterCaHash == "" {
   680  			return "", nil, fmt.Errorf("cannot bootstrap new nodes until first node has joined (cluster ca hash missing)")
   681  		}
   682  	}
   683  	return clusterCaHash, bootstrapTokenValues, nil
   684  }
   685  
   686  func (r *mutationResolver) checkEdgeBootstrapClusterEdgeID(ctx context.Context, clusterEdgeID string) error {
   687  	edgeBootstrapTokenClusterEdgeID, ok := ctx.Value(middleware.ClusterEdgeIDCtxKey).(string)
   688  	if !ok {
   689  		return nil
   690  	}
   691  	if clusterEdgeID != edgeBootstrapTokenClusterEdgeID {
   692  		return fmt.Errorf("edge bootstrap token was not matched to cluster")
   693  	}
   694  	return nil
   695  }
   696  
   697  func (r *mutationResolver) createEdgeIAMInSingleProject(ctx context.Context, projectID, serviceAccountName string, componentIAM *edgeiam.Component, roles []services.IAMRole) (*edgeiam.Component, error) {
   698  	EdgeIAM, err := r.IAMService.CreateOrUpdateEdgeIAM(ctx, projectID, projectID, serviceAccountName, componentIAM, roles)
   699  	if err != nil {
   700  		return nil, err
   701  	}
   702  	return EdgeIAM, nil
   703  }
   704  
   705  // sanitizeTerminalBootstrapConfig sanitizes sensitive information from the terminal
   706  func sanitizeTerminalBootstrapConfig(config string) string {
   707  	//sensitive yaml tags from the TerminalBootstrapConfig
   708  	sensitiveFields := []string{
   709  		"zylevel0_hash", //breakGlass
   710  		"grub_hash",
   711  		"grub_user",
   712  		"token", //bootstrap token
   713  		"clusterCaHash",
   714  	}
   715  
   716  	pattern := regexp.MustCompile(`(?i)(` + strings.Join(sensitiveFields, "|") + `)(:\s*).*`)
   717  
   718  	return pattern.ReplaceAllString(config, fmt.Sprintf("$1$2%s", graphqlhelpers.SensitiveMask))
   719  }
   720  

View as plain text