Skip to content

Commit

Permalink
Add functionality to the preparer binary to identify the build direct…
Browse files Browse the repository at this point in the history
…ory context and GOOGLE_BUILDABLE value.

PiperOrigin-RevId: 651864929
Change-Id: I269f648f44b064394e9b13546264585f0412ccbb
  • Loading branch information
GCP Buildpacks Team authored and copybara-github committed Jul 12, 2024
1 parent 9f79d07 commit 1628753
Show file tree
Hide file tree
Showing 7 changed files with 271 additions and 23 deletions.
30 changes: 25 additions & 5 deletions cmd/firebase/preparer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ import (
)

var (
apphostingYAMLFilePath = flag.String("apphostingyaml_filepath", "", "File path to user defined apphosting.yaml")
projectID = flag.String("project_id", "", "User's GCP project ID")
appHostingYAMLOutputFilePath = flag.String("apphostingyaml_output_filepath", "", "File path to write the validated and formatted apphosting.yaml to")
dotEnvOutputFilePath = flag.String("dot_env_output_filepath", "", "File path to write the output .env file to")
apphostingYAMLFilePath = flag.String("apphostingyaml_filepath", "", "File path to user defined apphosting.yaml")
projectID = flag.String("project_id", "", "User's GCP project ID")
appHostingYAMLOutputFilePath = flag.String("apphostingyaml_output_filepath", "", "File path to write the validated and formatted apphosting.yaml to")
dotEnvOutputFilePath = flag.String("dot_env_output_filepath", "", "File path to write the output .env file to")
backendRootDirectory = flag.String("backend_root_directory", "", "File path to the application directory specified by the user")
buildpackConfigOutputFilePath = flag.String("buildpack_config_output_filepath", "", "File path to write the buildpack config to")
)

func main() {
Expand All @@ -47,13 +49,31 @@ func main() {
log.Fatal("--dot_env_output_filepath flag not specified.")
}

if backendRootDirectory == nil {
log.Fatal("--backend_root_directory flag not specified.")
}

if *buildpackConfigOutputFilePath == "" {
log.Fatal("--buildpack_config_output_filepath flag not specified.")
}

secretClient, err := secretmanager.NewClient(context.Background())
if err != nil {
log.Fatal(fmt.Errorf("failed to create secretmanager client: %w", err))
}
defer secretClient.Close()

err = preparer.Prepare(context.Background(), secretClient, *apphostingYAMLFilePath, *projectID, *appHostingYAMLOutputFilePath, *dotEnvOutputFilePath)
opts := preparer.Options{
SecretClient: secretClient,
AppHostingYAMLPath: *apphostingYAMLFilePath,
ProjectID: *projectID,
AppHostingYAMLOutputFilePath: *appHostingYAMLOutputFilePath,
EnvDereferencedOutputFilePath: *dotEnvOutputFilePath,
BackendRootDirectory: *backendRootDirectory,
BuildpackConfigOutputFilePath: *buildpackConfigOutputFilePath,
}

err = preparer.Prepare(context.Background(), opts)
if err != nil {
log.Fatal(err)
}
Expand Down
1 change: 1 addition & 0 deletions pkg/firebase/preparer/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ go_library(
"//pkg/firebase/apphostingschema",
"//pkg/firebase/envvars",
"//pkg/firebase/secrets",
"//pkg/firebase/util",
],
)

Expand Down
48 changes: 32 additions & 16 deletions pkg/firebase/preparer/preparer.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,57 +19,73 @@ package preparer
import (
"context"
"fmt"
"os"

"github.com/GoogleCloudPlatform/buildpacks/pkg/firebase/apphostingschema"
"github.com/GoogleCloudPlatform/buildpacks/pkg/firebase/envvars"

secrets "github.com/GoogleCloudPlatform/buildpacks/pkg/firebase/secrets"
"github.com/GoogleCloudPlatform/buildpacks/pkg/firebase/util"
)

// Options contains data for the preparer to perform pre-build logic.
type Options struct {
SecretClient secrets.SecretManager
AppHostingYAMLPath string
ProjectID string
AppHostingYAMLOutputFilePath string
EnvDereferencedOutputFilePath string
BackendRootDirectory string
BuildpackConfigOutputFilePath string
}

