Skip to content

Commit ab2bbff

Browse files
authored
feat: host builder metadata envs (#2195)
1 parent 7bf3e10 commit ab2bbff

File tree

2 files changed

+132
-1
lines changed

2 files changed

+132
-1
lines changed

pkg/oci/builder_test.go

+89
Original file line numberDiff line numberDiff line change
@@ -330,3 +330,92 @@ type fileInfo struct {
330330
Executable bool
331331
Linkname string
332332
}
333+
334+
// TestBuilder_StaticEnvs ensures that certain "static" environment variables
335+
// comprising Function metadata are added to the config.
336+
func TestBuilder_StaticEnvs(t *testing.T) {
337+
root, done := Mktemp(t)
338+
defer done()
339+
340+
staticEnvs := []string{
341+
"FUNC_CREATED",
342+
"FUNC_VERSION",
343+
}
344+
345+
f, err := fn.New().Init(fn.Function{Root: root, Runtime: "go"})
346+
if err != nil {
347+
t.Fatal(err)
348+
}
349+
350+
if err := NewBuilder("", true).Build(context.Background(), f, TestPlatforms); err != nil {
351+
t.Fatal(err)
352+
}
353+
354+
// Assert
355+
// Check if the OCI container defines at least one of the static
356+
// variables on each of the constituent containers.
357+
// ---
358+
// Get the images list (manifest descripors) from the index
359+
ociPath := path(f.Root, fn.RunDataDir, "builds", "last", "oci")
360+
data, err := os.ReadFile(filepath.Join(ociPath, "index.json"))
361+
if err != nil {
362+
t.Fatal(err)
363+
}
364+
var index struct {
365+
Manifests []struct {
366+
Digest string `json:"digest"`
367+
} `json:"manifests"`
368+
}
369+
if err := json.Unmarshal(data, &index); err != nil {
370+
t.Fatal(err)
371+
}
372+
for _, manifestDesc := range index.Manifests {
373+
374+
// Dereference the manifest descriptor into the referenced image manifest
375+
manifestHash := strings.TrimPrefix(manifestDesc.Digest, "sha256:")
376+
data, err := os.ReadFile(filepath.Join(ociPath, "blobs", "sha256", manifestHash))
377+
if err != nil {
378+
t.Fatal(err)
379+
}
380+
var manifest struct {
381+
Config struct {
382+
Digest string `json:"digest"`
383+
} `json:"config"`
384+
}
385+
if err := json.Unmarshal(data, &manifest); err != nil {
386+
t.Fatal(err)
387+
}
388+
389+
// From the image manifest get the image's config.json
390+
configHash := strings.TrimPrefix(manifest.Config.Digest, "sha256:")
391+
data, err = os.ReadFile(filepath.Join(ociPath, "blobs", "sha256", configHash))
392+
if err != nil {
393+
t.Fatal(err)
394+
}
395+
var config struct {
396+
Config struct {
397+
Env []string `json:"Env"`
398+
} `json:"config"`
399+
}
400+
if err := json.Unmarshal(data, &config); err != nil {
401+
panic(err)
402+
}
403+
404+
containsEnv := func(ss []string, name string) bool {
405+
for _, s := range ss {
406+
if strings.HasPrefix(s, name) {
407+
return true
408+
}
409+
}
410+
return false
411+
}
412+
413+
for _, expected := range staticEnvs {
414+
t.Logf("checking for %q in slice %v", expected, config.Config.Env)
415+
if containsEnv(config.Config.Env, expected) {
416+
continue // to check the rest
417+
}
418+
t.Fatalf("static env %q not found in resultant container", expected)
419+
}
420+
}
421+
}

pkg/oci/containerize.go

+43-1
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import (
88
"io"
99
"io/fs"
1010
"os"
11+
"os/exec"
1112
slashpath "path"
1213
"path/filepath"
1314
"strings"
15+
"time"
1416

1517
v1 "github.com/google/go-containerregistry/pkg/v1"
1618
"github.com/google/go-containerregistry/pkg/v1/tarball"
@@ -428,7 +430,7 @@ func newConfig(cfg *buildConfig, p v1.Platform, layers ...v1.Layer) (desc v1.Des
428430
Variant: p.Variant,
429431
Config: v1.Config{
430432
ExposedPorts: map[string]struct{}{"8080/tcp": {}},
431-
Env: cfg.f.Run.Envs.Slice(),
433+
Env: newConfigEnvs(cfg),
432434
Cmd: []string{"/func/f"}, // NOTE: Using Cmd because Entrypoint can not be overridden
433435
WorkingDir: "/func/",
434436
StopSignal: "SIGKILL",
@@ -482,6 +484,46 @@ func newConfig(cfg *buildConfig, p v1.Platform, layers ...v1.Layer) (desc v1.Des
482484
return
483485
}
484486

487+
// newConfigEnvs returns the final set of environment variables to build into
488+
// the container. This consists of func-provided build metadata envs as well
489+
// as any environment variables provided on the function itself.
490+
func newConfigEnvs(cfg *buildConfig) []string {
491+
envs := []string{}
492+
493+
// FUNC_CREATED
494+
// Formats container timestamp as RFC3339; a stricter version of the ISO 8601
495+
// format used by the container image manifest's 'Created' attribute.
496+
envs = append(envs, "FUNC_CREATED="+cfg.t.Format(time.RFC3339))
497+
498+
// FUNC_VERSION
499+
// If source controlled, and if being built from a system with git, the
500+
// environment FUNC_VERSION will be populated. Otherwise it will exist
501+
// (to indicate this logic was executed) but have an empty value.
502+
if cfg.verbose {
503+
fmt.Printf("cd %v && export FUNC_VERSION=$(git describe --tags)\n", cfg.f.Root)
504+
}
505+
cmd := exec.CommandContext(cfg.ctx, "git", "describe", "--tags")
506+
cmd.Dir = cfg.f.Root
507+
output, err := cmd.Output()
508+
if err != nil {
509+
if cfg.verbose {
510+
fmt.Fprintf(os.Stderr, "unable to determine function version. %v", err)
511+
}
512+
envs = append(envs, "FUNC_VERSION=")
513+
} else {
514+
envs = append(envs, "FUNC_VERSION="+strings.TrimSpace(string(output)))
515+
}
516+
517+
// TODO: OTHERS?
518+
// Other metadata that may be useful. Perhaps:
519+
// - func client version (func cli) used when building this file?
520+
// - user/environment which triggered this build?
521+
// - A reflection of the function itself? Image, registry, etc. etc?
522+
523+
// ENVs defined on the Function
524+
return append(envs, cfg.f.Run.Envs.Slice()...)
525+
}
526+
485527
func newImageIndex(cfg *buildConfig, imageDescs []v1.Descriptor) (index v1.IndexManifest, err error) {
486528
index = v1.IndexManifest{
487529
SchemaVersion: 2,

0 commit comments

Comments
 (0)