Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions api/admin/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,38 @@ func UpdateSettings(c *gin.Context) {

l.Infof("platform admin: updating starlark exec limit to %d", *input.StarlarkExecLimit)
}

if input.BlockedImages != nil {
for _, restriction := range input.GetBlockedImages() {
if restriction.GetImage() == "" {
retErr := fmt.Errorf("blocked image entry missing image pattern")

util.HandleError(c, http.StatusBadRequest, retErr)

return
}
}

_s.SetBlockedImages(input.GetBlockedImages())

l.Infof("platform admin: updating blocked images to: %v", input.GetBlockedImages())
}

if input.WarnImages != nil {
for _, restriction := range input.GetWarnImages() {
if restriction.GetImage() == "" {
retErr := fmt.Errorf("warn image entry missing image pattern")

util.HandleError(c, http.StatusBadRequest, retErr)

return
}
}

_s.SetWarnImages(input.GetWarnImages())

l.Infof("platform admin: updating warn images to: %v", input.GetWarnImages())
}
}

if input.Queue != nil {
Expand Down
115 changes: 111 additions & 4 deletions api/types/settings/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,117 @@ package settings

import "fmt"

// ImageRestriction represents a container image pattern that is either
// blocked or warned about when used in a pipeline.
type ImageRestriction struct {
Image *string `json:"image,omitempty" yaml:"image,omitempty"`
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty"`
}

// GetImage returns the Image field.
//
// When the provided ImageRestriction type is nil, or the field within
// the type is nil, it returns the zero value for the field.
func (ir *ImageRestriction) GetImage() string {
if ir == nil || ir.Image == nil {
return ""
}

return *ir.Image
}

// GetReason returns the Reason field.
//
// When the provided ImageRestriction type is nil, or the field within
// the type is nil, it returns the zero value for the field.
func (ir *ImageRestriction) GetReason() string {
if ir == nil || ir.Reason == nil {
return ""
}

return *ir.Reason
}

// SetImage sets the Image field.
//
// When the provided ImageRestriction type is nil, it
// will set nothing and immediately return.
func (ir *ImageRestriction) SetImage(v string) {
if ir == nil {
return
}

ir.Image = &v
}

// SetReason sets the Reason field.
//
// When the provided ImageRestriction type is nil, it
// will set nothing and immediately return.
func (ir *ImageRestriction) SetReason(v string) {
if ir == nil {
return
}

ir.Reason = &v
}

type Compiler struct {
CloneImage *string `json:"clone_image,omitempty" yaml:"clone_image,omitempty"`
TemplateDepth *int `json:"template_depth,omitempty" yaml:"template_depth,omitempty"`
StarlarkExecLimit *int64 `json:"starlark_exec_limit,omitempty" yaml:"starlark_exec_limit,omitempty"`
CloneImage *string `json:"clone_image,omitempty" yaml:"clone_image,omitempty"`
TemplateDepth *int `json:"template_depth,omitempty" yaml:"template_depth,omitempty"`
StarlarkExecLimit *int64 `json:"starlark_exec_limit,omitempty" yaml:"starlark_exec_limit,omitempty"`
BlockedImages *[]ImageRestriction `json:"blocked_images,omitempty" yaml:"blocked_images,omitempty"`
WarnImages *[]ImageRestriction `json:"warn_images,omitempty" yaml:"warn_images,omitempty"`
}

// GetCloneImage returns the CloneImage field.
// GetBlockedImages returns the BlockedImages field.
//
// When the provided Compiler type is nil, or the field within
// the type is nil, it returns the zero value for the field.
func (cs *Compiler) GetBlockedImages() []ImageRestriction {
if cs == nil || cs.BlockedImages == nil {
return []ImageRestriction{}
}

return *cs.BlockedImages
}

// GetWarnImages returns the WarnImages field.
//
// When the provided Compiler type is nil, or the field within
// the type is nil, it returns the zero value for the field.
func (cs *Compiler) GetWarnImages() []ImageRestriction {
if cs == nil || cs.WarnImages == nil {
return []ImageRestriction{}
}

return *cs.WarnImages
}

// SetBlockedImages sets the BlockedImages field.
//
// When the provided Compiler type is nil, it
// will set nothing and immediately return.
func (cs *Compiler) SetBlockedImages(v []ImageRestriction) {
if cs == nil {
return
}

cs.BlockedImages = &v
}

// SetWarnImages sets the WarnImages field.
//
// When the provided Compiler type is nil, it
// will set nothing and immediately return.
func (cs *Compiler) SetWarnImages(v []ImageRestriction) {
if cs == nil {
return
}

cs.WarnImages = &v
}

// When the provided Compiler type is nil, or the field within
// the type is nil, it returns the zero value for the field.
func (cs *Compiler) GetCloneImage() string {
Expand Down Expand Up @@ -94,10 +197,14 @@ func (cs *Compiler) String() string {
CloneImage: %s,
TemplateDepth: %d,
StarlarkExecLimit: %d,
BlockedImages: %v,
WarnImages: %v,
}`,
cs.GetCloneImage(),
cs.GetTemplateDepth(),
cs.GetStarlarkExecLimit(),
cs.GetBlockedImages(),
cs.GetWarnImages(),
)
}

Expand Down
30 changes: 30 additions & 0 deletions api/types/settings/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ func TestTypes_Compiler_Getters(t *testing.T) {
if !reflect.DeepEqual(test.compiler.GetStarlarkExecLimit(), test.want.GetStarlarkExecLimit()) {
t.Errorf("GetStarlarkExecLimit is %v, want %v", test.compiler.GetStarlarkExecLimit(), test.want.GetStarlarkExecLimit())
}

if !reflect.DeepEqual(test.compiler.GetBlockedImages(), test.want.GetBlockedImages()) {
t.Errorf("GetBlockedImages is %v, want %v", test.compiler.GetBlockedImages(), test.want.GetBlockedImages())
}

if !reflect.DeepEqual(test.compiler.GetWarnImages(), test.want.GetWarnImages()) {
t.Errorf("GetWarnImages is %v, want %v", test.compiler.GetWarnImages(), test.want.GetWarnImages())
}
}
}

Expand Down Expand Up @@ -78,6 +86,18 @@ func TestTypes_Compiler_Setters(t *testing.T) {
if !reflect.DeepEqual(test.compiler.GetStarlarkExecLimit(), test.want.GetStarlarkExecLimit()) {
t.Errorf("SetStarlarkExecLimit is %v, want %v", test.compiler.GetStarlarkExecLimit(), test.want.GetStarlarkExecLimit())
}

test.compiler.SetBlockedImages(test.want.GetBlockedImages())

if !reflect.DeepEqual(test.compiler.GetBlockedImages(), test.want.GetBlockedImages()) {
t.Errorf("SetBlockedImages is %v, want %v", test.compiler.GetBlockedImages(), test.want.GetBlockedImages())
}

test.compiler.SetWarnImages(test.want.GetWarnImages())

if !reflect.DeepEqual(test.compiler.GetWarnImages(), test.want.GetWarnImages()) {
t.Errorf("SetWarnImages is %v, want %v", test.compiler.GetWarnImages(), test.want.GetWarnImages())
}
}
}

Expand All @@ -89,10 +109,14 @@ func TestTypes_Compiler_String(t *testing.T) {
CloneImage: %s,
TemplateDepth: %d,
StarlarkExecLimit: %d,
BlockedImages: %v,
WarnImages: %v,
}`,
cs.GetCloneImage(),
cs.GetTemplateDepth(),
cs.GetStarlarkExecLimit(),
cs.GetBlockedImages(),
cs.GetWarnImages(),
)

// run test
Expand All @@ -111,6 +135,12 @@ func testCompilerSettings() *Compiler {
cs.SetCloneImage("target/vela-git-slim:latest")
cs.SetTemplateDepth(1)
cs.SetStarlarkExecLimit(100)
cs.SetBlockedImages([]ImageRestriction{
{Image: new("docker.io/blocked/image:latest"), Reason: new("this image is blocked")},
})
cs.SetWarnImages([]ImageRestriction{
{Image: new("docker.io/deprecated/image:latest"), Reason: new("this image is deprecated")},
})

return cs
}
20 changes: 20 additions & 0 deletions compiler/native/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,16 @@ func (c *Client) compileSteps(ctx context.Context, p *yaml.Build, _pipeline *api
return nil, _pipeline, err
}

// check image restrictions (blocked → error, warned → warning)
imageWarnings, err := c.checkImageRestrictions(build)
if err != nil {
return nil, _pipeline, err
}

if len(imageWarnings) > 0 {
_pipeline.SetWarnings(append(_pipeline.GetWarnings(), imageWarnings...))
}

return build, _pipeline, nil
}

