Refactor internal services (#915)

This commit is contained in:
Anbraten 2024-02-11 18:42:33 +01:00 committed by GitHub
parent e1521ef460
commit 82e1ce937c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
66 changed files with 1163 additions and 993 deletions

View file

@ -42,12 +42,11 @@ import (
woodpeckerGrpcServer "go.woodpecker-ci.org/woodpecker/v2/server/grpc" woodpeckerGrpcServer "go.woodpecker-ci.org/woodpecker/v2/server/grpc"
"go.woodpecker-ci.org/woodpecker/v2/server/logging" "go.woodpecker-ci.org/woodpecker/v2/server/logging"
"go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/model"
// "go.woodpecker-ci.org/woodpecker/v2/server/plugins/encryption"
// encryptedStore "go.woodpecker-ci.org/woodpecker/v2/server/plugins/encryption/wrapper/store"
"go.woodpecker-ci.org/woodpecker/v2/server/plugins/permissions"
"go.woodpecker-ci.org/woodpecker/v2/server/pubsub" "go.woodpecker-ci.org/woodpecker/v2/server/pubsub"
"go.woodpecker-ci.org/woodpecker/v2/server/router" "go.woodpecker-ci.org/woodpecker/v2/server/router"
"go.woodpecker-ci.org/woodpecker/v2/server/router/middleware" "go.woodpecker-ci.org/woodpecker/v2/server/router/middleware"
"go.woodpecker-ci.org/woodpecker/v2/server/services"
"go.woodpecker-ci.org/woodpecker/v2/server/services/permissions"
"go.woodpecker-ci.org/woodpecker/v2/server/store" "go.woodpecker-ci.org/woodpecker/v2/server/store"
"go.woodpecker-ci.org/woodpecker/v2/server/web" "go.woodpecker-ci.org/woodpecker/v2/server/web"
"go.woodpecker-ci.org/woodpecker/v2/shared/constant" "go.woodpecker-ci.org/woodpecker/v2/shared/constant"
@ -271,48 +270,21 @@ func run(c *cli.Context) error {
return g.Wait() return g.Wait()
} }
func setupEvilGlobals(c *cli.Context, v store.Store, f forge.Forge) error { func setupEvilGlobals(c *cli.Context, s store.Store, f forge.Forge) error {
// forge // forge
server.Config.Services.Forge = f server.Config.Services.Forge = f
server.Config.Services.Timeout = c.Duration("forge-timeout")
// services // services
server.Config.Services.Queue = setupQueue(c, v) server.Config.Services.Queue = setupQueue(c, s)
server.Config.Services.Logs = logging.New() server.Config.Services.Logs = logging.New()
server.Config.Services.Pubsub = pubsub.New() server.Config.Services.Pubsub = pubsub.New()
var err error
server.Config.Services.Registries, err = setupRegistryService(c, v)
if err != nil {
return err
}
// TODO(1544): fix encrypted store
// // encryption
// encryptedSecretStore := encryptedStore.NewSecretStore(v)
// err := encryption.Encryption(c, v).WithClient(encryptedSecretStore).Build()
// if err != nil {
// log.Fatal().Err(err).Msg("could not create encryption service")
// }
// server.Config.Services.Secrets = setupSecretService(c, encryptedSecretStore)
server.Config.Services.Secrets, err = setupSecretService(c, v)
if err != nil {
return err
}
server.Config.Services.Environ, err = setupEnvironService(c, v)
if err != nil {
return err
}
server.Config.Services.Membership = setupMembershipService(c, f) server.Config.Services.Membership = setupMembershipService(c, f)
server.Config.Services.SignaturePrivateKey, server.Config.Services.SignaturePublicKey, err = setupSignatureKeys(v) serviceMangager, err := services.NewManager(c, s)
if err != nil { if err != nil {
return err return fmt.Errorf("could not setup service manager: %w", err)
}
server.Config.Services.ConfigService, err = setupConfigService(c)
if err != nil {
return err
} }
server.Config.Services.Manager = serviceMangager
// authentication // authentication
server.Config.Pipeline.AuthenticatePublicRepos = c.Bool("authenticate-public-repos") server.Config.Pipeline.AuthenticatePublicRepos = c.Bool("authenticate-public-repos")

View file

@ -17,11 +17,6 @@ package main
import ( import (
"context" "context"
"crypto"
"crypto/ed25519"
"crypto/rand"
"encoding/hex"
"errors"
"fmt" "fmt"
"net/url" "net/url"
"os" "os"
@ -41,15 +36,9 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/server/forge/gitea" "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/github"
"go.woodpecker-ci.org/woodpecker/v2/server/forge/gitlab" "go.woodpecker-ci.org/woodpecker/v2/server/forge/gitlab"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
"go.woodpecker-ci.org/woodpecker/v2/server/plugins/config"
"go.woodpecker-ci.org/woodpecker/v2/server/plugins/environments"
"go.woodpecker-ci.org/woodpecker/v2/server/plugins/registry"
"go.woodpecker-ci.org/woodpecker/v2/server/plugins/secrets"
"go.woodpecker-ci.org/woodpecker/v2/server/queue" "go.woodpecker-ci.org/woodpecker/v2/server/queue"
"go.woodpecker-ci.org/woodpecker/v2/server/store" "go.woodpecker-ci.org/woodpecker/v2/server/store"
"go.woodpecker-ci.org/woodpecker/v2/server/store/datastore" "go.woodpecker-ci.org/woodpecker/v2/server/store/datastore"
"go.woodpecker-ci.org/woodpecker/v2/server/store/types"
"go.woodpecker-ci.org/woodpecker/v2/shared/addon" "go.woodpecker-ci.org/woodpecker/v2/shared/addon"
addonTypes "go.woodpecker-ci.org/woodpecker/v2/shared/addon/types" addonTypes "go.woodpecker-ci.org/woodpecker/v2/shared/addon/types"
) )
@ -111,48 +100,6 @@ func setupQueue(c *cli.Context, s store.Store) queue.Queue {
return queue.WithTaskStore(queue.New(c.Context), s) return queue.WithTaskStore(queue.New(c.Context), s)
} }
func setupSecretService(c *cli.Context, s model.SecretStore) (model.SecretService, error) {
addonService, err := addon.Load[model.SecretService](c.StringSlice("addons"), addonTypes.TypeSecretService)
if err != nil {
return nil, err
}
if addonService != nil {
return addonService.Value, nil
}
return secrets.New(c.Context, s), nil
}
func setupRegistryService(c *cli.Context, s store.Store) (model.RegistryService, error) {
addonService, err := addon.Load[model.RegistryService](c.StringSlice("addons"), addonTypes.TypeRegistryService)
if err != nil {
return nil, err
}
if addonService != nil {
return addonService.Value, nil
}
if c.String("docker-config") != "" {
return registry.Combined(
registry.New(s),
registry.Filesystem(c.String("docker-config")),
), nil
}
return registry.New(s), nil
}
func setupEnvironService(c *cli.Context, _ store.Store) (model.EnvironService, error) {
addonService, err := addon.Load[model.EnvironService](c.StringSlice("addons"), addonTypes.TypeEnvironmentService)
if err != nil {
return nil, err
}
if addonService != nil {
return addonService.Value, nil
}
return environments.Parse(c.StringSlice("environment")), nil
}
func setupMembershipService(_ *cli.Context, r forge.Forge) cache.MembershipService { func setupMembershipService(_ *cli.Context, r forge.Forge) cache.MembershipService {
return cache.NewMembershipService(r) return cache.NewMembershipService(r)
} }
@ -292,46 +239,3 @@ func setupMetrics(g *errgroup.Group, _store store.Store) {
} }
}) })
} }
// setupSignatureKeys generate or load key pair to sign webhooks requests (i.e. used for extensions)
func setupSignatureKeys(_store store.Store) (crypto.PrivateKey, crypto.PublicKey, error) {
privKeyID := "signature-private-key"
privKey, err := _store.ServerConfigGet(privKeyID)
if errors.Is(err, types.RecordNotExist) {
_, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate private key: %w", err)
}
err = _store.ServerConfigSet(privKeyID, hex.EncodeToString(privKey))
if err != nil {
return nil, nil, fmt.Errorf("failed to store private key: %w", err)
}
log.Debug().Msg("created private key")
return privKey, privKey.Public(), nil
} else if err != nil {
return nil, nil, fmt.Errorf("failed to load private key: %w", err)
}
privKeyStr, err := hex.DecodeString(privKey)
if err != nil {
return nil, nil, fmt.Errorf("failed to decode private key: %w", err)
}
privateKey := ed25519.PrivateKey(privKeyStr)
return privateKey, privateKey.Public(), nil
}
func setupConfigService(c *cli.Context) (config.Extension, error) {
addonExt, err := addon.Load[config.Extension](c.StringSlice("addons"), addonTypes.TypeConfigService)
if err != nil {
return nil, err
}
if addonExt != nil {
return addonExt.Value, nil
}
if endpoint := c.String("config-service-endpoint"); endpoint != "" {
return config.NewHTTP(endpoint, server.Config.Services.SignaturePrivateKey), nil
}
return nil, nil
}

View file

@ -31,6 +31,7 @@
- **YAML File**: A file format used to define and configure [workflows][Workflow]. - **YAML File**: A file format used to define and configure [workflows][Workflow].
- **Dependency**: [Workflows][Workflow] can depend on each other, and if possible, they are executed in parallel. - **Dependency**: [Workflows][Workflow] can depend on each other, and if possible, they are executed in parallel.
- **Status**: Status refers to the outcome of a step or [workflow][Workflow] after it has been executed, determined by the internal command exit code. At the end of a [workflow][Workflow], its status is sent to the [forge][Forge]. - **Status**: Status refers to the outcome of a step or [workflow][Workflow] after it has been executed, determined by the internal command exit code. At the end of a [workflow][Workflow], its status is sent to the [forge][Forge].
- **Service extension**: Some parts of woodpecker internal services like secrets storage or config fetcher can be replaced through service extensions.
## Pipeline events ## Pipeline events

View file

