From d494b6a9590a4205e26a5383bcb054fd96f7ccb4 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Tue, 16 Apr 2024 08:04:55 +0200 Subject: [PATCH] Use forge from db (#1417) This is the first step towards support for multiple forges (#138). It inserts a forge using the currently existing env varaibles into db and uses this forge from db later on in all places of the code. closes #621 addresses #138 # TODO - [x] add forges table - [x] add id of forge to repo - [x] use forge of repo - [x] add forge from env vars to db if not exists - [x] migrate repo.ForgeID to the newly generated forge - [x] support cache with forge from repo - [x] maybe add forge loading cache? (use LRU cache for forges, I expect users to have less than 10 forges normally) --------- Co-authored-by: qwerty287 <80460567+qwerty287@users.noreply.github.com> --- cmd/server/docs/docs.go | 9 + cmd/server/flags.go | 132 ++------- cmd/server/server.go | 22 +- cmd/server/setup.go | 108 +------ pipeline/rpc/proto/woodpecker_grpc.pb.go | 63 ++-- server/api/cron.go | 21 +- server/api/helper.go | 8 +- server/api/hook.go | 8 +- server/api/login.go | 30 +- server/api/org.go | 18 +- server/api/pipeline.go | 20 +- server/api/repo.go | 68 +++-- server/api/user.go | 10 +- server/api/users.go | 2 + server/cache/membership.go | 16 +- server/config.go | 4 +- server/cron/cron.go | 20 +- server/cron/cron_test.go | 13 +- server/forge/setup/setup.go | 134 +++++++++ server/grpc/rpc.go | 12 +- server/grpc/server.go | 4 +- server/model/forge.go | 36 +++ server/model/org.go | 9 +- server/model/repo.go | 5 +- server/model/user.go | 2 + server/pipeline/approve.go | 14 +- server/pipeline/cancel.go | 8 +- server/pipeline/create.go | 28 +- server/pipeline/decline.go | 12 +- server/pipeline/helper.go | 6 +- server/pipeline/items.go | 15 +- server/pipeline/restart.go | 16 +- server/pipeline/start.go | 15 +- server/router/middleware/session/repo.go | 9 +- server/router/middleware/session/user.go | 9 +- server/router/middleware/token/token.go | 10 +- .../environment/{extension.go => service.go} | 0 server/services/manager.go | 104 +++++-- server/services/mocks/manager.go | 270 ++++++++++++++++++ server/services/setup.go | 68 +++++ server/store/datastore/forge.go | 54 ++++ server/store/datastore/forge_test.go | 71 +++++ .../migration/030_set_default_forge_id.go | 46 +++ server/store/datastore/migration/migration.go | 2 + server/store/mocks/store.go | 146 ++++++++++ server/store/store.go | 7 + server/web/config.go | 10 +- web/src/views/repo/RepoWrapper.vue | 2 +- 48 files changed, 1291 insertions(+), 405 deletions(-) create mode 100644 server/forge/setup/setup.go create mode 100644 server/model/forge.go rename server/services/environment/{extension.go => service.go} (100%) create mode 100644 server/services/mocks/manager.go create mode 100644 server/store/datastore/forge.go create mode 100644 server/store/datastore/forge_test.go create mode 100644 server/store/datastore/migration/030_set_default_forge_id.go diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index 51cfe5587..1195f9829 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -3891,6 +3891,9 @@ const docTemplate = `{ "Org": { "type": "object", "properties": { + "forge_id": { + "type": "integer" + }, "id": { "type": "integer" }, @@ -4122,6 +4125,9 @@ const docTemplate = `{ "default_branch": { "type": "string" }, + "forge_id": { + "type": "integer" + }, "forge_remote_id": { "description": "ForgeRemoteID is the unique identifier for the repository on the forge.", "type": "string" @@ -4418,6 +4424,9 @@ const docTemplate = `{ "description": "Email is the email address for this user.\n\nrequired: true", "type": "string" }, + "forge_id": { + "type": "integer" + }, "id": { "description": "the id for this user.\n\nrequired: true", "type": "integer" diff --git a/cmd/server/flags.go b/cmd/server/flags.go index 11d7ff2f3..9789a8089 100644 --- a/cmd/server/flags.go +++ b/cmd/server/flags.go @@ -246,11 +246,6 @@ var flags = append([]cli.Flag{ Usage: "Disable version check in admin web ui.", Name: "skip-version-check", }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_ADDON_FORGE"}, - Name: "addon-forge", - Usage: "forge addon", - }, // // backend options for pipeline compiler // @@ -309,6 +304,35 @@ var flags = append([]cli.Flag{ Usage: "set the cpus allowed to execute containers", }, // + &cli.StringFlag{ + Name: "forge-url", + Usage: "url of the forge", + EnvVars: []string{"WOODPECKER_FORGE_URL", "WOODPECKER_GITHUB_URL", "WOODPECKER_GITLAB_URL", "WOODPECKER_GITEA_URL", "WOODPECKER_BITBUCKET_URL"}, + }, + &cli.StringFlag{ + Name: "forge-oauth-client", + Usage: "oauth2 client id", + EnvVars: []string{"WOODPECKER_FORGE_CLIENT", "WOODPECKER_GITHUB_CLIENT", "WOODPECKER_GITLAB_CLIENT", "WOODPECKER_GITEA_CLIENT", "WOODPECKER_BITBUCKET_CLIENT", "WOODPECKER_BITBUCKET_DC_CLIENT_ID"}, + }, + &cli.StringFlag{ + Name: "forge-oauth-secret", + Usage: "oauth2 client secret", + EnvVars: []string{"WOODPECKER_FORGE_SECRET", "WOODPECKER_GITHUB_SECRET", "WOODPECKER_GITLAB_SECRET", "WOODPECKER_GITEA_SECRET", "WOODPECKER_BITBUCKET_SECRET", "WOODPECKER_BITBUCKET_DC_CLIENT_SECRET"}, + }, + &cli.BoolFlag{ + Name: "forge-skip-verify", + Usage: "skip ssl verification", + EnvVars: []string{"WOODPECKER_FORGE_SKIP_VERIFY", "WOODPECKER_GITHUB_SKIP_VERIFY", "WOODPECKER_GITLAB_SKIP_VERIFY", "WOODPECKER_GITEA_SKIP_VERIFY", "WOODPECKER_BITBUCKET_SKIP_VERIFY"}, + }, + // + // Addon + // + &cli.StringFlag{ + EnvVars: []string{"WOODPECKER_ADDON_FORGE"}, + Name: "addon-forge", + Usage: "path to forge addon executable", + }, + // // GitHub // &cli.BoolFlag{ @@ -316,24 +340,6 @@ var flags = append([]cli.Flag{ Name: "github", Usage: "github driver is enabled", }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_GITHUB_URL"}, - Name: "github-server", - Usage: "github server address", - Value: "https://github.com", - }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_GITHUB_CLIENT"}, - Name: "github-client", - Usage: "github oauth2 client id", - FilePath: os.Getenv("WOODPECKER_GITHUB_CLIENT_FILE"), - }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_GITHUB_SECRET"}, - Name: "github-secret", - Usage: "github oauth2 client secret", - FilePath: os.Getenv("WOODPECKER_GITHUB_SECRET_FILE"), - }, &cli.BoolFlag{ EnvVars: []string{"WOODPECKER_GITHUB_MERGE_REF"}, Name: "github-merge-ref", @@ -346,11 +352,6 @@ var flags = append([]cli.Flag{ Usage: "github tokens should only get access to public repos", Value: false, }, - &cli.BoolFlag{ - EnvVars: []string{"WOODPECKER_GITHUB_SKIP_VERIFY"}, - Name: "github-skip-verify", - Usage: "github skip ssl verification", - }, // // Gitea // @@ -359,29 +360,6 @@ var flags = append([]cli.Flag{ Name: "gitea", Usage: "gitea driver is enabled", }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_GITEA_URL"}, - Name: "gitea-server", - Usage: "gitea server address", - Value: "https://try.gitea.io", - }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_GITEA_CLIENT"}, - Name: "gitea-client", - Usage: "gitea oauth2 client id", - FilePath: os.Getenv("WOODPECKER_GITEA_CLIENT_FILE"), - }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_GITEA_SECRET"}, - Name: "gitea-secret", - Usage: "gitea oauth2 client secret", - FilePath: os.Getenv("WOODPECKER_GITEA_SECRET_FILE"), - }, - &cli.BoolFlag{ - EnvVars: []string{"WOODPECKER_GITEA_SKIP_VERIFY"}, - Name: "gitea-skip-verify", - Usage: "gitea skip ssl verification", - }, &cli.StringFlag{ EnvVars: []string{"WOODPECKER_DEV_GITEA_OAUTH_URL"}, Name: "gitea-oauth-server", @@ -395,18 +373,6 @@ var flags = append([]cli.Flag{ Name: "bitbucket", Usage: "bitbucket driver is enabled", }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_BITBUCKET_CLIENT"}, - Name: "bitbucket-client", - Usage: "bitbucket oauth2 client id", - FilePath: os.Getenv("WOODPECKER_BITBUCKET_CLIENT_FILE"), - }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_BITBUCKET_SECRET"}, - Name: "bitbucket-secret", - Usage: "bitbucket oauth2 client secret", - FilePath: os.Getenv("WOODPECKER_BITBUCKET_SECRET_FILE"), - }, // // Gitlab // @@ -415,29 +381,6 @@ var flags = append([]cli.Flag{ Name: "gitlab", Usage: "gitlab driver is enabled", }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_GITLAB_URL"}, - Name: "gitlab-server", - Usage: "gitlab server address", - Value: "https://gitlab.com", - }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_GITLAB_CLIENT"}, - Name: "gitlab-client", - Usage: "gitlab oauth2 client id", - FilePath: os.Getenv("WOODPECKER_GITLAB_CLIENT_FILE"), - }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_GITLAB_SECRET"}, - Name: "gitlab-secret", - Usage: "gitlab oauth2 client secret", - FilePath: os.Getenv("WOODPECKER_GITLAB_SECRET_FILE"), - }, - &cli.BoolFlag{ - EnvVars: []string{"WOODPECKER_GITLAB_SKIP_VERIFY"}, - Name: "gitlab-skip-verify", - Usage: "gitlab skip ssl verification", - }, // // Bitbucket DataCenter/Server (previously Stash) // @@ -446,23 +389,6 @@ var flags = append([]cli.Flag{ Name: "bitbucket-dc", Usage: "Bitbucket DataCenter/Server driver is enabled", }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_BITBUCKET_DC_URL"}, - Name: "bitbucket-dc-server", - Usage: "Bitbucket DataCenter/Server server address", - }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_BITBUCKET_DC_CLIENT_ID"}, - Name: "bitbucket-dc-client-id", - Usage: "Bitbucket DataCenter/Server OAuth 2.0 client id", - FilePath: os.Getenv("WOODPECKER_BITBUCKET_DC_CLIENT_ID_FILE"), - }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_BITBUCKET_DC_CLIENT_SECRET"}, - Name: "bitbucket-dc-client-secret", - Usage: "Bitbucket DataCenter/Server OAuth 2.0 client secret", - FilePath: os.Getenv("WOODPECKER_BITBUCKET_DC_CLIENT_SECRET_FILE"), - }, &cli.StringFlag{ EnvVars: []string{"WOODPECKER_BITBUCKET_DC_GIT_USERNAME"}, Name: "bitbucket-dc-git-username", diff --git a/cmd/server/server.go b/cmd/server/server.go index 3eb3b4bbc..39ddc2580 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -38,7 +38,7 @@ import ( "go.woodpecker-ci.org/woodpecker/v2/pipeline/rpc/proto" "go.woodpecker-ci.org/woodpecker/v2/server" "go.woodpecker-ci.org/woodpecker/v2/server/cron" - "go.woodpecker-ci.org/woodpecker/v2/server/forge" + "go.woodpecker-ci.org/woodpecker/v2/server/forge/setup" woodpeckerGrpcServer "go.woodpecker-ci.org/woodpecker/v2/server/grpc" "go.woodpecker-ci.org/woodpecker/v2/server/logging" "go.woodpecker-ci.org/woodpecker/v2/server/model" @@ -82,11 +82,6 @@ func run(c *cli.Context) error { ) } - _forge, err := setupForge(c) - if err != nil { - return fmt.Errorf("can't setup forge: %w", err) - } - _store, err := setupStore(c) if err != nil { return fmt.Errorf("can't setup store: %w", err) @@ -97,7 +92,7 @@ func run(c *cli.Context) error { } }() - err = setupEvilGlobals(c, _store, _forge) + err = setupEvilGlobals(c, _store) if err != nil { return fmt.Errorf("can't setup globals: %w", err) } @@ -107,7 +102,7 @@ func run(c *cli.Context) error { setupMetrics(&g, _store) g.Go(func() error { - return cron.Start(c.Context, _store, _forge) + return cron.Start(c.Context, _store) }) // start the grpc server @@ -130,7 +125,6 @@ func run(c *cli.Context) error { ) woodpeckerServer := woodpeckerGrpcServer.NewWoodpeckerServer( - _forge, server.Config.Services.Queue, server.Config.Services.Logs, server.Config.Services.Pubsub, @@ -270,17 +264,13 @@ func run(c *cli.Context) error { return g.Wait() } -func setupEvilGlobals(c *cli.Context, s store.Store, f forge.Forge) error { - // forge - server.Config.Services.Forge = f - +func setupEvilGlobals(c *cli.Context, s store.Store) error { // services server.Config.Services.Queue = setupQueue(c, s) server.Config.Services.Logs = logging.New() server.Config.Services.Pubsub = pubsub.New() - server.Config.Services.Membership = setupMembershipService(c, f) - - serviceMangager, err := services.NewManager(c, s) + server.Config.Services.Membership = setupMembershipService(c, s) + serviceMangager, err := services.NewManager(c, s, setup.Forge) if err != nil { return fmt.Errorf("could not setup service manager: %w", err) } diff --git a/cmd/server/setup.go b/cmd/server/setup.go index e2fa13e25..8df86cc86 100644 --- a/cmd/server/setup.go +++ b/cmd/server/setup.go @@ -18,9 +18,7 @@ package main import ( "context" "fmt" - "net/url" "os" - "strings" "time" "github.com/prometheus/client_golang/prometheus" @@ -31,13 +29,6 @@ import ( "go.woodpecker-ci.org/woodpecker/v2/server" "go.woodpecker-ci.org/woodpecker/v2/server/cache" - "go.woodpecker-ci.org/woodpecker/v2/server/forge" - "go.woodpecker-ci.org/woodpecker/v2/server/forge/addon" - "go.woodpecker-ci.org/woodpecker/v2/server/forge/bitbucket" - "go.woodpecker-ci.org/woodpecker/v2/server/forge/bitbucketdatacenter" - "go.woodpecker-ci.org/woodpecker/v2/server/forge/gitea" - "go.woodpecker-ci.org/woodpecker/v2/server/forge/github" - "go.woodpecker-ci.org/woodpecker/v2/server/forge/gitlab" "go.woodpecker-ci.org/woodpecker/v2/server/queue" "go.woodpecker-ci.org/woodpecker/v2/server/store" "go.woodpecker-ci.org/woodpecker/v2/server/store/datastore" @@ -100,103 +91,8 @@ func setupQueue(c *cli.Context, s store.Store) queue.Queue { return queue.WithTaskStore(queue.New(c.Context), s) } -func setupMembershipService(_ *cli.Context, r forge.Forge) cache.MembershipService { - return cache.NewMembershipService(r) -} - -// setupForge helper function to set up the forge from the CLI arguments. -func setupForge(c *cli.Context) (forge.Forge, error) { - switch { - case c.String("addon-forge") != "": - return addon.Load(c.String("addon-forge")) - case c.Bool("github"): - return setupGitHub(c) - case c.Bool("gitlab"): - return setupGitLab(c) - case c.Bool("bitbucket"): - return setupBitbucket(c) - case c.Bool("bitbucket-dc"): - return setupBitbucketDatacenter(c) - case c.Bool("gitea"): - return setupGitea(c) - default: - return nil, fmt.Errorf("version control system not configured") - } -} - -// setupBitbucket helper function to setup the Bitbucket forge from the CLI arguments. -func setupBitbucket(c *cli.Context) (forge.Forge, error) { - opts := &bitbucket.Opts{ - Client: c.String("bitbucket-client"), - Secret: c.String("bitbucket-secret"), - } - log.Trace().Msgf("forge (bitbucket) opts: %#v", opts) - return bitbucket.New(opts) -} - -// setupGitea helper function to set up the Gitea forge from the CLI arguments. -func setupGitea(c *cli.Context) (forge.Forge, error) { - server, err := url.Parse(c.String("gitea-server")) - if err != nil { - return nil, err - } - oauth2Server := c.String("gitea-oauth-server") - if oauth2Server != "" { - oauth2URL, err := url.Parse(oauth2Server) - if err != nil { - return nil, err - } - oauth2Server = strings.TrimRight(oauth2URL.String(), "/") - } - opts := gitea.Opts{ - URL: strings.TrimRight(server.String(), "/"), - OAuth2URL: oauth2Server, - Client: c.String("gitea-client"), - Secret: c.String("gitea-secret"), - SkipVerify: c.Bool("gitea-skip-verify"), - } - if len(opts.URL) == 0 { - return nil, fmt.Errorf("WOODPECKER_GITEA_URL must be set") - } - log.Trace().Msgf("forge (gitea) opts: %#v", opts) - return gitea.New(opts) -} - -// setupBitbucketDatacenter helper function to setup the Bitbucket DataCenter/Server forge from the CLI arguments. -func setupBitbucketDatacenter(c *cli.Context) (forge.Forge, error) { - opts := bitbucketdatacenter.Opts{ - URL: c.String("bitbucket-dc-server"), - Username: c.String("bitbucket-dc-git-username"), - Password: c.String("bitbucket-dc-git-password"), - ClientID: c.String("bitbucket-dc-client-id"), - ClientSecret: c.String("bitbucket-dc-client-secret"), - } - log.Trace().Msgf("Forge (bitbucketdatacenter) opts: %#v", opts) - return bitbucketdatacenter.New(opts) -} - -// setupGitLab helper function to setup the GitLab forge from the CLI arguments. -func setupGitLab(c *cli.Context) (forge.Forge, error) { - return gitlab.New(gitlab.Opts{ - URL: c.String("gitlab-server"), - ClientID: c.String("gitlab-client"), - ClientSecret: c.String("gitlab-secret"), - SkipVerify: c.Bool("gitlab-skip-verify"), - }) -} - -// setupGitHub helper function to setup the GitHub forge from the CLI arguments. -func setupGitHub(c *cli.Context) (forge.Forge, error) { - opts := github.Opts{ - URL: c.String("github-server"), - Client: c.String("github-client"), - Secret: c.String("github-secret"), - SkipVerify: c.Bool("github-skip-verify"), - MergeRef: c.Bool("github-merge-ref"), - OnlyPublic: c.Bool("github-public-only"), - } - log.Trace().Msgf("forge (github) opts: %#v", opts) - return github.New(opts) +func setupMembershipService(_ *cli.Context, _store store.Store) cache.MembershipService { + return cache.NewMembershipService(_store) } func setupMetrics(g *errgroup.Group, _store store.Store) { diff --git a/pipeline/rpc/proto/woodpecker_grpc.pb.go b/pipeline/rpc/proto/woodpecker_grpc.pb.go index 787f8f6e7..f591f4dfe 100644 --- a/pipeline/rpc/proto/woodpecker_grpc.pb.go +++ b/pipeline/rpc/proto/woodpecker_grpc.pb.go @@ -1,18 +1,3 @@ -// Copyright 2021 Woodpecker Authors -// Copyright 2011 Drone.IO Inc. -// -// 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. - // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 @@ -74,7 +59,7 @@ func NewWoodpeckerClient(cc grpc.ClientConnInterface) WoodpeckerClient { func (c *woodpeckerClient) Version(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*VersionResponse, error) { out := new(VersionResponse) - err := c.cc.Invoke(ctx, Woodpecker_Version_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/proto.Woodpecker/Version", in, out, opts...) if err != nil { return nil, err } @@ -83,7 +68,7 @@ func (c *woodpeckerClient) Version(ctx context.Context, in *Empty, opts ...grpc. func (c *woodpeckerClient) Next(ctx context.Context, in *NextRequest, opts ...grpc.CallOption) (*NextResponse, error) { out := new(NextResponse) - err := c.cc.Invoke(ctx, Woodpecker_Next_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/proto.Woodpecker/Next", in, out, opts...) if err != nil { return nil, err } @@ -92,7 +77,7 @@ func (c *woodpeckerClient) Next(ctx context.Context, in *NextRequest, opts ...gr func (c *woodpeckerClient) Init(ctx context.Context, in *InitRequest, opts ...grpc.CallOption) (*Empty, error) { out := new(Empty) - err := c.cc.Invoke(ctx, Woodpecker_Init_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/proto.Woodpecker/Init", in, out, opts...) if err != nil { return nil, err } @@ -101,7 +86,7 @@ func (c *woodpeckerClient) Init(ctx context.Context, in *InitRequest, opts ...gr func (c *woodpeckerClient) Wait(ctx context.Context, in *WaitRequest, opts ...grpc.CallOption) (*Empty, error) { out := new(Empty) - err := c.cc.Invoke(ctx, Woodpecker_Wait_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/proto.Woodpecker/Wait", in, out, opts...) if err != nil { return nil, err } @@ -110,7 +95,7 @@ func (c *woodpeckerClient) Wait(ctx context.Context, in *WaitRequest, opts ...gr func (c *woodpeckerClient) Done(ctx context.Context, in *DoneRequest, opts ...grpc.CallOption) (*Empty, error) { out := new(Empty) - err := c.cc.Invoke(ctx, Woodpecker_Done_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/proto.Woodpecker/Done", in, out, opts...) if err != nil { return nil, err } @@ -119,7 +104,7 @@ func (c *woodpeckerClient) Done(ctx context.Context, in *DoneRequest, opts ...gr func (c *woodpeckerClient) Extend(ctx context.Context, in *ExtendRequest, opts ...grpc.CallOption) (*Empty, error) { out := new(Empty) - err := c.cc.Invoke(ctx, Woodpecker_Extend_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/proto.Woodpecker/Extend", in, out, opts...) if err != nil { return nil, err } @@ -128,7 +113,7 @@ func (c *woodpeckerClient) Extend(ctx context.Context, in *ExtendRequest, opts . func (c *woodpeckerClient) Update(ctx context.Context, in *UpdateRequest, opts ...grpc.CallOption) (*Empty, error) { out := new(Empty) - err := c.cc.Invoke(ctx, Woodpecker_Update_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/proto.Woodpecker/Update", in, out, opts...) if err != nil { return nil, err } @@ -137,7 +122,7 @@ func (c *woodpeckerClient) Update(ctx context.Context, in *UpdateRequest, opts . func (c *woodpeckerClient) Log(ctx context.Context, in *LogRequest, opts ...grpc.CallOption) (*Empty, error) { out := new(Empty) - err := c.cc.Invoke(ctx, Woodpecker_Log_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/proto.Woodpecker/Log", in, out, opts...) if err != nil { return nil, err } @@ -146,7 +131,7 @@ func (c *woodpeckerClient) Log(ctx context.Context, in *LogRequest, opts ...grpc func (c *woodpeckerClient) RegisterAgent(ctx context.Context, in *RegisterAgentRequest, opts ...grpc.CallOption) (*RegisterAgentResponse, error) { out := new(RegisterAgentResponse) - err := c.cc.Invoke(ctx, Woodpecker_RegisterAgent_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/proto.Woodpecker/RegisterAgent", in, out, opts...) if err != nil { return nil, err } @@ -164,7 +149,7 @@ func (c *woodpeckerClient) UnregisterAgent(ctx context.Context, in *Empty, opts func (c *woodpeckerClient) ReportHealth(ctx context.Context, in *ReportHealthRequest, opts ...grpc.CallOption) (*Empty, error) { out := new(Empty) - err := c.cc.Invoke(ctx, Woodpecker_ReportHealth_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/proto.Woodpecker/ReportHealth", in, out, opts...) if err != nil { return nil, err } @@ -249,7 +234,7 @@ func _Woodpecker_Version_Handler(srv interface{}, ctx context.Context, dec func( } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Woodpecker_Version_FullMethodName, + FullMethod: "/proto.Woodpecker/Version", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(WoodpeckerServer).Version(ctx, req.(*Empty)) @@ -267,7 +252,7 @@ func _Woodpecker_Next_Handler(srv interface{}, ctx context.Context, dec func(int } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Woodpecker_Next_FullMethodName, + FullMethod: "/proto.Woodpecker/Next", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(WoodpeckerServer).Next(ctx, req.(*NextRequest)) @@ -285,7 +270,7 @@ func _Woodpecker_Init_Handler(srv interface{}, ctx context.Context, dec func(int } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Woodpecker_Init_FullMethodName, + FullMethod: "/proto.Woodpecker/Init", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(WoodpeckerServer).Init(ctx, req.(*InitRequest)) @@ -303,7 +288,7 @@ func _Woodpecker_Wait_Handler(srv interface{}, ctx context.Context, dec func(int } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Woodpecker_Wait_FullMethodName, + FullMethod: "/proto.Woodpecker/Wait", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(WoodpeckerServer).Wait(ctx, req.(*WaitRequest)) @@ -321,7 +306,7 @@ func _Woodpecker_Done_Handler(srv interface{}, ctx context.Context, dec func(int } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Woodpecker_Done_FullMethodName, + FullMethod: "/proto.Woodpecker/Done", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(WoodpeckerServer).Done(ctx, req.(*DoneRequest)) @@ -339,7 +324,7 @@ func _Woodpecker_Extend_Handler(srv interface{}, ctx context.Context, dec func(i } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Woodpecker_Extend_FullMethodName, + FullMethod: "/proto.Woodpecker/Extend", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(WoodpeckerServer).Extend(ctx, req.(*ExtendRequest)) @@ -357,7 +342,7 @@ func _Woodpecker_Update_Handler(srv interface{}, ctx context.Context, dec func(i } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Woodpecker_Update_FullMethodName, + FullMethod: "/proto.Woodpecker/Update", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(WoodpeckerServer).Update(ctx, req.(*UpdateRequest)) @@ -375,7 +360,7 @@ func _Woodpecker_Log_Handler(srv interface{}, ctx context.Context, dec func(inte } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Woodpecker_Log_FullMethodName, + FullMethod: "/proto.Woodpecker/Log", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(WoodpeckerServer).Log(ctx, req.(*LogRequest)) @@ -393,7 +378,7 @@ func _Woodpecker_RegisterAgent_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Woodpecker_RegisterAgent_FullMethodName, + FullMethod: "/proto.Woodpecker/RegisterAgent", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(WoodpeckerServer).RegisterAgent(ctx, req.(*RegisterAgentRequest)) @@ -429,7 +414,7 @@ func _Woodpecker_ReportHealth_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Woodpecker_ReportHealth_FullMethodName, + FullMethod: "/proto.Woodpecker/ReportHealth", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(WoodpeckerServer).ReportHealth(ctx, req.(*ReportHealthRequest)) @@ -493,10 +478,6 @@ var Woodpecker_ServiceDesc = grpc.ServiceDesc{ Metadata: "woodpecker.proto", } -const ( - WoodpeckerAuth_Auth_FullMethodName = "/proto.WoodpeckerAuth/Auth" -) - // WoodpeckerAuthClient is the client API for WoodpeckerAuth service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. @@ -514,7 +495,7 @@ func NewWoodpeckerAuthClient(cc grpc.ClientConnInterface) WoodpeckerAuthClient { func (c *woodpeckerAuthClient) Auth(ctx context.Context, in *AuthRequest, opts ...grpc.CallOption) (*AuthResponse, error) { out := new(AuthResponse) - err := c.cc.Invoke(ctx, WoodpeckerAuth_Auth_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/proto.WoodpeckerAuth/Auth", in, out, opts...) if err != nil { return nil, err } @@ -559,7 +540,7 @@ func _WoodpeckerAuth_Auth_Handler(srv interface{}, ctx context.Context, dec func } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: WoodpeckerAuth_Auth_FullMethodName, + FullMethod: "/proto.WoodpeckerAuth/Auth", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(WoodpeckerAuthServer).Auth(ctx, req.(*AuthRequest)) diff --git a/server/api/cron.go b/server/api/cron.go index c7bcb6371..7a9f797fc 100644 --- a/server/api/cron.go +++ b/server/api/cron.go @@ -20,6 +20,7 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" "go.woodpecker-ci.org/woodpecker/v2/server" cronScheduler "go.woodpecker-ci.org/woodpecker/v2/server/cron" @@ -80,7 +81,7 @@ func RunCron(c *gin.Context) { return } - repo, newPipeline, err := cronScheduler.CreatePipeline(c, _store, server.Config.Services.Forge, cron) + repo, newPipeline, err := cronScheduler.CreatePipeline(c, _store, cron) if err != nil { c.String(http.StatusInternalServerError, "Error creating pipeline for cron %q. %s", id, err) return @@ -109,7 +110,12 @@ func PostCron(c *gin.Context) { repo := session.Repo(c) user := session.User(c) _store := store.FromContext(c) - forge := server.Config.Services.Forge + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from repo") + c.AbortWithStatus(http.StatusInternalServerError) + return + } in := new(model.Cron) if err := c.Bind(in); err != nil { @@ -137,7 +143,7 @@ func PostCron(c *gin.Context) { if in.Branch != "" { // check if branch exists on forge - _, err := forge.BranchHead(c, user, repo, in.Branch) + _, err := _forge.BranchHead(c, user, repo, in.Branch) if err != nil { c.String(http.StatusBadRequest, "Error inserting cron. branch not resolved: %s", err) return @@ -166,7 +172,12 @@ func PatchCron(c *gin.Context) { repo := session.Repo(c) user := session.User(c) _store := store.FromContext(c) - forge := server.Config.Services.Forge + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from repo") + c.AbortWithStatus(http.StatusInternalServerError) + return + } id, err := strconv.ParseInt(c.Param("cron"), 10, 64) if err != nil { @@ -188,7 +199,7 @@ func PatchCron(c *gin.Context) { } if in.Branch != "" { // check if branch exists on forge - _, err := forge.BranchHead(c, user, repo, in.Branch) + _, err := _forge.BranchHead(c, user, repo, in.Branch) if err != nil { c.String(http.StatusBadRequest, "Error inserting cron. branch not resolved: %s", err) return diff --git a/server/api/helper.go b/server/api/helper.go index 51646e370..8b621487b 100644 --- a/server/api/helper.go +++ b/server/api/helper.go @@ -19,6 +19,7 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" "go.woodpecker-ci.org/woodpecker/v2/server" "go.woodpecker-ci.org/woodpecker/v2/server/forge" @@ -54,7 +55,12 @@ func handleDBError(c *gin.Context, err error) { // If the forge has a refresh token, the current access token may be stale. // Therefore, we should refresh prior to dispatching the job. func refreshUserToken(c *gin.Context, user *model.User) { - _forge := server.Config.Services.Forge _store := store.FromContext(c) + _forge, err := server.Config.Services.Manager.ForgeFromUser(user) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from user") + c.AbortWithStatus(http.StatusInternalServerError) + return + } forge.Refresh(c, _forge, _store, user) } diff --git a/server/api/hook.go b/server/api/hook.go index 4318ae480..b2e7251ba 100644 --- a/server/api/hook.go +++ b/server/api/hook.go @@ -104,7 +104,13 @@ func BlockTilQueueHasRunningItem(c *gin.Context) { // @Param hook body object true "the webhook payload; forge is automatically detected" func PostHook(c *gin.Context) { _store := store.FromContext(c) - _forge := server.Config.Services.Forge + + _forge, err := server.Config.Services.Manager.ForgeMain() // TODO: get the forge for the specific repo somehow + if err != nil { + log.Error().Err(err).Msg("Cannot get main forge") + c.AbortWithStatus(http.StatusInternalServerError) + return + } // // 1. Parse webhook diff --git a/server/api/login.go b/server/api/login.go index 4daea4a57..349861ce1 100644 --- a/server/api/login.go +++ b/server/api/login.go @@ -43,7 +43,12 @@ func HandleLogin(c *gin.Context) { func HandleAuth(c *gin.Context) { _store := store.FromContext(c) - _forge := server.Config.Services.Forge + _forge, err := server.Config.Services.Manager.ForgeMain() + if err != nil { + _ = c.AbortWithError(http.StatusInternalServerError, err) + return + } + forgeID := int64(1) // TODO: replace with forge id when multiple forges are supported // when dealing with redirects, we may need to adjust the content type. I // cannot, however, remember why, so need to revisit this line. @@ -68,12 +73,12 @@ func HandleAuth(c *gin.Context) { // get the user from the database u, err := _store.GetUserRemoteID(tmpuser.ForgeRemoteID, tmpuser.Login) - if err != nil { - if !errors.Is(err, types.RecordNotExist) { - _ = c.AbortWithError(http.StatusInternalServerError, err) - return - } + if err != nil && !errors.Is(err, types.RecordNotExist) { + _ = c.AbortWithError(http.StatusInternalServerError, err) + return + } + if errors.Is(err, types.RecordNotExist) { // if self-registration is disabled we should return a not authorized error if !server.Config.Permissions.Open && !server.Config.Permissions.Admins.IsAdmin(tmpuser) { log.Error().Msgf("cannot register %s. registration closed", tmpuser.Login) @@ -100,6 +105,7 @@ func HandleAuth(c *gin.Context) { Secret: tmpuser.Secret, Email: tmpuser.Email, Avatar: tmpuser.Avatar, + ForgeID: forgeID, Hash: base32.StdEncoding.EncodeToString( securecookie.GenerateRandomKey(32), ), @@ -129,6 +135,7 @@ func HandleAuth(c *gin.Context) { Name: u.Login, IsUser: true, Private: false, + ForgeID: u.ForgeID, } if err := _store.OrgCreate(org); err != nil { log.Error().Err(err).Msgf("on user creation, could create org for user") @@ -228,14 +235,21 @@ func GetLogout(c *gin.Context) { func GetLoginToken(c *gin.Context) { _store := store.FromContext(c) + _forge, err := server.Config.Services.Manager.ForgeMain() // TODO: get selected forge from auth request + if err != nil { + log.Error().Err(err).Msg("Cannot get main forge") + c.AbortWithStatus(http.StatusInternalServerError) + return + } + in := &tokenPayload{} - err := c.Bind(in) + err = c.Bind(in) if err != nil { _ = c.AbortWithError(http.StatusBadRequest, err) return } - login, err := server.Config.Services.Forge.Auth(c, in.Access, in.Refresh) + login, err := _forge.Auth(c, in.Access, in.Refresh) if err != nil { _ = c.AbortWithError(http.StatusUnauthorized, err) return diff --git a/server/api/org.go b/server/api/org.go index 09d677221..c30d55ebe 100644 --- a/server/api/org.go +++ b/server/api/org.go @@ -68,6 +68,13 @@ func GetOrgPermissions(c *gin.Context) { user := session.User(c) _store := store.FromContext(c) + _forge, err := server.Config.Services.Manager.ForgeFromUser(user) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from user") + c.AbortWithStatus(http.StatusInternalServerError) + return + } + orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64) if err != nil { c.String(http.StatusBadRequest, "Error parsing org id. %s", err) @@ -96,7 +103,7 @@ func GetOrgPermissions(c *gin.Context) { return } - perm, err := server.Config.Services.Membership.Get(c, user, org.Name) + perm, err := server.Config.Services.Membership.Get(c, _forge, user, org.Name) if err != nil { c.String(http.StatusInternalServerError, "Error getting membership for %d. %s", orgID, err) return @@ -116,6 +123,13 @@ func GetOrgPermissions(c *gin.Context) { // @Param org_full_name path string true "the organizations full-name / slug" func LookupOrg(c *gin.Context) { _store := store.FromContext(c) + user := session.User(c) + _forge, err := server.Config.Services.Manager.ForgeFromUser(user) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from user") + c.AbortWithStatus(http.StatusInternalServerError) + return + } orgFullName := strings.TrimLeft(c.Param("org_full_name"), "/") @@ -137,7 +151,7 @@ func LookupOrg(c *gin.Context) { c.AbortWithStatus(http.StatusNotFound) return } else if !user.Admin { - perm, err := server.Config.Services.Membership.Get(c, user, org.Name) + perm, err := server.Config.Services.Membership.Get(c, _forge, user, org.Name) if err != nil { log.Error().Err(err).Msg("failed to check membership") c.Status(http.StatusInternalServerError) diff --git a/server/api/pipeline.go b/server/api/pipeline.go index 0c5cdc5e1..8803ab95f 100644 --- a/server/api/pipeline.go +++ b/server/api/pipeline.go @@ -27,6 +27,7 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" "go.woodpecker-ci.org/woodpecker/v2/server" "go.woodpecker-ci.org/woodpecker/v2/server/model" @@ -48,10 +49,16 @@ import ( func CreatePipeline(c *gin.Context) { _store := store.FromContext(c) repo := session.Repo(c) + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from repo") + c.AbortWithStatus(http.StatusInternalServerError) + return + } // parse create options var opts model.PipelineOptions - err := json.NewDecoder(c.Request.Body).Decode(&opts) + err = json.NewDecoder(c.Request.Body).Decode(&opts) if err != nil { _ = c.AbortWithError(http.StatusBadRequest, err) return @@ -59,7 +66,7 @@ func CreatePipeline(c *gin.Context) { user := session.User(c) - lastCommit, _ := server.Config.Services.Forge.BranchHead(c, user, repo, opts.Branch) + lastCommit, _ := _forge.BranchHead(c, user, repo, opts.Branch) tmpPipeline := createTmpPipeline(model.EventManual, lastCommit, user, &opts) @@ -332,6 +339,13 @@ func CancelPipeline(c *gin.Context) { _store := store.FromContext(c) repo := session.Repo(c) user := session.User(c) + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from repo") + c.AbortWithStatus(http.StatusInternalServerError) + return + } + num, _ := strconv.ParseInt(c.Params.ByName("number"), 10, 64) pl, err := _store.GetPipelineNumber(repo, num) @@ -340,7 +354,7 @@ func CancelPipeline(c *gin.Context) { return } - if err := pipeline.Cancel(c, _store, repo, user, pl); err != nil { + if err := pipeline.Cancel(c, _forge, _store, repo, user, pl); err != nil { handlePipelineErr(c, err) } else { c.Status(http.StatusNoContent) diff --git a/server/api/repo.go b/server/api/repo.go index e9c4c3138..6d03dcede 100644 --- a/server/api/repo.go +++ b/server/api/repo.go @@ -45,9 +45,14 @@ import ( // @Param Authorization header string true "Insert your personal access token" default(Bearer ) // @Param forge_remote_id query string true "the id of a repository at the forge" func PostRepo(c *gin.Context) { - forge := server.Config.Services.Forge _store := store.FromContext(c) user := session.User(c) + _forge, err := server.Config.Services.Manager.ForgeFromUser(user) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from user") + c.AbortWithStatus(http.StatusInternalServerError) + return + } forgeRemoteID := model.ForgeRemoteID(c.Query("forge_remote_id")) if !forgeRemoteID.IsValid() { @@ -67,7 +72,7 @@ func PostRepo(c *gin.Context) { return } - from, err := forge.Repo(c, user, forgeRemoteID, "", "") + from, err := _forge.Repo(c, user, forgeRemoteID, "", "") if err != nil { c.String(http.StatusInternalServerError, "Could not fetch repository from forge.") return @@ -138,7 +143,7 @@ func PostRepo(c *gin.Context) { // create an org if it doesn't exist yet if errors.Is(err, types.RecordNotExist) { - org, err = forge.Org(c, user, repo.Owner) + org, err = _forge.Org(c, user, repo.Owner) if err != nil { msg := "could not fetch organization from forge." log.Error().Err(err).Msg(msg) @@ -146,6 +151,7 @@ func PostRepo(c *gin.Context) { return } + org.ForgeID = user.ForgeID err = _store.OrgCreate(org) if err != nil { msg := "could not create organization in store." @@ -157,7 +163,7 @@ func PostRepo(c *gin.Context) { repo.OrgID = org.ID - err = forge.Activate(c, user, repo, hookURL) + err = _forge.Activate(c, user, repo, hookURL) if err != nil { msg := "could not create webhook in forge." log.Error().Err(err).Msg(msg) @@ -168,6 +174,7 @@ func PostRepo(c *gin.Context) { if enabledOnce { err = _store.UpdateRepo(repo) } else { + repo.ForgeID = user.ForgeID // TODO: allow to use other connected forges of the user err = _store.CreateRepo(repo) } if err != nil { @@ -342,9 +349,14 @@ func GetRepoPermissions(c *gin.Context) { func GetRepoBranches(c *gin.Context) { repo := session.Repo(c) user := session.User(c) - f := server.Config.Services.Forge + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from repo") + c.AbortWithStatus(http.StatusInternalServerError) + return + } - branches, err := f.Branches(c, user, repo, session.Pagination(c)) + branches, err := _forge.Branches(c, user, repo, session.Pagination(c)) if err != nil { _ = c.AbortWithError(http.StatusInternalServerError, err) return @@ -367,9 +379,14 @@ func GetRepoBranches(c *gin.Context) { func GetRepoPullRequests(c *gin.Context) { repo := session.Repo(c) user := session.User(c) - f := server.Config.Services.Forge + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from repo") + c.AbortWithStatus(http.StatusInternalServerError) + return + } - prs, err := f.PullRequests(c, user, repo, session.Pagination(c)) + prs, err := _forge.PullRequests(c, user, repo, session.Pagination(c)) if err != nil { _ = c.AbortWithError(http.StatusInternalServerError, err) return @@ -390,9 +407,14 @@ func GetRepoPullRequests(c *gin.Context) { func DeleteRepo(c *gin.Context) { remove, _ := strconv.ParseBool(c.Query("remove")) _store := store.FromContext(c) - repo := session.Repo(c) user := session.User(c) + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from repo") + c.AbortWithStatus(http.StatusInternalServerError) + return + } repo.IsActive = false repo.UserID = 0 @@ -409,7 +431,7 @@ func DeleteRepo(c *gin.Context) { } } - if err := server.Config.Services.Forge.Deactivate(c, user, repo, server.Config.Server.WebhookHost); err != nil { + if err := _forge.Deactivate(c, user, repo, server.Config.Server.WebhookHost); err != nil { _ = c.AbortWithError(http.StatusInternalServerError, err) return } @@ -445,10 +467,15 @@ func RepairRepo(c *gin.Context) { // @Param repo_id path int true "the repository id" // @Param to query string true "the username to move the repository to" func MoveRepo(c *gin.Context) { - forge := server.Config.Services.Forge _store := store.FromContext(c) repo := session.Repo(c) user := session.User(c) + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from repo") + c.AbortWithStatus(http.StatusInternalServerError) + return + } to, exists := c.GetQuery("to") if !exists { @@ -463,7 +490,7 @@ func MoveRepo(c *gin.Context) { return } - from, err := forge.Repo(c, user, "", owner, name) + from, err := _forge.Repo(c, user, "", owner, name) if err != nil { _ = c.AbortWithError(http.StatusInternalServerError, err) return @@ -508,10 +535,10 @@ func MoveRepo(c *gin.Context) { sig, ) - if err := forge.Deactivate(c, user, repo, host); err != nil { + if err := _forge.Deactivate(c, user, repo, host); err != nil { log.Trace().Err(err).Msgf("deactivate repo '%s' for move to activate later, got an error", repo.FullName) } - if err := forge.Activate(c, user, repo, hookURL); err != nil { + if err := _forge.Activate(c, user, repo, hookURL); err != nil { c.String(http.StatusInternalServerError, err.Error()) return } @@ -571,8 +598,13 @@ func RepairAllRepos(c *gin.Context) { } func repairRepo(c *gin.Context, repo *model.Repo, withPerms, skipOnErr bool) { - forge := server.Config.Services.Forge _store := store.FromContext(c) + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from repo") + c.AbortWithStatus(http.StatusInternalServerError) + return + } user, err := _store.GetUser(repo.UserID) if err != nil { @@ -603,7 +635,7 @@ func repairRepo(c *gin.Context, repo *model.Repo, withPerms, skipOnErr bool) { sig, ) - from, err := forge.Repo(c, user, repo.ForgeRemoteID, repo.Owner, repo.Name) + from, err := _forge.Repo(c, user, repo.ForgeRemoteID, repo.Owner, repo.Name) if err != nil { log.Error().Err(err).Msgf("get repo '%s/%s' from forge", repo.Owner, repo.Name) if !skipOnErr { @@ -636,10 +668,10 @@ func repairRepo(c *gin.Context, repo *model.Repo, withPerms, skipOnErr bool) { } } - if err := forge.Deactivate(c, user, repo, host); err != nil { + if err := _forge.Deactivate(c, user, repo, host); err != nil { log.Trace().Err(err).Msgf("deactivate repo '%s' to repair failed", repo.FullName) } - if err := forge.Activate(c, user, repo, hookURL); err != nil { + if err := _forge.Activate(c, user, repo, hookURL); err != nil { c.String(http.StatusInternalServerError, err.Error()) return } diff --git a/server/api/user.go b/server/api/user.go index f33d4d32e..d24849bf3 100644 --- a/server/api/user.go +++ b/server/api/user.go @@ -21,6 +21,7 @@ import ( "github.com/gin-gonic/gin" "github.com/gorilla/securecookie" + "github.com/rs/zerolog/log" "go.woodpecker-ci.org/woodpecker/v2/server" "go.woodpecker-ci.org/woodpecker/v2/server/model" @@ -86,9 +87,14 @@ func GetFeed(c *gin.Context) { // @Param all query bool false "query all repos, including inactive ones" func GetRepos(c *gin.Context) { _store := store.FromContext(c) - _forge := server.Config.Services.Forge - user := session.User(c) + _forge, err := server.Config.Services.Manager.ForgeFromUser(user) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from user") + c.AbortWithStatus(http.StatusInternalServerError) + return + } + all, _ := strconv.ParseBool(c.Query("all")) if all { diff --git a/server/api/users.go b/server/api/users.go index 30fd8278f..c35339eef 100644 --- a/server/api/users.go +++ b/server/api/users.go @@ -132,6 +132,8 @@ func PostUser(c *gin.Context) { Hash: base32.StdEncoding.EncodeToString( securecookie.GenerateRandomKey(32), ), + ForgeID: 1, // TODO: replace with forge id when multiple forges are supported + ForgeRemoteID: model.ForgeRemoteID("0"), // TODO: search for the user in the forge and get the remote id } if err = user.Validate(); err != nil { c.String(http.StatusBadRequest, err.Error()) diff --git a/server/cache/membership.go b/server/cache/membership.go index 45b6af5d1..38926540f 100644 --- a/server/cache/membership.go +++ b/server/cache/membership.go @@ -23,39 +23,39 @@ import ( "go.woodpecker-ci.org/woodpecker/v2/server/forge" "go.woodpecker-ci.org/woodpecker/v2/server/model" + "go.woodpecker-ci.org/woodpecker/v2/server/store" ) // MembershipService is a service to check for user membership. type MembershipService interface { // Get returns if the user is a member of the organization. - Get(ctx context.Context, u *model.User, org string) (*model.OrgPerm, error) + Get(ctx context.Context, _forge forge.Forge, u *model.User, org string) (*model.OrgPerm, error) } type membershipCache struct { - forge forge.Forge cache *ttlcache.Cache[string, *model.OrgPerm] + store store.Store ttl time.Duration } // NewMembershipService creates a new membership service. -func NewMembershipService(f forge.Forge) MembershipService { - //nolint:gomnd +func NewMembershipService(_store store.Store) MembershipService { return &membershipCache{ - ttl: 10 * time.Minute, - forge: f, + ttl: 10 * time.Minute, //nolint: gomnd + store: _store, cache: ttlcache.New(ttlcache.WithDisableTouchOnHit[string, *model.OrgPerm]()), } } // Get returns if the user is a member of the organization. -func (c *membershipCache) Get(ctx context.Context, u *model.User, org string) (*model.OrgPerm, error) { +func (c *membershipCache) Get(ctx context.Context, _forge forge.Forge, u *model.User, org string) (*model.OrgPerm, error) { key := fmt.Sprintf("%s-%s", u.ForgeRemoteID, org) item := c.cache.Get(key) if item != nil && !item.IsExpired() { return item.Value(), nil } - perm, err := c.forge.OrgMembership(ctx, u, org) + perm, err := _forge.OrgMembership(ctx, u, org) if err != nil { return nil, err } diff --git a/server/config.go b/server/config.go index 84b56c29a..e8ac177ee 100644 --- a/server/config.go +++ b/server/config.go @@ -21,7 +21,6 @@ import ( "time" "go.woodpecker-ci.org/woodpecker/v2/server/cache" - "go.woodpecker-ci.org/woodpecker/v2/server/forge" "go.woodpecker-ci.org/woodpecker/v2/server/logging" "go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/pubsub" @@ -35,9 +34,8 @@ var Config = struct { Pubsub *pubsub.Publisher Queue queue.Queue Logs logging.Log - Forge forge.Forge Membership cache.MembershipService - Manager *services.Manager + Manager services.Manager } Server struct { Key string diff --git a/server/cron/cron.go b/server/cron/cron.go index a5dfc27fa..b3331f46c 100644 --- a/server/cron/cron.go +++ b/server/cron/cron.go @@ -22,6 +22,7 @@ import ( "github.com/robfig/cron" "github.com/rs/zerolog/log" + "go.woodpecker-ci.org/woodpecker/v2/server" "go.woodpecker-ci.org/woodpecker/v2/server/forge" "go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/pipeline" @@ -37,7 +38,7 @@ const ( ) // Start starts the cron scheduler loop -func Start(ctx context.Context, store store.Store, forge forge.Forge) error { +func Start(ctx context.Context, store store.Store) error { for { select { case <-ctx.Done(): @@ -54,7 +55,7 @@ func Start(ctx context.Context, store store.Store, forge forge.Forge) error { } for _, cron := range crons { - if err := runCron(ctx, store, forge, cron, now); err != nil { + if err := runCron(ctx, store, cron, now); err != nil { log.Error().Err(err).Int64("cronID", cron.ID).Msg("run cron failed") } } @@ -77,7 +78,7 @@ func CalcNewNext(schedule string, now time.Time) (time.Time, error) { return c.Next(now), nil } -func runCron(ctx context.Context, store store.Store, forge forge.Forge, cron *model.Cron, now time.Time) error { +func runCron(ctx context.Context, store store.Store, cron *model.Cron, now time.Time) error { log.Trace().Msgf("cron: run id[%d]", cron.ID) newNext, err := CalcNewNext(cron.Schedule, now) @@ -95,7 +96,7 @@ func runCron(ctx context.Context, store store.Store, forge forge.Forge, cron *mo return nil } - repo, newPipeline, err := CreatePipeline(ctx, store, forge, cron) + repo, newPipeline, err := CreatePipeline(ctx, store, cron) if err != nil { return err } @@ -104,12 +105,17 @@ func runCron(ctx context.Context, store store.Store, forge forge.Forge, cron *mo return err } -func CreatePipeline(ctx context.Context, store store.Store, f forge.Forge, cron *model.Cron) (*model.Repo, *model.Pipeline, error) { +func CreatePipeline(ctx context.Context, store store.Store, cron *model.Cron) (*model.Repo, *model.Pipeline, error) { repo, err := store.GetRepo(cron.RepoID) if err != nil { return nil, nil, err } + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + return nil, nil, err + } + if cron.Branch == "" { // fallback to the repos default branch cron.Branch = repo.Branch @@ -123,9 +129,9 @@ func CreatePipeline(ctx context.Context, store store.Store, f forge.Forge, cron // If the forge has a refresh token, the current access token // may be stale. Therefore, we should refresh prior to dispatching // the pipeline. - forge.Refresh(ctx, f, store, creator) + forge.Refresh(ctx, _forge, store, creator) - commit, err := f.BranchHead(ctx, creator, repo, cron.Branch) + commit, err := _forge.BranchHead(ctx, creator, repo, cron.Branch) if err != nil { return nil, nil, err } diff --git a/server/cron/cron_test.go b/server/cron/cron_test.go index 0b170cfe9..61f1a2538 100644 --- a/server/cron/cron_test.go +++ b/server/cron/cron_test.go @@ -22,13 +22,16 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "go.woodpecker-ci.org/woodpecker/v2/server" mocks_forge "go.woodpecker-ci.org/woodpecker/v2/server/forge/mocks" "go.woodpecker-ci.org/woodpecker/v2/server/model" + mocks_manager "go.woodpecker-ci.org/woodpecker/v2/server/services/mocks" mocks_store "go.woodpecker-ci.org/woodpecker/v2/server/store/mocks" ) -func TestCreateBuild(t *testing.T) { - forge := mocks_forge.NewForge(t) +func TestCreatePipeline(t *testing.T) { + _manager := mocks_manager.NewManager(t) + _forge := mocks_forge.NewForge(t) store := mocks_store.NewStore(t) ctx := context.Background() @@ -47,12 +50,14 @@ func TestCreateBuild(t *testing.T) { // mock things store.On("GetRepo", mock.Anything).Return(repo1, nil) store.On("GetUser", mock.Anything).Return(creator, nil) - forge.On("BranchHead", mock.Anything, creator, repo1, "default").Return(&model.Commit{ + _forge.On("BranchHead", mock.Anything, creator, repo1, "default").Return(&model.Commit{ ForgeURL: "https://example.com/sha1", SHA: "sha1", }, nil) + _manager.On("ForgeFromRepo", repo1).Return(_forge, nil) + server.Config.Services.Manager = _manager - _, pipeline, err := CreatePipeline(ctx, store, forge, &model.Cron{ + _, pipeline, err := CreatePipeline(ctx, store, &model.Cron{ Name: "test", }) assert.NoError(t, err) diff --git a/server/forge/setup/setup.go b/server/forge/setup/setup.go new file mode 100644 index 000000000..d5ce8f742 --- /dev/null +++ b/server/forge/setup/setup.go @@ -0,0 +1,134 @@ +package setup + +import ( + "fmt" + "net/url" + "strings" + + "github.com/rs/zerolog/log" + + "go.woodpecker-ci.org/woodpecker/v2/server/forge" + "go.woodpecker-ci.org/woodpecker/v2/server/forge/addon" + "go.woodpecker-ci.org/woodpecker/v2/server/forge/bitbucket" + "go.woodpecker-ci.org/woodpecker/v2/server/forge/bitbucketdatacenter" + "go.woodpecker-ci.org/woodpecker/v2/server/forge/gitea" + "go.woodpecker-ci.org/woodpecker/v2/server/forge/github" + "go.woodpecker-ci.org/woodpecker/v2/server/forge/gitlab" + "go.woodpecker-ci.org/woodpecker/v2/server/model" +) + +func Forge(forge *model.Forge) (forge.Forge, error) { + switch forge.Type { + case model.ForgeTypeAddon: + return setupAddon(forge) + case model.ForgeTypeGithub: + return setupGitHub(forge) + case model.ForgeTypeGitlab: + return setupGitLab(forge) + case model.ForgeTypeBitbucket: + return setupBitbucket(forge) + case model.ForgeTypeGitea: + return setupGitea(forge) + case model.ForgeTypeBitbucketDatacenter: + return setupBitbucketDatacenter(forge) + default: + return nil, fmt.Errorf("forge not configured") + } +} + +func setupBitbucket(forge *model.Forge) (forge.Forge, error) { + opts := &bitbucket.Opts{ + Client: forge.Client, + Secret: forge.ClientSecret, + } + log.Trace().Msgf("Forge (bitbucket) opts: %#v", opts) + return bitbucket.New(opts) +} + +func setupGitea(forge *model.Forge) (forge.Forge, error) { + server, err := url.Parse(forge.URL) + if err != nil { + return nil, err + } + + oauthURL, ok := forge.AdditionalOptions["oauth-server"].(string) + if !ok { + return nil, fmt.Errorf("missing oauth-server") + } + + opts := gitea.Opts{ + URL: strings.TrimRight(server.String(), "/"), + Client: forge.Client, + Secret: forge.ClientSecret, + SkipVerify: forge.SkipVerify, + OAuth2URL: oauthURL, + } + if len(opts.URL) == 0 { + return nil, fmt.Errorf("WOODPECKER_GITEA_URL must be set") + } + log.Trace().Msgf("Forge (gitea) opts: %#v", opts) + return gitea.New(opts) +} + +func setupGitLab(forge *model.Forge) (forge.Forge, error) { + return gitlab.New(gitlab.Opts{ + URL: forge.URL, + ClientID: forge.Client, + ClientSecret: forge.ClientSecret, + SkipVerify: forge.SkipVerify, + }) +} + +func setupGitHub(forge *model.Forge) (forge.Forge, error) { + mergeRef, ok := forge.AdditionalOptions["merge-ref"].(bool) + if !ok { + return nil, fmt.Errorf("missing merge-ref") + } + + publicOnly, ok := forge.AdditionalOptions["public-only"].(bool) + if !ok { + return nil, fmt.Errorf("missing public-only") + } + + opts := github.Opts{ + URL: forge.URL, + Client: forge.Client, + Secret: forge.ClientSecret, + SkipVerify: forge.SkipVerify, + MergeRef: mergeRef, + OnlyPublic: publicOnly, + } + log.Trace().Msgf("Forge (github) opts: %#v", opts) + return github.New(opts) +} + +func setupBitbucketDatacenter(forge *model.Forge) (forge.Forge, error) { + gitUsername, ok := forge.AdditionalOptions["git-username"].(string) + if !ok { + return nil, fmt.Errorf("missing git-username") + } + gitPassword, ok := forge.AdditionalOptions["git-password"].(string) + if !ok { + return nil, fmt.Errorf("missing git-password") + } + + opts := bitbucketdatacenter.Opts{ + URL: forge.URL, + ClientID: forge.Client, + ClientSecret: forge.ClientSecret, + Username: gitUsername, + Password: gitPassword, + } + log.Trace().Msgf("Forge (bitbucketdatacenter) opts: %#v", opts) + return bitbucketdatacenter.New(opts) +} + +func setupAddon(forge *model.Forge) (forge.Forge, error) { + executable, ok := forge.AdditionalOptions["executable"].(string) + if !ok { + return nil, fmt.Errorf("missing git-username") + } + + log.Trace().Msgf("Forge (addon) executable: %#v", executable) + return addon.Load(executable) +} diff --git a/server/grpc/rpc.go b/server/grpc/rpc.go index c4293c6ab..59e27885d 100644 --- a/server/grpc/rpc.go +++ b/server/grpc/rpc.go @@ -31,6 +31,7 @@ import ( grpcMetadata "google.golang.org/grpc/metadata" "go.woodpecker-ci.org/woodpecker/v2/pipeline/rpc" + "go.woodpecker-ci.org/woodpecker/v2/server" "go.woodpecker-ci.org/woodpecker/v2/server/forge" "go.woodpecker-ci.org/woodpecker/v2/server/logging" "go.woodpecker-ci.org/woodpecker/v2/server/model" @@ -41,7 +42,6 @@ import ( ) type RPC struct { - forge forge.Forge queue queue.Queue pubsub *pubsub.Publisher logger logging.Log @@ -418,11 +418,17 @@ func (s *RPC) updateForgeStatus(ctx context.Context, repo *model.Repo, pipeline return } - forge.Refresh(ctx, s.forge, s.store, user) + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + log.Error().Err(err).Msgf("can not get forge for repo '%s'", repo.FullName) + return + } + + forge.Refresh(ctx, _forge, s.store, user) // only do status updates for parent steps if workflow != nil { - err = s.forge.Status(ctx, user, repo, pipeline, workflow) + err = _forge.Status(ctx, user, repo, pipeline, workflow) if err != nil { log.Error().Err(err).Msgf("error setting commit status for %s/%d", repo.FullName, pipeline.Number) } diff --git a/server/grpc/server.go b/server/grpc/server.go index 83a983351..81b4dc8b1 100644 --- a/server/grpc/server.go +++ b/server/grpc/server.go @@ -23,7 +23,6 @@ import ( "go.woodpecker-ci.org/woodpecker/v2/pipeline/rpc" "go.woodpecker-ci.org/woodpecker/v2/pipeline/rpc/proto" - "go.woodpecker-ci.org/woodpecker/v2/server/forge" "go.woodpecker-ci.org/woodpecker/v2/server/logging" "go.woodpecker-ci.org/woodpecker/v2/server/pubsub" "go.woodpecker-ci.org/woodpecker/v2/server/queue" @@ -37,7 +36,7 @@ type WoodpeckerServer struct { peer RPC } -func NewWoodpeckerServer(forge forge.Forge, queue queue.Queue, logger logging.Log, pubsub *pubsub.Publisher, store store.Store) proto.WoodpeckerServer { +func NewWoodpeckerServer(queue queue.Queue, logger logging.Log, pubsub *pubsub.Publisher, store store.Store) proto.WoodpeckerServer { pipelineTime := promauto.NewGaugeVec(prometheus.GaugeOpts{ Namespace: "woodpecker", Name: "pipeline_time", @@ -49,7 +48,6 @@ func NewWoodpeckerServer(forge forge.Forge, queue queue.Queue, logger logging.Lo Help: "Pipeline count.", }, []string{"repo", "branch", "status", "pipeline"}) peer := RPC{ - forge: forge, store: store, queue: queue, pubsub: pubsub, diff --git a/server/model/forge.go b/server/model/forge.go new file mode 100644 index 000000000..8190fc34d --- /dev/null +++ b/server/model/forge.go @@ -0,0 +1,36 @@ +// Copyright 2024 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 model + +type ForgeType string + +const ( + ForgeTypeGithub ForgeType = "github" + ForgeTypeGitlab ForgeType = "gitlab" + ForgeTypeGitea ForgeType = "gitea" + ForgeTypeBitbucket ForgeType = "bitbucket" + ForgeTypeBitbucketDatacenter ForgeType = "bitbucket-dc" + ForgeTypeAddon ForgeType = "addon" +) + +type Forge struct { + ID int64 `xorm:"pk autoincr 'id'"` + Type ForgeType `xorm:"VARCHAR(250)"` + URL string `xorm:"VARCHAR(500) 'url'"` + Client string `xorm:"VARCHAR(250)"` + ClientSecret string `xorm:"VARCHAR(250)"` + SkipVerify bool `xorm:"bool"` + AdditionalOptions map[string]any `xorm:"json"` +} diff --git a/server/model/org.go b/server/model/org.go index db093bb64..0cf2ae3d0 100644 --- a/server/model/org.go +++ b/server/model/org.go @@ -16,11 +16,12 @@ package model // Org represents an organization. type Org struct { - ID int64 `json:"id,omitempty" xorm:"pk autoincr 'id'"` - Name string `json:"name" xorm:"UNIQUE 'name'"` - IsUser bool `json:"is_user" xorm:"is_user"` + ID int64 `json:"id,omitempty" xorm:"pk autoincr 'id'"` + ForgeID int64 `json:"forge_id,omitempty" xorm:"forge_id"` + Name string `json:"name" xorm:"UNIQUE 'name'"` + IsUser bool `json:"is_user" xorm:"is_user"` // if name lookup has to check for membership or not - Private bool `json:"-" xorm:"private"` + Private bool `json:"-" xorm:"private"` } // @name Org // TableName return database table name for xorm diff --git a/server/model/repo.go b/server/model/repo.go index 877185c3a..a2f7fca4f 100644 --- a/server/model/repo.go +++ b/server/model/repo.go @@ -22,8 +22,9 @@ import ( // Repo represents a repository. type Repo struct { - ID int64 `json:"id,omitempty" xorm:"pk autoincr 'repo_id'"` - UserID int64 `json:"-" xorm:"repo_user_id"` + ID int64 `json:"id,omitempty" xorm:"pk autoincr 'repo_id'"` + UserID int64 `json:"-" xorm:"repo_user_id"` + ForgeID int64 `json:"forge_id,omitempty" xorm:"forge_id"` // ForgeRemoteID is the unique identifier for the repository on the forge. ForgeRemoteID ForgeRemoteID `json:"forge_remote_id" xorm:"forge_remote_id"` OrgID int64 `json:"org_id" xorm:"repo_org_id"` diff --git a/server/model/user.go b/server/model/user.go index 152df68ac..578d6caf2 100644 --- a/server/model/user.go +++ b/server/model/user.go @@ -34,6 +34,8 @@ type User struct { // required: true ID int64 `json:"id" xorm:"pk autoincr 'user_id'"` + ForgeID int64 `json:"forge_id,omitempty" xorm:"forge_id"` + ForgeRemoteID ForgeRemoteID `json:"-" xorm:"forge_remote_id"` // Login is the username for this user. diff --git a/server/pipeline/approve.go b/server/pipeline/approve.go index 31db7fdff..22d8a8fa4 100644 --- a/server/pipeline/approve.go +++ b/server/pipeline/approve.go @@ -20,6 +20,7 @@ import ( "github.com/rs/zerolog/log" + "go.woodpecker-ci.org/woodpecker/v2/server" forge_types "go.woodpecker-ci.org/woodpecker/v2/server/forge/types" "go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/store" @@ -32,6 +33,13 @@ func Approve(ctx context.Context, store store.Store, currentPipeline *model.Pipe return nil, ErrBadRequest{Msg: fmt.Sprintf("cannot approve a pipeline with status %s", currentPipeline.Status)} } + forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + msg := fmt.Sprintf("failure to load forge for repo '%s'", repo.FullName) + log.Error().Err(err).Str("repo", repo.FullName).Msg(msg) + return nil, fmt.Errorf(msg) + } + // fetch the pipeline file from the database configs, err := store.ConfigsForPipeline(currentPipeline.ID) if err != nil { @@ -72,7 +80,7 @@ func Approve(ctx context.Context, store store.Store, currentPipeline *model.Pipe } } - currentPipeline, pipelineItems, err := createPipelineItems(ctx, store, currentPipeline, user, repo, yamls, nil) + currentPipeline, pipelineItems, err := createPipelineItems(ctx, forge, store, currentPipeline, user, repo, yamls, nil) if err != nil { msg := fmt.Sprintf("failure to createPipelineItems for %s", repo.FullName) log.Error().Err(err).Msg(msg) @@ -86,9 +94,9 @@ func Approve(ctx context.Context, store store.Store, currentPipeline *model.Pipe return nil, err } - publishPipeline(ctx, currentPipeline, repo, user) + publishPipeline(ctx, forge, currentPipeline, repo, user) - currentPipeline, err = start(ctx, store, currentPipeline, user, repo, pipelineItems) + currentPipeline, err = start(ctx, forge, store, currentPipeline, user, repo, pipelineItems) if err != nil { msg := fmt.Sprintf("failure to start pipeline for %s: %v", repo.FullName, err) log.Error().Err(err).Msg(msg) diff --git a/server/pipeline/cancel.go b/server/pipeline/cancel.go index 46407436e..253e1d586 100644 --- a/server/pipeline/cancel.go +++ b/server/pipeline/cancel.go @@ -21,13 +21,14 @@ import ( "github.com/rs/zerolog/log" "go.woodpecker-ci.org/woodpecker/v2/server" + "go.woodpecker-ci.org/woodpecker/v2/server/forge" "go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/queue" "go.woodpecker-ci.org/woodpecker/v2/server/store" ) // Cancel the pipeline and returns the status. -func Cancel(ctx context.Context, store store.Store, repo *model.Repo, user *model.User, pipeline *model.Pipeline) error { +func Cancel(ctx context.Context, _forge forge.Forge, store store.Store, repo *model.Repo, user *model.User, pipeline *model.Pipeline) error { if pipeline.Status != model.StatusRunning && pipeline.Status != model.StatusPending && pipeline.Status != model.StatusBlocked { return &ErrBadRequest{Msg: "Cannot cancel a non-running or non-pending or non-blocked pipeline"} } @@ -88,7 +89,7 @@ func Cancel(ctx context.Context, store store.Store, repo *model.Repo, user *mode return err } - updatePipelineStatus(ctx, killedPipeline, repo, user) + updatePipelineStatus(ctx, _forge, killedPipeline, repo, user) if killedPipeline.Workflows, err = store.WorkflowGetTree(killedPipeline); err != nil { return err @@ -100,6 +101,7 @@ func Cancel(ctx context.Context, store store.Store, repo *model.Repo, user *mode func cancelPreviousPipelines( ctx context.Context, + _forge forge.Forge, _store store.Store, pipeline *model.Pipeline, repo *model.Repo, @@ -150,7 +152,7 @@ func cancelPreviousPipelines( continue } - if err = Cancel(ctx, _store, repo, user, active); err != nil { + if err = Cancel(ctx, _forge, _store, repo, user, active); err != nil { log.Error(). Err(err). Str("ref", active.Ref). diff --git a/server/pipeline/create.go b/server/pipeline/create.go index cf8938b64..d2435959c 100644 --- a/server/pipeline/create.go +++ b/server/pipeline/create.go @@ -34,7 +34,6 @@ var skipPipelineRegex = regexp.MustCompile(`\[(?i:ci *skip|skip *ci)\]`) // Create a new pipeline and start it func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline *model.Pipeline) (*model.Pipeline, error) { - _forge := server.Config.Services.Forge repoUser, err := _store.GetUser(repo.UserID) if err != nil { msg := fmt.Sprintf("failure to find repo owner via id '%d'", repo.UserID) @@ -54,6 +53,13 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline } } + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + msg := fmt.Sprintf("failure to load forge for repo '%s'", repo.FullName) + log.Error().Err(err).Str("repo", repo.FullName).Msg(msg) + return nil, fmt.Errorf(msg) + } + // If the forge has a refresh token, the current access token // may be stale. Therefore, we should refresh prior to dispatching // the pipeline. @@ -82,13 +88,13 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline return nil, ErrFiltered } else if configFetchErr != nil { log.Debug().Str("repo", repo.FullName).Err(configFetchErr).Msgf("error while fetching config '%s' in '%s' with user: '%s'", repo.Config, pipeline.Ref, repoUser.Login) - return nil, updatePipelineWithErr(ctx, _store, pipeline, repo, repoUser, fmt.Errorf("pipeline definition not found in %s", repo.FullName)) + return nil, updatePipelineWithErr(ctx, _forge, _store, pipeline, repo, repoUser, fmt.Errorf("pipeline definition not found in %s", repo.FullName)) } - pipelineItems, parseErr := parsePipeline(_store, pipeline, repoUser, repo, forgeYamlConfigs, nil) + pipelineItems, parseErr := parsePipeline(_forge, _store, pipeline, repoUser, repo, forgeYamlConfigs, nil) if pipeline_errors.HasBlockingErrors(parseErr) { log.Debug().Str("repo", repo.FullName).Err(parseErr).Msg("failed to parse yaml") - return nil, updatePipelineWithErr(ctx, _store, pipeline, repo, repoUser, parseErr) + return nil, updatePipelineWithErr(ctx, _forge, _store, pipeline, repo, repoUser, parseErr) } else if parseErr != nil { pipeline.Errors = pipeline_errors.GetPipelineErrors(parseErr) } @@ -122,7 +128,7 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline return nil, fmt.Errorf(msg) } - if err := prepareStart(ctx, _store, pipeline, repoUser, repo); err != nil { + if err := prepareStart(ctx, _forge, _store, pipeline, repoUser, repo); err != nil { log.Error().Err(err).Str("repo", repo.FullName).Msgf("error preparing pipeline for %s#%d", repo.FullName, pipeline.Number) return nil, err } @@ -131,11 +137,11 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline return pipeline, nil } - if err := updatePipelinePending(ctx, _store, pipeline, repo, repoUser); err != nil { + if err := updatePipelinePending(ctx, _forge, _store, pipeline, repo, repoUser); err != nil { return nil, err } - pipeline, err = start(ctx, _store, pipeline, repoUser, repo, pipelineItems) + pipeline, err = start(ctx, _forge, _store, pipeline, repoUser, repo, pipelineItems) if err != nil { msg := fmt.Sprintf("failed to start pipeline for %s", repo.FullName) log.Error().Err(err).Msg(msg) @@ -145,7 +151,7 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline return pipeline, nil } -func updatePipelineWithErr(ctx context.Context, _store store.Store, pipeline *model.Pipeline, repo *model.Repo, repoUser *model.User, err error) error { +func updatePipelineWithErr(ctx context.Context, _forge forge.Forge, _store store.Store, pipeline *model.Pipeline, repo *model.Repo, repoUser *model.User, err error) error { _pipeline, err := UpdateToStatusError(_store, *pipeline, err) if err != nil { return err @@ -153,12 +159,12 @@ func updatePipelineWithErr(ctx context.Context, _store store.Store, pipeline *mo // update value in ref *pipeline = *_pipeline - publishPipeline(ctx, pipeline, repo, repoUser) + publishPipeline(ctx, _forge, pipeline, repo, repoUser) return nil } -func updatePipelinePending(ctx context.Context, _store store.Store, pipeline *model.Pipeline, repo *model.Repo, repoUser *model.User) error { +func updatePipelinePending(ctx context.Context, _forge forge.Forge, _store store.Store, pipeline *model.Pipeline, repo *model.Repo, repoUser *model.User) error { _pipeline, err := UpdateToStatusPending(_store, *pipeline, "") if err != nil { return err @@ -166,7 +172,7 @@ func updatePipelinePending(ctx context.Context, _store store.Store, pipeline *mo // update value in ref *pipeline = *_pipeline - publishPipeline(ctx, pipeline, repo, repoUser) + publishPipeline(ctx, _forge, pipeline, repo, repoUser) return nil } diff --git a/server/pipeline/decline.go b/server/pipeline/decline.go index 9587bf4ba..cf8dd0e9a 100644 --- a/server/pipeline/decline.go +++ b/server/pipeline/decline.go @@ -20,17 +20,25 @@ import ( "github.com/rs/zerolog/log" + "go.woodpecker-ci.org/woodpecker/v2/server" "go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/store" ) // Decline updates the status to declined for blocked pipelines because of a gated repo func Decline(ctx context.Context, store store.Store, pipeline *model.Pipeline, user *model.User, repo *model.Repo) (*model.Pipeline, error) { + forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + msg := fmt.Sprintf("failure to load forge for repo '%s'", repo.FullName) + log.Error().Err(err).Str("repo", repo.FullName).Msg(msg) + return nil, fmt.Errorf(msg) + } + if pipeline.Status != model.StatusBlocked { return nil, fmt.Errorf("cannot decline a pipeline with status %s", pipeline.Status) } - pipeline, err := UpdateToStatusDeclined(store, *pipeline, user.Login) + pipeline, err = UpdateToStatusDeclined(store, *pipeline, user.Login) if err != nil { return nil, fmt.Errorf("error updating pipeline. %w", err) } @@ -53,7 +61,7 @@ func Decline(ctx context.Context, store store.Store, pipeline *model.Pipeline, u } } - updatePipelineStatus(ctx, pipeline, repo, user) + updatePipelineStatus(ctx, forge, pipeline, repo, user) publishToTopic(pipeline, repo) diff --git a/server/pipeline/helper.go b/server/pipeline/helper.go index 497da291d..e8e3400a5 100644 --- a/server/pipeline/helper.go +++ b/server/pipeline/helper.go @@ -19,13 +19,13 @@ import ( "github.com/rs/zerolog/log" - "go.woodpecker-ci.org/woodpecker/v2/server" + "go.woodpecker-ci.org/woodpecker/v2/server/forge" "go.woodpecker-ci.org/woodpecker/v2/server/model" ) -func updatePipelineStatus(ctx context.Context, pipeline *model.Pipeline, repo *model.Repo, user *model.User) { +func updatePipelineStatus(ctx context.Context, forge forge.Forge, pipeline *model.Pipeline, repo *model.Repo, user *model.User) { for _, workflow := range pipeline.Workflows { - err := server.Config.Services.Forge.Status(ctx, user, repo, pipeline, workflow) + err := forge.Status(ctx, user, repo, pipeline, workflow) if err != nil { log.Error().Err(err).Msgf("error setting commit status for %s/%d", repo.FullName, pipeline.Number) return diff --git a/server/pipeline/items.go b/server/pipeline/items.go index b44a06d47..737f2fc43 100644 --- a/server/pipeline/items.go +++ b/server/pipeline/items.go @@ -24,14 +24,15 @@ import ( pipeline_errors "go.woodpecker-ci.org/woodpecker/v2/pipeline/errors" "go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/compiler" "go.woodpecker-ci.org/woodpecker/v2/server" + "go.woodpecker-ci.org/woodpecker/v2/server/forge" forge_types "go.woodpecker-ci.org/woodpecker/v2/server/forge/types" "go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/pipeline/stepbuilder" "go.woodpecker-ci.org/woodpecker/v2/server/store" ) -func parsePipeline(store store.Store, currentPipeline *model.Pipeline, user *model.User, repo *model.Repo, yamls []*forge_types.FileMeta, envs map[string]string) ([]*stepbuilder.Item, error) { - netrc, err := server.Config.Services.Forge.Netrc(user, repo) +func parsePipeline(forge forge.Forge, store store.Store, currentPipeline *model.Pipeline, user *model.User, repo *model.Repo, yamls []*forge_types.FileMeta, envs map[string]string) ([]*stepbuilder.Item, error) { + netrc, err := forge.Netrc(user, repo) if err != nil { log.Error().Err(err).Msg("failed to generate netrc file") } @@ -80,7 +81,7 @@ func parsePipeline(store store.Store, currentPipeline *model.Pipeline, user *mod Envs: envs, Host: server.Config.Server.Host, Yamls: yamls, - Forge: server.Config.Services.Forge, + Forge: forge, ProxyOpts: compiler.ProxyOptions{ NoProxy: server.Config.Pipeline.Proxy.No, HTTPProxy: server.Config.Pipeline.Proxy.HTTP, @@ -90,23 +91,23 @@ func parsePipeline(store store.Store, currentPipeline *model.Pipeline, user *mod return b.Build() } -func createPipelineItems(c context.Context, store store.Store, +func createPipelineItems(c context.Context, forge forge.Forge, store store.Store, currentPipeline *model.Pipeline, user *model.User, repo *model.Repo, yamls []*forge_types.FileMeta, envs map[string]string, ) (*model.Pipeline, []*stepbuilder.Item, error) { - pipelineItems, err := parsePipeline(store, currentPipeline, user, repo, yamls, envs) + pipelineItems, err := parsePipeline(forge, store, currentPipeline, user, repo, yamls, envs) if pipeline_errors.HasBlockingErrors(err) { currentPipeline, uerr := UpdateToStatusError(store, *currentPipeline, err) if uerr != nil { log.Error().Err(uerr).Msgf("error setting error status of pipeline for %s#%d", repo.FullName, currentPipeline.Number) } else { - updatePipelineStatus(c, currentPipeline, repo, user) + updatePipelineStatus(c, forge, currentPipeline, repo, user) } return currentPipeline, nil, err } else if err != nil { currentPipeline.Errors = pipeline_errors.GetPipelineErrors(err) - err = updatePipelinePending(c, store, currentPipeline, repo, user) + err = updatePipelinePending(c, forge, store, currentPipeline, repo, user) } currentPipeline = setPipelineStepsOnPipeline(currentPipeline, pipelineItems) diff --git a/server/pipeline/restart.go b/server/pipeline/restart.go index 11e3c68fc..fd0121974 100644 --- a/server/pipeline/restart.go +++ b/server/pipeline/restart.go @@ -29,7 +29,13 @@ import ( // Restart a pipeline by creating a new one out of the old and start it func Restart(ctx context.Context, store store.Store, lastPipeline *model.Pipeline, user *model.User, repo *model.Repo, envs map[string]string) (*model.Pipeline, error) { - forge := server.Config.Services.Forge + forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + msg := fmt.Sprintf("failure to load forge for repo '%s'", repo.FullName) + log.Error().Err(err).Str("repo", repo.FullName).Msg(msg) + return nil, fmt.Errorf(msg) + } + switch lastPipeline.Status { case model.StatusDeclined, model.StatusBlocked: @@ -72,7 +78,7 @@ func Restart(ctx context.Context, store store.Store, lastPipeline *model.Pipelin if uerr != nil { log.Debug().Err(uerr).Msg("failure to update pipeline status") } else { - updatePipelineStatus(ctx, newPipeline, repo, user) + updatePipelineStatus(ctx, forge, newPipeline, repo, user) } return newPipeline, nil } @@ -82,20 +88,20 @@ func Restart(ctx context.Context, store store.Store, lastPipeline *model.Pipelin return nil, fmt.Errorf(msg) } - newPipeline, pipelineItems, err := createPipelineItems(ctx, store, newPipeline, user, repo, pipelineFiles, envs) + newPipeline, pipelineItems, err := createPipelineItems(ctx, forge, store, newPipeline, user, repo, pipelineFiles, envs) if err != nil { msg := fmt.Sprintf("failure to createPipelineItems for %s", repo.FullName) log.Error().Err(err).Msg(msg) return nil, fmt.Errorf(msg) } - if err := prepareStart(ctx, store, newPipeline, user, repo); err != nil { + if err := prepareStart(ctx, forge, store, newPipeline, user, repo); err != nil { msg := fmt.Sprintf("failure to prepare pipeline for %s", repo.FullName) log.Error().Err(err).Msg(msg) return nil, fmt.Errorf(msg) } - newPipeline, err = start(ctx, store, newPipeline, user, repo, pipelineItems) + newPipeline, err = start(ctx, forge, store, newPipeline, user, repo, pipelineItems) if err != nil { msg := fmt.Sprintf("failure to start pipeline for %s", repo.FullName) log.Error().Err(err).Msg(msg) diff --git a/server/pipeline/start.go b/server/pipeline/start.go index afad25723..f136fea29 100644 --- a/server/pipeline/start.go +++ b/server/pipeline/start.go @@ -19,20 +19,21 @@ import ( "github.com/rs/zerolog/log" + "go.woodpecker-ci.org/woodpecker/v2/server/forge" "go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/pipeline/stepbuilder" "go.woodpecker-ci.org/woodpecker/v2/server/store" ) // start a pipeline, make sure it was stored persistent in the store before -func start(ctx context.Context, store store.Store, activePipeline *model.Pipeline, user *model.User, repo *model.Repo, pipelineItems []*stepbuilder.Item) (*model.Pipeline, error) { +func start(ctx context.Context, forge forge.Forge, store store.Store, activePipeline *model.Pipeline, user *model.User, repo *model.Repo, pipelineItems []*stepbuilder.Item) (*model.Pipeline, error) { // call to cancel previous pipelines if needed - if err := cancelPreviousPipelines(ctx, store, activePipeline, repo, user); err != nil { + if err := cancelPreviousPipelines(ctx, forge, store, activePipeline, repo, user); err != nil { // should be not breaking log.Error().Err(err).Msg("failed to cancel previous pipelines") } - publishPipeline(ctx, activePipeline, repo, user) + publishPipeline(ctx, forge, activePipeline, repo, user) if err := queuePipeline(ctx, repo, pipelineItems); err != nil { log.Error().Err(err).Msg("queuePipeline") @@ -42,17 +43,17 @@ func start(ctx context.Context, store store.Store, activePipeline *model.Pipelin return activePipeline, nil } -func prepareStart(ctx context.Context, store store.Store, activePipeline *model.Pipeline, user *model.User, repo *model.Repo) error { +func prepareStart(ctx context.Context, forge forge.Forge, store store.Store, activePipeline *model.Pipeline, user *model.User, repo *model.Repo) error { if err := store.WorkflowsCreate(activePipeline.Workflows); err != nil { log.Error().Err(err).Str("repo", repo.FullName).Msgf("error persisting steps for %s#%d", repo.FullName, activePipeline.Number) return err } - publishPipeline(ctx, activePipeline, repo, user) + publishPipeline(ctx, forge, activePipeline, repo, user) return nil } -func publishPipeline(ctx context.Context, pipeline *model.Pipeline, repo *model.Repo, repoUser *model.User) { +func publishPipeline(ctx context.Context, forge forge.Forge, pipeline *model.Pipeline, repo *model.Repo, repoUser *model.User) { publishToTopic(pipeline, repo) - updatePipelineStatus(ctx, pipeline, repo, repoUser) + updatePipelineStatus(ctx, forge, pipeline, repo, repoUser) } diff --git a/server/router/middleware/session/repo.go b/server/router/middleware/session/repo.go index b89f163f8..93cfb2c7c 100644 --- a/server/router/middleware/session/repo.go +++ b/server/router/middleware/session/repo.go @@ -106,6 +106,13 @@ func SetPerm() gin.HandlerFunc { _store := store.FromContext(c) user := User(c) repo := Repo(c) + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from repo") + c.AbortWithStatus(http.StatusInternalServerError) + return + } + perm := new(model.Perm) if user != nil { @@ -116,7 +123,7 @@ func SetPerm() gin.HandlerFunc { user.Login, repo.FullName) } if time.Unix(perm.Synced, 0).Add(time.Hour).Before(time.Now()) { - _repo, err := server.Config.Services.Forge.Repo(c, user, repo.ForgeRemoteID, repo.Owner, repo.Name) + _repo, err := _forge.Repo(c, user, repo.ForgeRemoteID, repo.Owner, repo.Name) if err == nil { log.Debug().Msgf("synced user permission for %s %s", user.Login, repo.FullName) perm = _repo.Perm diff --git a/server/router/middleware/session/user.go b/server/router/middleware/session/user.go index b0c1b7176..d1b1507ef 100644 --- a/server/router/middleware/session/user.go +++ b/server/router/middleware/session/user.go @@ -145,7 +145,14 @@ func MustOrgMember(admin bool) gin.HandlerFunc { return } - perm, err := server.Config.Services.Membership.Get(c, user, org.Name) + _forge, err := server.Config.Services.Manager.ForgeFromUser(user) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from user") + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + perm, err := server.Config.Services.Membership.Get(c, _forge, user, org.Name) if err != nil { log.Error().Err(err).Msg("failed to check membership") c.String(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) diff --git a/server/router/middleware/token/token.go b/server/router/middleware/token/token.go index 58415954d..8d45adda6 100644 --- a/server/router/middleware/token/token.go +++ b/server/router/middleware/token/token.go @@ -15,6 +15,8 @@ package token import ( + "net/http" + "github.com/gin-gonic/gin" "go.woodpecker-ci.org/woodpecker/v2/server" @@ -26,7 +28,13 @@ import ( func Refresh(c *gin.Context) { user := session.User(c) if user != nil { - forge.Refresh(c, server.Config.Services.Forge, store.FromContext(c), user) + _forge, err := server.Config.Services.Manager.ForgeFromUser(user) + if err != nil { + _ = c.AbortWithError(http.StatusInternalServerError, err) + return + } + + forge.Refresh(c, _forge, store.FromContext(c), user) } c.Next() diff --git a/server/services/environment/extension.go b/server/services/environment/service.go similarity index 100% rename from server/services/environment/extension.go rename to server/services/environment/service.go diff --git a/server/services/manager.go b/server/services/manager.go index d5669df2b..ff281c40a 100644 --- a/server/services/manager.go +++ b/server/services/manager.go @@ -16,9 +16,12 @@ package services import ( "crypto" + "time" + "github.com/jellydator/ttlcache/v3" "github.com/urfave/cli/v2" + "go.woodpecker-ci.org/woodpecker/v2/server/forge" "go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/services/config" "go.woodpecker-ci.org/woodpecker/v2/server/services/environment" @@ -27,56 +30,119 @@ import ( "go.woodpecker-ci.org/woodpecker/v2/server/store" ) -type Manager struct { +//go:generate mockery --name Manager --output mocks --case underscore + +const forgeCacheTTL = 10 * time.Minute + +type SetupForge func(forge *model.Forge) (forge.Forge, error) + +type Manager interface { + SignaturePublicKey() crypto.PublicKey + SecretServiceFromRepo(repo *model.Repo) secret.Service + SecretService() secret.Service + RegistryServiceFromRepo(repo *model.Repo) registry.Service + RegistryService() registry.Service + ConfigServiceFromRepo(repo *model.Repo) config.Service + EnvironmentService() environment.Service + ForgeFromRepo(repo *model.Repo) (forge.Forge, error) + ForgeFromUser(user *model.User) (forge.Forge, error) + ForgeMain() (forge.Forge, error) +} + +type manager struct { + signaturePrivateKey crypto.PrivateKey + signaturePublicKey crypto.PublicKey + store store.Store secret secret.Service registry registry.Service config config.Service environment environment.Service - signaturePrivateKey crypto.PrivateKey - signaturePublicKey crypto.PublicKey + forgeCache *ttlcache.Cache[int64, forge.Forge] + setupForge SetupForge } -func NewManager(c *cli.Context, store store.Store) (*Manager, error) { +func NewManager(c *cli.Context, store store.Store, setupForge SetupForge) (Manager, error) { signaturePrivateKey, signaturePublicKey, err := setupSignatureKeys(store) if err != nil { return nil, err } - return &Manager{ + err = setupForgeService(c, store) + if err != nil { + return nil, err + } + + return &manager{ signaturePrivateKey: signaturePrivateKey, signaturePublicKey: signaturePublicKey, + store: store, secret: setupSecretService(store), registry: setupRegistryService(store, c.String("docker-config")), config: setupConfigService(c, signaturePrivateKey), environment: environment.Parse(c.StringSlice("environment")), + forgeCache: ttlcache.New(ttlcache.WithDisableTouchOnHit[int64, forge.Forge]()), + setupForge: setupForge, }, nil } -func (e *Manager) SignaturePublicKey() crypto.PublicKey { - return e.signaturePublicKey +func (m *manager) SignaturePublicKey() crypto.PublicKey { + return m.signaturePublicKey } -func (e *Manager) SecretServiceFromRepo(_ *model.Repo) secret.Service { - return e.SecretService() +func (m *manager) SecretServiceFromRepo(_ *model.Repo) secret.Service { + return m.SecretService() } -func (e *Manager) SecretService() secret.Service { - return e.secret +func (m *manager) SecretService() secret.Service { + return m.secret } -func (e *Manager) RegistryServiceFromRepo(_ *model.Repo) registry.Service { - return e.RegistryService() +func (m *manager) RegistryServiceFromRepo(_ *model.Repo) registry.Service { + return m.RegistryService() } -func (e *Manager) RegistryService() registry.Service { - return e.registry +func (m *manager) RegistryService() registry.Service { + return m.registry } -func (e *Manager) ConfigServiceFromRepo(_ *model.Repo) config.Service { +func (m *manager) ConfigServiceFromRepo(_ *model.Repo) config.Service { // TODO: decied based on repo property which config service to use - return e.config + return m.config } -func (e *Manager) EnvironmentService() environment.Service { - return e.environment +func (m *manager) EnvironmentService() environment.Service { + return m.environment +} + +func (m *manager) ForgeFromRepo(repo *model.Repo) (forge.Forge, error) { + return m.getForgeByID(repo.ForgeID) +} + +func (m *manager) ForgeFromUser(user *model.User) (forge.Forge, error) { + return m.getForgeByID(user.ForgeID) +} + +func (m *manager) ForgeMain() (forge.Forge, error) { + return m.getForgeByID(1) // main forge is always 1 and is configured via environment variables +} + +func (m *manager) getForgeByID(id int64) (forge.Forge, error) { + item := m.forgeCache.Get(id) + if item != nil && !item.IsExpired() { + return item.Value(), nil + } + + forgeModel, err := m.store.ForgeGet(id) + if err != nil { + return nil, err + } + + forge, err := m.setupForge(forgeModel) + if err != nil { + return nil, err + } + + m.forgeCache.Set(id, forge, forgeCacheTTL) + + return forge, nil } diff --git a/server/services/mocks/manager.go b/server/services/mocks/manager.go new file mode 100644 index 000000000..3c67c2c44 --- /dev/null +++ b/server/services/mocks/manager.go @@ -0,0 +1,270 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + crypto "crypto" + + config "go.woodpecker-ci.org/woodpecker/v2/server/services/config" + + environment "go.woodpecker-ci.org/woodpecker/v2/server/services/environment" + + forge "go.woodpecker-ci.org/woodpecker/v2/server/forge" + + mock "github.com/stretchr/testify/mock" + + model "go.woodpecker-ci.org/woodpecker/v2/server/model" + + registry "go.woodpecker-ci.org/woodpecker/v2/server/services/registry" + + secret "go.woodpecker-ci.org/woodpecker/v2/server/services/secret" +) + +// Manager is an autogenerated mock type for the Manager type +type Manager struct { + mock.Mock +} + +// ConfigServiceFromRepo provides a mock function with given fields: repo +func (_m *Manager) ConfigServiceFromRepo(repo *model.Repo) config.Service { + ret := _m.Called(repo) + + if len(ret) == 0 { + panic("no return value specified for ConfigServiceFromRepo") + } + + var r0 config.Service + if rf, ok := ret.Get(0).(func(*model.Repo) config.Service); ok { + r0 = rf(repo) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(config.Service) + } + } + + return r0 +} + +// EnvironmentService provides a mock function with given fields: +func (_m *Manager) EnvironmentService() environment.Service { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for EnvironmentService") + } + + var r0 environment.Service + if rf, ok := ret.Get(0).(func() environment.Service); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(environment.Service) + } + } + + return r0 +} + +// ForgeFromRepo provides a mock function with given fields: repo +func (_m *Manager) ForgeFromRepo(repo *model.Repo) (forge.Forge, error) { + ret := _m.Called(repo) + + if len(ret) == 0 { + panic("no return value specified for ForgeFromRepo") + } + + var r0 forge.Forge + var r1 error + if rf, ok := ret.Get(0).(func(*model.Repo) (forge.Forge, error)); ok { + return rf(repo) + } + if rf, ok := ret.Get(0).(func(*model.Repo) forge.Forge); ok { + r0 = rf(repo) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(forge.Forge) + } + } + + if rf, ok := ret.Get(1).(func(*model.Repo) error); ok { + r1 = rf(repo) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ForgeFromUser provides a mock function with given fields: user +func (_m *Manager) ForgeFromUser(user *model.User) (forge.Forge, error) { + ret := _m.Called(user) + + if len(ret) == 0 { + panic("no return value specified for ForgeFromUser") + } + + var r0 forge.Forge + var r1 error + if rf, ok := ret.Get(0).(func(*model.User) (forge.Forge, error)); ok { + return rf(user) + } + if rf, ok := ret.Get(0).(func(*model.User) forge.Forge); ok { + r0 = rf(user) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(forge.Forge) + } + } + + if rf, ok := ret.Get(1).(func(*model.User) error); ok { + r1 = rf(user) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ForgeMain provides a mock function with given fields: +func (_m *Manager) ForgeMain() (forge.Forge, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ForgeMain") + } + + var r0 forge.Forge + var r1 error + if rf, ok := ret.Get(0).(func() (forge.Forge, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() forge.Forge); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(forge.Forge) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RegistryService provides a mock function with given fields: +func (_m *Manager) RegistryService() registry.Service { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for RegistryService") + } + + var r0 registry.Service + if rf, ok := ret.Get(0).(func() registry.Service); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(registry.Service) + } + } + + return r0 +} + +// RegistryServiceFromRepo provides a mock function with given fields: repo +func (_m *Manager) RegistryServiceFromRepo(repo *model.Repo) registry.Service { + ret := _m.Called(repo) + + if len(ret) == 0 { + panic("no return value specified for RegistryServiceFromRepo") + } + + var r0 registry.Service + if rf, ok := ret.Get(0).(func(*model.Repo) registry.Service); ok { + r0 = rf(repo) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(registry.Service) + } + } + + return r0 +} + +// SecretService provides a mock function with given fields: +func (_m *Manager) SecretService() secret.Service { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for SecretService") + } + + var r0 secret.Service + if rf, ok := ret.Get(0).(func() secret.Service); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(secret.Service) + } + } + + return r0 +} + +// SecretServiceFromRepo provides a mock function with given fields: repo +func (_m *Manager) SecretServiceFromRepo(repo *model.Repo) secret.Service { + ret := _m.Called(repo) + + if len(ret) == 0 { + panic("no return value specified for SecretServiceFromRepo") + } + + var r0 secret.Service + if rf, ok := ret.Get(0).(func(*model.Repo) secret.Service); ok { + r0 = rf(repo) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(secret.Service) + } + } + + return r0 +} + +// SignaturePublicKey provides a mock function with given fields: +func (_m *Manager) SignaturePublicKey() crypto.PublicKey { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for SignaturePublicKey") + } + + var r0 crypto.PublicKey + if rf, ok := ret.Get(0).(func() crypto.PublicKey); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(crypto.PublicKey) + } + } + + return r0 +} + +// NewManager creates a new instance of Manager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewManager(t interface { + mock.TestingT + Cleanup(func()) +}) *Manager { + mock := &Manager{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/server/services/setup.go b/server/services/setup.go index 1b25f82a3..c92ed9e3a 100644 --- a/server/services/setup.go +++ b/server/services/setup.go @@ -25,6 +25,7 @@ import ( "github.com/rs/zerolog/log" "github.com/urfave/cli/v2" + "go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/services/config" "go.woodpecker-ci.org/woodpecker/v2/server/services/registry" "go.woodpecker-ci.org/woodpecker/v2/server/services/secret" @@ -93,3 +94,70 @@ func setupSignatureKeys(_store store.Store) (crypto.PrivateKey, crypto.PublicKey privateKey := ed25519.PrivateKey(privKeyStr) return privateKey, privateKey.Public(), nil } + +func setupForgeService(c *cli.Context, _store store.Store) error { + _forge, err := _store.ForgeGet(1) + if err != nil && !errors.Is(err, types.RecordNotExist) { + return err + } + forgeExists := err == nil + if _forge == nil { + _forge = &model.Forge{ + ID: 0, + } + } + if _forge.AdditionalOptions == nil { + _forge.AdditionalOptions = make(map[string]any) + } + + _forge.Client = c.String("forge-oauth-client") + _forge.ClientSecret = c.String("forge-oauth-secret") + _forge.URL = c.String("forge-url") + _forge.SkipVerify = c.Bool("forge-skip-verify") + + switch { + case c.String("addon-forge") != "": + _forge.Type = model.ForgeTypeAddon + _forge.AdditionalOptions["executable"] = c.String("addon-forge") + case c.Bool("github"): + _forge.Type = model.ForgeTypeGithub + _forge.AdditionalOptions["merge-ref"] = c.Bool("github-merge-ref") + _forge.AdditionalOptions["public-only"] = c.Bool("github-public-only") + if _forge.URL == "" { + _forge.URL = "https://github.com" + } + case c.Bool("gitlab"): + _forge.Type = model.ForgeTypeGitlab + if _forge.URL == "" { + _forge.URL = "https://gitlab.com" + } + case c.Bool("gitea"): + _forge.Type = model.ForgeTypeGitea + _forge.AdditionalOptions["oauth-server"] = c.String("gitea-oauth-server") + if _forge.URL == "" { + _forge.URL = "https://try.gitea.com" + } + case c.Bool("bitbucket"): + _forge.Type = model.ForgeTypeBitbucket + case c.Bool("bitbucket-dc"): + _forge.Type = model.ForgeTypeBitbucketDatacenter + _forge.AdditionalOptions["git-username"] = c.String("bitbucket-dc-git-username") + _forge.AdditionalOptions["git-password"] = c.String("bitbucket-dc-git-password") + default: + return errors.New("forge not configured") + } + + if forgeExists { + err := _store.ForgeUpdate(_forge) + if err != nil { + return err + } + } else { + err := _store.ForgeCreate(_forge) + if err != nil { + return err + } + } + + return nil +} diff --git a/server/store/datastore/forge.go b/server/store/datastore/forge.go new file mode 100644 index 000000000..5c1a29efc --- /dev/null +++ b/server/store/datastore/forge.go @@ -0,0 +1,54 @@ +// Copyright 2022 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 ( + "go.woodpecker-ci.org/woodpecker/v2/server/model" +) + +func (s storage) ForgeGet(id int64) (*model.Forge, error) { + forge := new(model.Forge) + return forge, wrapGet(s.engine.ID(id).Get(forge)) +} + +func (s storage) ForgeList(p *model.ListOptions) ([]*model.Forge, error) { + forges := make([]*model.Forge, 0, 10) + return forges, s.paginate(p).Find(&forges) +} + +func (s storage) ForgeCreate(forge *model.Forge) error { + // only Insert set auto created ID back to object + _, err := s.engine.Insert(forge) + return err +} + +func (s storage) ForgeUpdate(forge *model.Forge) error { + _, err := s.engine.ID(forge.ID).AllCols().Update(forge) + return err +} + +func (s storage) ForgeDelete(forge *model.Forge) error { + sess := s.engine.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + + if _, err := sess.ID(forge.ID).Delete(new(model.Forge)); err != nil { + return err + } + + return sess.Commit() +} diff --git a/server/store/datastore/forge_test.go b/server/store/datastore/forge_test.go new file mode 100644 index 000000000..a7583d8db --- /dev/null +++ b/server/store/datastore/forge_test.go @@ -0,0 +1,71 @@ +// Copyright 2024 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 ( + "testing" + + "github.com/stretchr/testify/assert" + + "go.woodpecker-ci.org/woodpecker/v2/server/model" +) + +func TestForgeCRUD(t *testing.T) { + store, closer := newTestStore(t, new(model.Forge), new(model.Repo), new(model.User)) + defer closer() + + forge1 := &model.Forge{ + Type: "github", + URL: "https://github.com", + Client: "client", + ClientSecret: "secret", + SkipVerify: false, + AdditionalOptions: map[string]any{ + "foo": "bar", + }, + } + + // create first forge to play with + assert.NoError(t, store.ForgeCreate(forge1)) + assert.EqualValues(t, "github", forge1.Type) + + // retrieve it + forgeOne, err := store.ForgeGet(forge1.ID) + assert.NoError(t, err) + assert.EqualValues(t, forge1, forgeOne) + + // change type + assert.NoError(t, store.ForgeUpdate(&model.Forge{ID: forge1.ID, Type: "gitlab"})) + + // find updated forge by id + forgeOne, err = store.ForgeGet(forge1.ID) + assert.NoError(t, err) + assert.EqualValues(t, "gitlab", forgeOne.Type) + + // create two more forges and repos + someUser := &model.Forge{Type: "bitbucket"} + assert.NoError(t, store.ForgeCreate(someUser)) + assert.NoError(t, store.ForgeCreate(&model.Forge{Type: "gitea"})) + + // get all repos for a specific forge + forges, err := store.ForgeList(&model.ListOptions{All: true}) + assert.NoError(t, err) + assert.Len(t, forges, 3) + + // delete an forge and check if it's gone + assert.NoError(t, store.ForgeDelete(forge1)) + _, err = store.ForgeGet(forge1.ID) + assert.Error(t, err) +} diff --git a/server/store/datastore/migration/030_set_default_forge_id.go b/server/store/datastore/migration/030_set_default_forge_id.go new file mode 100644 index 000000000..994ebee16 --- /dev/null +++ b/server/store/datastore/migration/030_set_default_forge_id.go @@ -0,0 +1,46 @@ +// Copyright 2024 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 migration + +import ( + "fmt" + + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" + + "go.woodpecker-ci.org/woodpecker/v2/server/model" +) + +var setForgeID = xormigrate.Migration{ + ID: "set-forge-id", + MigrateSession: func(sess *xorm.Session) (err error) { + if err := sess.Sync(new(model.User), new(model.Repo), new(model.Forge), new(model.Org)); err != nil { + return fmt.Errorf("sync new models failed: %w", err) + } + + _, err = sess.Exec(fmt.Sprintf("UPDATE `%s` SET forge_id=1;", model.User{}.TableName())) + if err != nil { + return err + } + + _, err = sess.Exec(fmt.Sprintf("UPDATE `%s` SET forge_id=1;", model.Org{}.TableName())) + if err != nil { + return err + } + + _, err = sess.Exec(fmt.Sprintf("UPDATE `%s` SET forge_id=1;", model.Repo{}.TableName())) + return err + }, +} diff --git a/server/store/datastore/migration/migration.go b/server/store/datastore/migration/migration.go index 6799c3e8e..f2af625a9 100644 --- a/server/store/datastore/migration/migration.go +++ b/server/store/datastore/migration/migration.go @@ -59,6 +59,7 @@ var migrationTasks = []*xormigrate.Migration{ &convertToNewPipelineErrorFormat, &renameLinkToURL, &cleanRegistryPipeline, + &setForgeID, } var allBeans = []any{ @@ -77,6 +78,7 @@ var allBeans = []any{ new(model.ServerConfig), new(model.Cron), new(model.Redirection), + new(model.Forge), new(model.Workflow), new(model.Org), } diff --git a/server/store/mocks/store.go b/server/store/mocks/store.go index 229574a66..3007b3ef4 100644 --- a/server/store/mocks/store.go +++ b/server/store/mocks/store.go @@ -539,6 +539,152 @@ func (_m *Store) DeleteUser(_a0 *model.User) error { return r0 } +// ForgeCreate provides a mock function with given fields: _a0 +func (_m *Store) ForgeCreate(_a0 *model.Forge) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(*model.Forge) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ForgeDelete provides a mock function with given fields: _a0 +func (_m *Store) ForgeDelete(_a0 *model.Forge) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(*model.Forge) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ForgeFindByRepo provides a mock function with given fields: _a0 +func (_m *Store) ForgeFindByRepo(_a0 *model.Repo) (*model.Forge, error) { + ret := _m.Called(_a0) + + var r0 *model.Forge + var r1 error + if rf, ok := ret.Get(0).(func(*model.Repo) (*model.Forge, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(*model.Repo) *model.Forge); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Forge) + } + } + + if rf, ok := ret.Get(1).(func(*model.Repo) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ForgeFindByUser provides a mock function with given fields: _a0 +func (_m *Store) ForgeFindByUser(_a0 *model.User) (*model.Forge, error) { + ret := _m.Called(_a0) + + var r0 *model.Forge + var r1 error + if rf, ok := ret.Get(0).(func(*model.User) (*model.Forge, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(*model.User) *model.Forge); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Forge) + } + } + + if rf, ok := ret.Get(1).(func(*model.User) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ForgeGet provides a mock function with given fields: _a0 +func (_m *Store) ForgeGet(_a0 int64) (*model.Forge, error) { + ret := _m.Called(_a0) + + var r0 *model.Forge + var r1 error + if rf, ok := ret.Get(0).(func(int64) (*model.Forge, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(int64) *model.Forge); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Forge) + } + } + + if rf, ok := ret.Get(1).(func(int64) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ForgeList provides a mock function with given fields: p +func (_m *Store) ForgeList(p *model.ListOptions) ([]*model.Forge, error) { + ret := _m.Called(p) + + var r0 []*model.Forge + var r1 error + if rf, ok := ret.Get(0).(func(*model.ListOptions) ([]*model.Forge, error)); ok { + return rf(p) + } + if rf, ok := ret.Get(0).(func(*model.ListOptions) []*model.Forge); ok { + r0 = rf(p) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Forge) + } + } + + if rf, ok := ret.Get(1).(func(*model.ListOptions) error); ok { + r1 = rf(p) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ForgeUpdate provides a mock function with given fields: _a0 +func (_m *Store) ForgeUpdate(_a0 *model.Forge) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(*model.Forge) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // GetActivePipelineList provides a mock function with given fields: repo func (_m *Store) GetActivePipelineList(repo *model.Repo) ([]*model.Pipeline, error) { ret := _m.Called(repo) diff --git a/server/store/store.go b/server/store/store.go index 1a34deda2..6ba76573c 100644 --- a/server/store/store.go +++ b/server/store/store.go @@ -159,6 +159,13 @@ type Store interface { CronListNextExecute(int64, int64) ([]*model.Cron, error) CronGetLock(*model.Cron, int64) (bool, error) + // Forge + ForgeCreate(*model.Forge) error + ForgeGet(int64) (*model.Forge, error) + ForgeList(p *model.ListOptions) ([]*model.Forge, error) + ForgeUpdate(*model.Forge) error + ForgeDelete(*model.Forge) error + // Agent AgentCreate(*model.Agent) error AgentFind(int64) (*model.Agent, error) diff --git a/server/web/config.go b/server/web/config.go index 630ae9ee4..99e78461e 100644 --- a/server/web/config.go +++ b/server/web/config.go @@ -39,12 +39,20 @@ func Config(c *gin.Context) { ).Sign(user.Hash) } + // TODO: remove this and use the forge type from the corresponding repo + mainForge, err := server.Config.Services.Manager.ForgeMain() + if err != nil { + log.Error().Err(err).Msg("could not get main forge") + c.AbortWithStatus(http.StatusInternalServerError) + return + } + configData := map[string]any{ "user": user, "csrf": csrf, "version": version.String(), "skip_version_check": server.Config.WebUI.SkipVersionCheck, - "forge": server.Config.Services.Forge.Name(), + "forge": mainForge.Name(), "root_path": server.Config.Server.RootPath, "enable_swagger": server.Config.WebUI.EnableSwagger, } diff --git a/web/src/views/repo/RepoWrapper.vue b/web/src/views/repo/RepoWrapper.vue index c392f9ac7..a56db15f9 100644 --- a/web/src/views/repo/RepoWrapper.vue +++ b/web/src/views/repo/RepoWrapper.vue @@ -77,7 +77,7 @@ const router = useRouter(); const i18n = useI18n(); const config = useConfig(); -const { forge } = useConfig(); +const { forge } = useConfig(); // TODO: remove this and use the forge type from the corresponding repo const repo = repoStore.getRepo(repositoryId); const repoPermissions = ref(); const pipelines = pipelineStore.getRepoPipelines(repositoryId);