32136eae34
Fixes #1
502 lines
14 KiB
Go
502 lines
14 KiB
Go
// Copyright 2022 Lauris BH. All rights reserved.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package drone
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
transform "codeberg.org/lafriks/woodpecker-pipeline-transform"
|
|
"codeberg.org/lafriks/woodpecker-pipeline-transform/core"
|
|
|
|
"github.com/goccy/go-yaml"
|
|
)
|
|
|
|
type UnsupportedEventError struct {
|
|
Event string
|
|
}
|
|
|
|
func (e UnsupportedEventError) Error() string {
|
|
return "unsupported event: " + e.Event
|
|
}
|
|
|
|
func New() transform.Transformer {
|
|
return &DronePipeline{}
|
|
}
|
|
|
|
type DronePipeline struct{}
|
|
|
|
func (d DronePipeline) ConvertImage(image string) string {
|
|
return image
|
|
}
|
|
|
|
func (d DronePipeline) ConvertEvents(events core.Strings) (core.Strings, error) {
|
|
ev := make([]string, 0, len(events))
|
|
for _, event := range events {
|
|
switch event {
|
|
case "push":
|
|
ev = append(ev, "push")
|
|
case "pull_request":
|
|
ev = append(ev, "pull_request")
|
|
case "tag":
|
|
ev = append(ev, "tag")
|
|
case "promote":
|
|
ev = append(ev, "deployment")
|
|
// rollback, cron, custom
|
|
default:
|
|
return nil, UnsupportedEventError{Event: event}
|
|
}
|
|
}
|
|
return ev, nil
|
|
}
|
|
|
|
func (d DronePipeline) ConvertConditions(when *When) (*transform.When, error) {
|
|
if when == nil {
|
|
return nil, nil
|
|
}
|
|
if when.Repositories != nil &&
|
|
(len(when.Repositories.Include) > 0 ||
|
|
len(when.Repositories.Exclude) > 0 ||
|
|
len(when.Repositories.Conditions) > 0) {
|
|
return nil, errors.New("unsupported condition: repo")
|
|
}
|
|
if when.Cron != nil &&
|
|
(len(when.Cron.Include) > 0 ||
|
|
len(when.Cron.Exclude) > 0 ||
|
|
len(when.Cron.Conditions) > 0) {
|
|
return nil, errors.New("unsupported condition: cron")
|
|
}
|
|
// Branch conditions
|
|
branch := &transform.Conditions{}
|
|
if when.Branch != nil {
|
|
branch.Conditions = when.Branch.Conditions
|
|
branch.Include = when.Branch.Include
|
|
branch.Exclude = when.Branch.Exclude
|
|
}
|
|
ev, err := d.ConvertEvents(when.Event)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Refs (branches and tags)
|
|
var tags string
|
|
if when.Refs != nil {
|
|
for _, ref := range when.Refs.Conditions {
|
|
if strings.HasPrefix(ref, "refs/tags/") {
|
|
tags += strings.TrimPrefix(ref, "refs/tags/") + ","
|
|
} else if strings.HasPrefix(ref, "refs/heads/") {
|
|
branch.Conditions = append(branch.Conditions, strings.TrimPrefix(ref, "refs/heads/"))
|
|
}
|
|
}
|
|
for _, ref := range when.Refs.Include {
|
|
if strings.HasPrefix(ref, "refs/tags/") {
|
|
tags += strings.TrimPrefix(ref, "refs/tags/") + ","
|
|
} else if strings.HasPrefix(ref, "refs/heads/") {
|
|
branch.Include = append(branch.Include, strings.TrimPrefix(ref, "refs/heads/"))
|
|
}
|
|
}
|
|
for _, ref := range when.Refs.Exclude {
|
|
if strings.HasPrefix(ref, "refs/tags/") {
|
|
if tags != "" {
|
|
return nil, errors.New("tags include and exclude are mutually exclusive in Woodpecker CI")
|
|
}
|
|
tags += strings.TrimPrefix(ref, "refs/tags/") + "|"
|
|
} else if strings.HasPrefix(ref, "refs/heads/") {
|
|
branch.Exclude = append(branch.Exclude, strings.TrimPrefix(ref, "refs/heads/"))
|
|
}
|
|
}
|
|
}
|
|
if tags != "" {
|
|
if strings.Count(tags, ",") == 1 {
|
|
tags = strings.TrimSuffix(tags, ",")
|
|
} else if strings.HasSuffix(tags, ",") {
|
|
tags = "{" + strings.TrimSuffix(tags, ",") + "}"
|
|
} else if strings.HasSuffix(tags, "|") {
|
|
tags = "!(" + strings.TrimSuffix(tags, "|") + ")"
|
|
}
|
|
}
|
|
// Check if branch is empty condition
|
|
if branch.IsEmpty() {
|
|
branch = nil
|
|
}
|
|
// Instance condition
|
|
var instance string
|
|
if when.Instance != nil {
|
|
if len(when.Instance.Conditions)+len(when.Instance.Include) > 1 {
|
|
return nil, errors.New("unsupported condition: only single instance is supported")
|
|
}
|
|
if len(when.Instance.Exclude) > 0 {
|
|
return nil, errors.New("unsupported condition: instance exclude condition is not supported")
|
|
}
|
|
if len(when.Instance.Conditions) == 1 {
|
|
instance = when.Instance.Conditions[0]
|
|
} else if len(when.Instance.Include) == 1 {
|
|
instance = when.Instance.Include[0]
|
|
}
|
|
}
|
|
// Target condition
|
|
var target string
|
|
if when.Target != nil {
|
|
if len(when.Target.Conditions)+len(when.Target.Include) > 1 {
|
|
return nil, errors.New("unsupported condition: only single target is supported")
|
|
}
|
|
if len(when.Target.Exclude) > 0 {
|
|
return nil, errors.New("unsupported condition: target exclude condition is not supported")
|
|
}
|
|
if len(when.Target.Conditions) == 1 {
|
|
target = when.Target.Conditions[0]
|
|
} else if len(when.Target.Include) == 1 {
|
|
target = when.Target.Include[0]
|
|
}
|
|
}
|
|
r := &transform.When{
|
|
Branch: branch,
|
|
Event: ev,
|
|
Tag: tags,
|
|
Instance: instance,
|
|
Status: when.Status,
|
|
Environment: target,
|
|
}
|
|
// If there is no actual conditions, skip the when
|
|
if r.IsEmpty() {
|
|
r = nil
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func (d DronePipeline) ConvertVolumes(volumes []*Volume, refs []VolumeRef) ([]string, error) {
|
|
if len(refs) == 0 {
|
|
return nil, nil
|
|
}
|
|
v := make([]string, 0, len(refs))
|
|
for _, ref := range refs {
|
|
for _, volume := range volumes {
|
|
if volume.Name != ref.Name {
|
|
continue
|
|
}
|
|
if volume.Temp != nil {
|
|
return nil, errors.New("temporary volumes are not supported")
|
|
}
|
|
if volume.Host == nil || len(volume.Host.Path) == 0 {
|
|
return nil, errors.New("host path is required")
|
|
}
|
|
v = append(v, volume.Host.Path+":"+ref.Path)
|
|
break
|
|
}
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
func (d DronePipeline) Convert(pipeline *Pipeline) (*transform.Pipeline, error) {
|
|
if pipeline.Kind != "pipeline" {
|
|
return nil, transform.UnsupportedError
|
|
}
|
|
if pipeline.Type != "docker" && pipeline.Type != "exec" {
|
|
return nil, transform.UnsupportedError
|
|
}
|
|
if pipeline.Platform != nil && len(pipeline.Platform.Version) > 0 {
|
|
return nil, errors.New("unsupported platform property: version")
|
|
}
|
|
|
|
// Pipeline basics
|
|
p := &transform.Pipeline{
|
|
Name: pipeline.Name,
|
|
Steps: make(transform.Steps, 0, len(pipeline.Steps)),
|
|
Labels: pipeline.Node,
|
|
DependsOn: pipeline.DependsOn,
|
|
}
|
|
|
|
// Platform
|
|
if pipeline.Platform != nil && len(pipeline.Platform.OS) > 0 && len(pipeline.Platform.Arch) > 0 {
|
|
p.Platform = pipeline.Platform.OS + "/" + pipeline.Platform.Arch
|
|
}
|
|
|
|
// Workspace
|
|
if pipeline.Workspace != nil && len(pipeline.Workspace.Path) > 0 {
|
|
p.Workspace = &transform.Workspace{
|
|
Base: pipeline.Workspace.Path,
|
|
Path: pipeline.Workspace.Path,
|
|
}
|
|
}
|
|
|
|
// Services
|
|
for _, service := range pipeline.Services {
|
|
if service.Privileged {
|
|
return nil, errors.New("unsupported service property: privileged")
|
|
}
|
|
if len(service.WorkingDir) > 0 {
|
|
return nil, errors.New("unsupported service property: working_dir")
|
|
}
|
|
env := make([]string, 0, len(service.Environment))
|
|
for k, v := range service.Environment {
|
|
if v.Secret != "" {
|
|
continue
|
|
}
|
|
env = append(env, k+"="+v.Value)
|
|
}
|
|
secrets := make(transform.Secrets, 0)
|
|
for k, v := range service.Environment {
|
|
if v.Secret == "" {
|
|
continue
|
|
}
|
|
secrets = append(secrets, transform.Secret{
|
|
Target: k,
|
|
Source: v.Secret,
|
|
})
|
|
}
|
|
volumes, err := d.ConvertVolumes(pipeline.Volumes, service.Volumes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p.Services = append(p.Services, &transform.Service{
|
|
Name: service.Name,
|
|
Image: service.Image,
|
|
Pull: service.Pull == "always",
|
|
Privileged: service.Privileged,
|
|
Environment: env,
|
|
Secrets: secrets,
|
|
Commands: append(service.Entrypoint, service.Commands...),
|
|
Volumes: volumes,
|
|
})
|
|
}
|
|
|
|
// Clone
|
|
if pipeline.Clone != nil {
|
|
if pipeline.Clone.Disable {
|
|
p.SkipClone = true
|
|
}
|
|
if pipeline.Clone.Depth != nil {
|
|
p.Clone = &transform.Clone{
|
|
Git: &transform.CloneStep{
|
|
Image: "woodpeckerci/plugin-git",
|
|
Settings: transform.Settings{
|
|
transform.Setting{
|
|
Name: "depth",
|
|
Value: *pipeline.Clone.Depth,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
// Trigger
|
|
if pipeline.Trigger != nil {
|
|
if !pipeline.Trigger.Branch.IsEmpty() {
|
|
p.Branches = &transform.Conditions{
|
|
Conditions: pipeline.Trigger.Branch.Conditions,
|
|
Include: pipeline.Trigger.Branch.Include,
|
|
Exclude: pipeline.Trigger.Branch.Exclude,
|
|
}
|
|
}
|
|
if len(pipeline.Trigger.Status) > 0 {
|
|
p.RunsOn = pipeline.Trigger.Status
|
|
}
|
|
// Unsupported trigger conditions
|
|
if !pipeline.Trigger.Action.IsEmpty() ||
|
|
!pipeline.Trigger.Cron.IsEmpty() ||
|
|
len(pipeline.Trigger.Event) > 0 ||
|
|
!pipeline.Trigger.Refs.IsEmpty() ||
|
|
!pipeline.Trigger.Repositories.IsEmpty() ||
|
|
!pipeline.Trigger.Target.IsEmpty() {
|
|
return nil, errors.New("unsupported trigger condition")
|
|
}
|
|
}
|
|
|
|
// Steps
|
|
for _, step := range pipeline.Steps {
|
|
if len(step.DependsOn) > 0 {
|
|
return nil, errors.New("unsupported step property: depends_on")
|
|
}
|
|
if len(step.Failure) > 0 {
|
|
return nil, errors.New("unsupported step property: failure")
|
|
}
|
|
env := make([]string, 0, len(step.Environment))
|
|
for k, v := range step.Environment {
|
|
if v.Secret != "" {
|
|
continue
|
|
}
|
|
env = append(env, k+"="+v.Value)
|
|
}
|
|
secrets := make(transform.Secrets, 0)
|
|
for k, v := range step.Environment {
|
|
if v.Secret == "" {
|
|
continue
|
|
}
|
|
secrets = append(secrets, transform.Secret{
|
|
Target: k,
|
|
Source: v.Secret,
|
|
})
|
|
}
|
|
when, err := d.ConvertConditions(step.When)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
volumes, err := d.ConvertVolumes(pipeline.Volumes, step.Volumes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var image string
|
|
if pipeline.Type == "docker" {
|
|
image = d.ConvertImage(step.Image)
|
|
} else if pipeline.Type == "exec" {
|
|
image = "bash"
|
|
}
|
|
p.Steps = append(p.Steps, &transform.Step{
|
|
Name: step.Name,
|
|
Image: image,
|
|
Pull: step.Pull == "always",
|
|
Environment: env,
|
|
Secrets: secrets,
|
|
Settings: step.Settings,
|
|
Detach: step.Detach,
|
|
Privileged: step.Privileged,
|
|
Commands: step.Commands,
|
|
When: when,
|
|
Volumes: volumes,
|
|
})
|
|
}
|
|
|
|
return p, nil
|
|
}
|
|
|
|
func (d DronePipeline) Transform(sources []*transform.Source) ([]*transform.Pipeline, error) {
|
|
p := make([]*transform.Pipeline, 0, len(sources))
|
|
for _, source := range sources {
|
|
dec := yaml.NewDecoder(bytes.NewReader(source.Content))
|
|
var err error
|
|
for err == nil {
|
|
pipeline := &Pipeline{}
|
|
if err = dec.Decode(pipeline); err != nil {
|
|
if err.Error() != "EOF" {
|
|
return nil, err
|
|
}
|
|
break
|
|
}
|
|
r, err := d.Convert(pipeline)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p = append(p, r)
|
|
}
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
var envMap map[string]string = map[string]string{
|
|
"CI": "CI",
|
|
"DRONE": "",
|
|
"DRONE_BRANCH": "CI_COMMIT_BRANCH",
|
|
"DRONE_BUILD_ACTION": "",
|
|
"DRONE_BUILD_CREATED": "CI_BUILD_CREATED",
|
|
"DRONE_BUILD_EVENT": "CI_BUILD_EVENT",
|
|
"DRONE_BUILD_FINISHED": "CI_BUILD_FINISHED",
|
|
"DRONE_BUILD_LINK": "CI_BUILD_LINK",
|
|
"DRONE_BUILD_NUMBER": "CI_BUILD_NUMBER",
|
|
"DRONE_BUILD_PARENT": "CI_BUILD_PARENT",
|
|
"DRONE_BUILD_STARTED": "CI_BUILD_STARTED",
|
|
"DRONE_BUILD_STATUS": "CI_BUILD_STATUS",
|
|
"DRONE_CALVER": "",
|
|
"DRONE_COMMIT": "CI_COMMIT_SHA",
|
|
"DRONE_COMMIT_AFTER": "",
|
|
"DRONE_COMMIT_AUTHOR": "CI_COMMIT_AUTHOR",
|
|
"DRONE_COMMIT_AUTHOR_AVATAR": "CI_COMMIT_AUTHOR_AVATAR",
|
|
"DRONE_COMMIT_AUTHOR_EMAIL": "CI_COMMIT_AUTHOR_EMAIL",
|
|
"DRONE_COMMIT_AUTHOR_NAME": "",
|
|
"DRONE_COMMIT_BEFORE": "",
|
|
"DRONE_COMMIT_BRANCH": "CI_COMMIT_BRANCH",
|
|
"DRONE_COMMIT_LINK": "CI_COMMIT_LINK",
|
|
"DRONE_COMMIT_MESSAGE": "CI_COMMIT_MESSAGE",
|
|
"DRONE_COMMIT_REF": "CI_COMMIT_REF",
|
|
"DRONE_COMMIT_SHA": "CI_COMMIT_SHA",
|
|
"DRONE_DEPLOY_TO": "CI_BUILD_DEPLOY_TARGET",
|
|
"DRONE_FAILED_STAGES": "",
|
|
"DRONE_FAILED_STEPS": "",
|
|
"DRONE_GIT_HTTP_URL": "",
|
|
"DRONE_GIT_SSH_URL": "",
|
|
"DRONE_PULL_REQUEST": "CI_COMMIT_PULL_REQUEST",
|
|
"DRONE_PULL_REQUEST_TITLE": "",
|
|
"DRONE_REMOTE_URL": "CI_REPO_REMOTE",
|
|
"DRONE_REPO": "CI_REPO",
|
|
"DRONE_REPO_BRANCH": "CI_REPO_DEFAULT_BRANCH",
|
|
"DRONE_REPO_LINK": "CI_REPO_LINK",
|
|
"DRONE_REPO_NAME": "CI_REPO_NAME",
|
|
"DRONE_REPO_NAMESPACE": "CI_REPO_OWNER",
|
|
"DRONE_REPO_OWNER": "CI_REPO_OWNER",
|
|
"DRONE_REPO_PRIVATE": "CI_REPO_PRIVATE",
|
|
"DRONE_REPO_SCM": "CI_REPO_SCM",
|
|
"DRONE_REPO_VISIBILITY": "",
|
|
"DRONE_SEMVER": "",
|
|
"DRONE_SEMVER_BUILD": "",
|
|
"DRONE_SEMVER_ERROR": "",
|
|
"DRONE_SEMVER_MAJOR": "",
|
|
"DRONE_SEMVER_MINOR": "",
|
|
"DRONE_SEMVER_PATCH": "",
|
|
"DRONE_SEMVER_PRERELEASE": "",
|
|
"DRONE_SEMVER_SHORT": "",
|
|
"DRONE_SOURCE_BRANCH": "CI_COMMIT_SOURCE_BRANCH",
|
|
"DRONE_STAGE_ARCH": "",
|
|
"DRONE_STAGE_DEPENDS_ON": "",
|
|
"DRONE_STAGE_FINISHED": "",
|
|
"DRONE_STAGE_KIND": "",
|
|
"DRONE_STAGE_MACHINE": "",
|
|
"DRONE_STAGE_NAME": "",
|
|
"DRONE_STAGE_NUMBER": "",
|
|
"DRONE_STAGE_OS": "",
|
|
"DRONE_STAGE_STARTED": "",
|
|
"DRONE_STAGE_STATUS": "",
|
|
"DRONE_STAGE_TYPE": "",
|
|
"DRONE_STAGE_VARIANT": "",
|
|
"DRONE_STEP_NAME": "",
|
|
"DRONE_STEP_NUMBER": "",
|
|
"DRONE_SYSTEM_HOST": "CI_SYSTEM_HOST",
|
|
"DRONE_SYSTEM_HOSTNAME": "",
|
|
"DRONE_SYSTEM_PROTO": "",
|
|
"DRONE_SYSTEM_VERSION": "CI_SYSTEM_VERSION",
|
|
"DRONE_TAG": "CI_COMMIT_TAG",
|
|
"DRONE_TARGET_BRANCH": "CI_COMMIT_TARGET_BRANCH",
|
|
}
|
|
|
|
var (
|
|
envStart []byte = []byte("${")
|
|
envEnd []byte = []byte("}")
|
|
)
|
|
|
|
func (d DronePipeline) PostProcess(yaml []byte) ([]byte, error) {
|
|
buf := bytes.NewBuffer(nil)
|
|
for i := bytes.Index(yaml, envStart); i >= 0; i = bytes.Index(yaml, envStart) {
|
|
buf.Write(yaml[:i+len(envStart)])
|
|
yaml = yaml[i+len(envStart):]
|
|
e := bytes.Index(yaml, envEnd)
|
|
if e < 0 {
|
|
return nil, errors.New("unclosed environment variable")
|
|
}
|
|
exp := yaml[:e]
|
|
c := bytes.IndexAny(exp, "^,:#%/=")
|
|
env := exp
|
|
if c == 0 {
|
|
env = exp[1:]
|
|
} else if c > 0 {
|
|
env = exp[:c]
|
|
}
|
|
if v, ok := envMap[string(env)]; ok {
|
|
if c == 0 {
|
|
buf.WriteByte(exp[0])
|
|
}
|
|
buf.Write([]byte(v))
|
|
if c > 0 {
|
|
buf.Write(exp[c:])
|
|
}
|
|
} else {
|
|
return nil, fmt.Errorf("unknown environment variable %s" + string(env))
|
|
}
|
|
buf.Write(yaml[e : e+len(envEnd)])
|
|
yaml = yaml[e+len(envEnd):]
|
|
}
|
|
buf.Write(yaml)
|
|
return buf.Bytes(), nil
|
|
}
|