package resolver import ( "context" "encoding/json" "fmt" "net/http" "regexp" "strings" "github.com/rs/zerolog/log" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "edge-infra.dev/pkg/edge/api/graph/mapper" "edge-infra.dev/pkg/edge/api/graph/model" "edge-infra.dev/pkg/edge/api/graphqlhelpers" "edge-infra.dev/pkg/edge/api/middleware" "edge-infra.dev/pkg/edge/api/services" "edge-infra.dev/pkg/edge/api/utils" "edge-infra.dev/pkg/edge/constants" ctypes "edge-infra.dev/pkg/edge/constants/api/cluster" "edge-infra.dev/pkg/edge/constants/api/fleet" palletconst "edge-infra.dev/pkg/edge/constants/api/pallet" "edge-infra.dev/pkg/edge/externalsecrets" "edge-infra.dev/pkg/edge/flux/bootstrap" "edge-infra.dev/pkg/edge/k8objectsutils" "edge-infra.dev/pkg/f8n/warehouse" ff "edge-infra.dev/pkg/lib/featureflag" edgeiam "edge-infra.dev/pkg/lib/gcp/iam" "edge-infra.dev/pkg/lib/gcp/iam/roles" "edge-infra.dev/pkg/lib/runtime/version" "edge-infra.dev/pkg/lib/uuid" "edge-infra.dev/pkg/sds/k8s/bootstrap/tokens" ) func (r *mutationResolver) beginBootstrapCluster(ctx context.Context, payload model.BootstrapPayload) (*model.BootstrapResponse, error) { //nolint:gocyclo if err := r.checkEdgeBootstrapClusterEdgeID(ctx, payload.ClusterEdgeID); err != nil { return nil, err } if err := r.TerminalService.RemoveClusterEdgeBootstrapToken(ctx, payload.ClusterEdgeID); err != nil { return nil, err } //get project id var ( err error projectID string cluster *model.Cluster ) //todo remove the old cluster id option when rest of infra is updated if payload.ClusterEdgeID != "" { cluster, err = r.StoreClusterService.GetCluster(ctx, payload.ClusterEdgeID) } else { return nil, err } if err != nil { log.Ctx(ctx).Err(err).Msg("error getting cluster from edge db. was the cluster registered?") return nil, err } clusterActive := cluster.Active != nil && *cluster.Active forceBootstrap := payload.Force != nil && *payload.Force if clusterActive && !forceBootstrap { return nil, fmt.Errorf("cluster is already bootstrapped, to bootstrap again use the `force` flag") } labelKeys, err := r.LabelService.GetEdgeClusterLabelKeys(ctx, cluster.ClusterEdgeID) if err != nil { log.Ctx(ctx).Err(err).Msg("error getting cluster label keys: fleet/cluster type") return nil, err } fleetType, err := mapper.ToFleetType(labelKeys) if err != nil { log.Ctx(ctx).Err(err).Msg("error getting cluster label keys: fleet/cluster type") return nil, err } clusterType, err := mapper.ToClusterType(labelKeys) if err != nil { log.Ctx(ctx).Err(err).Msg("error getting cluster label keys: fleet/cluster type") return nil, err } //cluster infra (for old banners) and banner infra clusters always go to foreman project if fleetType == fleet.Banner { projectID = r.Config.Bff.TopLevelProjectID } else { projectID = cluster.ProjectID } var secrets []string clusterPath := cluster.ClusterEdgeID log.Ctx(ctx).Debug().Msgf("bootstrap api called %v", payload) //create external secret SA extSecretString, err := r.createExternalSecretSA(ctx, cluster, projectID) if err != nil { return nil, err } secrets = append(secrets, extSecretString) //create flux SA if clusterType != ctypes.GKE { fluxSAString, err := r.createFluxSA(ctx, cluster, projectID) if err != nil { return nil, err } secrets = append(secrets, fluxSAString) } //create lumper iam sa//create flux SA if clusterType != ctypes.GKE { lumperSAString, err := r.createLumperSA(ctx, cluster, projectID, r.Config.Bff.TopLevelProjectID) if err != nil { return nil, err } else if lumperSAString != "" { secrets = append(secrets, lumperSAString) } } //create warehouse docker pull for lumper dps, err := r.getDockerPullSecret(ctx, warehouse.WarehouseNamespace) if err != nil { return nil, err } secrets = append(secrets, dps) //create flux config bucketBuilder := bootstrap.BucketFluxConfig(). Name(constants.EdgeBucketName). Namespace(constants.FluxEdgeNamespace). BucketName(projectID). ForCluster(clusterPath) if clusterType != ctypes.GKE { bucketBuilder = bucketBuilder.SecretName(constants.TenantBucketSecret) } bucket := bucketBuilder.Build() fluxBucket, err := json.Marshal(bucket) if err != nil { err := fmt.Errorf("error marshaling flux bucket: %w", err) log.Ctx(ctx).Err(err).Msg("flux config marshaling failed") return nil, err } fluxBucketString := string(fluxBucket) //create kustomize file for cluster kustomizations, err := services.CreateKustomizations(ctx, constants.EdgeBucketName, clusterPath, cluster.FleetVersion) if err != nil { return nil, err } err = r.RegistrationService.UpdateClusterSQLEntry(ctx, true, cluster.ClusterEdgeID) if err != nil { err := fmt.Errorf("error updating cluster entry with active = true in sql: %w", err) log.Ctx(ctx).Err(err).Msg("cluster entry failed") return nil, err } if payload.ClusterCaHash != nil { // validate cluster ca hash if validHash, _ := regexp.MatchString("^[A-Fa-f0-9]{64}$", *payload.ClusterCaHash); !validHash { return nil, fmt.Errorf("cluster CA Hash is not valid") } err := r.RegistrationService.UploadClusterCaHash(ctx, cluster.ClusterEdgeID, *payload.ClusterCaHash) if err != nil { err = fmt.Errorf("error upload cluster ca hash: %w", err) return nil, err } } // these packages must be listed in config/instance//package-lock.yaml in order for `:latest` tags // to get updated in non-dev environments bsStrings, err := r.BootstrapService.GetManifests(ctx, clusterType, palletconst.BootstrapPallets, cluster) if err != nil { return nil, err } // get any namespaces needed before bootstrapping namespaces, err := r.getBootstrapNamespaces(constants.FluxEdgeNamespace) if err != nil { return nil, err } bsStrings = append(bsStrings, namespaces...) bsStrings = append(bsStrings, secrets...) bsStrings = append(bsStrings, fluxBucketString) bsStrings = append(bsStrings, kustomizations...) bootstrapResponse := &model.BootstrapResponse{ Secrets: secrets, ProjectID: &projectID, FluxConfig: &model.FluxBootstrapResponse{ FluxBucket: []string{fluxBucketString}, FluxKustomize: kustomizations, }, InstallManifests: bsStrings, } // for dsds cluster get the node agent pallet and other required resources for // installation before bootstrapping to configure egress gateway and bandwidth limiting if clusterType == ctypes.DSDS { preBootstrapManifests, err := r.getDSDSPreBootstrapManifests(ctx, cluster) if err != nil { return nil, err } bootstrapResponse.PreBootstrapManifests = preBootstrapManifests preBootstrapStaticManifests, err := r.BootstrapService.GetManifests( ctx, ctypes.DSDS, palletconst.PreBootstrapStaticPallets, cluster, ) if err != nil { return nil, err } bootstrapResponse.PreBootstrapStaticManifests = preBootstrapStaticManifests } return bootstrapResponse, nil } func (r *mutationResolver) getBootstrapNamespaces(namespaces ...string) ([]string, error) { var nsStrings []string if namespaces == nil { return nsStrings, nil } for _, namespace := range namespaces { ns, err := json.Marshal(&corev1.Namespace{ TypeMeta: metav1.TypeMeta{ Kind: "Namespace", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: namespace, }, }) if err != nil { return nil, err } nsStrings = append(nsStrings, string(ns)) } return nsStrings, nil } func (r *mutationResolver) getDSDSPreBootstrapManifests(ctx context.Context, cluster *model.Cluster) ([]*string, error) { //any pre bootstrap manifests that need deploying var ( preBootstrapManifests []*string ienCRs []*string ) // get any prebootstrapping namespaces namespaces, err := r.getBootstrapNamespaces(constants.SdsNamespace, constants.SpegelNamespace) if err != nil { return nil, err } nadps, err := r.getDockerPullSecret(ctx, constants.SdsNamespace) if err != nil { return nil, err } // these packages must be listed in config/instance//package-lock.yaml in order for `:latest` tags // to get updated in non-dev environments // the following is a list of pallets that need to be be applied before bootstrapping a cluster pbPallets, err := r.BootstrapService.GetManifests( ctx, ctypes.DSDS, palletconst.PreBootstrapPallets, cluster, ) if err != nil { return nil, err } // get IENode CR clusterNetworkServices, err := r.StoreClusterService.GetClusterNetworkServices(ctx, cluster.ClusterEdgeID) if err != nil { return nil, err } terminals, err := r.TerminalService.GetTerminals(ctx, &cluster.ClusterEdgeID, nil) if err != nil { return nil, err } for _, terminal := range terminals { customLabels, err := r.getTerminalCustomLabels(ctx, terminal.TerminalID) if err != nil { return nil, err } dsdsIENode := mapper.TerminalToIENode(terminal, clusterNetworkServices, customLabels, cluster.FleetVersion) bits, err := json.Marshal(dsdsIENode) if err != nil { return nil, err } ienCrString := string(bits) ienCRs = append(ienCRs, &ienCrString) } //get cluster config clusterConfig, err := r.ClusterConfigService.GetClusterConfig(ctx, cluster.ClusterEdgeID) if err != nil { return nil, err } topologyConfigMap := mapper.CreateTopologyInfoConfigMap(*clusterConfig) cfgBits, err := json.Marshal(topologyConfigMap) if err != nil { return nil, err } topologyConfigMapString := string(cfgBits) // get artifact registries and create spegel-config artifactRegistries, err := r.ArtifactRegistryService.GetArtifactRegistriesForCluster(ctx, cluster.ClusterEdgeID) if err != nil { return nil, err } spegelConfig := mapper.CreateSpegelConfigMap(artifactRegistries) cfgBits, err = json.Marshal(spegelConfig) if err != nil { return nil, err } spegelConfigString := string(cfgBits) for i := range namespaces { preBootstrapManifests = append(preBootstrapManifests, &namespaces[i]) } preBootstrapManifests = append(preBootstrapManifests, &nadps) preBootstrapManifests = append(preBootstrapManifests, &topologyConfigMapString) preBootstrapManifests = append(preBootstrapManifests, &spegelConfigString) for i := range pbPallets { preBootstrapManifests = append(preBootstrapManifests, &pbPallets[i]) } preBootstrapManifests = append(preBootstrapManifests, ienCRs...) return preBootstrapManifests, nil } func (r *mutationResolver) createExternalSecretSA(ctx context.Context, cluster *model.Cluster, projectID string) (string, error) { var ( // externalSecretSADisplayName is the service account display name for external secrets. externalSecretSADisplayName = fmt.Sprintf("%s Kubernetes External Secrets", cluster.Name) // externalSecretSADescription is the service account description for external secrets. externalSecretSADescription = "Used by external-secrets for a specific cluster." // name of the service account saName = fmt.Sprintf("ext-sec-%s", uuid.FromUUID(cluster.ClusterEdgeID).Hash()) ) extSecretIAM, err := r.IAMService.CreateSAandSAKey(ctx, projectID, saName, externalSecretSADisplayName, externalSecretSADescription) if err != nil { err := fmt.Errorf("error creating iam SA for external secrets: %w", err) log.Ctx(ctx).Err(err).Msg("external secret SA creation failed") return "", err } extSecretString, err := r.BootstrapService.GetSAKeySecret(externalsecrets.SecretName, externalsecrets.SecretNamespace, externalsecrets.SecretKey, extSecretIAM.APIKey.PrivateKeyData) if err != nil { err := fmt.Errorf("error creating iam SA key for external secrets: %w", err) log.Ctx(ctx).Err(err).Msg("building SA Key secret for external secret failed") return "", err } return extSecretString, nil } // todo refactor this to only grant flux read permissions on bucket func (r *mutationResolver) createFluxSA(ctx context.Context, cluster *model.Cluster, projectID string) (string, error) { var ( // fluxSADescription is the service account description for flux. fluxSADescription = "Service account for Flux Edge GKE bootstrapper" // fluxSADisplayName is the service account display name for flux. fluxSADisplayName = fmt.Sprintf("%s Flux edge storage bucket accessor", cluster.ClusterEdgeID) // getFluxRole is the iam role for flux to get a gcp bucket and objects. getFluxRole = fmt.Sprintf("projects/%s/roles/%s", projectID, bootstrap.FluxRoleGet) // listFluxRole is the iam role for flux to list objects in a gcp bucket. listFluxRole = fmt.Sprintf("projects/%s/roles/%s", projectID, bootstrap.FluxRoleList) // name of the service account saName = fmt.Sprintf("flux-%s", uuid.FromUUID(cluster.ClusterEdgeID).Hash()) ) fluxIAM, err := r.IAMService.CreateSAandSAKey(ctx, projectID, saName, fluxSADisplayName, fluxSADescription) if err != nil { err := fmt.Errorf("error creating iam resources for flux: %w", err) log.Ctx(ctx).Err(err).Msg("flux SA creation failed") return "", err } fluxIAM, err = r.createEdgeIAMInSingleProject(ctx, projectID, saName, fluxIAM, []services.IAMRole{services.NewIamRole(getFluxRole), services.NewIamRole(listFluxRole)}) if err != nil { err := fmt.Errorf("error update iam resources for flux: %w", err) log.Ctx(ctx).Err(err).Msg("flux SA policy update failed") return "", err } fluxSAString, err := r.BootstrapService.GetSAKeySecret(constants.TenantBucketSecret, constants.FluxEdgeNamespace, constants.FluxSAKey, fluxIAM.APIKey.PrivateKeyData) if err != nil { err := fmt.Errorf("error creating iam SA key for flux: %w", err) log.Ctx(ctx).Err(err).Msg("building SA Key secret for flux failed") return "", err } return fluxSAString, err } func (r *mutationResolver) createLumperSA(ctx context.Context, cluster *model.Cluster, projectID string, foremanProjectID string) (string, error) { lumperSADescription := "Service account for lumper ctl" hash := uuid.FromUUID(cluster.ClusterEdgeID).Hash() saName := fmt.Sprintf("lumperctl-%s", hash) lumperDisplayName := fmt.Sprintf("%s OCI controller", hash) isGKECluster := ctypes.GetClusterTypeFromLabels(cluster.Labels).IsGKE() //roles for lumper rolesForLumper := getRoleForLumper(isGKECluster) //create service account lumperIAM, err := r.IAMService.CreateSAandSAKey(ctx, projectID, saName, lumperDisplayName, lumperSADescription) if err != nil { err := fmt.Errorf("error creating iam resources for lumper: %w", err) log.Ctx(ctx).Err(err).Msg("lumper SA creation failed") return "", err } foremanArtifactory, err := ff.FeatureEnabled(ff.ForemanArtifactory, false) if err != nil { log.Ctx(ctx).Err(err).Msg(fmt.Sprintf("unable to get ld flag for foreman-artifactory. using default value %v.", false)) } //if foremanArtifactory is true, update the foreman project policy binding to add the new lumper account if foremanArtifactory { lumperIAM, err = r.IAMService.CreateOrUpdateEdgeIAM(ctx, projectID, foremanProjectID, saName, lumperIAM, rolesForLumper) if err != nil { err := fmt.Errorf("error creating policy binding for lumper: %w", err) log.Ctx(ctx).Err(err).Msg("binding foreman project artifact registry read role for lumper SA failed") return "", err } } else { lumperIAM, err = r.createEdgeIAMInSingleProject(ctx, projectID, saName, lumperIAM, rolesForLumper) if err != nil { err := fmt.Errorf("error creating policy binding for lumper: %w", err) log.Ctx(ctx).Err(err).Msg("binding banner project artifact registry read role for lumper SA failed") return "", err } } //Only create secret key is the type is not gke (wi will be used for gke clusters) //TODO other secrets will need to be updated to do this as they are installed using pallets like lumper if !isGKECluster { lumperSAString, err := r.BootstrapService.GetSAKeySecret(warehouse.ServiceAccountKey, warehouse.WarehouseNamespace, warehouse.SecretKey, lumperIAM.APIKey.PrivateKeyData) if err != nil { err := fmt.Errorf("error creating iam SA key for lumper: %w", err) log.Ctx(ctx).Err(err).Msg("building SA Key secret for lumper failed") return "", err } return lumperSAString, err } return "", err } func (r *Resolver) getDockerPullSecret(ctx context.Context, ns string) (string, error) { dp := constants.DockerPullCfg dps, err := r.GCPService.GetSecrets(ctx, &dp, nil, nil, true, r.Config.Bff.TopLevelProjectID) if err != nil { return "", err } secret := corev1.Secret{ TypeMeta: metav1.TypeMeta{ Kind: "Secret", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: constants.EdgeDockerSecret, Namespace: ns, }, Type: corev1.SecretTypeDockerConfigJson, Data: map[string][]byte{corev1.DockerConfigJsonKey: []byte(dps[0].Values[0].Value)}, } secretBits, err := json.Marshal(secret) if err != nil { return "", err } secretString := string(secretBits) return secretString, nil } func getRoleForLumper(isGKECluster bool) []services.IAMRole { rolesForLumper := []services.IAMRole{services.NewIamRole(roles.ArtifactoryReader)} if isGKECluster { rolesForLumper = append(rolesForLumper, services.NewWIIamRole(roles.WorkloadIdentityUser, warehouse.WarehouseNamespace, "lumperctl")) } return rolesForLumper } // beginTerminalBootstrap takes the potentially empty MAC addresses and checks for any matches with the MAC // addresses registered with the terminal. If no addresses were provided or there was a match then we execute // terminal bootstrap and return the TerminalBootstrap object with an error. Otherwise, we return a 400 status // code and an error without consuming the activation code. func (r *queryResolver) beginTerminalBootstrap(ctx context.Context, macAddresses []string, edgeOSVersion *string) (*model.TerminalBootstrap, error) { macAddresses, err := utils.FormatMacAddresses(macAddresses) if err != nil { return nil, err } terminalID, ok := ctx.Value(middleware.TerminalIDCtxKey).(string) if !ok { return nil, fmt.Errorf("could not retrieve terminalID from context") } getLabel := false terminal, err := r.TerminalService.GetTerminal(ctx, terminalID, &getLabel) if err != nil { return nil, err } macAddressMatch, err := utils.MatchTerminalMacAddresses(macAddresses, terminal) if err != nil { return nil, err } //check compatibility before terminal is bootstrapped cluster, err := r.StoreClusterService.GetCluster(ctx, terminal.ClusterEdgeID) if err != nil { return nil, err } //nolint:goconst if edgeOSVersion != nil && !(strings.HasSuffix(*edgeOSVersion, "-dev") || strings.Contains(*edgeOSVersion, "-rc")) { if cluster.FleetVersion == "latest" || cluster.FleetVersion == "" { cluster.FleetVersion = version.New().SemVer } infraBaseVersion, edgeOSMinorVersion := utils.SetVersionToBaseAndMinorVersion(&cluster.FleetVersion, edgeOSVersion) edgeOSArtifact := constants.EdgeOSArtifact compatibleOSVersions, err := r.CompatibilityService.GetArtifactVersionCompatibility(ctx, model.ArtifactVersion{Name: fleet.Store, Version: infraBaseVersion}, &edgeOSArtifact) if err != nil { return nil, err } if len(compatibleOSVersions.CompatibleArtifacts) == 0 { return nil, fmt.Errorf("the edge-os compatibility for %s does not exist in the current environment", cluster.FleetVersion) } if edgeOSMinorVersion != compatibleOSVersions.CompatibleArtifacts[0].Version { 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) } } if macAddresses == nil || macAddressMatch { return r.execTerminalBootstrap(ctx, terminal) } return nil, fmt.Errorf(`status code %d: the MAC address of this terminal does not match the MAC registered for this activation code - please check the activation code and try again`, http.StatusBadRequest) } func (r *queryResolver) execTerminalBootstrap(ctx context.Context, terminal *model.Terminal) (*model.TerminalBootstrap, error) { cluster, err := r.StoreClusterService.GetCluster(ctx, terminal.ClusterEdgeID) if err != nil { return nil, err } clusterInfra, err := r.BannerService.GetClusterInfraInfo(ctx, cluster.BannerEdgeID) if err != nil { return nil, err } tenant, err := r.TenantService.GetTenantByBannerID(ctx, cluster.BannerEdgeID) if err != nil { return nil, err } breakglassSecret, err := r.fetchClusterSecret(ctx, cluster.ClusterEdgeID, model.ClusterSecretTypeBreakglass, "latest") if err != nil { return nil, err } grubSecret, err := r.fetchClusterSecret(ctx, cluster.ClusterEdgeID, model.ClusterSecretTypeGrub, "latest") if err != nil { return nil, err } clusterNetworkServices, err := r.StoreClusterService.GetClusterNetworkServices(ctx, cluster.ClusterEdgeID) if err != nil { return nil, err } // First node cannot be a worker node if !*cluster.Active && terminal.Role == model.TerminalRoleTypeWorker { return nil, fmt.Errorf("cluster is not active and cannot join worker node %s first", terminal.Hostname) } // To be first node, cluster must be inactive and joining terminal must be a control-plane node isFirstNode := terminalIsFirstNode(cluster, terminal) bootstrapAck := true clusterConfig, _ := r.ClusterConfig(ctx, cluster.ClusterEdgeID) if clusterConfig != nil { bootstrapAck = clusterConfig.BootstrapAck } clusterCaHash, bootstrapTokenValues, err := r.getWorkerNodeConfig(ctx, cluster, isFirstNode) if err != nil { return nil, err } endpoint := r.Config.EdgeAPIEndpoint config, err := r.TerminalService.GetTerminalBootstrapConfig( ctx, terminal, clusterNetworkServices, breakglassSecret, grubSecret, bootstrapAck, isFirstNode, tenant.OrgName, bootstrapTokenValues, clusterCaHash, endpoint, ) if err != nil { return nil, err } // activation code has been used, mark it as empty string in DB // and delete the Secret/ExternalSecret resources if err := r.ActivationCodeService.MarkUsed(ctx, terminal.TerminalID, cluster, clusterInfra); err != nil { return nil, err } //log installation.yaml log.Ctx(ctx).Info().Msg(sanitizeTerminalBootstrapConfig(config)) return &model.TerminalBootstrap{ Configuration: config, }, nil } func (r *queryResolver) generateBootstrapToken(ctx context.Context, cluster *model.Cluster) ([]*model.KeyValues, error) { owner := string(constants.EdgeNamespaceSelector) bootstrapTokenValues, expireAt, err := tokens.GenerateBootstrapJoinToken() if err != nil { return nil, err } secretPrefix := "bootstrap-token" secretName := k8objectsutils.NameWithPrefix(secretPrefix, bootstrapTokenValues[0].Value) workload := string(constants.TenantNamespaceSelector) // Create secret manager secret err = r.GCPService.AddSecret(ctx, secretName, owner, secretPrefix, bootstrapTokenValues, cluster.ProjectID, &workload, expireAt) if err != nil { return nil, err } // create entry in DB so clusterctl can create relevant SyncedObject containing ExternalSecret if err := r.BootstrapService.CreateClusterBootstrapTokenEntry(ctx, cluster.ClusterEdgeID, secretName, *expireAt); err != nil { return nil, err } return bootstrapTokenValues, nil } // To be first node, cluster must be inactive and joining terminal must be a control-plane node func terminalIsFirstNode(cluster *model.Cluster, terminal *model.Terminal) bool { isControlPlane := terminal.Role == model.TerminalRoleTypeControlplane isInactive := !*cluster.Active return isControlPlane && isInactive } func (r *queryResolver) getWorkerNodeConfig(ctx context.Context, cluster *model.Cluster, isFirstNode bool) (string, []*model.KeyValues, error) { var clusterCaHash string var bootstrapTokenValues []*model.KeyValues var err error // Generate bootstrap token for subsequent node and retrieve cluster ca hash if !isFirstNode { bootstrapTokenValues, err = r.generateBootstrapToken(ctx, cluster) if err != nil { return "", nil, err } clusterCaHash, err = r.RegistrationService.ClusterCaHash(ctx, cluster.ClusterEdgeID) if err != nil { return "", nil, err } if clusterCaHash == "" { return "", nil, fmt.Errorf("cannot bootstrap new nodes until first node has joined (cluster ca hash missing)") } } return clusterCaHash, bootstrapTokenValues, nil } func (r *mutationResolver) checkEdgeBootstrapClusterEdgeID(ctx context.Context, clusterEdgeID string) error { edgeBootstrapTokenClusterEdgeID, ok := ctx.Value(middleware.ClusterEdgeIDCtxKey).(string) if !ok { return nil } if clusterEdgeID != edgeBootstrapTokenClusterEdgeID { return fmt.Errorf("edge bootstrap token was not matched to cluster") } return nil } func (r *mutationResolver) createEdgeIAMInSingleProject(ctx context.Context, projectID, serviceAccountName string, componentIAM *edgeiam.Component, roles []services.IAMRole) (*edgeiam.Component, error) { EdgeIAM, err := r.IAMService.CreateOrUpdateEdgeIAM(ctx, projectID, projectID, serviceAccountName, componentIAM, roles) if err != nil { return nil, err } return EdgeIAM, nil } // sanitizeTerminalBootstrapConfig sanitizes sensitive information from the terminal func sanitizeTerminalBootstrapConfig(config string) string { //sensitive yaml tags from the TerminalBootstrapConfig sensitiveFields := []string{ "zylevel0_hash", //breakGlass "grub_hash", "grub_user", "token", //bootstrap token "clusterCaHash", } pattern := regexp.MustCompile(`(?i)(` + strings.Join(sensitiveFields, "|") + `)(:\s*).*`) return pattern.ReplaceAllString(config, fmt.Sprintf("$1$2%s", graphqlhelpers.SensitiveMask)) }