diff --git a/pkg/oci/builder_test.go b/pkg/oci/builder_test.go index b824a6e69..dadcb2463 100644 --- a/pkg/oci/builder_test.go +++ b/pkg/oci/builder_test.go @@ -330,3 +330,92 @@ type fileInfo struct { Executable bool Linkname string } + +// TestBuilder_StaticEnvs ensures that certain "static" environment variables +// comprising Function metadata are added to the config. +func TestBuilder_StaticEnvs(t *testing.T) { + root, done := Mktemp(t) + defer done() + + staticEnvs := []string{ + "FUNC_CREATED", + "FUNC_VERSION", + } + + f, err := fn.New().Init(fn.Function{Root: root, Runtime: "go"}) + if err != nil { + t.Fatal(err) + } + + if err := NewBuilder("", true).Build(context.Background(), f, TestPlatforms); err != nil { + t.Fatal(err) + } + + // Assert + // Check if the OCI container defines at least one of the static + // variables on each of the constituent containers. + // --- + // Get the images list (manifest descripors) from the index + ociPath := path(f.Root, fn.RunDataDir, "builds", "last", "oci") + data, err := os.ReadFile(filepath.Join(ociPath, "index.json")) + if err != nil { + t.Fatal(err) + } + var index struct { + Manifests []struct { + Digest string `json:"digest"` + } `json:"manifests"` + } + if err := json.Unmarshal(data, &index); err != nil { + t.Fatal(err) + } + for _, manifestDesc := range index.Manifests { + + // Dereference the manifest descriptor into the referenced image manifest + manifestHash := strings.TrimPrefix(manifestDesc.Digest, "sha256:") + data, err := os.ReadFile(filepath.Join(ociPath, "blobs", "sha256", manifestHash)) + if err != nil { + t.Fatal(err) + } + var manifest struct { + Config struct { + Digest string `json:"digest"` + } `json:"config"` + } + if err := json.Unmarshal(data, &manifest); err != nil { + t.Fatal(err) + } + + // From the image manifest get the image's config.json + configHash := strings.TrimPrefix(manifest.Config.Digest, "sha256:") + data, err = os.ReadFile(filepath.Join(ociPath, "blobs", "sha256", configHash)) + if err != nil { + t.Fatal(err) + } + var config struct { + Config struct { + Env []string `json:"Env"` + } `json:"config"` + } + if err := json.Unmarshal(data, &config); err != nil { + panic(err) + } + + containsEnv := func(ss []string, name string) bool { + for _, s := range ss { + if strings.HasPrefix(s, name) { + return true + } + } + return false + } + + for _, expected := range staticEnvs { + t.Logf("checking for %q in slice %v", expected, config.Config.Env) + if containsEnv(config.Config.Env, expected) { + continue // to check the rest + } + t.Fatalf("static env %q not found in resultant container", expected) + } + } +} diff --git a/pkg/oci/containerize.go b/pkg/oci/containerize.go index 011ecc5e1..7559543c4 100644 --- a/pkg/oci/containerize.go +++ b/pkg/oci/containerize.go @@ -8,9 +8,11 @@ import ( "io" "io/fs" "os" + "os/exec" slashpath "path" "path/filepath" "strings" + "time" v1 "github.com/google/go-containerregistry/pkg/v1" "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 Variant: p.Variant, Config: v1.Config{ ExposedPorts: map[string]struct{}{"8080/tcp": {}}, - Env: cfg.f.Run.Envs.Slice(), + Env: newConfigEnvs(cfg), Cmd: []string{"/func/f"}, // NOTE: Using Cmd because Entrypoint can not be overridden WorkingDir: "/func/", StopSignal: "SIGKILL", @@ -482,6 +484,46 @@ func newConfig(cfg *buildConfig, p v1.Platform, layers ...v1.Layer) (desc v1.Des return } +// newConfigEnvs returns the final set of environment variables to build into +// the container. This consists of func-provided build metadata envs as well +// as any environment variables provided on the function itself. +func newConfigEnvs(cfg *buildConfig) []string { + envs := []string{} + + // FUNC_CREATED + // Formats container timestamp as RFC3339; a stricter version of the ISO 8601 + // format used by the container image manifest's 'Created' attribute. + envs = append(envs, "FUNC_CREATED="+cfg.t.Format(time.RFC3339)) + + // FUNC_VERSION + // If source controlled, and if being built from a system with git, the + // environment FUNC_VERSION will be populated. Otherwise it will exist + // (to indicate this logic was executed) but have an empty value. + if cfg.verbose { + fmt.Printf("cd %v && export FUNC_VERSION=$(git describe --tags)\n", cfg.f.Root) + } + cmd := exec.CommandContext(cfg.ctx, "git", "describe", "--tags") + cmd.Dir = cfg.f.Root + output, err := cmd.Output() + if err != nil { + if cfg.verbose { + fmt.Fprintf(os.Stderr, "unable to determine function version. %v", err) + } + envs = append(envs, "FUNC_VERSION=") + } else { + envs = append(envs, "FUNC_VERSION="+strings.TrimSpace(string(output))) + } + + // TODO: OTHERS? + // Other metadata that may be useful. Perhaps: + // - func client version (func cli) used when building this file? + // - user/environment which triggered this build? + // - A reflection of the function itself? Image, registry, etc. etc? + + // ENVs defined on the Function + return append(envs, cfg.f.Run.Envs.Slice()...) +} + func newImageIndex(cfg *buildConfig, imageDescs []v1.Descriptor) (index v1.IndexManifest, err error) { index = v1.IndexManifest{ SchemaVersion: 2,