zitadel/cmd/admin/start/start.go
Livio Amstutz 389eb4a27a
feat: run on a single port (#3163)
* start v2

* start

* run

* some cleanup

* remove v2 pkg again

* simplify

* webauthn

* remove unused config

* fix login path in Dockerfile

* fix asset_generator.go

* health handler

* fix grpc web

* refactor

* merge

* build new main.go

* run new main.go

* update logging pkg

* fix error msg

* update logging

* cleanup

* cleanup

* go mod tidy

* change localDevMode

* fix customEndpoints

* update logging

* comments

* change local flag to external configs

* fix location generated go code

* fix

Co-authored-by: fforootd <florian@caos.ch>
2022-02-14 17:22:30 +01:00

307 lines
11 KiB
Go

package start
import (
"context"
"database/sql"
_ "embed"
"errors"
"fmt"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/caos/logging"
"github.com/gorilla/mux"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
admin_es "github.com/caos/zitadel/internal/admin/repository/eventsourcing"
"github.com/caos/zitadel/internal/api"
"github.com/caos/zitadel/internal/api/assets"
internal_authz "github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/api/grpc/admin"
"github.com/caos/zitadel/internal/api/grpc/auth"
"github.com/caos/zitadel/internal/api/grpc/management"
http_util "github.com/caos/zitadel/internal/api/http"
"github.com/caos/zitadel/internal/api/http/middleware"
"github.com/caos/zitadel/internal/api/oidc"
"github.com/caos/zitadel/internal/api/ui/console"
"github.com/caos/zitadel/internal/api/ui/login"
auth_es "github.com/caos/zitadel/internal/auth/repository/eventsourcing"
"github.com/caos/zitadel/internal/authz"
authz_repo "github.com/caos/zitadel/internal/authz/repository"
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/database"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/id"
"github.com/caos/zitadel/internal/notification"
"github.com/caos/zitadel/internal/query"
"github.com/caos/zitadel/internal/query/projection"
"github.com/caos/zitadel/internal/static"
static_config "github.com/caos/zitadel/internal/static/config"
"github.com/caos/zitadel/internal/webauthn"
"github.com/caos/zitadel/openapi"
)
func New() *cobra.Command {
start := &cobra.Command{
Use: "start",
Short: "starts ZITADEL instance",
Long: `starts ZITADEL.
Requirements:
- cockroachdb`,
RunE: func(cmd *cobra.Command, args []string) error {
config := new(startConfig)
err := viper.Unmarshal(config, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(":"),
)))
if err != nil {
return err
}
err = config.Log.SetLogger()
if err != nil {
return err
}
return startZitadel(config)
},
}
bindUint16Flag(start, "port", "port to run ZITADEL on")
bindStringFlag(start, "externalDomain", "domain ZITADEL will be exposed on")
bindStringFlag(start, "externalPort", "port ZITADEL will be exposed on")
bindBoolFlag(start, "externalSecure", "if ZITADEL will be served on HTTPS")
return start
}
func bindStringFlag(cmd *cobra.Command, name, description string) {
cmd.PersistentFlags().String(name, viper.GetString(name), description)
viper.BindPFlag(name, cmd.PersistentFlags().Lookup(name))
}
func bindUint16Flag(cmd *cobra.Command, name, description string) {
cmd.PersistentFlags().Uint16(name, uint16(viper.GetUint(name)), description)
viper.BindPFlag(name, cmd.PersistentFlags().Lookup(name))
}
func bindBoolFlag(cmd *cobra.Command, name, description string) {
cmd.PersistentFlags().Bool(name, viper.GetBool(name), description)
viper.BindPFlag(name, cmd.PersistentFlags().Lookup(name))
}
type startConfig struct {
Log *logging.Config
Port uint16
ExternalPort uint16
ExternalDomain string
ExternalSecure bool
Database database.Config
Projections projectionConfig
AuthZ authz.Config
Auth auth_es.Config
Admin admin_es.Config
UserAgentCookie *middleware.UserAgentCookieConfig
OIDC oidc.Config
Login login.Config
Console console.Config
Notification notification.Config
AssetStorage static_config.AssetStorageConfig
InternalAuthZ internal_authz.Config
SystemDefaults systemdefaults.SystemDefaults
}
type projectionConfig struct {
projection.Config
KeyConfig *crypto.KeyConfig
}
func startZitadel(config *startConfig) error {
ctx := context.Background()
keyChan := make(chan interface{})
dbClient, err := database.Connect(config.Database)
if err != nil {
return fmt.Errorf("cannot start client for projection: %w", err)
}
var storage static.Storage
//TODO: enable when storage is implemented again
//if *assetsEnabled {
//storage, err = config.AssetStorage.Config.NewStorage()
//logging.Log("MAIN-Bfhe2").OnError(err).Fatal("Unable to start asset storage")
//}
eventstoreClient, err := eventstore.Start(dbClient)
if err != nil {
return fmt.Errorf("cannot start eventstore for queries: %w", err)
}
queries, err := query.StartQueries(ctx, eventstoreClient, dbClient, config.Projections.Config, config.SystemDefaults, config.Projections.KeyConfig, keyChan, config.InternalAuthZ.RolePermissionMappings)
if err != nil {
return fmt.Errorf("cannot start queries: %w", err)
}
authZRepo, err := authz.Start(config.AuthZ, config.SystemDefaults, queries, dbClient, config.OIDC.KeyConfig)
if err != nil {
return fmt.Errorf("error starting authz repo: %w", err)
}
webAuthNConfig := webauthn.Config{
ID: config.ExternalDomain,
Origin: http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure),
DisplayName: "ZITADEL",
}
commands, err := command.StartCommands(eventstoreClient, config.SystemDefaults, config.InternalAuthZ, storage, authZRepo, config.OIDC.KeyConfig, webAuthNConfig)
if err != nil {
return fmt.Errorf("cannot start commands: %w", err)
}
notification.Start(config.Notification, config.SystemDefaults, commands, queries, dbClient, assets.HandlerPrefix)
router := mux.NewRouter()
err = startAPIs(ctx, router, commands, queries, eventstoreClient, dbClient, keyChan, config, storage, authZRepo)
if err != nil {
return err
}
return listen(ctx, router, config.Port)
}
func startAPIs(ctx context.Context, router *mux.Router, commands *command.Commands, queries *query.Queries, eventstore *eventstore.Eventstore, dbClient *sql.DB, keyChan chan interface{}, config *startConfig, store static.Storage, authZRepo authz_repo.Repository) error {
repo := struct {
authz_repo.Repository
*query.Queries
}{
authZRepo,
queries,
}
verifier := internal_authz.Start(repo)
apis := api.New(config.Port, router, &repo, config.InternalAuthZ, config.SystemDefaults, config.ExternalSecure)
authRepo, err := auth_es.Start(config.Auth, config.SystemDefaults, commands, queries, dbClient, config.OIDC.KeyConfig, assets.HandlerPrefix)
if err != nil {
return fmt.Errorf("error starting auth repo: %w", err)
}
adminRepo, err := admin_es.Start(config.Admin, store, dbClient, login.HandlerPrefix)
if err != nil {
return fmt.Errorf("error starting admin repo: %w", err)
}
if err := apis.RegisterServer(ctx, admin.CreateServer(commands, queries, adminRepo, config.SystemDefaults.Domain, assets.HandlerPrefix)); err != nil {
return err
}
if err := apis.RegisterServer(ctx, management.CreateServer(commands, queries, config.SystemDefaults, assets.HandlerPrefix)); err != nil {
return err
}
if err := apis.RegisterServer(ctx, auth.CreateServer(commands, queries, authRepo, config.SystemDefaults, assets.HandlerPrefix)); err != nil {
return err
}
apis.RegisterHandler(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator, store, queries))
userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, config.ExternalDomain, id.SonyFlakeGenerator, config.ExternalSecure)
if err != nil {
return err
}
issuer := oidc.Issuer(config.ExternalDomain, config.ExternalPort, config.ExternalSecure)
oidcProvider, err := oidc.NewProvider(ctx, config.OIDC, issuer, login.DefaultLoggedOutPath, commands, queries, authRepo, config.SystemDefaults.KeyConfig, eventstore, dbClient, keyChan, userAgentInterceptor)
if err != nil {
return fmt.Errorf("unable to start oidc provider: %w", err)
}
apis.RegisterHandler(oidc.HandlerPrefix, oidcProvider.HttpHandler())
openAPIHandler, err := openapi.Start()
if err != nil {
return fmt.Errorf("unable to start openapi handler: %w", err)
}
apis.RegisterHandler(openapi.HandlerPrefix, openAPIHandler)
consoleID, err := consoleClientID(ctx, queries)
if err != nil {
return fmt.Errorf("unable to get client_id for console: %w", err)
}
c, err := console.Start(config.Console, config.ExternalDomain, http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure), issuer, consoleID)
if err != nil {
return fmt.Errorf("unable to start console: %w", err)
}
apis.RegisterHandler(console.HandlerPrefix, c)
l, err := login.CreateLogin(config.Login, commands, queries, authRepo, store, config.SystemDefaults, console.HandlerPrefix, config.ExternalDomain, oidc.AuthCallback, config.ExternalSecure, userAgentInterceptor)
if err != nil {
return fmt.Errorf("unable to start login: %w", err)
}
apis.RegisterHandler(login.HandlerPrefix, l.Handler())
return nil
}
func listen(ctx context.Context, router *mux.Router, port uint16) error {
http2Server := &http2.Server{}
http1Server := &http.Server{Handler: h2c.NewHandler(router, http2Server)}
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
return fmt.Errorf("tcp listener on %d failed: %w", port, err)
}
errCh := make(chan error)
go func() {
logging.Infof("server is listening on %s", lis.Addr().String())
errCh <- http1Server.Serve(lis)
}()
shutdown := make(chan os.Signal, 1)
signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
select {
case err := <-errCh:
return fmt.Errorf("error starting server: %w", err)
case <-shutdown:
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
return shutdownServer(ctx, http1Server)
case <-ctx.Done():
return shutdownServer(ctx, http1Server)
}
}
func shutdownServer(ctx context.Context, server *http.Server) error {
err := server.Shutdown(ctx)
if err != nil {
return fmt.Errorf("could not shutdown gracefully: %w", err)
}
logging.New().Info("server shutdown gracefully")
return nil
}
//TODO:!!??!!
func consoleClientID(ctx context.Context, queries *query.Queries) (string, error) {
iam, err := queries.IAMByID(ctx, domain.IAMID)
if err != nil {
return "", err
}
projectID, err := query.NewAppProjectIDSearchQuery(iam.IAMProjectID)
if err != nil {
return "", err
}
name, err := query.NewAppNameSearchQuery(query.TextContainsIgnoreCase, "console") //TODO:!!??!!
if err != nil {
return "", err
}
apps, err := queries.SearchApps(ctx, &query.AppSearchQueries{
Queries: []query.SearchQuery{projectID, name},
})
if err != nil {
return "", err
}
if len(apps.Apps) != 1 || apps.Apps[0].OIDCConfig == nil {
return "", errors.New("invalid app")
}
return apps.Apps[0].OIDCConfig.ClientID, nil
}