func/function.go

217 lines
6.5 KiB
Go

package faas
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"golang.org/x/net/publicsuffix"
)
type Function struct {
root string
runtime string // will be empty unless initialized/until initialized
name string // will be empty unless initialized/until initialized.
initializer Initializer
}
func NewFunction(root string) (f *Function, err error) {
f = &Function{}
// Default root to current directory, as an absolute path.
if root == "" {
root = "."
}
if root, err = filepath.Abs(root); err != nil {
return
}
f.root = root
// Populate with data from config if it exists.
err = applyConfig(f, root)
return
}
// DerivedName returns the name that will be used if path derivation is choosen, limited in its upward recursion.
// This is exposed for preemptive calculation for interactive confirmation, such as via a CLI.
func (f *Function) DerivedName(searchLimit int) string {
return pathToDomain(f.root, searchLimit)
}
func (f *Function) Initialize(runtime, context, name string, domainSearchLimit int, initializer Initializer) (err error) {
// Assert runtime is provided
if runtime == "" {
err = errors.New("runtime not specified")
return
}
// If there exists contentious files (congig files for instance), this function may have already been initialized.
files, err := contentiousFilesIn(f.root)
if err != nil {
return
} else if len(files) > 0 {
return errors.New(fmt.Sprintf("The chosen directory '%v' contains contentious files: %v. Has the Service Function already been created? Try either using a different directory, deleting the service function if it exists, or manually removing the files.", f.root, files))
}
// Ensure there are no non-hidden files, and again none of the aforementioned contentious files.
empty, err := isEffectivelyEmpty(f.root)
if err != nil {
return
} else if !empty {
err = errors.New("The directory must be empty of visible files and recognized config files before it can be initialized.")
return
}
// Derive a name if not provided
if name == "" {
name = pathToDomain(f.root, domainSearchLimit)
}
if name == "" {
err = errors.New("Function name must be provided or be derivable from path")
return
}
f.name = name
// Write the template implementation in the appropriate runtime
if err = initializer.Initialize(runtime, context, f.root); err != nil {
return
}
// runtime was validated
f.runtime = runtime
// Write out the state as a config file and return.
return writeConfig(f)
}
func (f *Function) Initialized() bool {
// TODO: this should probably be more robust than checking what amounts to a
// side-effect of the initialization process.
return (f.runtime != "" && f.name != "")
}
// contentiousFiles are files which, if extant, preclude the creation of a
// service function rooted in the given directory.
var contentiousFiles = []string{
".faas.yaml",
".appsody-config.yaml",
}
// contentiousFilesIn the given directoy
func contentiousFilesIn(dir string) (contentious []string, err error) {
files, err := ioutil.ReadDir(dir)
for _, file := range files {
for _, name := range contentiousFiles {
if file.Name() == name {
contentious = append(contentious, name)
}
}
}
return
}
// effectivelyEmpty directories are those which have no visible files,
// and none of the explicitly enumerated contentious files.
func isEffectivelyEmpty(dir string) (bool, error) {
// Check for contentious files
if contentious, err := contentiousFilesIn(dir); len(contentious) > 0 {
return false, err
}
// Check for any non-hidden files
files, err := ioutil.ReadDir(dir)
if err != nil {
return false, err
}
for _, file := range files {
if !strings.HasPrefix(file.Name(), ".") {
return false, nil
}
}
return true, nil
}
// Convert a path to a domain.
// Searches up the path string until a domain (TLD+1) is detected.
// Subdirectories are considered subdomains.
// Ex: Path: "/home/users/jane/src/example.com/admin/www"
// Returns: "www.admin.example.com"
// maxLevels is the number of directories to walk upwards beyond the current
// directory to determine domain (i.e. current directory is always considered.
// Zero indicates only consider last path element.)
func pathToDomain(path string, maxLevels int) string {
var (
// parts of the path, separated by os separator
parts = strings.Split(path, string(os.PathSeparator))
// subdomains derived from the path
subdomains []string
// domain derived from the path
domain string
)
// Loop over parts from back to front (recursing upwards), building
// optional subdomains until a root domain (TLD+1) is detected.
for i := len(parts) - 1; i >= 0; i-- {
part := parts[i]
// Support limited recursion
// Tests, for instance, need to be allowed to reliably fail by having their
// recursion contained within ./testdata if recursion is set to -1, there
// is no limit. 0 indicates only the current directory is considered.
iteration := len(parts) - 1 - i
if maxLevels >= 0 && iteration > maxLevels {
break
}
// Detect TLD+1
// If the current directory has a valid TLD plus one, it is a match.
// This is determined by using the public suffices list, which includes
// both ICANN managed TLDs as well as an extended list (matching, for
// instance 'cluster.local')
if suffix, _ := publicsuffix.EffectiveTLDPlusOne(part); suffix != "" {
domain = part
break // no directories above the nearest TLD+1 should be considered.
}
// Skip blanks
// Path elements which are blank, such as in the case of a trailing slash
// are ignored and the recursion continues, effectively collapsing ex: '//'.
if part == "" {
continue
}
// Build subdomain
// Each path element which appears before the TLD+1 is a subdomain.
// ex: '/home/users/jane/src/example.com/us-west-2/admin/www' creates the
// subdomain []string{'www', 'admin', 'us-west-2'}
subdomains = append(subdomains, part)
}
// Unable to derive domain
// If the entire path was searched, but no parts matched a TLD+1, the domain
// will be blank. In this case, the path was insufficient to derive a domain
// ex "/home/users/jane/src/test" contains no TLD, thus the final domain must
// be explicitly provided.
if domain == "" {
return ""
}
// Prepend subdomains
// If the path was a subdirectory within a TLD+1, these sudbomains
// are prepended to the TLD+1 to create the final domain.
// ex: '/home/users/jane/src/example.com/us-west-2/admin/www' yields
// www.admin.use-west-2.example.com
if len(subdomains) > 0 {
subdomains = append(subdomains, domain)
return strings.Join(subdomains, ".")
}
return domain
}