// Copyright 2023 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. package linter import ( "fmt" "codeberg.org/6543/xyaml" "go.uber.org/multierr" "go.woodpecker-ci.org/woodpecker/v2/pipeline/errors" "go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/linter/schema" "go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/types" ) // A Linter lints a pipeline configuration. type Linter struct { trusted bool } // New creates a new Linter with options. func New(opts ...Option) *Linter { linter := new(Linter) for _, opt := range opts { opt(linter) } return linter } type WorkflowConfig struct { // File is the path to the configuration file. File string // RawConfig is the raw configuration. RawConfig string // Config is the parsed configuration. Workflow *types.Workflow } // Lint lints the configuration. func (l *Linter) Lint(configs []*WorkflowConfig) error { var linterErr error for _, config := range configs { if err := l.lintFile(config); err != nil { linterErr = multierr.Append(linterErr, err) } } return linterErr } func (l *Linter) lintFile(config *WorkflowConfig) error { var linterErr error if len(config.Workflow.Steps.ContainerList) == 0 { linterErr = multierr.Append(linterErr, newLinterError("Invalid or missing steps section", config.File, "steps", false)) } if err := l.lintContainers(config, "clone"); err != nil { linterErr = multierr.Append(linterErr, err) } if err := l.lintContainers(config, "steps"); err != nil { linterErr = multierr.Append(linterErr, err) } if err := l.lintContainers(config, "services"); err != nil { linterErr = multierr.Append(linterErr, err) } if err := l.lintSchema(config); err != nil { linterErr = multierr.Append(linterErr, err) } if err := l.lintDeprecations(config); err != nil { linterErr = multierr.Append(linterErr, err) } if err := l.lintBadHabits(config); err != nil { linterErr = multierr.Append(linterErr, err) } return linterErr } func (l *Linter) lintContainers(config *WorkflowConfig, area string) error { var linterErr error var containers []*types.Container switch area { case "clone": containers = config.Workflow.Clone.ContainerList case "steps": containers = config.Workflow.Steps.ContainerList case "services": containers = config.Workflow.Services.ContainerList } for _, container := range containers { if err := l.lintImage(config, container, area); err != nil { linterErr = multierr.Append(linterErr, err) } if !l.trusted { if err := l.lintTrusted(config, container, area); err != nil { linterErr = multierr.Append(linterErr, err) } } if err := l.lintCommands(config, container, area); err != nil { linterErr = multierr.Append(linterErr, err) } } return linterErr } func (l *Linter) lintImage(config *WorkflowConfig, c *types.Container, area string) error { if len(c.Image) == 0 { return newLinterError("Invalid or missing image", config.File, fmt.Sprintf("%s.%s", area, c.Name), false) } return nil } func (l *Linter) lintCommands(config *WorkflowConfig, c *types.Container, field string) error { if len(c.Commands) == 0 { return nil } if len(c.Settings) != 0 { var keys []string for key := range c.Settings { keys = append(keys, key) } return newLinterError(fmt.Sprintf("Cannot configure both commands and custom attributes %v", keys), config.File, fmt.Sprintf("%s.%s", field, c.Name), false) } return nil } func (l *Linter) lintTrusted(config *WorkflowConfig, c *types.Container, area string) error { yamlPath := fmt.Sprintf("%s.%s", area, c.Name) err := "" if c.Privileged { err = "Insufficient privileges to use privileged mode" } if c.ShmSize != 0 { err = "Insufficient privileges to override shm_size" } if len(c.DNS) != 0 { err = "Insufficient privileges to use custom dns" } if len(c.DNSSearch) != 0 { err = "Insufficient privileges to use dns_search" } if len(c.Devices) != 0 { err = "Insufficient privileges to use devices" } if len(c.ExtraHosts) != 0 { err = "Insufficient privileges to use extra_hosts" } if len(c.NetworkMode) != 0 { err = "Insufficient privileges to use network_mode" } if c.Networks.Networks != nil && len(c.Networks.Networks) != 0 { err = "Insufficient privileges to use networks" } if c.Volumes.Volumes != nil && len(c.Volumes.Volumes) != 0 { err = "Insufficient privileges to use volumes" } if len(c.Tmpfs) != 0 { err = "Insufficient privileges to use tmpfs" } if len(err) != 0 { return newLinterError(err, config.File, yamlPath, false) } return nil } func (l *Linter) lintSchema(config *WorkflowConfig) error { var linterErr error schemaErrors, err := schema.LintString(config.RawConfig) if err != nil { for _, schemaError := range schemaErrors { linterErr = multierr.Append(linterErr, newLinterError( schemaError.Description(), config.File, schemaError.Field(), true, // TODO: let pipelines fail if the schema is invalid )) } } return linterErr } func (l *Linter) lintDeprecations(config *WorkflowConfig) (err error) { parsed := new(types.Workflow) err = xyaml.Unmarshal([]byte(config.RawConfig), parsed) if err != nil { return err } if parsed.PipelineDoNotUseIt.ContainerList != nil { err = multierr.Append(err, &errors.PipelineError{ Type: errors.PipelineErrorTypeDeprecation, Message: "Please use 'steps:' instead of deprecated 'pipeline:' list", Data: errors.DeprecationErrorData{ File: config.File, Field: "pipeline", Docs: "https://woodpecker-ci.org/docs/next/migrations#next-200", }, IsWarning: true, }) } if parsed.PlatformDoNotUseIt != "" { err = multierr.Append(err, &errors.PipelineError{ Type: errors.PipelineErrorTypeDeprecation, Message: "Please use labels instead of deprecated 'platform' filters", Data: errors.DeprecationErrorData{ File: config.File, Field: "platform", Docs: "https://woodpecker-ci.org/docs/next/migrations#next-200", }, IsWarning: true, }) } if parsed.BranchesDoNotUseIt != nil { err = multierr.Append(err, &errors.PipelineError{ Type: errors.PipelineErrorTypeDeprecation, Message: "Please use global when instead of deprecated 'branches' filter", Data: errors.DeprecationErrorData{ File: config.File, Field: "branches", Docs: "https://woodpecker-ci.org/docs/next/migrations#next-200", }, IsWarning: true, }) } for _, step := range parsed.Steps.ContainerList { if step.Group != "" { err = multierr.Append(err, &errors.PipelineError{ Type: errors.PipelineErrorTypeDeprecation, Message: "Please use depends_on instead of deprecated 'group' setting", Data: errors.DeprecationErrorData{ File: config.File, Field: "steps." + step.Name + ".group", Docs: "https://woodpecker-ci.org/docs/next/usage/workflow-syntax#depends_on", }, IsWarning: true, }) } } for i, c := range parsed.When.Constraints { if len(c.Event.Exclude) != 0 { err = multierr.Append(err, &errors.PipelineError{ Type: errors.PipelineErrorTypeDeprecation, Message: "Please only use allow lists for events", Data: errors.DeprecationErrorData{ File: config.File, Field: fmt.Sprintf("when[%d].event", i), Docs: "https://woodpecker-ci.org/docs/usage/workflow-syntax#event-1", }, IsWarning: true, }) } } for _, step := range parsed.Steps.ContainerList { for i, c := range step.When.Constraints { if len(c.Event.Exclude) != 0 { err = multierr.Append(err, &errors.PipelineError{ Type: errors.PipelineErrorTypeDeprecation, Message: "Please only use allow lists for events", Data: errors.DeprecationErrorData{ File: config.File, Field: fmt.Sprintf("steps.%s.when[%d].event", step.Name, i), Docs: "https://woodpecker-ci.org/docs/usage/workflow-syntax#event", }, IsWarning: true, }) } } } return err } func (l *Linter) lintBadHabits(config *WorkflowConfig) (err error) { parsed := new(types.Workflow) err = xyaml.Unmarshal([]byte(config.RawConfig), parsed) if err != nil { return err } rootEventFilters := len(parsed.When.Constraints) > 0 for _, c := range parsed.When.Constraints { if len(c.Event.Include) == 0 { rootEventFilters = false break } } if !rootEventFilters { // root whens do not necessarily have an event filter, check steps for _, step := range parsed.Steps.ContainerList { var field string if len(step.When.Constraints) == 0 { field = fmt.Sprintf("steps.%s", step.Name) } else { stepEventIndex := -1 for i, c := range step.When.Constraints { if len(c.Event.Include) == 0 { stepEventIndex = i break } } if stepEventIndex > -1 { field = fmt.Sprintf("steps.%s.when[%d]", step.Name, stepEventIndex) } } if field != "" { err = multierr.Append(err, &errors.PipelineError{ Type: errors.PipelineErrorTypeBadHabit, Message: "Please set an event filter on all when branches", Data: errors.LinterErrorData{ File: config.File, Field: field, }, IsWarning: true, }) } } } return }