2023-08-10 09:06:00 +00:00
// 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.
2017-03-05 07:56:08 +00:00
package linter
import (
"fmt"
2023-11-04 14:30:47 +00:00
"codeberg.org/6543/xyaml"
2023-11-03 10:44:03 +00:00
"go.uber.org/multierr"
2017-03-05 07:56:08 +00:00
2023-12-08 07:15:08 +00:00
"go.woodpecker-ci.org/woodpecker/v2/pipeline/errors"
2024-04-15 08:04:21 +00:00
errorTypes "go.woodpecker-ci.org/woodpecker/v2/pipeline/errors/types"
2023-12-08 07:15:08 +00:00
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/linter/schema"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/types"
2024-08-31 17:04:47 +00:00
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/utils"
2024-09-01 18:41:10 +00:00
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
2017-07-21 21:52:52 +00:00
)
2017-03-05 07:56:08 +00:00
// A Linter lints a pipeline configuration.
type Linter struct {
2024-11-01 20:37:31 +00:00
trusted TrustedConfiguration
2024-09-01 18:41:10 +00:00
privilegedPlugins * [ ] string
trustedClonePlugins * [ ] string
2017-03-05 07:56:08 +00:00
}
2024-11-01 20:37:31 +00:00
type TrustedConfiguration struct {
Network bool
Volumes bool
Security bool
}
2017-03-05 07:56:08 +00:00
// New creates a new Linter with options.
func New ( opts ... Option ) * Linter {
linter := new ( Linter )
for _ , opt := range opts {
opt ( linter )
}
return linter
}
2023-11-04 14:30:47 +00:00
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
}
2017-03-05 07:56:08 +00:00
// Lint lints the configuration.
2023-11-04 14:30:47 +00:00
func ( l * Linter ) Lint ( configs [ ] * WorkflowConfig ) error {
2023-11-03 10:44:03 +00:00
var linterErr error
2023-11-04 14:30:47 +00:00
for _ , config := range configs {
if err := l . lintFile ( config ) ; err != nil {
linterErr = multierr . Append ( linterErr , err )
}
2023-11-03 10:44:03 +00:00
}
2023-11-04 14:30:47 +00:00
return linterErr
}
func ( l * Linter ) lintFile ( config * WorkflowConfig ) error {
var linterErr error
if len ( config . Workflow . Steps . ContainerList ) == 0 {
2024-11-11 22:34:20 +00:00
linterErr = multierr . Append ( linterErr , newLinterError ( "Invalid or missing `steps` section" , config . File , "steps" , false ) )
2023-11-04 14:30:47 +00:00
}
2024-09-01 18:41:10 +00:00
if err := l . lintCloneSteps ( config ) ; err != nil {
linterErr = multierr . Append ( linterErr , err )
}
2023-11-04 14:30:47 +00:00
if err := l . lintContainers ( config , "clone" ) ; err != nil {
2023-11-03 10:44:03 +00:00
linterErr = multierr . Append ( linterErr , err )
2017-07-21 21:52:52 +00:00
}
2023-11-04 14:30:47 +00:00
if err := l . lintContainers ( config , "steps" ) ; err != nil {
2023-11-03 10:44:03 +00:00
linterErr = multierr . Append ( linterErr , err )
2017-07-21 21:52:52 +00:00
}
2023-11-04 14:30:47 +00:00
if err := l . lintContainers ( config , "services" ) ; err != nil {
2023-11-03 10:44:03 +00:00
linterErr = multierr . Append ( linterErr , err )
2017-07-21 21:52:52 +00:00
}
2023-11-03 10:44:03 +00:00
2023-11-04 14:30:47 +00:00
if err := l . lintSchema ( config ) ; err != nil {
2023-11-03 10:44:03 +00:00
linterErr = multierr . Append ( linterErr , err )
}
2023-11-04 14:30:47 +00:00
if err := l . lintDeprecations ( config ) ; err != nil {
2023-11-03 10:44:03 +00:00
linterErr = multierr . Append ( linterErr , err )
}
2023-11-04 14:30:47 +00:00
if err := l . lintBadHabits ( config ) ; err != nil {
2023-11-03 10:44:03 +00:00
linterErr = multierr . Append ( linterErr , err )
}
return linterErr
2017-07-21 21:52:52 +00:00
}
2017-03-05 07:56:08 +00:00
2024-09-01 18:41:10 +00:00
func ( l * Linter ) lintCloneSteps ( config * WorkflowConfig ) error {
if len ( config . Workflow . Clone . ContainerList ) == 0 {
return nil
}
trustedClonePlugins := constant . TrustedClonePlugins
if l . trustedClonePlugins != nil {
trustedClonePlugins = * l . trustedClonePlugins
}
var linterErr error
for _ , container := range config . Workflow . Clone . ContainerList {
if ! utils . MatchImageDynamic ( container . Image , trustedClonePlugins ... ) {
linterErr = multierr . Append ( linterErr ,
newLinterError (
2024-11-11 22:34:20 +00:00
"Specified clone image does not match allow list, netrc is not injected" ,
2024-09-01 18:41:10 +00:00
config . File , fmt . Sprintf ( "clone.%s" , container . Name ) , true ) ,
)
}
}
return linterErr
}
2023-11-04 14:30:47 +00:00
func ( l * Linter ) lintContainers ( config * WorkflowConfig , area string ) error {
2023-11-03 10:44:03 +00:00
var linterErr error
2023-11-04 14:30:47 +00:00
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
}
2017-03-05 07:56:08 +00:00
for _ , container := range containers {
2023-11-04 14:30:47 +00:00
if err := l . lintImage ( config , container , area ) ; err != nil {
2023-11-03 10:44:03 +00:00
linterErr = multierr . Append ( linterErr , err )
2017-03-05 07:56:08 +00:00
}
2024-11-01 20:37:31 +00:00
if err := l . lintTrusted ( config , container , area ) ; err != nil {
linterErr = multierr . Append ( linterErr , err )
2017-03-05 07:56:08 +00:00
}
2024-07-14 21:35:19 +00:00
if err := l . lintSettings ( config , container , area ) ; err != nil {
2023-11-03 10:44:03 +00:00
linterErr = multierr . Append ( linterErr , err )
2017-07-21 21:52:52 +00:00
}
2024-08-31 17:04:47 +00:00
if err := l . lintPrivilegedPlugins ( config , container , area ) ; err != nil {
linterErr = multierr . Append ( linterErr , err )
}
2017-03-05 07:56:08 +00:00
}
2023-11-03 10:44:03 +00:00
return linterErr
2017-03-05 07:56:08 +00:00
}
2023-11-04 14:30:47 +00:00
func ( l * Linter ) lintImage ( config * WorkflowConfig , c * types . Container , area string ) error {
2017-03-05 07:56:08 +00:00
if len ( c . Image ) == 0 {
2023-11-04 14:30:47 +00:00
return newLinterError ( "Invalid or missing image" , config . File , fmt . Sprintf ( "%s.%s" , area , c . Name ) , false )
2017-03-05 07:56:08 +00:00
}
return nil
}
2024-08-31 17:04:47 +00:00
func ( l * Linter ) lintPrivilegedPlugins ( config * WorkflowConfig , c * types . Container , area string ) error {
// lint for conflicts of https://github.com/woodpecker-ci/woodpecker/pull/3918
2024-09-02 08:41:20 +00:00
if utils . MatchImage ( c . Image , "plugins/docker" , "plugins/gcr" , "plugins/ecr" , "woodpeckerci/plugin-docker-buildx" ) {
2024-11-11 22:34:20 +00:00
msg := fmt . Sprintf ( "The formerly privileged plugin `%s` is no longer privileged by default, if required, add it to `WOODPECKER_PLUGINS_PRIVILEGED`" , c . Image )
2024-08-31 17:04:47 +00:00
// check first if user did not add them back
2024-09-04 22:25:22 +00:00
if l . privilegedPlugins != nil && ! utils . MatchImageDynamic ( c . Image , * l . privilegedPlugins ... ) {
2024-08-31 17:04:47 +00:00
return newLinterError ( msg , config . File , fmt . Sprintf ( "%s.%s" , area , c . Name ) , false )
} else if l . privilegedPlugins == nil {
// if linter has no info of current privileged plugins, it's just a warning
return newLinterError ( msg , config . File , fmt . Sprintf ( "%s.%s" , area , c . Name ) , true )
}
}
return nil
}
2024-07-14 21:35:19 +00:00
func ( l * Linter ) lintSettings ( config * WorkflowConfig , c * types . Container , field string ) error {
if len ( c . Settings ) == 0 {
2017-07-21 21:52:52 +00:00
return nil
}
2024-07-14 21:35:19 +00:00
if len ( c . Commands ) != 0 {
2024-11-11 22:34:20 +00:00
return newLinterError ( "Cannot configure both `commands` and `settings`" , config . File , fmt . Sprintf ( "%s.%s" , field , c . Name ) , false )
2024-07-14 21:35:19 +00:00
}
if len ( c . Entrypoint ) != 0 {
2024-11-11 22:34:20 +00:00
return newLinterError ( "Cannot configure both `entrypoint` and `settings`" , config . File , fmt . Sprintf ( "%s.%s" , field , c . Name ) , false )
2024-07-14 21:35:19 +00:00
}
if len ( c . Environment ) != 0 {
2024-11-11 22:34:20 +00:00
return newLinterError ( "Should not configure both `environment` and `settings`" , config . File , fmt . Sprintf ( "%s.%s" , field , c . Name ) , true )
2024-08-15 05:40:14 +00:00
}
2024-08-15 16:58:51 +00:00
if len ( c . Secrets ) != 0 {
2024-11-11 22:34:20 +00:00
return newLinterError ( "Should not configure both `secrets` and `settings`" , config . File , fmt . Sprintf ( "%s.%s" , field , c . Name ) , true )
2017-07-21 21:52:52 +00:00
}
2017-03-05 07:56:08 +00:00
return nil
}
2023-11-04 14:30:47 +00:00
func ( l * Linter ) lintTrusted ( config * WorkflowConfig , c * types . Container , area string ) error {
yamlPath := fmt . Sprintf ( "%s.%s" , area , c . Name )
2024-05-30 16:53:03 +00:00
errors := [ ] string { }
2024-11-01 20:37:31 +00:00
if ! l . trusted . Security {
if c . Privileged {
2024-11-11 22:34:20 +00:00
errors = append ( errors , "Insufficient trust level to use `privileged` mode" )
2024-11-01 20:37:31 +00:00
}
2017-03-05 07:56:08 +00:00
}
2024-11-01 20:37:31 +00:00
if ! l . trusted . Network {
if len ( c . DNS ) != 0 {
2024-11-11 22:34:20 +00:00
errors = append ( errors , "Insufficient trust level to use custom `dns`" )
2024-11-01 20:37:31 +00:00
}
if len ( c . DNSSearch ) != 0 {
2024-11-11 22:34:20 +00:00
errors = append ( errors , "Insufficient trust level to use `dns_search`" )
2024-11-01 20:37:31 +00:00
}
if len ( c . ExtraHosts ) != 0 {
2024-11-11 22:34:20 +00:00
errors = append ( errors , "Insufficient trust level to use `extra_hosts`" )
2024-11-01 20:37:31 +00:00
}
if len ( c . NetworkMode ) != 0 {
2024-11-11 22:34:20 +00:00
errors = append ( errors , "Insufficient trust level to use `network_mode`" )
2024-11-01 20:37:31 +00:00
}
2017-03-05 07:56:08 +00:00
}
2024-11-01 20:37:31 +00:00
if ! l . trusted . Volumes {
if len ( c . Devices ) != 0 {
2024-11-11 22:34:20 +00:00
errors = append ( errors , "Insufficient trust level to use `devices`" )
2024-11-01 20:37:31 +00:00
}
if len ( c . Volumes . Volumes ) != 0 {
2024-11-11 22:34:20 +00:00
errors = append ( errors , "Insufficient trust level to use `volumes`" )
2024-11-01 20:37:31 +00:00
}
if len ( c . Tmpfs ) != 0 {
2024-11-11 22:34:20 +00:00
errors = append ( errors , "Insufficient trust level to use `tmpfs`" )
2024-11-01 20:37:31 +00:00
}
2023-11-04 14:30:47 +00:00
}
2024-05-30 16:53:03 +00:00
if len ( errors ) > 0 {
var err error
for _ , e := range errors {
err = multierr . Append ( err , newLinterError ( e , config . File , yamlPath , false ) )
}
return err
2023-11-03 10:44:03 +00:00
}
2023-11-04 14:30:47 +00:00
2023-11-03 10:44:03 +00:00
return nil
}
2023-11-04 14:30:47 +00:00
func ( l * Linter ) lintSchema ( config * WorkflowConfig ) error {
2023-11-03 10:44:03 +00:00
var linterErr error
2023-11-04 14:30:47 +00:00
schemaErrors , err := schema . LintString ( config . RawConfig )
2023-11-03 10:44:03 +00:00
if err != nil {
for _ , schemaError := range schemaErrors {
linterErr = multierr . Append ( linterErr , newLinterError (
schemaError . Description ( ) ,
2023-11-04 14:30:47 +00:00
config . File ,
2023-11-03 10:44:03 +00:00
schemaError . Field ( ) ,
true , // TODO: let pipelines fail if the schema is invalid
) )
}
2017-09-08 00:43:33 +00:00
}
2023-11-03 10:44:03 +00:00
return linterErr
}
2023-11-04 14:30:47 +00:00
func ( l * Linter ) lintDeprecations ( config * WorkflowConfig ) ( err error ) {
parsed := new ( types . Workflow )
err = xyaml . Unmarshal ( [ ] byte ( config . RawConfig ) , parsed )
if err != nil {
return err
}
2024-10-24 05:36:29 +00:00
for _ , container := range parsed . Steps . ContainerList {
if len ( container . Secrets ) > 0 {
err = multierr . Append ( err , & errorTypes . PipelineError {
Type : errorTypes . PipelineErrorTypeDeprecation ,
2024-11-11 22:34:20 +00:00
Message : "Usage of `secrets` is deprecated, use `environment` with `from_secret`" ,
2024-10-24 05:36:29 +00:00
Data : errors . DeprecationErrorData {
File : config . File ,
Field : fmt . Sprintf ( "steps.%s.secrets" , container . Name ) ,
Docs : "https://woodpecker-ci.org/docs/usage/secrets#usage" ,
} ,
IsWarning : true ,
} )
}
}
2024-08-31 17:04:47 +00:00
return nil
2023-11-03 10:44:03 +00:00
}
2024-02-10 16:33:05 +00:00
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 {
2024-08-15 16:58:51 +00:00
if len ( c . Event ) == 0 {
2024-02-10 16:33:05 +00:00
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 {
2024-08-15 16:58:51 +00:00
if len ( c . Event ) == 0 {
2024-02-10 16:33:05 +00:00
stepEventIndex = i
break
}
}
if stepEventIndex > - 1 {
field = fmt . Sprintf ( "steps.%s.when[%d]" , step . Name , stepEventIndex )
}
}
if field != "" {
2024-04-15 08:04:21 +00:00
err = multierr . Append ( err , & errorTypes . PipelineError {
Type : errorTypes . PipelineErrorTypeBadHabit ,
2024-11-11 22:34:20 +00:00
Message : "Set an event filter for all steps or the entire workflow on all items of the `when` block" ,
2024-04-24 14:07:16 +00:00
Data : errors . BadHabitErrorData {
2024-02-10 16:33:05 +00:00
File : config . File ,
Field : field ,
2024-04-24 14:07:16 +00:00
Docs : "https://woodpecker-ci.org/docs/usage/linter#event-filter-for-all-steps" ,
2024-02-10 16:33:05 +00:00
} ,
IsWarning : true ,
} )
}
}
}
return
2017-03-05 07:56:08 +00:00
}