woodpecker/cmd/server/server.go

376 lines
12 KiB
Go
Raw Normal View History

2018-02-19 22:24:10 +00:00
// Copyright 2018 Drone.IO Inc.
2018-03-21 13:02:17 +00:00
//
2018-02-19 22:24:10 +00:00
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
2018-03-21 13:02:17 +00:00
//
2018-02-19 22:24:10 +00:00
// http://www.apache.org/licenses/LICENSE-2.0
2018-03-21 13:02:17 +00:00
//
2018-02-19 22:24:10 +00:00
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
2017-06-29 22:51:22 +00:00
package main
import (
"context"
2017-07-24 23:15:25 +00:00
"crypto/tls"
2017-06-29 22:51:22 +00:00
"net"
"net/http"
"net/http/httputil"
2017-06-29 22:51:22 +00:00
"net/url"
2017-07-24 23:15:25 +00:00
"os"
2017-06-29 22:51:22 +00:00
"strings"
"time"
"github.com/caddyserver/certmagic"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
"golang.org/x/sync/errgroup"
2017-06-29 22:51:22 +00:00
"google.golang.org/grpc"
"google.golang.org/grpc/keepalive"
2017-06-29 22:51:22 +00:00
"github.com/woodpecker-ci/woodpecker/pipeline/rpc/proto"
"github.com/woodpecker-ci/woodpecker/server"
"github.com/woodpecker-ci/woodpecker/server/cron"
"github.com/woodpecker-ci/woodpecker/server/forge"
woodpeckerGrpcServer "github.com/woodpecker-ci/woodpecker/server/grpc"
"github.com/woodpecker-ci/woodpecker/server/logging"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/plugins/config"
"github.com/woodpecker-ci/woodpecker/server/pubsub"
"github.com/woodpecker-ci/woodpecker/server/router"
"github.com/woodpecker-ci/woodpecker/server/router/middleware"
"github.com/woodpecker-ci/woodpecker/server/store"
2021-11-26 08:50:56 +00:00
"github.com/woodpecker-ci/woodpecker/server/web"
"github.com/woodpecker-ci/woodpecker/shared/constant"
"github.com/woodpecker-ci/woodpecker/version"
// "github.com/woodpecker-ci/woodpecker/server/plugins/encryption"
// encryptedStore "github.com/woodpecker-ci/woodpecker/server/plugins/encryption/wrapper/store"
2017-06-29 22:51:22 +00:00
)
func run(c *cli.Context) error {
if c.Bool("pretty") {
log.Logger = log.Output(
zerolog.ConsoleWriter{
Out: os.Stderr,
NoColor: c.Bool("nocolor"),
},
)
2017-06-29 22:51:22 +00:00
}
// TODO: format output & options to switch to json aka. option to add channels to send logs to
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if c.IsSet("log-level") {
logLevelFlag := c.String("log-level")
lvl, err := zerolog.ParseLevel(logLevelFlag)
if err != nil {
log.Fatal().Msgf("unknown logging level: %s", logLevelFlag)
}
zerolog.SetGlobalLevel(lvl)
}
if zerolog.GlobalLevel() <= zerolog.DebugLevel {
log.Logger = log.With().Caller().Logger()
} else {
gin.SetMode(gin.ReleaseMode)
}
log.Log().Msgf("LogLevel = %s", zerolog.GlobalLevel().String())
2017-07-12 18:48:56 +00:00
if c.String("server-host") == "" {
log.Fatal().Msg("WOODPECKER_HOST is not properly configured")
2017-07-12 18:48:56 +00:00
}
if !strings.Contains(c.String("server-host"), "://") {
log.Fatal().Msg(
"WOODPECKER_HOST must be <scheme>://<hostname> format",
)
}
if strings.Contains(c.String("server-host"), "://localhost") {
log.Warn().Msg(
"WOODPECKER_HOST should probably be publicly accessible (not localhost)",
)
}
2017-12-20 12:49:02 +00:00
if strings.HasSuffix(c.String("server-host"), "/") {
log.Fatal().Msg(
"WOODPECKER_HOST must not have trailing slash",
)
}
_forge, err := setupForge(c)
2017-06-29 22:51:22 +00:00
if err != nil {
log.Fatal().Err(err).Msg("")
2017-06-29 22:51:22 +00:00
}
_store, err := setupStore(c)
if err != nil {
log.Fatal().Err(err).Msg("")
}
defer func() {
if err := _store.Close(); err != nil {
log.Error().Err(err).Msg("could not close store")
}
}()
setupEvilGlobals(c, _store, _forge)
2017-06-29 22:51:22 +00:00
var g errgroup.Group
2017-07-31 19:15:05 +00:00
setupMetrics(&g, _store)
2017-06-29 22:51:22 +00:00
g.Go(func() error {
return cron.Start(c.Context, _store, _forge)
})
2017-06-29 22:51:22 +00:00
// start the grpc server
g.Go(func() error {
lis, err := net.Listen("tcp", c.String("grpc-addr"))
2017-06-29 22:51:22 +00:00
if err != nil {
log.Error().Err(err).Msg("failed to listen on grpc-addr")
2017-06-29 22:51:22 +00:00
return err
}
jwtSecret := c.String("grpc-secret")
jwtManager := woodpeckerGrpcServer.NewJWTManager(jwtSecret)
authorizer := woodpeckerGrpcServer.NewAuthorizer(jwtManager)
2019-06-28 12:23:52 +00:00
grpcServer := grpc.NewServer(
grpc.StreamInterceptor(authorizer.StreamInterceptor),
grpc.UnaryInterceptor(authorizer.UnaryInterceptor),
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
MinTime: c.Duration("keepalive-min-time"),
}),
2017-06-29 23:35:38 +00:00
)
woodpeckerServer := woodpeckerGrpcServer.NewWoodpeckerServer(
_forge,
server.Config.Services.Queue,
server.Config.Services.Logs,
server.Config.Services.Pubsub,
_store,
server.Config.Server.Host,
)
proto.RegisterWoodpeckerServer(grpcServer, woodpeckerServer)
2017-06-29 22:51:22 +00:00
woodpeckerAuthServer := woodpeckerGrpcServer.NewWoodpeckerAuthServer(
jwtManager,
server.Config.Server.AgentToken,
_store,
)
proto.RegisterWoodpeckerAuthServer(grpcServer, woodpeckerAuthServer)
2019-06-28 12:23:52 +00:00
err = grpcServer.Serve(lis)
2017-06-29 22:51:22 +00:00
if err != nil {
log.Error().Err(err).Msg("failed to serve grpc server")
2017-06-29 22:51:22 +00:00
return err
}
return nil
})
proxyWebUI := c.String("www-proxy")
var webUIServe func(w http.ResponseWriter, r *http.Request)
2019-05-30 10:15:29 +00:00
if proxyWebUI == "" {
webEngine, err := web.New()
if err != nil {
log.Error().Err(err).Msg("failed to create web engine")
return err
}
webUIServe = webEngine.ServeHTTP
} else {
origin, _ := url.Parse(proxyWebUI)
director := func(req *http.Request) {
req.Header.Add("X-Forwarded-Host", req.Host)
req.Header.Add("X-Origin-Host", origin.Host)
req.URL.Scheme = origin.Scheme
req.URL.Host = origin.Host
}
proxy := &httputil.ReverseProxy{Director: director}
webUIServe = proxy.ServeHTTP
}
// setup the server and start the listener
handler := router.Load(
webUIServe,
middleware.Logger(time.RFC3339, true),
middleware.Version,
middleware.Config(c),
middleware.Store(c, _store),
)
2017-06-29 22:51:22 +00:00
if c.String("server-cert") != "" {
// start the server with tls enabled
g.Go(func() error {
serve := &http.Server{
Addr: server.Config.Server.PortTLS,
Handler: handler,
TLSConfig: &tls.Config{
NextProtos: []string{"h2", "http/1.1"},
},
}
return serve.ListenAndServeTLS(
c.String("server-cert"),
c.String("server-key"),
)
})
2017-06-29 22:51:22 +00:00
// http to https redirect
redirect := func(w http.ResponseWriter, req *http.Request) {
serverHost := server.Config.Server.Host
serverHost = strings.TrimPrefix(serverHost, "http://")
serverHost = strings.TrimPrefix(serverHost, "https://")
req.URL.Scheme = "https"
req.URL.Host = serverHost
w.Header().Set("Strict-Transport-Security", "max-age=31536000")
http.Redirect(w, req, req.URL.String(), http.StatusMovedPermanently)
}
g.Go(func() error {
return http.ListenAndServe(server.Config.Server.Port, http.HandlerFunc(redirect))
})
} else if c.Bool("lets-encrypt") {
// start the server with lets-encrypt
certmagic.DefaultACME.Email = c.String("lets-encrypt-email")
certmagic.DefaultACME.Agreed = true
address, err := url.Parse(c.String("server-host"))
if err != nil {
return err
}
2017-06-29 22:51:22 +00:00
g.Go(func() error {
if err := certmagic.HTTPS([]string{address.Host}, handler); err != nil {
log.Err(err).Msg("certmagic does not work")
os.Exit(1)
}
return nil
})
} else {
// start the server without tls
g.Go(func() error {
return http.ListenAndServe(
c.String("server-addr"),
handler,
)
})
}
if metricsServerAddr := c.String("metrics-server-addr"); metricsServerAddr != "" {
g.Go(func() error {
metricsRouter := gin.New()
metricsRouter.GET("/metrics", gin.WrapH(promhttp.Handler()))
return http.ListenAndServe(metricsServerAddr, metricsRouter)
})
}
log.Info().Msgf("Starting Woodpecker server with version '%s'", version.String())
2017-06-29 22:51:22 +00:00
return g.Wait()
}
func setupEvilGlobals(c *cli.Context, v store.Store, f forge.Forge) {
// forge
server.Config.Services.Forge = f
server.Config.Services.Timeout = c.Duration("forge-timeout")
2017-06-29 22:51:22 +00:00
// services
server.Config.Services.Queue = setupQueue(c, v)
server.Config.Services.Logs = logging.New()
server.Config.Services.Pubsub = pubsub.New()
if err := server.Config.Services.Pubsub.Create(context.Background(), "topic/events"); err != nil {
log.Error().Err(err).Msg("could not create pubsub service")
}
server.Config.Services.Registries = setupRegistryService(c, v)
// TODO(1544): fix encrypted store
// // encryption
// encryptedSecretStore := encryptedStore.NewSecretStore(v)
// err := encryption.Encryption(c, v).WithClient(encryptedSecretStore).Build()
// if err != nil {
// log.Fatal().Err(err).Msg("could not create encryption service")
// }
// server.Config.Services.Secrets = setupSecretService(c, encryptedSecretStore)
server.Config.Services.Secrets = setupSecretService(c, v)
server.Config.Services.Environ = setupEnvironService(c, v)
server.Config.Services.Membership = setupMembershipService(c, f)
2017-06-29 22:51:22 +00:00
server.Config.Services.SignaturePrivateKey, server.Config.Services.SignaturePublicKey = setupSignatureKeys(v)
Add support for pipeline configuration service (#804) * Add configuration extension flags to server Add httpsignatures dependency Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Add http fetching to config fetcher Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Refetch config on rebuild Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * - Ensure multipipeline compatiblity - Send original config in http request Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Basic tests of config api Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Simple docs page Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Better flag naming Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Rename usages of the term yaml Rename ConfigAPI struct Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Doc adjustments Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * More docs touchups Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Fix env vars in docs Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * fix json tags for api calls Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Add example config service Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Consistent naming for configService Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Docs: Change example repository location Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Fix tests after response field rename Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Revert accidential unrelated change in api hook Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Update server flag descriptions Co-authored-by: Anbraten <anton@ju60.de> Co-authored-by: Anbraten <anton@ju60.de>
2022-02-28 09:56:23 +00:00
if endpoint := c.String("config-service-endpoint"); endpoint != "" {
server.Config.Services.ConfigService = config.NewHTTP(endpoint, server.Config.Services.SignaturePrivateKey)
Add support for pipeline configuration service (#804) * Add configuration extension flags to server Add httpsignatures dependency Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Add http fetching to config fetcher Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Refetch config on rebuild Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * - Ensure multipipeline compatiblity - Send original config in http request Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Basic tests of config api Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Simple docs page Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Better flag naming Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Rename usages of the term yaml Rename ConfigAPI struct Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Doc adjustments Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * More docs touchups Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Fix env vars in docs Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * fix json tags for api calls Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Add example config service Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Consistent naming for configService Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Docs: Change example repository location Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Fix tests after response field rename Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Revert accidential unrelated change in api hook Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Update server flag descriptions Co-authored-by: Anbraten <anton@ju60.de> Co-authored-by: Anbraten <anton@ju60.de>
2022-02-28 09:56:23 +00:00
}
// authentication
server.Config.Pipeline.AuthenticatePublicRepos = c.Bool("authenticate-public-repos")
// Cloning
server.Config.Pipeline.DefaultCloneImage = c.String("default-clone-image")
constant.TrustedCloneImages = append(constant.TrustedCloneImages, server.Config.Pipeline.DefaultCloneImage)
// Execution
_events := c.StringSlice("default-cancel-previous-pipeline-events")
events := make([]model.WebhookEvent, len(_events))
for _, v := range _events {
events = append(events, model.WebhookEvent(v))
}
server.Config.Pipeline.DefaultCancelPreviousPipelineEvents = events
server.Config.Pipeline.DefaultTimeout = c.Int64("default-pipeline-timeout")
server.Config.Pipeline.MaxTimeout = c.Int64("max-pipeline-timeout")
2017-06-29 22:51:22 +00:00
// limits
server.Config.Pipeline.Limits.MemSwapLimit = c.Int64("limit-mem-swap")
server.Config.Pipeline.Limits.MemLimit = c.Int64("limit-mem")
server.Config.Pipeline.Limits.ShmSize = c.Int64("limit-shm-size")
server.Config.Pipeline.Limits.CPUQuota = c.Int64("limit-cpu-quota")
server.Config.Pipeline.Limits.CPUShares = c.Int64("limit-cpu-shares")
server.Config.Pipeline.Limits.CPUSet = c.String("limit-cpu-set")
2017-06-29 22:51:22 +00:00
// server configuration
server.Config.Server.Cert = c.String("server-cert")
server.Config.Server.Key = c.String("server-key")
server.Config.Server.AgentToken = c.String("agent-secret")
server.Config.Server.Host = c.String("server-host")
if c.IsSet("server-webhook-host") {
server.Config.Server.WebhookHost = c.String("server-webhook-host")
} else {
server.Config.Server.WebhookHost = c.String("server-host")
}
if c.IsSet("server-dev-oauth-host") {
server.Config.Server.OAuthHost = c.String("server-dev-oauth-host")
} else {
server.Config.Server.OAuthHost = c.String("server-host")
}
server.Config.Server.Port = c.String("server-addr")
server.Config.Server.PortTLS = c.String("server-addr-tls")
server.Config.Server.Docs = c.String("docs")
server.Config.Server.StatusContext = c.String("status-context")
server.Config.Server.StatusContextFormat = c.String("status-context-format")
server.Config.Server.SessionExpires = c.Duration("session-expires")
rootPath := strings.TrimSuffix(c.String("root-path"), "/")
if rootPath != "" && !strings.HasPrefix(rootPath, "/") {
rootPath = "/" + rootPath
}
server.Config.Server.RootPath = rootPath
support custom .JS and .CSS files for custom banner messages (white-labeling) (#1781) This PR introduces two new server configuration options, for providing a custom .JS and .CSS file. These can be used to show custom banner messages, add environment-dependent signals, or simply a corporate logo. ### Motivation (what problem I try to solve) I'm operating Woodpecker in multiple k8s clusters for different environments. When having multiple browser tabs open, I prefer strong indicators for each environment. E.g. a red "PROD" banner, or just a blue "QA" banner. Also, we sometimes need to have the chance for maintenance, and instead of broadcasting emails, I prefer a banner message, stating something like: "Heads-up: there's a planned downtime, next Friday, blabla...". Also, I like to have the firm's logo visible, which makes Woodpecker look more like an integral part of our platform. ### Implementation notes * Two new config options are introduced ```WOODPECKER_CUSTOM_CSS_FILE``` and ```WOODPECKER_CUSTOM_JS_FILE``` * I've piggy-bagged the existing handler for assets, as it seemed to me a minimally invasive approach * the option along with an example is documented * a simple unit test for the Gin-handler ensures some regression safety * no extra dependencies are introduced ### Visual example The documented example will look like this. ![Screenshot 2023-05-27 at 17 00 44](https://github.com/woodpecker-ci/woodpecker/assets/1189394/8940392e-463c-4651-a1eb-f017cd3cd64d) ### Areas of uncertainty This is my first contribution to Woodpecker and I tried my best to align with your conventions. That said, I found myself uncertain about these things and would be glad about getting feedback. * The handler tests are somewhat different than the other ones because I wanted to keep them simple - I hope that still matches your coding guidelines * caching the page sometimes will let the browser not recognize changes and a user must reload. I'm not fully into the details of how caching is implemented and neither can judge if it's a real problem. Another pair of eyes would be good.
2023-07-10 10:46:35 +00:00
server.Config.Server.CustomCSSFile = strings.TrimSpace(c.String("custom-css-file"))
server.Config.Server.CustomJsFile = strings.TrimSpace(c.String("custom-js-file"))
server.Config.Pipeline.Networks = c.StringSlice("network")
server.Config.Pipeline.Volumes = c.StringSlice("volume")
server.Config.Pipeline.Privileged = c.StringSlice("escalate")
server.Config.Server.Migrations.AllowLong = c.Bool("migrations-allow-long")
server.Config.Server.EnableSwagger = c.Bool("enable-swagger")
// prometheus
server.Config.Prometheus.AuthToken = c.String("prometheus-auth-token")
2017-06-29 22:51:22 +00:00
}