portainer/portainer

View on GitHub
api/cmd/portainer/main.go

Summary

Maintainability
D
1 day
Test Coverage
package main

import (
    "context"
    "crypto/sha256"
    "os"
    "path"
    "strings"

    portainer "github.com/portainer/portainer/api"
    "github.com/portainer/portainer/api/apikey"
    "github.com/portainer/portainer/api/build"
    "github.com/portainer/portainer/api/chisel"
    "github.com/portainer/portainer/api/cli"
    "github.com/portainer/portainer/api/crypto"
    "github.com/portainer/portainer/api/database"
    "github.com/portainer/portainer/api/database/boltdb"
    "github.com/portainer/portainer/api/database/models"
    "github.com/portainer/portainer/api/dataservices"
    "github.com/portainer/portainer/api/datastore"
    "github.com/portainer/portainer/api/datastore/migrator"
    "github.com/portainer/portainer/api/demo"
    "github.com/portainer/portainer/api/docker"
    dockerclient "github.com/portainer/portainer/api/docker/client"
    "github.com/portainer/portainer/api/exec"
    "github.com/portainer/portainer/api/filesystem"
    "github.com/portainer/portainer/api/git"
    "github.com/portainer/portainer/api/hostmanagement/openamt"
    "github.com/portainer/portainer/api/http"
    "github.com/portainer/portainer/api/http/proxy"
    kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
    "github.com/portainer/portainer/api/internal/authorization"
    "github.com/portainer/portainer/api/internal/edge"
    "github.com/portainer/portainer/api/internal/edge/edgestacks"
    "github.com/portainer/portainer/api/internal/endpointutils"
    "github.com/portainer/portainer/api/internal/snapshot"
    "github.com/portainer/portainer/api/internal/ssl"
    "github.com/portainer/portainer/api/internal/upgrade"
    "github.com/portainer/portainer/api/jwt"
    "github.com/portainer/portainer/api/kubernetes"
    kubecli "github.com/portainer/portainer/api/kubernetes/cli"
    "github.com/portainer/portainer/api/ldap"
    "github.com/portainer/portainer/api/oauth"
    "github.com/portainer/portainer/api/pendingactions"
    "github.com/portainer/portainer/api/scheduler"
    "github.com/portainer/portainer/api/stacks/deployments"
    "github.com/portainer/portainer/pkg/featureflags"
    "github.com/portainer/portainer/pkg/libhelm"
    "github.com/portainer/portainer/pkg/libstack"
    "github.com/portainer/portainer/pkg/libstack/compose"

    "github.com/gofrs/uuid"
    "github.com/rs/zerolog/log"
)

func initCLI() *portainer.CLIFlags {
    var cliService portainer.CLIService = &cli.Service{}
    flags, err := cliService.ParseFlags(portainer.APIVersion)
    if err != nil {
        log.Fatal().Err(err).Msg("failed parsing flags")
    }

    err = cliService.ValidateFlags(flags)
    if err != nil {
        log.Fatal().Err(err).Msg("failed validating flags")
    }

    return flags
}

func initFileService(dataStorePath string) portainer.FileService {
    fileService, err := filesystem.NewService(dataStorePath, "")
    if err != nil {
        log.Fatal().Err(err).Msg("failed creating file service")
    }

    return fileService
}

func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService portainer.FileService, shutdownCtx context.Context) dataservices.DataStore {
    connection, err := database.NewDatabase("boltdb", *flags.Data, secretKey)
    if err != nil {
        log.Fatal().Err(err).Msg("failed creating database connection")
    }

    if bconn, ok := connection.(*boltdb.DbConnection); ok {
        bconn.MaxBatchSize = *flags.MaxBatchSize
        bconn.MaxBatchDelay = *flags.MaxBatchDelay
        bconn.InitialMmapSize = *flags.InitialMmapSize
    } else {
        log.Fatal().Msg("failed creating database connection: expecting a boltdb database type but a different one was received")
    }

    store := datastore.NewStore(*flags.Data, fileService, connection)
    isNew, err := store.Open()
    if err != nil {
        log.Fatal().Err(err).Msg("failed opening store")
    }

    if *flags.Rollback {
        err := store.Rollback(false)
        if err != nil {
            log.Fatal().Err(err).Msg("failed rolling back")
        }

        log.Info().Msg("exiting rollback")
        os.Exit(0)
    }

    // Init sets some defaults - it's basically a migration
    err = store.Init()
    if err != nil {
        log.Fatal().Err(err).Msg("failed initializing data store")
    }

    if isNew {
        instanceId, err := uuid.NewV4()
        if err != nil {
            log.Fatal().Err(err).Msg("failed generating instance id")
        }

        migratorInstance := migrator.NewMigrator(&migrator.MigratorParameters{})
        migratorCount := migratorInstance.GetMigratorCountOfCurrentAPIVersion()

        // from MigrateData
        v := models.Version{
            SchemaVersion: portainer.APIVersion,
            Edition:       int(portainer.PortainerCE),
            InstanceID:    instanceId.String(),
            MigratorCount: migratorCount,
        }
        store.VersionService.UpdateVersion(&v)

        err = updateSettingsFromFlags(store, flags)
        if err != nil {
            log.Fatal().Err(err).Msg("failed updating settings from flags")
        }
    } else {
        err = store.MigrateData()
        if err != nil {
            log.Fatal().Err(err).Msg("failed migration")
        }
    }

    err = updateSettingsFromFlags(store, flags)
    if err != nil {
        log.Fatal().Err(err).Msg("failed updating settings from flags")
    }

    // this is for the db restore functionality - needs more tests.
    go func() {
        <-shutdownCtx.Done()
        defer connection.Close()
    }()

    return store
}

