builder/build/cnb/cnb_test.go
Rama main · réplica de lectura del repositorio original.
package cnb
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
"github.com/eapache/channels"
"github.com/goodrain/rainbond/builder"
"github.com/goodrain/rainbond/builder/build"
"github.com/goodrain/rainbond/builder/parser/code"
"github.com/goodrain/rainbond/event"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
)
// --- helpers ---
func newNodeDir(t *testing.T) string {
t.Helper()
dir := t.TempDir()
os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"name":"test"}`), 0644)
return dir
}
// --- config.go ---
// capability_id: rainbond.cnb.builder-image
func TestGetCNBBuilderImage(t *testing.T) {
t.Run("default", func(t *testing.T) {
os.Unsetenv("CNB_BUILDER_IMAGE")
if got := GetCNBBuilderImage(); got != DefaultCNBBuilder {
t.Errorf("got %q; want %q", got, DefaultCNBBuilder)
}
})
t.Run("custom", func(t *testing.T) {
os.Setenv("CNB_BUILDER_IMAGE", "custom:latest")
defer os.Unsetenv("CNB_BUILDER_IMAGE")
if got := GetCNBBuilderImage(); got != "custom:latest" {
t.Errorf("got %q; want %q", got, "custom:latest")
}
})
}
// capability_id: rainbond.cnb.run-image
func TestGetCNBRunImage(t *testing.T) {
os.Unsetenv("CNB_RUN_IMAGE")
if got := GetCNBRunImage(); got != DefaultCNBRunImage {
t.Errorf("got %q; want %q", got, DefaultCNBRunImage)
}
}
// --- order.go ---
// capability_id: rainbond.cnb.order-toml
func TestWriteCustomOrder(t *testing.T) {
b := &Builder{}
t.Run("writes order.toml with version", func(t *testing.T) {
dir := t.TempDir()
re := &build.Request{SourceDir: dir}
bps := []orderBuildpack{{ID: "paketo-buildpacks/nginx", Version: "1.0.12"}}
flag := b.writeCustomOrder(re, bps, "test")
if flag != "-order=/workspace/.cnb-order.toml" {
t.Errorf("got flag %q", flag)
}
content, _ := os.ReadFile(filepath.Join(dir, ".cnb-order.toml"))
if !strings.Contains(string(content), `id = "paketo-buildpacks/nginx"`) {
t.Error("expected nginx buildpack ID")
}
if !strings.Contains(string(content), `version = "1.0.12"`) {
t.Error("expected version")
}
})
t.Run("optional buildpack", func(t *testing.T) {
dir := t.TempDir()
re := &build.Request{SourceDir: dir}
bps := []orderBuildpack{{ID: "bp/test", Optional: true}}
b.writeCustomOrder(re, bps, "test")
content, _ := os.ReadFile(filepath.Join(dir, ".cnb-order.toml"))
if !strings.Contains(string(content), "optional = true") {
t.Error("expected optional = true")
}
})
t.Run("no version omits version line", func(t *testing.T) {
dir := t.TempDir()
re := &build.Request{SourceDir: dir}
bps := []orderBuildpack{{ID: "bp/test"}}
b.writeCustomOrder(re, bps, "test")
content, _ := os.ReadFile(filepath.Join(dir, ".cnb-order.toml"))
if strings.Contains(string(content), "version") {
t.Error("expected no version line")
}
})
}
// capability_id: rainbond.cnb.static-buildpacks
func TestStaticBuildpacks(t *testing.T) {
s := &staticConfig{}
dir := t.TempDir() // no package.json = pure static
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{}}
bps := s.CustomOrder(re)
if len(bps) != 1 || bps[0].ID != "paketo-buildpacks/nginx" {
t.Errorf("expected single nginx buildpack, got %+v", bps)
}
}
// --- mirror.go ---
// capability_id: rainbond.cnb.mirror-config
func TestInjectMirrorConfig(t *testing.T) {
n := &nodejsConfig{}
t.Run("skip for pure static", func(t *testing.T) {
dir := t.TempDir()
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{}}
n.InjectMirrorConfig(re)
if _, err := os.Stat(filepath.Join(dir, ".npmrc")); !os.IsNotExist(err) {
t.Error("expected no .npmrc for pure static")
}
})
t.Run("project source with existing config", func(t *testing.T) {
dir := newNodeDir(t)
os.WriteFile(filepath.Join(dir, ".npmrc"), []byte("custom"), 0644)
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{"CNB_MIRROR_SOURCE": "project"}}
n.InjectMirrorConfig(re)
content, _ := os.ReadFile(filepath.Join(dir, ".npmrc"))
if string(content) != "custom" {
t.Error("project .npmrc should be preserved")
}
})
t.Run("project source without config falls through", func(t *testing.T) {
dir := newNodeDir(t)
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{"CNB_MIRROR_SOURCE": "project"}}
n.InjectMirrorConfig(re)
if _, err := os.Stat(filepath.Join(dir, ".npmrc")); os.IsNotExist(err) {
t.Error("expected .npmrc fallback")
}
})
t.Run("creates both npmrc and yarnrc", func(t *testing.T) {
dir := newNodeDir(t)
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{}}
n.InjectMirrorConfig(re)
for _, f := range []string{".npmrc", ".yarnrc"} {
if _, err := os.Stat(filepath.Join(dir, f)); os.IsNotExist(err) {
t.Errorf("expected %s to be created", f)
}
}
})
}
// capability_id: rainbond.cnb.config-file
func TestInjectConfigFile(t *testing.T) {
t.Run("existing file not overwritten", func(t *testing.T) {
dir := t.TempDir()
os.WriteFile(filepath.Join(dir, ".npmrc"), []byte("existing"), 0644)
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{}}
injectConfigFile(re, ".npmrc", "CNB_MIRROR_NPMRC")
content, _ := os.ReadFile(filepath.Join(dir, ".npmrc"))
if string(content) != "existing" {
t.Error("should not overwrite")
}
})
t.Run("user provided content", func(t *testing.T) {
dir := t.TempDir()
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{"CNB_MIRROR_NPMRC": "custom"}}
injectConfigFile(re, ".npmrc", "CNB_MIRROR_NPMRC")
content, _ := os.ReadFile(filepath.Join(dir, ".npmrc"))
if string(content) != "custom" {
t.Errorf("got %q; want custom", string(content))
}
})
t.Run("china mirror disabled", func(t *testing.T) {
dir := t.TempDir()
os.Setenv("ENABLE_CHINA_MIRROR", "false")
defer os.Unsetenv("ENABLE_CHINA_MIRROR")
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{}}
injectConfigFile(re, ".npmrc", "CNB_MIRROR_NPMRC")
if _, err := os.Stat(filepath.Join(dir, ".npmrc")); !os.IsNotExist(err) {
t.Error("expected no .npmrc when mirror disabled")
}
})
t.Run("default npmrc", func(t *testing.T) {
dir := t.TempDir()
os.Setenv("ENABLE_CHINA_MIRROR", "true")
defer os.Unsetenv("ENABLE_CHINA_MIRROR")
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{}}
injectConfigFile(re, ".npmrc", "CNB_MIRROR_NPMRC")
content, _ := os.ReadFile(filepath.Join(dir, ".npmrc"))
if string(content) != DefaultNpmrcContent {
t.Errorf("got %q; want default", string(content))
}
})
t.Run("default yarnrc", func(t *testing.T) {
dir := t.TempDir()
os.Setenv("ENABLE_CHINA_MIRROR", "true")
defer os.Unsetenv("ENABLE_CHINA_MIRROR")
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{}}
injectConfigFile(re, ".yarnrc", "CNB_MIRROR_YARNRC")
content, _ := os.ReadFile(filepath.Join(dir, ".yarnrc"))
if string(content) != DefaultYarnrcContent {
t.Errorf("got %q; want default", string(content))
}
})
t.Run("unknown file type", func(t *testing.T) {
dir := t.TempDir()
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{}}
if err := injectConfigFile(re, ".unknown", "K"); err != nil {
t.Errorf("unexpected error: %v", err)
}
})
}
// capability_id: rainbond.cnb.npmrc-registry-parse
func TestNpmRegistryFromNpmrc(t *testing.T) {
tests := []struct{ name, content, want string }{
{"simple", "registry=https://registry.npmmirror.com\n", "https://registry.npmmirror.com"},
{"no trailing newline", "registry=https://registry.npmmirror.com", "https://registry.npmmirror.com"},
{"surrounding space", " registry = https://registry.npmmirror.com \n", "https://registry.npmmirror.com"},
{"quoted", `registry="https://registry.npmmirror.com"`, "https://registry.npmmirror.com"},
{"among other keys", "shamefully-hoist=true\nregistry=https://nexus.internal/repo\nstrict-peer-dependencies=false\n", "https://nexus.internal/repo"},
{"comments skipped", "# registry=https://wrong.example\n; registry=https://wrong.example\nregistry=https://right.example\n", "https://right.example"},
{"scoped only", "@myorg:registry=https://nexus.internal/repo\n", ""},
{"no registry key", "shamefully-hoist=true\n", ""},
{"empty", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := npmRegistryFromNpmrc(tt.content); got != tt.want {
t.Errorf("npmRegistryFromNpmrc(%q) = %q; want %q", tt.content, got, tt.want)
}
})
}
}
// capability_id: rainbond.cnb.npm-global-registry
func TestResolveNpmRegistry(t *testing.T) {
t.Setenv("ENABLE_CHINA_MIRROR", "true")
t.Run("project npmrc wins over platform default", func(t *testing.T) {
dir := newNodeDir(t)
os.WriteFile(filepath.Join(dir, ".npmrc"), []byte("registry=https://nexus.internal/repo\n"), 0644)
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{"CNB_MIRROR_SOURCE": "project"}}
if got := resolveNpmRegistry(re); got != "https://nexus.internal/repo" {
t.Errorf("got %q; want the project registry", got)
}
})
t.Run("explicit env wins over project npmrc", func(t *testing.T) {
dir := newNodeDir(t)
os.WriteFile(filepath.Join(dir, ".npmrc"), []byte("registry=https://nexus.internal/repo\n"), 0644)
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{"CNB_NPM_REGISTRY": "https://explicit.example"}}
if got := resolveNpmRegistry(re); got != "https://explicit.example" {
t.Errorf("got %q; want the explicit override", got)
}
})
t.Run("BP_NODE_PROJECT_PATH npmrc preferred over root", func(t *testing.T) {
dir := newNodeDir(t)
os.WriteFile(filepath.Join(dir, ".npmrc"), []byte("registry=https://root.example\n"), 0644)
sub := filepath.Join(dir, "frontend")
os.MkdirAll(sub, 0755)
os.WriteFile(filepath.Join(sub, ".npmrc"), []byte("registry=https://sub.example\n"), 0644)
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{"BP_NODE_PROJECT_PATH": "frontend"}}
if got := resolveNpmRegistry(re); got != "https://sub.example" {
t.Errorf("got %q; want the subdirectory registry", got)
}
})
t.Run("falls back to CNB_MIRROR_NPMRC", func(t *testing.T) {
dir := newNodeDir(t)
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{"CNB_MIRROR_NPMRC": "registry=https://custom.example\n"}}
if got := resolveNpmRegistry(re); got != "https://custom.example" {
t.Errorf("got %q; want the CNB_MIRROR_NPMRC registry", got)
}
})
t.Run("falls back to china mirror default", func(t *testing.T) {
dir := newNodeDir(t)
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{}}
if got := resolveNpmRegistry(re); got != "https://registry.npmmirror.com" {
t.Errorf("got %q; want the default mirror", got)
}
})
t.Run("project npmrc without registry key falls through", func(t *testing.T) {
dir := newNodeDir(t)
os.WriteFile(filepath.Join(dir, ".npmrc"), []byte("shamefully-hoist=true\n"), 0644)
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{}}
if got := resolveNpmRegistry(re); got != "https://registry.npmmirror.com" {
t.Errorf("got %q; want the default mirror", got)
}
})
t.Run("china mirror disabled yields nothing", func(t *testing.T) {
t.Setenv("ENABLE_CHINA_MIRROR", "false")
dir := newNodeDir(t)
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{}}
if got := resolveNpmRegistry(re); got != "" {
t.Errorf("got %q; want empty", got)
}
})
}
// capability_id: rainbond.cnb.npm-global-registry
// NPM_CONFIG_REGISTRY must reach the buildpack process: npm ignores the project
// .npmrc during `npm install -g pnpm@...`, so the env var is the only lever.
func TestNodejsNpmRegistryAnnotation(t *testing.T) {
t.Setenv("ENABLE_CHINA_MIRROR", "true")
dir := newNodeDir(t)
os.WriteFile(filepath.Join(dir, ".npmrc"), []byte("registry=https://registry.npmmirror.com\n"), 0644)
annotations := (&Builder{}).buildPlatformAnnotations(&build.Request{
SourceDir: dir,
BuildEnvs: map[string]string{"CNB_MIRROR_SOURCE": "project", "CNB_FRAMEWORK": "vite"},
})
if got := annotations["cnb-npm-config-registry"]; got != "https://registry.npmmirror.com" {
t.Fatalf("cnb-npm-config-registry = %q; want the project registry", got)
}
if got := annotationKeyToBPEnv("cnb-npm-config-registry"); got != "NPM_CONFIG_REGISTRY" {
t.Fatalf("annotation maps to env %q; want NPM_CONFIG_REGISTRY", got)
}
volume, _ := (&Builder{}).createPlatformVolume(annotations, nil)
if volume == nil {
t.Fatal("expected a platform volume")
}
var found bool
for _, source := range volume.VolumeSource.Projected.Sources {
if source.DownwardAPI == nil {
continue
}
for _, item := range source.DownwardAPI.Items {
if item.Path == "env/NPM_CONFIG_REGISTRY" {
found = true
}
}
}
if !found {
t.Error("platform volume missing env/NPM_CONFIG_REGISTRY")
}
}
// --- platform.go ---
// capability_id: rainbond.cnb.annotation-key-encode
func TestBpEnvToAnnotationKey(t *testing.T) {
tests := []struct{ input, want string }{
{"BP_NODE_VERSION", "cnb-bp-node-version"},
{"BP_WEB_SERVER", "cnb-bp-web-server"},
{"BP_", "cnb-bp-"},
}
for _, tt := range tests {
if got := bpEnvToAnnotationKey(tt.input); got != tt.want {
t.Errorf("bpEnvToAnnotationKey(%q) = %q; want %q", tt.input, got, tt.want)
}
}
}
// capability_id: rainbond.cnb.annotation-key-decode
func TestAnnotationKeyToBPEnv(t *testing.T) {
tests := []struct{ input, want string }{
{"cnb-bp-node-version", "BP_NODE_VERSION"},
{"cnb-bp-web-server", "BP_WEB_SERVER"},
{"cnb-node-env", "NODE_ENV"},
}
for _, tt := range tests {
if got := annotationKeyToBPEnv(tt.input); got != tt.want {
t.Errorf("annotationKeyToBPEnv(%q) = %q; want %q", tt.input, got, tt.want)
}
}
}
// capability_id: rainbond.cnb.platform-annotations
func TestBuildPlatformAnnotations(t *testing.T) {
nodeDir := newNodeDir(t)
t.Run("node.js defaults", func(t *testing.T) {
ann := (&Builder{}).buildPlatformAnnotations(&build.Request{SourceDir: nodeDir, BuildEnvs: map[string]string{}})
if ann["cnb-node-env"] != "production" {
t.Errorf("cnb-node-env = %q; want production", ann["cnb-node-env"])
}
})
t.Run("custom NODE_ENV", func(t *testing.T) {
ann := (&Builder{}).buildPlatformAnnotations(&build.Request{SourceDir: nodeDir, BuildEnvs: map[string]string{"CNB_NODE_ENV": "development"}})
if ann["cnb-node-env"] != "development" {
t.Errorf("got %q", ann["cnb-node-env"])
}
})
t.Run("node version", func(t *testing.T) {
ann := (&Builder{}).buildPlatformAnnotations(&build.Request{SourceDir: nodeDir, BuildEnvs: map[string]string{"CNB_NODE_VERSION": "20.10.0"}})
if ann["cnb-bp-node-version"] != "20.10.0" {
t.Errorf("got %q", ann["cnb-bp-node-version"])
}
})
t.Run("RUNTIMES fallback", func(t *testing.T) {
ann := (&Builder{}).buildPlatformAnnotations(&build.Request{SourceDir: nodeDir, BuildEnvs: map[string]string{"RUNTIMES": "18.17.0"}})
if ann["cnb-bp-node-version"] != "18.17.0" {
t.Errorf("got %q", ann["cnb-bp-node-version"])
}
})
t.Run("static build", func(t *testing.T) {
ann := (&Builder{}).buildPlatformAnnotations(&build.Request{SourceDir: nodeDir, BuildEnvs: map[string]string{"CNB_OUTPUT_DIR": "dist"}})
if ann["cnb-bp-web-server"] != "nginx" {
t.Error("expected nginx")
}
if ann["cnb-bp-web-server-root"] != "dist" {
t.Errorf("got root %q", ann["cnb-bp-web-server-root"])
}
})
t.Run("server framework skips nginx", func(t *testing.T) {
ann := (&Builder{}).buildPlatformAnnotations(&build.Request{SourceDir: nodeDir, BuildEnvs: map[string]string{
"CNB_FRAMEWORK": "nestjs",
"CNB_OUTPUT_DIR": "dist",
}})
if _, ok := ann["cnb-bp-web-server"]; ok {
t.Error("server framework should not have nginx")
}
})
t.Run("nextjs SSR skips nginx", func(t *testing.T) {
ann := (&Builder{}).buildPlatformAnnotations(&build.Request{SourceDir: nodeDir, BuildEnvs: map[string]string{
"CNB_FRAMEWORK": "nextjs",
}})
if _, ok := ann["cnb-bp-web-server"]; ok {
t.Error("nextjs SSR without CNB_OUTPUT_DIR should not have nginx")
}
})
t.Run("nextjs static export gets nginx", func(t *testing.T) {
ann := (&Builder{}).buildPlatformAnnotations(&build.Request{SourceDir: nodeDir, BuildEnvs: map[string]string{
"CNB_FRAMEWORK": "nextjs-static",
"CNB_OUTPUT_DIR": "out",
}})
if ann["cnb-bp-web-server"] != "nginx" {
t.Error("nextjs-static should have nginx")
}
if ann["cnb-bp-web-server-root"] != "out" {
t.Errorf("expected output dir 'out', got %q", ann["cnb-bp-web-server-root"])
}
})
t.Run("nuxt static export gets nginx", func(t *testing.T) {
ann := (&Builder{}).buildPlatformAnnotations(&build.Request{SourceDir: nodeDir, BuildEnvs: map[string]string{
"CNB_FRAMEWORK": "nuxt-static",
"CNB_OUTPUT_DIR": "dist",
}})
if ann["cnb-bp-web-server"] != "nginx" {
t.Error("nuxt-static should have nginx")
}
})
t.Run("static framework gets nginx", func(t *testing.T) {
ann := (&Builder{}).buildPlatformAnnotations(&build.Request{SourceDir: nodeDir, BuildEnvs: map[string]string{
"CNB_FRAMEWORK": "react",
}})
if ann["cnb-bp-web-server"] != "nginx" {
t.Error("static framework should have nginx")
}
if ann["cnb-bp-web-server-root"] != "dist" {
t.Errorf("default output dir should be dist, got %q", ann["cnb-bp-web-server-root"])
}
})
t.Run("static framework with custom output dir", func(t *testing.T) {
ann := (&Builder{}).buildPlatformAnnotations(&build.Request{SourceDir: nodeDir, BuildEnvs: map[string]string{
"CNB_FRAMEWORK": "vue",
"CNB_OUTPUT_DIR": "build",
}})
if ann["cnb-bp-web-server-root"] != "build" {
t.Errorf("got %q; want build", ann["cnb-bp-web-server-root"])
}
})
t.Run("build script", func(t *testing.T) {
ann := (&Builder{}).buildPlatformAnnotations(&build.Request{SourceDir: nodeDir, BuildEnvs: map[string]string{"CNB_BUILD_SCRIPT": "build:prod"}})
if ann["cnb-bp-node-run-scripts"] != "build:prod" {
t.Errorf("got %q", ann["cnb-bp-node-run-scripts"])
}
})
t.Run("BP_ passthrough", func(t *testing.T) {
ann := (&Builder{}).buildPlatformAnnotations(&build.Request{SourceDir: nodeDir, BuildEnvs: map[string]string{"BP_CUSTOM_VAR": "val"}})
if ann["cnb-bp-custom-var"] != "val" {
t.Error("BP_ passthrough failed")
}
})
t.Run("CNB_START_SCRIPT to BP_NPM_START_SCRIPT", func(t *testing.T) {
ann := (&Builder{}).buildPlatformAnnotations(&build.Request{SourceDir: nodeDir, BuildEnvs: map[string]string{"CNB_START_SCRIPT": "start:prod"}})
if ann["cnb-bp-npm-start-script"] != "start:prod" {
t.Errorf("expected start:prod, got %q", ann["cnb-bp-npm-start-script"])
}
})
t.Run("CNB_START_SCRIPT with pnpm", func(t *testing.T) {
ann := (&Builder{}).buildPlatformAnnotations(&build.Request{SourceDir: nodeDir, BuildEnvs: map[string]string{
"CNB_START_SCRIPT": "serve",
"CNB_PACKAGE_TOOL": "pnpm",
}})
if ann["cnb-bp-npm-start-script"] != "serve" {
t.Errorf("expected serve, got %q", ann["cnb-bp-npm-start-script"])
}
})
t.Run("BP_NPM_START_SCRIPT passthrough", func(t *testing.T) {
ann := (&Builder{}).buildPlatformAnnotations(&build.Request{SourceDir: nodeDir, BuildEnvs: map[string]string{"BP_NPM_START_SCRIPT": "start:prod"}})
if ann["cnb-bp-npm-start-script"] != "start:prod" {
t.Errorf("expected start:prod, got %q", ann["cnb-bp-npm-start-script"])
}
})
t.Run("pure static project", func(t *testing.T) {
dir := t.TempDir()
ann := (&Builder{}).buildPlatformAnnotations(&build.Request{SourceDir: dir, BuildEnvs: map[string]string{}})
if ann["cnb-bp-web-server"] != "nginx" {
t.Error("pure static should have nginx")
}
if ann["cnb-bp-web-server-root"] != "." {
t.Errorf("got %q; want '.'", ann["cnb-bp-web-server-root"])
}
})
t.Run("pure static with custom output dir", func(t *testing.T) {
dir := t.TempDir()
ann := (&Builder{}).buildPlatformAnnotations(&build.Request{SourceDir: dir, BuildEnvs: map[string]string{"CNB_OUTPUT_DIR": "public"}})
if ann["cnb-bp-web-server-root"] != "public" {
t.Errorf("got %q; want public", ann["cnb-bp-web-server-root"])
}
})
t.Run("static project nginx config takes precedence over generated defaults", func(t *testing.T) {
for _, lang := range []code.Lang{code.Static, code.Nodejs} {
t.Run(string(lang), func(t *testing.T) {
dir := t.TempDir()
configPath := filepath.Join(dir, "nginx.conf")
config := []byte("daemon off;\nevents {}\nhttp { server { listen 8080; root /workspace; } }\n")
if err := os.WriteFile(configPath, config, 0644); err != nil {
t.Fatal(err)
}
b := &Builder{}
re := &build.Request{Lang: lang, SourceDir: dir, BuildEnvs: map[string]string{"CNB_OUTPUT_DIR": "public"}}
ann := b.buildPlatformAnnotations(re)
for _, key := range []string{"cnb-bp-web-server", "cnb-bp-web-server-root", "cnb-bp-web-server-enable-push-state"} {
if value, exists := ann[key]; exists {
t.Errorf("project nginx.conf must suppress generated default %s=%q", key, value)
}
}
vol, _ := b.createPlatformVolume(ann, nil)
for _, source := range vol.Projected.Sources {
if source.DownwardAPI == nil {
continue
}
for _, item := range source.DownwardAPI.Items {
if item.Path == "env/BP_WEB_SERVER" {
t.Error("buildpack must not receive automatic config generation flag")
}
}
}
bps := getLanguageConfig(re).CustomOrder(re)
if len(bps) != 1 || bps[0].ID != "paketo-buildpacks/nginx" {
t.Fatalf("custom config still requires nginx buildpack, got %+v", bps)
}
got, err := os.ReadFile(configPath)
if err != nil || string(got) != string(config) {
t.Fatalf("project config changed: content=%q, err=%v", got, err)
}
})
}
})
t.Run("nginx config directory does not suppress generated defaults", func(t *testing.T) {
dir := t.TempDir()
if err := os.Mkdir(filepath.Join(dir, "nginx.conf"), 0755); err != nil {
t.Fatal(err)
}
ann := (&Builder{}).buildPlatformAnnotations(&build.Request{Lang: code.Static, SourceDir: dir})
if ann["cnb-bp-web-server"] != "nginx" || ann["cnb-bp-web-server-root"] != "." || ann["cnb-bp-web-server-enable-push-state"] != "true" {
t.Errorf("expected generated static defaults, got %v", ann)
}
})
}
// capability_id: rainbond.cnb.platform-volume
func TestCreatePlatformVolume(t *testing.T) {
b := &Builder{}
dir := newNodeDir(t)
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{"CNB_NODE_VERSION": "20"}}
annotations := b.buildPlatformAnnotations(re)
vol, mount := b.createPlatformVolume(annotations, nil)
if vol == nil || mount == nil {
t.Fatal("expected volume and mount")
}
if vol.Name != "platform" || mount.MountPath != "/platform" {
t.Errorf("vol=%q mount=%q", vol.Name, mount.MountPath)
}
if vol.Projected == nil || len(vol.Projected.Sources) == 0 {
t.Fatal("expected projected volume sources")
}
if vol.Projected.Sources[0].DownwardAPI == nil || len(vol.Projected.Sources[0].DownwardAPI.Items) == 0 {
t.Error("expected DownwardAPI items")
}
}
func TestCreatePlatformVolumeWithBinding(t *testing.T) {
b := &Builder{}
annotations := map[string]string{
"cnb-bp-jvm-version": "17",
"cnb-binding-java-maven-type": "maven",
}
bindings := []platformBinding{{
Name: "java-maven",
Type: "maven",
ConfigMapName: "java-maven",
ConfigMapKey: "mavensetting",
TargetFile: "settings.xml",
}}
vol, _ := b.createPlatformVolume(annotations, bindings)
if vol == nil || vol.Projected == nil {
t.Fatal("expected projected platform volume")
}
if len(vol.Projected.Sources) != 2 {
t.Fatalf("expected 2 projected sources, got %d", len(vol.Projected.Sources))
}
configSource := vol.Projected.Sources[1].ConfigMap
if configSource == nil {
t.Fatal("expected configmap projection for binding")
}
if configSource.Name != "java-maven" {
t.Fatalf("expected java-maven configmap, got %q", configSource.Name)
}
if len(configSource.Items) != 1 || configSource.Items[0].Path != "bindings/java-maven/settings.xml" {
t.Fatalf("unexpected binding projection items: %+v", configSource.Items)
}
}
func TestBindingTypeAnnotationKeyShortensLongBindingNames(t *testing.T) {
bindingName := "06c421b8d22d718065929cfdea86b232-20260401205923-procfile-9mcpf"
got := bindingTypeAnnotationKey(bindingName)
if len(got) > 63 {
t.Fatalf("annotation key length = %d, want <= 63: %q", len(got), got)
}
if !strings.HasPrefix(got, "cnb-binding-") {
t.Fatalf("annotation key %q missing prefix", got)
}
if !strings.HasSuffix(got, "-type") {
t.Fatalf("annotation key %q missing suffix", got)
}
if got == "cnb-binding-"+bindingName+"-type" {
t.Fatalf("expected long binding name to be shortened, got %q", got)
}
if again := bindingTypeAnnotationKey(bindingName); again != got {
t.Fatalf("annotation key should be deterministic: got %q, again %q", got, again)
}
if other := bindingTypeAnnotationKey(bindingName + "-other"); other == got {
t.Fatalf("different binding names should not share annotation key: %q", other)
}
}
// --- job.go ---
// capability_id: rainbond.cnb.env-vars
func TestBuildEnvVars(t *testing.T) {
b := &Builder{}
t.Run("basic envs", func(t *testing.T) {
envs := b.buildEnvVars(&build.Request{BuildEnvs: map[string]string{}})
m := make(map[string]string)
for _, e := range envs {
m[e.Name] = e.Value
}
if m["CNB_PLATFORM_API"] != "0.13" {
t.Error("expected CNB_PLATFORM_API=0.13")
}
if m["DOCKER_CONFIG"] != "/home/cnb/.docker" {
t.Error("expected DOCKER_CONFIG=/home/cnb/.docker")
}
})
t.Run("proxy settings", func(t *testing.T) {
os.Setenv("HTTP_PROXY", "http://p:8080")
os.Setenv("HTTPS_PROXY", "https://p:8443")
os.Setenv("NO_PROXY", "localhost")
defer func() {
os.Unsetenv("HTTP_PROXY")
os.Unsetenv("HTTPS_PROXY")
os.Unsetenv("NO_PROXY")
}()
envs := b.buildEnvVars(&build.Request{BuildEnvs: map[string]string{}})
m := make(map[string]string)
for _, e := range envs {
m[e.Name] = e.Value
}
if m["HTTP_PROXY"] != "http://p:8080" {
t.Error("HTTP_PROXY not set")
}
})
}
// capability_id: rainbond.cnb.creator-args
func TestBuildCreatorArgs(t *testing.T) {
b := &Builder{}
t.Run("basic args", func(t *testing.T) {
dir := newNodeDir(t)
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{}}
args := b.buildCreatorArgs(re, "img:v1", "run:v1")
for _, r := range []string{"-app=/workspace", "-layers=/layers", "-platform=/platform"} {
found := false
for _, a := range args {
if a == r {
found = true
}
}
if !found {
t.Errorf("missing arg %s", r)
}
}
if args[len(args)-1] != "img:v1" {
t.Errorf("last arg = %q", args[len(args)-1])
}
})
t.Run("pure static gets custom order", func(t *testing.T) {
dir := t.TempDir()
args := b.buildCreatorArgs(&build.Request{SourceDir: dir, BuildEnvs: map[string]string{}}, "img:v1", "run:v1")
hasOrder := false
for _, a := range args {
if strings.HasPrefix(a, "-order=") {
hasOrder = true
}
}
if !hasOrder {
t.Error("expected -order flag for pure static")
}
})
t.Run("node project uses default order", func(t *testing.T) {
dir := newNodeDir(t)
args := b.buildCreatorArgs(&build.Request{SourceDir: dir, BuildEnvs: map[string]string{}}, "img:v1", "run:v1")
for _, a := range args {
if strings.HasPrefix(a, "-order=") {
t.Error("node project should use default builder order, not custom order")
}
}
})
}
// capability_id: rainbond.cnb.volume-mounts
func TestCreateVolumeAndMount(t *testing.T) {
b := &Builder{}
re := &build.Request{SourceDir: "/tmp/src", CacheDir: "/tmp/cache"}
vols, mounts := b.createVolumeAndMount(re, "secret-name")
if len(vols) != 4 {
t.Errorf("expected 4 volumes, got %d", len(vols))
}
if len(mounts) != 4 {
t.Errorf("expected 4 mounts, got %d", len(mounts))
}
// Check docker-config uses secret
found := false
for _, v := range vols {
if v.Name == "docker-config" && v.Secret != nil && v.Secret.SecretName == "secret-name" {
found = true
}
}
if !found {
t.Error("expected docker-config volume with secret")
}
}
// --- build.go ---
// capability_id: rainbond.cnb.source-dir-permissions
func TestSetSourceDirPermissions(t *testing.T) {
b := &Builder{}
t.Run("removes .git and sets permissions recursively", func(t *testing.T) {
dir := t.TempDir()
gitDir := filepath.Join(dir, ".git")
os.Mkdir(gitDir, 0755)
os.WriteFile(filepath.Join(gitDir, "config"), []byte("test"), 0644)
subDir := filepath.Join(dir, "src")
os.Mkdir(subDir, 0755)
os.WriteFile(filepath.Join(subDir, "main.go"), []byte("package main"), 0644)
b.setSourceDirPermissions(&build.Request{SourceDir: dir})
if _, err := os.Stat(gitDir); !os.IsNotExist(err) {
t.Error(".git should be removed")
}
info, _ := os.Stat(dir)
if info.Mode().Perm() != 0777 {
t.Errorf("dir permissions = %o; want 0777", info.Mode().Perm())
}
subInfo, _ := os.Stat(subDir)
if subInfo.Mode().Perm() != 0777 {
t.Errorf("subdir permissions = %o; want 0777", subInfo.Mode().Perm())
}
fileInfo, _ := os.Stat(filepath.Join(subDir, "main.go"))
if fileInfo.Mode().Perm() != 0777 {
t.Errorf("file permissions = %o; want 0777", fileInfo.Mode().Perm())
}
})
t.Run("no .git directory", func(t *testing.T) {
dir := t.TempDir()
if err := b.setSourceDirPermissions(&build.Request{SourceDir: dir}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("keeps .git when BUILD_KEEP_GIT is true", func(t *testing.T) {
dir := t.TempDir()
gitDir := filepath.Join(dir, ".git")
if err := os.Mkdir(gitDir, 0755); err != nil {
t.Fatalf("create .git dir: %v", err)
}
if err := os.WriteFile(filepath.Join(gitDir, "config"), []byte("test"), 0644); err != nil {
t.Fatalf("write .git config: %v", err)
}
err := b.setSourceDirPermissions(&build.Request{
SourceDir: dir,
BuildEnvs: map[string]string{"BUILD_KEEP_GIT": "true"},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if _, err := os.Stat(gitDir); err != nil {
t.Fatalf(".git should be kept when BUILD_KEEP_GIT=true, got %v", err)
}
})
}
// capability_id: rainbond.cnb.new-builder
func TestNewBuilder(t *testing.T) {
b, err := NewBuilder()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if b == nil {
t.Error("expected non-nil builder")
}
}
// capability_id: rainbond.cnb.project-file-validation
func TestValidateProjectFiles(t *testing.T) {
b := &Builder{}
logger := event.NewLogger("test", nil)
t.Run("empty lock file returns error", func(t *testing.T) {
dir := t.TempDir()
os.WriteFile(filepath.Join(dir, "package-lock.json"), []byte{}, 0644)
re := &build.Request{SourceDir: dir, Logger: logger}
if err := b.validateProjectFiles(re); err == nil {
t.Error("expected error for empty lock file")
}
})
t.Run("valid lock file passes", func(t *testing.T) {
dir := t.TempDir()
os.WriteFile(filepath.Join(dir, "package-lock.json"), []byte(`{"lockfileVersion":3}`), 0644)
re := &build.Request{SourceDir: dir, Logger: logger}
if err := b.validateProjectFiles(re); err != nil {
t.Errorf("unexpected error: %v", err)
}
})
t.Run("no lock file passes", func(t *testing.T) {
dir := t.TempDir()
re := &build.Request{SourceDir: dir, Logger: logger}
if err := b.validateProjectFiles(re); err != nil {
t.Errorf("unexpected error: %v", err)
}
})
}
// noopLogger and related types removed - using event.NewLogger instead
// --- waitingComplete (job.go) ---
// capability_id: rainbond.cnb.waiting-complete
func TestWaitingComplete(t *testing.T) {
b := &Builder{}
logger := event.GetTestLogger()
t.Run("complete then logcomplete", func(t *testing.T) {
reChan := channels.NewRingChannel(10)
re := &build.Request{Logger: logger}
go func() {
reChan.In() <- "complete"
reChan.In() <- "logcomplete"
}()
if err := b.waitingComplete(re, reChan); err != nil {
t.Errorf("unexpected error: %v", err)
}
})
t.Run("logcomplete then complete", func(t *testing.T) {
reChan := channels.NewRingChannel(10)
re := &build.Request{Logger: logger}
go func() {
reChan.In() <- "logcomplete"
reChan.In() <- "complete"
}()
if err := b.waitingComplete(re, reChan); err != nil {
t.Errorf("unexpected error: %v", err)
}
})
t.Run("failed then logcomplete", func(t *testing.T) {
reChan := channels.NewRingChannel(10)
re := &build.Request{Logger: logger}
go func() {
reChan.In() <- "failed"
reChan.In() <- "logcomplete"
}()
err := b.waitingComplete(re, reChan)
if err == nil {
t.Error("expected error for failed job")
}
})
t.Run("logcomplete then failed", func(t *testing.T) {
reChan := channels.NewRingChannel(10)
re := &build.Request{Logger: logger}
go func() {
reChan.In() <- "logcomplete"
reChan.In() <- "failed"
}()
err := b.waitingComplete(re, reChan)
if err == nil {
t.Error("expected error for failed job")
}
})
t.Run("cancel then logcomplete", func(t *testing.T) {
reChan := channels.NewRingChannel(10)
re := &build.Request{Logger: logger}
go func() {
reChan.In() <- "cancel"
reChan.In() <- "logcomplete"
}()
err := b.waitingComplete(re, reChan)
if err == nil {
t.Error("expected error for canceled job")
}
if !strings.Contains(err.Error(), "canceled") {
t.Errorf("expected 'canceled' in error, got %q", err.Error())
}
})
t.Run("non-string status ignored", func(t *testing.T) {
reChan := channels.NewRingChannel(10)
re := &build.Request{Logger: logger}
go func() {
reChan.In() <- 12345
reChan.In() <- "complete"
reChan.In() <- "logcomplete"
}()
if err := b.waitingComplete(re, reChan); err != nil {
t.Errorf("unexpected error: %v", err)
}
})
}
// --- createPlatformVolume edge cases ---
// capability_id: rainbond.cnb.platform-volume
func TestCreatePlatformVolumeEmpty(t *testing.T) {
b := &Builder{}
vol, mount := b.createPlatformVolume(map[string]string{}, nil)
if vol != nil || mount != nil {
t.Error("expected nil for empty annotations")
}
}
// capability_id: rainbond.cnb.platform-volume
func TestCreatePlatformVolumeNonCNBKeys(t *testing.T) {
b := &Builder{}
vol, mount := b.createPlatformVolume(map[string]string{"other-key": "val"}, nil)
if vol != nil || mount != nil {
t.Error("expected nil when no cnb- prefixed keys")
}
}
// --- writeCustomOrder error path ---
// capability_id: rainbond.cnb.order-write-failure
func TestWriteCustomOrderFailure(t *testing.T) {
b := &Builder{}
re := &build.Request{SourceDir: "/nonexistent/path/that/does/not/exist"}
bps := []orderBuildpack{{ID: "bp/test"}}
flag := b.writeCustomOrder(re, bps, "test")
if flag != "" {
t.Errorf("expected empty flag on write failure, got %q", flag)
}
}
// --- injectMirrorConfig error propagation ---
// capability_id: rainbond.cnb.mirror-config-write-error
func TestInjectMirrorConfigWriteError(t *testing.T) {
n := &nodejsConfig{}
dir := newNodeDir(t)
// Make dir read-only so WriteFile fails
os.Chmod(dir, 0555)
defer os.Chmod(dir, 0755)
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{
"CNB_MIRROR_NPMRC": "custom-content",
}}
err := n.InjectMirrorConfig(re)
if err == nil {
t.Error("expected error when directory is read-only")
}
}
// --- setSourceDirPermissions with nonexistent dir ---
// capability_id: rainbond.cnb.source-dir-permissions
func TestSetSourceDirPermissionsNonexistent(t *testing.T) {
b := &Builder{}
err := b.setSourceDirPermissions(&build.Request{SourceDir: "/nonexistent/path"})
if err == nil {
t.Error("expected error for nonexistent directory")
}
}
// --- dependency mirror defaults to online URL ---
// capability_id: rainbond.cnb.dependency-mirror
func TestBuildPlatformAnnotationsMirrorDefault(t *testing.T) {
os.Unsetenv("BP_DEPENDENCY_MIRROR")
dir := newNodeDir(t)
ann := (&Builder{}).buildPlatformAnnotations(&build.Request{SourceDir: dir, BuildEnvs: map[string]string{}})
if ann["cnb-bp-dependency-mirror"] != defaultOnlineMirror {
t.Errorf("expected default dependency mirror %s, got %q", defaultOnlineMirror, ann["cnb-bp-dependency-mirror"])
}
}
// capability_id: rainbond.cnb.dependency-mirror
func TestBuildPlatformAnnotationsMirrorExplicit(t *testing.T) {
os.Setenv("BP_DEPENDENCY_MIRROR", "https://example.com/mirror")
defer os.Unsetenv("BP_DEPENDENCY_MIRROR")
dir := newNodeDir(t)
ann := (&Builder{}).buildPlatformAnnotations(&build.Request{SourceDir: dir, BuildEnvs: map[string]string{}})
if ann["cnb-bp-dependency-mirror"] != "https://example.com/mirror" {
t.Errorf("expected dependency mirror from env, got %q", ann["cnb-bp-dependency-mirror"])
}
}
// capability_id: rainbond.cnb.dependency-mirror
func TestGetDependencyMirrorOffline(t *testing.T) {
os.Unsetenv("BP_DEPENDENCY_MIRROR")
dir := t.TempDir()
// Temporarily override the marker path for testing
origMarker := offlineMirrorMarker
marker := filepath.Join(dir, "BP_DEPENDENCY_MIRROR")
os.WriteFile(marker, []byte("file:///grdata/cnb\n"), 0644)
// We can't easily override the const, so test getDependencyMirror indirectly
// by setting the env var (highest priority)
os.Setenv("BP_DEPENDENCY_MIRROR", "file:///grdata/cnb")
defer os.Unsetenv("BP_DEPENDENCY_MIRROR")
got := getDependencyMirror()
if got != "file:///grdata/cnb" {
t.Errorf("expected file:///grdata/cnb, got %q", got)
}
_ = origMarker
}
// --- BP_ passthrough does not override explicit keys ---
// capability_id: rainbond.cnb.bp-annotation-priority
func TestBuildPlatformAnnotationsBPNoOverride(t *testing.T) {
dir := newNodeDir(t)
ann := (&Builder{}).buildPlatformAnnotations(&build.Request{
SourceDir: dir,
BuildEnvs: map[string]string{
"CNB_BUILD_SCRIPT": "build:prod",
"BP_NODE_RUN_SCRIPTS": "should-not-override",
},
})
if ann["cnb-bp-node-run-scripts"] != "build:prod" {
t.Errorf("BP_ passthrough should not override explicit key, got %q", ann["cnb-bp-node-run-scripts"])
}
}
// --- mock job controller ---
type mockJobCtrl struct {
jobs []*corev1.Pod
getJobsErr error
execJobErr error
execJobFn func(ctx context.Context, job *corev1.Pod, logger io.Writer, result *channels.RingChannel) error
deleted []string
languageBuildSettings map[string]string
defaultLanguageBuildSetting string
}
func (m *mockJobCtrl) ExecJob(ctx context.Context, job *corev1.Pod, logger io.Writer, result *channels.RingChannel) error {
if m.execJobFn != nil {
return m.execJobFn(ctx, job, logger, result)
}
return m.execJobErr
}
func (m *mockJobCtrl) GetJob(name string) (*corev1.Pod, error) { return nil, nil }
func (m *mockJobCtrl) GetServiceJobs(serviceID string) ([]*corev1.Pod, error) {
return m.jobs, m.getJobsErr
}
func (m *mockJobCtrl) DeleteJob(name string) { m.deleted = append(m.deleted, name) }
func (m *mockJobCtrl) GetLanguageBuildSetting(_ context.Context, _ code.Lang, name string) string {
if m.languageBuildSettings == nil {
return ""
}
return m.languageBuildSettings[name]
}
func (m *mockJobCtrl) GetDefaultLanguageBuildSetting(_ context.Context, _ code.Lang) string {
return m.defaultLanguageBuildSetting
}
func newTestBuilder(ctrl *mockJobCtrl) *Builder {
return &Builder{
jobCtrl: ctrl,
createAuthSecret: func(re *build.Request) (corev1.Secret, error) {
return corev1.Secret{}, nil
},
deleteAuthSecret: func(re *build.Request, name string) {},
prepareBuildKit: func(ctx context.Context, kubeClient kubernetes.Interface, namespace, cmName, imageDomain string) error {
return nil
},
}
}
// --- stopPreBuildJob ---
// capability_id: rainbond.cnb.prebuild-job-cleanup
func TestStopPreBuildJob(t *testing.T) {
t.Run("deletes existing jobs", func(t *testing.T) {
ctrl := &mockJobCtrl{
jobs: []*corev1.Pod{
{ObjectMeta: corev1.Pod{}.ObjectMeta},
},
}
ctrl.jobs[0].Name = "job-1"
b := newTestBuilder(ctrl)
b.stopPreBuildJob(&build.Request{ServiceID: "svc1"})
if len(ctrl.deleted) != 1 || ctrl.deleted[0] != "job-1" {
t.Errorf("expected job-1 deleted, got %v", ctrl.deleted)
}
})
t.Run("no jobs", func(t *testing.T) {
ctrl := &mockJobCtrl{}
b := newTestBuilder(ctrl)
b.stopPreBuildJob(&build.Request{ServiceID: "svc1"})
if len(ctrl.deleted) != 0 {
t.Error("expected no deletions")
}
})
t.Run("error getting jobs", func(t *testing.T) {
ctrl := &mockJobCtrl{getJobsErr: fmt.Errorf("api error")}
b := newTestBuilder(ctrl)
b.stopPreBuildJob(&build.Request{ServiceID: "svc1"})
if len(ctrl.deleted) != 0 {
t.Error("expected no deletions on error")
}
})
}
// --- runCNBBuildJob ---
// capability_id: rainbond.cnb.build-job-execution
func TestRunCNBBuildJob(t *testing.T) {
logger := event.GetTestLogger()
t.Run("exec job failure", func(t *testing.T) {
ctrl := &mockJobCtrl{execJobErr: fmt.Errorf("exec failed")}
b := newTestBuilder(ctrl)
dir := newNodeDir(t)
re := &build.Request{
ServiceID: "svc1",
DeployVersion: "v1",
RbdNamespace: "ns",
Arch: "amd64",
SourceDir: dir,
CacheDir: "/tmp/cache",
BuildEnvs: map[string]string{},
Logger: logger,
}
err := b.runCNBBuildJob(re, "img:v1")
if err == nil || !strings.Contains(err.Error(), "exec failed") {
t.Errorf("expected exec failed error, got %v", err)
}
})
t.Run("auth secret failure", func(t *testing.T) {
ctrl := &mockJobCtrl{}
b := &Builder{
jobCtrl: ctrl,
createAuthSecret: func(re *build.Request) (corev1.Secret, error) {
return corev1.Secret{}, fmt.Errorf("auth failed")
},
deleteAuthSecret: func(re *build.Request, name string) {},
prepareBuildKit: func(ctx context.Context, kubeClient kubernetes.Interface, namespace, cmName, imageDomain string) error {
return nil
},
}
dir := newNodeDir(t)
re := &build.Request{
ServiceID: "svc1",
DeployVersion: "v1",
RbdNamespace: "ns",
Arch: "amd64",
SourceDir: dir,
CacheDir: "/tmp/cache",
BuildEnvs: map[string]string{},
Logger: logger,
}
err := b.runCNBBuildJob(re, "img:v1")
if err == nil || !strings.Contains(err.Error(), "auth failed") {
t.Errorf("expected auth failed error, got %v", err)
}
})
t.Run("successful job with complete", func(t *testing.T) {
ctrl := &mockJobCtrl{
execJobFn: func(ctx context.Context, job *corev1.Pod, logger io.Writer, result *channels.RingChannel) error {
go func() {
result.In() <- "complete"
result.In() <- "logcomplete"
}()
return nil
},
}
b := newTestBuilder(ctrl)
dir := newNodeDir(t)
re := &build.Request{
ServiceID: "svc1",
DeployVersion: "v1",
RbdNamespace: "ns",
Arch: "amd64",
SourceDir: dir,
CacheDir: "/tmp/cache",
BuildEnvs: map[string]string{},
Logger: logger,
}
err := b.runCNBBuildJob(re, "img:v1")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if len(ctrl.deleted) != 1 {
t.Errorf("expected job cleanup, got %d deletions", len(ctrl.deleted))
}
})
t.Run("procfile build env is injected through binding and cleaned up", func(t *testing.T) {
var capturedPod *corev1.Pod
ctrl := &mockJobCtrl{
execJobFn: func(ctx context.Context, job *corev1.Pod, logger io.Writer, result *channels.RingChannel) error {
capturedPod = job
go func() {
result.In() <- "complete"
result.In() <- "logcomplete"
}()
return nil
},
}
var createdConfigMap *corev1.ConfigMap
var deletedConfigMapNames []string
b := &Builder{
jobCtrl: ctrl,
createAuthSecret: func(re *build.Request) (corev1.Secret, error) {
return corev1.Secret{}, nil
},
deleteAuthSecret: func(re *build.Request, name string) {},
prepareBuildKit: func(ctx context.Context, kubeClient kubernetes.Interface, namespace, cmName, imageDomain string) error {
return nil
},
createConfigMap: func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, cm *corev1.ConfigMap) (*corev1.ConfigMap, error) {
createdConfigMap = cm.DeepCopy()
if createdConfigMap.Name == "" {
createdConfigMap.Name = createdConfigMap.GenerateName + "abc123"
}
return createdConfigMap, nil
},
deleteConfigMap: func(ctx context.Context, kubeClient kubernetes.Interface, namespace, name string) error {
deletedConfigMapNames = append(deletedConfigMapNames, name)
return nil
},
}
re := &build.Request{
ServiceID: "svc1",
DeployVersion: "v1",
RbdNamespace: "ns",
Arch: "amd64",
SourceDir: t.TempDir(),
CacheDir: "/tmp/cache",
BuildEnvs: map[string]string{
"BUILD_AUTO_PROCFILE": "web: python manage.py runserver 0.0.0.0:$_PORT",
},
Logger: logger,
}
err := b.runCNBBuildJob(re, "img:v1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if capturedPod == nil {
t.Fatal("pod was not captured")
}
var bindingName string
foundBindingProjection := false
for _, vol := range capturedPod.Spec.Volumes {
if vol.Name != "platform" || vol.Projected == nil {
continue
}
for _, source := range vol.Projected.Sources {
if source.ConfigMap == nil || len(source.ConfigMap.Items) != 1 {
continue
}
item := source.ConfigMap.Items[0]
if !strings.HasSuffix(item.Path, "/Procfile") {
continue
}
foundBindingProjection = true
bindingName = source.ConfigMap.Name
wantPath := "bindings/" + bindingName + "/Procfile"
if item.Path != wantPath {
t.Fatalf("procfile binding path = %q, want %q", item.Path, wantPath)
}
}
}
if !foundBindingProjection {
t.Fatal("expected Procfile binding projection in CNB platform volume")
}
if got := capturedPod.Annotations[bindingTypeAnnotationKey(bindingName)]; got != "Procfile" {
t.Fatalf("binding annotation = %q, want Procfile", got)
}
if createdConfigMap == nil {
t.Fatal("expected temporary Procfile ConfigMap to be created")
}
if createdConfigMap.Data["Procfile"] != "web: python manage.py runserver 0.0.0.0:$_PORT\n" {
t.Fatalf("created Procfile content = %q, want %q", createdConfigMap.Data["Procfile"], "web: python manage.py runserver 0.0.0.0:$_PORT\n")
}
if createdConfigMap.Name != bindingName {
t.Fatalf("created ConfigMap name = %q, want %q", createdConfigMap.Name, bindingName)
}
if len(deletedConfigMapNames) != 1 || deletedConfigMapNames[0] != bindingName {
t.Fatalf("expected temporary Procfile ConfigMap cleanup for %q, got %v", bindingName, deletedConfigMapNames)
}
})
t.Run("long procfile binding names keep annotation keys within kubernetes limits", func(t *testing.T) {
var capturedPod *corev1.Pod
ctrl := &mockJobCtrl{
execJobFn: func(ctx context.Context, job *corev1.Pod, logger io.Writer, result *channels.RingChannel) error {
capturedPod = job
go func() {
result.In() <- "complete"
result.In() <- "logcomplete"
}()
return nil
},
}
var createdConfigMap *corev1.ConfigMap
b := &Builder{
jobCtrl: ctrl,
createAuthSecret: func(re *build.Request) (corev1.Secret, error) {
return corev1.Secret{}, nil
},
deleteAuthSecret: func(re *build.Request, name string) {},
prepareBuildKit: func(ctx context.Context, kubeClient kubernetes.Interface, namespace, cmName, imageDomain string) error {
return nil
},
createConfigMap: func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, cm *corev1.ConfigMap) (*corev1.ConfigMap, error) {
createdConfigMap = cm.DeepCopy()
createdConfigMap.Name = cm.GenerateName + "9mcpf"
return createdConfigMap, nil
},
deleteConfigMap: func(ctx context.Context, kubeClient kubernetes.Interface, namespace, name string) error {
return nil
},
}
re := &build.Request{
ServiceID: "06c421b8d22d718065929cfdea86b232",
DeployVersion: "20260401205923",
RbdNamespace: "ns",
Arch: "amd64",
SourceDir: t.TempDir(),
CacheDir: "/tmp/cache",
BuildEnvs: map[string]string{
"BUILD_AUTO_PROCFILE": "web: python manage.py runserver 0.0.0.0:$_PORT",
},
Logger: logger,
}
err := b.runCNBBuildJob(re, "img:v1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if capturedPod == nil {
t.Fatal("pod was not captured")
}
if createdConfigMap == nil {
t.Fatal("expected temporary Procfile ConfigMap to be created")
}
annotationKey := bindingTypeAnnotationKey(createdConfigMap.Name)
if len(annotationKey) > 63 {
t.Fatalf("annotation key length = %d, want <= 63: %q", len(annotationKey), annotationKey)
}
if got := capturedPod.Annotations[annotationKey]; got != "Procfile" {
t.Fatalf("binding annotation = %q, want Procfile", got)
}
foundPlatformMount := false
foundBindingFieldRef := false
wantFieldPath := "metadata.annotations['" + annotationKey + "']"
for _, mount := range capturedPod.Spec.Containers[0].VolumeMounts {
if mount.Name == "platform" && mount.MountPath == "/platform" {
foundPlatformMount = true
}
}
for _, vol := range capturedPod.Spec.Volumes {
if vol.Name != "platform" || vol.Projected == nil {
continue
}
for _, source := range vol.Projected.Sources {
if source.DownwardAPI == nil {
continue
}
for _, item := range source.DownwardAPI.Items {
if item.Path != "bindings/"+createdConfigMap.Name+"/type" {
continue
}
foundBindingFieldRef = true
if item.FieldRef == nil || item.FieldRef.FieldPath != wantFieldPath {
t.Fatalf("binding type fieldRef = %+v, want %q", item.FieldRef, wantFieldPath)
}
}
}
}
if !foundPlatformMount {
t.Fatal("expected platform volume mount to be present")
}
if !foundBindingFieldRef {
t.Fatal("expected binding type DownwardAPI item for long Procfile binding")
}
})
t.Run("CNB_START_SCRIPT in pod annotations and downward API", func(t *testing.T) {
var capturedPod *corev1.Pod
ctrl := &mockJobCtrl{
execJobFn: func(ctx context.Context, job *corev1.Pod, logger io.Writer, result *channels.RingChannel) error {
capturedPod = job
go func() {
result.In() <- "complete"
result.In() <- "logcomplete"
}()
return nil
},
}
b := newTestBuilder(ctrl)
dir := newNodeDir(t)
re := &build.Request{
ServiceID: "svc1",
DeployVersion: "v1",
RbdNamespace: "ns",
Arch: "amd64",
SourceDir: dir,
CacheDir: "/tmp/cache",
BuildEnvs: map[string]string{"CNB_START_SCRIPT": "start:prod"},
Logger: logger,
}
err := b.runCNBBuildJob(re, "img:v1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if capturedPod == nil {
t.Fatal("pod was not captured")
}
if len(capturedPod.Spec.Containers) != 1 {
t.Fatalf("expected 1 container, got %d", len(capturedPod.Spec.Containers))
}
if capturedPod.Spec.Containers[0].ImagePullPolicy != corev1.PullIfNotPresent {
t.Fatalf("container ImagePullPolicy = %q; want %q", capturedPod.Spec.Containers[0].ImagePullPolicy, corev1.PullIfNotPresent)
}
// Verify Pod annotation
if v, ok := capturedPod.Annotations["cnb-bp-npm-start-script"]; !ok || v != "start:prod" {
t.Errorf("Pod annotation cnb-bp-npm-start-script = %q (exists=%v); want start:prod", v, ok)
t.Logf("All annotations: %v", capturedPod.Annotations)
}
// Verify DownwardAPI volume has the env file
foundEnvFile := false
for _, vol := range capturedPod.Spec.Volumes {
if vol.Name == "platform" && vol.Projected != nil {
for _, source := range vol.Projected.Sources {
if source.DownwardAPI == nil {
continue
}
for _, item := range source.DownwardAPI.Items {
if item.Path == "env/BP_NPM_START_SCRIPT" {
foundEnvFile = true
if item.FieldRef.FieldPath != "metadata.annotations['cnb-bp-npm-start-script']" {
t.Errorf("FieldPath = %q; want metadata.annotations['cnb-bp-npm-start-script']", item.FieldRef.FieldPath)
}
}
}
}
}
}
if !foundEnvFile {
t.Error("DownwardAPI volume missing env/BP_NPM_START_SCRIPT item")
}
})
t.Run("CNB_START_SCRIPT and BP_* passthrough in pod annotations and downward API", func(t *testing.T) {
var capturedPod *corev1.Pod
ctrl := &mockJobCtrl{
execJobFn: func(ctx context.Context, job *corev1.Pod, logger io.Writer, result *channels.RingChannel) error {
capturedPod = job
go func() {
result.In() <- "complete"
result.In() <- "logcomplete"
}()
return nil
},
}
b := newTestBuilder(ctrl)
dir := newNodeDir(t)
re := &build.Request{
ServiceID: "svc1",
DeployVersion: "v1",
RbdNamespace: "ns",
Arch: "amd64",
SourceDir: dir,
CacheDir: "/tmp/cache",
BuildEnvs: map[string]string{
"CNB_START_SCRIPT": "start:prod",
"BP_NODE_VERSION": "20.10.0",
"BP_CUSTOM_FLAG": "enabled",
},
Logger: logger,
}
err := b.runCNBBuildJob(re, "img:v1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if capturedPod == nil {
t.Fatal("pod was not captured")
}
// Verify all annotations exist on the Pod
wantAnnotations := map[string]string{
"cnb-bp-npm-start-script": "start:prod",
"cnb-bp-node-version": "20.10.0",
"cnb-bp-custom-flag": "enabled",
}
for annKey, wantVal := range wantAnnotations {
if got, ok := capturedPod.Annotations[annKey]; !ok {
t.Errorf("missing annotation %q", annKey)
} else if got != wantVal {
t.Errorf("annotation %q = %q; want %q", annKey, got, wantVal)
}
}
// Verify DownwardAPI volume has all env files
wantEnvFiles := map[string]string{
"env/BP_NPM_START_SCRIPT": "metadata.annotations['cnb-bp-npm-start-script']",
"env/BP_NODE_VERSION": "metadata.annotations['cnb-bp-node-version']",
"env/BP_CUSTOM_FLAG": "metadata.annotations['cnb-bp-custom-flag']",
}
foundFiles := make(map[string]bool)
for _, vol := range capturedPod.Spec.Volumes {
if vol.Name == "platform" && vol.Projected != nil {
for _, source := range vol.Projected.Sources {
if source.DownwardAPI == nil {
continue
}
for _, item := range source.DownwardAPI.Items {
if wantField, ok := wantEnvFiles[item.Path]; ok {
foundFiles[item.Path] = true
if item.FieldRef.FieldPath != wantField {
t.Errorf("DownwardAPI %s FieldPath = %q; want %q", item.Path, item.FieldRef.FieldPath, wantField)
}
}
}
}
}
}
for path := range wantEnvFiles {
if !foundFiles[path] {
t.Errorf("DownwardAPI volume missing %s item", path)
}
}
})
t.Run("java maven fields are visible in pod annotations and envs", func(t *testing.T) {
var capturedPod *corev1.Pod
ctrl := &mockJobCtrl{
defaultLanguageBuildSetting: "default-maven",
execJobFn: func(ctx context.Context, job *corev1.Pod, logger io.Writer, result *channels.RingChannel) error {
capturedPod = job
go func() {
result.In() <- "complete"
result.In() <- "logcomplete"
}()
return nil
},
}
b := newTestBuilder(ctrl)
re := &build.Request{
ServiceID: "svc1",
DeployVersion: "v1",
RbdNamespace: "ns",
Arch: "amd64",
SourceDir: t.TempDir(),
CacheDir: "/tmp/cache",
Lang: code.JavaMaven,
BuildEnvs: map[string]string{
"BUILD_MAVEN_CUSTOM_GOALS": "clean package",
"BUILD_MAVEN_CUSTOM_OPTS": "-DskipTests",
"BUILD_MAVEN_JAVA_OPTS": "-Xmx1024m",
"BUILD_MAVEN_BUILT_MODULE": "service-a",
"BUILD_MAVEN_BUILT_ARTIFACT": "service-a/target/app.jar",
},
Logger: logger,
}
err := b.runCNBBuildJob(re, "img:v1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if capturedPod == nil {
t.Fatal("pod was not captured")
}
if len(capturedPod.Spec.Containers) != 1 {
t.Fatalf("expected 1 container, got %d", len(capturedPod.Spec.Containers))
}
wantAnnotations := map[string]string{
"cnb-bp-maven-build-arguments": "clean package",
"cnb-bp-maven-additional-build-arguments": "-DskipTests",
"cnb-bp-maven-built-module": "service-a",
"cnb-bp-maven-built-artifact": "service-a/target/app.jar",
"cnb-bp-maven-settings-path": "/platform/bindings/default-maven/settings.xml",
"cnb-lang": "C.UTF-8",
"cnb-lc-all": "C.UTF-8",
"cnb-maven-opts": "-Xmx1024m -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8",
}
for key, wantValue := range wantAnnotations {
if got := capturedPod.Annotations[key]; got != wantValue {
t.Fatalf("annotation %s = %q, want %q", key, got, wantValue)
}
}
wantEnv := map[string]string{
"BP_MAVEN_BUILD_ARGUMENTS": "clean package",
"BP_MAVEN_ADDITIONAL_BUILD_ARGUMENTS": "-DskipTests",
"BP_MAVEN_BUILT_MODULE": "service-a",
"BP_MAVEN_BUILT_ARTIFACT": "service-a/target/app.jar",
"MAVEN_OPTS": "-Xmx1024m -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8",
}
foundEnv := map[string]bool{}
for _, env := range capturedPod.Spec.Containers[0].Env {
if wantValue, ok := wantEnv[env.Name]; ok {
foundEnv[env.Name] = true
if env.Value != wantValue {
t.Fatalf("env %s = %q, want %q", env.Name, env.Value, wantValue)
}
}
}
for name := range wantEnv {
if !foundEnv[name] {
t.Fatalf("missing env %s in CNB pod", name)
}
}
wantPlatformEnvFiles := map[string]string{
"env/LANG": "metadata.annotations['cnb-lang']",
"env/LC_ALL": "metadata.annotations['cnb-lc-all']",
"env/MAVEN_OPTS": "metadata.annotations['cnb-maven-opts']",
}
foundPlatformEnvFiles := map[string]bool{}
for _, vol := range capturedPod.Spec.Volumes {
if vol.Name != "platform" || vol.Projected == nil {
continue
}
for _, source := range vol.Projected.Sources {
if source.DownwardAPI == nil {
continue
}
for _, item := range source.DownwardAPI.Items {
if wantField, ok := wantPlatformEnvFiles[item.Path]; ok {
foundPlatformEnvFiles[item.Path] = true
if item.FieldRef == nil || item.FieldRef.FieldPath != wantField {
t.Fatalf("platform env %s fieldRef = %+v, want %q", item.Path, item.FieldRef, wantField)
}
}
}
}
}
for path := range wantPlatformEnvFiles {
if !foundPlatformEnvFiles[path] {
t.Fatalf("missing platform env file %s in CNB pod", path)
}
}
})
t.Run("java maven derives built module from built artifact in pod", func(t *testing.T) {
var capturedPod *corev1.Pod
ctrl := &mockJobCtrl{
defaultLanguageBuildSetting: "default-maven",
execJobFn: func(ctx context.Context, job *corev1.Pod, logger io.Writer, result *channels.RingChannel) error {
capturedPod = job
go func() {
result.In() <- "complete"
result.In() <- "logcomplete"
}()
return nil
},
}
b := newTestBuilder(ctrl)
re := &build.Request{
ServiceID: "svc1",
DeployVersion: "v1",
RbdNamespace: "ns",
Arch: "amd64",
SourceDir: t.TempDir(),
CacheDir: "/tmp/cache",
Lang: code.JavaMaven,
BuildEnvs: map[string]string{
"BUILD_MAVEN_BUILT_ARTIFACT": "ruoyi-admin/target/ruoyi-admin.jar",
},
Logger: logger,
}
err := b.runCNBBuildJob(re, "img:v1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if capturedPod == nil {
t.Fatal("pod was not captured")
}
if got := capturedPod.Annotations["cnb-bp-maven-built-module"]; got != "ruoyi-admin" {
t.Fatalf("annotation cnb-bp-maven-built-module = %q, want %q", got, "ruoyi-admin")
}
if got := capturedPod.Annotations["cnb-bp-maven-built-artifact"]; got != "ruoyi-admin/target/ruoyi-admin.jar" {
t.Fatalf("annotation cnb-bp-maven-built-artifact = %q, want %q", got, "ruoyi-admin/target/ruoyi-admin.jar")
}
foundEnv := map[string]string{}
for _, env := range capturedPod.Spec.Containers[0].Env {
foundEnv[env.Name] = env.Value
}
if got := foundEnv["BP_MAVEN_BUILT_MODULE"]; got != "ruoyi-admin" {
t.Fatalf("env BP_MAVEN_BUILT_MODULE = %q, want %q", got, "ruoyi-admin")
}
if got := foundEnv["BP_MAVEN_BUILT_ARTIFACT"]; got != "ruoyi-admin/target/ruoyi-admin.jar" {
t.Fatalf("env BP_MAVEN_BUILT_ARTIFACT = %q, want %q", got, "ruoyi-admin/target/ruoyi-admin.jar")
}
})
}
// --- config.go: offline mode ---
// capability_id: rainbond.cnb.offline-mode
func TestIsOfflineMode(t *testing.T) {
t.Run("offline when marker file exists", func(t *testing.T) {
dir := t.TempDir()
marker := filepath.Join(dir, "BP_DEPENDENCY_MIRROR")
os.WriteFile(marker, []byte("file:///grdata/cnb"), 0644)
orig := offlineMirrorMarker
offlineMirrorMarker = marker
defer func() { offlineMirrorMarker = orig }()
if !isOfflineMode() {
t.Error("expected offline mode when marker file exists")
}
})
t.Run("online when marker file absent", func(t *testing.T) {
orig := offlineMirrorMarker
offlineMirrorMarker = "/nonexistent/path/marker"
defer func() { offlineMirrorMarker = orig }()
if isOfflineMode() {
t.Error("expected online mode when marker file absent")
}
})
}
// capability_id: rainbond.cnb.offline-mode
func TestGetCNBBuilderImageOffline(t *testing.T) {
tests := []struct {
name string
envVar string
offline bool
registryDomain string
want string
}{
{
name: "default online",
want: DefaultCNBBuilder,
},
{
name: "env var override",
envVar: "custom-registry.com/builder:v1",
want: "custom-registry.com/builder:v1",
},
{
name: "offline with goodrain.me",
offline: true,
registryDomain: "goodrain.me",
want: "goodrain.me/" + cnbBuilderShortName,
},
{
name: "offline with custom registry",
offline: true,
registryDomain: "harbor.example.com/lib",
want: "harbor.example.com/lib/" + cnbBuilderShortName,
},
{
name: "env var takes priority over offline",
envVar: "custom:v1",
offline: true,
registryDomain: "goodrain.me",
want: "custom:v1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
os.Unsetenv("CNB_BUILDER_IMAGE")
if tt.envVar != "" {
os.Setenv("CNB_BUILDER_IMAGE", tt.envVar)
defer os.Unsetenv("CNB_BUILDER_IMAGE")
}
orig := offlineMirrorMarker
if tt.offline {
dir := t.TempDir()
marker := filepath.Join(dir, "marker")
os.WriteFile(marker, []byte("file:///grdata/cnb"), 0644)
offlineMirrorMarker = marker
} else {
offlineMirrorMarker = "/nonexistent/marker"
}
defer func() { offlineMirrorMarker = orig }()
if tt.registryDomain != "" {
origDomain := builder.REGISTRYDOMAIN
builder.REGISTRYDOMAIN = tt.registryDomain
defer func() { builder.REGISTRYDOMAIN = origDomain }()
}
if got := GetCNBBuilderImage(); got != tt.want {
t.Errorf("GetCNBBuilderImage() = %q; want %q", got, tt.want)
}
})
}
}
// capability_id: rainbond.cnb.offline-mode
func TestGetCNBRunImageOffline(t *testing.T) {
tests := []struct {
name string
envVar string
offline bool
registryDomain string
want string
}{
{
name: "default online",
want: DefaultCNBRunImage,
},
{
name: "offline with goodrain.me",
offline: true,
registryDomain: "goodrain.me",
want: "goodrain.me/" + cnbRunShortName,
},
{
name: "offline with custom registry",
offline: true,
registryDomain: "harbor.example.com/lib",
want: "harbor.example.com/lib/" + cnbRunShortName,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
os.Unsetenv("CNB_RUN_IMAGE")
if tt.envVar != "" {
os.Setenv("CNB_RUN_IMAGE", tt.envVar)
defer os.Unsetenv("CNB_RUN_IMAGE")
}
orig := offlineMirrorMarker
if tt.offline {
dir := t.TempDir()
marker := filepath.Join(dir, "marker")
os.WriteFile(marker, []byte("file:///grdata/cnb"), 0644)
offlineMirrorMarker = marker
} else {
offlineMirrorMarker = "/nonexistent/marker"
}
defer func() { offlineMirrorMarker = orig }()
if tt.registryDomain != "" {
origDomain := builder.REGISTRYDOMAIN
builder.REGISTRYDOMAIN = tt.registryDomain
defer func() { builder.REGISTRYDOMAIN = origDomain }()
}
if got := GetCNBRunImage(); got != tt.want {
t.Errorf("GetCNBRunImage() = %q; want %q", got, tt.want)
}
})
}
}
// --- job.go: insecure-registry for run image ---
// capability_id: rainbond.cnb.creator-args-insecure-registry
func TestBuildCreatorArgsInsecureRegistry(t *testing.T) {
b := &Builder{}
t.Run("same registry - single insecure-registry", func(t *testing.T) {
origDomain := builder.REGISTRYDOMAIN
builder.REGISTRYDOMAIN = "goodrain.me"
defer func() { builder.REGISTRYDOMAIN = origDomain }()
dir := newNodeDir(t)
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{}}
args := b.buildCreatorArgs(re, "goodrain.me/app:v1", "goodrain.me/run:v1")
count := 0
for _, a := range args {
if strings.HasPrefix(a, "-insecure-registry=") {
count++
}
}
if count != 1 {
t.Errorf("expected 1 insecure-registry flag, got %d; args=%v", count, args)
}
})
t.Run("different registry - only local insecure-registry", func(t *testing.T) {
origDomain := builder.REGISTRYDOMAIN
builder.REGISTRYDOMAIN = "goodrain.me"
defer func() { builder.REGISTRYDOMAIN = origDomain }()
dir := newNodeDir(t)
re := &build.Request{SourceDir: dir, BuildEnvs: map[string]string{}}
args := b.buildCreatorArgs(re, "goodrain.me/app:v1", "registry.cn-hangzhou.aliyuncs.com/goodrain/run:v1")
insecureFlags := []string{}
for _, a := range args {
if strings.HasPrefix(a, "-insecure-registry=") {
insecureFlags = append(insecureFlags, a)
}
}
if len(insecureFlags) != 1 {
t.Errorf("expected 1 insecure-registry flag (local only), got %d; flags=%v", len(insecureFlags), insecureFlags)
}
for _, f := range insecureFlags {
if strings.Contains(f, "registry.cn-hangzhou.aliyuncs.com") {
t.Errorf("external registry should NOT be marked as insecure; flags=%v", insecureFlags)
}
}
})
}