Gcloud functions deployment doesn't find Golang template files - go

I have written some Golang code which works when tested on my local machine. When I deploy this as a Google Cloud function it fails because it cannot open a template file. The line of code failing is:
t, err := template.New("list.gohtml").ParseFiles("list.gohtml")
After this call err is set to open list.gohtml: no such file or directory
The file is in the same directory as the go source file and is not listed in .gcloudignore or .gitignore. The gcloud functions documentation says all files in the directory will be uploaded unless listed in one of those ignore files and if I run gcloud meta list-files-for-upload then the file list.gohtml is included in the list displayed.
Is there some magic folder layout to make this work, or an option to the gcloud functions deploy command?

Based on #DazWilkin's reply, I now call the function below at the start of the serving function.
Rather than hardwiring the path into the template file names (which would make it fail when tested locally) this simply checks for the presence of the Gcloud source file directory below the current one, and if present, makes it the working directory, so file resolution will now happen exactly as it does when tested locally.
import "os"
const gcloudFuncSourceDir = "serverless_function_source_code"
func fixDir() {
fileInfo, err := os.Stat(gcloudFuncSourceDir)
if err == nil && fileInfo.IsDir() {
_ = os.Chdir(gcloudFuncSourceDir)
}
}

I created a Function that enumerates the uploaded files:
func L(w http.ResponseWriter, r *http.Request) {
var files []string
err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
files = append(files, path)
return nil
})
if err != nil {
panic(err)
}
for _, file := range files {
fmt.Fprintln(w, file)
}
}
It output:
.
go.mod
go.sum
main.go
serverless_function_source_code
serverless_function_source_code/f.go
serverless_function_source_code/go.mod
serverless_function_source_code/test.tmpl
Rewriting the template-using function:
tmpl, err := template.New("test.tmpl").ParseFiles("serverless_function_source_code/test.tmpl")
Works!
However, this is undocumented and a hack :-(
The alternative is to embed the templates as strings within Golang files.
I recommend submitting a feature request on Google's Issue Tracker for Cloud Functions

If there's no go.mod file (in other words, if modules aren't enabled), then the behavior changes and the files are under src/<package-name>/, not under serverless_function_source_code/. The main.go file used to drive the cloud function resides in src/serverless_function_app/main/main.go.
For example:
I have a Go application with the following two files:
listfiles/file_system.go
listfiles/tmpl/letter.html
I deployed it with these commands:
cd listfiles
gcloud functions deploy ListFiles --runtime go113 --trigger-http --allow-unauthenticated
The result was that the current directory was set to /srv. Underneath /srv was a typical GOROOT tree with src/ and pkg/ directories. For example:
.googlebuild
.googlebuild/source-code.tar.gz
pkg
pkg/linux_amd64
pkg/linux_amd64/github.com
pkg/linux_amd64/github.com/GoogleCloudPlatform
pkg/linux_amd64/github.com/GoogleCloudPlatform/functions-framework-go
pkg/linux_amd64/github.com/GoogleCloudPlatform/functions-framework-go/funcframework.a
src
src/cloud.google.com
src/cloud.google.com/go
src/cloud.google.com/go/.git
src/cloud.google.com/go/.git/HEAD
src/cloud.google.com/go/.git/branches
[... snip...]
src/go.uber.org/zap/zaptest/testingt_test.go
src/go.uber.org/zap/zaptest/timeout.go
src/go.uber.org/zap/zaptest/timeout_test.go
src/go.uber.org/zap/zaptest/writer.go
src/go.uber.org/zap/zaptest/writer_test.go
src/listfiles
src/listfiles/file_system.go
src/listfiles/tmpl
src/listfiles/tmpl/letter.html
src/serverless_function_app
src/serverless_function_app/main
src/serverless_function_app/main/main.go

Related

Failing to understand go embed