// checkDBSchemaServerVersionMatch checks if the server version matches the db scehma version
func checkDBSchemaServerVersionMatch(dbStore dataservices.DataStore, serverVersion string, serverEdition int) bool {
    v, err := dbStore.Version().Version()
    if err != nil {
        return false
    }

    return v.SchemaVersion == serverVersion && v.Edition == serverEdition
}

func initComposeStackManager(composeDeployer libstack.Deployer, proxyManager *proxy.Manager) portainer.ComposeStackManager {
    composeWrapper, err := exec.NewComposeStackManager(composeDeployer, proxyManager)
    if err != nil {
        log.Fatal().Err(err).Msg("failed creating compose manager")
    }

    return composeWrapper
}

func initSwarmStackManager(
    assetsPath string,
    configPath string,
    signatureService portainer.DigitalSignatureService,
    fileService portainer.FileService,
    reverseTunnelService portainer.ReverseTunnelService,
    dataStore dataservices.DataStore,
) (portainer.SwarmStackManager, error) {
    return exec.NewSwarmStackManager(assetsPath, configPath, signatureService, fileService, reverseTunnelService, dataStore)
}

func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore dataservices.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, assetsPath string) portainer.KubernetesDeployer {
    return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, assetsPath)
}

func initHelmPackageManager(assetsPath string) (libhelm.HelmPackageManager, error) {
    return libhelm.NewHelmPackageManager(libhelm.HelmConfig{BinaryPath: assetsPath})
}

func initAPIKeyService(datastore dataservices.DataStore) apikey.APIKeyService {
    return apikey.NewAPIKeyService(datastore.APIKeyRepository(), datastore.User())
}

func initJWTService(userSessionTimeout string, dataStore dataservices.DataStore) (portainer.JWTService, error) {
    if userSessionTimeout == "" {
        userSessionTimeout = portainer.DefaultUserSessionTimeout
    }

    jwtService, err := jwt.NewService(userSessionTimeout, dataStore)
    if err != nil {
        return nil, err
    }

    return jwtService, nil
}

func initDigitalSignatureService() portainer.DigitalSignatureService {
    return crypto.NewECDSAService(os.Getenv("AGENT_SECRET"))
}

func initCryptoService() portainer.CryptoService {
    return &crypto.Service{}
}

func initLDAPService() portainer.LDAPService {
    return &ldap.Service{}
}

func initOAuthService() portainer.OAuthService {
    return oauth.NewService()
}

func initGitService(ctx context.Context) portainer.GitService {
    return git.NewService(ctx)
}

func initSSLService(addr, certPath, keyPath string, fileService portainer.FileService, dataStore dataservices.DataStore, shutdownTrigger context.CancelFunc) (*ssl.Service, error) {
    slices := strings.Split(addr, ":")
    host := slices[0]
    if host == "" {
        host = "0.0.0.0"
    }

    sslService := ssl.NewService(fileService, dataStore, shutdownTrigger)

    err := sslService.Init(host, certPath, keyPath)
    if err != nil {
        return nil, err
    }

    return sslService, nil
}

func initDockerClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *dockerclient.ClientFactory {
    return dockerclient.NewClientFactory(signatureService, reverseTunnelService)
}