Expand Down Expand Up @@ -517,6 +527,16 @@ func (c *Client) compileStages(ctx context.Context, p *yaml.Build, _pipeline *ap
return nil, _pipeline, err
}

// check image restrictions (blocked → error, warned → warning)
imageWarnings, err := c.checkImageRestrictions(build)
if err != nil {
return nil, _pipeline, err
}

if len(imageWarnings) > 0 {
_pipeline.SetWarnings(append(_pipeline.GetWarnings(), imageWarnings...))
}

return build, _pipeline, nil
}

Expand Down
6 changes: 6 additions & 0 deletions compiler/native/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,11 @@ func (c *Client) SetSettings(s *settings.Platform) {
c.SetCloneImage(s.GetCloneImage())
c.SetTemplateDepth(s.GetTemplateDepth())
c.SetStarlarkExecLimit(s.GetStarlarkExecLimit())

// copy pointer fields directly to preserve nil vs empty-slice distinction
if s.Compiler != nil {
c.Compiler.BlockedImages = s.Compiler.BlockedImages
c.Compiler.WarnImages = s.Compiler.WarnImages
}
}
}
89 changes: 89 additions & 0 deletions compiler/native/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ package native

import (
"fmt"
"path/filepath"
"slices"

"github.com/hashicorp/go-multierror"

"github.com/go-vela/server/api/types/settings"
"github.com/go-vela/server/compiler/types/pipeline"
"github.com/go-vela/server/compiler/types/yaml"
"github.com/go-vela/server/constants"
"github.com/go-vela/server/internal/image"
)

