More logging and refactor (#457)

* only use "context"
* enable 'h2' support at server
* trace log  remote and database config
* log loglevel on start
This commit is contained in:
6543 2021-10-19 11:44:49 +02:00 committed by GitHub
parent 2778bfed8e
commit 5990d32fd3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 135 additions and 89 deletions

View file

@ -25,7 +25,6 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/tevino/abool" "github.com/tevino/abool"
"github.com/urfave/cli" "github.com/urfave/cli"
oldcontext "golang.org/x/net/context"
"google.golang.org/grpc" "google.golang.org/grpc"
grpccredentials "google.golang.org/grpc/credentials" grpccredentials "google.golang.org/grpc/credentials"
"google.golang.org/grpc/keepalive" "google.golang.org/grpc/keepalive"
@ -160,7 +159,7 @@ type credentials struct {
password string password string
} }
func (c *credentials) GetRequestMetadata(oldcontext.Context, ...string) (map[string]string, error) { func (c *credentials) GetRequestMetadata(context.Context, ...string) (map[string]string, error) {
return map[string]string{ return map[string]string{
"username": c.username, "username": c.username,
"password": c.password, "password": c.password,

View file

@ -31,7 +31,6 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/urfave/cli" "github.com/urfave/cli"
"golang.org/x/crypto/acme/autocert" "golang.org/x/crypto/acme/autocert"
oldcontext "golang.org/x/net/context"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/keepalive" "google.golang.org/grpc/keepalive"
@ -68,7 +67,6 @@ func loop(c *cli.Context) error {
log.Warn().Msg("--debug is deprecated, use --log-level instead") log.Warn().Msg("--debug is deprecated, use --log-level instead")
zerolog.SetGlobalLevel(zerolog.DebugLevel) zerolog.SetGlobalLevel(zerolog.DebugLevel)
} }
if c.IsSet("log-level") { if c.IsSet("log-level") {
logLevelFlag := c.String("log-level") logLevelFlag := c.String("log-level")
lvl, err := zerolog.ParseLevel(logLevelFlag) lvl, err := zerolog.ParseLevel(logLevelFlag)
@ -77,6 +75,7 @@ func loop(c *cli.Context) error {
} }
zerolog.SetGlobalLevel(lvl) zerolog.SetGlobalLevel(lvl)
} }
log.Log().Msgf("LogLevel = %s", zerolog.GlobalLevel().String())
if c.String("server-host") == "" { if c.String("server-host") == "" {
log.Fatal().Msg("WOODPECKER_HOST is not properly configured") log.Fatal().Msg("WOODPECKER_HOST is not properly configured")
@ -105,7 +104,11 @@ func loop(c *cli.Context) error {
log.Fatal().Err(err).Msg("") log.Fatal().Err(err).Msg("")
} }
store_ := setupStore(c) store_, err := setupStore(c)
if err != nil {
log.Fatal().Err(err).Msg("")
}
setupEvilGlobals(c, store_, remote_) setupEvilGlobals(c, store_, remote_)
proxyWebUI := c.String("www-proxy") proxyWebUI := c.String("www-proxy")
@ -189,7 +192,7 @@ func loop(c *cli.Context) error {
Addr: ":https", Addr: ":https",
Handler: handler, Handler: handler,
TLSConfig: &tls.Config{ TLSConfig: &tls.Config{
NextProtos: []string{"http/1.1"}, // disable h2 because Safari :( NextProtos: []string{"h2", "http/1.1"},
}, },
} }
return serve.ListenAndServeTLS( return serve.ListenAndServeTLS(
@ -232,7 +235,7 @@ func loop(c *cli.Context) error {
Handler: handler, Handler: handler,
TLSConfig: &tls.Config{ TLSConfig: &tls.Config{
GetCertificate: manager.GetCertificate, GetCertificate: manager.GetCertificate,
NextProtos: []string{"http/1.1"}, // disable h2 because Safari :( NextProtos: []string{"h2", "http/1.1"},
}, },
} }
return serve.ListenAndServeTLS("", "") return serve.ListenAndServeTLS("", "")
@ -296,7 +299,7 @@ func (a *authorizer) streamInterceptor(srv interface{}, stream grpc.ServerStream
return handler(srv, stream) return handler(srv, stream)
} }
func (a *authorizer) unaryIntercaptor(ctx oldcontext.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { func (a *authorizer) unaryIntercaptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
if err := a.authorize(ctx); err != nil { if err := a.authorize(ctx); err != nil {
return nil, err return nil, err
} }

View file

@ -44,11 +44,13 @@ import (
"github.com/woodpecker-ci/woodpecker/server/web" "github.com/woodpecker-ci/woodpecker/server/web"
) )
func setupStore(c *cli.Context) store.Store { func setupStore(c *cli.Context) (store.Store, error) {
return datastore.New( opts := &datastore.Opts{
c.String("driver"), Driver: c.String("driver"),
c.String("datasource"), Config: c.String("datasource"),
) }
log.Trace().Msgf("setup datastore: %#v", opts)
return datastore.New(opts)
} }
func setupQueue(c *cli.Context, s store.Store) queue.Queue { func setupQueue(c *cli.Context, s store.Store) queue.Queue {
@ -98,21 +100,25 @@ func SetupRemote(c *cli.Context) (remote.Remote, error) {
// helper function to setup the Bitbucket remote from the CLI arguments. // helper function to setup the Bitbucket remote from the CLI arguments.
func setupBitbucket(c *cli.Context) (remote.Remote, error) { func setupBitbucket(c *cli.Context) (remote.Remote, error) {
return bitbucket.New( opts := &bitbucket.Opts{
c.String("bitbucket-client"), Client: c.String("bitbucket-client"),
c.String("bitbucket-secret"), Secret: c.String("bitbucket-secret"),
), nil }
log.Trace().Msgf("Remote (bitbucket) opts: %#v", opts)
return bitbucket.New(opts)
} }
// helper function to setup the Gogs remote from the CLI arguments. // helper function to setup the Gogs remote from the CLI arguments.
func setupGogs(c *cli.Context) (remote.Remote, error) { func setupGogs(c *cli.Context) (remote.Remote, error) {
return gogs.New(gogs.Opts{ opts := gogs.Opts{
URL: c.String("gogs-server"), URL: c.String("gogs-server"),
Username: c.String("gogs-git-username"), Username: c.String("gogs-git-username"),
Password: c.String("gogs-git-password"), Password: c.String("gogs-git-password"),
PrivateMode: c.Bool("gogs-private-mode"), PrivateMode: c.Bool("gogs-private-mode"),
SkipVerify: c.Bool("gogs-skip-verify"), SkipVerify: c.Bool("gogs-skip-verify"),
}) }
log.Trace().Msgf("Remote (gogs) opts: %#v", opts)
return gogs.New(opts)
} }
// helper function to setup the Gitea remote from the CLI arguments. // helper function to setup the Gitea remote from the CLI arguments.
@ -130,12 +136,13 @@ func setupGitea(c *cli.Context) (remote.Remote, error) {
if len(opts.URL) == 0 { if len(opts.URL) == 0 {
log.Fatal().Msg("WOODPECKER_GITEA_URL must be set") log.Fatal().Msg("WOODPECKER_GITEA_URL must be set")
} }
log.Trace().Msgf("Remote (gitea) opts: %#v", opts)
return gitea.New(opts) return gitea.New(opts)
} }
// helper function to setup the Stash remote from the CLI arguments. // helper function to setup the Stash remote from the CLI arguments.
func setupStash(c *cli.Context) (remote.Remote, error) { func setupStash(c *cli.Context) (remote.Remote, error) {
return bitbucketserver.New(bitbucketserver.Opts{ opts := bitbucketserver.Opts{
URL: c.String("stash-server"), URL: c.String("stash-server"),
Username: c.String("stash-git-username"), Username: c.String("stash-git-username"),
Password: c.String("stash-git-password"), Password: c.String("stash-git-password"),
@ -143,7 +150,9 @@ func setupStash(c *cli.Context) (remote.Remote, error) {
ConsumerRSA: c.String("stash-consumer-rsa"), ConsumerRSA: c.String("stash-consumer-rsa"),
ConsumerRSAString: c.String("stash-consumer-rsa-string"), ConsumerRSAString: c.String("stash-consumer-rsa-string"),
SkipVerify: c.Bool("stash-skip-verify"), SkipVerify: c.Bool("stash-skip-verify"),
}) }
log.Trace().Msgf("Remote (bitbucketserver) opts: %#v", opts)
return bitbucketserver.New(opts)
} }
// helper function to setup the Gitlab remote from the CLI arguments. // helper function to setup the Gitlab remote from the CLI arguments.
@ -161,7 +170,7 @@ func setupGitlab(c *cli.Context) (remote.Remote, error) {
// helper function to setup the GitHub remote from the CLI arguments. // helper function to setup the GitHub remote from the CLI arguments.
func setupGithub(c *cli.Context) (remote.Remote, error) { func setupGithub(c *cli.Context) (remote.Remote, error) {
return github.New(github.Opts{ opts := github.Opts{
URL: c.String("github-server"), URL: c.String("github-server"),
Context: c.String("github-context"), Context: c.String("github-context"),
Client: c.String("github-client"), Client: c.String("github-client"),
@ -172,12 +181,14 @@ func setupGithub(c *cli.Context) (remote.Remote, error) {
PrivateMode: c.Bool("github-private-mode"), PrivateMode: c.Bool("github-private-mode"),
SkipVerify: c.Bool("github-skip-verify"), SkipVerify: c.Bool("github-skip-verify"),
MergeRef: c.BoolT("github-merge-ref"), MergeRef: c.BoolT("github-merge-ref"),
}) }
log.Trace().Msgf("Remote (github) opts: %#v", opts)
return github.New(opts)
} }
// helper function to setup the Coding remote from the CLI arguments. // helper function to setup the Coding remote from the CLI arguments.
func setupCoding(c *cli.Context) (remote.Remote, error) { func setupCoding(c *cli.Context) (remote.Remote, error) {
return coding.New(coding.Opts{ opts := coding.Opts{
URL: c.String("coding-server"), URL: c.String("coding-server"),
Client: c.String("coding-client"), Client: c.String("coding-client"),
Secret: c.String("coding-secret"), Secret: c.String("coding-secret"),
@ -186,7 +197,9 @@ func setupCoding(c *cli.Context) (remote.Remote, error) {
Username: c.String("coding-git-username"), Username: c.String("coding-git-username"),
Password: c.String("coding-git-password"), Password: c.String("coding-git-password"),
SkipVerify: c.Bool("coding-skip-verify"), SkipVerify: c.Bool("coding-skip-verify"),
}) }
log.Trace().Msgf("Remote (coding) opts: %#v", opts)
return coding.New(opts)
} }
func setupTree(c *cli.Context) *gin.Engine { func setupTree(c *cli.Context) *gin.Engine {

View file

@ -28,7 +28,6 @@ import (
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
oldcontext "golang.org/x/net/context"
grpcMetadata "google.golang.org/grpc/metadata" grpcMetadata "google.golang.org/grpc/metadata"
"github.com/woodpecker-ci/expr" "github.com/woodpecker-ci/expr"
@ -505,7 +504,7 @@ func NewWoodpeckerServer(remote remote.Remote, queue queue.Queue, logger logging
return &WoodpeckerServer{peer: peer} return &WoodpeckerServer{peer: peer}
} }
func (s *WoodpeckerServer) Next(c oldcontext.Context, req *proto.NextRequest) (*proto.NextReply, error) { func (s *WoodpeckerServer) Next(c context.Context, req *proto.NextRequest) (*proto.NextReply, error) {
filter := rpc.Filter{ filter := rpc.Filter{
Labels: req.GetFilter().GetLabels(), Labels: req.GetFilter().GetLabels(),
Expr: req.GetFilter().GetExpr(), Expr: req.GetFilter().GetExpr(),
@ -528,7 +527,7 @@ func (s *WoodpeckerServer) Next(c oldcontext.Context, req *proto.NextRequest) (*
return res, err return res, err
} }
func (s *WoodpeckerServer) Init(c oldcontext.Context, req *proto.InitRequest) (*proto.Empty, error) { func (s *WoodpeckerServer) Init(c context.Context, req *proto.InitRequest) (*proto.Empty, error) {
state := rpc.State{ state := rpc.State{
Error: req.GetState().GetError(), Error: req.GetState().GetError(),
ExitCode: int(req.GetState().GetExitCode()), ExitCode: int(req.GetState().GetExitCode()),
@ -542,7 +541,7 @@ func (s *WoodpeckerServer) Init(c oldcontext.Context, req *proto.InitRequest) (*
return res, err return res, err
} }
func (s *WoodpeckerServer) Update(c oldcontext.Context, req *proto.UpdateRequest) (*proto.Empty, error) { func (s *WoodpeckerServer) Update(c context.Context, req *proto.UpdateRequest) (*proto.Empty, error) {
state := rpc.State{ state := rpc.State{
Error: req.GetState().GetError(), Error: req.GetState().GetError(),
ExitCode: int(req.GetState().GetExitCode()), ExitCode: int(req.GetState().GetExitCode()),
@ -556,7 +555,7 @@ func (s *WoodpeckerServer) Update(c oldcontext.Context, req *proto.UpdateRequest
return res, err return res, err
} }
func (s *WoodpeckerServer) Upload(c oldcontext.Context, req *proto.UploadRequest) (*proto.Empty, error) { func (s *WoodpeckerServer) Upload(c context.Context, req *proto.UploadRequest) (*proto.Empty, error) {
file := &rpc.File{ file := &rpc.File{
Data: req.GetFile().GetData(), Data: req.GetFile().GetData(),
Mime: req.GetFile().GetMime(), Mime: req.GetFile().GetMime(),
@ -572,7 +571,7 @@ func (s *WoodpeckerServer) Upload(c oldcontext.Context, req *proto.UploadRequest
return res, err return res, err
} }
func (s *WoodpeckerServer) Done(c oldcontext.Context, req *proto.DoneRequest) (*proto.Empty, error) { func (s *WoodpeckerServer) Done(c context.Context, req *proto.DoneRequest) (*proto.Empty, error) {
state := rpc.State{ state := rpc.State{
Error: req.GetState().GetError(), Error: req.GetState().GetError(),
ExitCode: int(req.GetState().GetExitCode()), ExitCode: int(req.GetState().GetExitCode()),
@ -586,19 +585,19 @@ func (s *WoodpeckerServer) Done(c oldcontext.Context, req *proto.DoneRequest) (*
return res, err return res, err
} }
func (s *WoodpeckerServer) Wait(c oldcontext.Context, req *proto.WaitRequest) (*proto.Empty, error) { func (s *WoodpeckerServer) Wait(c context.Context, req *proto.WaitRequest) (*proto.Empty, error) {
res := new(proto.Empty) res := new(proto.Empty)
err := s.peer.Wait(c, req.GetId()) err := s.peer.Wait(c, req.GetId())
return res, err return res, err
} }
func (s *WoodpeckerServer) Extend(c oldcontext.Context, req *proto.ExtendRequest) (*proto.Empty, error) { func (s *WoodpeckerServer) Extend(c context.Context, req *proto.ExtendRequest) (*proto.Empty, error) {
res := new(proto.Empty) res := new(proto.Empty)
err := s.peer.Extend(c, req.GetId()) err := s.peer.Extend(c, req.GetId())
return res, err return res, err
} }
func (s *WoodpeckerServer) Log(c oldcontext.Context, req *proto.LogRequest) (*proto.Empty, error) { func (s *WoodpeckerServer) Log(c context.Context, req *proto.LogRequest) (*proto.Empty, error) {
line := &rpc.Line{ line := &rpc.Line{
Out: req.GetLine().GetOut(), Out: req.GetLine().GetOut(),
Pos: int(req.GetLine().GetPos()), Pos: int(req.GetLine().GetPos()),

View file

@ -34,6 +34,12 @@ const (
DefaultURL = "https://bitbucket.org" DefaultURL = "https://bitbucket.org"
) )
// Opts are remote options for bitbucket
type Opts struct {
Client string
Secret string
}
type config struct { type config struct {
API string API string
URL string URL string
@ -43,13 +49,14 @@ type config struct {
// New returns a new remote Configuration for integrating with the Bitbucket // New returns a new remote Configuration for integrating with the Bitbucket
// repository hosting service at https://bitbucket.org // repository hosting service at https://bitbucket.org
func New(client, secret string) remote.Remote { func New(opts *Opts) (remote.Remote, error) {
return &config{ return &config{
API: DefaultAPI, API: DefaultAPI,
URL: DefaultURL, URL: DefaultURL,
Client: client, Client: opts.Client,
Secret: secret, Secret: opts.Secret,
} }, nil
// TODO: add checks
} }
// Login authenticates an account with Bitbucket using the oauth2 protocol. The // Login authenticates an account with Bitbucket using the oauth2 protocol. The

View file

@ -44,7 +44,7 @@ func Test_bitbucket(t *testing.T) {
}) })
g.It("Should return client with default endpoint", func() { g.It("Should return client with default endpoint", func() {
remote := New("4vyW6b49Z", "a5012f6c6") remote, _ := New(&Opts{Client: "4vyW6b49Z", Secret: "a5012f6c6"})
g.Assert(remote.(*config).URL).Equal(DefaultURL) g.Assert(remote.(*config).URL).Equal(DefaultURL)
g.Assert(remote.(*config).API).Equal(DefaultAPI) g.Assert(remote.(*config).API).Equal(DefaultAPI)
g.Assert(remote.(*config).Client).Equal("4vyW6b49Z") g.Assert(remote.(*config).Client).Equal("4vyW6b49Z")
@ -52,7 +52,7 @@ func Test_bitbucket(t *testing.T) {
}) })
g.It("Should return the netrc file", func() { g.It("Should return the netrc file", func() {
remote := New("", "") remote, _ := New(&Opts{})
netrc, _ := remote.Netrc(fakeUser, nil) netrc, _ := remote.Netrc(fakeUser, nil)
g.Assert(netrc.Machine).Equal("bitbucket.org") g.Assert(netrc.Machine).Equal("bitbucket.org")
g.Assert(netrc.Login).Equal("x-token-auth") g.Assert(netrc.Login).Equal("x-token-auth")

View file

@ -15,7 +15,7 @@
package remote package remote
import ( import (
"golang.org/x/net/context" "context"
) )
const key = "remote" const key = "remote"

View file

@ -39,6 +39,7 @@ func (cf *configFetcher) Fetch(ctx context.Context) (files []*remote.FileMeta, e
// try to fetch 3 times, timeout is one second longer each time // try to fetch 3 times, timeout is one second longer each time
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
files, err = cf.fetch(ctx, time.Second*time.Duration(configFetchTimeout), strings.TrimSpace(cf.repo.Config)) files, err = cf.fetch(ctx, time.Second*time.Duration(configFetchTimeout), strings.TrimSpace(cf.repo.Config))
log.Trace().Msgf("%d try failed: %v", i, err)
if errors.Is(err, context.DeadlineExceeded) { if errors.Is(err, context.DeadlineExceeded) {
continue continue
} }

View file

@ -15,7 +15,7 @@
package store package store
import ( import (
"golang.org/x/net/context" "context"
) )
const key = "store" const key = "store"

View file

@ -16,7 +16,7 @@ package datastore
import ( import (
"database/sql" "database/sql"
"os" "fmt"
"time" "time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -35,14 +35,21 @@ type datastore struct {
config string config string
} }
// Opts are options for a new database connection
type Opts struct {
Driver string
Config string
}
// New creates a database connection for the given driver and datasource // New creates a database connection for the given driver and datasource
// and returns a new Store. // and returns a new Store.
func New(driver, config string) store.Store { func New(opts *Opts) (store.Store, error) {
db, err := open(opts.Driver, opts.Config)
return &datastore{ return &datastore{
DB: open(driver, config), DB: db,
driver: driver, driver: opts.Driver,
config: config, config: opts.Config,
} }, err
} }
// From returns a Store using an existing database connection. // From returns a Store using an existing database connection.
@ -52,10 +59,10 @@ func From(db *sql.DB) store.Store {
// open opens a new database connection with the specified // open opens a new database connection with the specified
// driver and connection string and returns a store. // driver and connection string and returns a store.
func open(driver, config string) *sql.DB { func open(driver, config string) (*sql.DB, error) {
db, err := sql.Open(driver, config) db, err := sql.Open(driver, config)
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("database connection failed") return nil, fmt.Errorf("database connection failed: %v", err)
} }
if driver == "mysql" { if driver == "mysql" {
// per issue https://github.com/go-sql-driver/mysql/issues/257 // per issue https://github.com/go-sql-driver/mysql/issues/257
@ -65,47 +72,13 @@ func open(driver, config string) *sql.DB {
setupMeddler(driver) setupMeddler(driver)
if err := pingDatabase(db); err != nil { if err := pingDatabase(db); err != nil {
log.Fatal().Err(err).Msg("database ping attempts failed") return nil, fmt.Errorf("database ping attempts failed: %v", err)
} }
if err := setupDatabase(driver, db); err != nil { if err := setupDatabase(driver, db); err != nil {
log.Fatal().Err(err).Msg("migration failed") return nil, fmt.Errorf("database migration failed: %v", err)
}
return db
}
// openTest opens a new database connection for testing purposes.
// The database driver and connection string are provided by
// environment variables, with fallback to in-memory sqlite.
func openTest() *sql.DB {
var (
driver = "sqlite3"
config = ":memory:"
)
if os.Getenv("WOODPECKER_DATABASE_DRIVER") != "" {
driver = os.Getenv("WOODPECKER_DATABASE_DRIVER")
config = os.Getenv("WOODPECKER_DATABASE_CONFIG")
}
return open(driver, config)
}
// newTest creates a new database connection for testing purposes.
// The database driver and connection string are provided by
// environment variables, with fallback to in-memory sqlite.
func newTest() *datastore {
var (
driver = "sqlite3"
config = ":memory:"
)
if os.Getenv("WOODPECKER_DATABASE_DRIVER") != "" {
driver = os.Getenv("WOODPECKER_DATABASE_DRIVER")
config = os.Getenv("WOODPECKER_DATABASE_CONFIG")
}
return &datastore{
DB: open(driver, config),
driver: driver,
config: config,
} }
return db, nil
} }
// helper function to ping the database with backoff to ensure // helper function to ping the database with backoff to ensure

View file

@ -0,0 +1,52 @@
// Copyright 2021 Woodpecker Authors
//
// 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// 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.
package datastore
import (
"database/sql"
"os"
)
func testDriverConfig() (driver, config string) {
driver = "sqlite3"
config = ":memory:"
if os.Getenv("WOODPECKER_DATABASE_DRIVER") != "" {
driver = os.Getenv("WOODPECKER_DATABASE_DRIVER")
config = os.Getenv("WOODPECKER_DATABASE_CONFIG")
}
return
}
// openTest opens a new database connection for testing purposes.
// The database driver and connection string are provided by
// environment variables, with fallback to in-memory sqlite.
func openTest() *sql.DB {
db, _ := open(testDriverConfig())
return db
}
// newTest creates a new database connection for testing purposes.
// The database driver and connection string are provided by
// environment variables, with fallback to in-memory sqlite.
func newTest() *datastore {
driver, config := testDriverConfig()
return &datastore{
DB: openTest(),
driver: driver,
config: config,
}
}

View file

@ -15,10 +15,9 @@
package store package store
import ( import (
"context"
"io" "io"
"golang.org/x/net/context"
"github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/model"
) )