1
0
Fork 0
woodpecker-pipeline-transform/drone/drone.go
2022-08-01 18:01:09 +03:00

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
}