mirror of https://github.com/knative/func.git
217 lines
6.5 KiB
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
|
|
}
|