func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, dataStore dataservices.DataStore, instanceID, addrHTTPS, userSessionTimeout string) (*kubecli.ClientFactory, error) {
    return kubecli.NewClientFactory(signatureService, reverseTunnelService, dataStore, instanceID, addrHTTPS, userSessionTimeout)
}

func initSnapshotService(
    snapshotIntervalFromFlag string,
    dataStore dataservices.DataStore,
    dockerClientFactory *dockerclient.ClientFactory,
    kubernetesClientFactory *kubecli.ClientFactory,
    shutdownCtx context.Context,
    pendingActionsService *pendingactions.PendingActionsService,
) (portainer.SnapshotService, error) {
    dockerSnapshotter := docker.NewSnapshotter(dockerClientFactory)
    kubernetesSnapshotter := kubernetes.NewSnapshotter(kubernetesClientFactory)

    snapshotService, err := snapshot.NewService(snapshotIntervalFromFlag, dataStore, dockerSnapshotter, kubernetesSnapshotter, shutdownCtx, pendingActionsService)
    if err != nil {
        return nil, err
    }

    return snapshotService, nil
}

func initStatus(instanceID string) *portainer.Status {
    return &portainer.Status{
        Version:    portainer.APIVersion,
        InstanceID: instanceID,
    }
}

func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.CLIFlags) error {
    settings, err := dataStore.Settings().Settings()
    if err != nil {
        return err
    }

    if *flags.SnapshotInterval != "" {
        settings.SnapshotInterval = *flags.SnapshotInterval
    }

    if *flags.Logo != "" {
        settings.LogoURL = *flags.Logo
    }

    if *flags.EnableEdgeComputeFeatures {
        settings.EnableEdgeComputeFeatures = *flags.EnableEdgeComputeFeatures
    }

    if *flags.Templates != "" {
        settings.TemplatesURL = *flags.Templates
    }

    if *flags.Labels != nil {
        settings.BlackListedLabels = *flags.Labels
    }

    if agentKey, ok := os.LookupEnv("AGENT_SECRET"); ok {
        settings.AgentSecret = agentKey
    } else {
        settings.AgentSecret = ""
    }

    err = dataStore.Settings().UpdateSettings(settings)
    if err != nil {
        return err
    }

    sslSettings, err := dataStore.SSLSettings().Settings()
    if err != nil {
        return err
    }

    if *flags.HTTPDisabled {
        sslSettings.HTTPEnabled = false
    } else if *flags.HTTPEnabled {
        sslSettings.HTTPEnabled = true
    }

    return dataStore.SSLSettings().UpdateSettings(sslSettings)
}

func loadAndParseKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
    private, public, err := fileService.LoadKeyPair()
    if err != nil {
        return err
    }
    return signatureService.ParseKeyPair(private, public)
}

func generateAndStoreKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
    private, public, err := signatureService.GenerateKeyPair()
    if err != nil {
        return err
    }
    privateHeader, publicHeader := signatureService.PEMHeaders()
    return fileService.StoreKeyPair(private, public, privateHeader, publicHeader)
}

func initKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
    existingKeyPair, err := fileService.KeyPairFilesExist()
    if err != nil {
        log.Fatal().Err(err).Msg("failed checking for existing key pair")
    }

    if existingKeyPair {
        return loadAndParseKeyPair(fileService, signatureService)
    }
    return generateAndStoreKeyPair(fileService, signatureService)
}

func loadEncryptionSecretKey(keyfilename string) []byte {
    content, err := os.ReadFile(path.Join("/run/secrets", keyfilename))
    if err != nil {
        if os.IsNotExist(err) {
            log.Info().Str("filename", keyfilename).Msg("encryption key file not present")
        } else {
            log.Info().Err(err).Msg("error reading encryption key file")
        }

        return nil
    }

    // return a 32 byte hash of the secret (required for AES)
    hash := sha256.Sum256(content)
    return hash[:]
}

