From ea51f80ffaba8355f11e5cdc18d27d82fbd25cd8 Mon Sep 17 00:00:00 2001 From: qwerty287 Date: Fri, 27 Mar 2026 15:31:07 +0100 Subject: [PATCH 1/3] Add netrc option for config/registry extension --- cmd/server/flags.go | 10 +++++++ cmd/server/openapi/docs.go | 18 +++++++++++ .../40-configuration-extension.md | 15 ++++++++-- .../72-extensions/50-registry-extension.md | 15 ++++++++++ .../10-configuration/10-server.md | 26 ++++++++++++++++ server/api/hook_test.go | 4 +-- server/api/pipeline_test.go | 8 ++--- server/api/repo.go | 6 ++++ server/model/repo.go | 4 +++ server/pipeline/items.go | 2 +- server/pipeline/items_test.go | 4 +-- server/services/config/combined_test.go | 2 +- server/services/config/http.go | 23 +++++++------- server/services/manager.go | 8 ++--- server/services/registry/combined.go | 4 +-- server/services/registry/combined_test.go | 1 + server/services/registry/db.go | 2 +- server/services/registry/http.go | 15 ++++++---- .../services/registry/mocks/mock_Service.go | 30 +++++++++++-------- server/services/registry/service.go | 2 +- server/services/registry/with_extension.go | 6 ++-- .../services/registry/with_extension_test.go | 3 +- server/services/secret/combined_test.go | 2 +- server/services/setup.go | 6 ++-- web/src/assets/locales/en.json | 4 +-- web/src/lib/api/types/repo.ts | 8 +++++ web/src/views/repo/settings/Extensions.vue | 20 +++++++++++-- 27 files changed, 188 insertions(+), 60 deletions(-) diff --git a/cmd/server/flags.go b/cmd/server/flags.go index e0594247898..e85f9a99b54 100644 --- a/cmd/server/flags.go +++ b/cmd/server/flags.go @@ -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", diff --git a/cmd/server/openapi/docs.go b/cmd/server/openapi/docs.go index 8bf2e041010..bb4c0a76b39 100644 --- a/cmd/server/openapi/docs.go +++ b/cmd/server/openapi/docs.go @@ -5243,6 +5243,9 @@ const docTemplate = `{ "config_extension_exclusive": { "type": "boolean" }, + "config_extension_netrc": { + "type": "boolean" + }, "config_file": { "type": "string" }, @@ -5289,6 +5292,9 @@ const docTemplate = `{ "registry_extension_endpoint": { "type": "string" }, + "registry_extension_netrc": { + "type": "boolean" + }, "require_approval": { "$ref": "#/definitions/model.ApprovalMode" }, @@ -5348,6 +5354,9 @@ const docTemplate = `{ "config_extension_exclusive": { "type": "boolean" }, + "config_extension_netrc": { + "type": "boolean" + }, "config_file": { "type": "string" }, @@ -5397,6 +5406,9 @@ const docTemplate = `{ "registry_extension_endpoint": { "type": "string" }, + "registry_extension_netrc": { + "type": "boolean" + }, "require_approval": { "$ref": "#/definitions/model.ApprovalMode" }, @@ -5444,6 +5456,9 @@ const docTemplate = `{ "config_extension_exclusive": { "type": "boolean" }, + "config_extension_netrc": { + "type": "boolean" + }, "config_file": { "type": "string" }, @@ -5456,6 +5471,9 @@ const docTemplate = `{ "registry_extension_endpoint": { "type": "string" }, + "registry_extension_netrc": { + "type": "boolean" + }, "require_approval": { "type": "string" }, diff --git a/docs/docs/20-usage/72-extensions/40-configuration-extension.md b/docs/docs/20-usage/72-extensions/40-configuration-extension.md index 13125743bfa..f263a0693c8 100644 --- a/docs/docs/20-usage/72-extensions/40-configuration-extension.md +++ b/docs/docs/20-usage/72-extensions/40-configuration-extension.md @@ -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 @@ -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" + } } ``` diff --git a/docs/docs/20-usage/72-extensions/50-registry-extension.md b/docs/docs/20-usage/72-extensions/50-registry-extension.md index dd9d0831b6a..c4f46032786 100644 --- a/docs/docs/20-usage/72-extensions/50-registry-extension.md +++ b/docs/docs/20-usage/72-extensions/50-registry-extension.md @@ -33,10 +33,15 @@ 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) } ``` @@ -44,6 +49,11 @@ 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: @@ -111,6 +121,11 @@ Example request: "title": "", "updated_at": 0, "verified": false + }, + "netrc": { + "machine": "myforge.com", + "login": "myUser", + "password": "forge-access-token" } } ``` diff --git a/docs/docs/30-administration/10-configuration/10-server.md b/docs/docs/30-administration/10-configuration/10-server.md index c18a2836e2a..3318f7c95f3 100644 --- a/docs/docs/30-administration/10-configuration/10-server.md +++ b/docs/docs/30-administration/10-configuration/10-server.md @@ -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` @@ -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` diff --git a/server/api/hook_test.go b/server/api/hook_test.go index dafabee7ffc..18b79c87d37 100644 --- a/server/api/hook_test.go +++ b/server/api/hook_test.go @@ -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) diff --git a/server/api/pipeline_test.go b/server/api/pipeline_test.go index 5c4d9fad48d..a49f061ad98 100644 --- a/server/api/pipeline_test.go +++ b/server/api/pipeline_test.go @@ -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) @@ -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) diff --git a/server/api/repo.go b/server/api/repo.go index d1cf3d2e75d..3a6b89e23eb 100644 --- a/server/api/repo.go +++ b/server/api/repo.go @@ -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 } diff --git a/server/model/repo.go b/server/model/repo.go index 1c9672582c8..c037050c5f8 100644 --- a/server/model/repo.go +++ b/server/model/repo.go @@ -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 @@ -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 diff --git a/server/pipeline/items.go b/server/pipeline/items.go index 7791541ecf5..3a801dd29b1 100644 --- a/server/pipeline/items.go +++ b/server/pipeline/items.go @@ -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) } diff --git a/server/pipeline/items_test.go b/server/pipeline/items_test.go index cdec0f62af5..e4c44bafe6b 100644 --- a/server/pipeline/items_test.go +++ b/server/pipeline/items_test.go @@ -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", @@ -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", diff --git a/server/services/config/combined_test.go b/server/services/config/combined_test.go index e4a71596c99..3c274bf92d5 100644 --- a/server/services/config/combined_test.go +++ b/server/services/config/combined_test.go @@ -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) { diff --git a/server/services/config/http.go b/server/services/config/http.go index 81701e5ed5d..bbc9414c5b1 100644 --- a/server/services/config/http.go +++ b/server/services/config/http.go @@ -28,8 +28,9 @@ import ( ) type http 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. @@ -49,16 +50,11 @@ type responseStructure struct { Configs []*configData `json:"configs"` } -func NewHTTP(endpoint string, client *utils.Client) Service { - return &http{endpoint, client} +func NewHTTP(endpoint string, client *utils.Client, includeNetrc bool) Service { + return &http{endpoint, client, includeNetrc} } 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) { - 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)} @@ -68,10 +64,17 @@ func (h *http) Fetch(ctx context.Context, forge forge.Forge, user *model.User, r 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, net_http.MethodPost, h.endpoint, body, response) if err != nil && status != net_http.StatusNoContent { return nil, fmt.Errorf("failed to fetch config via http (status: %d): %w", status, err) diff --git a/server/services/manager.go b/server/services/manager.go index 50a91d61c4b..411fa6611b3 100644 --- a/server/services/manager.go +++ b/server/services/manager.go @@ -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]()), @@ -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() } @@ -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 diff --git a/server/services/registry/combined.go b/server/services/registry/combined.go index d0d5836d352..d28d104a761 100644 --- a/server/services/registry/combined.go +++ b/server/services/registry/combined.go @@ -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 } diff --git a/server/services/registry/combined_test.go b/server/services/registry/combined_test.go index 2ff8938708b..72010fd31ec 100644 --- a/server/services/registry/combined_test.go +++ b/server/services/registry/combined_test.go @@ -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") diff --git a/server/services/registry/db.go b/server/services/registry/db.go index f06842429aa..a55e9572ae4 100644 --- a/server/services/registry/db.go +++ b/server/services/registry/db.go @@ -38,7 +38,7 @@ func (d *db) RegistryList(repo *model.Repo, p *model.ListOptions) ([]*model.Regi return d.store.RegistryList(repo, false, p) } -func (d *db) RegistryListPipeline(_ context.Context, repo *model.Repo, _ *model.Pipeline) ([]*model.Registry, error) { +func (d *db) RegistryListPipeline(_ context.Context, repo *model.Repo, _ *model.Pipeline, _ *model.Netrc) ([]*model.Registry, error) { r, err := d.store.RegistryList(repo, true, &model.ListOptions{All: true}) if err != nil { return nil, err diff --git a/server/services/registry/http.go b/server/services/registry/http.go index 81c836fac2c..07647c561b7 100644 --- a/server/services/registry/http.go +++ b/server/services/registry/http.go @@ -24,13 +24,15 @@ import ( ) type httpExtension struct { - endpoint string - client *utils.Client + endpoint string + client *utils.Client + includeNetrc bool } type requestStructure struct { Repo *model.Repo `json:"repo"` Pipeline *model.Pipeline `json:"pipeline"` + Netrc *model.Netrc `json:"netrc,omitempty"` } type responseStructure struct { @@ -44,17 +46,20 @@ type registryData struct { } // NewHTTP returns a new HTTP registry extension client. -func NewHTTP(endpoint string, client *utils.Client) *httpExtension { - return &httpExtension{endpoint, client} +func NewHTTP(endpoint string, client *utils.Client, includeNetrc bool) *httpExtension { + return &httpExtension{endpoint, client, includeNetrc} } // RegistryListPipeline fetches registry credentials from an external HTTP extension. -func (h *httpExtension) RegistryListPipeline(ctx context.Context, repo *model.Repo, pipeline *model.Pipeline) ([]*model.Registry, error) { +func (h *httpExtension) RegistryListPipeline(ctx context.Context, repo *model.Repo, pipeline *model.Pipeline, netrc *model.Netrc) ([]*model.Registry, error) { response := new(responseStructure) body := requestStructure{ Repo: repo, Pipeline: pipeline, } + if h.includeNetrc { + body.Netrc = netrc + } status, err := h.client.Send(ctx, net_http.MethodPost, h.endpoint, body, response) if err != nil && status != net_http.StatusNoContent { diff --git a/server/services/registry/mocks/mock_Service.go b/server/services/registry/mocks/mock_Service.go index 109460c3ab4..3339b79e60c 100644 --- a/server/services/registry/mocks/mock_Service.go +++ b/server/services/registry/mocks/mock_Service.go @@ -873,8 +873,8 @@ func (_c *MockService_RegistryList_Call) RunAndReturn(run func(repo *model.Repo, } // RegistryListPipeline provides a mock function for the type MockService -func (_mock *MockService) RegistryListPipeline(context1 context.Context, repo *model.Repo, pipeline *model.Pipeline) ([]*model.Registry, error) { - ret := _mock.Called(context1, repo, pipeline) +func (_mock *MockService) RegistryListPipeline(context1 context.Context, repo *model.Repo, pipeline *model.Pipeline, netrc *model.Netrc) ([]*model.Registry, error) { + ret := _mock.Called(context1, repo, pipeline, netrc) if len(ret) == 0 { panic("no return value specified for RegistryListPipeline") @@ -882,18 +882,18 @@ func (_mock *MockService) RegistryListPipeline(context1 context.Context, repo *m var r0 []*model.Registry var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, *model.Repo, *model.Pipeline) ([]*model.Registry, error)); ok { - return returnFunc(context1, repo, pipeline) + if returnFunc, ok := ret.Get(0).(func(context.Context, *model.Repo, *model.Pipeline, *model.Netrc) ([]*model.Registry, error)); ok { + return returnFunc(context1, repo, pipeline, netrc) } - if returnFunc, ok := ret.Get(0).(func(context.Context, *model.Repo, *model.Pipeline) []*model.Registry); ok { - r0 = returnFunc(context1, repo, pipeline) + if returnFunc, ok := ret.Get(0).(func(context.Context, *model.Repo, *model.Pipeline, *model.Netrc) []*model.Registry); ok { + r0 = returnFunc(context1, repo, pipeline, netrc) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.Registry) } } - if returnFunc, ok := ret.Get(1).(func(context.Context, *model.Repo, *model.Pipeline) error); ok { - r1 = returnFunc(context1, repo, pipeline) + if returnFunc, ok := ret.Get(1).(func(context.Context, *model.Repo, *model.Pipeline, *model.Netrc) error); ok { + r1 = returnFunc(context1, repo, pipeline, netrc) } else { r1 = ret.Error(1) } @@ -909,11 +909,12 @@ type MockService_RegistryListPipeline_Call struct { // - context1 context.Context // - repo *model.Repo // - pipeline *model.Pipeline -func (_e *MockService_Expecter) RegistryListPipeline(context1 interface{}, repo interface{}, pipeline interface{}) *MockService_RegistryListPipeline_Call { - return &MockService_RegistryListPipeline_Call{Call: _e.mock.On("RegistryListPipeline", context1, repo, pipeline)} +// - netrc *model.Netrc +func (_e *MockService_Expecter) RegistryListPipeline(context1 interface{}, repo interface{}, pipeline interface{}, netrc interface{}) *MockService_RegistryListPipeline_Call { + return &MockService_RegistryListPipeline_Call{Call: _e.mock.On("RegistryListPipeline", context1, repo, pipeline, netrc)} } -func (_c *MockService_RegistryListPipeline_Call) Run(run func(context1 context.Context, repo *model.Repo, pipeline *model.Pipeline)) *MockService_RegistryListPipeline_Call { +func (_c *MockService_RegistryListPipeline_Call) Run(run func(context1 context.Context, repo *model.Repo, pipeline *model.Pipeline, netrc *model.Netrc)) *MockService_RegistryListPipeline_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { @@ -927,10 +928,15 @@ func (_c *MockService_RegistryListPipeline_Call) Run(run func(context1 context.C if args[2] != nil { arg2 = args[2].(*model.Pipeline) } + var arg3 *model.Netrc + if args[3] != nil { + arg3 = args[3].(*model.Netrc) + } run( arg0, arg1, arg2, + arg3, ) }) return _c @@ -941,7 +947,7 @@ func (_c *MockService_RegistryListPipeline_Call) Return(registrys []*model.Regis return _c } -func (_c *MockService_RegistryListPipeline_Call) RunAndReturn(run func(context1 context.Context, repo *model.Repo, pipeline *model.Pipeline) ([]*model.Registry, error)) *MockService_RegistryListPipeline_Call { +func (_c *MockService_RegistryListPipeline_Call) RunAndReturn(run func(context1 context.Context, repo *model.Repo, pipeline *model.Pipeline, netrc *model.Netrc) ([]*model.Registry, error)) *MockService_RegistryListPipeline_Call { _c.Call.Return(run) return _c } diff --git a/server/services/registry/service.go b/server/services/registry/service.go index 5e45d272c7e..6da947446d3 100644 --- a/server/services/registry/service.go +++ b/server/services/registry/service.go @@ -22,7 +22,7 @@ import ( // Service defines a service for managing registries. type Service interface { - RegistryListPipeline(context.Context, *model.Repo, *model.Pipeline) ([]*model.Registry, error) + RegistryListPipeline(context.Context, *model.Repo, *model.Pipeline, *model.Netrc) ([]*model.Registry, error) // Repository registries RegistryFind(*model.Repo, string) (*model.Registry, error) RegistryList(*model.Repo, *model.ListOptions) ([]*model.Registry, error) diff --git a/server/services/registry/with_extension.go b/server/services/registry/with_extension.go index 0e233c4accb..3b3b8343241 100644 --- a/server/services/registry/with_extension.go +++ b/server/services/registry/with_extension.go @@ -34,15 +34,15 @@ func NewWithExtension(base Service, extension *httpExtension) Service { return &withExtension{base, extension} } -func (w *withExtension) RegistryListPipeline(ctx context.Context, repo *model.Repo, pipeline *model.Pipeline) ([]*model.Registry, error) { +func (w *withExtension) RegistryListPipeline(ctx context.Context, repo *model.Repo, pipeline *model.Pipeline, netrc *model.Netrc) ([]*model.Registry, error) { // Get registries from base service - baseRegistries, err := w.base.RegistryListPipeline(ctx, repo, pipeline) + baseRegistries, err := w.base.RegistryListPipeline(ctx, repo, pipeline, netrc) if err != nil { return nil, err } // Get registries from HTTP extension - extensionRegistries, err := w.extension.RegistryListPipeline(ctx, repo, pipeline) + extensionRegistries, err := w.extension.RegistryListPipeline(ctx, repo, pipeline, netrc) if err != nil { // Log the error but don't fail - use base registries only log.Warn().Err(err).Msg("failed to fetch registries from extension") diff --git a/server/services/registry/with_extension_test.go b/server/services/registry/with_extension_test.go index 4b7d571e826..f0ea500f36a 100644 --- a/server/services/registry/with_extension_test.go +++ b/server/services/registry/with_extension_test.go @@ -145,7 +145,7 @@ func TestWithExtensionRegistryListPipeline(t *testing.T) { client, err := utils.NewHTTPClient(privEd25519Key, "loopback") require.NoError(t, err) - httpExtension := NewHTTP(ts.URL, client) + httpExtension := NewHTTP(ts.URL, client, true) for _, tt := range testTable { t.Run(tt.name, func(t *testing.T) { @@ -158,6 +158,7 @@ func TestWithExtensionRegistryListPipeline(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") diff --git a/server/services/secret/combined_test.go b/server/services/secret/combined_test.go index b522773e31d..a2a4ee05e74 100644 --- a/server/services/secret/combined_test.go +++ b/server/services/secret/combined_test.go @@ -146,7 +146,7 @@ func TestCombinedSecretListPipeline(t *testing.T) { client, err := utils.NewHTTPClient(privEd25519Key, "loopback") require.NoError(t, err) - httpExtension := secret.NewHTTP(ts.URL, client, false) + httpExtension := secret.NewHTTP(ts.URL, client, true) for _, tt := range testTable { t.Run(tt.name, func(t *testing.T) { diff --git a/server/services/setup.go b/server/services/setup.go index 5a2d40b2f14..d11395f2ceb 100644 --- a/server/services/setup.go +++ b/server/services/setup.go @@ -35,7 +35,7 @@ import ( "go.woodpecker-ci.org/woodpecker/v3/server/store/types" ) -func setupRegistryService(store store.Store, dockerConfig, endpoint string, client *utils.Client) registry.Service { +func setupRegistryService(store store.Store, dockerConfig, endpoint string, includeNetrc bool, client *utils.Client) registry.Service { var service registry.Service if dockerConfig != "" { service = registry.NewCombined( @@ -48,7 +48,7 @@ func setupRegistryService(store store.Store, dockerConfig, endpoint string, clie // Wrap with global HTTP extension if configured if endpoint != "" { - service = registry.NewWithExtension(service, registry.NewHTTP(endpoint, client)) + service = registry.NewWithExtension(service, registry.NewHTTP(endpoint, client, includeNetrc)) } return service @@ -79,7 +79,7 @@ func setupConfigService(c *cli.Command, client *utils.Client) (config.Service, e configFetcher := config.NewForge(timeout, retries) if endpoint := c.String("config-extension-endpoint"); endpoint != "" { - httpFetcher := config.NewHTTP(endpoint, client) + httpFetcher := config.NewHTTP(endpoint, client, c.Bool("config-extension-netrc")) if c.Bool("config-extension-exclusive") { return httpFetcher, nil } diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json index 274f6fb9def..e06aaa5a1df 100644 --- a/web/src/assets/locales/en.json +++ b/web/src/assets/locales/en.json @@ -541,8 +541,8 @@ "config_extension_exclusive": "Exclusive", "config_extension_exclusive_desc": "If enabled, will skip all other ways of configuration fetching, including the forge.", "secret_extension_endpoint": "Secret extension endpoint", - "secret_extension_netrc": "Include netrc credentials", - "secret_extension_netrc_desc": "Send forge netrc credentials to the secret extension.", + "extension_netrc": "Include netrc credentials", + "extension_netrc_desc": "Send forge netrc credentials to the secret extension.", "extensions_signatures_public_key": "Public key for signatures", "extensions_signatures_public_key_description": "This public key should be used by your extensions to verify webhook calls from Woodpecker.", "extensions_configuration_saved": "Extensions configuration saved", diff --git a/web/src/lib/api/types/repo.ts b/web/src/lib/api/types/repo.ts index a52f0968cbf..79aa0d4a09b 100644 --- a/web/src/lib/api/types/repo.ts +++ b/web/src/lib/api/types/repo.ts @@ -85,9 +85,15 @@ export interface Repo { config_extension_exclusive: boolean; + // Whether to include netrc credentials in config extension requests + config_extension_netrc: boolean; + // Endpoint for registry extensions registry_extension_endpoint: string; + // Whether to include netrc credentials in registry extension requests + registry_extension_netrc: boolean; + // Endpoint for secret extensions secret_extension_endpoint: string; @@ -128,7 +134,9 @@ export type ExtensionSettings = Pick< Repo, | 'config_extension_endpoint' | 'config_extension_exclusive' + | 'config_extension_netrc' | 'registry_extension_endpoint' + | 'registry_extension_netrc' | 'secret_extension_endpoint' | 'secret_extension_netrc' >; diff --git a/web/src/views/repo/settings/Extensions.vue b/web/src/views/repo/settings/Extensions.vue index 5539e48fe4a..4d12bbc2160 100644 --- a/web/src/views/repo/settings/Extensions.vue +++ b/web/src/views/repo/settings/Extensions.vue @@ -16,6 +16,13 @@ :label="$t('config_extension_exclusive')" :description="$t('config_extension_exclusive_desc')" /> + + @@ -23,6 +30,13 @@ v-model="extensions.registry_extension_endpoint" :placeholder="$t('extension_endpoint_placeholder')" /> + + @@ -31,8 +45,8 @@