2022-10-18 01:24:12 +00:00
|
|
|
// 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.
|
|
|
|
|
2022-11-06 11:44:04 +00:00
|
|
|
package forge
|
2019-06-04 13:04:18 +00:00
|
|
|
|
|
|
|
import (
|
2021-09-28 10:56:59 +00:00
|
|
|
"context"
|
|
|
|
"errors"
|
2021-09-25 23:23:17 +00:00
|
|
|
"fmt"
|
2019-06-24 07:04:30 +00:00
|
|
|
"strings"
|
2019-06-04 13:04:18 +00:00
|
|
|
"time"
|
|
|
|
|
2021-10-12 07:25:13 +00:00
|
|
|
"github.com/rs/zerolog/log"
|
|
|
|
|
2023-12-08 07:15:08 +00:00
|
|
|
"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"
|
2019-06-04 13:04:18 +00:00
|
|
|
)
|
|
|
|
|
2021-12-01 13:22:06 +00:00
|
|
|
type ConfigFetcher interface {
|
2022-11-06 11:44:04 +00:00
|
|
|
Fetch(ctx context.Context) (files []*types.FileMeta, err error)
|
2021-12-01 13:22:06 +00:00
|
|
|
}
|
|
|
|
|
2019-06-04 13:04:18 +00:00
|
|
|
type configFetcher struct {
|
2022-11-06 11:44:04 +00:00
|
|
|
forge Forge
|
2022-06-01 18:06:27 +00:00
|
|
|
user *model.User
|
|
|
|
repo *model.Repo
|
2022-10-18 01:24:12 +00:00
|
|
|
pipeline *model.Pipeline
|
2022-06-01 18:06:27 +00:00
|
|
|
configExtension config.Extension
|
2023-04-30 15:02:47 +00:00
|
|
|
configPath string
|
2023-02-01 17:53:19 +00:00
|
|
|
timeout time.Duration
|
2019-06-04 13:04:18 +00:00
|
|
|
}
|
|
|
|
|
2023-02-01 17:53:19 +00:00
|
|
|
func NewConfigFetcher(forge Forge, timeout time.Duration, configExtension config.Extension, user *model.User, repo *model.Repo, pipeline *model.Pipeline) ConfigFetcher {
|
2021-08-20 14:32:52 +00:00
|
|
|
return &configFetcher{
|
2022-11-04 23:35:06 +00:00
|
|
|
forge: forge,
|
2022-06-01 18:06:27 +00:00
|
|
|
user: user,
|
|
|
|
repo: repo,
|
2022-10-18 01:24:12 +00:00
|
|
|
pipeline: pipeline,
|
2022-06-01 18:06:27 +00:00
|
|
|
configExtension: configExtension,
|
2023-04-30 15:02:47 +00:00
|
|
|
configPath: repo.Config,
|
2023-02-01 17:53:19 +00:00
|
|
|
timeout: timeout,
|
2021-08-20 14:32:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-28 10:56:59 +00:00
|
|
|
// Fetch pipeline config from source forge
|
2022-11-06 11:44:04 +00:00
|
|
|
func (cf *configFetcher) Fetch(ctx context.Context) (files []*types.FileMeta, err error) {
|
2021-10-12 07:25:13 +00:00
|
|
|
log.Trace().Msgf("Start Fetching config for '%s'", cf.repo.FullName)
|
2021-09-27 21:32:08 +00:00
|
|
|
|
2022-02-28 09:56:23 +00:00
|
|
|
// try to fetch 3 times
|
2021-09-28 10:56:59 +00:00
|
|
|
for i := 0; i < 3; i++ {
|
2023-04-30 15:02:47 +00:00
|
|
|
files, err = cf.fetch(ctx, time.Second*cf.timeout, strings.TrimSpace(cf.configPath))
|
2021-12-19 00:12:09 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Trace().Err(err).Msgf("%d. try failed", i+1)
|
|
|
|
}
|
2021-09-28 10:56:59 +00:00
|
|
|
if errors.Is(err, context.DeadlineExceeded) {
|
|
|
|
continue
|
|
|
|
}
|
2022-02-28 09:56:23 +00:00
|
|
|
|
2023-10-07 14:41:25 +00:00
|
|
|
if cf.configExtension != nil {
|
2023-02-01 17:53:19 +00:00
|
|
|
fetchCtx, cancel := context.WithTimeout(ctx, cf.timeout)
|
2022-02-28 09:56:23 +00:00
|
|
|
defer cancel() // ok here as we only try http fetching once, returning on fail and success
|
|
|
|
|
|
|
|
log.Trace().Msgf("ConfigFetch[%s]: getting config from external http service", cf.repo.FullName)
|
2023-08-21 11:22:33 +00:00
|
|
|
netrc, err := cf.forge.Netrc(cf.user, cf.repo)
|
2022-02-28 09:56:23 +00:00
|
|
|
if err != nil {
|
2023-08-21 11:22:33 +00:00
|
|
|
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)
|
2022-02-28 09:56:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if !useOld {
|
|
|
|
return newConfigs, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-28 10:56:59 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// fetch config by timeout
|
2022-11-06 11:44:04 +00:00
|
|
|
func (cf *configFetcher) fetch(c context.Context, timeout time.Duration, config string) ([]*types.FileMeta, error) {
|
2021-09-28 10:56:59 +00:00
|
|
|
ctx, cancel := context.WithTimeout(c, timeout)
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
if len(config) > 0 {
|
2021-10-12 07:25:13 +00:00
|
|
|
log.Trace().Msgf("ConfigFetch[%s]: use user config '%s'", cf.repo.FullName, config)
|
2019-06-25 11:45:43 +00:00
|
|
|
|
2022-11-03 18:12:40 +00:00
|
|
|
// could be adapted to allow the user to supply a list like we do in the defaults
|
|
|
|
configs := []string{config}
|
2021-09-28 10:56:59 +00:00
|
|
|
|
2023-12-21 10:13:25 +00:00
|
|
|
fileMeta, err := cf.getFirstAvailableConfig(ctx, configs)
|
2022-11-03 18:12:40 +00:00
|
|
|
if err == nil {
|
|
|
|
return fileMeta, err
|
|
|
|
}
|
2021-09-28 10:56:59 +00:00
|
|
|
|
2023-02-01 23:08:02 +00:00
|
|
|
return nil, fmt.Errorf("user defined config '%s' not found: %w", config, err)
|
2021-09-28 10:56:59 +00:00
|
|
|
}
|
|
|
|
|
2023-08-21 11:22:33 +00:00
|
|
|
log.Trace().Msgf("ConfigFetch[%s]: user did not define own config, following default procedure", cf.repo.FullName)
|
2022-11-03 18:12:40 +00:00
|
|
|
// for the order see shared/constants/constants.go
|
2023-12-21 10:13:25 +00:00
|
|
|
fileMeta, err := cf.getFirstAvailableConfig(ctx, constant.DefaultConfigOrder[:])
|
2022-11-03 18:12:40 +00:00
|
|
|
if err == nil {
|
|
|
|
return fileMeta, err
|
2019-06-04 13:04:18 +00:00
|
|
|
}
|
2021-08-30 20:54:21 +00:00
|
|
|
|
2021-09-28 10:56:59 +00:00
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return nil, ctx.Err()
|
|
|
|
default:
|
2023-02-01 23:08:02 +00:00
|
|
|
return []*types.FileMeta{}, fmt.Errorf("ConfigFetcher: Fallback did not find config: %w", err)
|
2021-09-28 10:56:59 +00:00
|
|
|
}
|
2019-06-04 13:04:18 +00:00
|
|
|
}
|
2021-08-30 20:54:21 +00:00
|
|
|
|
2022-11-06 11:44:04 +00:00
|
|
|
func filterPipelineFiles(files []*types.FileMeta) []*types.FileMeta {
|
|
|
|
var res []*types.FileMeta
|
2021-08-30 20:54:21 +00:00
|
|
|
|
|
|
|
for _, file := range files {
|
2022-11-03 18:12:40 +00:00
|
|
|
if strings.HasSuffix(file.Name, ".yml") || strings.HasSuffix(file.Name, ".yaml") {
|
2021-08-30 20:54:21 +00:00
|
|
|
res = append(res, file)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return res
|
|
|
|
}
|
2022-11-03 18:12:40 +00:00
|
|
|
|
2023-12-21 10:13:25 +00:00
|
|
|
func (cf *configFetcher) checkPipelineFile(c context.Context, config string) ([]*types.FileMeta, error) {
|
2022-11-04 23:35:06 +00:00
|
|
|
file, err := cf.forge.File(c, cf.user, cf.repo, cf.pipeline, config)
|
2022-11-03 18:12:40 +00:00
|
|
|
|
|
|
|
if err == nil && len(file) != 0 {
|
|
|
|
log.Trace().Msgf("ConfigFetch[%s]: found file '%s'", cf.repo.FullName, config)
|
|
|
|
|
2022-11-06 11:44:04 +00:00
|
|
|
return []*types.FileMeta{{
|
2022-11-03 18:12:40 +00:00
|
|
|
Name: config,
|
|
|
|
Data: file,
|
2023-12-21 10:13:25 +00:00
|
|
|
}}, nil
|
2022-11-03 18:12:40 +00:00
|
|
|
}
|
|
|
|
|
2023-12-21 10:13:25 +00:00
|
|
|
return nil, err
|
2022-11-03 18:12:40 +00:00
|
|
|
}
|
|
|
|
|
2023-12-21 10:13:25 +00:00
|
|
|
func (cf *configFetcher) getFirstAvailableConfig(c context.Context, configs []string) ([]*types.FileMeta, error) {
|
|
|
|
var forgeErr []error
|
2022-11-03 18:12:40 +00:00
|
|
|
for _, fileOrFolder := range configs {
|
|
|
|
if strings.HasSuffix(fileOrFolder, "/") {
|
|
|
|
// config is a folder
|
2022-11-04 23:35:06 +00:00
|
|
|
files, err := cf.forge.Dir(c, cf.user, cf.repo, cf.pipeline, strings.TrimSuffix(fileOrFolder, "/"))
|
2022-11-06 11:44:04 +00:00
|
|
|
// if folder is not supported we will get a "Not implemented" error and continue
|
2023-12-21 10:13:25 +00:00
|
|
|
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
|
2022-11-06 11:44:04 +00:00
|
|
|
}
|
2022-11-03 18:12:40 +00:00
|
|
|
files = filterPipelineFiles(files)
|
2023-12-21 10:13:25 +00:00
|
|
|
if len(files) != 0 {
|
|
|
|
log.Trace().Msgf("ConfigFetch[%s]: found %d files in '%s'", cf.repo.FullName, len(files), fileOrFolder)
|
2022-11-03 18:12:40 +00:00
|
|
|
return files, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// config is a file
|
2023-12-21 10:13:25 +00:00
|
|
|
if fileMeta, err := cf.checkPipelineFile(c, fileOrFolder); err == nil {
|
|
|
|
log.Trace().Msgf("ConfigFetch[%s]: found file: '%s'", cf.repo.FullName, fileOrFolder)
|
2022-11-03 18:12:40 +00:00
|
|
|
return fileMeta, nil
|
2023-12-21 10:13:25 +00:00
|
|
|
} else if !errors.Is(err, &types.ErrConfigNotFound{}) {
|
|
|
|
forgeErr = append(forgeErr, err)
|
2022-11-03 18:12:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-21 10:13:25 +00:00
|
|
|
// got unexpected errors
|
|
|
|
if len(forgeErr) != 0 {
|
|
|
|
return nil, errors.Join(forgeErr...)
|
|
|
|
}
|
|
|
|
|
2022-11-03 18:12:40 +00:00
|
|
|
// nothing found
|
2023-12-21 10:13:25 +00:00
|
|
|
return nil, &types.ErrConfigNotFound{Configs: configs}
|
2022-11-03 18:12:40 +00:00
|
|
|
}
|