refactor: config path accessors with instantiation cleanup (#686)

* feat: config and repository path creation

Removes need to use a client to trigger creation of paths
Adds back static path accessors
Enables creation of paths when configured repos is outside config
Cleans up instantiation logic, including removal of some setters

* fix spelling mistakes per review
This commit is contained in:
Luke Kingland 2021-12-06 23:03:28 +09:00 committed by GitHub
parent 2f241824ff
commit 92ac14a6f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 83 additions and 76 deletions

124
client.go
View File

@ -8,6 +8,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"runtime/debug"
"time" "time"
"github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
@ -26,12 +27,17 @@ const (
// DefaultVersion is the initial value for string members whose implicit type // DefaultVersion is the initial value for string members whose implicit type
// is a semver. // is a semver.
DefaultVersion = "0.0.0" DefaultVersion = "0.0.0"
// DefaultConfigPath is used in the unlikely event that
// the user has no home directory (no ~), there is no
// XDG_CONFIG_HOME set, and no WithConfigPath was used.
DefaultConfigPath = ".config/func"
) )
// Client for managing Function instances. // Client for managing Function instances.
type Client struct { type Client struct {
repositories *Repositories // Repositories management repositoriesPath string // path to repositories
templates *Templates // Templates management repositoriesURI string // repo URI (overrides repositories path)
verbose bool // print verbose logs verbose bool // print verbose logs
builder Builder // Builds a runnable image source builder Builder // Builds a runnable image source
pusher Pusher // Pushes Funcation image to a remote pusher Pusher // Pushes Funcation image to a remote
@ -44,6 +50,8 @@ type Client struct {
registry string // default registry for OCI image tags registry string // default registry for OCI image tags
progressListener ProgressListener // progress listener progressListener ProgressListener // progress listener
emitter Emitter // Emits CloudEvents to functions emitter Emitter // Emits CloudEvents to functions
repositories *Repositories // Repositories management
templates *Templates // Templates management
} }
// ErrNotBuilt indicates the Function has not yet been built. // ErrNotBuilt indicates the Function has not yet been built.
@ -161,14 +169,8 @@ type Emitter interface {
// New client for Function management. // New client for Function management.
func New(options ...Option) *Client { func New(options ...Option) *Client {
// Assert the global config directory exists (including child directories
// such as repositories)
assertConfigDir()
// Instantiate client with static defaults. // Instantiate client with static defaults.
c := &Client{ c := &Client{
repositories: &Repositories{},
templates: &Templates{},
builder: &noopBuilder{output: os.Stdout}, builder: &noopBuilder{output: os.Stdout},
pusher: &noopPusher{output: os.Stdout}, pusher: &noopPusher{output: os.Stdout},
deployer: &noopDeployer{output: os.Stdout}, deployer: &noopDeployer{output: os.Stdout},
@ -178,54 +180,61 @@ func New(options ...Option) *Client {
dnsProvider: &noopDNSProvider{output: os.Stdout}, dnsProvider: &noopDNSProvider{output: os.Stdout},
progressListener: &NoopProgressListener{}, progressListener: &NoopProgressListener{},
emitter: &noopEmitter{}, emitter: &noopEmitter{},
repositoriesPath: filepath.Join(ConfigPath(), "repositories"),
} }
c.repositories = newRepositories(c)
c.templates = newTemplates(c)
for _, o := range options { for _, o := range options {
o(c) o(c)
} }
// Initialize sub-managers using now-fully-initialized client.
c.repositories = newRepositories(c)
c.templates = newTemplates(c)
// Trigger the creation of the config and repository paths
_ = ConfigPath() // Config is package-global scoped
_ = c.RepositoriesPath() // Repositories is Client-specific
return c return c
} }
// assertConfigDir makes a best-effort attempt to create the config directory // The default config path is evaluated in the following order, from lowest
// (including required subdirectories). // to highest precedence.
func assertConfigDir() { // 1. The static default is DefaultConfigPath (./.config/func)
// NOTE: regarding running as a user with no home directory: // 2. ~/.config/func if it exists (can be expanded: user has a home dir)
// The default is .config/func in current working directory when there is no // 3. The value of $XDG_CONFIG_PATH/func if the environment variable exists.
// available HOME in which to find .`~/.config/func`. // The path will be created if it does not already exist.
// Since it is expected that the code elsewhere never assume the config func ConfigPath() (path string) {
// directory exists (doing so is a racing condition), it is valid to simply path = DefaultConfigPath
// handle errors at this level.
if err := os.MkdirAll(RepositoriesPath(), 0700); err != nil {
fmt.Fprintf(os.Stderr, "Error creating '%v': %v", RepositoriesPath(), err)
}
}
// RepositoriesPath is the static default path to repositories used by a Client.
// This path can be overridden on intantiation of a client using the
// WithRepositories option.
func RepositoriesPath() string {
return filepath.Join(ConfigPath(), "repositories")
}
// ConfigPath returns the default config directory path used by the Client.
// The default is ~/.config/func. If defined, XDG_CONFIG_HOME is used.
// In the event of an error calculating ~ (no home directory for the current
// user), the relative path '.config/func' is used.
func ConfigPath() string {
// 'XDG_CONFIG_HOME/func' takes precidence if defined
if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
return filepath.Join(xdg, "func")
}
// ~/.config/func is the default if ~ can be expanded // ~/.config/func is the default if ~ can be expanded
if home, err := homedir.Expand("~"); err == nil { if home, err := homedir.Expand("~"); err == nil {
return filepath.Join(home, ".config", "func") path = filepath.Join(home, ".config", "func")
} }
// The default (edge case) is to return a relative path of .config inidicating // 'XDG_CONFIG_HOME/func' takes precidence if defined
// the current working directory when neither aforementioned exist. if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
return ".config/func" path = filepath.Join(xdg, "func")
}
mkdir(path) // make sure it exists
return
}
// RepositoriesPath accesses the currently effective repositories path,
// which defaults to [ConfigPath]/repositories but can be set explicitly using
// the WithRepositories option when creating the client..
// The path will be created if it does not already exist.
func (c *Client) RepositoriesPath() (path string) {
path = c.repositories.Path()
mkdir(path) // make sure it exists
return
}
// RepositoriesPath is a convenience method for accessing the default path to
// repositories that will be used by new instances of a Client unless options
// such as WithRepositories are used to override.
// The path will be created if it does not already exist.
func RepositoriesPath() string {
return New().RepositoriesPath()
} }
// OPTIONS // OPTIONS
@ -308,21 +317,21 @@ func WithDNSProvider(provider DNSProvider) Option {
} }
} }
// WithRepositories sets the location to use for extensible template repositories. // WithRepositories sets the location to use for extensible template
// Extensible template repositories are additional templates that exist on disk and are // repositories. Extensible template repositories are additional templates
// not built into the binary. // that exist on disk and are not built into the binary.
func WithRepositories(path string) Option { func WithRepositories(path string) Option {
return func(c *Client) { return func(c *Client) {
c.Repositories().SetPath(path) c.repositoriesPath = path
} }
} }
// WithRepository sets a specific URL to a Git repository from which to pull templates. // WithRepository sets a specific URL to a Git repository from which to pull
// This setting's existence precldes the use of either the inbuilt templates or any // templates. This setting's existence precldes the use of either the inbuilt
// repositories from the extensible repositories path. // templates or any repositories from the extensible repositories path.
func WithRepository(uri string) Option { func WithRepository(uri string) Option {
return func(c *Client) { return func(c *Client) {
c.Repositories().SetRemote(uri) c.repositoriesURI = uri
} }
} }
@ -784,3 +793,14 @@ func (p *NoopProgressListener) Increment(m string) {}
func (p *NoopProgressListener) Complete(m string) {} func (p *NoopProgressListener) Complete(m string) {}
func (p *NoopProgressListener) Stopping() {} func (p *NoopProgressListener) Stopping() {}
func (p *NoopProgressListener) Done() {} func (p *NoopProgressListener) Done() {}
// mkdir attempts to mkdir, writing any errors to stderr.
func mkdir(path string) {
// Since it is expected that the code elsewhere never assume directories
// exist (doing so is a racing condition), it is valid to simply
// handle errors at this level.
if err := os.MkdirAll(path, 0700); err != nil {
fmt.Fprintf(os.Stderr, "Error creating '%v': %v", path, err)
debug.PrintStack()
}
}