Goal is to embed a directory tree in my binary, then copy it to a user directory later. It consists mostly of text files. I am attempting to display contents of the files in that subdirectory within the project, named .config (hence the use of go:embed all).
As I understand Go embedding, the following should work outside its own directory, but when executed, lists the name of the first
file in the directory tree but cannot open it or display its contents. Within its own project directory it works fine.
//go:embed all:.config
var configFiles embed.FS
func main() {
ls(configFiles)
}
func ls(files embed.FS) error {
fs.WalkDir(files, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
show(path) // Display contents of file
}
return nil
})
return nil
}
(Complete code at https://go.dev/play/p/A0HzD0rbvX- but it doesn't work because there's no .config directory)
The program walks the embedded file system, but opens files using the operating system. Fix by opening the file in the file system.
Pass the file system to show:
if !d.IsDir() {
show(files, path) // <-- pass files here
}
Open the file using the file system:
func show(files fs.FS, filename string) { // <-- add arg here
f, err := files.Open(filename) // <-- FS.Open

How to find a sibling file when the os.Getwd() is different in different environments

myprogram/
|
|-main.go
|-dir1/
|-data/
|-datafile.json
|-runner.go
|-runner_test.go
In runner.go, I have a simple function that reads the datafile.json. Something like
func GetPayload() (string, err) {
dBytes, dErr := ioutil.ReadFile("dir1/data/datafile.json")
if dErr != nil { return nil, dErr}
return dBytes, nil
}
I'm using Go in a Lambda with a structure similar to above. When the Lambda runs in its actual environment, it starts at main.go, and then invokes GetPayload() from runner.go. However, I have a test in a simple worker node machine in runner_test.go that also hits GetPayload() .
During "normal" execution (from main.go) - this works OK. However, when GetPayload() is invoked from runner_test.go, it errors, saying
open dir1/data/datafile.json no such file or directory
This makes sense, because during the test, the working directory is the directory that houses runner_test.go, which is data/, so there is no dir1 as a child of it. I've been trying to play with using os.Getwd() and getting the paths from there like:
pwd, _ := os.Getwd()
dBytes, dErr := ioutil.ReadFile(pwd + "dir1/data/datafile.json")
But again, that won't work, because for runner_test.go pwd is user/myname/myprogram/dir1, but from main.go, it turns up as user/myname/myprogram.
Any idea how I can find and open datafile.json from within GetPayload() in any environment? I could pass an optional parameter to GetPayload() but if possible, it'd be great to avoid that.
If the file is static (meaning that it doesn't need to change after you build the program), you can embed it into the built program. This means you no longer have to worry about run-time file paths.
import (
"embed"
)
//go:embed data/*
var dataFiles embed.FS
func GetPayload() (string, err) {
dBytes, dErr := dataFiles.ReadFile(dataFiles, "data/datafile.json")
if dErr != nil { return nil, dErr}
return dBytes, nil
}
Now the files in your data/ directory are embedded in this variable dataFiles which acts as a read-only file system.
For more info:
Read more about embed in the package documentation overview.
Read my answer about "when to use embed"
For data files that your program needs during runtime, either use a fixed directory and refer to that, or accept a command line argument or some sort of configuration that tells you where the file is.
When running unit tests, the wd is the directory containing the test file. One convention is to use a testdata/ directory under the directory containing the test, and put all data files there. That way you can refer to that file from the test by using testdata/datafile.json.
You can use a copy of the file you need during runtime as your test file, or you can use a symlink from the runtime data file to the test file under the testdata/ dir.
For data files that your program needs during runtime, either use a fixed
directory and refer to that
Someone made this suggestion, which I agree with. To that end, you can use
something like this:
package main
import (
"os"
"path/filepath"
)
func main() {
d, err := os.UserCacheDir()
if err != nil {
panic(err)
}
d = filepath.Join(d, "file.json")
f, err := os.Open(d)
if err != nil {
panic(err)
}
defer f.Close()
os.Stdout.ReadFrom(f)
}
https://golang.org/pkg/os#UserCacheDir
https://golang.org/pkg/os#UserConfigDir

how to set my config file in viper present outside of my working repository

I need to set a path to my config file. The problem is, for example, my config file is in my_app/config but my working directory is in my_app/workingdir and I should not set an absolute path or a path from my working dir because in production the absolute path and working directory will change. For test I know I can set the path from my code location, but I need a cleaner way.
workingdir, err := os.Getwd()
if err != nil {
logger.Error(err)
}
viper.SetConfigFile(workingdir + "/config/local.yaml")
Don't keep your config file out of your working directory, always make a new folder in your main package like conf and put your file into it.
So, the directory structure will be
my_app/conf/config.yml
Then set the config file into viper through init function of your main.go
//main.go
func init() {
setUpViper()
}
func setUpViper() {
viper.AddConfigPath("./conf")
viper.SetConfigName("config")
}
Use ".." to specify a parent of a directory.
Because relative paths are rooted at the working directory, there's no need to get the working directory in the application code.
Use the following to specify the path to the configuration file:
viper.SetConfigFile("../config/local.yaml")
You can use .. to go to parent directory, and then from there go to config folder.
workingdir, err := os.Getwd()
if err != nil {
logger.Error(err)
}
viper.SetConfigFile(workingdir + "/../config/local.yaml")

Example using go-assets in gin

I'd like to have a single binary for a go-app, rather than having to bundle static files alongside the deployment.
I'm using a function like this to access PNGs I'm loading:
func getFileList(dir string) (fileList []os.FileInfo, err error) {
// USAGE:
// fileList,_ := getFileList(PNG_DIR)
f, err := os.Open(PNG_DIR)
defer f.Close()
checkErr(err)
fileList, err = f.Readdir(0)
checkErr(err)
return fileList, err
}
I take this list of files and serve it on a static endpoint, with some logic.
I have read the following documentation for using go-assets
https://github.com/jessevdk/go-assets-builder/blob/master/builder.go
https://github.com/jessevdk/go-assets-builder
https://github.com/jessevdk/go-assets/blob/master/generate.go
As well as this gin specific example:
https://github.com/gin-gonic/gin/blob/master/examples/assets-in-binary/assets.go
https://github.com/jessevdk/go-assets/blob/master/example_test.go
https://github.com/gin-gonic/gin/tree/master/examples/assets-in-binary
Which contains the following example:
Prepare Packages
go get github.com/gin-gonic/gin go get
github.com/jessevdk/go-assets-builder
Generate assets.go
go-assets-builder html -o assets.go
Build the server
go build -o assets-in-binary
Run
./assets-in-binary
However, it's not clear to me how to call this file I have built. For example, What do I change in my getFileList() function to now point to whatever I built in the binary, what is it even called and how would I know that?
Usually on gin you would use router.Statuc(path, dir) however you said you first load a list of files and I guess you will later use http.ServeFile.
With go-bindata you have all the files already inside the executable, you can access them using the Asset(file) function...
Basically this is a very simple static handler for gin:
func StaticHandler(c *gin.Context) {
p := c.Param("filepath")
data, err := Assets(p)
if err != nil { return }
c.Writer.Write(data)
}
You can register the static handler into your router:
router.GET("/static/*filepath", StaticHandler)
This allows to access static resources the following way: /static/css/style.css and will load the file css/style.css
You could get the list of files inside your folder, create a map and use that map for the static handler (to limit what files are accesed)

Go get parent directory

I've some command line program which I need to read files from parent folder, I mean
-parentDir
-- myproject
--- cmd
----main.go
--otherdir
-file.json
As you can see otherdir is like sibling to myproject and I need from my main.go read the file.json
what I've tried is like following
func visit(path string, f os.FileInfo, err error) error {
fmt.Printf("Visited: %s\n", path)
return nil
}
func main() {
flag.Parse()
root := flag.Arg(0)
err := filepath.Walk(root, visit)
fmt.Printf("filepath.Walk() returned %v\n", err)
}
I've also try to provide args(-1) which doesnt help...
Any idea how from command line program I can read some files that on level up from my executable ?
I've also tried with
import "github.com/mitchellh/go-homedir"
func Path(path string) (error, string) {
home, err := homedir.Dir()
}
this give the root directory which doesnt help either...
It doesn't matter where the binary is, it matters what the working directory is (the directory you're in when you execute the program). All relative paths will be relative to the current working directory. So, if you're executing from myproject, you'd use something like ../ as the root path to Walk.
That said, I would highly recommend you make the path configurable, rather than assuming the binary will always be executed from some particular location within the source tree.

Resources