func buildServer(flags *portainer.CLIFlags) portainer.Server {
    shutdownCtx, shutdownTrigger := context.WithCancel(context.Background())

    if flags.FeatureFlags != nil {
        featureflags.Parse(*flags.FeatureFlags, portainer.SupportedFeatureFlags)
    }

    fileService := initFileService(*flags.Data)
    encryptionKey := loadEncryptionSecretKey(*flags.SecretKeyName)
    if encryptionKey == nil {
        log.Info().Msg("proceeding without encryption key")
    }

    dataStore := initDataStore(flags, encryptionKey, fileService, shutdownCtx)

    if err := dataStore.CheckCurrentEdition(); err != nil {
        log.Fatal().Err(err).Msg("")
    }

    // check if the db schema version matches with server version
    if !checkDBSchemaServerVersionMatch(dataStore, portainer.APIVersion, int(portainer.Edition)) {
        log.Fatal().Msg("The database schema version does not align with the server version. Please consider reverting to the previous server version or addressing the database migration issue.")
    }

    instanceID, err := dataStore.Version().InstanceID()
    if err != nil {
        log.Fatal().Err(err).Msg("failed getting instance id")
    }

    apiKeyService := initAPIKeyService(dataStore)

    settings, err := dataStore.Settings().Settings()
    if err != nil {
        log.Fatal().Err(err).Msg("")
    }

    jwtService, err := initJWTService(settings.UserSessionTimeout, dataStore)
    if err != nil {
        log.Fatal().Err(err).Msg("failed initializing JWT service")
    }

    ldapService := initLDAPService()

    oauthService := initOAuthService()

    gitService := initGitService(shutdownCtx)

    openAMTService := openamt.NewService()

    cryptoService := initCryptoService()

    digitalSignatureService := initDigitalSignatureService()

    edgeStacksService := edgestacks.NewService(dataStore)

    sslService, err := initSSLService(*flags.AddrHTTPS, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
    if err != nil {
        log.Fatal().Err(err).Msg("")
    }

    sslSettings, err := sslService.GetSSLSettings()
    if err != nil {
        log.Fatal().Err(err).Msg("failed to get SSL settings")
    }

    err = initKeyPair(fileService, digitalSignatureService)
    if err != nil {
        log.Fatal().Err(err).Msg("failed initializing key pair")
    }

    reverseTunnelService := chisel.NewService(dataStore, shutdownCtx, fileService)

    dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
    kubernetesClientFactory, err := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, dataStore, instanceID, *flags.AddrHTTPS, settings.UserSessionTimeout)

    authorizationService := authorization.NewService(dataStore)
    authorizationService.K8sClientFactory = kubernetesClientFactory

    pendingActionsService := pendingactions.NewService(dataStore, kubernetesClientFactory, authorizationService, shutdownCtx)

    snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx, pendingActionsService)
    if err != nil {
        log.Fatal().Err(err).Msg("failed initializing snapshot service")
    }
    snapshotService.Start()

    kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()

    kubeClusterAccessService := kubernetes.NewKubeClusterAccessService(*flags.BaseURL, *flags.AddrHTTPS, sslSettings.CertPath)

    proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService)

    reverseTunnelService.ProxyManager = proxyManager

    dockerConfigPath := fileService.GetDockerConfigPath()

    composeDeployer, err := compose.NewComposeDeployer(*flags.Assets, dockerConfigPath)
    if err != nil {
        log.Fatal().Err(err).Msg("failed initializing compose deployer")
    }

    composeStackManager := initComposeStackManager(composeDeployer, proxyManager)

    swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, digitalSignatureService, fileService, reverseTunnelService, dataStore)
    if err != nil {
        log.Fatal().Err(err).Msg("failed initializing swarm stack manager")
    }

    kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, proxyManager, *flags.Assets)

    helmPackageManager, err := initHelmPackageManager(*flags.Assets)
    if err != nil {
        log.Fatal().Err(err).Msg("failed initializing helm package manager")
    }

    err = edge.LoadEdgeJobs(dataStore, reverseTunnelService)
    if err != nil {
        log.Fatal().Err(err).Msg("failed loading edge jobs from database")
    }

    applicationStatus := initStatus(instanceID)

    demoService := demo.NewService()
    if *flags.DemoEnvironment {
        err := demoService.Init(dataStore, cryptoService)
        if err != nil {
            log.Fatal().Err(err).Msg("failed initializing demo environment")
        }
    }

    // channel to control when the admin user is created
    adminCreationDone := make(chan struct{}, 1)

    go endpointutils.InitEndpoint(shutdownCtx, adminCreationDone, flags, dataStore, snapshotService)

    adminPasswordHash := ""
    if *flags.AdminPasswordFile != "" {
        content, err := fileService.GetFileContent(*flags.AdminPasswordFile, "")
        if err != nil {
            log.Fatal().Err(err).Msg("failed getting admin password file")
        }

        adminPasswordHash, err = cryptoService.Hash(strings.TrimSuffix(string(content), "\n"))
        if err != nil {
            log.Fatal().Err(err).Msg("failed hashing admin password")
        }
    } else if *flags.AdminPassword != "" {
        adminPasswordHash = *flags.AdminPassword
    }

    if adminPasswordHash != "" {
        users, err := dataStore.User().UsersByRole(portainer.AdministratorRole)
        if err != nil {
            log.Fatal().Err(err).Msg("failed getting admin user")
        }

        if len(users) == 0 {
            log.Info().Msg("created admin user with the given password.")
            user := &portainer.User{
                Username: "admin",
                Role:     portainer.AdministratorRole,
                Password: adminPasswordHash,
            }

            err := dataStore.User().Create(user)
            if err != nil {
                log.Fatal().Err(err).Msg("failed creating admin user")
            }

            // notify the admin user is created, the endpoint initialization can start
            adminCreationDone <- struct{}{}
        } else {
            log.Info().Msg("instance already has an administrator user defined, skipping admin password related flags.")
        }
    }

    err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService)
    if err != nil {
        log.Fatal().Err(err).Msg("failed starting tunnel server")
    }

    scheduler := scheduler.NewScheduler(shutdownCtx)
    stackDeployer := deployments.NewStackDeployer(swarmStackManager, composeStackManager, kubernetesDeployer, dockerClientFactory, dataStore)
    deployments.StartStackSchedules(scheduler, stackDeployer, dataStore, gitService)

    sslDBSettings, err := dataStore.SSLSettings().Settings()
    if err != nil {
        log.Fatal().Msg("failed to fetch SSL settings from DB")
    }

    upgradeService, err := upgrade.NewService(*flags.Assets, composeDeployer, kubernetesClientFactory)
    if err != nil {
        log.Fatal().Err(err).Msg("failed initializing upgrade service")
    }

    // Our normal migrations run as part of the database initialization
    // but some more complex migrations require access to a kubernetes or docker
    // client. Therefore we run a separate migration process just before
    // starting the server.
    postInitMigrator := datastore.NewPostInitMigrator(
        kubernetesClientFactory,
        dockerClientFactory,
        dataStore,
    )
    if err := postInitMigrator.PostInitMigrate(); err != nil {
        log.Fatal().Err(err).Msg("failure during post init migrations")
    }

    return &http.Server{
        AuthorizationService:        authorizationService,
        ReverseTunnelService:        reverseTunnelService,
        Status:                      applicationStatus,
        BindAddress:                 *flags.Addr,
        BindAddressHTTPS:            *flags.AddrHTTPS,
        HTTPEnabled:                 sslDBSettings.HTTPEnabled,
        AssetsPath:                  *flags.Assets,
        DataStore:                   dataStore,
        EdgeStacksService:           edgeStacksService,
        SwarmStackManager:           swarmStackManager,
        ComposeStackManager:         composeStackManager,
        KubernetesDeployer:          kubernetesDeployer,
        HelmPackageManager:          helmPackageManager,
        APIKeyService:               apiKeyService,
        CryptoService:               cryptoService,
        JWTService:                  jwtService,
        FileService:                 fileService,
        LDAPService:                 ldapService,
        OAuthService:                oauthService,
        GitService:                  gitService,
        OpenAMTService:              openAMTService,
        ProxyManager:                proxyManager,
        KubernetesTokenCacheManager: kubernetesTokenCacheManager,
        KubeClusterAccessService:    kubeClusterAccessService,
        SignatureService:            digitalSignatureService,
        SnapshotService:             snapshotService,
        SSLService:                  sslService,
        DockerClientFactory:         dockerClientFactory,
        KubernetesClientFactory:     kubernetesClientFactory,
        Scheduler:                   scheduler,
        ShutdownCtx:                 shutdownCtx,
        ShutdownTrigger:             shutdownTrigger,
        StackDeployer:               stackDeployer,
        DemoService:                 demoService,
        UpgradeService:              upgradeService,
        AdminCreationDone:           adminCreationDone,
        PendingActionsService:       pendingActionsService,
    }
}

func main() {
    configureLogger()
    setLoggingMode("PRETTY")

    flags := initCLI()

    setLoggingLevel(*flags.LogLevel)
    setLoggingMode(*flags.LogMode)

    for {
        server := buildServer(flags)
        log.Info().
            Str("version", portainer.APIVersion).
            Str("build_number", build.BuildNumber).
            Str("image_tag", build.ImageTag).
            Str("nodejs_version", build.NodejsVersion).
            Str("yarn_version", build.YarnVersion).
            Str("webpack_version", build.WebpackVersion).
            Str("go_version", build.GoVersion).
            Msg("starting Portainer")

        err := server.Start()
        log.Info().Err(err).Msg("HTTP server exited")
    }
}