View File

@ -161,7 +161,6 @@ func TestRemoteRepositories(t *testing.T) {
client := fn.New( client := fn.New(
fn.WithRegistry(DefaultRegistry), fn.WithRegistry(DefaultRegistry),
fn.WithRepository("https://github.com/boson-project/test-templates"), fn.WithRepository("https://github.com/boson-project/test-templates"),
fn.WithRepositories("testdata/repositories"),
) )
err := client.Create(fn.Function{ err := client.Create(fn.Function{
Root: ".", Root: ".",

View File

@ -246,7 +246,7 @@ func newCreateConfig(args []string, clientFn createClientFn) (cfg createConfig,
// it is still available as an environment variable. // it is still available as an environment variable.
repositories = os.Getenv("FUNC_REPOSITORIES") repositories = os.Getenv("FUNC_REPOSITORIES")
if repositories == "" { // if no env var provided if repositories == "" { // if no env var provided
repositories = fn.RepositoriesPath() // use ~/.config/func/repositories repositories = fn.New().RepositoriesPath() // use ~/.config/func/repositories
} }
// Config is the final default values based off the execution context. // Config is the final default values based off the execution context.

View File

@ -90,10 +90,6 @@ func NewCredentialsProvider(
} }
} }
// Creating an instance of the fn.Client ensures that the config path
// exists:
_ = fn.New()
authFilePath := filepath.Join(fn.ConfigPath(), "auth.json") authFilePath := filepath.Join(fn.ConfigPath(), "auth.json")
sys := &containersTypes.SystemContext{ sys := &containersTypes.SystemContext{
AuthFilePath: authFilePath, AuthFilePath: authFilePath,

View File

@ -31,9 +31,16 @@ type Repositories struct {
path string path string
// Optional uri of a single repo to use in leau of embedded and extensible. // Optional uri of a single repo to use in leau of embedded and extensible.
// Enables single-repository mode. This replaces the default embedded repo
// and extended repositories. This is an important mode for both diskless
// (config-less) operation, such as security-restrited environments, and for
// running as a library in which case environmental settings should be
// ignored in favor of a more functional approach in which only inputs affect
// outputs.
remote string remote string
// backreference to the client enabling full api access for the repo manager // backreference to the client enabling this repositorires manager to
// have full API access.
client *Client client *Client
} }
@ -42,32 +49,17 @@ type Repositories struct {
// full client API during implementations. // full client API during implementations.
func newRepositories(client *Client) *Repositories { func newRepositories(client *Client) *Repositories {
return &Repositories{ return &Repositories{
path: DefaultRepositoriesPath,
client: client, client: client,
path: client.repositoriesPath,
remote: client.repositoriesURI,
} }
} }
// SetPath to repositories under management.
func (r *Repositories) SetPath(path string) {
r.path = path
}
// Path returns the currently active repositories path under management. // Path returns the currently active repositories path under management.
func (r *Repositories) Path() string { func (r *Repositories) Path() string {
return r.path return r.path
} }
// SetRemote enables single-repository mode.
// Enables single-repository mode. This replaces the default embedded repo
// and extended repositories. This is an important mode for both diskless
// (config-less) operation, such as security-restrited environments, and for
// running as a library in which case environmental settings should be
// ignored in favor of a more functional approach in which only inputs affect
// outputs.
func (r *Repositories) SetRemote(uri string) {
r.remote = uri
}
// List all repositories the current configuration of the repo manager has // List all repositories the current configuration of the repo manager has
// defined. // defined.
func (r *Repositories) List() ([]string, error) { func (r *Repositories) List() ([]string, error) {