diff --git a/cmd/build.go b/cmd/build.go index 1c16cdce9b..54889429a5 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -70,7 +70,7 @@ EXAMPLES `, SuggestFor: []string{"biuld", "buidl", "built"}, PreRunE: bindEnv("image", "path", "builder", "registry", "confirm", - "push", "builder-image", "base-image", "platform", "verbose", + "push", "builder-image", "base-image", "ca-bundle", "platform", "verbose", "build-timestamp", "registry-insecure", "registry-authfile", "username", "password", "token"), RunE: func(cmd *cobra.Command, args []string) error { return runBuild(cmd, args, newClient) @@ -114,6 +114,8 @@ EXAMPLES "Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE)") cmd.Flags().StringP("base-image", "", f.Build.BaseImage, "Override the base image for your function (host builder only)") + cmd.Flags().StringP("ca-bundle", "", f.Build.CACertBundle, + "Path to CA certificate bundle file for SSL verification during build. Useful when building behind corporate proxies. ($FUNC_CA_BUNDLE)") cmd.Flags().StringP("image", "i", f.Image, "Full image name in the form [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry ($FUNC_IMAGE)") @@ -230,6 +232,9 @@ type buildConfig struct { // TODO: gauron99 -- make option to add a path to dockerfile ? BaseImage string + // CACertBundle is the path to a CA certificate bundle file for SSL verification + CACertBundle string + // Path of the function implementation on local disk. Defaults to current // working directory of the process. Path string @@ -271,6 +276,7 @@ func newBuildConfig() buildConfig { }, BuilderImage: viper.GetString("builder-image"), BaseImage: viper.GetString("base-image"), + CACertBundle: viper.GetString("ca-bundle"), Image: viper.GetString("image"), Path: viper.GetString("path"), Platform: viper.GetString("platform"), @@ -294,6 +300,7 @@ func (c buildConfig) Configure(f fn.Function) fn.Function { } f.Image = c.Image f.Build.BaseImage = c.BaseImage + f.Build.CACertBundle = c.CACertBundle // Path, Platform and Push are not part of a function's state. return f } diff --git a/docs/reference/func_build.md b/docs/reference/func_build.md index 344574a334..9c68bee41f 100644 --- a/docs/reference/func_build.md +++ b/docs/reference/func_build.md @@ -61,6 +61,7 @@ func build --build-timestamp Use the actual time as the created time for the docker image. This is only useful for buildpacks builder. -b, --builder string Builder to use when creating the function's container. Currently supported builders are "host", "pack" and "s2i". ($FUNC_BUILDER) (default "pack") --builder-image string Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE) + --ca-bundle string Path to CA certificate bundle file for SSL verification during build. Useful when building behind corporate proxies. ($FUNC_CA_BUNDLE) -c, --confirm Prompt to confirm options interactively ($FUNC_CONFIRM) -h, --help help for build -i, --image string Full image name in the form [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry ($FUNC_IMAGE) diff --git a/pkg/buildpacks/builder.go b/pkg/buildpacks/builder.go index 40a9492068..bb57f18ab1 100644 --- a/pkg/buildpacks/builder.go +++ b/pkg/buildpacks/builder.go @@ -224,11 +224,70 @@ func (b *Builder) Build(ctx context.Context, f fn.Function, platforms []fn.Platf opts.Env["BP_IMAGE_LABELS"] = strings.Join(imageLabels, " ") } - var bindings = make([]string, 0, len(f.Build.Mounts)) - for _, m := range f.Build.Mounts { - bindings = append(bindings, fmt.Sprintf("%s:%s", m.Source, m.Destination)) + // CA Certificate Bundle support for corporate proxies + // Use Paketo CA certificates buildpack binding instead of manual env vars + if f.Build.CACertBundle != "" { + // Resolve to absolute path if relative + caBundlePath := f.Build.CACertBundle + if !filepath.IsAbs(caBundlePath) { + caBundlePath = filepath.Join(f.Root, caBundlePath) + } + + // Validate that the CA bundle file exists + if _, err := os.Stat(caBundlePath); err != nil { + return fmt.Errorf("CA bundle file not found: %w", err) + } + + // Create a temporary directory for the CA certificates binding + caCertsBindingDir, err := os.MkdirTemp("", "func-ca-certs-*") + if err != nil { + return fmt.Errorf("failed to create CA certificates binding directory: %w", err) + } + // Note: We don't defer cleanup here because the build process needs it + // The directory will be cleaned up when the temp dir is cleaned by the OS + + // Write the binding type file + if err := os.WriteFile(filepath.Join(caCertsBindingDir, "type"), []byte("ca-certificates"), 0644); err != nil { + return fmt.Errorf("failed to write CA certificates binding type: %w", err) + } + + // Copy the CA bundle to the binding directory + caData, err := os.ReadFile(caBundlePath) + if err != nil { + return fmt.Errorf("failed to read CA bundle: %w", err) + } + if err := os.WriteFile(filepath.Join(caCertsBindingDir, "ca-certificates.crt"), caData, 0644); err != nil { + return fmt.Errorf("failed to write CA certificates to binding: %w", err) + } + + // Set SERVICE_BINDING_ROOT if not already set + if _, ok := opts.Env["SERVICE_BINDING_ROOT"]; !ok { + opts.Env["SERVICE_BINDING_ROOT"] = "/platform/bindings" + } + + // Create a local copy of mounts and add the CA certificates binding + // We don't modify f.Build.Mounts directly to avoid race conditions + mounts := make([]fn.MountSpec, len(f.Build.Mounts), len(f.Build.Mounts)+1) + copy(mounts, f.Build.Mounts) + mounts = append(mounts, fn.MountSpec{ + Source: caCertsBindingDir, + Destination: filepath.Join(opts.Env["SERVICE_BINDING_ROOT"], "ca-certificates"), + }) + + // Convert mounts to bindings format + var bindings = make([]string, 0, len(mounts)) + for _, m := range mounts { + bindings = append(bindings, fmt.Sprintf("%s:%s", m.Source, m.Destination)) + } + opts.ContainerConfig.Volumes = bindings + } else { + // No CA bundle, just use the existing mounts + var bindings = make([]string, 0, len(f.Build.Mounts)) + for _, m := range f.Build.Mounts { + bindings = append(bindings, fmt.Sprintf("%s:%s", m.Source, m.Destination)) + } + opts.ContainerConfig.Volumes = bindings } - opts.ContainerConfig.Volumes = bindings // only trust our known builders opts.TrustBuilder = TrustBuilder diff --git a/pkg/functions/function.go b/pkg/functions/function.go index 09d2506273..1d27e33bbd 100644 --- a/pkg/functions/function.go +++ b/pkg/functions/function.go @@ -150,6 +150,11 @@ type BuildSpec struct { // Build Env variables to be set BuildEnvs Envs `yaml:"buildEnvs,omitempty"` + // CACertBundle specifies the path to a CA certificate bundle file to use + // for SSL verification during build. This is useful when building behind + // corporate proxies with SSL inspection. + CACertBundle string `yaml:"caCertBundle,omitempty" jsonschema:"description=Path to CA certificate bundle for SSL verification during build"` + // PVCSize specifies the size of persistent volume claim used to store function // when using deployment and remote build process (only relevant when Remote is true). PVCSize string `yaml:"pvcSize,omitempty"` diff --git a/pkg/s2i/builder.go b/pkg/s2i/builder.go index 9b621e77bb..36b7aa77c8 100644 --- a/pkg/s2i/builder.go +++ b/pkg/s2i/builder.go @@ -220,6 +220,38 @@ func (b *Builder) Build(ctx context.Context, f fn.Function, platforms []fn.Platf cfg.BuildVolumes = append(cfg.BuildVolumes, fmt.Sprintf("%s:%s:ro,Z", m.Source, m.Destination)) } + // CA Certificate Bundle support for corporate proxies + if f.Build.CACertBundle != "" { + // Resolve to absolute path if relative + caBundlePath := f.Build.CACertBundle + if !filepath.IsAbs(caBundlePath) { + caBundlePath = filepath.Join(f.Root, caBundlePath) + } + + // Validate that the CA bundle file exists + if _, err := os.Stat(caBundlePath); err != nil { + return fmt.Errorf("CA bundle file not found: %w", err) + } + + // For S2I, we need to mount the CA bundle and set environment variables + // Mount the CA bundle into a standard location + caDestPath := "/tmp/ca-certificates.crt" + cfg.BuildVolumes = append(cfg.BuildVolumes, + fmt.Sprintf("%s:%s:ro,Z", caBundlePath, caDestPath)) + + // Set environment variables for various runtimes to use the CA bundle + cfg.Environment = append(cfg.Environment, + api.EnvironmentSpec{Name: "SSL_CERT_FILE", Value: caDestPath}, + api.EnvironmentSpec{Name: "REQUESTS_CA_BUNDLE", Value: caDestPath}, // Python + api.EnvironmentSpec{Name: "NODE_EXTRA_CA_CERTS", Value: caDestPath}, // Node.js + api.EnvironmentSpec{Name: "CURL_CA_BUNDLE", Value: caDestPath}, // curl + api.EnvironmentSpec{Name: "GIT_SSL_CAINFO", Value: caDestPath}, // git + api.EnvironmentSpec{Name: "PIP_CERT", Value: caDestPath}, // pip (Python) + api.EnvironmentSpec{Name: "NPM_CONFIG_CAFILE", Value: caDestPath}, // npm (Node.js) + api.EnvironmentSpec{Name: "CARGO_HTTP_CAINFO", Value: caDestPath}, // cargo (Rust) + ) + } + if runtime.GOOS == "linux" { cfg.DockerNetworkMode = "host" } diff --git a/schema/func_yaml-schema.json b/schema/func_yaml-schema.json index 762abdfe47..9ff99c9114 100644 --- a/schema/func_yaml-schema.json +++ b/schema/func_yaml-schema.json @@ -41,6 +41,10 @@ "type": "array", "description": "Build Env variables to be set" }, + "caCertBundle": { + "type": "string", + "description": "Path to CA certificate bundle for SSL verification during build" + }, "pvcSize": { "type": "string", "description": "PVCSize specifies the size of persistent volume claim used to store function\nwhen using deployment and remote build process (only relevant when Remote is true)."