@ -13,10 +13,6 @@ To adapt Woodpecker to your needs beyond the [configuration](../10-server-config
Addons can be used for: Addons can be used for:
- Forges - Forges
- Config services
- Secret services
- Environment services
- Registry services
## Restrictions ## Restrictions

View file

@ -19,13 +19,9 @@ Directly import Woodpecker's Go package (`go.woodpecker-ci.org/woodpecker/woodpe
### Return types ### Return types
| Addon type | Return type | | Addon type | Return type |
| -------------------- | ---------------------------------------------------------------------- | | ---------- | -------------------------------------------------------------------- |
| `Forge` | `"go.woodpecker-ci.org/woodpecker/woodpecker/v2/server/forge".Forge` | | `Forge` | `"go.woodpecker-ci.org/woodpecker/woodpecker/v2/server/forge".Forge` |
| `ConfigService` | `"go.woodpecker-ci.org/woodpecker/v2/server/plugins/config".Extension` |
| `SecretService` | `"go.woodpecker-ci.org/woodpecker/v2/server/model".SecretService` |
| `EnvironmentService` | `"go.woodpecker-ci.org/woodpecker/v2/server/model".EnvironmentService` |
| `RegistryService` | `"go.woodpecker-ci.org/woodpecker/v2/server/model".RegistryService` |
### Using configurations ### Using configurations

View file

@ -35,7 +35,8 @@ import (
// @Param page query int false "for response pagination, page offset number" default(1) // @Param page query int false "for response pagination, page offset number" default(1)
// @Param perPage query int false "for response pagination, max items per page" default(50) // @Param perPage query int false "for response pagination, max items per page" default(50)
func GetGlobalSecretList(c *gin.Context) { func GetGlobalSecretList(c *gin.Context) {
list, err := server.Config.Services.Secrets.GlobalSecretList(session.Pagination(c)) secretService := server.Config.Services.Manager.SecretService()
list, err := secretService.GlobalSecretList(session.Pagination(c))
if err != nil { if err != nil {
c.String(http.StatusInternalServerError, "Error getting global secret list. %s", err) c.String(http.StatusInternalServerError, "Error getting global secret list. %s", err)
return return
@ -59,7 +60,8 @@ func GetGlobalSecretList(c *gin.Context) {
// @Param secret path string true "the secret's name" // @Param secret path string true "the secret's name"
func GetGlobalSecret(c *gin.Context) { func GetGlobalSecret(c *gin.Context) {
name := c.Param("secret") name := c.Param("secret")
secret, err := server.Config.Services.Secrets.GlobalSecretFind(name) secretService := server.Config.Services.Manager.SecretService()
secret, err := secretService.GlobalSecretFind(name)
if err != nil { if err != nil {
handleDBError(c, err) handleDBError(c, err)
return return
@ -92,7 +94,9 @@ func PostGlobalSecret(c *gin.Context) {
c.String(http.StatusBadRequest, "Error inserting global secret. %s", err) c.String(http.StatusBadRequest, "Error inserting global secret. %s", err)
return return
} }
if err := server.Config.Services.Secrets.GlobalSecretCreate(secret); err != nil {
secretService := server.Config.Services.Manager.SecretService()
if err := secretService.GlobalSecretCreate(secret); err != nil {
c.String(http.StatusInternalServerError, "Error inserting global secret %q. %s", in.Name, err) c.String(http.StatusInternalServerError, "Error inserting global secret %q. %s", in.Name, err)
return return
} }
@ -119,7 +123,8 @@ func PatchGlobalSecret(c *gin.Context) {
return return
} }
secret, err := server.Config.Services.Secrets.GlobalSecretFind(name) secretService := server.Config.Services.Manager.SecretService()
secret, err := secretService.GlobalSecretFind(name)
if err != nil { if err != nil {
handleDBError(c, err) handleDBError(c, err)
return return
@ -138,7 +143,8 @@ func PatchGlobalSecret(c *gin.Context) {
c.String(http.StatusBadRequest, "Error updating global secret. %s", err) c.String(http.StatusBadRequest, "Error updating global secret. %s", err)
return return
} }
if err := server.Config.Services.Secrets.GlobalSecretUpdate(secret); err != nil {
if err := secretService.GlobalSecretUpdate(secret); err != nil {
c.String(http.StatusInternalServerError, "Error updating global secret %q. %s", in.Name, err) c.String(http.StatusInternalServerError, "Error updating global secret %q. %s", in.Name, err)
return return
} }
@ -156,7 +162,8 @@ func PatchGlobalSecret(c *gin.Context) {
// @Param secret path string true "the secret's name" // @Param secret path string true "the secret's name"
func DeleteGlobalSecret(c *gin.Context) { func DeleteGlobalSecret(c *gin.Context) {
name := c.Param("secret") name := c.Param("secret")
if err := server.Config.Services.Secrets.GlobalSecretDelete(name); err != nil { secretService := server.Config.Services.Manager.SecretService()
if err := secretService.GlobalSecretDelete(name); err != nil {
handleDBError(c, err) handleDBError(c, err)
return return
} }

View file

@ -44,7 +44,8 @@ func GetOrgSecret(c *gin.Context) {
return return
} }
secret, err := server.Config.Services.Secrets.OrgSecretFind(orgID, name) secretService := server.Config.Services.Manager.SecretService()
secret, err := secretService.OrgSecretFind(orgID, name)
if err != nil { if err != nil {
handleDBError(c, err) handleDBError(c, err)
return return
@ -70,7 +71,8 @@ func GetOrgSecretList(c *gin.Context) {
return return
} }
list, err := server.Config.Services.Secrets.OrgSecretList(orgID, session.Pagination(c)) secretService := server.Config.Services.Manager.SecretService()
list, err := secretService.OrgSecretList(orgID, session.Pagination(c))
if err != nil { if err != nil {
c.String(http.StatusInternalServerError, "Error getting secret list for %q. %s", orgID, err) c.String(http.StatusInternalServerError, "Error getting secret list for %q. %s", orgID, err)
return return
@ -116,7 +118,9 @@ func PostOrgSecret(c *gin.Context) {
c.String(http.StatusUnprocessableEntity, "Error inserting org %q secret. %s", orgID, err) c.String(http.StatusUnprocessableEntity, "Error inserting org %q secret. %s", orgID, err)
return return
} }
if err := server.Config.Services.Secrets.OrgSecretCreate(orgID, secret); err != nil {
secretService := server.Config.Services.Manager.SecretService()
if err := secretService.OrgSecretCreate(orgID, secret); err != nil {
c.String(http.StatusInternalServerError, "Error inserting org %q secret %q. %s", orgID, in.Name, err) c.String(http.StatusInternalServerError, "Error inserting org %q secret %q. %s", orgID, in.Name, err)
return return
} }
@ -149,7 +153,8 @@ func PatchOrgSecret(c *gin.Context) {
return return
} }
secret, err := server.Config.Services.Secrets.OrgSecretFind(orgID, name) secretService := server.Config.Services.Manager.SecretService()
secret, err := secretService.OrgSecretFind(orgID, name)
if err != nil { if err != nil {
handleDBError(c, err) handleDBError(c, err)
return return
@ -168,7 +173,8 @@ func PatchOrgSecret(c *gin.Context) {
c.String(http.StatusUnprocessableEntity, "Error updating org %q secret. %s", orgID, err) c.String(http.StatusUnprocessableEntity, "Error updating org %q secret. %s", orgID, err)
return return
} }
if err := server.Config.Services.Secrets.OrgSecretUpdate(orgID, secret); err != nil {
if err := secretService.OrgSecretUpdate(orgID, secret); err != nil {
c.String(http.StatusInternalServerError, "Error updating org %q secret %q. %s", orgID, in.Name, err) c.String(http.StatusInternalServerError, "Error updating org %q secret %q. %s", orgID, in.Name, err)
return return
} }
@ -193,7 +199,8 @@ func DeleteOrgSecret(c *gin.Context) {
return return
} }
if err := server.Config.Services.Secrets.OrgSecretDelete(orgID, name); err != nil { secretService := server.Config.Services.Manager.SecretService()
if err := secretService.OrgSecretDelete(orgID, name); err != nil {
handleDBError(c, err) handleDBError(c, err)
return return
} }

View file

@ -434,13 +434,7 @@ func PostPipeline(c *gin.Context) {
} }
} }
netrc, err := server.Config.Services.Forge.Netrc(user, repo) newpipeline, err := pipeline.Restart(c, _store, pl, user, repo, envs)
if err != nil {
handlePipelineErr(c, err)
return
}
newpipeline, err := pipeline.Restart(c, _store, pl, user, repo, envs, netrc)
if err != nil { if err != nil {
handlePipelineErr(c, err) handlePipelineErr(c, err)
} else { } else {

View file

@ -35,11 +35,11 @@ import (
// @Param repo_id path int true "the repository id" // @Param repo_id path int true "the repository id"
// @Param registry path string true "the registry name" // @Param registry path string true "the registry name"
func GetRegistry(c *gin.Context) { func GetRegistry(c *gin.Context) {
var ( repo := session.Repo(c)
repo = session.Repo(c) name := c.Param("registry")
name = c.Param("registry")
) registryService := server.Config.Services.Manager.RegistryServiceFromRepo(repo)
registry, err := server.Config.Services.Registries.RegistryFind(repo, name) registry, err := registryService.RegistryFind(repo, name)
if err != nil { if err != nil {
handleDBError(c, err) handleDBError(c, err)
return return
@ -75,7 +75,9 @@ func PostRegistry(c *gin.Context) {
c.String(http.StatusBadRequest, "Error inserting registry. %s", err) c.String(http.StatusBadRequest, "Error inserting registry. %s", err)
return return
} }
if err := server.Config.Services.Registries.RegistryCreate(repo, registry); err != nil {
registryService := server.Config.Services.Manager.RegistryServiceFromRepo(repo)
if err := registryService.RegistryCreate(repo, registry); err != nil {
c.String(http.StatusInternalServerError, "Error inserting registry %q. %s", in.Address, err) c.String(http.StatusInternalServerError, "Error inserting registry %q. %s", in.Address, err)
return return
} }
@ -106,7 +108,8 @@ func PatchRegistry(c *gin.Context) {
return return
} }
registry, err := server.Config.Services.Registries.RegistryFind(repo, name) registryService := server.Config.Services.Manager.RegistryServiceFromRepo(repo)
registry, err := registryService.RegistryFind(repo, name)
if err != nil { if err != nil {
handleDBError(c, err) handleDBError(c, err)
return return
@ -122,7 +125,7 @@ func PatchRegistry(c *gin.Context) {
c.String(http.StatusUnprocessableEntity, "Error updating registry. %s", err) c.String(http.StatusUnprocessableEntity, "Error updating registry. %s", err)
return return
} }
if err := server.Config.Services.Registries.RegistryUpdate(repo, registry); err != nil { if err := registryService.RegistryUpdate(repo, registry); err != nil {
c.String(http.StatusInternalServerError, "Error updating registry %q. %s", in.Address, err) c.String(http.StatusInternalServerError, "Error updating registry %q. %s", in.Address, err)
return return
} }
@ -142,7 +145,8 @@ func PatchRegistry(c *gin.Context) {
// @Param perPage query int false "for response pagination, max items per page" default(50) // @Param perPage query int false "for response pagination, max items per page" default(50)
func GetRegistryList(c *gin.Context) { func GetRegistryList(c *gin.Context) {
repo := session.Repo(c) repo := session.Repo(c)
list, err := server.Config.Services.Registries.RegistryList(repo, session.Pagination(c)) registryService := server.Config.Services.Manager.RegistryServiceFromRepo(repo)
list, err := registryService.RegistryList(repo, session.Pagination(c))
if err != nil { if err != nil {
c.String(http.StatusInternalServerError, "Error getting registry list. %s", err) c.String(http.StatusInternalServerError, "Error getting registry list. %s", err)
return return
@ -166,11 +170,11 @@ func GetRegistryList(c *gin.Context) {
// @Param repo_id path int true "the repository id" // @Param repo_id path int true "the repository id"
// @Param registry path string true "the registry name" // @Param registry path string true "the registry name"
func DeleteRegistry(c *gin.Context) { func DeleteRegistry(c *gin.Context) {
var ( repo := session.Repo(c)
repo = session.Repo(c) name := c.Param("registry")
name = c.Param("registry")
) registryService := server.Config.Services.Manager.RegistryServiceFromRepo(repo)
err := server.Config.Services.Registries.RegistryDelete(repo, name) err := registryService.RegistryDelete(repo, name)
if err != nil { if err != nil {
handleDBError(c, err) handleDBError(c, err)
return return

View file

@ -36,11 +36,11 @@ import (
// @Param repo_id path int true "the repository id" // @Param repo_id path int true "the repository id"
// @Param secretName path string true "the secret name" // @Param secretName path string true "the secret name"
func GetSecret(c *gin.Context) { func GetSecret(c *gin.Context) {
var ( repo := session.Repo(c)
repo = session.Repo(c) name := c.Param("secret")
name = c.Param("secret")
) secretService := server.Config.Services.Manager.SecretServiceFromRepo(repo)
secret, err := server.Config.Services.Secrets.SecretFind(repo, name) secret, err := secretService.SecretFind(repo, name)
if err != nil { if err != nil {
handleDBError(c, err) handleDBError(c, err)
return return
@ -77,7 +77,9 @@ func PostSecret(c *gin.Context) {
c.String(http.StatusUnprocessableEntity, "Error inserting secret. %s", err) c.String(http.StatusUnprocessableEntity, "Error inserting secret. %s", err)
return return
} }
if err := server.Config.Services.Secrets.SecretCreate(repo, secret); err != nil {
secretService := server.Config.Services.Manager.SecretServiceFromRepo(repo)
if err := secretService.SecretCreate(repo, secret); err != nil {
c.String(http.StatusInternalServerError, "Error inserting secret %q. %s", in.Name, err) c.String(http.StatusInternalServerError, "Error inserting secret %q. %s", in.Name, err)
return return
} }
@ -108,7 +110,8 @@ func PatchSecret(c *gin.Context) {
return return
} }
secret, err := server.Config.Services.Secrets.SecretFind(repo, name) secretService := server.Config.Services.Manager.SecretServiceFromRepo(repo)
secret, err := secretService.SecretFind(repo, name)
if err != nil { if err != nil {
handleDBError(c, err) handleDBError(c, err)
return return
@ -127,7 +130,7 @@ func PatchSecret(c *gin.Context) {
c.String(http.StatusUnprocessableEntity, "Error updating secret. %s", err) c.String(http.StatusUnprocessableEntity, "Error updating secret. %s", err)
return return
} }
if err := server.Config.Services.Secrets.SecretUpdate(repo, secret); err != nil { if err := secretService.SecretUpdate(repo, secret); err != nil {
c.String(http.StatusInternalServerError, "Error updating secret %q. %s", in.Name, err) c.String(http.StatusInternalServerError, "Error updating secret %q. %s", in.Name, err)
return return
} }
@ -147,7 +150,8 @@ func PatchSecret(c *gin.Context) {
// @Param perPage query int false "for response pagination, max items per page" default(50) // @Param perPage query int false "for response pagination, max items per page" default(50)
func GetSecretList(c *gin.Context) { func GetSecretList(c *gin.Context) {
repo := session.Repo(c) repo := session.Repo(c)
list, err := server.Config.Services.Secrets.SecretList(repo, session.Pagination(c)) secretService := server.Config.Services.Manager.SecretServiceFromRepo(repo)
list, err := secretService.SecretList(repo, session.Pagination(c))
if err != nil { if err != nil {
c.String(http.StatusInternalServerError, "Error getting secret list. %s", err) c.String(http.StatusInternalServerError, "Error getting secret list. %s", err)
return return
@ -171,11 +175,11 @@ func GetSecretList(c *gin.Context) {
// @Param repo_id path int true "the repository id" // @Param repo_id path int true "the repository id"
// @Param secretName path string true "the secret name" // @Param secretName path string true "the secret name"
func DeleteSecret(c *gin.Context) { func DeleteSecret(c *gin.Context) {
var ( repo := session.Repo(c)
repo = session.Repo(c) name := c.Param("secret")
name = c.Param("secret")
) secretService := server.Config.Services.Manager.SecretServiceFromRepo(repo)
if err := server.Config.Services.Secrets.SecretDelete(repo, name); err != nil { if err := secretService.SecretDelete(repo, name); err != nil {
handleDBError(c, err) handleDBError(c, err)
return return
} }

View file

@ -34,7 +34,7 @@ import (
// @Tags System // @Tags System
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>) // @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
func GetSignaturePublicKey(c *gin.Context) { func GetSignaturePublicKey(c *gin.Context) {
b, err := x509.MarshalPKIXPublicKey(server.Config.Services.SignaturePublicKey) b, err := x509.MarshalPKIXPublicKey(server.Config.Services.Manager.SignaturePublicKey())
if err != nil { if err != nil {
log.Error().Err(err).Msg("can't marshal public key") log.Error().Err(err).Msg("can't marshal public key")
c.AbortWithStatus(http.StatusInternalServerError) c.AbortWithStatus(http.StatusInternalServerError)

View file

@ -18,42 +18,26 @@
package server package server
import ( import (
"crypto"
"time" "time"
"go.woodpecker-ci.org/woodpecker/v2/server/cache" "go.woodpecker-ci.org/woodpecker/v2/server/cache"
"go.woodpecker-ci.org/woodpecker/v2/server/forge" "go.woodpecker-ci.org/woodpecker/v2/server/forge"
"go.woodpecker-ci.org/woodpecker/v2/server/logging" "go.woodpecker-ci.org/woodpecker/v2/server/logging"
"go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/model"
"go.woodpecker-ci.org/woodpecker/v2/server/plugins/config"
"go.woodpecker-ci.org/woodpecker/v2/server/plugins/permissions"
"go.woodpecker-ci.org/woodpecker/v2/server/pubsub" "go.woodpecker-ci.org/woodpecker/v2/server/pubsub"
"go.woodpecker-ci.org/woodpecker/v2/server/queue" "go.woodpecker-ci.org/woodpecker/v2/server/queue"
"go.woodpecker-ci.org/woodpecker/v2/server/services"
"go.woodpecker-ci.org/woodpecker/v2/server/services/permissions"
) )
var Config = struct { var Config = struct {
Services struct { Services struct {
Pubsub *pubsub.Publisher Pubsub *pubsub.Publisher
Queue queue.Queue Queue queue.Queue
Logs logging.Log Logs logging.Log
Secrets model.SecretService Forge forge.Forge
Registries model.RegistryService Membership cache.MembershipService
Environ model.EnvironService Manager *services.Manager
Forge forge.Forge
Timeout time.Duration
Membership cache.MembershipService
ConfigService config.Extension
SignaturePrivateKey crypto.PrivateKey
SignaturePublicKey crypto.PublicKey
}
Storage struct {
// Users model.UserStore
// Repos model.RepoStore
// Builds model.BuildStore
// Logs model.LogStore
Steps model.StepStore
// Registries model.RegistryStore
// Secrets model.SecretStore
} }
Server struct { Server struct {
Key string Key string

View file

@ -1,190 +0,0 @@
// 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 forge
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/rs/zerolog/log"
"go.woodpecker-ci.org/woodpecker/v2/server/forge/types"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
"go.woodpecker-ci.org/woodpecker/v2/server/plugins/config"
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
)
type ConfigFetcher struct {
forge Forge
user *model.User
repo *model.Repo
pipeline *model.Pipeline
configExtension config.Extension
timeout time.Duration
}
func NewConfigFetcher(forge Forge, timeout time.Duration, configExtension config.Extension, user *model.User, repo *model.Repo, pipeline *model.Pipeline) *ConfigFetcher {
return &ConfigFetcher{
forge: forge,
user: user,
repo: repo,
pipeline: pipeline,
configExtension: configExtension,
timeout: timeout,
}
}
// Fetch pipeline config from source forge
func (cf *ConfigFetcher) Fetch(ctx context.Context) (files []*types.FileMeta, err error) {
log.Trace().Msgf("start fetching config for '%s'", cf.repo.FullName)
// try to fetch 3 times
for i := 0; i < 3; i++ {
files, err = cf.fetch(ctx, strings.TrimSpace(cf.repo.Config))
if err != nil {
log.Trace().Err(err).Msgf("%d. try failed", i+1)
}
if errors.Is(err, context.DeadlineExceeded) {
continue
}
if cf.configExtension != nil {
fetchCtx, cancel := context.WithTimeout(ctx, cf.timeout)
defer cancel() // ok here as we only try http fetching once, returning on fail and success
log.Trace().Msgf("configFetcher[%s]: getting config from external http service", cf.repo.FullName)
netrc, err := cf.forge.Netrc(cf.user, cf.repo)
if err != nil {
return nil, fmt.Errorf("could not get Netrc data from forge: %w", err)
}
newConfigs, useOld, err := cf.configExtension.FetchConfig(fetchCtx, cf.repo, cf.pipeline, files, netrc)
if err != nil {
log.Error().Err(err).Msg("could not fetch config via http")
return nil, fmt.Errorf("could not fetch config via http: %w", err)
}
if !useOld {
return newConfigs, nil
}
}
return
}
return
}
// fetch config by timeout
func (cf *ConfigFetcher) fetch(c context.Context, config string) ([]*types.FileMeta, error) {
ctx, cancel := context.WithTimeout(c, cf.timeout)
defer cancel()
if len(config) > 0 {
log.Trace().Msgf("configFetcher[%s]: use user config '%s'", cf.repo.FullName, config)
// could be adapted to allow the user to supply a list like we do in the defaults
configs := []string{config}
fileMeta, err := cf.getFirstAvailableConfig(ctx, configs)
if err == nil {
return fileMeta, err
}
return nil, fmt.Errorf("user defined config '%s' not found: %w", config, err)
}
log.Trace().Msgf("configFetcher[%s]: user did not define own config, following default procedure", cf.repo.FullName)
// for the order see shared/constants/constants.go
fileMeta, err := cf.getFirstAvailableConfig(ctx, constant.DefaultConfigOrder[:])
if err == nil {
return fileMeta, err
}
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
return []*types.FileMeta{}, fmt.Errorf("configFetcher: fallback did not find config: %w", err)
}
}
func filterPipelineFiles(files []*types.FileMeta) []*types.FileMeta {
var res []*types.FileMeta
for _, file := range files {
if strings.HasSuffix(file.Name, ".yml") || strings.HasSuffix(file.Name, ".yaml") {
res = append(res, file)
}
}
return res
}
func (cf *ConfigFetcher) checkPipelineFile(c context.Context, config string) ([]*types.FileMeta, error) {
file, err := cf.forge.File(c, cf.user, cf.repo, cf.pipeline, config)
if err == nil && len(file) != 0 {
log.Trace().Msgf("configFetcher[%s]: found file '%s'", cf.repo.FullName, config)
return []*types.FileMeta{{
Name: config,
Data: file,
}}, nil
}
return nil, err
}
func (cf *ConfigFetcher) getFirstAvailableConfig(c context.Context, configs []string) ([]*types.FileMeta, error) {
var forgeErr []error
for _, fileOrFolder := range configs {
if strings.HasSuffix(fileOrFolder, "/") {
// config is a folder
files, err := cf.forge.Dir(c, cf.user, cf.repo, cf.pipeline, strings.TrimSuffix(fileOrFolder, "/"))
// if folder is not supported we will get a "Not implemented" error and continue
if err != nil {
if !(errors.Is(err, types.ErrNotImplemented) || errors.Is(err, &types.ErrConfigNotFound{})) {
log.Error().Err(err).Str("repo", cf.repo.FullName).Str("user", cf.user.Login).Msg("could not get folder from forge")
forgeErr = append(forgeErr, err)
}
continue
}
files = filterPipelineFiles(files)
if len(files) != 0 {
log.Trace().Msgf("configFetcher[%s]: found %d files in '%s'", cf.repo.FullName, len(files), fileOrFolder)
return files, nil
}
}
// config is a file
if fileMeta, err := cf.checkPipelineFile(c, fileOrFolder); err == nil {
log.Trace().Msgf("configFetcher[%s]: found file: '%s'", cf.repo.FullName, fileOrFolder)
return fileMeta, nil
} else if !errors.Is(err, &types.ErrConfigNotFound{}) {
forgeErr = append(forgeErr, err)
}
}
// got unexpected errors
if len(forgeErr) != 0 {
return nil, errors.Join(forgeErr...)
}
// nothing found
return nil, &types.ErrConfigNotFound{Configs: configs}
}

View file

@ -24,11 +24,6 @@ var (
errEnvironValueInvalid = errors.New("invalid Environment Variable Value") errEnvironValueInvalid = errors.New("invalid Environment Variable Value")
) )
// EnvironService defines a service for managing environment variables.
type EnvironService interface {
EnvironList(*Repo) ([]*Environ, error)
}
// EnvironStore persists environment information to storage. // EnvironStore persists environment information to storage.
type EnvironStore interface { type EnvironStore interface {
EnvironList(*Repo) ([]*Environ, error) EnvironList(*Repo) ([]*Environ, error)

View file

@ -26,21 +26,6 @@ var (
errRegistryPasswordInvalid = errors.New("invalid registry password") errRegistryPasswordInvalid = errors.New("invalid registry password")
) )
// RegistryService defines a service for managing registries.
type RegistryService interface {
RegistryFind(*Repo, string) (*Registry, error)
RegistryList(*Repo, *ListOptions) ([]*Registry, error)
RegistryCreate(*Repo, *Registry) error
RegistryUpdate(*Repo, *Registry) error
RegistryDelete(*Repo, string) error
}
// ReadOnlyRegistryService defines a service for managing registries.
type ReadOnlyRegistryService interface {
RegistryFind(*Repo, string) (*Registry, error)
RegistryList(*Repo, *ListOptions) ([]*Registry, error)
}
// RegistryStore persists registry information to storage. // RegistryStore persists registry information to storage.
type RegistryStore interface { type RegistryStore interface {
RegistryFind(*Repo, string) (*Registry, error) RegistryFind(*Repo, string) (*Registry, error)

View file

@ -29,29 +29,6 @@ var (
ErrSecretEventInvalid = errors.New("invalid secret event") ErrSecretEventInvalid = errors.New("invalid secret event")
) )
// SecretService defines a service for managing secrets.
type SecretService interface {
SecretListPipeline(*Repo, *Pipeline, *ListOptions) ([]*Secret, error)
// Repository secrets
SecretFind(*Repo, string) (*Secret, error)
SecretList(*Repo, *ListOptions) ([]*Secret, error)
SecretCreate(*Repo, *Secret) error
SecretUpdate(*Repo, *Secret) error
SecretDelete(*Repo, string) error
// Organization secrets
OrgSecretFind(int64, string) (*Secret, error)
OrgSecretList(int64, *ListOptions) ([]*Secret, error)
OrgSecretCreate(int64, *Secret) error
OrgSecretUpdate(int64, *Secret) error
OrgSecretDelete(int64, string) error
// Global secrets
GlobalSecretFind(string) (*Secret, error)
GlobalSecretList(*ListOptions) ([]*Secret, error)
GlobalSecretCreate(*Secret) error
GlobalSecretUpdate(*Secret) error
GlobalSecretDelete(string) error
}
// SecretStore persists secret information to storage. // SecretStore persists secret information to storage.
type SecretStore interface { type SecretStore interface {
SecretFind(*Repo, string) (*Secret, error) SecretFind(*Repo, string) (*Secret, error)

View file

@ -34,6 +34,7 @@ var skipPipelineRegex = regexp.MustCompile(`\[(?i:ci *skip|skip *ci)\]`)
// Create a new pipeline and start it // Create a new pipeline and start it
func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline *model.Pipeline) (*model.Pipeline, error) { 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) repoUser, err := _store.GetUser(repo.UserID)
if err != nil { if err != nil {
msg := fmt.Sprintf("failure to find repo owner via id '%d'", repo.UserID) msg := fmt.Sprintf("failure to find repo owner via id '%d'", repo.UserID)
@ -54,7 +55,7 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline
// If the forge has a refresh token, the current access token // If the forge has a refresh token, the current access token
// may be stale. Therefore, we should refresh prior to dispatching // may be stale. Therefore, we should refresh prior to dispatching
// the pipeline. // the pipeline.
forge.Refresh(ctx, server.Config.Services.Forge, _store, repoUser) forge.Refresh(ctx, _forge, _store, repoUser)
// update some pipeline fields // update some pipeline fields
pipeline.RepoID = repo.ID pipeline.RepoID = repo.ID
@ -68,8 +69,8 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline
} }
// fetch the pipeline file from the forge // fetch the pipeline file from the forge
configFetcher := forge.NewConfigFetcher(server.Config.Services.Forge, server.Config.Services.Timeout, server.Config.Services.ConfigService, repoUser, repo, pipeline) configService := server.Config.Services.Manager.ConfigServiceFromRepo(repo)
forgeYamlConfigs, configFetchErr := configFetcher.Fetch(ctx) forgeYamlConfigs, configFetchErr := configService.Fetch(ctx, _forge, repoUser, repo, pipeline, nil, false)
if errors.Is(configFetchErr, &forge_types.ErrConfigNotFound{}) { if errors.Is(configFetchErr, &forge_types.ErrConfigNotFound{}) {
log.Debug().Str("repo", repo.FullName).Err(configFetchErr).Msgf("cannot find config '%s' in '%s' with user: '%s'", repo.Config, pipeline.Ref, repoUser.Login) log.Debug().Str("repo", repo.FullName).Err(configFetchErr).Msgf("cannot find config '%s' in '%s' with user: '%s'", repo.Config, pipeline.Ref, repoUser.Login)
if err := _store.DeletePipeline(pipeline); err != nil { if err := _store.DeletePipeline(pipeline); err != nil {

View file

@ -42,12 +42,14 @@ func parsePipeline(store store.Store, currentPipeline *model.Pipeline, user *mod
log.Error().Err(err).Str("repo", repo.FullName).Msgf("error getting last pipeline before pipeline number '%d'", currentPipeline.Number) log.Error().Err(err).Str("repo", repo.FullName).Msgf("error getting last pipeline before pipeline number '%d'", currentPipeline.Number)
} }
secs, err := server.Config.Services.Secrets.SecretListPipeline(repo, currentPipeline, &model.ListOptions{All: true}) secretService := server.Config.Services.Manager.SecretServiceFromRepo(repo)
secs, err := secretService.SecretListPipeline(repo, currentPipeline, &model.ListOptions{All: true})
if err != nil { if err != nil {
log.Error().Err(err).Msgf("error getting secrets for %s#%d", repo.FullName, currentPipeline.Number) log.Error().Err(err).Msgf("error getting secrets for %s#%d", repo.FullName, currentPipeline.Number)
} }
regs, err := server.Config.Services.Registries.RegistryList(repo, &model.ListOptions{All: true}) registryService := server.Config.Services.Manager.RegistryServiceFromRepo(repo)
regs, err := registryService.RegistryList(repo, &model.ListOptions{All: true})
if err != nil { if err != nil {
log.Error().Err(err).Msgf("error getting registry credentials for %s#%d", repo.FullName, currentPipeline.Number) log.Error().Err(err).Msgf("error getting registry credentials for %s#%d", repo.FullName, currentPipeline.Number)
} }
@ -55,8 +57,10 @@ func parsePipeline(store store.Store, currentPipeline *model.Pipeline, user *mod
if envs == nil { if envs == nil {
envs = map[string]string{} envs = map[string]string{}
} }
if server.Config.Services.Environ != nil {
globals, _ := server.Config.Services.Environ.EnvironList(repo) environmentService := server.Config.Services.Manager.EnvironmentService()
if environmentService != nil {
globals, _ := environmentService.EnvironList(repo)
for _, global := range globals { for _, global := range globals {
envs[global.Name] = global.Value envs[global.Name] = global.Value
} }

View file

@ -28,15 +28,14 @@ import (
) )
// Restart a pipeline by creating a new one out of the old and start it // 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, netrc *model.Netrc) (*model.Pipeline, error) { 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
switch lastPipeline.Status { switch lastPipeline.Status {
case model.StatusDeclined, case model.StatusDeclined,
model.StatusBlocked: model.StatusBlocked:
return nil, &ErrBadRequest{Msg: fmt.Sprintf("cannot restart a pipeline with status %s", lastPipeline.Status)} return nil, &ErrBadRequest{Msg: fmt.Sprintf("cannot restart a pipeline with status %s", lastPipeline.Status)}
} }
var pipelineFiles []*forge_types.FileMeta
// fetch the old pipeline config from the database // fetch the old pipeline config from the database
configs, err := store.ConfigsForPipeline(lastPipeline.ID) configs, err := store.ConfigsForPipeline(lastPipeline.ID)
if err != nil { if err != nil {
@ -44,25 +43,17 @@ func Restart(ctx context.Context, store store.Store, lastPipeline *model.Pipelin
return nil, &ErrNotFound{Msg: fmt.Sprintf("failure to get pipeline config for %s. %s", repo.FullName, err)} return nil, &ErrNotFound{Msg: fmt.Sprintf("failure to get pipeline config for %s. %s", repo.FullName, err)}
} }
var pipelineFiles []*forge_types.FileMeta
for _, y := range configs { for _, y := range configs {
pipelineFiles = append(pipelineFiles, &forge_types.FileMeta{Data: y.Data, Name: y.Name}) pipelineFiles = append(pipelineFiles, &forge_types.FileMeta{Data: y.Data, Name: y.Name})
} }
// If the config extension is active we should refetch the config in case something changed // If the config service is active we should refetch the config in case something changed
if server.Config.Services.ConfigService != nil { configService := server.Config.Services.Manager.ConfigServiceFromRepo(repo)
currentFileMeta := make([]*forge_types.FileMeta, len(configs)) pipelineFiles, err = configService.Fetch(ctx, forge, user, repo, lastPipeline, pipelineFiles, true)
for i, cfg := range configs { if err != nil {
currentFileMeta[i] = &forge_types.FileMeta{Name: cfg.Name, Data: cfg.Data} return nil, &ErrBadRequest{
} Msg: fmt.Sprintf("On fetching external pipeline config: %s", err),
newConfig, useOld, err := server.Config.Services.ConfigService.FetchConfig(ctx, repo, lastPipeline, currentFileMeta, netrc)
if err != nil {
return nil, &ErrBadRequest{
Msg: fmt.Sprintf("On fetching external pipeline config: %s", err),
}
}
if !useOld {
pipelineFiles = newConfig
} }
} }

View file

@ -1,83 +0,0 @@
// 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 config
import (
"context"
"crypto"
"fmt"
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/plugins/utils"
)
type http struct {
endpoint string
privateKey crypto.PrivateKey
}
// Same as forge.FileMeta but with json tags and string data
type config struct {
Name string `json:"name"`
Data string `json:"data"`
}
type requestStructure struct {
Repo *model.Repo `json:"repo"`
Pipeline *model.Pipeline `json:"pipeline"`
Configuration []*config `json:"configs"`
Netrc *model.Netrc `json:"netrc"`
}
type responseStructure struct {
Configs []config `json:"configs"`
}
func NewHTTP(endpoint string, privateKey crypto.PrivateKey) Extension {
return &http{endpoint, privateKey}
}
func (cp *http) FetchConfig(ctx context.Context, repo *model.Repo, pipeline *model.Pipeline, currentFileMeta []*forge_types.FileMeta, netrc *model.Netrc) (configData []*forge_types.FileMeta, useOld bool, err error) {
currentConfigs := make([]*config, len(currentFileMeta))
for i, pipe := range currentFileMeta {
currentConfigs[i] = &config{Name: pipe.Name, Data: string(pipe.Data)}
}
response := new(responseStructure)
body := requestStructure{
Repo: repo,
Pipeline: pipeline,
Configuration: currentConfigs,
Netrc: netrc,
}
status, err := utils.Send(ctx, "POST", cp.endpoint, cp.privateKey, body, response)
if err != nil && status != 204 {
return nil, false, fmt.Errorf("failed to fetch config via http (%d) %w", status, err)
}
var newFileMeta []*forge_types.FileMeta
if status != 200 {
newFileMeta = make([]*forge_types.FileMeta, 0)
} else {
newFileMeta = make([]*forge_types.FileMeta, len(response.Configs))
for i, pipe := range response.Configs {
newFileMeta[i] = &forge_types.FileMeta{Name: pipe.Name, Data: []byte(pipe.Data)}
}
}
return newFileMeta, status == 204, nil
}

View file

@ -1,136 +0,0 @@
// 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 secrets
import (
"context"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
)
type builtin struct {
context.Context
store model.SecretStore
}
// New returns a new local secret service.
func New(ctx context.Context, store model.SecretStore) model.SecretService {
return &builtin{store: store, Context: ctx}
}
func (b *builtin) SecretFind(repo *model.Repo, name string) (*model.Secret, error) {
return b.store.SecretFind(repo, name)
}
func (b *builtin) SecretList(repo *model.Repo, p *model.ListOptions) ([]*model.Secret, error) {
return b.store.SecretList(repo, false, p)
}
func (b *builtin) SecretListPipeline(repo *model.Repo, _ *model.Pipeline, p *model.ListOptions) ([]*model.Secret, error) {
s, err := b.store.SecretList(repo, true, p)
if err != nil {
return nil, err
}
// Return only secrets with unique name
// Priority order in case of duplicate names are repository, user/organization, global
secrets := make([]*model.Secret, 0, len(s))
uniq := make(map[string]struct{})
for _, condition := range []struct {
IsRepository bool
IsOrganization bool
IsGlobal bool
}{
{IsRepository: true},
{IsOrganization: true},
{IsGlobal: true},
} {
for _, secret := range s {
if secret.IsRepository() != condition.IsRepository || secret.IsOrganization() != condition.IsOrganization || secret.IsGlobal() != condition.IsGlobal {
continue
}
if _, ok := uniq[secret.Name]; ok {
continue
}
uniq[secret.Name] = struct{}{}
secrets = append(secrets, secret)
}
}
return secrets, nil
}
func (b *builtin) SecretCreate(_ *model.Repo, in *model.Secret) error {
return b.store.SecretCreate(in)
}
func (b *builtin) SecretUpdate(_ *model.Repo, in *model.Secret) error {
return b.store.SecretUpdate(in)
}
func (b *builtin) SecretDelete(repo *model.Repo, name string) error {
secret, err := b.store.SecretFind(repo, name)
if err != nil {
return err
}
return b.store.SecretDelete(secret)
}
func (b *builtin) OrgSecretFind(owner int64, name string) (*model.Secret, error) {
return b.store.OrgSecretFind(owner, name)
}
func (b *builtin) OrgSecretList(owner int64, p *model.ListOptions) ([]*model.Secret, error) {
return b.store.OrgSecretList(owner, p)
}
func (b *builtin) OrgSecretCreate(_ int64, in *model.Secret) error {
return b.store.SecretCreate(in)
}
func (b *builtin) OrgSecretUpdate(_ int64, in *model.Secret) error {
return b.store.SecretUpdate(in)
}
func (b *builtin) OrgSecretDelete(owner int64, name string) error {
secret, err := b.store.OrgSecretFind(owner, name)
if err != nil {
return err
}
return b.store.SecretDelete(secret)
}
func (b *builtin) GlobalSecretFind(owner string) (*model.Secret, error) {
return b.store.GlobalSecretFind(owner)
}
func (b *builtin) GlobalSecretList(p *model.ListOptions) ([]*model.Secret, error) {
return b.store.GlobalSecretList(p)
}
func (b *builtin) GlobalSecretCreate(in *model.Secret) error {
return b.store.SecretCreate(in)
}
func (b *builtin) GlobalSecretUpdate(in *model.Secret) error {
return b.store.SecretUpdate(in)
}
func (b *builtin) GlobalSecretDelete(name string) error {
secret, err := b.store.GlobalSecretFind(name)
if err != nil {
return err
}
return b.store.SecretDelete(secret)
}

View file

@ -0,0 +1,40 @@
// 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 config
import (
"context"
"go.woodpecker-ci.org/woodpecker/v2/server/forge"
"go.woodpecker-ci.org/woodpecker/v2/server/forge/types"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
)
type combined struct {
services []Service
}
func NewCombined(services ...Service) Service {
return &combined{services: services}
}
func (c *combined) Fetch(ctx context.Context, forge forge.Forge, user *model.User, repo *model.Repo, pipeline *model.Pipeline, oldConfigData []*types.FileMeta, restart bool) (files []*types.FileMeta, err error) {
files = oldConfigData
for _, s := range c.services {
files, err = s.Fetch(ctx, forge, user, repo, pipeline, files, restart)
}
return files, err
}

View file

@ -0,0 +1,248 @@
// 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 config_test
import (
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"time"
"github.com/go-ap/httpsig"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"go.woodpecker-ci.org/woodpecker/v2/server/forge/mocks"
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/services/config"
)
func TestFetchFromConfigService(t *testing.T) {
t.Parallel()
type file struct {
name string
data []byte
}
dummyData := []byte("TEST")
testTable := []struct {
name string
repoConfig string
files []file
expectedFileNames []string
expectedError bool
}{
{
name: "External Fetch empty repo",
repoConfig: "",
files: []file{},
expectedFileNames: []string{"override1", "override2", "override3"},
expectedError: false,
},
{
name: "Default config - Additional sub-folders",
repoConfig: "",
files: []file{{
name: ".woodpecker/test.yml",
data: dummyData,
}, {
name: ".woodpecker/sub-folder/config.yml",
data: dummyData,
}},
expectedFileNames: []string{"override1", "override2", "override3"},
expectedError: false,
},
{
name: "Fetch empty",
repoConfig: " ",
files: []file{{
name: ".woodpecker/.keep",
data: dummyData,
}, {
name: ".woodpecker.yml",
data: nil,
}, {
name: ".woodpecker.yaml",
data: dummyData,
}},
expectedFileNames: []string{},
expectedError: true,
},
{
name: "Use old config",
repoConfig: ".my-ci-folder/",
files: []file{{
name: ".woodpecker/test.yml",
data: dummyData,
}, {
name: ".woodpecker.yml",
data: dummyData,
}, {
name: ".woodpecker.yaml",
data: dummyData,
}, {
name: ".my-ci-folder/test.yml",
data: dummyData,
}},
expectedFileNames: []string{
".my-ci-folder/test.yml",
},
expectedError: false,
},
}
pubEd25519Key, privEd25519Key, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal("can't generate ed25519 key pair")
}
fixtureHandler := func(w http.ResponseWriter, r *http.Request) {
// check signature
pubKeyID := "woodpecker-ci-plugins"
keystore := httpsig.NewMemoryKeyStore()
keystore.SetKey(pubKeyID, pubEd25519Key)
verifier := httpsig.NewVerifier(keystore)
verifier.SetRequiredHeaders([]string{"(request-target)", "date"})
keyID, err := verifier.Verify(r)
if err != nil {
http.Error(w, "Invalid signature", http.StatusBadRequest)
return
}
if keyID != pubKeyID {
http.Error(w, "Used wrong key", http.StatusBadRequest)
return
}
type config struct {
Name string `json:"name"`
Data string `json:"data"`
}
type incoming struct {
Repo *model.Repo `json:"repo"`
Build *model.Pipeline `json:"pipeline"`
Configuration []*config `json:"config"`
}
var req incoming
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "can't read body", http.StatusBadRequest)
return
}
err = json.Unmarshal(body, &req)
if err != nil {
http.Error(w, "Failed to parse JSON"+err.Error(), http.StatusBadRequest)
return
}
if req.Repo.Name == "Fetch empty" {
w.WriteHeader(404)
return
}
if req.Repo.Name == "Use old config" {
w.WriteHeader(204)
return
}
fmt.Fprint(w, `{
"configs": [
{
"name": "override1",
"data": "some new pipelineconfig \n pipe, pipe, pipe"
},
{
"name": "override2",
"data": "some new pipelineconfig \n pipe, pipe, pipe"
},
{
"name": "override3",
"data": "some new pipelineconfig \n pipe, pipe, pipe"
}
]
}`)
}
ts := httptest.NewServer(http.HandlerFunc(fixtureHandler))
defer ts.Close()
httpFetcher := config.NewHTTP(ts.URL, privEd25519Key)
for _, tt := range testTable {
t.Run(tt.name, func(t *testing.T) {
repo := &model.Repo{Owner: "laszlocph", Name: tt.name, Config: tt.repoConfig} // Using test name as repo name to provide different responses in mock server
f := new(mocks.Forge)
dirs := map[string][]*forge_types.FileMeta{}
for _, file := range tt.files {
f.On("File", mock.Anything, mock.Anything, mock.Anything, mock.Anything, file.name).Return(file.data, nil)
path := filepath.Dir(file.name)
if path != "." {
dirs[path] = append(dirs[path], &forge_types.FileMeta{
Name: file.name,
Data: file.data,
})
}
}
for path, files := range dirs {
f.On("Dir", mock.Anything, mock.Anything, mock.Anything, mock.Anything, path).Return(files, nil)
}
// if the previous mocks do not match return not found errors
f.On("File", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("file not found"))
f.On("Dir", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("directory not found"))
f.On("Netrc", mock.Anything, mock.Anything).Return(&model.Netrc{Machine: "mock", Login: "mock", Password: "mock"}, nil)
forgeFetcher := config.NewForge(time.Second * 3)
configFetcher := config.NewCombined(forgeFetcher, httpFetcher)
files, err := configFetcher.Fetch(
context.Background(),
f,
&model.User{Token: "xxx"},
repo,
&model.Pipeline{Commit: "89ab7b2d6bfb347144ac7c557e638ab402848fee"},
[]*forge_types.FileMeta{},
false,
)
if tt.expectedError && err == nil {
t.Fatal("expected an error")
} else if !tt.expectedError && err != nil {
t.Fatal("error fetching config:", err)
}
matchingFiles := make([]string, len(files))
for i := range files {
matchingFiles[i] = files[i].Name
}
assert.ElementsMatch(t, tt.expectedFileNames, matchingFiles, "expected some other pipeline files")
})
}
}

View file

@ -0,0 +1,180 @@
// 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 config
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/rs/zerolog/log"
"go.woodpecker-ci.org/woodpecker/v2/server/forge"
"go.woodpecker-ci.org/woodpecker/v2/server/forge/types"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
)
const (
forgeFetchingRetryCount = 3
)
type forgeFetcher struct {
timeout time.Duration
}
func NewForge(timeout time.Duration) Service {
return &forgeFetcher{
timeout: timeout,
}
}
func (f *forgeFetcher) Fetch(ctx context.Context, forge forge.Forge, user *model.User, repo *model.Repo, pipeline *model.Pipeline, oldConfigData []*types.FileMeta, restart bool) (files []*types.FileMeta, err error) {
// skip fetching if we are restarting and have the old config
if restart && len(oldConfigData) > 0 {
return
}
ffc := &forgeFetcherContext{
forge: forge,
user: user,
repo: repo,
pipeline: pipeline,
timeout: f.timeout,
}
// try to fetch multiple times
for i := 0; i < forgeFetchingRetryCount; i++ {
files, err = ffc.fetch(ctx, strings.TrimSpace(repo.Config))
if err != nil {
log.Trace().Err(err).Msgf("%d. try failed", i+1)
}
if errors.Is(err, context.DeadlineExceeded) {
continue
}
}
return
}
type forgeFetcherContext struct {
forge forge.Forge
user *model.User
repo *model.Repo
pipeline *model.Pipeline
timeout time.Duration
}
// fetch config by timeout
func (f *forgeFetcherContext) fetch(c context.Context, config string) ([]*types.FileMeta, error) {
ctx, cancel := context.WithTimeout(c, f.timeout)
defer cancel()
if len(config) > 0 {
log.Trace().Msgf("configFetcher[%s]: use user config '%s'", f.repo.FullName, config)
// could be adapted to allow the user to supply a list like we do in the defaults
configs := []string{config}
fileMetas, err := f.getFirstAvailableConfig(ctx, configs)
if err == nil {
return fileMetas, err
}
return nil, fmt.Errorf("user defined config '%s' not found: %w", config, err)
}
log.Trace().Msgf("configFetcher[%s]: user did not define own config, following default procedure", f.repo.FullName)
// for the order see shared/constants/constants.go
fileMetas, err := f.getFirstAvailableConfig(ctx, constant.DefaultConfigOrder[:])
if err == nil {
return fileMetas, err
}
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
return nil, fmt.Errorf("configFetcher: fallback did not find config: %w", err)
}
}
func filterPipelineFiles(files []*types.FileMeta) []*types.FileMeta {
var res []*types.FileMeta
for _, file := range files {
if strings.HasSuffix(file.Name, ".yml") || strings.HasSuffix(file.Name, ".yaml") {
res = append(res, file)
}
}
return res
}
func (f *forgeFetcherContext) checkPipelineFile(c context.Context, config string) ([]*types.FileMeta, error) {
file, err := f.forge.File(c, f.user, f.repo, f.pipeline, config)
if err == nil && len(file) != 0 {
log.Trace().Msgf("configFetcher[%s]: found file '%s'", f.repo.FullName, config)
return []*types.FileMeta{{
Name: config,
Data: file,
}}, nil
}
return nil, err
}
func (f *forgeFetcherContext) getFirstAvailableConfig(c context.Context, configs []string) ([]*types.FileMeta, error) {
var forgeErr []error
for _, fileOrFolder := range configs {
if strings.HasSuffix(fileOrFolder, "/") {
// config is a folder
files, err := f.forge.Dir(c, f.user, f.repo, f.pipeline, strings.TrimSuffix(fileOrFolder, "/"))
// if folder is not supported we will get a "Not implemented" error and continue
if err != nil {
if !(errors.Is(err, types.ErrNotImplemented) || errors.Is(err, &types.ErrConfigNotFound{})) {
log.Error().Err(err).Str("repo", f.repo.FullName).Str("user", f.user.Login).Msg("could not get folder from forge")
forgeErr = append(forgeErr, err)
}
continue
}
files = filterPipelineFiles(files)
if len(files) != 0 {
log.Trace().Msgf("configFetcher[%s]: found %d files in '%s'", f.repo.FullName, len(files), fileOrFolder)
return files, nil
}
}
// config is a file
if fileMeta, err := f.checkPipelineFile(c, fileOrFolder); err == nil {
log.Trace().Msgf("configFetcher[%s]: found file: '%s'", f.repo.FullName, fileOrFolder)
return fileMeta, nil
} else if !errors.Is(err, &types.ErrConfigNotFound{}) {
forgeErr = append(forgeErr, err)
}
}
// got unexpected errors
if len(forgeErr) != 0 {
return nil, errors.Join(forgeErr...)
}
// nothing found
return nil, &types.ErrConfigNotFound{Configs: configs}
}

View file

@ -12,30 +12,22 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package forge_test package config_test
import ( import (
"context" "context"
"crypto/ed25519"
"crypto/rand"
"encoding/json"
"fmt" "fmt"
"io"
"net/http"
"net/http/httptest"
"path/filepath" "path/filepath"
"testing" "testing"
"time" "time"
"github.com/go-ap/httpsig"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"go.woodpecker-ci.org/woodpecker/v2/server/forge"
"go.woodpecker-ci.org/woodpecker/v2/server/forge/mocks" "go.woodpecker-ci.org/woodpecker/v2/server/forge/mocks"
forge_types "go.woodpecker-ci.org/woodpecker/v2/server/forge/types" 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/model"
"go.woodpecker-ci.org/woodpecker/v2/server/plugins/config" "go.woodpecker-ci.org/woodpecker/v2/server/services/config"
) )
func TestFetch(t *testing.T) { func TestFetch(t *testing.T) {
@ -313,223 +305,18 @@ func TestFetch(t *testing.T) {
f.On("File", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("file not found")) f.On("File", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("file not found"))
f.On("Dir", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("directory not found")) f.On("Dir", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("directory not found"))
configFetcher := forge.NewConfigFetcher( configFetcher := config.NewForge(
time.Second * 3,
)
files, err := configFetcher.Fetch(
context.Background(),
f, f,
time.Second*3, &model.User{Token: "xxx"},
repo,
&model.Pipeline{Commit: "89ab7b2d6bfb347144ac7c557e638ab402848fee"},
nil, nil,
&model.User{Token: "xxx"}, false,
repo,
&model.Pipeline{Commit: "89ab7b2d6bfb347144ac7c557e638ab402848fee"},
) )
files, err := configFetcher.Fetch(context.Background())
if tt.expectedError && err == nil {
t.Fatal("expected an error")
} else if !tt.expectedError && err != nil {
t.Fatal("error fetching config:", err)
}
matchingFiles := make([]string, len(files))
for i := range files {
matchingFiles[i] = files[i].Name
}
assert.ElementsMatch(t, tt.expectedFileNames, matchingFiles, "expected some other pipeline files")
})
}
}
func TestFetchFromConfigService(t *testing.T) {
t.Parallel()
type file struct {
name string
data []byte
}
dummyData := []byte("TEST")
testTable := []struct {
name string
repoConfig string
files []file
expectedFileNames []string
expectedError bool
}{
{
name: "External Fetch empty repo",
repoConfig: "",
files: []file{},
expectedFileNames: []string{"override1", "override2", "override3"},
expectedError: false,
},
{
name: "Default config - Additional sub-folders",
repoConfig: "",
files: []file{{
name: ".woodpecker/test.yml",
data: dummyData,
}, {
name: ".woodpecker/sub-folder/config.yml",
data: dummyData,
}},
expectedFileNames: []string{"override1", "override2", "override3"},
expectedError: false,
},
{
name: "Fetch empty",
repoConfig: " ",
files: []file{{
name: ".woodpecker/.keep",
data: dummyData,
}, {
name: ".woodpecker.yml",
data: nil,
}, {
name: ".woodpecker.yaml",
data: dummyData,
}},
expectedFileNames: []string{},
expectedError: true,
},
{
name: "Use old config",
repoConfig: ".my-ci-folder/",
files: []file{{
name: ".woodpecker/test.yml",
data: dummyData,
}, {
name: ".woodpecker.yml",
data: dummyData,
}, {
name: ".woodpecker.yaml",
data: dummyData,
}, {
name: ".my-ci-folder/test.yml",
data: dummyData,
}},
expectedFileNames: []string{
".my-ci-folder/test.yml",
},
expectedError: false,
},
}
pubEd25519Key, privEd25519Key, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal("can't generate ed25519 key pair")
}
fixtureHandler := func(w http.ResponseWriter, r *http.Request) {
// check signature
pubKeyID := "woodpecker-ci-plugins"
keystore := httpsig.NewMemoryKeyStore()
keystore.SetKey(pubKeyID, pubEd25519Key)
verifier := httpsig.NewVerifier(keystore)
verifier.SetRequiredHeaders([]string{"(request-target)", "date"})
keyID, err := verifier.Verify(r)
if err != nil {
http.Error(w, "Invalid signature", http.StatusBadRequest)
return
}
if keyID != pubKeyID {
http.Error(w, "Used wrong key", http.StatusBadRequest)
return
}
type config struct {
Name string `json:"name"`
Data string `json:"data"`
}
type incoming struct {
Repo *model.Repo `json:"repo"`
Build *model.Pipeline `json:"pipeline"`
Configuration []*config `json:"config"`
}
var req incoming
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "can't read body", http.StatusBadRequest)
return
}
err = json.Unmarshal(body, &req)
if err != nil {
http.Error(w, "Failed to parse JSON"+err.Error(), http.StatusBadRequest)
return
}
if req.Repo.Name == "Fetch empty" {
w.WriteHeader(404)
return
}
if req.Repo.Name == "Use old config" {
w.WriteHeader(204)
return
}
fmt.Fprint(w, `{
"configs": [
{
"name": "override1",
"data": "some new pipelineconfig \n pipe, pipe, pipe"
},
{
"name": "override2",
"data": "some new pipelineconfig \n pipe, pipe, pipe"
},
{
"name": "override3",
"data": "some new pipelineconfig \n pipe, pipe, pipe"
}
]
}`)
}
ts := httptest.NewServer(http.HandlerFunc(fixtureHandler))
defer ts.Close()
configAPI := config.NewHTTP(ts.URL, privEd25519Key)
for _, tt := range testTable {
t.Run(tt.name, func(t *testing.T) {
repo := &model.Repo{Owner: "laszlocph", Name: tt.name, Config: tt.repoConfig} // Using test name as repo name to provide different responses in mock server
f := new(mocks.Forge)
dirs := map[string][]*forge_types.FileMeta{}
for _, file := range tt.files {
f.On("File", mock.Anything, mock.Anything, mock.Anything, mock.Anything, file.name).Return(file.data, nil)
path := filepath.Dir(file.name)
if path != "." {
dirs[path] = append(dirs[path], &forge_types.FileMeta{
Name: file.name,
Data: file.data,
})
}
}
for path, files := range dirs {
f.On("Dir", mock.Anything, mock.Anything, mock.Anything, mock.Anything, path).Return(files, nil)
}
// if the previous mocks do not match return not found errors
f.On("File", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("file not found"))
f.On("Dir", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("directory not found"))
f.On("Netrc", mock.Anything, mock.Anything).Return(&model.Netrc{Machine: "mock", Login: "mock", Password: "mock"}, nil)
configFetcher := forge.NewConfigFetcher(
f,
time.Second*3,
configAPI,
&model.User{Token: "xxx"},
repo,
&model.Pipeline{Commit: "89ab7b2d6bfb347144ac7c557e638ab402848fee"},
)
files, err := configFetcher.Fetch(context.Background())
if tt.expectedError && err == nil { if tt.expectedError && err == nil {
t.Fatal("expected an error") t.Fatal("expected an error")
} else if !tt.expectedError && err != nil { } else if !tt.expectedError && err != nil {

View file

@ -0,0 +1,88 @@
// 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 config
import (
"context"
"crypto"
"fmt"
"go.woodpecker-ci.org/woodpecker/v2/server/forge"
"go.woodpecker-ci.org/woodpecker/v2/server/forge/types"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
"go.woodpecker-ci.org/woodpecker/v2/server/services/utils"
)
type http struct {
endpoint string
privateKey crypto.PrivateKey
}
// configData same as forge.FileMeta but with json tags and string data
type configData struct {
Name string `json:"name"`
Data string `json:"data"`
}
type requestStructure struct {
Repo *model.Repo `json:"repo"`
Pipeline *model.Pipeline `json:"pipeline"`
Netrc *model.Netrc `json:"netrc"`
Configuration []*configData `json:"configs"` // TODO: deprecate in favor of netrc and remove in next major release
}
type responseStructure struct {
Configs []*configData `json:"configs"`
}
func NewHTTP(endpoint string, privateKey crypto.PrivateKey) Service {
return &http{endpoint, privateKey}
}
func (h *http) Fetch(ctx context.Context, forge forge.Forge, user *model.User, repo *model.Repo, pipeline *model.Pipeline, oldConfigData []*types.FileMeta, _ bool) ([]*types.FileMeta, error) {
currentConfigs := make([]*configData, len(oldConfigData))
for i, pipe := range oldConfigData {
currentConfigs[i] = &configData{Name: pipe.Name, Data: string(pipe.Data)}
}
netrc, err := forge.Netrc(user, repo)
if err != nil {
return nil, fmt.Errorf("could not get Netrc data from forge: %w", err)
}
response := new(responseStructure)
body := requestStructure{
Repo: repo,
Pipeline: pipeline,
Configuration: currentConfigs,
Netrc: netrc,
}
status, err := utils.Send(ctx, "POST", h.endpoint, h.privateKey, body, response)
if err != nil && status != 204 {
return nil, fmt.Errorf("failed to fetch config via http (%d) %w", status, err)
}
if status != 200 {
return oldConfigData, nil
}
fileMetas := make([]*types.FileMeta, len(response.Configs))
for i, config := range response.Configs {
fileMetas[i] = &types.FileMeta{Name: config.Name, Data: []byte(config.Data)}
}
return fileMetas, nil
}

View file

@ -1,4 +1,4 @@
// Copyright 2022 Woodpecker Authors // Copyright 2024 Woodpecker Authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -17,10 +17,11 @@ package config
import ( import (
"context" "context"
forge_types "go.woodpecker-ci.org/woodpecker/v2/server/forge/types" "go.woodpecker-ci.org/woodpecker/v2/server/forge"
"go.woodpecker-ci.org/woodpecker/v2/server/forge/types"
"go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/model"
) )
type Extension interface { type Service interface {
FetchConfig(ctx context.Context, repo *model.Repo, pipeline *model.Pipeline, currentFileMeta []*forge_types.FileMeta, netrc *model.Netrc) (configData []*forge_types.FileMeta, useOld bool, err error) Fetch(ctx context.Context, forge forge.Forge, user *model.User, repo *model.Repo, pipeline *model.Pipeline, oldConfigData []*types.FileMeta, restart bool) (configData []*types.FileMeta, err error)
} }

View file

@ -21,7 +21,7 @@ import (
"github.com/google/tink/go/subtle/random" "github.com/google/tink/go/subtle/random"
"go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/services/encryption/types"
"go.woodpecker-ci.org/woodpecker/v2/server/store" "go.woodpecker-ci.org/woodpecker/v2/server/store"
) )
@ -29,7 +29,7 @@ type aesEncryptionService struct {
cipher cipher.AEAD cipher cipher.AEAD
keyID string keyID string
store store.Store store store.Store
clients []model.EncryptionClient clients []types.EncryptionClient
} }
func (svc *aesEncryptionService) Encrypt(plaintext, associatedData string) (string, error) { func (svc *aesEncryptionService) Encrypt(plaintext, associatedData string) (string, error) {

View file

@ -20,27 +20,27 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/services/encryption/types"
"go.woodpecker-ci.org/woodpecker/v2/server/store" "go.woodpecker-ci.org/woodpecker/v2/server/store"
) )
type aesConfiguration struct { type aesConfiguration struct {
password string password string
store store.Store store store.Store
clients []model.EncryptionClient clients []types.EncryptionClient
} }
func newAES(ctx *cli.Context, s store.Store) model.EncryptionServiceBuilder { func newAES(ctx *cli.Context, s store.Store) types.EncryptionServiceBuilder {
key := ctx.String(rawKeyConfigFlag) key := ctx.String(rawKeyConfigFlag)
return &aesConfiguration{key, s, nil} return &aesConfiguration{key, s, nil}
} }
func (c aesConfiguration) WithClients(clients []model.EncryptionClient) model.EncryptionServiceBuilder { func (c aesConfiguration) WithClients(clients []types.EncryptionClient) types.EncryptionServiceBuilder {
c.clients = clients c.clients = clients
return c return c
} }
func (c aesConfiguration) Build() (model.EncryptionService, error) { func (c aesConfiguration) Build() (types.EncryptionService, error) {
svc := &aesEncryptionService{ svc := &aesEncryptionService{
cipher: nil, cipher: nil,
store: c.store, store: c.store,

View file

@ -64,9 +64,9 @@ const (
logMessageEncryptionDisabled = "encryption disabled" logMessageEncryptionDisabled = "encryption disabled"
logMessageEncryptionKeyRegistered = "registered new encryption key" logMessageEncryptionKeyRegistered = "registered new encryption key"
logMessageClientsInitialized = "initialized encryption on registered clients" logMessageClientsInitialized = "initialized encryption on registered clients"
logMessageClientsEnabled = "enabled encryption on registered services" logMessageClientsEnabled = "enabled encryption on registered service"
logMessageClientsRotated = "updated encryption key on registered services" logMessageClientsRotated = "updated encryption key on registered service"
logMessageClientsDecrypted = "disabled encryption on registered services" logMessageClientsDecrypted = "disabled encryption on registered service"
) )
// tink // tink

View file

@ -19,21 +19,21 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/services/encryption/types"
"go.woodpecker-ci.org/woodpecker/v2/server/store" "go.woodpecker-ci.org/woodpecker/v2/server/store"
) )
type builder struct { type builder struct {
store store.Store store store.Store
ctx *cli.Context ctx *cli.Context
clients []model.EncryptionClient clients []types.EncryptionClient
} }
func Encryption(ctx *cli.Context, s store.Store) model.EncryptionBuilder { func Encryption(ctx *cli.Context, s store.Store) types.EncryptionBuilder {
return &builder{store: s, ctx: ctx} return &builder{store: s, ctx: ctx}
} }
func (b builder) WithClient(client model.EncryptionClient) model.EncryptionBuilder { func (b builder) WithClient(client types.EncryptionClient) types.EncryptionBuilder {
b.clients = append(b.clients, client) b.clients = append(b.clients, client)
return b return b
} }

View file

@ -18,11 +18,11 @@ import (
"errors" "errors"
"fmt" "fmt"
"go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/services/encryption/types"
"go.woodpecker-ci.org/woodpecker/v2/server/store/types" storeTypes "go.woodpecker-ci.org/woodpecker/v2/server/store/types"
) )
func (b builder) getService(keyType string) (model.EncryptionService, error) { func (b builder) getService(keyType string) (types.EncryptionService, error) {
if keyType == keyTypeNone { if keyType == keyTypeNone {
return nil, errors.New(errMessageNoKeysProvided) return nil, errors.New(errMessageNoKeysProvided)
} }
@ -41,7 +41,7 @@ func (b builder) getService(keyType string) (model.EncryptionService, error) {
func (b builder) isEnabled() (bool, error) { func (b builder) isEnabled() (bool, error) {
_, err := b.store.ServerConfigGet(ciphertextSampleConfigKey) _, err := b.store.ServerConfigGet(ciphertextSampleConfigKey)
if err != nil && !errors.Is(err, types.RecordNotExist) { if err != nil && !errors.Is(err, storeTypes.RecordNotExist) {
return false, fmt.Errorf(errTemplateFailedLoadingServerConfig, err) return false, fmt.Errorf(errTemplateFailedLoadingServerConfig, err)
} }
return err == nil, nil return err == nil, nil
@ -61,7 +61,7 @@ func (b builder) detectKeyType() (string, error) {
return keyTypeNone, nil return keyTypeNone, nil
} }
func (b builder) serviceBuilder(keyType string) (model.EncryptionServiceBuilder, error) { func (b builder) serviceBuilder(keyType string) (types.EncryptionServiceBuilder, error) {
switch { switch {
case keyType == keyTypeTink: case keyType == keyTypeTink:
return newTink(b.ctx, b.store), nil return newTink(b.ctx, b.store), nil

View file

@ -14,18 +14,18 @@
package encryption package encryption
import "go.woodpecker-ci.org/woodpecker/v2/server/model" import "go.woodpecker-ci.org/woodpecker/v2/server/services/encryption/types"
type noEncryptionBuilder struct { type noEncryptionBuilder struct {
clients []model.EncryptionClient clients []types.EncryptionClient
} }
func (b noEncryptionBuilder) WithClients(clients []model.EncryptionClient) model.EncryptionServiceBuilder { func (b noEncryptionBuilder) WithClients(clients []types.EncryptionClient) types.EncryptionServiceBuilder {
b.clients = clients b.clients = clients
return b return b
} }
func (b noEncryptionBuilder) Build() (model.EncryptionService, error) { func (b noEncryptionBuilder) Build() (types.EncryptionService, error) {
svc := &noEncryption{} svc := &noEncryption{}
for _, client := range b.clients { for _, client := range b.clients {
err := client.SetEncryptionService(svc) err := client.SetEncryptionService(svc)

View file

@ -21,7 +21,7 @@ import (
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
"github.com/google/tink/go/tink" "github.com/google/tink/go/tink"
"go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/services/encryption/types"
"go.woodpecker-ci.org/woodpecker/v2/server/store" "go.woodpecker-ci.org/woodpecker/v2/server/store"
) )
@ -31,7 +31,7 @@ type tinkEncryptionService struct {
encryption tink.AEAD encryption tink.AEAD
store store.Store store store.Store
keysetFileWatcher *fsnotify.Watcher keysetFileWatcher *fsnotify.Watcher
clients []model.EncryptionClient clients []types.EncryptionClient
} }
func (svc *tinkEncryptionService) Encrypt(plaintext, associatedData string) (string, error) { func (svc *tinkEncryptionService) Encrypt(plaintext, associatedData string) (string, error) {

View file

@ -20,27 +20,27 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/services/encryption/types"
"go.woodpecker-ci.org/woodpecker/v2/server/store" "go.woodpecker-ci.org/woodpecker/v2/server/store"
) )
type tinkConfiguration struct { type tinkConfiguration struct {
keysetFilePath string keysetFilePath string
store store.Store store store.Store
clients []model.EncryptionClient clients []types.EncryptionClient
} }
func newTink(ctx *cli.Context, s store.Store) model.EncryptionServiceBuilder { func newTink(ctx *cli.Context, s store.Store) types.EncryptionServiceBuilder {
filepath := ctx.String(tinkKeysetFilepathConfigFlag) filepath := ctx.String(tinkKeysetFilepathConfigFlag)
return &tinkConfiguration{filepath, s, nil} return &tinkConfiguration{filepath, s, nil}
} }
func (c tinkConfiguration) WithClients(clients []model.EncryptionClient) model.EncryptionServiceBuilder { func (c tinkConfiguration) WithClients(clients []types.EncryptionClient) types.EncryptionServiceBuilder {
c.clients = clients c.clients = clients
return c return c
} }
func (c tinkConfiguration) Build() (model.EncryptionService, error) { func (c tinkConfiguration) Build() (types.EncryptionService, error) {
svc := &tinkEncryptionService{ svc := &tinkEncryptionService{
keysetFilePath: c.keysetFilePath, keysetFilePath: c.keysetFilePath,
primaryKeyID: "", primaryKeyID: "",

View file

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package model package types
// EncryptionBuilder is user API to obtain correctly configured encryption // EncryptionBuilder is user API to obtain correctly configured encryption
type EncryptionBuilder interface { type EncryptionBuilder interface {

View file

@ -22,11 +22,12 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/model"
"go.woodpecker-ci.org/woodpecker/v2/server/services/encryption/types"
) )
type EncryptedSecretStore struct { type EncryptedSecretStore struct {
store model.SecretStore store model.SecretStore
encryption model.EncryptionService encryption types.EncryptionService
} }
// ensure wrapper match interface // ensure wrapper match interface
@ -37,7 +38,7 @@ func NewSecretStore(secretStore model.SecretStore) *EncryptedSecretStore {
return &wrapper return &wrapper
} }
func (wrapper *EncryptedSecretStore) SetEncryptionService(service model.EncryptionService) error { func (wrapper *EncryptedSecretStore) SetEncryptionService(service types.EncryptionService) error {
if wrapper.encryption != nil { if wrapper.encryption != nil {
return errors.New(errMessageInitSeveralTimes) return errors.New(errMessageInitSeveralTimes)
} }
@ -63,7 +64,7 @@ func (wrapper *EncryptedSecretStore) EnableEncryption() error {
return nil return nil
} }
func (wrapper *EncryptedSecretStore) MigrateEncryption(newEncryptionService model.EncryptionService) error { func (wrapper *EncryptedSecretStore) MigrateEncryption(newEncryptionService types.EncryptionService) error {
log.Warn().Msg(logMessageMigratingSecretsEncryption) log.Warn().Msg(logMessageMigratingSecretsEncryption)
secrets, err := wrapper.store.SecretListAll() secrets, err := wrapper.store.SecretListAll()
if err != nil { if err != nil {

View file

@ -0,0 +1,22 @@
// 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 environment
import "go.woodpecker-ci.org/woodpecker/v2/server/model"
// Service defines a service for managing environment variables.
type Service interface {
EnvironList(*model.Repo) ([]*model.Environ, error)
}

View file

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package environments package environment
import ( import (
"strings" "strings"
@ -26,8 +26,8 @@ type builtin struct {
globals []*model.Environ globals []*model.Environ
} }
// Parse returns a model.EnvironService based on a string slice where key and value are separated by a ":" delimiter. // Parse returns a Service based on a string slice where key and value are separated by a ":" delimiter.
func Parse(params []string) model.EnvironService { func Parse(params []string) Service {
var globals []*model.Environ var globals []*model.Environ
for _, item := range params { for _, item := range params {

View file

@ -1,4 +1,18 @@
package environments // 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 environment
import ( import (
"testing" "testing"

View file

@ -0,0 +1,82 @@
// 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 services
import (
"crypto"
"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/environment"
"go.woodpecker-ci.org/woodpecker/v2/server/services/registry"
"go.woodpecker-ci.org/woodpecker/v2/server/services/secret"
"go.woodpecker-ci.org/woodpecker/v2/server/store"
)
type Manager struct {
secret secret.Service
registry registry.Service
config config.Service
environment environment.Service
signaturePrivateKey crypto.PrivateKey
signaturePublicKey crypto.PublicKey
}
func NewManager(c *cli.Context, store store.Store) (*Manager, error) {
signaturePrivateKey, signaturePublicKey, err := setupSignatureKeys(store)
if err != nil {
return nil, err
}
return &Manager{
signaturePrivateKey: signaturePrivateKey,
signaturePublicKey: signaturePublicKey,
secret: setupSecretService(store),
registry: setupRegistryService(store, c.String("docker-config")),
config: setupConfigService(c, signaturePrivateKey),
environment: environment.Parse(c.StringSlice("environment")),
}, nil
}
func (e *Manager) SignaturePublicKey() crypto.PublicKey {
return e.signaturePublicKey
}
func (e *Manager) SecretServiceFromRepo(_ *model.Repo) secret.Service {
return e.SecretService()
}
func (e *Manager) SecretService() secret.Service {
return e.secret
}
func (e *Manager) RegistryServiceFromRepo(_ *model.Repo) registry.Service {
return e.RegistryService()
}
func (e *Manager) RegistryService() registry.Service {
return e.registry
}
func (e *Manager) ConfigServiceFromRepo(_ *model.Repo) config.Service {
// TODO: decied based on repo property which config service to use
return e.config
}
func (e *Manager) EnvironmentService() environment.Service {
return e.environment
}

View file

@ -19,11 +19,11 @@ import (
) )
type combined struct { type combined struct {
registries []model.ReadOnlyRegistryService registries []ReadOnlyService
dbRegistry model.RegistryService dbRegistry Service
} }
func Combined(dbRegistry model.RegistryService, registries ...model.ReadOnlyRegistryService) model.RegistryService { func NewCombined(dbRegistry Service, registries ...ReadOnlyService) Service {
registries = append(registries, dbRegistry) registries = append(registries, dbRegistry)
return &combined{ return &combined{
registries: registries, registries: registries,
@ -31,7 +31,7 @@ func Combined(dbRegistry model.RegistryService, registries ...model.ReadOnlyRegi
} }
} }
func (c combined) RegistryFind(repo *model.Repo, name string) (*model.Registry, error) { func (c *combined) RegistryFind(repo *model.Repo, name string) (*model.Registry, error) {
for _, registry := range c.registries { for _, registry := range c.registries {
res, err := registry.RegistryFind(repo, name) res, err := registry.RegistryFind(repo, name)
if err != nil { if err != nil {
@ -44,7 +44,7 @@ func (c combined) RegistryFind(repo *model.Repo, name string) (*model.Registry,
return nil, nil return nil, nil
} }
func (c combined) RegistryList(repo *model.Repo, p *model.ListOptions) ([]*model.Registry, error) { func (c *combined) RegistryList(repo *model.Repo, p *model.ListOptions) ([]*model.Registry, error) {
var registries []*model.Registry var registries []*model.Registry
for _, registry := range c.registries { for _, registry := range c.registries {
list, err := registry.RegistryList(repo, &model.ListOptions{All: true}) list, err := registry.RegistryList(repo, &model.ListOptions{All: true})
@ -56,14 +56,14 @@ func (c combined) RegistryList(repo *model.Repo, p *model.ListOptions) ([]*model
return model.ApplyPagination(p, registries), nil return model.ApplyPagination(p, registries), nil
} }
func (c combined) RegistryCreate(repo *model.Repo, registry *model.Registry) error { func (c *combined) RegistryCreate(repo *model.Repo, registry *model.Registry) error {
return c.dbRegistry.RegistryCreate(repo, registry) return c.dbRegistry.RegistryCreate(repo, registry)
} }
func (c combined) RegistryUpdate(repo *model.Repo, registry *model.Registry) error { func (c *combined) RegistryUpdate(repo *model.Repo, registry *model.Registry) error {
return c.dbRegistry.RegistryUpdate(repo, registry) return c.dbRegistry.RegistryUpdate(repo, registry)
} }
func (c combined) RegistryDelete(repo *model.Repo, name string) error { func (c *combined) RegistryDelete(repo *model.Repo, name string) error {
return c.dbRegistry.RegistryDelete(repo, name) return c.dbRegistry.RegistryDelete(repo, name)
} }

View file

@ -23,26 +23,26 @@ type db struct {
} }
// New returns a new local registry service. // New returns a new local registry service.
func New(store model.RegistryStore) model.RegistryService { func NewDB(store model.RegistryStore) Service {
return &db{store} return &db{store}
} }
func (b *db) RegistryFind(repo *model.Repo, name string) (*model.Registry, error) { func (d *db) RegistryFind(repo *model.Repo, name string) (*model.Registry, error) {
return b.store.RegistryFind(repo, name) return d.store.RegistryFind(repo, name)
} }
func (b *db) RegistryList(repo *model.Repo, p *model.ListOptions) ([]*model.Registry, error) { func (d *db) RegistryList(repo *model.Repo, p *model.ListOptions) ([]*model.Registry, error) {
return b.store.RegistryList(repo, p) return d.store.RegistryList(repo, p)
} }
func (b *db) RegistryCreate(_ *model.Repo, in *model.Registry) error { func (d *db) RegistryCreate(_ *model.Repo, in *model.Registry) error {
return b.store.RegistryCreate(in) return d.store.RegistryCreate(in)
} }
func (b *db) RegistryUpdate(_ *model.Repo, in *model.Registry) error { func (d *db) RegistryUpdate(_ *model.Repo, in *model.Registry) error {
return b.store.RegistryUpdate(in) return d.store.RegistryUpdate(in)
} }
func (b *db) RegistryDelete(repo *model.Repo, addr string) error { func (d *db) RegistryDelete(repo *model.Repo, addr string) error {
return b.store.RegistryDelete(repo, addr) return d.store.RegistryDelete(repo, addr)
} }

View file

@ -31,7 +31,7 @@ type filesystem struct {
path string path string
} }
func Filesystem(path string) model.ReadOnlyRegistryService { func NewFilesystem(path string) ReadOnlyService {
return &filesystem{path} return &filesystem{path}
} }
@ -85,12 +85,12 @@ func parseDockerConfig(path string) ([]*model.Registry, error) {
return auths, nil return auths, nil
} }
func (b *filesystem) RegistryFind(*model.Repo, string) (*model.Registry, error) { func (f *filesystem) RegistryFind(*model.Repo, string) (*model.Registry, error) {
return nil, nil return nil, nil
} }
func (b *filesystem) RegistryList(_ *model.Repo, p *model.ListOptions) ([]*model.Registry, error) { func (f *filesystem) RegistryList(_ *model.Repo, p *model.ListOptions) ([]*model.Registry, error) {
regs, err := parseDockerConfig(b.path) regs, err := parseDockerConfig(f.path)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -0,0 +1,32 @@
// 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 registry
import "go.woodpecker-ci.org/woodpecker/v2/server/model"
// Service defines a service for managing registries.
type Service interface {
RegistryFind(*model.Repo, string) (*model.Registry, error)
RegistryList(*model.Repo, *model.ListOptions) ([]*model.Registry, error)
RegistryCreate(*model.Repo, *model.Registry) error
RegistryUpdate(*model.Repo, *model.Registry) error
RegistryDelete(*model.Repo, string) error
}
// ReadOnlyService defines a service for managing registries.
type ReadOnlyService interface {
RegistryFind(*model.Repo, string) (*model.Registry, error)
RegistryList(*model.Repo, *model.ListOptions) ([]*model.Registry, error)
}

View file

@ -0,0 +1,133 @@
// 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 secret
import (
"go.woodpecker-ci.org/woodpecker/v2/server/model"
)
type db struct {
store model.SecretStore
}
// NewDB returns a new local secret service.
func NewDB(store model.SecretStore) Service {
return &db{store: store}
}
func (d *db) SecretFind(repo *model.Repo, name string) (*model.Secret, error) {
return d.store.SecretFind(repo, name)
}
func (d *db) SecretList(repo *model.Repo, p *model.ListOptions) ([]*model.Secret, error) {
return d.store.SecretList(repo, false, p)
}
func (d *db) SecretListPipeline(repo *model.Repo, _ *model.Pipeline, p *model.ListOptions) ([]*model.Secret, error) {
s, err := d.store.SecretList(repo, true, p)
if err != nil {
return nil, err
}
// Return only secrets with unique name
// Priority order in case of duplicate names are repository, user/organization, global
secrets := make([]*model.Secret, 0, len(s))
uniq := make(map[string]struct{})
for _, condition := range []struct {
IsRepository bool
IsOrganization bool
IsGlobal bool
}{
{IsRepository: true},
{IsOrganization: true},
{IsGlobal: true},
} {
for _, secret := range s {
if secret.IsRepository() != condition.IsRepository || secret.IsOrganization() != condition.IsOrganization || secret.IsGlobal() != condition.IsGlobal {
continue
}
if _, ok := uniq[secret.Name]; ok {
continue
}
uniq[secret.Name] = struct{}{}
secrets = append(secrets, secret)
}
}
return secrets, nil
}
func (d *db) SecretCreate(_ *model.Repo, in *model.Secret) error {
return d.store.SecretCreate(in)
}
func (d *db) SecretUpdate(_ *model.Repo, in *model.Secret) error {
return d.store.SecretUpdate(in)
}
func (d *db) SecretDelete(repo *model.Repo, name string) error {
secret, err := d.store.SecretFind(repo, name)
if err != nil {
return err
}
return d.store.SecretDelete(secret)
}
func (d *db) OrgSecretFind(owner int64, name string) (*model.Secret, error) {
return d.store.OrgSecretFind(owner, name)
}
func (d *db) OrgSecretList(owner int64, p *model.ListOptions) ([]*model.Secret, error) {
return d.store.OrgSecretList(owner, p)
}
func (d *db) OrgSecretCreate(_ int64, in *model.Secret) error {
return d.store.SecretCreate(in)
}
func (d *db) OrgSecretUpdate(_ int64, in *model.Secret) error {
return d.store.SecretUpdate(in)
}
func (d *db) OrgSecretDelete(owner int64, name string) error {
secret, err := d.store.OrgSecretFind(owner, name)
if err != nil {
return err
}
return d.store.SecretDelete(secret)
}
func (d *db) GlobalSecretFind(owner string) (*model.Secret, error) {
return d.store.GlobalSecretFind(owner)
}
func (d *db) GlobalSecretList(p *model.ListOptions) ([]*model.Secret, error) {
return d.store.GlobalSecretList(p)
}
func (d *db) GlobalSecretCreate(in *model.Secret) error {
return d.store.SecretCreate(in)
}
func (d *db) GlobalSecretUpdate(in *model.Secret) error {
return d.store.SecretUpdate(in)
}
func (d *db) GlobalSecretDelete(name string) error {
secret, err := d.store.GlobalSecretFind(name)
if err != nil {
return err
}
return d.store.SecretDelete(secret)
}

View file

@ -12,23 +12,21 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package secrets_test package secret_test
import ( import (
"context"
"testing" "testing"
"github.com/franela/goblin" "github.com/franela/goblin"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/model"
"go.woodpecker-ci.org/woodpecker/v2/server/plugins/secrets" "go.woodpecker-ci.org/woodpecker/v2/server/services/secret"
mocks_store "go.woodpecker-ci.org/woodpecker/v2/server/store/mocks" mocks_store "go.woodpecker-ci.org/woodpecker/v2/server/store/mocks"
) )
func TestSecretListPipeline(t *testing.T) { func TestSecretListPipeline(t *testing.T) {
g := goblin.Goblin(t) g := goblin.Goblin(t)
ctx := context.Background()
mockStore := mocks_store.NewStore(t) mockStore := mocks_store.NewStore(t)
// global secret // global secret
@ -66,7 +64,7 @@ func TestSecretListPipeline(t *testing.T) {
repoSecret, repoSecret,
}, nil) }, nil)
s, err := secrets.New(ctx, mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{}, &model.ListOptions{}) s, err := secret.NewDB(mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{}, &model.ListOptions{})
g.Assert(err).IsNil() g.Assert(err).IsNil()
g.Assert(len(s)).Equal(1) g.Assert(len(s)).Equal(1)
@ -79,7 +77,7 @@ func TestSecretListPipeline(t *testing.T) {
orgSecret, orgSecret,
}, nil) }, nil)
s, err := secrets.New(ctx, mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{}, &model.ListOptions{}) s, err := secret.NewDB(mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{}, &model.ListOptions{})
g.Assert(err).IsNil() g.Assert(err).IsNil()
g.Assert(len(s)).Equal(1) g.Assert(len(s)).Equal(1)
@ -91,7 +89,7 @@ func TestSecretListPipeline(t *testing.T) {
globalSecret, globalSecret,
}, nil) }, nil)
s, err := secrets.New(ctx, mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{}, &model.ListOptions{}) s, err := secret.NewDB(mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{}, &model.ListOptions{})
g.Assert(err).IsNil() g.Assert(err).IsNil()
g.Assert(len(s)).Equal(1) g.Assert(len(s)).Equal(1)

View file

@ -0,0 +1,40 @@
// 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 secret
import "go.woodpecker-ci.org/woodpecker/v2/server/model"
// Service defines a service for managing secrets.
type Service interface {
SecretListPipeline(*model.Repo, *model.Pipeline, *model.ListOptions) ([]*model.Secret, error)
// Repository secrets
SecretFind(*model.Repo, string) (*model.Secret, error)
SecretList(*model.Repo, *model.ListOptions) ([]*model.Secret, error)
SecretCreate(*model.Repo, *model.Secret) error
SecretUpdate(*model.Repo, *model.Secret) error
SecretDelete(*model.Repo, string) error
// Organization secrets
OrgSecretFind(int64, string) (*model.Secret, error)
OrgSecretList(int64, *model.ListOptions) ([]*model.Secret, error)
OrgSecretCreate(int64, *model.Secret) error
OrgSecretUpdate(int64, *model.Secret) error
OrgSecretDelete(int64, string) error
// Global secrets
GlobalSecretFind(string) (*model.Secret, error)
GlobalSecretList(*model.ListOptions) ([]*model.Secret, error)
GlobalSecretCreate(*model.Secret) error
GlobalSecretUpdate(*model.Secret) error
GlobalSecretDelete(string) error
}

95
server/services/setup.go Normal file
View file

@ -0,0 +1,95 @@
// 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 services
import (
"crypto"
"crypto/ed25519"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
"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"
"go.woodpecker-ci.org/woodpecker/v2/server/store"
"go.woodpecker-ci.org/woodpecker/v2/server/store/types"
)
func setupRegistryService(store store.Store, dockerConfig string) registry.Service {
if dockerConfig != "" {
return registry.NewCombined(
registry.NewDB(store),
registry.NewFilesystem(dockerConfig),
)
}
return registry.NewDB(store)
}
func setupSecretService(store store.Store) secret.Service {
// TODO(1544): fix encrypted store
// // encryption
// encryptedSecretStore := encryptedStore.NewSecretStore(v)
// err := encryption.Encryption(c, v).WithClient(encryptedSecretStore).Build()
// if err != nil {
// log.Fatal().Err(err).Msg("could not create encryption service")
// }
return secret.NewDB(store)
}
func setupConfigService(c *cli.Context, privateSignatureKey crypto.PrivateKey) config.Service {
timeout := c.Duration("forge-timeout")
configFetcher := config.NewForge(timeout)
if endpoint := c.String("config-extension-endpoint"); endpoint != "" {
httpFetcher := config.NewHTTP(endpoint, privateSignatureKey)
return config.NewCombined(configFetcher, httpFetcher)
}
return configFetcher
}
// setupSignatureKeys generate or load key pair to sign webhooks requests (i.e. used for service extensions)
func setupSignatureKeys(_store store.Store) (crypto.PrivateKey, crypto.PublicKey, error) {
privKeyID := "signature-private-key"
privKey, err := _store.ServerConfigGet(privKeyID)
if errors.Is(err, types.RecordNotExist) {
_, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate private key: %w", err)
}
err = _store.ServerConfigSet(privKeyID, hex.EncodeToString(privKey))
if err != nil {
return nil, nil, fmt.Errorf("failed to store private key: %w", err)
}
log.Debug().Msg("created private key")
return privKey, privKey.Public(), nil
} else if err != nil {
return nil, nil, fmt.Errorf("failed to load private key: %w", err)
}
privKeyStr, err := hex.DecodeString(privKey)
if err != nil {
return nil, nil, fmt.Errorf("failed to decode private key: %w", err)
}
privateKey := ed25519.PrivateKey(privKeyStr)
return privateKey, privateKey.Public(), nil
}

View file

@ -25,7 +25,7 @@ import (
"github.com/go-ap/httpsig" "github.com/go-ap/httpsig"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"go.woodpecker-ci.org/woodpecker/v2/server/plugins/utils" "go.woodpecker-ci.org/woodpecker/v2/server/services/utils"
) )
func TestSign(t *testing.T) { func TestSign(t *testing.T) {

View file

@ -3,9 +3,5 @@ package types
type Type string type Type string
const ( const (
TypeForge Type = "forge" TypeForge Type = "forge"
TypeConfigService Type = "config_service"
TypeSecretService Type = "secret_service"
TypeEnvironmentService Type = "environment_service"
TypeRegistryService Type = "registry_service"
) )