package edgebsl import ( "context" "database/sql" "encoding/json" "errors" "fmt" "io" "net/http" "os" "strconv" "strings" "time" "github.com/gin-contrib/requestid" "github.com/gin-gonic/gin" server "edge-infra.dev/pkg/x/tonic" "edge-infra.dev/pkg/edge/api/middleware" "edge-infra.dev/pkg/lib/gcp/cloudsql" "github.com/go-logr/logr" "edge-infra.dev/pkg/edge/api/graph/model" "edge-infra.dev/pkg/edge/api/services" btypes "edge-infra.dev/pkg/edge/api/types" "edge-infra.dev/pkg/edge/bsl" bslMetrics "edge-infra.dev/pkg/edge/bsl-reconciler/metrics" "edge-infra.dev/pkg/edge/constants" secretMgrApi "edge-infra.dev/pkg/lib/gcp/secretmanager" gcputils "edge-infra.dev/pkg/lib/gcp/utils" logger "edge-infra.dev/pkg/lib/logging" "edge-infra.dev/pkg/lib/runtime/manager" "edge-infra.dev/pkg/lib/runtime/metrics" ) type BSL struct { logger logr.Logger metrics *bslMetrics.Metrics secretManagerProvider btypes.SecretManagerFunc bslConfig BslConfig db *sql.DB TenantService services.TenantService } const ( DefaultReconcileInterval = "60" alreadyExistsLogFormat = "%s for org %s" SubscriptionOrgNameAttrKey = "OnboardedOrganization" // Username and Password TODO will remove Username = "username" Password = "password" ) func RunManager(ctx context.Context) error { log := logger.NewLogger().Logger cfg, _, err := NewConfig(os.Args) if err != nil { log.Error(err, "failed to parse startup configuration") os.Exit(1) } secretManagerProvider := func(ctx context.Context, projectID string) (btypes.SecretManagerService, error) { return secretMgrApi.NewWithOptions(ctx, projectID) } startManager(ctx, cfg, secretManagerProvider, log, metrics.DefaultBindAddress) return nil } func startManager(ctx context.Context, cfg BslConfig, secretManagerProvider btypes.SecretManagerFunc, log logr.Logger, port string) { mgr, err := manager.New(manager.Options{MetricsBindAddress: port}) if err != nil { log.Error(err, "failed to create a new manager") os.Exit(1) } dbConnection, err := DB(cfg, log) if err != nil { log.Error(err, "An error occurred creating sql db connection") os.Exit(1) } bslOperations := New(secretManagerProvider, log, cfg, dbConnection) if err != nil { log.Error(err, "failed to create a bsl ops") os.Exit(1) } if err := mgr.Add(bslOperations); err != nil { log.Error(err, "failed to add bsl ops") os.Exit(1) } bslServer := server.NewWithOptions(cfg.Port, cfg.Mode). SetLogger(log). SetMiddlewares(middleware.SetRequestContext(), requestid.New(), middleware.RequestLogger(log), gin.Recovery(), server.CorsMiddleware()). WithHealthRoute(). WithReadyRoute(). SetRoutes(server.Route{ Path: "/new-org-created/", Action: http.MethodPost, Handlers: []gin.HandlerFunc{ gin.BasicAuth(gin.Accounts{ cfg.BasicAuthCreds.Username: cfg.BasicAuthCreds.Password, }), func(c *gin.Context) { payload, err := io.ReadAll(c.Request.Body) if err != nil { log.Error(err, "Error reading BSL new org payload data") } else { payloadJSONStr := string(payload) log.Info(fmt.Sprintf("BSL Organization Created: %s", payloadJSONStr)) bslSubscriptionPostPayload := &BSLSubscriptionPayload{} if err := json.Unmarshal(payload, bslSubscriptionPostPayload); err != nil { log.Error(err, "Error de-coding BSL new org JSON payload data") } var OrgName string for _, val := range bslSubscriptionPostPayload.Attributes { if val.Key == SubscriptionOrgNameAttrKey { OrgName = val.Value } } orgToProcess := &AllEdgeOrgsPageContent{ OrganizationName: OrgName, ID: "", DisplayName: "", Parent: true, } tenantsNames := bslOperations.GetTenantsList(ctx) sctmrg, _ := secretMgrApi.NewWithOptions(ctx, cfg.TopLevelProjectID) log.Info("Starting org processing for: ", orgToProcess.OrganizationName) err := bslOperations.processEdgeOrganization(c, orgToProcess, sctmrg, tenantsNames) if err != nil { log.Error(err, "Error processing organization") } c.IndentedJSON(http.StatusOK, orgToProcess) } }}, }).With404Route() if err := mgr.Add(bslServer); err != nil { log.Error(err, "failed to add bsl server") os.Exit(1) } if err := mgr.Start(ctx); err != nil { log.Error(err, "bsl errored") os.Exit(1) } } func New(secretManagerProvider btypes.SecretManagerFunc, log logr.Logger, cfg BslConfig, dbConnection *sql.DB) *BSL { m := bslMetrics.NewMetrics() m.InitMetrics() b := &BSL{ logger: log, metrics: m, secretManagerProvider: secretManagerProvider, bslConfig: cfg, db: dbConnection, TenantService: services.NewTenantService(dbConnection, nil, nil), } return b } func (b *BSL) Start(ctx context.Context) error { reconcileInterval, err := strconv.Atoi(b.bslConfig.BslReconcilesInterval) if err != nil { return fmt.Errorf("unable to convert BSL_RECONCILE_INTERVAL, %v", err) } if err = b.Reconcile(ctx); err != nil { return fmt.Errorf("unable to reconcile, %v", err) } for { select { case <-time.After(time.Duration(reconcileInterval) * time.Minute): err = b.Reconcile(ctx) if err != nil { return fmt.Errorf("unable to reconcile, %v", err) } case <-ctx.Done(): return nil } } } // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. func (b *BSL) Reconcile(ctx context.Context) error { log := b.logger.WithValues("bsl operator", "reconcile") b.metrics.ReconcileInc() tenantsNames := b.GetTenantsList(ctx) allOrgsList, err := b.bslConfig.GetAllEdgeOrgs(ctx) if err != nil { log.Error(err, "failed to get bsl edge orgs") b.metrics.ErrorInc("bsl_error", "", err.Error()) return err } sm, err := b.secretManagerProvider(ctx, b.bslConfig.TopLevelProjectID) if err != nil { log.Error(err, "fail to create new secret manager api", "project", b.bslConfig.TopLevelProjectID) b.metrics.ErrorInc("kube_error", "", err.Error()) return err } defer gcputils.CloseConnection(ctx, sm) return b.processAllEdgeOrganizations(ctx, allOrgsList, sm, tenantsNames) } func (b *BSL) GetTenantsList(ctx context.Context) map[string]struct{} { log := b.logger.WithValues("bsl operator", "Get Tenants List") tenants, err := b.TenantService.List(ctx) if err != nil { log.Error(err, "An error occurred getting tenants info") b.metrics.ErrorInc("sql_error", "", err.Error()) } tenantsNames := make(map[string]struct{}, 0) for _, tenant := range tenants { tenantsNames[tenant.OrgName] = struct{}{} } return tenantsNames } func (b *BSL) CreateUserAndSecret(ctx context.Context, sm btypes.SecretManagerService, orgName string, bslConfig *BslConfig, log logr.Logger, secretName string, bffuser bool) error { //check to see if the secret exists in the edge-system namespace _, err := sm.GetSecret(ctx, secretName) if err != nil && strings.Contains(err.Error(), "not found") { //nolint b.metrics.NewOrgInc(secretName) err = nil username := newBSLUsername(6, bffuser) password, bslerr := bslConfig.CreateBSLUser(ctx, orgName, username) if bslerr != nil { log.Error(bslerr, LogWithSecret(secretName, "failed to create bsl user")) return fmt.Errorf("fail to create new bff user and password, error; %v", bslerr) } //create secret in edge-system, if the secret and user does not already exist // create shared and secret key for user res, err := bslConfig.CreateBSLUserAccessKey(ctx, orgName, username) if err != nil { log.Error(err, LogWithSecret(secretName, "failed to create access key for user")) return fmt.Errorf("failed to create access key for user, error; %v", bslerr) } if err := createUpdateSecretManagerSecret(ctx, sm, secretName, username, password, res.SharedKey, res.SecretKey); err != nil { log.Error(err, LogWithSecret(secretName, "failed to create bsl org secret in secret manager")) return err } if bffuser { //assign bff user to EDGE_SUPER_ADMIN if err := bslConfig.AssignBSLUserToGroup(ctx, orgName, string(model.RoleEdgeSuperAdmin), username); err != nil { if !errors.Is(err, ErrorResourceAlreadyExists) { log.Error(err, LogWithSecret(secretName, "failed to assign bsl user to EDGE_SUPER_ADMIN")) } else { log.Info(LogWithSecret(secretName, "failed to assign bsl user to EDGE_SUPER_ADMIN, the user is already assigned to EDGE_SUPER_ADMIN")) } } } else { //assign bff user to EDGE_ORG_ADMIN if err := bslConfig.AssignBSLUserToGroup(ctx, orgName, string(model.RoleEdgeOrgAdmin), username); err != nil { if !errors.Is(err, ErrorResourceAlreadyExists) { log.Error(err, LogWithSecret(secretName, "failed to assign bsl user to EDGE_ORG_ADMIN")) } else { log.Info(LogWithSecret(secretName, "failed to assign bsl user to EDGE_ORG_ADMIN, the user is already assigned to EDGE_ORG_ADMIN")) } } } } return err } func createUpdateSecretManagerSecret(ctx context.Context, sm btypes.SecretManagerService, name, username, password, sharedKey, secretKey string) error { data, err := json.Marshal(map[string]string{ Username: username, Password: password, bsl.SharedKey: sharedKey, bsl.SecretKey: secretKey, }) if err != nil { return err } labels := map[string]string{ secretMgrApi.SecretLabel: "platform", secretMgrApi.SecretTypeLabel: "bsl-secret", secretMgrApi.SecretOwnerLabel: "edge", secretMgrApi.SecretNamespaceSelectorLabel: string(constants.PlatformNamespaceSelector), } return sm.AddSecret(ctx, name, data, labels, true, nil, "") } func DB(config BslConfig, log logr.Logger) (*sql.DB, error) { dbConnection, err := cloudsql.GCPPostgresConnection(config.SQL.ConnectionName). DBName(config.SQL.DatabaseName). Password(config.SQL.Password). Username(config.SQL.User). NewConnection() if err != nil { log.Error(err, "failed to create sql db connection") } return dbConnection, err }