// ValidateYAML verifies the yaml configuration is valid.
Expand Down Expand Up @@ -247,3 +250,89 @@ func validatePipelineContainers(s pipeline.ContainerSlice, reportCount, gitToken

return nil
}

// checkImageRestrictions inspects every container in the compiled pipeline against
// the platform's blocked and warn image lists. Blocked images cause compilation to
// fail. Warned images produce non-fatal warning strings that are surfaced on the
// build's Pipeline tab.
func (c *Client) checkImageRestrictions(p *pipeline.Build) ([]string, error) {
var (
result error
warnings []string
)

// collect all containers: steps, services, and secret origins
containers := make(pipeline.ContainerSlice, 0, len(p.Steps)+len(p.Services))
containers = append(containers, p.Steps...)
containers = append(containers, p.Services...)

for _, s := range p.Secrets {
if !s.Origin.Empty() {
containers = append(containers, s.Origin)
}
}

for _, stage := range p.Stages {
containers = append(containers, stage.Steps...)
}

for _, ctn := range containers {
// skip injected init and clone containers
if ctn.Name == constants.CloneName || ctn.Name == constants.InitName {
continue
}

for _, restriction := range c.GetBlockedImages() {
if matchesImagePattern(restriction.GetImage(), ctn.Image) {
result = multierror.Append(result,
fmt.Errorf("image %s for %s is blocked: %s", ctn.Image, ctn.Name, restriction.GetReason()),
)
}
}

for _, restriction := range c.GetWarnImages() {
if matchesImagePattern(restriction.GetImage(), ctn.Image) {
warnings = append(warnings,
fmt.Sprintf("image %s for %s: %s", ctn.Image, ctn.Name, restriction.GetReason()),
)
}
}
}

return warnings, result
}

// matchesImagePattern reports whether the provided image matches the given pattern.
// Patterns support glob wildcards via filepath.Match (e.g. "index.docker.io/org/*").
// Both the raw image and its normalized (fully-qualified) form are tested so that
// patterns can omit the registry prefix or tag.
func matchesImagePattern(pattern, img string) bool {
if pattern == "" || img == "" {
return false
}

// direct match against the image as provided
if ok, err := filepath.Match(pattern, img); err == nil && ok {
return true
}

// match against the normalized, fully-qualified image reference
normalized, err := image.ParseWithError(img)
if err == nil && normalized != img {
if ok, err := filepath.Match(pattern, normalized); err == nil && ok {
return true
}
}

return false
}

// imageRestrictionsFromSettings is a helper used in tests to build a Compiler
// settings value containing image restriction lists.
func imageRestrictionsFromSettings(blocked, warned []settings.ImageRestriction) settings.Compiler {
cs := settings.Compiler{}
cs.SetBlockedImages(blocked)
cs.SetWarnImages(warned)

return cs
}
Loading
Loading