mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2025-01-03 06:08:42 +00:00
Refactor internal services (#915)
This commit is contained in:
parent
e1521ef460
commit
82e1ce937c
66 changed files with 1163 additions and 993 deletions
|
@ -42,12 +42,11 @@ import (
|
|||
woodpeckerGrpcServer "go.woodpecker-ci.org/woodpecker/v2/server/grpc"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/logging"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
||||
// "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/router"
|
||||
"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/web"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
|
||||
|
@ -271,48 +270,21 @@ func run(c *cli.Context) error {
|
|||
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
|
||||
server.Config.Services.Forge = f
|
||||
server.Config.Services.Timeout = c.Duration("forge-timeout")
|
||||
|
||||
// services
|
||||
server.Config.Services.Queue = setupQueue(c, v)
|
||||
server.Config.Services.Queue = setupQueue(c, s)
|
||||
server.Config.Services.Logs = logging.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.SignaturePrivateKey, server.Config.Services.SignaturePublicKey, err = setupSignatureKeys(v)
|
||||
serviceMangager, err := services.NewManager(c, s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
server.Config.Services.ConfigService, err = setupConfigService(c)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("could not setup service manager: %w", err)
|
||||
}
|
||||
server.Config.Services.Manager = serviceMangager
|
||||
|
||||
// authentication
|
||||
server.Config.Pipeline.AuthenticatePublicRepos = c.Bool("authenticate-public-repos")
|
||||
|
|
|
@ -17,11 +17,6 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
|
@ -41,15 +36,9 @@ import (
|
|||
"go.woodpecker-ci.org/woodpecker/v2/server/forge/gitea"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge/github"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge/gitlab"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
||||
"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/store"
|
||||
"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"
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
- **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.
|
||||
- **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
|
||||
|
||||
|
|
|
@ -13,10 +13,6 @@ To adapt Woodpecker to your needs beyond the [configuration](../10-server-config
|
|||
Addons can be used for:
|
||||
|
||||
- Forges
|
||||
- Config services
|
||||
- Secret services
|
||||
- Environment services
|
||||
- Registry services
|
||||
|
||||
## Restrictions
|
||||
|
||||
|
|
|
@ -20,12 +20,8 @@ Directly import Woodpecker's Go package (`go.woodpecker-ci.org/woodpecker/woodpe
|
|||
### Return types
|
||||
|
||||
| Addon type | Return type |
|
||||
| -------------------- | ---------------------------------------------------------------------- |
|
||||
| ---------- | -------------------------------------------------------------------- |
|
||||
| `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
|
||||
|
||||
|
|
|
@ -35,7 +35,8 @@ import (
|
|||
// @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)
|
||||
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 {
|
||||
c.String(http.StatusInternalServerError, "Error getting global secret list. %s", err)
|
||||
return
|
||||
|
@ -59,7 +60,8 @@ func GetGlobalSecretList(c *gin.Context) {
|
|||
// @Param secret path string true "the secret's name"
|
||||
func GetGlobalSecret(c *gin.Context) {
|
||||
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 {
|
||||
handleDBError(c, err)
|
||||
return
|
||||
|
@ -92,7 +94,9 @@ func PostGlobalSecret(c *gin.Context) {
|
|||
c.String(http.StatusBadRequest, "Error inserting global secret. %s", err)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
@ -119,7 +123,8 @@ func PatchGlobalSecret(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
secret, err := server.Config.Services.Secrets.GlobalSecretFind(name)
|
||||
secretService := server.Config.Services.Manager.SecretService()
|
||||
secret, err := secretService.GlobalSecretFind(name)
|
||||
if err != nil {
|
||||
handleDBError(c, err)
|
||||
return
|
||||
|
@ -138,7 +143,8 @@ func PatchGlobalSecret(c *gin.Context) {
|
|||
c.String(http.StatusBadRequest, "Error updating global secret. %s", err)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
@ -156,7 +162,8 @@ func PatchGlobalSecret(c *gin.Context) {
|
|||
// @Param secret path string true "the secret's name"
|
||||
func DeleteGlobalSecret(c *gin.Context) {
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -44,7 +44,8 @@ func GetOrgSecret(c *gin.Context) {
|
|||
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 {
|
||||
handleDBError(c, err)
|
||||
return
|
||||
|
@ -70,7 +71,8 @@ func GetOrgSecretList(c *gin.Context) {
|
|||
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 {
|
||||
c.String(http.StatusInternalServerError, "Error getting secret list for %q. %s", orgID, err)
|
||||
return
|
||||
|
@ -116,7 +118,9 @@ func PostOrgSecret(c *gin.Context) {
|
|||
c.String(http.StatusUnprocessableEntity, "Error inserting org %q secret. %s", orgID, err)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
@ -149,7 +153,8 @@ func PatchOrgSecret(c *gin.Context) {
|
|||
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 {
|
||||
handleDBError(c, err)
|
||||
return
|
||||
|
@ -168,7 +173,8 @@ func PatchOrgSecret(c *gin.Context) {
|
|||
c.String(http.StatusUnprocessableEntity, "Error updating org %q secret. %s", orgID, err)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
@ -193,7 +199,8 @@ func DeleteOrgSecret(c *gin.Context) {
|
|||
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)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -434,13 +434,7 @@ func PostPipeline(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
netrc, err := server.Config.Services.Forge.Netrc(user, repo)
|
||||
if err != nil {
|
||||
handlePipelineErr(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
newpipeline, err := pipeline.Restart(c, _store, pl, user, repo, envs, netrc)
|
||||
newpipeline, err := pipeline.Restart(c, _store, pl, user, repo, envs)
|
||||
if err != nil {
|
||||
handlePipelineErr(c, err)
|
||||
} else {
|
||||
|
|
|
@ -35,11 +35,11 @@ import (
|
|||
// @Param repo_id path int true "the repository id"
|
||||
// @Param registry path string true "the registry name"
|
||||
func GetRegistry(c *gin.Context) {
|
||||
var (
|
||||
repo = session.Repo(c)
|
||||
name = c.Param("registry")
|
||||
)
|
||||
registry, err := server.Config.Services.Registries.RegistryFind(repo, name)
|
||||
repo := session.Repo(c)
|
||||
name := c.Param("registry")
|
||||
|
||||
registryService := server.Config.Services.Manager.RegistryServiceFromRepo(repo)
|
||||
registry, err := registryService.RegistryFind(repo, name)
|
||||
if err != nil {
|
||||
handleDBError(c, err)
|
||||
return
|
||||
|
@ -75,7 +75,9 @@ func PostRegistry(c *gin.Context) {
|
|||
c.String(http.StatusBadRequest, "Error inserting registry. %s", err)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
@ -106,7 +108,8 @@ func PatchRegistry(c *gin.Context) {
|
|||
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 {
|
||||
handleDBError(c, err)
|
||||
return
|
||||
|
@ -122,7 +125,7 @@ func PatchRegistry(c *gin.Context) {
|
|||
c.String(http.StatusUnprocessableEntity, "Error updating registry. %s", err)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
@ -142,7 +145,8 @@ func PatchRegistry(c *gin.Context) {
|
|||
// @Param perPage query int false "for response pagination, max items per page" default(50)
|
||||
func GetRegistryList(c *gin.Context) {
|
||||
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 {
|
||||
c.String(http.StatusInternalServerError, "Error getting registry list. %s", err)
|
||||
return
|
||||
|
@ -166,11 +170,11 @@ func GetRegistryList(c *gin.Context) {
|
|||
// @Param repo_id path int true "the repository id"
|
||||
// @Param registry path string true "the registry name"
|
||||
func DeleteRegistry(c *gin.Context) {
|
||||
var (
|
||||
repo = session.Repo(c)
|
||||
name = c.Param("registry")
|
||||
)
|
||||
err := server.Config.Services.Registries.RegistryDelete(repo, name)
|
||||
repo := session.Repo(c)
|
||||
name := c.Param("registry")
|
||||
|
||||
registryService := server.Config.Services.Manager.RegistryServiceFromRepo(repo)
|
||||
err := registryService.RegistryDelete(repo, name)
|
||||
if err != nil {
|
||||
handleDBError(c, err)
|
||||
return
|
||||
|
|
|
@ -36,11 +36,11 @@ import (
|
|||
// @Param repo_id path int true "the repository id"
|
||||
// @Param secretName path string true "the secret name"
|
||||
func GetSecret(c *gin.Context) {
|
||||
var (
|
||||
repo = session.Repo(c)
|
||||
name = c.Param("secret")
|
||||
)
|
||||
secret, err := server.Config.Services.Secrets.SecretFind(repo, name)
|
||||
repo := session.Repo(c)
|
||||
name := c.Param("secret")
|
||||
|
||||
secretService := server.Config.Services.Manager.SecretServiceFromRepo(repo)
|
||||
secret, err := secretService.SecretFind(repo, name)
|
||||
if err != nil {
|
||||
handleDBError(c, err)
|
||||
return
|
||||
|
@ -77,7 +77,9 @@ func PostSecret(c *gin.Context) {
|
|||
c.String(http.StatusUnprocessableEntity, "Error inserting secret. %s", err)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
@ -108,7 +110,8 @@ func PatchSecret(c *gin.Context) {
|
|||
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 {
|
||||
handleDBError(c, err)
|
||||
return
|
||||
|
@ -127,7 +130,7 @@ func PatchSecret(c *gin.Context) {
|
|||
c.String(http.StatusUnprocessableEntity, "Error updating secret. %s", err)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
@ -147,7 +150,8 @@ func PatchSecret(c *gin.Context) {
|
|||
// @Param perPage query int false "for response pagination, max items per page" default(50)
|
||||
func GetSecretList(c *gin.Context) {
|
||||
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 {
|
||||
c.String(http.StatusInternalServerError, "Error getting secret list. %s", err)
|
||||
return
|
||||
|
@ -171,11 +175,11 @@ func GetSecretList(c *gin.Context) {
|
|||
// @Param repo_id path int true "the repository id"
|
||||
// @Param secretName path string true "the secret name"
|
||||
func DeleteSecret(c *gin.Context) {
|
||||
var (
|
||||
repo = session.Repo(c)
|
||||
name = c.Param("secret")
|
||||
)
|
||||
if err := server.Config.Services.Secrets.SecretDelete(repo, name); err != nil {
|
||||
repo := session.Repo(c)
|
||||
name := c.Param("secret")
|
||||
|
||||
secretService := server.Config.Services.Manager.SecretServiceFromRepo(repo)
|
||||
if err := secretService.SecretDelete(repo, name); err != nil {
|
||||
handleDBError(c, err)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ import (
|
|||
// @Tags System
|
||||
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
|
||||
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 {
|
||||
log.Error().Err(err).Msg("can't marshal public key")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
|
|
|
@ -18,17 +18,16 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"time"
|
||||
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/cache"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/logging"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/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/queue"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/services"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/services/permissions"
|
||||
)
|
||||
|
||||
var Config = struct {
|
||||
|
@ -36,24 +35,9 @@ var Config = struct {
|
|||
Pubsub *pubsub.Publisher
|
||||
Queue queue.Queue
|
||||
Logs logging.Log
|
||||
Secrets model.SecretService
|
||||
Registries model.RegistryService
|
||||
Environ model.EnvironService
|
||||
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
|
||||
Manager *services.Manager
|
||||
}
|
||||
Server struct {
|
||||
Key string
|
||||
|
|
|
@ -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}
|
||||
}
|
|
@ -24,11 +24,6 @@ var (
|
|||
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.
|
||||
type EnvironStore interface {
|
||||
EnvironList(*Repo) ([]*Environ, error)
|
||||
|
|
|
@ -26,21 +26,6 @@ var (
|
|||
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.
|
||||
type RegistryStore interface {
|
||||
RegistryFind(*Repo, string) (*Registry, error)
|
||||
|
|
|
@ -29,29 +29,6 @@ var (
|
|||
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.
|
||||
type SecretStore interface {
|
||||
SecretFind(*Repo, string) (*Secret, error)
|
||||
|
|
|
@ -34,6 +34,7 @@ var skipPipelineRegex = regexp.MustCompile(`\[(?i:ci *skip|skip *ci)\]`)
|
|||
|
||||
// Create a new pipeline and start it
|
||||
func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline *model.Pipeline) (*model.Pipeline, error) {
|
||||
_forge := server.Config.Services.Forge
|
||||
repoUser, err := _store.GetUser(repo.UserID)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("failure to find repo owner via id '%d'", repo.UserID)
|
||||
|
@ -54,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
|
||||
// may be stale. Therefore, we should refresh prior to dispatching
|
||||
// the pipeline.
|
||||
forge.Refresh(ctx, server.Config.Services.Forge, _store, repoUser)
|
||||
forge.Refresh(ctx, _forge, _store, repoUser)
|
||||
|
||||
// update some pipeline fields
|
||||
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
|
||||
configFetcher := forge.NewConfigFetcher(server.Config.Services.Forge, server.Config.Services.Timeout, server.Config.Services.ConfigService, repoUser, repo, pipeline)
|
||||
forgeYamlConfigs, configFetchErr := configFetcher.Fetch(ctx)
|
||||
configService := server.Config.Services.Manager.ConfigServiceFromRepo(repo)
|
||||
forgeYamlConfigs, configFetchErr := configService.Fetch(ctx, _forge, repoUser, repo, pipeline, nil, false)
|
||||
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)
|
||||
if err := _store.DeletePipeline(pipeline); err != nil {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
envs[global.Name] = global.Value
|
||||
}
|
||||
|
|
|
@ -28,15 +28,14 @@ import (
|
|||
)
|
||||
|
||||
// Restart a pipeline by creating a new one out of the old and start it
|
||||
func Restart(ctx context.Context, store store.Store, lastPipeline *model.Pipeline, user *model.User, repo *model.Repo, envs map[string]string, 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 {
|
||||
case model.StatusDeclined,
|
||||
model.StatusBlocked:
|
||||
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
|
||||
configs, err := store.ConfigsForPipeline(lastPipeline.ID)
|
||||
if err != nil {
|
||||
|
@ -44,27 +43,19 @@ 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)}
|
||||
}
|
||||
|
||||
var pipelineFiles []*forge_types.FileMeta
|
||||
for _, y := range configs {
|
||||
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 server.Config.Services.ConfigService != nil {
|
||||
currentFileMeta := make([]*forge_types.FileMeta, len(configs))
|
||||
for i, cfg := range configs {
|
||||
currentFileMeta[i] = &forge_types.FileMeta{Name: cfg.Name, Data: cfg.Data}
|
||||
}
|
||||
|
||||
newConfig, useOld, err := server.Config.Services.ConfigService.FetchConfig(ctx, repo, lastPipeline, currentFileMeta, netrc)
|
||||
// If the config service is active we should refetch the config in case something changed
|
||||
configService := server.Config.Services.Manager.ConfigServiceFromRepo(repo)
|
||||
pipelineFiles, err = configService.Fetch(ctx, forge, user, repo, lastPipeline, pipelineFiles, true)
|
||||
if err != nil {
|
||||
return nil, &ErrBadRequest{
|
||||
Msg: fmt.Sprintf("On fetching external pipeline config: %s", err),
|
||||
}
|
||||
}
|
||||
if !useOld {
|
||||
pipelineFiles = newConfig
|
||||
}
|
||||
}
|
||||
|
||||
newPipeline := createNewOutOfOld(lastPipeline)
|
||||
newPipeline.Parent = lastPipeline.ID
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
40
server/services/config/combined.go
Normal file
40
server/services/config/combined.go
Normal 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
|
||||
}
|
248
server/services/config/combined_test.go
Normal file
248
server/services/config/combined_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
}
|
180
server/services/config/forge.go
Normal file
180
server/services/config/forge.go
Normal 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}
|
||||
}
|
|
@ -12,30 +12,22 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package forge_test
|
||||
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"
|
||||
"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/plugins/config"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/services/config"
|
||||
)
|
||||
|
||||
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("Dir", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("directory not found"))
|
||||
|
||||
configFetcher := forge.NewConfigFetcher(
|
||||
f,
|
||||
configFetcher := config.NewForge(
|
||||
time.Second * 3,
|
||||
)
|
||||
files, err := configFetcher.Fetch(
|
||||
context.Background(),
|
||||
f,
|
||||
&model.User{Token: "xxx"},
|
||||
repo,
|
||||
&model.Pipeline{Commit: "89ab7b2d6bfb347144ac7c557e638ab402848fee"},
|
||||
nil,
|
||||
&model.User{Token: "xxx"},
|
||||
repo,
|
||||
&model.Pipeline{Commit: "89ab7b2d6bfb347144ac7c557e638ab402848fee"},
|
||||
false,
|
||||
)
|
||||
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 {
|
||||
t.Fatal("expected an error")
|
||||
} else if !tt.expectedError && err != nil {
|
88
server/services/config/http.go
Normal file
88
server/services/config/http.go
Normal 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
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2022 Woodpecker Authors
|
||||
// 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.
|
||||
|
@ -17,10 +17,11 @@ package config
|
|||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
type Extension 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)
|
||||
type Service interface {
|
||||
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)
|
||||
}
|
|
@ -21,7 +21,7 @@ import (
|
|||
|
||||
"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"
|
||||
)
|
||||
|
||||
|
@ -29,7 +29,7 @@ type aesEncryptionService struct {
|
|||
cipher cipher.AEAD
|
||||
keyID string
|
||||
store store.Store
|
||||
clients []model.EncryptionClient
|
||||
clients []types.EncryptionClient
|
||||
}
|
||||
|
||||
func (svc *aesEncryptionService) Encrypt(plaintext, associatedData string) (string, error) {
|
|
@ -20,27 +20,27 @@ import (
|
|||
|
||||
"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"
|
||||
)
|
||||
|
||||
type aesConfiguration struct {
|
||||
password string
|
||||
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)
|
||||
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
|
||||
return c
|
||||
}
|
||||
|
||||
func (c aesConfiguration) Build() (model.EncryptionService, error) {
|
||||
func (c aesConfiguration) Build() (types.EncryptionService, error) {
|
||||
svc := &aesEncryptionService{
|
||||
cipher: nil,
|
||||
store: c.store,
|
|
@ -64,9 +64,9 @@ const (
|
|||
logMessageEncryptionDisabled = "encryption disabled"
|
||||
logMessageEncryptionKeyRegistered = "registered new encryption key"
|
||||
logMessageClientsInitialized = "initialized encryption on registered clients"
|
||||
logMessageClientsEnabled = "enabled encryption on registered services"
|
||||
logMessageClientsRotated = "updated encryption key on registered services"
|
||||
logMessageClientsDecrypted = "disabled encryption on registered services"
|
||||
logMessageClientsEnabled = "enabled encryption on registered service"
|
||||
logMessageClientsRotated = "updated encryption key on registered service"
|
||||
logMessageClientsDecrypted = "disabled encryption on registered service"
|
||||
)
|
||||
|
||||
// tink
|
|
@ -19,21 +19,21 @@ import (
|
|||
|
||||
"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"
|
||||
)
|
||||
|
||||
type builder struct {
|
||||
store store.Store
|
||||
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}
|
||||
}
|
||||
|
||||
func (b builder) WithClient(client model.EncryptionClient) model.EncryptionBuilder {
|
||||
func (b builder) WithClient(client types.EncryptionClient) types.EncryptionBuilder {
|
||||
b.clients = append(b.clients, client)
|
||||
return b
|
||||
}
|
|
@ -18,11 +18,11 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/store/types"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/services/encryption/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 {
|
||||
return nil, errors.New(errMessageNoKeysProvided)
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ func (b builder) getService(keyType string) (model.EncryptionService, error) {
|
|||
|
||||
func (b builder) isEnabled() (bool, error) {
|
||||
_, 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 err == nil, nil
|
||||
|
@ -61,7 +61,7 @@ func (b builder) detectKeyType() (string, error) {
|
|||
return keyTypeNone, nil
|
||||
}
|
||||
|
||||
func (b builder) serviceBuilder(keyType string) (model.EncryptionServiceBuilder, error) {
|
||||
func (b builder) serviceBuilder(keyType string) (types.EncryptionServiceBuilder, error) {
|
||||
switch {
|
||||
case keyType == keyTypeTink:
|
||||
return newTink(b.ctx, b.store), nil
|
|
@ -14,18 +14,18 @@
|
|||
|
||||
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 {
|
||||
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
|
||||
return b
|
||||
}
|
||||
|
||||
func (b noEncryptionBuilder) Build() (model.EncryptionService, error) {
|
||||
func (b noEncryptionBuilder) Build() (types.EncryptionService, error) {
|
||||
svc := &noEncryption{}
|
||||
for _, client := range b.clients {
|
||||
err := client.SetEncryptionService(svc)
|
|
@ -21,7 +21,7 @@ import (
|
|||
"github.com/fsnotify/fsnotify"
|
||||
"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"
|
||||
)
|
||||
|
||||
|
@ -31,7 +31,7 @@ type tinkEncryptionService struct {
|
|||
encryption tink.AEAD
|
||||
store store.Store
|
||||
keysetFileWatcher *fsnotify.Watcher
|
||||
clients []model.EncryptionClient
|
||||
clients []types.EncryptionClient
|
||||
}
|
||||
|
||||
func (svc *tinkEncryptionService) Encrypt(plaintext, associatedData string) (string, error) {
|
|
@ -20,27 +20,27 @@ import (
|
|||
|
||||
"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"
|
||||
)
|
||||
|
||||
type tinkConfiguration struct {
|
||||
keysetFilePath string
|
||||
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)
|
||||
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
|
||||
return c
|
||||
}
|
||||
|
||||
func (c tinkConfiguration) Build() (model.EncryptionService, error) {
|
||||
func (c tinkConfiguration) Build() (types.EncryptionService, error) {
|
||||
svc := &tinkEncryptionService{
|
||||
keysetFilePath: c.keysetFilePath,
|
||||
primaryKeyID: "",
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package model
|
||||
package types
|
||||
|
||||
// EncryptionBuilder is user API to obtain correctly configured encryption
|
||||
type EncryptionBuilder interface {
|
|
@ -22,11 +22,12 @@ import (
|
|||
"github.com/rs/zerolog/log"
|
||||
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/services/encryption/types"
|
||||
)
|
||||
|
||||
type EncryptedSecretStore struct {
|
||||
store model.SecretStore
|
||||
encryption model.EncryptionService
|
||||
encryption types.EncryptionService
|
||||
}
|
||||
|
||||
// ensure wrapper match interface
|
||||
|
@ -37,7 +38,7 @@ func NewSecretStore(secretStore model.SecretStore) *EncryptedSecretStore {
|
|||
return &wrapper
|
||||
}
|
||||
|
||||
func (wrapper *EncryptedSecretStore) SetEncryptionService(service model.EncryptionService) error {
|
||||
func (wrapper *EncryptedSecretStore) SetEncryptionService(service types.EncryptionService) error {
|
||||
if wrapper.encryption != nil {
|
||||
return errors.New(errMessageInitSeveralTimes)
|
||||
}
|
||||
|
@ -63,7 +64,7 @@ func (wrapper *EncryptedSecretStore) EnableEncryption() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (wrapper *EncryptedSecretStore) MigrateEncryption(newEncryptionService model.EncryptionService) error {
|
||||
func (wrapper *EncryptedSecretStore) MigrateEncryption(newEncryptionService types.EncryptionService) error {
|
||||
log.Warn().Msg(logMessageMigratingSecretsEncryption)
|
||||
secrets, err := wrapper.store.SecretListAll()
|
||||
if err != nil {
|
22
server/services/environment/extension.go
Normal file
22
server/services/environment/extension.go
Normal 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)
|
||||
}
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package environments
|
||||
package environment
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
@ -26,8 +26,8 @@ type builtin struct {
|
|||
globals []*model.Environ
|
||||
}
|
||||
|
||||
// Parse returns a model.EnvironService based on a string slice where key and value are separated by a ":" delimiter.
|
||||
func Parse(params []string) model.EnvironService {
|
||||
// Parse returns a Service based on a string slice where key and value are separated by a ":" delimiter.
|
||||
func Parse(params []string) Service {
|
||||
var globals []*model.Environ
|
||||
|
||||
for _, item := range params {
|
|
@ -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 (
|
||||
"testing"
|
82
server/services/manager.go
Normal file
82
server/services/manager.go
Normal 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
|
||||
}
|
|
@ -19,11 +19,11 @@ import (
|
|||
)
|
||||
|
||||
type combined struct {
|
||||
registries []model.ReadOnlyRegistryService
|
||||
dbRegistry model.RegistryService
|
||||
registries []ReadOnlyService
|
||||
dbRegistry Service
|
||||
}
|
||||
|
||||
func Combined(dbRegistry model.RegistryService, registries ...model.ReadOnlyRegistryService) model.RegistryService {
|
||||
func NewCombined(dbRegistry Service, registries ...ReadOnlyService) Service {
|
||||
registries = append(registries, dbRegistry)
|
||||
return &combined{
|
||||
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 {
|
||||
res, err := registry.RegistryFind(repo, name)
|
||||
if err != nil {
|
||||
|
@ -44,7 +44,7 @@ func (c combined) RegistryFind(repo *model.Repo, name string) (*model.Registry,
|
|||
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
|
||||
for _, registry := range c.registries {
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
|
@ -23,26 +23,26 @@ type db struct {
|
|||
}
|
||||
|
||||
// New returns a new local registry service.
|
||||
func New(store model.RegistryStore) model.RegistryService {
|
||||
func NewDB(store model.RegistryStore) Service {
|
||||
return &db{store}
|
||||
}
|
||||
|
||||
func (b *db) RegistryFind(repo *model.Repo, name string) (*model.Registry, error) {
|
||||
return b.store.RegistryFind(repo, name)
|
||||
func (d *db) RegistryFind(repo *model.Repo, name string) (*model.Registry, error) {
|
||||
return d.store.RegistryFind(repo, name)
|
||||
}
|
||||
|
||||
func (b *db) RegistryList(repo *model.Repo, p *model.ListOptions) ([]*model.Registry, error) {
|
||||
return b.store.RegistryList(repo, p)
|
||||
func (d *db) RegistryList(repo *model.Repo, p *model.ListOptions) ([]*model.Registry, error) {
|
||||
return d.store.RegistryList(repo, p)
|
||||
}
|
||||
|
||||
func (b *db) RegistryCreate(_ *model.Repo, in *model.Registry) error {
|
||||
return b.store.RegistryCreate(in)
|
||||
func (d *db) RegistryCreate(_ *model.Repo, in *model.Registry) error {
|
||||
return d.store.RegistryCreate(in)
|
||||
}
|
||||
|
||||
func (b *db) RegistryUpdate(_ *model.Repo, in *model.Registry) error {
|
||||
return b.store.RegistryUpdate(in)
|
||||
func (d *db) RegistryUpdate(_ *model.Repo, in *model.Registry) error {
|
||||
return d.store.RegistryUpdate(in)
|
||||
}
|
||||
|
||||
func (b *db) RegistryDelete(repo *model.Repo, addr string) error {
|
||||
return b.store.RegistryDelete(repo, addr)
|
||||
func (d *db) RegistryDelete(repo *model.Repo, addr string) error {
|
||||
return d.store.RegistryDelete(repo, addr)
|
||||
}
|
|
@ -31,7 +31,7 @@ type filesystem struct {
|
|||
path string
|
||||
}
|
||||
|
||||
func Filesystem(path string) model.ReadOnlyRegistryService {
|
||||
func NewFilesystem(path string) ReadOnlyService {
|
||||
return &filesystem{path}
|
||||
}
|
||||
|
||||
|
@ -85,12 +85,12 @@ func parseDockerConfig(path string) ([]*model.Registry, error) {
|
|||
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
|
||||
}
|
||||
|
||||
func (b *filesystem) RegistryList(_ *model.Repo, p *model.ListOptions) ([]*model.Registry, error) {
|
||||
regs, err := parseDockerConfig(b.path)
|
||||
func (f *filesystem) RegistryList(_ *model.Repo, p *model.ListOptions) ([]*model.Registry, error) {
|
||||
regs, err := parseDockerConfig(f.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
32
server/services/registry/service.go
Normal file
32
server/services/registry/service.go
Normal 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)
|
||||
}
|
133
server/services/secret/db.go
Normal file
133
server/services/secret/db.go
Normal 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)
|
||||
}
|
|
@ -12,23 +12,21 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package secrets_test
|
||||
package secret_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/franela/goblin"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func TestSecretListPipeline(t *testing.T) {
|
||||
g := goblin.Goblin(t)
|
||||
ctx := context.Background()
|
||||
mockStore := mocks_store.NewStore(t)
|
||||
|
||||
// global secret
|
||||
|
@ -66,7 +64,7 @@ func TestSecretListPipeline(t *testing.T) {
|
|||
repoSecret,
|
||||
}, 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(len(s)).Equal(1)
|
||||
|
@ -79,7 +77,7 @@ func TestSecretListPipeline(t *testing.T) {
|
|||
orgSecret,
|
||||
}, 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(len(s)).Equal(1)
|
||||
|
@ -91,7 +89,7 @@ func TestSecretListPipeline(t *testing.T) {
|
|||
globalSecret,
|
||||
}, 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(len(s)).Equal(1)
|
40
server/services/secret/service.go
Normal file
40
server/services/secret/service.go
Normal 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
95
server/services/setup.go
Normal 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
|
||||
}
|
|
@ -25,7 +25,7 @@ import (
|
|||
"github.com/go-ap/httpsig"
|
||||
"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) {
|
|
@ -4,8 +4,4 @@ type Type string
|
|||
|
||||
const (
|
||||
TypeForge Type = "forge"
|
||||
TypeConfigService Type = "config_service"
|
||||
TypeSecretService Type = "secret_service"
|
||||
TypeEnvironmentService Type = "environment_service"
|
||||
TypeRegistryService Type = "registry_service"
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue