Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions cmd/server/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,11 +284,21 @@ var flags = append([]cli.Flag{
Name: "config-extension-exclusive",
Usage: "whether global configuration service endpoint should be exclusive (skip forge)",
},
&cli.BoolFlag{
Sources: cli.EnvVars("WOODPECKER_CONFIG_EXTENSION_NETRC"),
Name: "config-extension-netrc",
Usage: "whether global configuration extension should receive netrc data",
},
&cli.StringFlag{
Sources: cli.EnvVars("WOODPECKER_REGISTRY_EXTENSION_ENDPOINT"),
Name: "registry-extension-endpoint",
Usage: "url used for calling registry service endpoint",
},
&cli.BoolFlag{
Sources: cli.EnvVars("WOODPECKER_REGISTRY_EXTENSION_NETRC"),
Name: "registry-extension-netrc",
Usage: "whether global registry extension should receive netrc data",
},
&cli.StringFlag{
Sources: cli.EnvVars("WOODPECKER_SECRET_EXTENSION_ENDPOINT"),
Name: "secret-extension-endpoint",
Expand Down
18 changes: 18 additions & 0 deletions cmd/server/openapi/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -5250,6 +5250,9 @@ const docTemplate = `{
"config_extension_exclusive": {
"type": "boolean"
},
"config_extension_netrc": {
"type": "boolean"
},
"config_file": {
"type": "string"
},
Expand Down Expand Up @@ -5296,6 +5299,9 @@ const docTemplate = `{
"registry_extension_endpoint": {
"type": "string"
},
"registry_extension_netrc": {
"type": "boolean"
},
"require_approval": {
"$ref": "#/definitions/model.ApprovalMode"
},
Expand Down Expand Up @@ -5355,6 +5361,9 @@ const docTemplate = `{
"config_extension_exclusive": {
"type": "boolean"
},
"config_extension_netrc": {
"type": "boolean"
},
"config_file": {
"type": "string"
},
Expand Down Expand Up @@ -5404,6 +5413,9 @@ const docTemplate = `{
"registry_extension_endpoint": {
"type": "string"
},
"registry_extension_netrc": {
"type": "boolean"
},
"require_approval": {
"$ref": "#/definitions/model.ApprovalMode"
},
Expand Down Expand Up @@ -5451,6 +5463,9 @@ const docTemplate = `{
"config_extension_exclusive": {
"type": "boolean"
},
"config_extension_netrc": {
"type": "boolean"
},
"config_file": {
"type": "string"
},
Expand All @@ -5463,6 +5478,9 @@ const docTemplate = `{
"registry_extension_endpoint": {
"type": "string"
},
"registry_extension_netrc": {
"type": "boolean"
},
"require_approval": {
"type": "string"
},
Expand Down
15 changes: 12 additions & 3 deletions docs/docs/20-usage/72-extensions/40-configuration-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,16 @@ You can enable the exclusive setting (both globally and on a per-repo level). Th

The extension receives an HTTP POST request with the following JSON payload:

:::info
The `netrc` field is only included in the request when the global `WOODPECKER_CONFIG_EXTENSION_NETRC` is set to `true` (default: `false`) or the per-repo "Send netrc credentials" is checked.
:::

```ts
class Request {
repo: Repo;
pipeline: Pipeline;
netrc: Netrc;
configuration: {
netrc?: Netrc; // only included when netrc sending is enabled (see above)
configuration?: {
// list of configurations. Not send if there was none.
name: string; // filename of the configuration file
data: string; // content of the configuration file
Expand Down Expand Up @@ -131,7 +135,12 @@ Example request:
"name": ".woodpecker.yaml",
"data": "steps:\n - name: backend\n image: alpine\n commands:\n - echo \"Hello there from Repo (.woodpecker.yaml)\"\n"
}
]
],
"netrc": {
"machine": "myforge.com",
"login": "myUser",
"password": "forge-access-token"
}
}
```

Expand Down
15 changes: 15 additions & 0 deletions docs/docs/20-usage/72-extensions/50-registry-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,27 @@ When a pipeline is triggered, Woodpecker will fetch the credentials from your se

The extension receives an HTTP POST request with the following JSON payload:

:::info
The `netrc` field is only included in the request when the global `WOODPECKER_REGISTRY_EXTENSION_NETRC` is set to `true` (default: `false`) or the per-repo "Send netrc credentials" is checked.
:::

```ts
class Request {
repo: Repo;
pipeline: Pipeline;
netrc?: Netrc; // only included when netrc sending is enabled (see above)
}
```

Checkout the following models for more information:

- [repo model](https://github.com/woodpecker-ci/woodpecker/blob/main/server/model/repo.go)
- [pipeline model](https://github.com/woodpecker-ci/woodpecker/blob/main/server/model/pipeline.go)
- [netrc model](https://github.com/woodpecker-ci/woodpecker/blob/main/server/model/netrc.go)

:::tip
The `netrc` data is pretty powerful as it contains credentials to access the repository. You can use this to clone the repository or even use the forge (Github or Gitlab, ...) API to get more information about the repository.
:::

Example request:

Expand Down Expand Up @@ -111,6 +121,11 @@ Example request:
"title": "",
"updated_at": 0,
"verified": false
},
"netrc": {
"machine": "myforge.com",
"login": "myUser",
"password": "forge-access-token"
}
}
```
Expand Down
26 changes: 26 additions & 0 deletions docs/docs/30-administration/10-configuration/10-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,19 @@ If you enable this, all repos will exclusively use the global config service end

---

### CONFIG_EXTENSION_NETRC

- Name: `WOODPECKER_CONFIG_EXTENSION_NETRC`
- Default: false

Send `netrc` to the config extension endpoint.

:::warning
The `netrc` data is pretty powerful as it contains credentials to access the repository. You can use this to clone the repository or even use the forge API to get more information about the repository.
:::

---

### SECRET_EXTENSION_ENDPOINT

- Name: `WOODPECKER_SECRET_EXTENSION_ENDPOINT`
Expand Down Expand Up @@ -1024,6 +1037,19 @@ Specify a registry extension endpoint, see [Registry Extension](../../20-usage/7

---

### REGISTRY_EXTENSION_NETRC

- Name: `WOODPECKER_REGISTRY_EXTENSION_NETRC`
- Default: false

Send `netrc` to the registry extension endpoint.

:::warning
The `netrc` data is pretty powerful as it contains credentials to access the repository. You can use this to clone the repository or even use the forge API to get more information about the repository.
:::

---

### EXTENSIONS_ALLOWED_HOSTS

- Name: `WOODPECKER_EXTENSIONS_ALLOWED_HOSTS`
Expand Down
4 changes: 2 additions & 2 deletions server/api/hook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ func TestHook(t *testing.T) {
_forge.On("Netrc", mock.Anything, mock.Anything).Return(&model.Netrc{}, nil)
_store.On("GetPipelineLastBefore", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
_manager.On("SecretServiceFromRepo", repo).Return(_secretService)
_secretService.On("SecretListPipeline", mock.Anything, repo, mock.Anything, mock.Anything).Return(nil, nil)
_secretService.On("SecretListPipeline", mock.Anything, repo, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
_manager.On("RegistryServiceFromRepo", repo).Return(_registryService)
_registryService.On("RegistryListPipeline", mock.Anything, repo, mock.Anything).Return(nil, nil)
_registryService.On("RegistryListPipeline", mock.Anything, repo, mock.Anything, mock.Anything).Return(nil, nil)
_manager.On("EnvironmentService").Return(nil)
_store.On("DeletePipeline", mock.Anything).Return(nil)

Expand Down
8 changes: 4 additions & 4 deletions server/api/pipeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,8 @@ func TestCreatePipeline(t *testing.T) {
}, nil).Maybe()
mockForge.On("Status", mock.Anything, fakeUser, fakeRepo, mock.Anything, mock.Anything).Return(nil).Maybe()

mockSecretService.On("SecretListPipeline", mock.Anything, fakeRepo, mock.Anything, mock.Anything).Return([]*model.Secret{}, nil).Maybe()
mockRegistryService.On("RegistryListPipeline", mock.Anything, fakeRepo, mock.Anything).Return([]*model.Registry{}, nil).Maybe()
mockSecretService.On("SecretListPipeline", mock.Anything, fakeRepo, mock.Anything, mock.Anything, mock.Anything).Return([]*model.Secret{}, nil).Maybe()
mockRegistryService.On("RegistryListPipeline", mock.Anything, fakeRepo, mock.Anything, mock.Anything).Return([]*model.Registry{}, nil).Maybe()

mockManager := manager_mocks.NewMockManager(t)
mockManager.On("ForgeFromRepo", fakeRepo).Return(mockForge, nil)
Expand Down Expand Up @@ -377,8 +377,8 @@ func TestCreatePipeline(t *testing.T) {
}, nil).Maybe()

mockForge.On("Status", mock.Anything, fakeUser, fakeRepo, mock.Anything, mock.Anything).Return(nil).Maybe()
mockSecretService.On("SecretListPipeline", mock.Anything, fakeRepo, mock.Anything, mock.Anything).Return([]*model.Secret{}, nil).Maybe()
mockRegistryService.On("RegistryListPipeline", mock.Anything, fakeRepo, mock.Anything).Return([]*model.Registry{}, nil).Maybe()
mockSecretService.On("SecretListPipeline", mock.Anything, fakeRepo, mock.Anything, mock.Anything, mock.Anything).Return([]*model.Secret{}, nil).Maybe()
mockRegistryService.On("RegistryListPipeline", mock.Anything, fakeRepo, mock.Anything, mock.Anything).Return([]*model.Registry{}, nil).Maybe()

mockManager := manager_mocks.NewMockManager(t)
mockManager.On("ForgeFromRepo", fakeRepo).Return(mockForge, nil)
Expand Down
6 changes: 6 additions & 0 deletions server/api/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,15 @@ func PatchRepo(c *gin.Context) {
if in.ConfigExtensionExclusive != nil {
repo.ConfigExtensionExclusive = *in.ConfigExtensionExclusive
}
if in.ConfigExtensionNetrc != nil {
repo.ConfigExtensionNetrc = *in.ConfigExtensionNetrc
}
if in.RegistryExtensionEndpoint != nil {
repo.RegistryExtensionEndpoint = *in.RegistryExtensionEndpoint
}
if in.RegistryExtensionNetrc != nil {
repo.RegistryExtensionNetrc = *in.RegistryExtensionNetrc
}
if in.SecretExtensionEndpoint != nil {
repo.SecretExtensionEndpoint = *in.SecretExtensionEndpoint
}
Expand Down
4 changes: 4 additions & 0 deletions server/model/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ type Repo struct {
NetrcTrustedPlugins []string `json:"netrc_trusted" xorm:"json 'netrc_trusted'"`
ConfigExtensionEndpoint string `json:"config_extension_endpoint" xorm:"varchar(500) 'config_extension_endpoint'"`
ConfigExtensionExclusive bool `json:"config_extension_exclusive" xorm:"DEFAULT FALSE 'config_extension_exclusive'"`
ConfigExtensionNetrc bool `json:"config_extension_netrc" xorm:"DEFAULT FALSE 'config_extension_netrc'"`
RegistryExtensionEndpoint string `json:"registry_extension_endpoint" xorm:"varchar(500) 'registry_extension_endpoint'"`
RegistryExtensionNetrc bool `json:"registry_extension_netrc" xorm:"DEFAULT FALSE 'registry_extension_netrc'"`
SecretExtensionEndpoint string `json:"secret_extension_endpoint" xorm:"varchar(500) 'secret_extension_endpoint'"`
SecretExtensionNetrc bool `json:"secret_extension_netrc" xorm:"DEFAULT FALSE 'secret_extension_netrc'"`
} // @name Repo
Expand Down Expand Up @@ -149,7 +151,9 @@ type RepoPatch struct {
Trusted *TrustedConfigurationPatch `json:"trusted"`
ConfigExtensionEndpoint *string `json:"config_extension_endpoint,omitempty"`
ConfigExtensionExclusive *bool `json:"config_extension_exclusive"`
ConfigExtensionNetrc *bool `json:"config_extension_netrc"`
RegistryExtensionEndpoint *string `json:"registry_extension_endpoint,omitempty"`
RegistryExtensionNetrc *bool `json:"registry_extension_netrc"`
SecretExtensionEndpoint *string `json:"secret_extension_endpoint,omitempty"`
SecretExtensionNetrc *bool `json:"secret_extension_netrc,omitempty"`
} // @name RepoPatch
Expand Down
2 changes: 1 addition & 1 deletion server/pipeline/items.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func parsePipeline(ctx context.Context, forge forge.Forge, store store.Store, cu
}

registryService := server.Config.Services.Manager.RegistryServiceFromRepo(repo)
regs, err := registryService.RegistryListPipeline(ctx, repo, currentPipeline)
regs, err := registryService.RegistryListPipeline(ctx, repo, currentPipeline, netrc)
if err != nil {
log.Error().Err(err).Msgf("error getting registry credentials for %s#%d", repo.FullName, currentPipeline.Number)
}
Expand Down
4 changes: 2 additions & 2 deletions server/pipeline/items_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ steps:
server.Config.Services.Manager = mockManager

secretService := secret_service_mocks.NewMockService(t)
secretService.On("SecretListPipeline", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]*model.Secret{
secretService.On("SecretListPipeline", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]*model.Secret{
{
Name: "hello",
Value: "secret world",
Expand All @@ -141,7 +141,7 @@ steps:
mockManager.On("SecretServiceFromRepo", mock.Anything).Return(secretService, nil)

registryService := registry_service_mocks.NewMockService(t)
registryService.On("RegistryListPipeline", mock.Anything, mock.Anything, mock.Anything).Return([]*model.Registry{
registryService.On("RegistryListPipeline", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]*model.Registry{
{
Address: "docker.io",
Username: "user",
Expand Down
2 changes: 1 addition & 1 deletion server/services/config/combined_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ func TestFetchFromConfigService(t *testing.T) {
client, err := utils.NewHTTPClient(privEd25519Key, "loopback")
require.NoError(t, err)

httpFetcher := config.NewHTTP(ts.URL+"/", client)
httpFetcher := config.NewHTTP(ts.URL+"/", client, true)

for _, tt := range testTable {
t.Run(tt.name, func(t *testing.T) {
Expand Down
23 changes: 13 additions & 10 deletions server/services/config/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ import (
)

type httpService struct {
endpoint string
client *utils.Client
endpoint string
client *utils.Client
includeNetrc bool
}

// configData same as forge.FileMeta but with json tags and string data.
Expand All @@ -49,16 +50,11 @@ type responseStructure struct {
Configs []*configData `json:"configs"`
}

func NewHTTP(endpoint string, client *utils.Client) Service {
return &httpService{endpoint, client}
func NewHTTP(endpoint string, client *utils.Client, includeNetrc bool) Service {
return &httpService{endpoint, client, includeNetrc}
}

func (h *httpService) Fetch(ctx context.Context, forge forge.Forge, user *model.User, repo *model.Repo, pipeline *model.Pipeline, oldConfigData []*types.FileMeta, _ bool) ([]*types.FileMeta, error) {
netrc, err := forge.Netrc(user, repo)
if err != nil {
return nil, fmt.Errorf("could not get Netrc data from forge: %w", err)
}

configuration := make([]*configData, len(oldConfigData))
for i, oldConfig := range oldConfigData {
configuration[i] = &configData{Name: oldConfig.Name, Data: string(oldConfig.Data)}
Expand All @@ -68,10 +64,17 @@ func (h *httpService) Fetch(ctx context.Context, forge forge.Forge, user *model.
body := requestStructure{
Repo: repo,
Pipeline: pipeline,
Netrc: netrc,
Configuration: configuration,
}

if h.includeNetrc {
netrc, err := forge.Netrc(user, repo)
if err != nil {
return nil, fmt.Errorf("could not get Netrc data from forge: %w", err)
}
body.Netrc = netrc
}

status, err := h.client.Send(ctx, http.MethodPost, h.endpoint, body, response)
if err != nil && status != http.StatusNoContent {
return nil, fmt.Errorf("failed to fetch config via http (status: %d): %w", status, err)
Expand Down
8 changes: 4 additions & 4 deletions server/services/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func NewManager(c *cli.Command, store store.Store, setupForge SetupForge) (Manag
signaturePublicKey: signaturePublicKey,
store: store,
secret: setupSecretService(store, c.String("secret-extension-endpoint"), client, c.Bool("secret-extension-netrc")),
registry: setupRegistryService(store, c.String("docker-config"), c.String("registry-extension-endpoint"), client),
registry: setupRegistryService(store, c.String("docker-config"), c.String("registry-extension-endpoint"), c.Bool("registry-extension-netrc"), client),
config: configService,
environment: environment.Parse(c.StringSlice("environment")),
forgeCache: ttlcache.New(ttlcache.WithDisableTouchOnHit[int64, forge.Forge]()),
Expand All @@ -115,7 +115,7 @@ func (m *manager) SecretService() secret.Service {

func (m *manager) RegistryServiceFromRepo(repo *model.Repo) registry.Service {
if repo.RegistryExtensionEndpoint != "" {
return registry.NewWithExtension(m.registry, registry.NewHTTP(strings.TrimRight(repo.RegistryExtensionEndpoint, "/"), m.client))
return registry.NewWithExtension(m.registry, registry.NewHTTP(strings.TrimRight(repo.RegistryExtensionEndpoint, "/"), m.client, repo.RegistryExtensionNetrc))
}
return m.RegistryService()
}
Expand All @@ -127,9 +127,9 @@ func (m *manager) RegistryService() registry.Service {
func (m *manager) ConfigServiceFromRepo(repo *model.Repo) config.Service {
if repo.ConfigExtensionEndpoint != "" {
if repo.ConfigExtensionExclusive {
return config.NewHTTP(strings.TrimRight(repo.ConfigExtensionEndpoint, "/"), m.client)
return config.NewHTTP(strings.TrimRight(repo.ConfigExtensionEndpoint, "/"), m.client, repo.ConfigExtensionNetrc)
}
return config.NewCombined(m.config, config.NewHTTP(strings.TrimRight(repo.ConfigExtensionEndpoint, "/"), m.client))
return config.NewCombined(m.config, config.NewHTTP(strings.TrimRight(repo.ConfigExtensionEndpoint, "/"), m.client, repo.ConfigExtensionNetrc))
}

return m.config
Expand Down
4 changes: 2 additions & 2 deletions server/services/registry/combined.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ func (c *combined) RegistryList(repo *model.Repo, p *model.ListOptions) ([]*mode
return c.dbRegistry.RegistryList(repo, p)
}

func (c *combined) RegistryListPipeline(ctx context.Context, repo *model.Repo, pipeline *model.Pipeline) ([]*model.Registry, error) {
dbRegistries, err := c.dbRegistry.RegistryListPipeline(ctx, repo, pipeline)
func (c *combined) RegistryListPipeline(ctx context.Context, repo *model.Repo, pipeline *model.Pipeline, netrc *model.Netrc) ([]*model.Registry, error) {
dbRegistries, err := c.dbRegistry.RegistryListPipeline(ctx, repo, pipeline, netrc)
if err != nil {
return nil, err
}
Expand Down
1 change: 1 addition & 0 deletions server/services/registry/combined_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func TestCombinedRegistryListPipeline(t *testing.T) {
t.Context(),
&model.Repo{ID: 1, Name: tt.repoName},
&model.Pipeline{},
nil,
)
if tt.expectedError {
require.Error(t, err, "expected an error")
Expand Down
Loading