Skip to content

Commit

Permalink
feat: decouple function name from function domain (#127)
Browse files Browse the repository at this point in the history
* decouple function name from function domain

Signed-off-by: Zbynek Roubalik <zroubali@redhat.com>
  • Loading branch information
zroubalik committed Sep 24, 2020
1 parent 02309a2 commit 0258626
Show file tree
Hide file tree
Showing 15 changed files with 126 additions and 312 deletions.
17 changes: 7 additions & 10 deletions client.go
Expand Up @@ -11,7 +11,6 @@ const (
DefaultRegistry = "docker.io"
DefaultRuntime = "go"
DefaultTrigger = "http"
DefaultMaxRecursion = 5 // when determining a name from path
)

// Client for managing Function instances.
Expand Down Expand Up @@ -127,7 +126,6 @@ func New(options ...Option) *Client {
lister: &noopLister{output: os.Stdout},
dnsProvider: &noopDNSProvider{output: os.Stdout},
progressListener: &noopProgressListener{},
domainSearchLimit: DefaultMaxRecursion, // no recursion limit deriving domain by default.
}

// Apply passed options, which take ultimate precidence.
Expand Down Expand Up @@ -306,6 +304,12 @@ func (c *Client) Create(cfg Function) (err error) {
// Initialize creates a new Function project locally using the settings
// provided on a Function object.
func (c *Client) Initialize(cfg Function) (err error) {

// Create project root directory, if it doesn't already exist
if err = os.MkdirAll(cfg.Root, 0755); err != nil {
return
}

// Create Function of the given root path.
f, err := NewFunction(cfg.Root)
if err != nil {
Expand All @@ -320,15 +324,8 @@ func (c *Client) Initialize(cfg Function) (err error) {

f.Image = cfg.Image

// Set the name to that provided, defaulting to path derivation if empty.
// Set the name to that provided.
f.Name = cfg.Name
if cfg.Name == "" {
f.Name = pathToDomain(f.Root, c.domainSearchLimit)
if f.Name == "" {
err = errors.New("Function name must be deriveable from path or explicitly provided")
return
}
}

// Assert runtime was provided, or default.
f.Runtime = cfg.Runtime
Expand Down
78 changes: 21 additions & 57 deletions client_test.go
Expand Up @@ -56,8 +56,14 @@ func TestCreateWritesTemplate(t *testing.T) {
// TestCreateInitializedAborts ensures that a directory which contains an initialized
// function does not reinitialize
func TestCreateInitializedAborts(t *testing.T) {
root := "testdata/example.com/testCreateInitializedAborts" // contains only a .faas.config
root := "testdata/example.com/testCreateInitializedAborts"
defer os.RemoveAll(root)

client := faas.New()
if err := client.Initialize(faas.Function{Root: root}); err != nil {
t.Fatal(err)
}

if err := client.Initialize(faas.Function{Root: root}); err == nil {
t.Fatal("error expected initilizing a path already containing an initialized Function")
}
Expand All @@ -67,6 +73,15 @@ func TestCreateInitializedAborts(t *testing.T) {
// files aborts.
func TestCreateNonemptyDirectoryAborts(t *testing.T) {
root := "testdata/example.com/testCreateNonemptyDirectoryAborts" // contains only a single visible file.
if err := os.MkdirAll(root, 0744); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
_, err := os.Create(root + "/file.txt")
if err != nil {
t.Fatal(err)
}

client := faas.New()
if err := client.Initialize(faas.Function{Root: root}); err == nil {
t.Fatal("error expected initilizing a Function in a nonempty directory")
Expand Down Expand Up @@ -212,57 +227,6 @@ func TestUnsupportedRuntime(t *testing.T) {
}
}

// TestDeriveDomain ensures that the name of the service is a domain derived
// from the current path if possible.
// see unit tests on the pathToDomain for more detailed logic.
func TestDeriveName(t *testing.T) {
// Create the root Function directory
root := "testdata/example.com/testDeriveDomain"
if err := os.MkdirAll(root, 0700); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)

client := faas.New(faas.WithRepository(TestRepository))
if err := client.Create(faas.Function{Root: root}); err != nil {
t.Fatal(err)
}

f, err := faas.NewFunction(root)
if err != nil {
t.Fatal(err)
}

if f.Name != "testDeriveDomain.example.com" {
t.Fatalf("unexpected function name '%v'", f.Name)
}
}

// TestDeriveSubdomans ensures that a subdirectory structure is interpreted as
// multilevel subdomains when calculating a derived name for a service.
func TestDeriveSubdomains(t *testing.T) {
// Create the test Function root
root := "testdata/example.com/region1/testDeriveSubdomains"
if err := os.MkdirAll(root, 0700); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)

client := faas.New(faas.WithRepository(TestRepository))
if err := client.Create(faas.Function{Root: root}); err != nil {
t.Fatal(err)
}

f, err := faas.NewFunction(root)
if err != nil {
t.Fatal(err)
}

if f.Name != "testDeriveSubdomains.region1.example.com" {
t.Fatalf("unexpected function name '%v'", f.Name)
}
}

// TestNamed ensures that an explicitly passed name is used in leau of the
// path derived name when provided, and persists through instantiations.
func TestNamed(t *testing.T) {
Expand Down Expand Up @@ -387,8 +351,8 @@ func TestDeriveImageDefaultRegistry(t *testing.T) {
func TestCreateDelegates(t *testing.T) {
var (
root = "testdata/example.com/testCreateDelegates" // .. in which to initialize
expectedName = "testCreateDelegates.example.com" // expected to be derived
expectedImage = "quay.io/alice/testCreateDelegates.example.com:latest"
expectedName = "testCreateDelegates" // expected to be derived
expectedImage = "quay.io/alice/testCreateDelegates:latest"
builder = mock.NewBuilder()
pusher = mock.NewPusher()
deployer = mock.NewDeployer()
Expand Down Expand Up @@ -501,8 +465,8 @@ func TestRun(t *testing.T) {
func TestUpdate(t *testing.T) {
var (
root = "testdata/example.com/testUpdate"
expectedName = "testUpdate.example.com"
expectedImage = "quay.io/alice/testUpdate.example.com:latest"
expectedName = "testUpdate"
expectedImage = "quay.io/alice/testUpdate:latest"
builder = mock.NewBuilder()
pusher = mock.NewPusher()
updater = mock.NewUpdater()
Expand Down Expand Up @@ -580,7 +544,7 @@ func TestUpdate(t *testing.T) {
func TestRemoveByPath(t *testing.T) {
var (
root = "testdata/example.com/testRemoveByPath"
expectedName = "testRemoveByPath.example.com"
expectedName = "testRemoveByPath"
remover = mock.NewRemover()
)

Expand Down
18 changes: 9 additions & 9 deletions cmd/create.go
Expand Up @@ -20,7 +20,6 @@ func init() {
createCmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options - $FAAS_CONFIRM")
createCmd.Flags().StringP("image", "i", "", "Optional full image name, in form [registry]/[namespace]/[name]:[tag] for example quay.io/myrepo/project.name:latest (overrides --repository) - $FAAS_IMAGE")
createCmd.Flags().StringP("namespace", "n", "", "Override namespace into which the Function is deployed (on supported platforms). Default is to use currently active underlying platform setting - $FAAS_NAMESPACE")
createCmd.Flags().StringP("path", "p", cwd(), "Path to the new project directory - $FAAS_PATH")
createCmd.Flags().StringP("repository", "r", "", "Repository for built images, ex 'docker.io/myuser' or just 'myuser'. Optional if --image provided. - $FAAS_REPOSITORY")
createCmd.Flags().StringP("runtime", "l", faas.DefaultRuntime, "Function runtime language/framework. - $FAAS_RUNTIME")
createCmd.Flags().StringP("templates", "", filepath.Join(configPath(), "templates"), "Extensible templates path. - $FAAS_TEMPLATES")
Expand All @@ -38,18 +37,18 @@ func init() {
}

var createCmd = &cobra.Command{
Use: "create <name>",
Use: "create <path>",
Short: "Create a new Function, including initialization of local files and deployment.",
SuggestFor: []string{"cerate", "new"},
PreRunE: bindEnv("image", "namespace", "path", "repository", "runtime", "templates", "trigger", "confirm"),
PreRunE: bindEnv("image", "namespace", "repository", "runtime", "templates", "trigger", "confirm"),
RunE: runCreate,
}

func runCreate(cmd *cobra.Command, args []string) (err error) {
config := newCreateConfig(args).Prompt()

function := faas.Function{
Name: config.Name,
Name: config.initConfig.Name,
Root: config.initConfig.Path,
Runtime: config.initConfig.Runtime,
Trigger: config.Trigger,
Expand Down Expand Up @@ -106,23 +105,24 @@ func newCreateConfig(args []string) createConfig {
}
}

// Prompt the user with value of config members, allowing for interaractive changes.
// Prompt the user with value of config members, allowing for interactive changes.
// Skipped if not in an interactive terminal (non-TTY), or if --confirm (agree to
// all prompts) was not explicitly set.
func (c createConfig) Prompt() createConfig {
name := deriveName(c.Name, c.initConfig.Path)
if !interactiveTerminal() || !c.initConfig.Confirm {
// Just print the basics if not confirming
fmt.Printf("Project path: %v\n", c.initConfig.Path)
fmt.Printf("Project name: %v\n", name)
fmt.Printf("Function name: %v\n", c.initConfig.Name)
fmt.Printf("Runtime: %v\n", c.Runtime)
fmt.Printf("Trigger: %v\n", c.Trigger)
return c
}

derivedName, derivedPath := deriveNameAndAbsolutePathFromPath(prompt.ForString("Project path", c.initConfig.Path, prompt.WithRequired(true)))
return createConfig{
initConfig: initConfig{
Path: prompt.ForString("Project path", c.initConfig.Path),
Name: prompt.ForString("Project name", name, prompt.WithRequired(true)),
Name: derivedName,
Path: derivedPath,
Runtime: prompt.ForString("Runtime", c.Runtime),
Trigger: prompt.ForString("Trigger", c.Trigger),
// Templates intentionally omitted from prompt for being an edge case.
Expand Down
29 changes: 15 additions & 14 deletions cmd/init.go
Expand Up @@ -14,7 +14,6 @@ import (
func init() {
root.AddCommand(initCmd)
initCmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options - $FAAS_CONFIRM")
initCmd.Flags().StringP("path", "p", cwd(), "Path to the new project directory - $FAAS_PATH")
initCmd.Flags().StringP("runtime", "l", faas.DefaultRuntime, "Function runtime language/framework. - $FAAS_RUNTIME")
initCmd.Flags().StringP("templates", "", filepath.Join(configPath(), "templates"), "Extensible templates path. - $FAAS_TEMPLATES")
initCmd.Flags().StringP("trigger", "t", faas.DefaultTrigger, "Function trigger (ex: 'http','events') - $FAAS_TRIGGER")
Expand All @@ -25,10 +24,10 @@ func init() {
}

var initCmd = &cobra.Command{
Use: "init <name>",
Use: "init <path>",
Short: "Initialize a new Function project",
SuggestFor: []string{"inti", "new"},
PreRunE: bindEnv("path", "runtime", "templates", "trigger", "confirm"),
PreRunE: bindEnv("runtime", "templates", "trigger", "confirm"),
RunE: runInit,
// TODO: autocomplate Functions for runtime and trigger.
}
Expand All @@ -49,10 +48,10 @@ func runInit(cmd *cobra.Command, args []string) error {
}

type initConfig struct {
// Name of the service in DNS-compatible format (ex myfunc.example.com)
// Name of the Function.
Name string

// Path to files on disk. Defaults to current working directory.
// Absolute path to Function on disk.
Path string

// Runtime language/framework.
Expand All @@ -78,13 +77,15 @@ type initConfig struct {
// newInitConfig returns a config populated from the current execution context
// (args, flags and environment variables)
func newInitConfig(args []string) initConfig {
var name string
var path string
if len(args) > 0 {
name = args[0] // If explicitly provided, use.
path = args[0] // If explicitly provided, use.
}

derivedName, derivedPath := deriveNameAndAbsolutePathFromPath(path)
return initConfig{
Name: deriveName(name, viper.GetString("path")), // args[0] or derived
Path: viper.GetString("path"),
Name: derivedName,
Path: derivedPath,
Runtime: viper.GetString("runtime"),
Templates: viper.GetString("templates"),
Trigger: viper.GetString("trigger"),
Expand All @@ -96,19 +97,19 @@ func newInitConfig(args []string) initConfig {
// Skipped if not in an interactive terminal (non-TTY), or if --confirm false (agree to
// all prompts) was set (default).
func (c initConfig) Prompt() initConfig {
name := deriveName(c.Name, c.Path)
if !interactiveTerminal() || !c.Confirm {
// Just print the basics if not confirming
fmt.Printf("Project path: %v\n", c.Path)
fmt.Printf("Project name: %v\n", name)
fmt.Printf("Function name: %v\n", c.Name)
fmt.Printf("Runtime: %v\n", c.Runtime)
fmt.Printf("Trigger: %v\n", c.Trigger)
return c
}

derivedName, derivedPath := deriveNameAndAbsolutePathFromPath(prompt.ForString("Project path", c.Path, prompt.WithRequired(true)))
return initConfig{
// TODO: Path should be prompted for and set prior to name attempting path derivation. Test/fix this if necessary.
Path: prompt.ForString("Project path", c.Path),
Name: prompt.ForString("Project name", name, prompt.WithRequired(true)),
Name: derivedName,
Path: derivedPath,
Runtime: prompt.ForString("Runtime", c.Runtime),
Trigger: prompt.ForString("Trigger", c.Trigger),
// Templates intentiopnally omitted from prompt for being an edge case.
Expand Down
32 changes: 27 additions & 5 deletions cmd/root.go
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/mitchellh/go-homedir"
"github.com/ory/viper"
Expand Down Expand Up @@ -165,21 +166,42 @@ func functionWithOverrides(root, namespace, image string) (f faas.Function, err

// deriveName returns the explicit value (if provided) or attempts to derive
// from the given path. Path is defaulted to current working directory, where
// a function configuration, if it exists and contains a name, is used. Lastly
// derivation using the path us used.
// a Function configuration, if it exists and contains a name, is used.
func deriveName(explicitName string, path string) string {
// If the name was explicitly provided, use it.
if explicitName != "" {
return explicitName
}

// If the directory at path contains an initialized Function, use the name therein
f, err := faas.NewFunction(path)
if err == nil && f.Name != "" {
return f.Name
}
maxRecursion := faas.DefaultMaxRecursion
derivedName, _ := faas.DerivedName(path, maxRecursion)
return derivedName

return ""
}

// deriveNameAndAbsolutePathFromPath returns resolved Function name and absolute path
// to the Function project root. The input parameter path could be one of:
// 'relative/path/to/foo', '/absolute/path/to/foo', 'foo' or ''
func deriveNameAndAbsolutePathFromPath(path string) (string, string) {
var absPath string

// If path is not specifed, we would like to use current working dir
if path == "" {
path = cwd()
}

// Expand the passed Function name to its absolute path
absPath, err := filepath.Abs(path)
if err != nil {
return "", ""
}

// Get the name of the Function, which equals to name of the current directory
pathParts := strings.Split(strings.TrimRight(path, string(os.PathSeparator)), string(os.PathSeparator))
return pathParts[len(pathParts)-1], absPath
}

// deriveImage returns the same image name which will be used if no explicit
Expand Down
2 changes: 1 addition & 1 deletion docs/commands.md
Expand Up @@ -2,7 +2,7 @@

## `init`

Creates a new Function project at _`path`_. If _`path`_ is unspecified, assumes the current directory. If _`path`_ does not exist, it will be created. The user can specify the runtime and trigger with flags.
Creates a new Function project at _`path`_. If _`path`_ is unspecified, assumes the current directory. If _`path`_ does not exist, it will be created. The function name is the name of the leaf directory at path. The user can specify the runtime and trigger with flags.

Similar `kn` command: none.

Expand Down

0 comments on commit 0258626

Please sign in to comment.