woodpecker/cmd/server/server.go

370 lines
10 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 23:35:38 +00:00
"errors"
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"
"path/filepath"
2017-06-29 22:51:22 +00:00
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
"golang.org/x/crypto/acme/autocert"
"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 23:35:38 +00:00
"google.golang.org/grpc/metadata"
2017-06-29 22:51:22 +00:00
"github.com/woodpecker-ci/woodpecker/pipeline/rpc/proto"
"github.com/woodpecker-ci/woodpecker/server"
woodpeckerGrpcServer "github.com/woodpecker-ci/woodpecker/server/grpc"
"github.com/woodpecker-ci/woodpecker/server/logging"
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
"github.com/woodpecker-ci/woodpecker/server/plugins/configuration"
"github.com/woodpecker-ci/woodpecker/server/plugins/sender"
"github.com/woodpecker-ci/woodpecker/server/pubsub"
"github.com/woodpecker-ci/woodpecker/server/remote"
"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"
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.WarnLevel)
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",
)
}
_remote, err := setupRemote(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, _remote)
2017-06-29 22:51:22 +00:00
proxyWebUI := c.String("www-proxy")
var webUIServe func(w http.ResponseWriter, r *http.Request)
if proxyWebUI == "" {
2021-11-26 08:50:56 +00:00
webUIServe = web.New().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
}
2017-07-31 19:15:05 +00:00
2017-06-29 22:51:22 +00:00
// setup the server and start the listener
handler := router.Load(
webUIServe,
2021-11-26 08:50:56 +00:00
middleware.Logger(time.RFC3339, true),
2017-06-29 22:51:22 +00:00
middleware.Version,
middleware.Config(c),
middleware.Store(c, _store),
2017-06-29 22:51:22 +00:00
)
var g errgroup.Group
// 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.Err(err).Msg("")
2017-06-29 22:51:22 +00:00
return err
}
authorizer := &authorizer{
2017-06-29 23:35:38 +00:00
password: c.String("agent-secret"),
}
2019-06-28 12:23:52 +00:00
grpcServer := grpc.NewServer(
grpc.StreamInterceptor(authorizer.streamInterceptor),
grpc.UnaryInterceptor(authorizer.unaryIntercaptor),
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
MinTime: c.Duration("keepalive-min-time"),
}),
2017-06-29 23:35:38 +00:00
)
woodpeckerServer := woodpeckerGrpcServer.NewWoodpeckerServer(
_remote,
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
2019-06-28 12:23:52 +00:00
err = grpcServer.Serve(lis)
2017-06-29 22:51:22 +00:00
if err != nil {
log.Err(err).Msg("")
2017-06-29 22:51:22 +00:00
return err
}
return nil
})
setupMetrics(&g, _store)
2019-05-30 10:15:29 +00:00
2017-06-29 22:51:22 +00:00
// start the server with tls enabled
if c.String("server-cert") != "" {
g.Go(func() error {
return http.ListenAndServe(":http", http.HandlerFunc(redirect))
})
g.Go(func() error {
serve := &http.Server{
Addr: ":https",
Handler: handler,
TLSConfig: &tls.Config{
NextProtos: []string{"h2", "http/1.1"},
},
}
return serve.ListenAndServeTLS(
c.String("server-cert"),
c.String("server-key"),
)
})
return g.Wait()
2017-06-29 22:51:22 +00:00
}
// start the server without tls enabled
if !c.Bool("lets-encrypt") {
return http.ListenAndServe(
c.String("server-addr"),
handler,
)
}
// start the server with lets encrypt enabled
// listen on ports 443 and 80
address, err := url.Parse(c.String("server-host"))
if err != nil {
return err
}
dir := cacheDir()
2022-01-05 20:50:23 +00:00
if err := os.MkdirAll(dir, 0o700); err != nil {
return err
}
2018-01-18 16:33:41 +00:00
manager := &autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(address.Host),
Cache: autocert.DirCache(dir),
}
2017-06-29 22:51:22 +00:00
g.Go(func() error {
return http.ListenAndServe(":http", manager.HTTPHandler(http.HandlerFunc(redirect)))
2017-06-29 22:51:22 +00:00
})
g.Go(func() error {
serve := &http.Server{
Addr: ":https",
Handler: handler,
TLSConfig: &tls.Config{
GetCertificate: manager.GetCertificate,
NextProtos: []string{"h2", "http/1.1"},
},
2017-07-24 23:15:25 +00:00
}
return serve.ListenAndServeTLS("", "")
2017-06-29 22:51:22 +00:00
})
return g.Wait()
}
func setupEvilGlobals(c *cli.Context, v store.Store, r remote.Remote) {
// storage
server.Config.Storage.Files = v
2017-06-29 22:51:22 +00:00
// remote
server.Config.Services.Remote = r
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)
server.Config.Services.Secrets = setupSecretService(c, v)
server.Config.Services.Senders = sender.New(v, v)
server.Config.Services.Environ = setupEnvironService(c, v)
2017-06-29 22:51:22 +00:00
if endpoint := c.String("gating-service"); endpoint != "" {
server.Config.Services.Senders = sender.NewRemote(endpoint)
2017-06-29 22:51:22 +00:00
}
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 != "" {
secret := c.String("config-service-secret")
if secret == "" {
log.Error().Msg("could not configure configuration service, missing secret")
} else {
server.Config.Services.ConfigService = configuration.NewAPI(endpoint, secret)
}
}
// authentication
server.Config.Pipeline.AuthenticatePublicRepos = c.Bool("authenticate-public-repos")
// Cloning
server.Config.Pipeline.DefaultCloneImage = c.String("default-clone-image")
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.Pass = c.String("agent-secret")
server.Config.Server.Host = 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.Docs = c.String("docs")
server.Config.Server.StatusContext = c.String("status-context")
server.Config.Server.SessionExpires = c.Duration("session-expires")
server.Config.Pipeline.Networks = c.StringSlice("network")
server.Config.Pipeline.Volumes = c.StringSlice("volume")
server.Config.Pipeline.Privileged = c.StringSlice("escalate")
// prometheus
server.Config.Prometheus.AuthToken = c.String("prometheus-auth-token")
// TODO(485) temporary workaround to not hit api rate limits
server.Config.FlatPermissions = c.Bool("flat-permissions")
2017-06-29 22:51:22 +00:00
}
2017-06-29 23:35:38 +00:00
type authorizer struct {
password string
}
func (a *authorizer) streamInterceptor(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
if err := a.authorize(stream.Context()); err != nil {
return err
}
return handler(srv, stream)
}
func (a *authorizer) unaryIntercaptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
2017-06-29 23:35:38 +00:00
if err := a.authorize(ctx); err != nil {
return nil, err
}
return handler(ctx, req)
}
func (a *authorizer) authorize(ctx context.Context) error {
2020-05-18 14:46:13 +00:00
if md, ok := metadata.FromIncomingContext(ctx); ok {
2017-06-29 23:35:38 +00:00
if len(md["password"]) > 0 && md["password"][0] == a.password {
return nil
}
return errors.New("invalid agent token")
}
return errors.New("missing agent token")
}
2017-07-24 23:15:25 +00:00
func redirect(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)
}
2017-07-24 23:15:25 +00:00
func cacheDir() string {
const base = "golang-autocert"
if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" {
return filepath.Join(xdg, base)
}
return filepath.Join(os.Getenv("HOME"), ".cache", base)
}