// Prepare performs pre-build logic for App Hosting backends including:
// * Reading, sanitizing, and writing user-defined environment variables in apphosting.yaml to a new file.
// * Dereferencing secrets in apphosting.yaml.
// * Writing the build directory context to files on disk for other build steps to consume.
//
// Preparer will always write a prepared apphosting.yaml and a .env file to disk, even if there
// is no schema to write.
func Prepare(ctx context.Context, secretClient secrets.SecretManager, appHostingYAMLPath string, projectID string, appHostingYAMLOutputFilePath string, envDereferencedOutputFilePath string) error {
func Prepare(ctx context.Context, opts Options) error {
dereferencedEnvMap := map[string]string{} // Env map with dereferenced secret material
appHostingYAML := apphostingschema.AppHostingSchema{}

if appHostingYAMLPath != "" {
if opts.AppHostingYAMLPath != "" {
var err error

appHostingYAML, err = apphostingschema.ReadAndValidateAppHostingSchemaFromFile(appHostingYAMLPath)
appHostingYAML, err = apphostingschema.ReadAndValidateAppHostingSchemaFromFile(opts.AppHostingYAMLPath)
if err != nil {
return fmt.Errorf("reading in and validating apphosting.yaml at path %v: %w", appHostingYAMLPath, err)
return fmt.Errorf("reading in and validating apphosting.yaml at path %v: %w", opts.AppHostingYAMLPath, err)
}

apphostingschema.Sanitize(&appHostingYAML)

err = secrets.Normalize(appHostingYAML.Env, projectID)
if err != nil {
if err := secrets.Normalize(appHostingYAML.Env, opts.ProjectID); err != nil {
return fmt.Errorf("normalizing apphosting.yaml fields: %w", err)
}

err = secrets.PinVersions(ctx, secretClient, appHostingYAML.Env)
if err != nil {
if err := secrets.PinVersions(ctx, opts.SecretClient, appHostingYAML.Env); err != nil {
return fmt.Errorf("pinning secrets in apphosting.yaml: %w", err)
}

dereferencedEnvMap, err = secrets.GenerateBuildDereferencedEnvMap(ctx, secretClient, appHostingYAML.Env)
if err != nil {
if dereferencedEnvMap, err = secrets.GenerateBuildDereferencedEnvMap(ctx, opts.SecretClient, appHostingYAML.Env); err != nil {
return fmt.Errorf("dereferencing secrets in apphosting.yaml: %w", err)
}
}

err := appHostingYAML.WriteToFile(appHostingYAMLOutputFilePath)
if err != nil {
return fmt.Errorf("writing final apphosting.yaml to %v: %w", appHostingYAMLOutputFilePath, err)
if err := appHostingYAML.WriteToFile(opts.AppHostingYAMLOutputFilePath); err != nil {
return fmt.Errorf("writing final apphosting.yaml to %v: %w", opts.AppHostingYAMLOutputFilePath, err)
}

err = envvars.Write(dereferencedEnvMap, envDereferencedOutputFilePath)
if err := envvars.Write(dereferencedEnvMap, opts.EnvDereferencedOutputFilePath); err != nil {
return fmt.Errorf("writing final dereferenced environment variables to %v: %w", opts.EnvDereferencedOutputFilePath, err)
}

cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("writing final dereferenced environment variables to %v: %w", envDereferencedOutputFilePath, err)
return fmt.Errorf("failed to get current working directory: %w", err)
}
if err := util.WriteBuildDirectoryContext(cwd, opts.BackendRootDirectory, opts.BuildpackConfigOutputFilePath); err != nil {
return fmt.Errorf("writing build directory context to %v: %w", opts.BuildpackConfigOutputFilePath, err)
}

return nil
Expand Down
13 changes: 12 additions & 1 deletion pkg/firebase/preparer/preparer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func TestPrepare(t *testing.T) {
testDir := t.TempDir()
outputFilePathYAML := testDir + "/outputYAML"
outputFilePathEnv := testDir + "/outputEnv"
outputFilePathBuildpackConfig := testDir + "/outputBuildpackConfig"

testCases := []struct {
desc string
Expand Down Expand Up @@ -115,7 +116,17 @@ func TestPrepare(t *testing.T) {

// Testing happy paths
for _, test := range testCases {
if err := Prepare(context.Background(), fakeSecretClient, test.appHostingYAMLPath, test.projectID, outputFilePathYAML, outputFilePathEnv); err != nil {
opts := Options{
SecretClient: fakeSecretClient,
AppHostingYAMLPath: test.appHostingYAMLPath,
ProjectID: test.projectID,
AppHostingYAMLOutputFilePath: outputFilePathYAML,
EnvDereferencedOutputFilePath: outputFilePathEnv,
BackendRootDirectory: "",
BuildpackConfigOutputFilePath: outputFilePathBuildpackConfig,
}

if err := Prepare(context.Background(), opts); err != nil {
t.Errorf("Error in test '%v'. Error was %v", test.desc, err)
}

Expand Down
9 changes: 8 additions & 1 deletion pkg/firebase/util/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

package(default_visibility = ["//:__subpackages__"])

Expand All @@ -13,3 +13,10 @@ go_library(
"//pkg/gcpbuildpack",
],
)

go_test(
name = "util_test",
srcs = ["util_test.go"],
embed = [":util"],
rundir = ".",
)
95 changes: 95 additions & 0 deletions pkg/firebase/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
package util

import (
"errors"
"fmt"
"os"
"path/filepath"

Expand All @@ -24,6 +26,10 @@ import (
gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack"
)

var (
supportedMonorepoConfigFiles = []string{"nx.json"}
)

// ApplicationDirectory looks up the path to the application directory from the environment. Returns
// the application root by default.
func ApplicationDirectory(ctx *gcp.Context) string {
Expand All @@ -33,3 +39,92 @@ func ApplicationDirectory(ctx *gcp.Context) string {
}
return appDir
}

// supportedMonorepoConfigFileExists checks if a supported monorepo config file exists in the
// specified directory.
func supportedMonorepoConfigFileExists(dir string) (bool, error) {
for _, filename := range supportedMonorepoConfigFiles {
f := filepath.Join(dir, filename)
_, err := os.ReadFile(f)
if os.IsNotExist(err) {
continue
}
if err != nil {
return false, err
}
return true, nil
}
return false, nil
}

// buildDirectoryContext returns (1) the "build directory" from which the buildpacks will be run,
// and (2) the directory containing the application to be built, relative to the build directory.
//
// The build directory and application directory are different in monorepo contexts, in which we
// want to run the buildpacks process from the root of the monorepo to ensure all necessary files
// are accessible, but we want to build the application inside the user-specified subdirectory.
// see go/apphosting-monorepo-support for more details.
func buildDirectoryContext(cwd, userSpecifiedAppDirPath string) (string, string, error) {
if userSpecifiedAppDirPath == "" {
return "", "", nil
}

absoluteAppDirPath := filepath.Join(cwd, userSpecifiedAppDirPath)
_, err := os.Stat(absoluteAppDirPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return "", "", fmt.Errorf("cannot find user-specified backend.root_directory path %q in repo: %w", userSpecifiedAppDirPath, err)
}
return "", "", err
}
var monorepoRootPath string
curr := absoluteAppDirPath
for {
exists, err := supportedMonorepoConfigFileExists(curr)
if err != nil {
return "", "", err
}
if exists {
monorepoRootPath = curr
break
}
if curr == cwd || curr == "/" || curr == "." {
break
}
curr = filepath.Dir(curr)
}
if monorepoRootPath == "" {
// If no monorepo config file is detected, then the user-specified app directory path is the
// root of an application in a subdirectory.
return userSpecifiedAppDirPath, "", nil
}
// If a monorepo config file is detected, then the monorepo root is the "build directory" and the
// user-specified app directory path is the root of the sub-application.
mrp, err := filepath.Rel(cwd, monorepoRootPath)
if err != nil {
return "", "", err
}
adp, err := filepath.Rel(monorepoRootPath, absoluteAppDirPath)
if err != nil {
return "", "", err
}
return mrp, adp, nil
}

// WriteBuildDirectoryContext writes the build directory context to the specified buildpack config
// file path.
func WriteBuildDirectoryContext(cwd, appDirectoryPath, buildpackConfigOutputFilePath string) error {
buildDirectory, relativeProjectDirectory, err := buildDirectoryContext(cwd, appDirectoryPath)
if err != nil {
return err
}
err = os.MkdirAll(buildpackConfigOutputFilePath, 0755)
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(buildpackConfigOutputFilePath, "build-directory.txt"), []byte(buildDirectory), 0644)
if err != nil {
return err
}
return os.WriteFile(filepath.Join(buildpackConfigOutputFilePath, "relative-project-directory.txt"), []byte(relativeProjectDirectory), 0644)
}
Loading

0 comments on commit 1628753

Please sign in to comment.