package services import ( "context" "database/sql" "encoding/json" "errors" "fmt" "time" iamv1beta1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/iam/v1beta1" "github.com/google/uuid" "github.com/rs/zerolog/log" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" sqlerr "edge-infra.dev/pkg/edge/api/apierror/sql" "edge-infra.dev/pkg/edge/api/clients" "edge-infra.dev/pkg/edge/api/graph/mapper" "edge-infra.dev/pkg/edge/api/graph/model" sqlquery "edge-infra.dev/pkg/edge/api/sql" "edge-infra.dev/pkg/edge/api/types" "edge-infra.dev/pkg/edge/constants" "edge-infra.dev/pkg/edge/flux/bootstrap" "edge-infra.dev/pkg/edge/k8objectsutils/gcp/iamcomponent" ptype "edge-infra.dev/pkg/f8n/warehouse/cluster" "edge-infra.dev/pkg/f8n/warehouse/lift/unpack" "edge-infra.dev/pkg/f8n/warehouse/oci/layer" ) //go:generate mockgen -destination=../mocks/mock_bootstrap_service.go -package=mocks edge-infra.dev/pkg/edge/api/services BootstrapService type BootstrapService interface { GetSAKeySecret(name, namespace, secretKey, saKey string) (string, error) GetManifests(ctx context.Context, clusterType string, pallets []types.Pallet, cluster *model.Cluster) ([]string, error) CreateClusterBootstrapTokenEntry(ctx context.Context, clusterEdgeID string, secretName string, expireAt time.Time) error GetClusterBootstrapTokens(ctx context.Context, clusterEdgeID string) ([]types.ClusterBootstrapToken, error) DeleteExpiredClusterBootstrapTokens(ctx context.Context, clusterEdgeID string) error } type bootstrapService struct { GkeService GkeClient TopLevelProjectID string ArtifactRegistryClient clients.ArtifactRegistryClient SQLDB *sql.DB } // GetClusterFleetVersion gets the tag or digest for the cluster's fleet package. Returns the {fleet_version} portion // of a package of the form {fleet_type}(@|:){fleet_version}, e.g. store[@sha256:digest | :tag] func (s *bootstrapService) GetClusterFleetVersion(ctx context.Context, clusterEdgeID string) (string, error) { var fleetVersion string row := s.SQLDB.QueryRowContext(ctx, sqlquery.GetClusterFleetVersion, clusterEdgeID) err := row.Scan(&fleetVersion) if err != nil { return "", sqlerr.Wrap(err) } return fleetVersion, nil } func CreateKustomizations(ctx context.Context, bucketName, clusterPath, storeVersion string) ([]string, error) { kustomizationList := []string{} kustomization := bootstrap.KustomizeFluxConfig(). Name("flux-config-kustomization"). Namespace(constants.FluxEdgeNamespace). BucketName(bucketName). ForStoreVersion(storeVersion). Path(fmt.Sprintf("./%s/fluxcfg/", clusterPath)). Force(true). Build() fluxKustomization, err := json.Marshal(kustomization) if err != nil { err = fmt.Errorf("error marshaling flux kustomization: %w", err) log.Ctx(ctx).Err(err).Msg("cluster kustomization marshalling failed") return nil, err } fluxKustomizationString := string(fluxKustomization) kustomizationList = append(kustomizationList, fluxKustomizationString) return kustomizationList, nil } func (s *bootstrapService) GetSAKeySecret(name, namespace, secretKey, saKey string) (string, error) { values := []*model.KeyValues{{Key: secretKey, Value: saKey}} secret, err := mapper.ToCreateSecretObject(name, namespace, values) if err != nil { return "", err } monSASecret, err := json.Marshal(secret) if err != nil { return "", err } monSASecretString := string(monSASecret) return monSASecretString, nil } func (s *bootstrapService) CreateAgentRoleOnRegion(ctx context.Context, cluster *types.GkeCluster, agentIAM *iamcomponent.Component, clusterName string) error { runtimeClient, err := s.GkeService.GetRuntimeClient(ctx, cluster) if err != nil { return err } // create rbac to map gcp service account to join request role agentRole := &rbacv1.Role{ ObjectMeta: metav1.ObjectMeta{ Name: "agent-role", Namespace: clusterName, }, Rules: []rbacv1.PolicyRule{ { APIGroups: []string{"edge.ncr.com"}, Resources: []string{"viewrequests", "actionrequests", "stores"}, Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}, }, { APIGroups: []string{"edge.ncr.com"}, Resources: []string{"viewrequests/status", "actionrequests/status", "stores/status"}, Verbs: []string{"create", "get", "update", "list", "watch", "patch", "delete"}, }, }, } agentRole.Kind = "Role" agentRole.APIVersion = rbacv1.SchemeGroupVersion.Group + "/" + rbacv1.SchemeGroupVersion.Version agentrole := agentRole.DeepCopy() _, err = controllerutil.CreateOrUpdate(ctx, runtimeClient, agentrole, func() error { agentrole.Rules = agentRole.Rules return nil }) if err != nil { return err } // get agent service account with updated status key := client.ObjectKeyFromObject(&agentIAM.ServiceAccount) agentSA := &iamv1beta1.IAMServiceAccount{} err = runtimeClient.Get(ctx, key, agentSA) if err != nil { return errors.New("timeout getting data, unable to fetch service account or service account status") } //kind: User agentRoleBinding := &rbacv1.RoleBinding{ ObjectMeta: metav1.ObjectMeta{ Name: "agent-rolebinding", Namespace: clusterName, }, RoleRef: rbacv1.RoleRef{ APIGroup: "rbac.authorization.k8s.io", Kind: "Role", Name: "agent-role", }, Subjects: []rbacv1.Subject{{ Name: *agentSA.Status.Email, Kind: "User", }}, } agentRoleBinding.Kind = "RoleBinding" agentRoleBinding.APIVersion = rbacv1.SchemeGroupVersion.Group + "/" + rbacv1.SchemeGroupVersion.Version agentrolebinding := agentRoleBinding.DeepCopy() _, err = controllerutil.CreateOrUpdate(ctx, runtimeClient, agentrolebinding, func() error { agentrolebinding.Subjects = agentRoleBinding.Subjects return nil }) if err != nil { return err } return nil } // GetManifests gets the bootstrapping manifests for installing apps to get // cluster syncing with edge func (s *bootstrapService) GetManifests(ctx context.Context, clusterType string, pallets []types.Pallet, cluster *model.Cluster) ([]string, error) { opts := []unpack.Option{ unpack.ForProvider(ptype.Provider(clusterType)), unpack.RenderWith(map[string]string{ "gcp_project_id": cluster.ProjectID, "cluster_uuid": cluster.ClusterEdgeID, "foreman_gcp_project_id": s.TopLevelProjectID, }), unpack.ForLayerTypes(layer.Runtime), } fleetVersion, err := s.GetClusterFleetVersion(ctx, cluster.ClusterEdgeID) if err != nil { return nil, err } manifests := []string{} for _, p := range pallets { deploy, err := p.IsDeployable(fleetVersion) if err != nil { return nil, err } if !deploy { continue } a, err := s.ArtifactRegistryClient.Get(s.TopLevelProjectID, p.Name, fleetVersion) if err != nil { return nil, err } lays, err := unpack.Layers(a, opts...) if err != nil { return nil, err } uns, err := lays[0].Unstructured() if err != nil { return nil, err } for _, u := range uns { if !p.IsManifestDeployable(u) { continue } bits, err := json.Marshal(u) if err != nil { return nil, err } manifests = append(manifests, string(bits)) } } return manifests, nil } func (s *bootstrapService) CreateClusterBootstrapTokenEntry(ctx context.Context, clusterEdgeID string, secretName string, expireAt time.Time) error { _, err := s.SQLDB.ExecContext(ctx, sqlquery.CreateClusterBootstrapToken, uuid.NewString(), secretName, clusterEdgeID, expireAt) return err } func (s *bootstrapService) GetClusterBootstrapTokens(ctx context.Context, clusterEdgeID string) ([]types.ClusterBootstrapToken, error) { rows, err := s.SQLDB.QueryContext(ctx, sqlquery.GetClusterBootstrapTokens, clusterEdgeID) if err != nil { return []types.ClusterBootstrapToken{}, sqlerr.Wrap(err) } defer rows.Close() bootstrapTokenList := []types.ClusterBootstrapToken{} for rows.Next() { var bootstrapToken types.ClusterBootstrapToken if err := rows.Scan(&bootstrapToken.SecretName, &bootstrapToken.ExpireAt); err != nil { return []types.ClusterBootstrapToken{}, sqlerr.Wrap(err) } bootstrapTokenList = append(bootstrapTokenList, bootstrapToken) } if err := rows.Err(); err != nil { return nil, sqlerr.Wrap(err) } return bootstrapTokenList, nil } func (s *bootstrapService) DeleteExpiredClusterBootstrapTokens(ctx context.Context, clusterEdgeID string) error { _, err := s.SQLDB.ExecContext(ctx, sqlquery.DeleteExpiredClusterBootstrapTokens, clusterEdgeID) return err } func NewBootstrapService(topLevelProjectID string, artifactRegistryClient clients.ArtifactRegistryClient, sqlDB *sql.DB) *bootstrapService { //nolint stupid return &bootstrapService{ TopLevelProjectID: topLevelProjectID, ArtifactRegistryClient: artifactRegistryClient, SQLDB: sqlDB, } }