From 7ac81505d078a02e4cab3d5b822732ecb46da937 Mon Sep 17 00:00:00 2001 From: JordanBrockopp Date: Tue, 31 Mar 2026 14:14:32 -0500 Subject: [PATCH 1/6] feat: init of image registry --- api/admin/settings.go | 32 +++++ api/types/settings/compiler.go | 115 +++++++++++++++- api/types/settings/compiler_test.go | 35 +++++ compiler/native/compile.go | 20 +++ compiler/native/settings.go | 6 + compiler/native/validate.go | 89 ++++++++++++ compiler/native/validate_test.go | 202 ++++++++++++++++++++++++++++ database/types/settings.go | 18 ++- database/types/settings_test.go | 23 ++++ 9 files changed, 533 insertions(+), 7 deletions(-) diff --git a/api/admin/settings.go b/api/admin/settings.go index ee0d4cd4c..d7b65e22b 100644 --- a/api/admin/settings.go +++ b/api/admin/settings.go @@ -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 { diff --git a/api/types/settings/compiler.go b/api/types/settings/compiler.go index 3a945c637..ef30cb1eb 100644 --- a/api/types/settings/compiler.go +++ b/api/types/settings/compiler.go @@ -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 { @@ -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(), ) } diff --git a/api/types/settings/compiler_test.go b/api/types/settings/compiler_test.go index a479fc8be..59fd070be 100644 --- a/api/types/settings/compiler_test.go +++ b/api/types/settings/compiler_test.go @@ -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()) + } } } @@ -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()) + } } } @@ -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 @@ -111,6 +135,17 @@ 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 } + +//go:fix inline +func stringPtr(s string) *string { + return new(s) +} diff --git a/compiler/native/compile.go b/compiler/native/compile.go index 931d93776..205e10bc5 100644 --- a/compiler/native/compile.go +++ b/compiler/native/compile.go @@ -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 } @@ -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 } diff --git a/compiler/native/settings.go b/compiler/native/settings.go index 1cecab3e2..127eb0258 100644 --- a/compiler/native/settings.go +++ b/compiler/native/settings.go @@ -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 + } } } diff --git a/compiler/native/validate.go b/compiler/native/validate.go index 3212f151c..1361ba5e7 100644 --- a/compiler/native/validate.go +++ b/compiler/native/validate.go @@ -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. @@ -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 +} diff --git a/compiler/native/validate_test.go b/compiler/native/validate_test.go index f782d2e2b..0f73f988a 100644 --- a/compiler/native/validate_test.go +++ b/compiler/native/validate_test.go @@ -7,6 +7,7 @@ import ( "fmt" "testing" + "github.com/go-vela/server/api/types/settings" "github.com/go-vela/server/compiler/types/pipeline" "github.com/go-vela/server/compiler/types/raw" "github.com/go-vela/server/compiler/types/yaml" @@ -812,3 +813,204 @@ func TestNative_Validate_Secrets_SecretOriginNameConflict(t *testing.T) { t.Errorf("Validate should have returned err") } } + +func TestNative_CheckImageRestrictions_BlockedImage(t *testing.T) { + compiler, err := FromCLICommand(context.Background(), testCommand(t, "http://foo.example.com")) + if err != nil { + t.Errorf("Unable to create new compiler: %v", err) + } + + compiler.Compiler = imageRestrictionsFromSettings( + []settings.ImageRestriction{ + {Image: new("docker.io/blocked/image:latest"), Reason: new("this image is not allowed")}, + }, + nil, + ) + + p := &pipeline.Build{ + Steps: pipeline.ContainerSlice{ + {Name: "blocked-step", Image: "blocked/image:latest"}, + {Name: "allowed-step", Image: "alpine:latest"}, + }, + } + + warnings, err := compiler.checkImageRestrictions(p) + if err == nil { + t.Errorf("checkImageRestrictions should have returned err for blocked image") + } + + if len(warnings) != 0 { + t.Errorf("checkImageRestrictions should not have returned warnings, got: %v", warnings) + } +} + +func TestNative_CheckImageRestrictions_WarnImage(t *testing.T) { + compiler, err := FromCLICommand(context.Background(), testCommand(t, "http://foo.example.com")) + if err != nil { + t.Errorf("Unable to create new compiler: %v", err) + } + + compiler.Compiler = imageRestrictionsFromSettings( + nil, + []settings.ImageRestriction{ + {Image: new("docker.io/deprecated/image:latest"), Reason: new("this image is deprecated")}, + }, + ) + + p := &pipeline.Build{ + Steps: pipeline.ContainerSlice{ + {Name: "warn-step", Image: "deprecated/image:latest"}, + {Name: "fine-step", Image: "alpine:latest"}, + }, + } + + warnings, err := compiler.checkImageRestrictions(p) + if err != nil { + t.Errorf("checkImageRestrictions returned unexpected err: %v", err) + } + + if len(warnings) != 1 { + t.Errorf("checkImageRestrictions should have returned 1 warning, got: %d", len(warnings)) + } +} + +func TestNative_CheckImageRestrictions_WildcardPattern(t *testing.T) { + compiler, err := FromCLICommand(context.Background(), testCommand(t, "http://foo.example.com")) + if err != nil { + t.Errorf("Unable to create new compiler: %v", err) + } + + compiler.Compiler = imageRestrictionsFromSettings( + []settings.ImageRestriction{ + {Image: new("docker.io/blocked/*"), Reason: new("entire org is blocked")}, + }, + nil, + ) + + p := &pipeline.Build{ + Steps: pipeline.ContainerSlice{ + {Name: "step-a", Image: "blocked/image-one:latest"}, + {Name: "step-b", Image: "blocked/image-two:v2"}, + {Name: "step-c", Image: "allowed/image:latest"}, + }, + } + + _, err = compiler.checkImageRestrictions(p) + if err == nil { + t.Errorf("checkImageRestrictions should have returned err for wildcard blocked images") + } +} + +func TestNative_CheckImageRestrictions_NoMatch(t *testing.T) { + compiler, err := FromCLICommand(context.Background(), testCommand(t, "http://foo.example.com")) + if err != nil { + t.Errorf("Unable to create new compiler: %v", err) + } + + compiler.Compiler = imageRestrictionsFromSettings( + []settings.ImageRestriction{ + {Image: new("docker.io/blocked/image:latest"), Reason: new("blocked")}, + }, + []settings.ImageRestriction{ + {Image: new("docker.io/deprecated/image:latest"), Reason: new("deprecated")}, + }, + ) + + p := &pipeline.Build{ + Steps: pipeline.ContainerSlice{ + {Name: "fine-step", Image: "alpine:latest"}, + }, + } + + warnings, err := compiler.checkImageRestrictions(p) + if err != nil { + t.Errorf("checkImageRestrictions returned unexpected err: %v", err) + } + + if len(warnings) != 0 { + t.Errorf("checkImageRestrictions should not have returned warnings, got: %v", warnings) + } +} + +func TestNative_CheckImageRestrictions_EmptyLists(t *testing.T) { + compiler, err := FromCLICommand(context.Background(), testCommand(t, "http://foo.example.com")) + if err != nil { + t.Errorf("Unable to create new compiler: %v", err) + } + + p := &pipeline.Build{ + Steps: pipeline.ContainerSlice{ + {Name: "step", Image: "alpine:latest"}, + }, + } + + warnings, err := compiler.checkImageRestrictions(p) + if err != nil { + t.Errorf("checkImageRestrictions returned unexpected err: %v", err) + } + + if len(warnings) != 0 { + t.Errorf("checkImageRestrictions should not have returned warnings, got: %v", warnings) + } +} + +func TestNative_MatchesImagePattern(t *testing.T) { + tests := []struct { + name string + pattern string + image string + want bool + }{ + { + name: "exact match normalized", + pattern: "docker.io/library/alpine:latest", + image: "alpine:latest", + want: true, + }, + { + name: "wildcard tag", + pattern: "docker.io/org/image:*", + image: "org/image:v1.2.3", + want: true, + }, + { + name: "wildcard org", + pattern: "docker.io/blocked/*", + image: "blocked/tool:latest", + want: true, + }, + { + name: "no match", + pattern: "docker.io/blocked/image:latest", + image: "allowed/image:latest", + want: false, + }, + { + name: "empty pattern", + pattern: "", + image: "alpine:latest", + want: false, + }, + { + name: "empty image", + pattern: "alpine:latest", + image: "", + want: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := matchesImagePattern(test.pattern, test.image) + + if got != test.want { + t.Errorf("matchesImagePattern(%q, %q) = %v, want %v", test.pattern, test.image, got, test.want) + } + }) + } +} + +//go:fix inline +func strPtr(s string) *string { + return new(s) +} diff --git a/database/types/settings.go b/database/types/settings.go index 209b93735..60faa966d 100644 --- a/database/types/settings.go +++ b/database/types/settings.go @@ -44,9 +44,11 @@ type ( // Compiler is the database representation of compiler settings. Compiler struct { - CloneImage sql.NullString `json:"clone_image" sql:"clone_image"` - TemplateDepth sql.NullInt64 `json:"template_depth" sql:"template_depth"` - StarlarkExecLimit sql.NullInt64 `json:"starlark_exec_limit" sql:"starlark_exec_limit"` + CloneImage sql.NullString `json:"clone_image" sql:"clone_image"` + TemplateDepth sql.NullInt64 `json:"template_depth" sql:"template_depth"` + StarlarkExecLimit sql.NullInt64 `json:"starlark_exec_limit" sql:"starlark_exec_limit"` + BlockedImages []settings.ImageRestriction `json:"blocked_images,omitempty" sql:"blocked_images"` + WarnImages []settings.ImageRestriction `json:"warn_images,omitempty" sql:"warn_images"` } // Queue is the database representation of queue settings. @@ -179,6 +181,14 @@ func (ps *Platform) ToAPI() *settings.Platform { psAPI.SetTemplateDepth(int(ps.TemplateDepth.Int64)) psAPI.SetStarlarkExecLimit(ps.StarlarkExecLimit.Int64) + if len(ps.BlockedImages) > 0 { + psAPI.SetBlockedImages(ps.BlockedImages) + } + + if len(ps.WarnImages) > 0 { + psAPI.SetWarnImages(ps.WarnImages) + } + psAPI.Queue = new(settings.Queue) psAPI.SetRoutes(ps.Routes) @@ -262,6 +272,8 @@ func SettingsFromAPI(s *settings.Platform) *Platform { CloneImage: sql.NullString{String: s.GetCloneImage(), Valid: true}, TemplateDepth: sql.NullInt64{Int64: int64(s.GetTemplateDepth()), Valid: true}, StarlarkExecLimit: sql.NullInt64{Int64: s.GetStarlarkExecLimit(), Valid: true}, + BlockedImages: s.GetBlockedImages(), + WarnImages: s.GetWarnImages(), }, Queue: Queue{ Routes: pq.StringArray(s.GetRoutes()), diff --git a/database/types/settings_test.go b/database/types/settings_test.go index eacc05397..348d33576 100644 --- a/database/types/settings_test.go +++ b/database/types/settings_test.go @@ -66,6 +66,12 @@ func TestTypes_Platform_ToAPI(t *testing.T) { want.SetCloneImage("target/vela-git-slim:latest") want.SetTemplateDepth(10) want.SetStarlarkExecLimit(100) + want.SetBlockedImages([]api.ImageRestriction{ + {Image: new("docker.io/blocked/image:latest"), Reason: new("this image is blocked")}, + }) + want.SetWarnImages([]api.ImageRestriction{ + {Image: new("docker.io/deprecated/image:latest"), Reason: new("this image is deprecated")}, + }) want.Queue = new(api.Queue) want.SetRoutes([]string{"vela"}) @@ -210,6 +216,12 @@ func TestTypes_Platform_PlatformFromAPI(t *testing.T) { s.SetCloneImage("target/vela-git-slim:latest") s.SetTemplateDepth(10) s.SetStarlarkExecLimit(100) + s.SetBlockedImages([]api.ImageRestriction{ + {Image: new("docker.io/blocked/image:latest"), Reason: new("this image is blocked")}, + }) + s.SetWarnImages([]api.ImageRestriction{ + {Image: new("docker.io/deprecated/image:latest"), Reason: new("this image is deprecated")}, + }) s.Queue = new(api.Queue) s.SetRoutes([]string{"vela"}) @@ -246,6 +258,12 @@ func testPlatform() *Platform { CloneImage: sql.NullString{String: "target/vela-git-slim:latest", Valid: true}, TemplateDepth: sql.NullInt64{Int64: 10, Valid: true}, StarlarkExecLimit: sql.NullInt64{Int64: 100, Valid: true}, + BlockedImages: []api.ImageRestriction{ + {Image: new("docker.io/blocked/image:latest"), Reason: new("this image is blocked")}, + }, + WarnImages: []api.ImageRestriction{ + {Image: new("docker.io/deprecated/image:latest"), Reason: new("this image is deprecated")}, + }, }, Queue: Queue{ Routes: []string{"vela"}, @@ -275,3 +293,8 @@ func testPlatform() *Platform { UpdatedBy: sql.NullString{String: "", Valid: true}, } } + +//go:fix inline +func stringPtr(s string) *string { + return new(s) +} From 61ce65677e500f7f42d5beeece8cf655a9ea7078 Mon Sep 17 00:00:00 2001 From: JordanBrockopp Date: Tue, 31 Mar 2026 14:34:56 -0500 Subject: [PATCH 2/6] chore: address linter feedback --- api/types/settings/compiler_test.go | 5 ----- compiler/native/validate_test.go | 5 ----- database/types/settings.go | 6 +++--- database/types/settings_test.go | 5 ----- 4 files changed, 3 insertions(+), 18 deletions(-) diff --git a/api/types/settings/compiler_test.go b/api/types/settings/compiler_test.go index 59fd070be..3ad0b2bf6 100644 --- a/api/types/settings/compiler_test.go +++ b/api/types/settings/compiler_test.go @@ -144,8 +144,3 @@ func testCompilerSettings() *Compiler { return cs } - -//go:fix inline -func stringPtr(s string) *string { - return new(s) -} diff --git a/compiler/native/validate_test.go b/compiler/native/validate_test.go index 0f73f988a..d0f7f05fc 100644 --- a/compiler/native/validate_test.go +++ b/compiler/native/validate_test.go @@ -1009,8 +1009,3 @@ func TestNative_MatchesImagePattern(t *testing.T) { }) } } - -//go:fix inline -func strPtr(s string) *string { - return new(s) -} diff --git a/database/types/settings.go b/database/types/settings.go index 60faa966d..3cb0b016c 100644 --- a/database/types/settings.go +++ b/database/types/settings.go @@ -44,9 +44,9 @@ type ( // Compiler is the database representation of compiler settings. Compiler struct { - CloneImage sql.NullString `json:"clone_image" sql:"clone_image"` - TemplateDepth sql.NullInt64 `json:"template_depth" sql:"template_depth"` - StarlarkExecLimit sql.NullInt64 `json:"starlark_exec_limit" sql:"starlark_exec_limit"` + CloneImage sql.NullString `json:"clone_image" sql:"clone_image"` + TemplateDepth sql.NullInt64 `json:"template_depth" sql:"template_depth"` + StarlarkExecLimit sql.NullInt64 `json:"starlark_exec_limit" sql:"starlark_exec_limit"` BlockedImages []settings.ImageRestriction `json:"blocked_images,omitempty" sql:"blocked_images"` WarnImages []settings.ImageRestriction `json:"warn_images,omitempty" sql:"warn_images"` } diff --git a/database/types/settings_test.go b/database/types/settings_test.go index 348d33576..a15ba77c2 100644 --- a/database/types/settings_test.go +++ b/database/types/settings_test.go @@ -293,8 +293,3 @@ func testPlatform() *Platform { UpdatedBy: sql.NullString{String: "", Valid: true}, } } - -//go:fix inline -func stringPtr(s string) *string { - return new(s) -} From 5910e0dbe6a81e263115b97384db10c0c4b40df7 Mon Sep 17 00:00:00 2001 From: JordanBrockopp Date: Fri, 10 Apr 2026 10:50:13 -0500 Subject: [PATCH 3/6] chore: finalize --- api/build/compile_publish.go | 20 ++++++++++++++++ compiler/native/native.go | 2 ++ compiler/native/settings.go | 8 ++----- compiler/native/validate.go | 23 +++++------------- compiler/native/validate_test.go | 35 +++++++++++++--------------- database/types/settings.go | 40 +++++++++++++++++++++----------- 6 files changed, 73 insertions(+), 55 deletions(-) diff --git a/api/build/compile_publish.go b/api/build/compile_publish.go index 7a6c7c894..02b95b786 100644 --- a/api/build/compile_publish.go +++ b/api/build/compile_publish.go @@ -346,6 +346,26 @@ func CompileAndPublish( "repo": r.GetName(), "repo_id": r.GetID(), }).Info("pipeline created") + } else { + // reset the pipeline warnings if the compiled pipeline returned any + // + // The pipeline warnings can change at any time due to the image registry since + // new images could be added to the blocked or warning list. To account for this, + // we update the pipeline warnings to match what was compiled with the latest + // results. In general, this shouldn't be called often since we create a new + // pipeline record for every new commit so this covers scenarios where this + // isn't the case such as restarting a build. + if len(compiled.GetWarnings()) > 0 { + pipeline.SetWarnings(compiled.GetWarnings()) + + // send API call to update the pipeline + pipeline, err = database.UpdatePipeline(ctx, pipeline) + if err != nil { + retErr := fmt.Errorf("%s: failed to update pipeline for %s: %w", baseErr, r.GetFullName(), err) + + return nil, nil, http.StatusInternalServerError, retErr + } + } } b.SetPipelineID(pipeline.GetID()) diff --git a/compiler/native/native.go b/compiler/native/native.go index 4952b1a29..538871d7b 100644 --- a/compiler/native/native.go +++ b/compiler/native/native.go @@ -140,6 +140,8 @@ func (c *Client) Duplicate() compiler.Engine { cc.CloneImage = c.CloneImage cc.TemplateDepth = c.TemplateDepth cc.StarlarkExecLimit = c.StarlarkExecLimit + cc.BlockedImages = c.BlockedImages + cc.WarnImages = c.WarnImages cc.TemplateCache = make(map[string][]byte) return cc diff --git a/compiler/native/settings.go b/compiler/native/settings.go index 127eb0258..cf6db8484 100644 --- a/compiler/native/settings.go +++ b/compiler/native/settings.go @@ -17,11 +17,7 @@ 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 - } + c.SetBlockedImages(s.GetBlockedImages()) + c.SetWarnImages(s.GetWarnImages()) } } diff --git a/compiler/native/validate.go b/compiler/native/validate.go index 1361ba5e7..1eaccdd0e 100644 --- a/compiler/native/validate.go +++ b/compiler/native/validate.go @@ -9,7 +9,6 @@ import ( "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" @@ -261,17 +260,17 @@ func (c *Client) checkImageRestrictions(p *pipeline.Build) ([]string, 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...) + // collect all containers from the steps and services + containers := append(p.Steps, p.Services...) + // collect all containers from the secrets for _, s := range p.Secrets { if !s.Origin.Empty() { containers = append(containers, s.Origin) } } + // collect all containers from the stages for _, stage := range p.Stages { containers = append(containers, stage.Steps...) } @@ -285,7 +284,7 @@ func (c *Client) checkImageRestrictions(p *pipeline.Build) ([]string, error) { 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()), + fmt.Errorf("image %s for container %s is blocked: %s", ctn.Image, ctn.Name, restriction.GetReason()), ) } } @@ -293,7 +292,7 @@ func (c *Client) checkImageRestrictions(p *pipeline.Build) ([]string, error) { 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()), + fmt.Sprintf("image %s for container %s has warning: %s", ctn.Image, ctn.Name, restriction.GetReason()), ) } } @@ -326,13 +325,3 @@ func matchesImagePattern(pattern, img string) bool { 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 -} diff --git a/compiler/native/validate_test.go b/compiler/native/validate_test.go index d0f7f05fc..e2e35bb68 100644 --- a/compiler/native/validate_test.go +++ b/compiler/native/validate_test.go @@ -820,12 +820,11 @@ func TestNative_CheckImageRestrictions_BlockedImage(t *testing.T) { t.Errorf("Unable to create new compiler: %v", err) } - compiler.Compiler = imageRestrictionsFromSettings( - []settings.ImageRestriction{ + compiler.Compiler = settings.Compiler{ + BlockedImages: &[]settings.ImageRestriction{ {Image: new("docker.io/blocked/image:latest"), Reason: new("this image is not allowed")}, }, - nil, - ) + } p := &pipeline.Build{ Steps: pipeline.ContainerSlice{ @@ -850,12 +849,11 @@ func TestNative_CheckImageRestrictions_WarnImage(t *testing.T) { t.Errorf("Unable to create new compiler: %v", err) } - compiler.Compiler = imageRestrictionsFromSettings( - nil, - []settings.ImageRestriction{ + compiler.Compiler = settings.Compiler{ + WarnImages: &[]settings.ImageRestriction{ {Image: new("docker.io/deprecated/image:latest"), Reason: new("this image is deprecated")}, }, - ) + } p := &pipeline.Build{ Steps: pipeline.ContainerSlice{ @@ -880,12 +878,11 @@ func TestNative_CheckImageRestrictions_WildcardPattern(t *testing.T) { t.Errorf("Unable to create new compiler: %v", err) } - compiler.Compiler = imageRestrictionsFromSettings( - []settings.ImageRestriction{ - {Image: new("docker.io/blocked/*"), Reason: new("entire org is blocked")}, + compiler.Compiler = settings.Compiler{ + BlockedImages: &[]settings.ImageRestriction{ + {Image: new("docker.io/blocked/*"), Reason: new("entire namespace is blocked")}, }, - nil, - ) + } p := &pipeline.Build{ Steps: pipeline.ContainerSlice{ @@ -907,14 +904,14 @@ func TestNative_CheckImageRestrictions_NoMatch(t *testing.T) { t.Errorf("Unable to create new compiler: %v", err) } - compiler.Compiler = imageRestrictionsFromSettings( - []settings.ImageRestriction{ - {Image: new("docker.io/blocked/image:latest"), Reason: new("blocked")}, + compiler.Compiler = settings.Compiler{ + BlockedImages: &[]settings.ImageRestriction{ + {Image: new("docker.io/blocked/image:latest"), Reason: new("this image is not allowed")}, }, - []settings.ImageRestriction{ - {Image: new("docker.io/deprecated/image:latest"), Reason: new("deprecated")}, + WarnImages: &[]settings.ImageRestriction{ + {Image: new("docker.io/deprecated/image:latest"), Reason: new("this image is deprecated")}, }, - ) + } p := &pipeline.Build{ Steps: pipeline.ContainerSlice{ diff --git a/database/types/settings.go b/database/types/settings.go index 3cb0b016c..3a2c091a2 100644 --- a/database/types/settings.go +++ b/database/types/settings.go @@ -44,11 +44,11 @@ type ( // Compiler is the database representation of compiler settings. Compiler struct { - CloneImage sql.NullString `json:"clone_image" sql:"clone_image"` - TemplateDepth sql.NullInt64 `json:"template_depth" sql:"template_depth"` - StarlarkExecLimit sql.NullInt64 `json:"starlark_exec_limit" sql:"starlark_exec_limit"` - BlockedImages []settings.ImageRestriction `json:"blocked_images,omitempty" sql:"blocked_images"` - WarnImages []settings.ImageRestriction `json:"warn_images,omitempty" sql:"warn_images"` + CloneImage sql.NullString `json:"clone_image" sql:"clone_image"` + TemplateDepth sql.NullInt64 `json:"template_depth" sql:"template_depth"` + StarlarkExecLimit sql.NullInt64 `json:"starlark_exec_limit" sql:"starlark_exec_limit"` + BlockedImages ImageRestrictionJSON `json:"blocked_images" sql:"blocked_images"` + WarnImages ImageRestrictionJSON `json:"warn_images" sql:"warn_images"` } // Queue is the database representation of queue settings. @@ -62,8 +62,28 @@ type ( OrgRoleMap map[string]string `json:"org_role_map" sql:"org_role_map"` TeamRoleMap map[string]string `json:"team_role_map" sql:"team_role_map"` } + + ImageRestrictionJSON []settings.ImageRestriction ) +// Value - Implementation of valuer for database/sql for ImageRestrictionJSON. +func (i ImageRestrictionJSON) Value() (driver.Value, error) { + valueString, err := json.Marshal(i) + return string(valueString), err +} + +// Scan - Implement the database/sql scanner interface for ImageRestrictionJSON. +func (i *ImageRestrictionJSON) Scan(value any) error { + switch v := value.(type) { + case []byte: + return json.Unmarshal(v, &i) + case string: + return json.Unmarshal([]byte(v), &i) + default: + return fmt.Errorf("wrong type for repos: %T", v) + } +} + // Value - Implementation of valuer for database/sql for Compiler. func (r Compiler) Value() (driver.Value, error) { valueString, err := json.Marshal(r) @@ -180,14 +200,8 @@ func (ps *Platform) ToAPI() *settings.Platform { psAPI.SetCloneImage(ps.CloneImage.String) psAPI.SetTemplateDepth(int(ps.TemplateDepth.Int64)) psAPI.SetStarlarkExecLimit(ps.StarlarkExecLimit.Int64) - - if len(ps.BlockedImages) > 0 { - psAPI.SetBlockedImages(ps.BlockedImages) - } - - if len(ps.WarnImages) > 0 { - psAPI.SetWarnImages(ps.WarnImages) - } + psAPI.SetBlockedImages(ps.BlockedImages) + psAPI.SetWarnImages(ps.WarnImages) psAPI.Queue = new(settings.Queue) psAPI.SetRoutes(ps.Routes) From 296c4a25738437d577c555402473d28edce8da5b Mon Sep 17 00:00:00 2001 From: JordanBrockopp Date: Fri, 10 Apr 2026 11:23:30 -0500 Subject: [PATCH 4/6] chore: finalize --- api/types/settings/compiler.go | 13 +++++++++ api/types/settings/compiler_test.go | 41 +++++++++++++++++++++++++++++ compiler/native/native.go | 3 +++ database/settings/create_test.go | 9 ++++++- database/settings/table.go | 2 +- database/settings/update_test.go | 9 ++++++- router/middleware/compiler_test.go | 29 +++++++++++++++++++- 7 files changed, 102 insertions(+), 4 deletions(-) diff --git a/api/types/settings/compiler.go b/api/types/settings/compiler.go index ef30cb1eb..93db2fd09 100644 --- a/api/types/settings/compiler.go +++ b/api/types/settings/compiler.go @@ -59,6 +59,17 @@ func (ir *ImageRestriction) SetReason(v string) { ir.Reason = &v } +// String implements the Stringer interface for the ImageRestriction type. +func (ir *ImageRestriction) String() string { + return fmt.Sprintf(`{ + Image: %s, + Reason: %s, +}`, + ir.GetImage(), + ir.GetReason(), + ) +} + type Compiler struct { CloneImage *string `json:"clone_image,omitempty" yaml:"clone_image,omitempty"` TemplateDepth *int `json:"template_depth,omitempty" yaml:"template_depth,omitempty"` @@ -214,6 +225,8 @@ func CompilerMockEmpty() Compiler { cs.SetCloneImage("") cs.SetTemplateDepth(0) cs.SetStarlarkExecLimit(0) + cs.SetBlockedImages(nil) + cs.SetWarnImages(nil) return cs } diff --git a/api/types/settings/compiler_test.go b/api/types/settings/compiler_test.go index 3ad0b2bf6..08494e34a 100644 --- a/api/types/settings/compiler_test.go +++ b/api/types/settings/compiler_test.go @@ -8,6 +8,47 @@ import ( "testing" ) +func TestTypes_ImageRestriction_String(t *testing.T) { + // setup types + blocked := ImageRestriction{ + Image: new("docker.io/blocked/image:latest"), + Reason: new("this image is blocked"), + } + + warning := ImageRestriction{ + Image: new("docker.io/deprecated/image:latest"), + Reason: new("this image is deprecated"), + } + + wantBlocked := fmt.Sprintf(`{ + Image: %s, + Reason: %s, +}`, + blocked.GetImage(), + blocked.GetReason(), + ) + + wantWarning := fmt.Sprintf(`{ + Image: %s, + Reason: %s, +}`, + warning.GetImage(), + warning.GetReason(), + ) + + // run test + gotBlocked := blocked.String() + if !reflect.DeepEqual(gotBlocked, wantBlocked) { + t.Errorf("String is %v, want %v", gotBlocked, wantBlocked) + } + + gotWarning := warning.String() + if !reflect.DeepEqual(gotWarning, wantWarning) { + t.Errorf("String is %v, want %v", gotWarning, wantWarning) + } + +} + func TestTypes_Compiler_Getters(t *testing.T) { // setup tests tests := []struct { diff --git a/compiler/native/native.go b/compiler/native/native.go index 538871d7b..97eb47561 100644 --- a/compiler/native/native.go +++ b/compiler/native/native.go @@ -109,6 +109,9 @@ func FromCLICommand(ctx context.Context, cmd *cli.Command) (*Client, error) { c.UsePrivateGithub = true } + c.SetBlockedImages(nil) + c.SetWarnImages(nil) + c.TemplateCache = make(map[string][]byte) return c, nil diff --git a/database/settings/create_test.go b/database/settings/create_test.go index 696491ec0..56ae496d2 100644 --- a/database/settings/create_test.go +++ b/database/settings/create_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/server/api/types/settings" ) func TestSettings_Engine_CreateSettings(t *testing.T) { @@ -17,6 +18,12 @@ func TestSettings_Engine_CreateSettings(t *testing.T) { _settings.SetCloneImage("target/vela-git-slim:latest") _settings.SetTemplateDepth(10) _settings.SetStarlarkExecLimit(100) + _settings.SetBlockedImages([]settings.ImageRestriction{ + {Image: new("docker.io/blocked/image:latest"), Reason: new("this image is blocked")}, + }) + _settings.SetWarnImages([]settings.ImageRestriction{ + {Image: new("docker.io/deprecated/image:latest"), Reason: new("this image is deprecated")}, + }) _settings.SetRoutes([]string{"vela"}) _settings.SetRepoRoleMap(map[string]string{"admin": "admin", "triage": "read"}) _settings.SetOrgRoleMap(map[string]string{"admin": "admin", "member": "read"}) @@ -41,7 +48,7 @@ func TestSettings_Engine_CreateSettings(t *testing.T) { // ensure the mock expects the query _mock.ExpectQuery(`INSERT INTO "settings" ("compiler","queue","scm","repo_allowlist","schedule_allowlist","max_dashboard_repos","queue_restart_limit","enable_repo_secrets","enable_org_secrets","enable_shared_secrets","created_at","updated_at","updated_by","id") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING "id"`). - WithArgs(`{"clone_image":{"String":"target/vela-git-slim:latest","Valid":true},"template_depth":{"Int64":10,"Valid":true},"starlark_exec_limit":{"Int64":100,"Valid":true}}`, + WithArgs(`{"clone_image":{"String":"target/vela-git-slim:latest","Valid":true},"template_depth":{"Int64":10,"Valid":true},"starlark_exec_limit":{"Int64":100,"Valid":true},"blocked_images":[{"image":"docker.io/blocked/image:latest","reason":"this image is blocked"}],"warn_images":[{"image":"docker.io/deprecated/image:latest","reason":"this image is deprecated"}]}`, `{"routes":["vela"]}`, `{"repo_role_map":{"admin":"admin","triage":"read"},"org_role_map":{"admin":"admin","member":"read"},"team_role_map":{"admin":"admin"}}`, `{"octocat/hello-world"}`, `{"*"}`, 10, 30, true, true, true, 1, 1, ``, 1). WillReturnRows(_rows) diff --git a/database/settings/table.go b/database/settings/table.go index d746bec20..e2c66d12d 100644 --- a/database/settings/table.go +++ b/database/settings/table.go @@ -17,7 +17,7 @@ settings ( id SERIAL PRIMARY KEY, compiler JSON DEFAULT NULL, queue JSON DEFAULT NULL, - scm JSON DEFAULT NULL, + scm JSON DEFAULT NULL, repo_allowlist VARCHAR(1000), schedule_allowlist VARCHAR(1000), max_dashboard_repos INTEGER, diff --git a/database/settings/update_test.go b/database/settings/update_test.go index 7c7fa23d3..61e4743a7 100644 --- a/database/settings/update_test.go +++ b/database/settings/update_test.go @@ -9,6 +9,7 @@ import ( "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/server/api/types/settings" "github.com/go-vela/server/database/testutils" ) @@ -19,6 +20,12 @@ func TestSettings_Engine_UpdateSettings(t *testing.T) { _settings.SetCloneImage("target/vela-git-slim:latest") _settings.SetTemplateDepth(10) _settings.SetStarlarkExecLimit(100) + _settings.SetBlockedImages([]settings.ImageRestriction{ + {Image: new("docker.io/blocked/image:latest"), Reason: new("this image is blocked")}, + }) + _settings.SetWarnImages([]settings.ImageRestriction{ + {Image: new("docker.io/deprecated/image:latest"), Reason: new("this image is deprecated")}, + }) _settings.SetRoutes([]string{"vela", "large"}) _settings.SetRepoRoleMap(map[string]string{"admin": "admin", "triage": "read"}) _settings.SetOrgRoleMap(map[string]string{"admin": "admin", "member": "read"}) @@ -40,7 +47,7 @@ func TestSettings_Engine_UpdateSettings(t *testing.T) { // ensure the mock expects the query _mock.ExpectExec(`UPDATE "settings" SET "compiler"=$1,"queue"=$2,"scm"=$3,"repo_allowlist"=$4,"schedule_allowlist"=$5,"max_dashboard_repos"=$6,"queue_restart_limit"=$7,"enable_repo_secrets"=$8,"enable_org_secrets"=$9,"enable_shared_secrets"=$10,"created_at"=$11,"updated_at"=$12,"updated_by"=$13 WHERE "id" = $14`). - WithArgs(`{"clone_image":{"String":"target/vela-git-slim:latest","Valid":true},"template_depth":{"Int64":10,"Valid":true},"starlark_exec_limit":{"Int64":100,"Valid":true}}`, + WithArgs(`{"clone_image":{"String":"target/vela-git-slim:latest","Valid":true},"template_depth":{"Int64":10,"Valid":true},"starlark_exec_limit":{"Int64":100,"Valid":true},"blocked_images":[{"image":"docker.io/blocked/image:latest","reason":"this image is blocked"}],"warn_images":[{"image":"docker.io/deprecated/image:latest","reason":"this image is deprecated"}]}`, `{"routes":["vela","large"]}`, `{"repo_role_map":{"admin":"admin","triage":"read"},"org_role_map":{"admin":"admin","member":"read"},"team_role_map":{"admin":"admin"}}`, `{"octocat/hello-world"}`, `{"*"}`, 10, 30, true, true, true, 1, testutils.AnyArgument{}, "octocat", 1). WillReturnResult(sqlmock.NewResult(1, 1)) diff --git a/router/middleware/compiler_test.go b/router/middleware/compiler_test.go index c3bcf09f5..76e172122 100644 --- a/router/middleware/compiler_test.go +++ b/router/middleware/compiler_test.go @@ -34,6 +34,18 @@ func TestMiddleware_CompilerNative(t *testing.T) { want, _ := native.FromCLICommand(context.Background(), c) want.SetCloneImage(wantCloneImage) + want.SetBlockedImages([]settings.ImageRestriction{ + { + Image: new("docker.io/blocked/image:latest"), + Reason: new("this image is blocked"), + }, + }) + want.SetWarnImages([]settings.ImageRestriction{ + { + Image: new("docker.io/deprecated/image:latest"), + Reason: new("this image is deprecated"), + }, + }) var got compiler.Engine @@ -48,9 +60,24 @@ func TestMiddleware_CompilerNative(t *testing.T) { engine.Use(func() gin.HandlerFunc { return func(c *gin.Context) { s := settings.Platform{ - Compiler: &settings.Compiler{}, + Compiler: &settings.Compiler{ + BlockedImages: &[]settings.ImageRestriction{ + { + Image: new("docker.io/blocked/image:latest"), + Reason: new("this image is blocked"), + }, + }, + WarnImages: &[]settings.ImageRestriction{ + { + Image: new("docker.io/deprecated/image:latest"), + Reason: new("this image is deprecated"), + }, + }, + }, } s.SetCloneImage(wantCloneImage) + s.SetBlockedImages(*want.BlockedImages) + s.SetWarnImages(*want.WarnImages) sMiddleware.ToContext(c, &s) From 3c141f8286a9c2578cb1de150c90202a6c1fbaa5 Mon Sep 17 00:00:00 2001 From: JordanBrockopp Date: Fri, 10 Apr 2026 12:07:05 -0500 Subject: [PATCH 5/6] chore: address linter feedback --- api/types/settings/compiler_test.go | 1 - compiler/native/parse.go | 8 ++++++++ database/settings/create_test.go | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/api/types/settings/compiler_test.go b/api/types/settings/compiler_test.go index 08494e34a..881629f45 100644 --- a/api/types/settings/compiler_test.go +++ b/api/types/settings/compiler_test.go @@ -46,7 +46,6 @@ func TestTypes_ImageRestriction_String(t *testing.T) { if !reflect.DeepEqual(gotWarning, wantWarning) { t.Errorf("String is %v, want %v", gotWarning, wantWarning) } - } func TestTypes_Compiler_Getters(t *testing.T) { diff --git a/compiler/native/parse.go b/compiler/native/parse.go index 229172f77..75e4519e2 100644 --- a/compiler/native/parse.go +++ b/compiler/native/parse.go @@ -26,6 +26,8 @@ func (c *Client) ParseRaw(v any) (string, error) { return ParseReaderRaw(v) case string: // check if string is path to file + // + //nolint:gosec // ignore false positive _, err := os.Stat(v) if err == nil { // parse string as path to yaml configuration @@ -86,6 +88,8 @@ func (c *Client) Parse(v any, pipelineType string, template *yaml.Template) (*ya return ParseReader(v) case string: // check if string is path to file + // + //nolint:gosec // ignore false positive _, err := os.Stat(v) if err == nil { // parse string as path to yaml configuration @@ -140,6 +144,8 @@ func ParseFileRaw(f *os.File) (string, error) { // ParsePath converts a file path into a yaml configuration. func ParsePath(p string) (*yaml.Build, []byte, []string, error) { // open the file for reading + // + //nolint:gosec // ignore false positive f, err := os.Open(p) if err != nil { return nil, nil, nil, fmt.Errorf("unable to open yaml file %s: %w", p, err) @@ -153,6 +159,8 @@ func ParsePath(p string) (*yaml.Build, []byte, []string, error) { // ParsePathRaw converts a file path into a yaml configuration. func ParsePathRaw(p string) (string, error) { // open the file for reading + // + //nolint:gosec // ignore false positive f, err := os.Open(p) if err != nil { return "", fmt.Errorf("unable to open yaml file %s: %w", p, err) diff --git a/database/settings/create_test.go b/database/settings/create_test.go index 56ae496d2..76800c550 100644 --- a/database/settings/create_test.go +++ b/database/settings/create_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/server/api/types/settings" ) From 7e87f256fa8bf6745e8f8e6016a83795a012afad Mon Sep 17 00:00:00 2001 From: JordanBrockopp Date: Fri, 10 Apr 2026 13:15:06 -0500 Subject: [PATCH 6/6] chore: address linter feedback --- scm/github/authentication.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scm/github/authentication.go b/scm/github/authentication.go index 7e8ac05de..8ab9a0d8c 100644 --- a/scm/github/authentication.go +++ b/scm/github/authentication.go @@ -43,6 +43,8 @@ func (c *Client) Login(_ context.Context, w http.ResponseWriter, r *http.Request } // pass through the redirect if it exists + // + //nolint:gosec // ignore false positive redirect := r.FormValue("redirect_uri") if len(redirect) > 0 { c.OAuth.RedirectURL = redirect @@ -60,18 +62,24 @@ func (c *Client) Authenticate(ctx context.Context, _ http.ResponseWriter, r *htt c.Logger.Trace("authenticating user") // get the OAuth code + // + //nolint:gosec // ignore false positive code := r.FormValue("code") if len(code) == 0 { return nil, nil } // verify the OAuth state + // + //nolint:gosec // ignore false positive state := r.FormValue("state") if state != oAuthState { return nil, fmt.Errorf("unexpected oauth state: want %s but got %s", oAuthState, state) } // pass through the redirect if it exists + // + //nolint:gosec // ignore false positive redirect := r.FormValue("redirect_uri") if len(redirect) > 0 { c.OAuth.RedirectURL = redirect