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"
|
woodpeckerGrpcServer "go.woodpecker-ci.org/woodpecker/v2/server/grpc"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/logging"
|
"go.woodpecker-ci.org/woodpecker/v2/server/logging"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
||||||
// "go.woodpecker-ci.org/woodpecker/v2/server/plugins/encryption"
|
|
||||||
// encryptedStore "go.woodpecker-ci.org/woodpecker/v2/server/plugins/encryption/wrapper/store"
|
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/plugins/permissions"
|
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/pubsub"
|
"go.woodpecker-ci.org/woodpecker/v2/server/pubsub"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/router"
|
"go.woodpecker-ci.org/woodpecker/v2/server/router"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/router/middleware"
|
"go.woodpecker-ci.org/woodpecker/v2/server/router/middleware"
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v2/server/services"
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v2/server/services/permissions"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/store"
|
"go.woodpecker-ci.org/woodpecker/v2/server/store"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/web"
|
"go.woodpecker-ci.org/woodpecker/v2/server/web"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
|
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
|
||||||
|
@ -271,48 +270,21 @@ func run(c *cli.Context) error {
|
||||||
return g.Wait()
|
return g.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupEvilGlobals(c *cli.Context, v store.Store, f forge.Forge) error {
|
func setupEvilGlobals(c *cli.Context, s store.Store, f forge.Forge) error {
|
||||||
// forge
|
// forge
|
||||||
server.Config.Services.Forge = f
|
server.Config.Services.Forge = f
|
||||||
server.Config.Services.Timeout = c.Duration("forge-timeout")
|
|
||||||
|
|
||||||
// services
|
// services
|
||||||
server.Config.Services.Queue = setupQueue(c, v)
|
server.Config.Services.Queue = setupQueue(c, s)
|
||||||
server.Config.Services.Logs = logging.New()
|
server.Config.Services.Logs = logging.New()
|
||||||
server.Config.Services.Pubsub = pubsub.New()
|
server.Config.Services.Pubsub = pubsub.New()
|
||||||
var err error
|
|
||||||
server.Config.Services.Registries, err = setupRegistryService(c, v)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(1544): fix encrypted store
|
|
||||||
// // encryption
|
|
||||||
// encryptedSecretStore := encryptedStore.NewSecretStore(v)
|
|
||||||
// err := encryption.Encryption(c, v).WithClient(encryptedSecretStore).Build()
|
|
||||||
// if err != nil {
|
|
||||||
// log.Fatal().Err(err).Msg("could not create encryption service")
|
|
||||||
// }
|
|
||||||
// server.Config.Services.Secrets = setupSecretService(c, encryptedSecretStore)
|
|
||||||
server.Config.Services.Secrets, err = setupSecretService(c, v)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
server.Config.Services.Environ, err = setupEnvironService(c, v)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
server.Config.Services.Membership = setupMembershipService(c, f)
|
server.Config.Services.Membership = setupMembershipService(c, f)
|
||||||
|
|
||||||
server.Config.Services.SignaturePrivateKey, server.Config.Services.SignaturePublicKey, err = setupSignatureKeys(v)
|
serviceMangager, err := services.NewManager(c, s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("could not setup service manager: %w", err)
|
||||||
}
|
|
||||||
|
|
||||||
server.Config.Services.ConfigService, err = setupConfigService(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
server.Config.Services.Manager = serviceMangager
|
||||||
|
|
||||||
// authentication
|
// authentication
|
||||||
server.Config.Pipeline.AuthenticatePublicRepos = c.Bool("authenticate-public-repos")
|
server.Config.Pipeline.AuthenticatePublicRepos = c.Bool("authenticate-public-repos")
|
||||||
|
|
|
@ -17,11 +17,6 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto"
|
|
||||||
"crypto/ed25519"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
@ -41,15 +36,9 @@ import (
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge/gitea"
|
"go.woodpecker-ci.org/woodpecker/v2/server/forge/gitea"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge/github"
|
"go.woodpecker-ci.org/woodpecker/v2/server/forge/github"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge/gitlab"
|
"go.woodpecker-ci.org/woodpecker/v2/server/forge/gitlab"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/plugins/config"
|
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/plugins/environments"
|
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/plugins/registry"
|
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/plugins/secrets"
|
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/queue"
|
"go.woodpecker-ci.org/woodpecker/v2/server/queue"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/store"
|
"go.woodpecker-ci.org/woodpecker/v2/server/store"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/store/datastore"
|
"go.woodpecker-ci.org/woodpecker/v2/server/store/datastore"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/store/types"
|
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/shared/addon"
|
"go.woodpecker-ci.org/woodpecker/v2/shared/addon"
|
||||||
addonTypes "go.woodpecker-ci.org/woodpecker/v2/shared/addon/types"
|
addonTypes "go.woodpecker-ci.org/woodpecker/v2/shared/addon/types"
|
||||||
)
|
)
|
||||||
|
@ -111,48 +100,6 @@ func setupQueue(c *cli.Context, s store.Store) queue.Queue {
|
||||||
return queue.WithTaskStore(queue.New(c.Context), s)
|
return queue.WithTaskStore(queue.New(c.Context), s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupSecretService(c *cli.Context, s model.SecretStore) (model.SecretService, error) {
|
|
||||||
addonService, err := addon.Load[model.SecretService](c.StringSlice("addons"), addonTypes.TypeSecretService)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if addonService != nil {
|
|
||||||
return addonService.Value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return secrets.New(c.Context, s), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupRegistryService(c *cli.Context, s store.Store) (model.RegistryService, error) {
|
|
||||||
addonService, err := addon.Load[model.RegistryService](c.StringSlice("addons"), addonTypes.TypeRegistryService)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if addonService != nil {
|
|
||||||
return addonService.Value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.String("docker-config") != "" {
|
|
||||||
return registry.Combined(
|
|
||||||
registry.New(s),
|
|
||||||
registry.Filesystem(c.String("docker-config")),
|
|
||||||
), nil
|
|
||||||
}
|
|
||||||
return registry.New(s), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupEnvironService(c *cli.Context, _ store.Store) (model.EnvironService, error) {
|
|
||||||
addonService, err := addon.Load[model.EnvironService](c.StringSlice("addons"), addonTypes.TypeEnvironmentService)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if addonService != nil {
|
|
||||||
return addonService.Value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return environments.Parse(c.StringSlice("environment")), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupMembershipService(_ *cli.Context, r forge.Forge) cache.MembershipService {
|
func setupMembershipService(_ *cli.Context, r forge.Forge) cache.MembershipService {
|
||||||
return cache.NewMembershipService(r)
|
return cache.NewMembershipService(r)
|
||||||
}
|
}
|
||||||
|
@ -292,46 +239,3 @@ func setupMetrics(g *errgroup.Group, _store store.Store) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// setupSignatureKeys generate or load key pair to sign webhooks requests (i.e. used for extensions)
|
|
||||||
func setupSignatureKeys(_store store.Store) (crypto.PrivateKey, crypto.PublicKey, error) {
|
|
||||||
privKeyID := "signature-private-key"
|
|
||||||
|
|
||||||
privKey, err := _store.ServerConfigGet(privKeyID)
|
|
||||||
if errors.Is(err, types.RecordNotExist) {
|
|
||||||
_, privKey, err := ed25519.GenerateKey(rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("failed to generate private key: %w", err)
|
|
||||||
}
|
|
||||||
err = _store.ServerConfigSet(privKeyID, hex.EncodeToString(privKey))
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("failed to store private key: %w", err)
|
|
||||||
}
|
|
||||||
log.Debug().Msg("created private key")
|
|
||||||
return privKey, privKey.Public(), nil
|
|
||||||
} else if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("failed to load private key: %w", err)
|
|
||||||
}
|
|
||||||
privKeyStr, err := hex.DecodeString(privKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("failed to decode private key: %w", err)
|
|
||||||
}
|
|
||||||
privateKey := ed25519.PrivateKey(privKeyStr)
|
|
||||||
return privateKey, privateKey.Public(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupConfigService(c *cli.Context) (config.Extension, error) {
|
|
||||||
addonExt, err := addon.Load[config.Extension](c.StringSlice("addons"), addonTypes.TypeConfigService)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if addonExt != nil {
|
|
||||||
return addonExt.Value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if endpoint := c.String("config-service-endpoint"); endpoint != "" {
|
|
||||||
return config.NewHTTP(endpoint, server.Config.Services.SignaturePrivateKey), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
- **YAML File**: A file format used to define and configure [workflows][Workflow].
|
- **YAML File**: A file format used to define and configure [workflows][Workflow].
|
||||||
- **Dependency**: [Workflows][Workflow] can depend on each other, and if possible, they are executed in parallel.
|
- **Dependency**: [Workflows][Workflow] can depend on each other, and if possible, they are executed in parallel.
|
||||||
- **Status**: Status refers to the outcome of a step or [workflow][Workflow] after it has been executed, determined by the internal command exit code. At the end of a [workflow][Workflow], its status is sent to the [forge][Forge].
|
- **Status**: Status refers to the outcome of a step or [workflow][Workflow] after it has been executed, determined by the internal command exit code. At the end of a [workflow][Workflow], its status is sent to the [forge][Forge].
|
||||||
|
- **Service extension**: Some parts of woodpecker internal services like secrets storage or config fetcher can be replaced through service extensions.
|
||||||
|
|
||||||
## Pipeline events
|
## Pipeline events
|
||||||
|
|
||||||
|
|
|
@ -13,10 +13,6 @@ To adapt Woodpecker to your needs beyond the [configuration](../10-server-config
|
||||||
Addons can be used for:
|
Addons can be used for:
|
||||||
|
|
||||||
- Forges
|
- Forges
|
||||||
- Config services
|
|
||||||
- Secret services
|
|
||||||
- Environment services
|
|
||||||
- Registry services
|
|
||||||
|
|
||||||
## Restrictions
|
## Restrictions
|
||||||
|
|
||||||
|
|
|
@ -19,13 +19,9 @@ Directly import Woodpecker's Go package (`go.woodpecker-ci.org/woodpecker/woodpe
|
||||||
|
|
||||||
### Return types
|
### Return types
|
||||||
|
|
||||||
| Addon type | Return type |
|
| Addon type | Return type |
|
||||||
| -------------------- | ---------------------------------------------------------------------- |
|
| ---------- | -------------------------------------------------------------------- |
|
||||||
| `Forge` | `"go.woodpecker-ci.org/woodpecker/woodpecker/v2/server/forge".Forge` |
|
| `Forge` | `"go.woodpecker-ci.org/woodpecker/woodpecker/v2/server/forge".Forge` |
|
||||||
| `ConfigService` | `"go.woodpecker-ci.org/woodpecker/v2/server/plugins/config".Extension` |
|
|
||||||
| `SecretService` | `"go.woodpecker-ci.org/woodpecker/v2/server/model".SecretService` |
|
|
||||||
| `EnvironmentService` | `"go.woodpecker-ci.org/woodpecker/v2/server/model".EnvironmentService` |
|
|
||||||
| `RegistryService` | `"go.woodpecker-ci.org/woodpecker/v2/server/model".RegistryService` |
|
|
||||||
|
|
||||||
### Using configurations
|
### Using configurations
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,8 @@ import (
|
||||||
// @Param page query int false "for response pagination, page offset number" default(1)
|
// @Param page query int false "for response pagination, page offset number" default(1)
|
||||||
// @Param perPage query int false "for response pagination, max items per page" default(50)
|
// @Param perPage query int false "for response pagination, max items per page" default(50)
|
||||||
func GetGlobalSecretList(c *gin.Context) {
|
func GetGlobalSecretList(c *gin.Context) {
|
||||||
list, err := server.Config.Services.Secrets.GlobalSecretList(session.Pagination(c))
|
secretService := server.Config.Services.Manager.SecretService()
|
||||||
|
list, err := secretService.GlobalSecretList(session.Pagination(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.String(http.StatusInternalServerError, "Error getting global secret list. %s", err)
|
c.String(http.StatusInternalServerError, "Error getting global secret list. %s", err)
|
||||||
return
|
return
|
||||||
|
@ -59,7 +60,8 @@ func GetGlobalSecretList(c *gin.Context) {
|
||||||
// @Param secret path string true "the secret's name"
|
// @Param secret path string true "the secret's name"
|
||||||
func GetGlobalSecret(c *gin.Context) {
|
func GetGlobalSecret(c *gin.Context) {
|
||||||
name := c.Param("secret")
|
name := c.Param("secret")
|
||||||
secret, err := server.Config.Services.Secrets.GlobalSecretFind(name)
|
secretService := server.Config.Services.Manager.SecretService()
|
||||||
|
secret, err := secretService.GlobalSecretFind(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleDBError(c, err)
|
handleDBError(c, err)
|
||||||
return
|
return
|
||||||
|
@ -92,7 +94,9 @@ func PostGlobalSecret(c *gin.Context) {
|
||||||
c.String(http.StatusBadRequest, "Error inserting global secret. %s", err)
|
c.String(http.StatusBadRequest, "Error inserting global secret. %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := server.Config.Services.Secrets.GlobalSecretCreate(secret); err != nil {
|
|
||||||
|
secretService := server.Config.Services.Manager.SecretService()
|
||||||
|
if err := secretService.GlobalSecretCreate(secret); err != nil {
|
||||||
c.String(http.StatusInternalServerError, "Error inserting global secret %q. %s", in.Name, err)
|
c.String(http.StatusInternalServerError, "Error inserting global secret %q. %s", in.Name, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -119,7 +123,8 @@ func PatchGlobalSecret(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
secret, err := server.Config.Services.Secrets.GlobalSecretFind(name)
|
secretService := server.Config.Services.Manager.SecretService()
|
||||||
|
secret, err := secretService.GlobalSecretFind(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleDBError(c, err)
|
handleDBError(c, err)
|
||||||
return
|
return
|
||||||
|
@ -138,7 +143,8 @@ func PatchGlobalSecret(c *gin.Context) {
|
||||||
c.String(http.StatusBadRequest, "Error updating global secret. %s", err)
|
c.String(http.StatusBadRequest, "Error updating global secret. %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := server.Config.Services.Secrets.GlobalSecretUpdate(secret); err != nil {
|
|
||||||
|
if err := secretService.GlobalSecretUpdate(secret); err != nil {
|
||||||
c.String(http.StatusInternalServerError, "Error updating global secret %q. %s", in.Name, err)
|
c.String(http.StatusInternalServerError, "Error updating global secret %q. %s", in.Name, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -156,7 +162,8 @@ func PatchGlobalSecret(c *gin.Context) {
|
||||||
// @Param secret path string true "the secret's name"
|
// @Param secret path string true "the secret's name"
|
||||||
func DeleteGlobalSecret(c *gin.Context) {
|
func DeleteGlobalSecret(c *gin.Context) {
|
||||||
name := c.Param("secret")
|
name := c.Param("secret")
|
||||||
if err := server.Config.Services.Secrets.GlobalSecretDelete(name); err != nil {
|
secretService := server.Config.Services.Manager.SecretService()
|
||||||
|
if err := secretService.GlobalSecretDelete(name); err != nil {
|
||||||
handleDBError(c, err)
|
handleDBError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,8 @@ func GetOrgSecret(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
secret, err := server.Config.Services.Secrets.OrgSecretFind(orgID, name)
|
secretService := server.Config.Services.Manager.SecretService()
|
||||||
|
secret, err := secretService.OrgSecretFind(orgID, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleDBError(c, err)
|
handleDBError(c, err)
|
||||||
return
|
return
|
||||||
|
@ -70,7 +71,8 @@ func GetOrgSecretList(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
list, err := server.Config.Services.Secrets.OrgSecretList(orgID, session.Pagination(c))
|
secretService := server.Config.Services.Manager.SecretService()
|
||||||
|
list, err := secretService.OrgSecretList(orgID, session.Pagination(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.String(http.StatusInternalServerError, "Error getting secret list for %q. %s", orgID, err)
|
c.String(http.StatusInternalServerError, "Error getting secret list for %q. %s", orgID, err)
|
||||||
return
|
return
|
||||||
|
@ -116,7 +118,9 @@ func PostOrgSecret(c *gin.Context) {
|
||||||
c.String(http.StatusUnprocessableEntity, "Error inserting org %q secret. %s", orgID, err)
|
c.String(http.StatusUnprocessableEntity, "Error inserting org %q secret. %s", orgID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := server.Config.Services.Secrets.OrgSecretCreate(orgID, secret); err != nil {
|
|
||||||
|
secretService := server.Config.Services.Manager.SecretService()
|
||||||
|
if err := secretService.OrgSecretCreate(orgID, secret); err != nil {
|
||||||
c.String(http.StatusInternalServerError, "Error inserting org %q secret %q. %s", orgID, in.Name, err)
|
c.String(http.StatusInternalServerError, "Error inserting org %q secret %q. %s", orgID, in.Name, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -149,7 +153,8 @@ func PatchOrgSecret(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
secret, err := server.Config.Services.Secrets.OrgSecretFind(orgID, name)
|
secretService := server.Config.Services.Manager.SecretService()
|
||||||
|
secret, err := secretService.OrgSecretFind(orgID, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleDBError(c, err)
|
handleDBError(c, err)
|
||||||
return
|
return
|
||||||
|
@ -168,7 +173,8 @@ func PatchOrgSecret(c *gin.Context) {
|
||||||
c.String(http.StatusUnprocessableEntity, "Error updating org %q secret. %s", orgID, err)
|
c.String(http.StatusUnprocessableEntity, "Error updating org %q secret. %s", orgID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := server.Config.Services.Secrets.OrgSecretUpdate(orgID, secret); err != nil {
|
|
||||||
|
if err := secretService.OrgSecretUpdate(orgID, secret); err != nil {
|
||||||
c.String(http.StatusInternalServerError, "Error updating org %q secret %q. %s", orgID, in.Name, err)
|
c.String(http.StatusInternalServerError, "Error updating org %q secret %q. %s", orgID, in.Name, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -193,7 +199,8 @@ func DeleteOrgSecret(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := server.Config.Services.Secrets.OrgSecretDelete(orgID, name); err != nil {
|
secretService := server.Config.Services.Manager.SecretService()
|
||||||
|
if err := secretService.OrgSecretDelete(orgID, name); err != nil {
|
||||||
handleDBError(c, err)
|
handleDBError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -434,13 +434,7 @@ func PostPipeline(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
netrc, err := server.Config.Services.Forge.Netrc(user, repo)
|
newpipeline, err := pipeline.Restart(c, _store, pl, user, repo, envs)
|
||||||
if err != nil {
|
|
||||||
handlePipelineErr(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
newpipeline, err := pipeline.Restart(c, _store, pl, user, repo, envs, netrc)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handlePipelineErr(c, err)
|
handlePipelineErr(c, err)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -35,11 +35,11 @@ import (
|
||||||
// @Param repo_id path int true "the repository id"
|
// @Param repo_id path int true "the repository id"
|
||||||
// @Param registry path string true "the registry name"
|
// @Param registry path string true "the registry name"
|
||||||
func GetRegistry(c *gin.Context) {
|
func GetRegistry(c *gin.Context) {
|
||||||
var (
|
repo := session.Repo(c)
|
||||||
repo = session.Repo(c)
|
name := c.Param("registry")
|
||||||
name = c.Param("registry")
|
|
||||||
)
|
registryService := server.Config.Services.Manager.RegistryServiceFromRepo(repo)
|
||||||
registry, err := server.Config.Services.Registries.RegistryFind(repo, name)
|
registry, err := registryService.RegistryFind(repo, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleDBError(c, err)
|
handleDBError(c, err)
|
||||||
return
|
return
|
||||||
|
@ -75,7 +75,9 @@ func PostRegistry(c *gin.Context) {
|
||||||
c.String(http.StatusBadRequest, "Error inserting registry. %s", err)
|
c.String(http.StatusBadRequest, "Error inserting registry. %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := server.Config.Services.Registries.RegistryCreate(repo, registry); err != nil {
|
|
||||||
|
registryService := server.Config.Services.Manager.RegistryServiceFromRepo(repo)
|
||||||
|
if err := registryService.RegistryCreate(repo, registry); err != nil {
|
||||||
c.String(http.StatusInternalServerError, "Error inserting registry %q. %s", in.Address, err)
|
c.String(http.StatusInternalServerError, "Error inserting registry %q. %s", in.Address, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -106,7 +108,8 @@ func PatchRegistry(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
registry, err := server.Config.Services.Registries.RegistryFind(repo, name)
|
registryService := server.Config.Services.Manager.RegistryServiceFromRepo(repo)
|
||||||
|
registry, err := registryService.RegistryFind(repo, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleDBError(c, err)
|
handleDBError(c, err)
|
||||||
return
|
return
|
||||||
|
@ -122,7 +125,7 @@ func PatchRegistry(c *gin.Context) {
|
||||||
c.String(http.StatusUnprocessableEntity, "Error updating registry. %s", err)
|
c.String(http.StatusUnprocessableEntity, "Error updating registry. %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := server.Config.Services.Registries.RegistryUpdate(repo, registry); err != nil {
|
if err := registryService.RegistryUpdate(repo, registry); err != nil {
|
||||||
c.String(http.StatusInternalServerError, "Error updating registry %q. %s", in.Address, err)
|
c.String(http.StatusInternalServerError, "Error updating registry %q. %s", in.Address, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -142,7 +145,8 @@ func PatchRegistry(c *gin.Context) {
|
||||||
// @Param perPage query int false "for response pagination, max items per page" default(50)
|
// @Param perPage query int false "for response pagination, max items per page" default(50)
|
||||||
func GetRegistryList(c *gin.Context) {
|
func GetRegistryList(c *gin.Context) {
|
||||||
repo := session.Repo(c)
|
repo := session.Repo(c)
|
||||||
list, err := server.Config.Services.Registries.RegistryList(repo, session.Pagination(c))
|
registryService := server.Config.Services.Manager.RegistryServiceFromRepo(repo)
|
||||||
|
list, err := registryService.RegistryList(repo, session.Pagination(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.String(http.StatusInternalServerError, "Error getting registry list. %s", err)
|
c.String(http.StatusInternalServerError, "Error getting registry list. %s", err)
|
||||||
return
|
return
|
||||||
|
@ -166,11 +170,11 @@ func GetRegistryList(c *gin.Context) {
|
||||||
// @Param repo_id path int true "the repository id"
|
// @Param repo_id path int true "the repository id"
|
||||||
// @Param registry path string true "the registry name"
|
// @Param registry path string true "the registry name"
|
||||||
func DeleteRegistry(c *gin.Context) {
|
func DeleteRegistry(c *gin.Context) {
|
||||||
var (
|
repo := session.Repo(c)
|
||||||
repo = session.Repo(c)
|
name := c.Param("registry")
|
||||||
name = c.Param("registry")
|
|
||||||
)
|
registryService := server.Config.Services.Manager.RegistryServiceFromRepo(repo)
|
||||||
err := server.Config.Services.Registries.RegistryDelete(repo, name)
|
err := registryService.RegistryDelete(repo, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleDBError(c, err)
|
handleDBError(c, err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -36,11 +36,11 @@ import (
|
||||||
// @Param repo_id path int true "the repository id"
|
// @Param repo_id path int true "the repository id"
|
||||||
// @Param secretName path string true "the secret name"
|
// @Param secretName path string true "the secret name"
|
||||||
func GetSecret(c *gin.Context) {
|
func GetSecret(c *gin.Context) {
|
||||||
var (
|
repo := session.Repo(c)
|
||||||
repo = session.Repo(c)
|
name := c.Param("secret")
|
||||||
name = c.Param("secret")
|
|
||||||
)
|
secretService := server.Config.Services.Manager.SecretServiceFromRepo(repo)
|
||||||
secret, err := server.Config.Services.Secrets.SecretFind(repo, name)
|
secret, err := secretService.SecretFind(repo, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleDBError(c, err)
|
handleDBError(c, err)
|
||||||
return
|
return
|
||||||
|
@ -77,7 +77,9 @@ func PostSecret(c *gin.Context) {
|
||||||
c.String(http.StatusUnprocessableEntity, "Error inserting secret. %s", err)
|
c.String(http.StatusUnprocessableEntity, "Error inserting secret. %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := server.Config.Services.Secrets.SecretCreate(repo, secret); err != nil {
|
|
||||||
|
secretService := server.Config.Services.Manager.SecretServiceFromRepo(repo)
|
||||||
|
if err := secretService.SecretCreate(repo, secret); err != nil {
|
||||||
c.String(http.StatusInternalServerError, "Error inserting secret %q. %s", in.Name, err)
|
c.String(http.StatusInternalServerError, "Error inserting secret %q. %s", in.Name, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -108,7 +110,8 @@ func PatchSecret(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
secret, err := server.Config.Services.Secrets.SecretFind(repo, name)
|
secretService := server.Config.Services.Manager.SecretServiceFromRepo(repo)
|
||||||
|
secret, err := secretService.SecretFind(repo, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleDBError(c, err)
|
handleDBError(c, err)
|
||||||
return
|
return
|
||||||
|
@ -127,7 +130,7 @@ func PatchSecret(c *gin.Context) {
|
||||||
c.String(http.StatusUnprocessableEntity, "Error updating secret. %s", err)
|
c.String(http.StatusUnprocessableEntity, "Error updating secret. %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := server.Config.Services.Secrets.SecretUpdate(repo, secret); err != nil {
|
if err := secretService.SecretUpdate(repo, secret); err != nil {
|
||||||
c.String(http.StatusInternalServerError, "Error updating secret %q. %s", in.Name, err)
|
c.String(http.StatusInternalServerError, "Error updating secret %q. %s", in.Name, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -147,7 +150,8 @@ func PatchSecret(c *gin.Context) {
|
||||||
// @Param perPage query int false "for response pagination, max items per page" default(50)
|
// @Param perPage query int false "for response pagination, max items per page" default(50)
|
||||||
func GetSecretList(c *gin.Context) {
|
func GetSecretList(c *gin.Context) {
|
||||||
repo := session.Repo(c)
|
repo := session.Repo(c)
|
||||||
list, err := server.Config.Services.Secrets.SecretList(repo, session.Pagination(c))
|
secretService := server.Config.Services.Manager.SecretServiceFromRepo(repo)
|
||||||
|
list, err := secretService.SecretList(repo, session.Pagination(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.String(http.StatusInternalServerError, "Error getting secret list. %s", err)
|
c.String(http.StatusInternalServerError, "Error getting secret list. %s", err)
|
||||||
return
|
return
|
||||||
|
@ -171,11 +175,11 @@ func GetSecretList(c *gin.Context) {
|
||||||
// @Param repo_id path int true "the repository id"
|
// @Param repo_id path int true "the repository id"
|
||||||
// @Param secretName path string true "the secret name"
|
// @Param secretName path string true "the secret name"
|
||||||
func DeleteSecret(c *gin.Context) {
|
func DeleteSecret(c *gin.Context) {
|
||||||
var (
|
repo := session.Repo(c)
|
||||||
repo = session.Repo(c)
|
name := c.Param("secret")
|
||||||
name = c.Param("secret")
|
|
||||||
)
|
secretService := server.Config.Services.Manager.SecretServiceFromRepo(repo)
|
||||||
if err := server.Config.Services.Secrets.SecretDelete(repo, name); err != nil {
|
if err := secretService.SecretDelete(repo, name); err != nil {
|
||||||
handleDBError(c, err)
|
handleDBError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ import (
|
||||||
// @Tags System
|
// @Tags System
|
||||||
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
|
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
|
||||||
func GetSignaturePublicKey(c *gin.Context) {
|
func GetSignaturePublicKey(c *gin.Context) {
|
||||||
b, err := x509.MarshalPKIXPublicKey(server.Config.Services.SignaturePublicKey)
|
b, err := x509.MarshalPKIXPublicKey(server.Config.Services.Manager.SignaturePublicKey())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("can't marshal public key")
|
log.Error().Err(err).Msg("can't marshal public key")
|
||||||
c.AbortWithStatus(http.StatusInternalServerError)
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
|
|
@ -18,42 +18,26 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/cache"
|
"go.woodpecker-ci.org/woodpecker/v2/server/cache"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge"
|
"go.woodpecker-ci.org/woodpecker/v2/server/forge"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/logging"
|
"go.woodpecker-ci.org/woodpecker/v2/server/logging"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/plugins/config"
|
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/plugins/permissions"
|
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/pubsub"
|
"go.woodpecker-ci.org/woodpecker/v2/server/pubsub"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/queue"
|
"go.woodpecker-ci.org/woodpecker/v2/server/queue"
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v2/server/services"
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v2/server/services/permissions"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Config = struct {
|
var Config = struct {
|
||||||
Services struct {
|
Services struct {
|
||||||
Pubsub *pubsub.Publisher
|
Pubsub *pubsub.Publisher
|
||||||
Queue queue.Queue
|
Queue queue.Queue
|
||||||
Logs logging.Log
|
Logs logging.Log
|
||||||
Secrets model.SecretService
|
Forge forge.Forge
|
||||||
Registries model.RegistryService
|
Membership cache.MembershipService
|
||||||
Environ model.EnvironService
|
Manager *services.Manager
|
||||||
Forge forge.Forge
|
|
||||||
Timeout time.Duration
|
|
||||||
Membership cache.MembershipService
|
|
||||||
ConfigService config.Extension
|
|
||||||
SignaturePrivateKey crypto.PrivateKey
|
|
||||||
SignaturePublicKey crypto.PublicKey
|
|
||||||
}
|
|
||||||
Storage struct {
|
|
||||||
// Users model.UserStore
|
|
||||||
// Repos model.RepoStore
|
|
||||||
// Builds model.BuildStore
|
|
||||||
// Logs model.LogStore
|
|
||||||
Steps model.StepStore
|
|
||||||
// Registries model.RegistryStore
|
|
||||||
// Secrets model.SecretStore
|
|
||||||
}
|
}
|
||||||
Server struct {
|
Server struct {
|
||||||
Key string
|
Key string
|
||||||
|
|
|
@ -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")
|
errEnvironValueInvalid = errors.New("invalid Environment Variable Value")
|
||||||
)
|
)
|
||||||
|
|
||||||
// EnvironService defines a service for managing environment variables.
|
|
||||||
type EnvironService interface {
|
|
||||||
EnvironList(*Repo) ([]*Environ, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnvironStore persists environment information to storage.
|
// EnvironStore persists environment information to storage.
|
||||||
type EnvironStore interface {
|
type EnvironStore interface {
|
||||||
EnvironList(*Repo) ([]*Environ, error)
|
EnvironList(*Repo) ([]*Environ, error)
|
||||||
|
|
|
@ -26,21 +26,6 @@ var (
|
||||||
errRegistryPasswordInvalid = errors.New("invalid registry password")
|
errRegistryPasswordInvalid = errors.New("invalid registry password")
|
||||||
)
|
)
|
||||||
|
|
||||||
// RegistryService defines a service for managing registries.
|
|
||||||
type RegistryService interface {
|
|
||||||
RegistryFind(*Repo, string) (*Registry, error)
|
|
||||||
RegistryList(*Repo, *ListOptions) ([]*Registry, error)
|
|
||||||
RegistryCreate(*Repo, *Registry) error
|
|
||||||
RegistryUpdate(*Repo, *Registry) error
|
|
||||||
RegistryDelete(*Repo, string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadOnlyRegistryService defines a service for managing registries.
|
|
||||||
type ReadOnlyRegistryService interface {
|
|
||||||
RegistryFind(*Repo, string) (*Registry, error)
|
|
||||||
RegistryList(*Repo, *ListOptions) ([]*Registry, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegistryStore persists registry information to storage.
|
// RegistryStore persists registry information to storage.
|
||||||
type RegistryStore interface {
|
type RegistryStore interface {
|
||||||
RegistryFind(*Repo, string) (*Registry, error)
|
RegistryFind(*Repo, string) (*Registry, error)
|
||||||
|
|
|
@ -29,29 +29,6 @@ var (
|
||||||
ErrSecretEventInvalid = errors.New("invalid secret event")
|
ErrSecretEventInvalid = errors.New("invalid secret event")
|
||||||
)
|
)
|
||||||
|
|
||||||
// SecretService defines a service for managing secrets.
|
|
||||||
type SecretService interface {
|
|
||||||
SecretListPipeline(*Repo, *Pipeline, *ListOptions) ([]*Secret, error)
|
|
||||||
// Repository secrets
|
|
||||||
SecretFind(*Repo, string) (*Secret, error)
|
|
||||||
SecretList(*Repo, *ListOptions) ([]*Secret, error)
|
|
||||||
SecretCreate(*Repo, *Secret) error
|
|
||||||
SecretUpdate(*Repo, *Secret) error
|
|
||||||
SecretDelete(*Repo, string) error
|
|
||||||
// Organization secrets
|
|
||||||
OrgSecretFind(int64, string) (*Secret, error)
|
|
||||||
OrgSecretList(int64, *ListOptions) ([]*Secret, error)
|
|
||||||
OrgSecretCreate(int64, *Secret) error
|
|
||||||
OrgSecretUpdate(int64, *Secret) error
|
|
||||||
OrgSecretDelete(int64, string) error
|
|
||||||
// Global secrets
|
|
||||||
GlobalSecretFind(string) (*Secret, error)
|
|
||||||
GlobalSecretList(*ListOptions) ([]*Secret, error)
|
|
||||||
GlobalSecretCreate(*Secret) error
|
|
||||||
GlobalSecretUpdate(*Secret) error
|
|
||||||
GlobalSecretDelete(string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// SecretStore persists secret information to storage.
|
// SecretStore persists secret information to storage.
|
||||||
type SecretStore interface {
|
type SecretStore interface {
|
||||||
SecretFind(*Repo, string) (*Secret, error)
|
SecretFind(*Repo, string) (*Secret, error)
|
||||||
|
|
|
@ -34,6 +34,7 @@ var skipPipelineRegex = regexp.MustCompile(`\[(?i:ci *skip|skip *ci)\]`)
|
||||||
|
|
||||||
// Create a new pipeline and start it
|
// Create a new pipeline and start it
|
||||||
func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline *model.Pipeline) (*model.Pipeline, error) {
|
func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline *model.Pipeline) (*model.Pipeline, error) {
|
||||||
|
_forge := server.Config.Services.Forge
|
||||||
repoUser, err := _store.GetUser(repo.UserID)
|
repoUser, err := _store.GetUser(repo.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg := fmt.Sprintf("failure to find repo owner via id '%d'", repo.UserID)
|
msg := fmt.Sprintf("failure to find repo owner via id '%d'", repo.UserID)
|
||||||
|
@ -54,7 +55,7 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline
|
||||||
// If the forge has a refresh token, the current access token
|
// If the forge has a refresh token, the current access token
|
||||||
// may be stale. Therefore, we should refresh prior to dispatching
|
// may be stale. Therefore, we should refresh prior to dispatching
|
||||||
// the pipeline.
|
// the pipeline.
|
||||||
forge.Refresh(ctx, server.Config.Services.Forge, _store, repoUser)
|
forge.Refresh(ctx, _forge, _store, repoUser)
|
||||||
|
|
||||||
// update some pipeline fields
|
// update some pipeline fields
|
||||||
pipeline.RepoID = repo.ID
|
pipeline.RepoID = repo.ID
|
||||||
|
@ -68,8 +69,8 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch the pipeline file from the forge
|
// fetch the pipeline file from the forge
|
||||||
configFetcher := forge.NewConfigFetcher(server.Config.Services.Forge, server.Config.Services.Timeout, server.Config.Services.ConfigService, repoUser, repo, pipeline)
|
configService := server.Config.Services.Manager.ConfigServiceFromRepo(repo)
|
||||||
forgeYamlConfigs, configFetchErr := configFetcher.Fetch(ctx)
|
forgeYamlConfigs, configFetchErr := configService.Fetch(ctx, _forge, repoUser, repo, pipeline, nil, false)
|
||||||
if errors.Is(configFetchErr, &forge_types.ErrConfigNotFound{}) {
|
if errors.Is(configFetchErr, &forge_types.ErrConfigNotFound{}) {
|
||||||
log.Debug().Str("repo", repo.FullName).Err(configFetchErr).Msgf("cannot find config '%s' in '%s' with user: '%s'", repo.Config, pipeline.Ref, repoUser.Login)
|
log.Debug().Str("repo", repo.FullName).Err(configFetchErr).Msgf("cannot find config '%s' in '%s' with user: '%s'", repo.Config, pipeline.Ref, repoUser.Login)
|
||||||
if err := _store.DeletePipeline(pipeline); err != nil {
|
if err := _store.DeletePipeline(pipeline); err != nil {
|
||||||
|
|
|
@ -42,12 +42,14 @@ func parsePipeline(store store.Store, currentPipeline *model.Pipeline, user *mod
|
||||||
log.Error().Err(err).Str("repo", repo.FullName).Msgf("error getting last pipeline before pipeline number '%d'", currentPipeline.Number)
|
log.Error().Err(err).Str("repo", repo.FullName).Msgf("error getting last pipeline before pipeline number '%d'", currentPipeline.Number)
|
||||||
}
|
}
|
||||||
|
|
||||||
secs, err := server.Config.Services.Secrets.SecretListPipeline(repo, currentPipeline, &model.ListOptions{All: true})
|
secretService := server.Config.Services.Manager.SecretServiceFromRepo(repo)
|
||||||
|
secs, err := secretService.SecretListPipeline(repo, currentPipeline, &model.ListOptions{All: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("error getting secrets for %s#%d", repo.FullName, currentPipeline.Number)
|
log.Error().Err(err).Msgf("error getting secrets for %s#%d", repo.FullName, currentPipeline.Number)
|
||||||
}
|
}
|
||||||
|
|
||||||
regs, err := server.Config.Services.Registries.RegistryList(repo, &model.ListOptions{All: true})
|
registryService := server.Config.Services.Manager.RegistryServiceFromRepo(repo)
|
||||||
|
regs, err := registryService.RegistryList(repo, &model.ListOptions{All: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("error getting registry credentials for %s#%d", repo.FullName, currentPipeline.Number)
|
log.Error().Err(err).Msgf("error getting registry credentials for %s#%d", repo.FullName, currentPipeline.Number)
|
||||||
}
|
}
|
||||||
|
@ -55,8 +57,10 @@ func parsePipeline(store store.Store, currentPipeline *model.Pipeline, user *mod
|
||||||
if envs == nil {
|
if envs == nil {
|
||||||
envs = map[string]string{}
|
envs = map[string]string{}
|
||||||
}
|
}
|
||||||
if server.Config.Services.Environ != nil {
|
|
||||||
globals, _ := server.Config.Services.Environ.EnvironList(repo)
|
environmentService := server.Config.Services.Manager.EnvironmentService()
|
||||||
|
if environmentService != nil {
|
||||||
|
globals, _ := environmentService.EnvironList(repo)
|
||||||
for _, global := range globals {
|
for _, global := range globals {
|
||||||
envs[global.Name] = global.Value
|
envs[global.Name] = global.Value
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,15 +28,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Restart a pipeline by creating a new one out of the old and start it
|
// Restart a pipeline by creating a new one out of the old and start it
|
||||||
func Restart(ctx context.Context, store store.Store, lastPipeline *model.Pipeline, user *model.User, repo *model.Repo, envs map[string]string, netrc *model.Netrc) (*model.Pipeline, error) {
|
func Restart(ctx context.Context, store store.Store, lastPipeline *model.Pipeline, user *model.User, repo *model.Repo, envs map[string]string) (*model.Pipeline, error) {
|
||||||
|
forge := server.Config.Services.Forge
|
||||||
switch lastPipeline.Status {
|
switch lastPipeline.Status {
|
||||||
case model.StatusDeclined,
|
case model.StatusDeclined,
|
||||||
model.StatusBlocked:
|
model.StatusBlocked:
|
||||||
return nil, &ErrBadRequest{Msg: fmt.Sprintf("cannot restart a pipeline with status %s", lastPipeline.Status)}
|
return nil, &ErrBadRequest{Msg: fmt.Sprintf("cannot restart a pipeline with status %s", lastPipeline.Status)}
|
||||||
}
|
}
|
||||||
|
|
||||||
var pipelineFiles []*forge_types.FileMeta
|
|
||||||
|
|
||||||
// fetch the old pipeline config from the database
|
// fetch the old pipeline config from the database
|
||||||
configs, err := store.ConfigsForPipeline(lastPipeline.ID)
|
configs, err := store.ConfigsForPipeline(lastPipeline.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -44,25 +43,17 @@ func Restart(ctx context.Context, store store.Store, lastPipeline *model.Pipelin
|
||||||
return nil, &ErrNotFound{Msg: fmt.Sprintf("failure to get pipeline config for %s. %s", repo.FullName, err)}
|
return nil, &ErrNotFound{Msg: fmt.Sprintf("failure to get pipeline config for %s. %s", repo.FullName, err)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pipelineFiles []*forge_types.FileMeta
|
||||||
for _, y := range configs {
|
for _, y := range configs {
|
||||||
pipelineFiles = append(pipelineFiles, &forge_types.FileMeta{Data: y.Data, Name: y.Name})
|
pipelineFiles = append(pipelineFiles, &forge_types.FileMeta{Data: y.Data, Name: y.Name})
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the config extension is active we should refetch the config in case something changed
|
// If the config service is active we should refetch the config in case something changed
|
||||||
if server.Config.Services.ConfigService != nil {
|
configService := server.Config.Services.Manager.ConfigServiceFromRepo(repo)
|
||||||
currentFileMeta := make([]*forge_types.FileMeta, len(configs))
|
pipelineFiles, err = configService.Fetch(ctx, forge, user, repo, lastPipeline, pipelineFiles, true)
|
||||||
for i, cfg := range configs {
|
if err != nil {
|
||||||
currentFileMeta[i] = &forge_types.FileMeta{Name: cfg.Name, Data: cfg.Data}
|
return nil, &ErrBadRequest{
|
||||||
}
|
Msg: fmt.Sprintf("On fetching external pipeline config: %s", err),
|
||||||
|
|
||||||
newConfig, useOld, err := server.Config.Services.ConfigService.FetchConfig(ctx, repo, lastPipeline, currentFileMeta, netrc)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &ErrBadRequest{
|
|
||||||
Msg: fmt.Sprintf("On fetching external pipeline config: %s", err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !useOld {
|
|
||||||
pipelineFiles = newConfig
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package forge_test
|
package config_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/ed25519"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-ap/httpsig"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge"
|
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge/mocks"
|
"go.woodpecker-ci.org/woodpecker/v2/server/forge/mocks"
|
||||||
forge_types "go.woodpecker-ci.org/woodpecker/v2/server/forge/types"
|
forge_types "go.woodpecker-ci.org/woodpecker/v2/server/forge/types"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/plugins/config"
|
"go.woodpecker-ci.org/woodpecker/v2/server/services/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFetch(t *testing.T) {
|
func TestFetch(t *testing.T) {
|
||||||
|
@ -313,223 +305,18 @@ func TestFetch(t *testing.T) {
|
||||||
f.On("File", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("file not found"))
|
f.On("File", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("file not found"))
|
||||||
f.On("Dir", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("directory not found"))
|
f.On("Dir", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("directory not found"))
|
||||||
|
|
||||||
configFetcher := forge.NewConfigFetcher(
|
configFetcher := config.NewForge(
|
||||||
|
time.Second * 3,
|
||||||
|
)
|
||||||
|
files, err := configFetcher.Fetch(
|
||||||
|
context.Background(),
|
||||||
f,
|
f,
|
||||||
time.Second*3,
|
&model.User{Token: "xxx"},
|
||||||
|
repo,
|
||||||
|
&model.Pipeline{Commit: "89ab7b2d6bfb347144ac7c557e638ab402848fee"},
|
||||||
nil,
|
nil,
|
||||||
&model.User{Token: "xxx"},
|
false,
|
||||||
repo,
|
|
||||||
&model.Pipeline{Commit: "89ab7b2d6bfb347144ac7c557e638ab402848fee"},
|
|
||||||
)
|
)
|
||||||
files, err := configFetcher.Fetch(context.Background())
|
|
||||||
if tt.expectedError && err == nil {
|
|
||||||
t.Fatal("expected an error")
|
|
||||||
} else if !tt.expectedError && err != nil {
|
|
||||||
t.Fatal("error fetching config:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
matchingFiles := make([]string, len(files))
|
|
||||||
for i := range files {
|
|
||||||
matchingFiles[i] = files[i].Name
|
|
||||||
}
|
|
||||||
assert.ElementsMatch(t, tt.expectedFileNames, matchingFiles, "expected some other pipeline files")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFetchFromConfigService(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
type file struct {
|
|
||||||
name string
|
|
||||||
data []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
dummyData := []byte("TEST")
|
|
||||||
|
|
||||||
testTable := []struct {
|
|
||||||
name string
|
|
||||||
repoConfig string
|
|
||||||
files []file
|
|
||||||
expectedFileNames []string
|
|
||||||
expectedError bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "External Fetch empty repo",
|
|
||||||
repoConfig: "",
|
|
||||||
files: []file{},
|
|
||||||
expectedFileNames: []string{"override1", "override2", "override3"},
|
|
||||||
expectedError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Default config - Additional sub-folders",
|
|
||||||
repoConfig: "",
|
|
||||||
files: []file{{
|
|
||||||
name: ".woodpecker/test.yml",
|
|
||||||
data: dummyData,
|
|
||||||
}, {
|
|
||||||
name: ".woodpecker/sub-folder/config.yml",
|
|
||||||
data: dummyData,
|
|
||||||
}},
|
|
||||||
expectedFileNames: []string{"override1", "override2", "override3"},
|
|
||||||
expectedError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Fetch empty",
|
|
||||||
repoConfig: " ",
|
|
||||||
files: []file{{
|
|
||||||
name: ".woodpecker/.keep",
|
|
||||||
data: dummyData,
|
|
||||||
}, {
|
|
||||||
name: ".woodpecker.yml",
|
|
||||||
data: nil,
|
|
||||||
}, {
|
|
||||||
name: ".woodpecker.yaml",
|
|
||||||
data: dummyData,
|
|
||||||
}},
|
|
||||||
expectedFileNames: []string{},
|
|
||||||
expectedError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Use old config",
|
|
||||||
repoConfig: ".my-ci-folder/",
|
|
||||||
files: []file{{
|
|
||||||
name: ".woodpecker/test.yml",
|
|
||||||
data: dummyData,
|
|
||||||
}, {
|
|
||||||
name: ".woodpecker.yml",
|
|
||||||
data: dummyData,
|
|
||||||
}, {
|
|
||||||
name: ".woodpecker.yaml",
|
|
||||||
data: dummyData,
|
|
||||||
}, {
|
|
||||||
name: ".my-ci-folder/test.yml",
|
|
||||||
data: dummyData,
|
|
||||||
}},
|
|
||||||
expectedFileNames: []string{
|
|
||||||
".my-ci-folder/test.yml",
|
|
||||||
},
|
|
||||||
expectedError: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pubEd25519Key, privEd25519Key, err := ed25519.GenerateKey(rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("can't generate ed25519 key pair")
|
|
||||||
}
|
|
||||||
|
|
||||||
fixtureHandler := func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// check signature
|
|
||||||
pubKeyID := "woodpecker-ci-plugins"
|
|
||||||
|
|
||||||
keystore := httpsig.NewMemoryKeyStore()
|
|
||||||
keystore.SetKey(pubKeyID, pubEd25519Key)
|
|
||||||
|
|
||||||
verifier := httpsig.NewVerifier(keystore)
|
|
||||||
verifier.SetRequiredHeaders([]string{"(request-target)", "date"})
|
|
||||||
|
|
||||||
keyID, err := verifier.Verify(r)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Invalid signature", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if keyID != pubKeyID {
|
|
||||||
http.Error(w, "Used wrong key", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type config struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Data string `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type incoming struct {
|
|
||||||
Repo *model.Repo `json:"repo"`
|
|
||||||
Build *model.Pipeline `json:"pipeline"`
|
|
||||||
Configuration []*config `json:"config"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var req incoming
|
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "can't read body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(body, &req)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Failed to parse JSON"+err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Repo.Name == "Fetch empty" {
|
|
||||||
w.WriteHeader(404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Repo.Name == "Use old config" {
|
|
||||||
w.WriteHeader(204)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprint(w, `{
|
|
||||||
"configs": [
|
|
||||||
{
|
|
||||||
"name": "override1",
|
|
||||||
"data": "some new pipelineconfig \n pipe, pipe, pipe"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "override2",
|
|
||||||
"data": "some new pipelineconfig \n pipe, pipe, pipe"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "override3",
|
|
||||||
"data": "some new pipelineconfig \n pipe, pipe, pipe"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(fixtureHandler))
|
|
||||||
defer ts.Close()
|
|
||||||
configAPI := config.NewHTTP(ts.URL, privEd25519Key)
|
|
||||||
|
|
||||||
for _, tt := range testTable {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
repo := &model.Repo{Owner: "laszlocph", Name: tt.name, Config: tt.repoConfig} // Using test name as repo name to provide different responses in mock server
|
|
||||||
|
|
||||||
f := new(mocks.Forge)
|
|
||||||
dirs := map[string][]*forge_types.FileMeta{}
|
|
||||||
for _, file := range tt.files {
|
|
||||||
f.On("File", mock.Anything, mock.Anything, mock.Anything, mock.Anything, file.name).Return(file.data, nil)
|
|
||||||
path := filepath.Dir(file.name)
|
|
||||||
if path != "." {
|
|
||||||
dirs[path] = append(dirs[path], &forge_types.FileMeta{
|
|
||||||
Name: file.name,
|
|
||||||
Data: file.data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for path, files := range dirs {
|
|
||||||
f.On("Dir", mock.Anything, mock.Anything, mock.Anything, mock.Anything, path).Return(files, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the previous mocks do not match return not found errors
|
|
||||||
f.On("File", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("file not found"))
|
|
||||||
f.On("Dir", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("directory not found"))
|
|
||||||
|
|
||||||
f.On("Netrc", mock.Anything, mock.Anything).Return(&model.Netrc{Machine: "mock", Login: "mock", Password: "mock"}, nil)
|
|
||||||
|
|
||||||
configFetcher := forge.NewConfigFetcher(
|
|
||||||
f,
|
|
||||||
time.Second*3,
|
|
||||||
configAPI,
|
|
||||||
&model.User{Token: "xxx"},
|
|
||||||
repo,
|
|
||||||
&model.Pipeline{Commit: "89ab7b2d6bfb347144ac7c557e638ab402848fee"},
|
|
||||||
)
|
|
||||||
files, err := configFetcher.Fetch(context.Background())
|
|
||||||
if tt.expectedError && err == nil {
|
if tt.expectedError && err == nil {
|
||||||
t.Fatal("expected an error")
|
t.Fatal("expected an error")
|
||||||
} else if !tt.expectedError && err != nil {
|
} else if !tt.expectedError && err != nil {
|
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");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -17,10 +17,11 @@ package config
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
forge_types "go.woodpecker-ci.org/woodpecker/v2/server/forge/types"
|
"go.woodpecker-ci.org/woodpecker/v2/server/forge"
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v2/server/forge/types"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Extension interface {
|
type Service interface {
|
||||||
FetchConfig(ctx context.Context, repo *model.Repo, pipeline *model.Pipeline, currentFileMeta []*forge_types.FileMeta, netrc *model.Netrc) (configData []*forge_types.FileMeta, useOld bool, err error)
|
Fetch(ctx context.Context, forge forge.Forge, user *model.User, repo *model.Repo, pipeline *model.Pipeline, oldConfigData []*types.FileMeta, restart bool) (configData []*types.FileMeta, err error)
|
||||||
}
|
}
|
|
@ -21,7 +21,7 @@ import (
|
||||||
|
|
||||||
"github.com/google/tink/go/subtle/random"
|
"github.com/google/tink/go/subtle/random"
|
||||||
|
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
"go.woodpecker-ci.org/woodpecker/v2/server/services/encryption/types"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/store"
|
"go.woodpecker-ci.org/woodpecker/v2/server/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ type aesEncryptionService struct {
|
||||||
cipher cipher.AEAD
|
cipher cipher.AEAD
|
||||||
keyID string
|
keyID string
|
||||||
store store.Store
|
store store.Store
|
||||||
clients []model.EncryptionClient
|
clients []types.EncryptionClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *aesEncryptionService) Encrypt(plaintext, associatedData string) (string, error) {
|
func (svc *aesEncryptionService) Encrypt(plaintext, associatedData string) (string, error) {
|
|
@ -20,27 +20,27 @@ import (
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
"go.woodpecker-ci.org/woodpecker/v2/server/services/encryption/types"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/store"
|
"go.woodpecker-ci.org/woodpecker/v2/server/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
type aesConfiguration struct {
|
type aesConfiguration struct {
|
||||||
password string
|
password string
|
||||||
store store.Store
|
store store.Store
|
||||||
clients []model.EncryptionClient
|
clients []types.EncryptionClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAES(ctx *cli.Context, s store.Store) model.EncryptionServiceBuilder {
|
func newAES(ctx *cli.Context, s store.Store) types.EncryptionServiceBuilder {
|
||||||
key := ctx.String(rawKeyConfigFlag)
|
key := ctx.String(rawKeyConfigFlag)
|
||||||
return &aesConfiguration{key, s, nil}
|
return &aesConfiguration{key, s, nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c aesConfiguration) WithClients(clients []model.EncryptionClient) model.EncryptionServiceBuilder {
|
func (c aesConfiguration) WithClients(clients []types.EncryptionClient) types.EncryptionServiceBuilder {
|
||||||
c.clients = clients
|
c.clients = clients
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c aesConfiguration) Build() (model.EncryptionService, error) {
|
func (c aesConfiguration) Build() (types.EncryptionService, error) {
|
||||||
svc := &aesEncryptionService{
|
svc := &aesEncryptionService{
|
||||||
cipher: nil,
|
cipher: nil,
|
||||||
store: c.store,
|
store: c.store,
|
|
@ -64,9 +64,9 @@ const (
|
||||||
logMessageEncryptionDisabled = "encryption disabled"
|
logMessageEncryptionDisabled = "encryption disabled"
|
||||||
logMessageEncryptionKeyRegistered = "registered new encryption key"
|
logMessageEncryptionKeyRegistered = "registered new encryption key"
|
||||||
logMessageClientsInitialized = "initialized encryption on registered clients"
|
logMessageClientsInitialized = "initialized encryption on registered clients"
|
||||||
logMessageClientsEnabled = "enabled encryption on registered services"
|
logMessageClientsEnabled = "enabled encryption on registered service"
|
||||||
logMessageClientsRotated = "updated encryption key on registered services"
|
logMessageClientsRotated = "updated encryption key on registered service"
|
||||||
logMessageClientsDecrypted = "disabled encryption on registered services"
|
logMessageClientsDecrypted = "disabled encryption on registered service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// tink
|
// tink
|
|
@ -19,21 +19,21 @@ import (
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
"go.woodpecker-ci.org/woodpecker/v2/server/services/encryption/types"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/store"
|
"go.woodpecker-ci.org/woodpecker/v2/server/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
type builder struct {
|
type builder struct {
|
||||||
store store.Store
|
store store.Store
|
||||||
ctx *cli.Context
|
ctx *cli.Context
|
||||||
clients []model.EncryptionClient
|
clients []types.EncryptionClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func Encryption(ctx *cli.Context, s store.Store) model.EncryptionBuilder {
|
func Encryption(ctx *cli.Context, s store.Store) types.EncryptionBuilder {
|
||||||
return &builder{store: s, ctx: ctx}
|
return &builder{store: s, ctx: ctx}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b builder) WithClient(client model.EncryptionClient) model.EncryptionBuilder {
|
func (b builder) WithClient(client types.EncryptionClient) types.EncryptionBuilder {
|
||||||
b.clients = append(b.clients, client)
|
b.clients = append(b.clients, client)
|
||||||
return b
|
return b
|
||||||
}
|
}
|
|
@ -18,11 +18,11 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
"go.woodpecker-ci.org/woodpecker/v2/server/services/encryption/types"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/store/types"
|
storeTypes "go.woodpecker-ci.org/woodpecker/v2/server/store/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (b builder) getService(keyType string) (model.EncryptionService, error) {
|
func (b builder) getService(keyType string) (types.EncryptionService, error) {
|
||||||
if keyType == keyTypeNone {
|
if keyType == keyTypeNone {
|
||||||
return nil, errors.New(errMessageNoKeysProvided)
|
return nil, errors.New(errMessageNoKeysProvided)
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ func (b builder) getService(keyType string) (model.EncryptionService, error) {
|
||||||
|
|
||||||
func (b builder) isEnabled() (bool, error) {
|
func (b builder) isEnabled() (bool, error) {
|
||||||
_, err := b.store.ServerConfigGet(ciphertextSampleConfigKey)
|
_, err := b.store.ServerConfigGet(ciphertextSampleConfigKey)
|
||||||
if err != nil && !errors.Is(err, types.RecordNotExist) {
|
if err != nil && !errors.Is(err, storeTypes.RecordNotExist) {
|
||||||
return false, fmt.Errorf(errTemplateFailedLoadingServerConfig, err)
|
return false, fmt.Errorf(errTemplateFailedLoadingServerConfig, err)
|
||||||
}
|
}
|
||||||
return err == nil, nil
|
return err == nil, nil
|
||||||
|
@ -61,7 +61,7 @@ func (b builder) detectKeyType() (string, error) {
|
||||||
return keyTypeNone, nil
|
return keyTypeNone, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b builder) serviceBuilder(keyType string) (model.EncryptionServiceBuilder, error) {
|
func (b builder) serviceBuilder(keyType string) (types.EncryptionServiceBuilder, error) {
|
||||||
switch {
|
switch {
|
||||||
case keyType == keyTypeTink:
|
case keyType == keyTypeTink:
|
||||||
return newTink(b.ctx, b.store), nil
|
return newTink(b.ctx, b.store), nil
|
|
@ -14,18 +14,18 @@
|
||||||
|
|
||||||
package encryption
|
package encryption
|
||||||
|
|
||||||
import "go.woodpecker-ci.org/woodpecker/v2/server/model"
|
import "go.woodpecker-ci.org/woodpecker/v2/server/services/encryption/types"
|
||||||
|
|
||||||
type noEncryptionBuilder struct {
|
type noEncryptionBuilder struct {
|
||||||
clients []model.EncryptionClient
|
clients []types.EncryptionClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b noEncryptionBuilder) WithClients(clients []model.EncryptionClient) model.EncryptionServiceBuilder {
|
func (b noEncryptionBuilder) WithClients(clients []types.EncryptionClient) types.EncryptionServiceBuilder {
|
||||||
b.clients = clients
|
b.clients = clients
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b noEncryptionBuilder) Build() (model.EncryptionService, error) {
|
func (b noEncryptionBuilder) Build() (types.EncryptionService, error) {
|
||||||
svc := &noEncryption{}
|
svc := &noEncryption{}
|
||||||
for _, client := range b.clients {
|
for _, client := range b.clients {
|
||||||
err := client.SetEncryptionService(svc)
|
err := client.SetEncryptionService(svc)
|
|
@ -21,7 +21,7 @@ import (
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
"github.com/google/tink/go/tink"
|
"github.com/google/tink/go/tink"
|
||||||
|
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
"go.woodpecker-ci.org/woodpecker/v2/server/services/encryption/types"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/store"
|
"go.woodpecker-ci.org/woodpecker/v2/server/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ type tinkEncryptionService struct {
|
||||||
encryption tink.AEAD
|
encryption tink.AEAD
|
||||||
store store.Store
|
store store.Store
|
||||||
keysetFileWatcher *fsnotify.Watcher
|
keysetFileWatcher *fsnotify.Watcher
|
||||||
clients []model.EncryptionClient
|
clients []types.EncryptionClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *tinkEncryptionService) Encrypt(plaintext, associatedData string) (string, error) {
|
func (svc *tinkEncryptionService) Encrypt(plaintext, associatedData string) (string, error) {
|
|
@ -20,27 +20,27 @@ import (
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
"go.woodpecker-ci.org/woodpecker/v2/server/services/encryption/types"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/store"
|
"go.woodpecker-ci.org/woodpecker/v2/server/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
type tinkConfiguration struct {
|
type tinkConfiguration struct {
|
||||||
keysetFilePath string
|
keysetFilePath string
|
||||||
store store.Store
|
store store.Store
|
||||||
clients []model.EncryptionClient
|
clients []types.EncryptionClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTink(ctx *cli.Context, s store.Store) model.EncryptionServiceBuilder {
|
func newTink(ctx *cli.Context, s store.Store) types.EncryptionServiceBuilder {
|
||||||
filepath := ctx.String(tinkKeysetFilepathConfigFlag)
|
filepath := ctx.String(tinkKeysetFilepathConfigFlag)
|
||||||
return &tinkConfiguration{filepath, s, nil}
|
return &tinkConfiguration{filepath, s, nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c tinkConfiguration) WithClients(clients []model.EncryptionClient) model.EncryptionServiceBuilder {
|
func (c tinkConfiguration) WithClients(clients []types.EncryptionClient) types.EncryptionServiceBuilder {
|
||||||
c.clients = clients
|
c.clients = clients
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c tinkConfiguration) Build() (model.EncryptionService, error) {
|
func (c tinkConfiguration) Build() (types.EncryptionService, error) {
|
||||||
svc := &tinkEncryptionService{
|
svc := &tinkEncryptionService{
|
||||||
keysetFilePath: c.keysetFilePath,
|
keysetFilePath: c.keysetFilePath,
|
||||||
primaryKeyID: "",
|
primaryKeyID: "",
|
|
@ -12,7 +12,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package model
|
package types
|
||||||
|
|
||||||
// EncryptionBuilder is user API to obtain correctly configured encryption
|
// EncryptionBuilder is user API to obtain correctly configured encryption
|
||||||
type EncryptionBuilder interface {
|
type EncryptionBuilder interface {
|
|
@ -22,11 +22,12 @@ import (
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v2/server/services/encryption/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EncryptedSecretStore struct {
|
type EncryptedSecretStore struct {
|
||||||
store model.SecretStore
|
store model.SecretStore
|
||||||
encryption model.EncryptionService
|
encryption types.EncryptionService
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure wrapper match interface
|
// ensure wrapper match interface
|
||||||
|
@ -37,7 +38,7 @@ func NewSecretStore(secretStore model.SecretStore) *EncryptedSecretStore {
|
||||||
return &wrapper
|
return &wrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wrapper *EncryptedSecretStore) SetEncryptionService(service model.EncryptionService) error {
|
func (wrapper *EncryptedSecretStore) SetEncryptionService(service types.EncryptionService) error {
|
||||||
if wrapper.encryption != nil {
|
if wrapper.encryption != nil {
|
||||||
return errors.New(errMessageInitSeveralTimes)
|
return errors.New(errMessageInitSeveralTimes)
|
||||||
}
|
}
|
||||||
|
@ -63,7 +64,7 @@ func (wrapper *EncryptedSecretStore) EnableEncryption() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wrapper *EncryptedSecretStore) MigrateEncryption(newEncryptionService model.EncryptionService) error {
|
func (wrapper *EncryptedSecretStore) MigrateEncryption(newEncryptionService types.EncryptionService) error {
|
||||||
log.Warn().Msg(logMessageMigratingSecretsEncryption)
|
log.Warn().Msg(logMessageMigratingSecretsEncryption)
|
||||||
secrets, err := wrapper.store.SecretListAll()
|
secrets, err := wrapper.store.SecretListAll()
|
||||||
if err != nil {
|
if err != nil {
|
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
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package environments
|
package environment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -26,8 +26,8 @@ type builtin struct {
|
||||||
globals []*model.Environ
|
globals []*model.Environ
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse returns a model.EnvironService based on a string slice where key and value are separated by a ":" delimiter.
|
// Parse returns a Service based on a string slice where key and value are separated by a ":" delimiter.
|
||||||
func Parse(params []string) model.EnvironService {
|
func Parse(params []string) Service {
|
||||||
var globals []*model.Environ
|
var globals []*model.Environ
|
||||||
|
|
||||||
for _, item := range params {
|
for _, item := range params {
|
|
@ -1,4 +1,18 @@
|
||||||
package environments
|
// Copyright 2024 Woodpecker Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package environment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
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 {
|
type combined struct {
|
||||||
registries []model.ReadOnlyRegistryService
|
registries []ReadOnlyService
|
||||||
dbRegistry model.RegistryService
|
dbRegistry Service
|
||||||
}
|
}
|
||||||
|
|
||||||
func Combined(dbRegistry model.RegistryService, registries ...model.ReadOnlyRegistryService) model.RegistryService {
|
func NewCombined(dbRegistry Service, registries ...ReadOnlyService) Service {
|
||||||
registries = append(registries, dbRegistry)
|
registries = append(registries, dbRegistry)
|
||||||
return &combined{
|
return &combined{
|
||||||
registries: registries,
|
registries: registries,
|
||||||
|
@ -31,7 +31,7 @@ func Combined(dbRegistry model.RegistryService, registries ...model.ReadOnlyRegi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c combined) RegistryFind(repo *model.Repo, name string) (*model.Registry, error) {
|
func (c *combined) RegistryFind(repo *model.Repo, name string) (*model.Registry, error) {
|
||||||
for _, registry := range c.registries {
|
for _, registry := range c.registries {
|
||||||
res, err := registry.RegistryFind(repo, name)
|
res, err := registry.RegistryFind(repo, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -44,7 +44,7 @@ func (c combined) RegistryFind(repo *model.Repo, name string) (*model.Registry,
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c combined) RegistryList(repo *model.Repo, p *model.ListOptions) ([]*model.Registry, error) {
|
func (c *combined) RegistryList(repo *model.Repo, p *model.ListOptions) ([]*model.Registry, error) {
|
||||||
var registries []*model.Registry
|
var registries []*model.Registry
|
||||||
for _, registry := range c.registries {
|
for _, registry := range c.registries {
|
||||||
list, err := registry.RegistryList(repo, &model.ListOptions{All: true})
|
list, err := registry.RegistryList(repo, &model.ListOptions{All: true})
|
||||||
|
@ -56,14 +56,14 @@ func (c combined) RegistryList(repo *model.Repo, p *model.ListOptions) ([]*model
|
||||||
return model.ApplyPagination(p, registries), nil
|
return model.ApplyPagination(p, registries), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c combined) RegistryCreate(repo *model.Repo, registry *model.Registry) error {
|
func (c *combined) RegistryCreate(repo *model.Repo, registry *model.Registry) error {
|
||||||
return c.dbRegistry.RegistryCreate(repo, registry)
|
return c.dbRegistry.RegistryCreate(repo, registry)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c combined) RegistryUpdate(repo *model.Repo, registry *model.Registry) error {
|
func (c *combined) RegistryUpdate(repo *model.Repo, registry *model.Registry) error {
|
||||||
return c.dbRegistry.RegistryUpdate(repo, registry)
|
return c.dbRegistry.RegistryUpdate(repo, registry)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c combined) RegistryDelete(repo *model.Repo, name string) error {
|
func (c *combined) RegistryDelete(repo *model.Repo, name string) error {
|
||||||
return c.dbRegistry.RegistryDelete(repo, name)
|
return c.dbRegistry.RegistryDelete(repo, name)
|
||||||
}
|
}
|
|
@ -23,26 +23,26 @@ type db struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new local registry service.
|
// New returns a new local registry service.
|
||||||
func New(store model.RegistryStore) model.RegistryService {
|
func NewDB(store model.RegistryStore) Service {
|
||||||
return &db{store}
|
return &db{store}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *db) RegistryFind(repo *model.Repo, name string) (*model.Registry, error) {
|
func (d *db) RegistryFind(repo *model.Repo, name string) (*model.Registry, error) {
|
||||||
return b.store.RegistryFind(repo, name)
|
return d.store.RegistryFind(repo, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *db) RegistryList(repo *model.Repo, p *model.ListOptions) ([]*model.Registry, error) {
|
func (d *db) RegistryList(repo *model.Repo, p *model.ListOptions) ([]*model.Registry, error) {
|
||||||
return b.store.RegistryList(repo, p)
|
return d.store.RegistryList(repo, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *db) RegistryCreate(_ *model.Repo, in *model.Registry) error {
|
func (d *db) RegistryCreate(_ *model.Repo, in *model.Registry) error {
|
||||||
return b.store.RegistryCreate(in)
|
return d.store.RegistryCreate(in)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *db) RegistryUpdate(_ *model.Repo, in *model.Registry) error {
|
func (d *db) RegistryUpdate(_ *model.Repo, in *model.Registry) error {
|
||||||
return b.store.RegistryUpdate(in)
|
return d.store.RegistryUpdate(in)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *db) RegistryDelete(repo *model.Repo, addr string) error {
|
func (d *db) RegistryDelete(repo *model.Repo, addr string) error {
|
||||||
return b.store.RegistryDelete(repo, addr)
|
return d.store.RegistryDelete(repo, addr)
|
||||||
}
|
}
|
|
@ -31,7 +31,7 @@ type filesystem struct {
|
||||||
path string
|
path string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Filesystem(path string) model.ReadOnlyRegistryService {
|
func NewFilesystem(path string) ReadOnlyService {
|
||||||
return &filesystem{path}
|
return &filesystem{path}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,12 +85,12 @@ func parseDockerConfig(path string) ([]*model.Registry, error) {
|
||||||
return auths, nil
|
return auths, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *filesystem) RegistryFind(*model.Repo, string) (*model.Registry, error) {
|
func (f *filesystem) RegistryFind(*model.Repo, string) (*model.Registry, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *filesystem) RegistryList(_ *model.Repo, p *model.ListOptions) ([]*model.Registry, error) {
|
func (f *filesystem) RegistryList(_ *model.Repo, p *model.ListOptions) ([]*model.Registry, error) {
|
||||||
regs, err := parseDockerConfig(b.path)
|
regs, err := parseDockerConfig(f.path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
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
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package secrets_test
|
package secret_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/franela/goblin"
|
"github.com/franela/goblin"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/plugins/secrets"
|
"go.woodpecker-ci.org/woodpecker/v2/server/services/secret"
|
||||||
mocks_store "go.woodpecker-ci.org/woodpecker/v2/server/store/mocks"
|
mocks_store "go.woodpecker-ci.org/woodpecker/v2/server/store/mocks"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSecretListPipeline(t *testing.T) {
|
func TestSecretListPipeline(t *testing.T) {
|
||||||
g := goblin.Goblin(t)
|
g := goblin.Goblin(t)
|
||||||
ctx := context.Background()
|
|
||||||
mockStore := mocks_store.NewStore(t)
|
mockStore := mocks_store.NewStore(t)
|
||||||
|
|
||||||
// global secret
|
// global secret
|
||||||
|
@ -66,7 +64,7 @@ func TestSecretListPipeline(t *testing.T) {
|
||||||
repoSecret,
|
repoSecret,
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
s, err := secrets.New(ctx, mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{}, &model.ListOptions{})
|
s, err := secret.NewDB(mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{}, &model.ListOptions{})
|
||||||
g.Assert(err).IsNil()
|
g.Assert(err).IsNil()
|
||||||
|
|
||||||
g.Assert(len(s)).Equal(1)
|
g.Assert(len(s)).Equal(1)
|
||||||
|
@ -79,7 +77,7 @@ func TestSecretListPipeline(t *testing.T) {
|
||||||
orgSecret,
|
orgSecret,
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
s, err := secrets.New(ctx, mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{}, &model.ListOptions{})
|
s, err := secret.NewDB(mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{}, &model.ListOptions{})
|
||||||
g.Assert(err).IsNil()
|
g.Assert(err).IsNil()
|
||||||
|
|
||||||
g.Assert(len(s)).Equal(1)
|
g.Assert(len(s)).Equal(1)
|
||||||
|
@ -91,7 +89,7 @@ func TestSecretListPipeline(t *testing.T) {
|
||||||
globalSecret,
|
globalSecret,
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
s, err := secrets.New(ctx, mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{}, &model.ListOptions{})
|
s, err := secret.NewDB(mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{}, &model.ListOptions{})
|
||||||
g.Assert(err).IsNil()
|
g.Assert(err).IsNil()
|
||||||
|
|
||||||
g.Assert(len(s)).Equal(1)
|
g.Assert(len(s)).Equal(1)
|
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/go-ap/httpsig"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/server/plugins/utils"
|
"go.woodpecker-ci.org/woodpecker/v2/server/services/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSign(t *testing.T) {
|
func TestSign(t *testing.T) {
|
|
@ -3,9 +3,5 @@ package types
|
||||||
type Type string
|
type Type string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TypeForge Type = "forge"
|
TypeForge Type = "forge"
|
||||||
TypeConfigService Type = "config_service"
|
|
||||||
TypeSecretService Type = "secret_service"
|
|
||||||
TypeEnvironmentService Type = "environment_service"
|
|
||||||
TypeRegistryService Type = "registry_service"
|
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue